github.com/lalkh/containerd@v1.4.3/metadata/db_test.go (about) 1 /* 2 Copyright The containerd Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package metadata 18 19 import ( 20 "context" 21 "encoding/binary" 22 "fmt" 23 "io" 24 "io/ioutil" 25 "math/rand" 26 "os" 27 "path/filepath" 28 "runtime/pprof" 29 "strings" 30 "testing" 31 "time" 32 33 "github.com/containerd/containerd/containers" 34 "github.com/containerd/containerd/content" 35 "github.com/containerd/containerd/content/local" 36 "github.com/containerd/containerd/errdefs" 37 "github.com/containerd/containerd/gc" 38 "github.com/containerd/containerd/images" 39 "github.com/containerd/containerd/leases" 40 "github.com/containerd/containerd/log/logtest" 41 "github.com/containerd/containerd/namespaces" 42 "github.com/containerd/containerd/snapshots" 43 "github.com/containerd/containerd/snapshots/native" 44 "github.com/gogo/protobuf/types" 45 digest "github.com/opencontainers/go-digest" 46 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 47 "github.com/pkg/errors" 48 bolt "go.etcd.io/bbolt" 49 ) 50 51 type testOptions struct { 52 extraSnapshots map[string]func(string) (snapshots.Snapshotter, error) 53 } 54 55 type testOpt func(*testOptions) 56 57 func withSnapshotter(name string, fn func(string) (snapshots.Snapshotter, error)) testOpt { 58 return func(to *testOptions) { 59 if to.extraSnapshots == nil { 60 to.extraSnapshots = map[string]func(string) (snapshots.Snapshotter, error){} 61 } 62 to.extraSnapshots[name] = fn 63 } 64 } 65 66 func testDB(t *testing.T, opt ...testOpt) (context.Context, *DB, func()) { 67 ctx, cancel := context.WithCancel(context.Background()) 68 ctx = namespaces.WithNamespace(ctx, "testing") 69 ctx = logtest.WithT(ctx, t) 70 71 var topts testOptions 72 73 for _, o := range opt { 74 o(&topts) 75 } 76 77 dirname, err := ioutil.TempDir("", strings.Replace(t.Name(), "/", "_", -1)+"-") 78 if err != nil { 79 t.Fatal(err) 80 } 81 82 snapshotter, err := native.NewSnapshotter(filepath.Join(dirname, "native")) 83 if err != nil { 84 t.Fatal(err) 85 } 86 87 snapshotters := map[string]snapshots.Snapshotter{ 88 "native": snapshotter, 89 } 90 91 for name, fn := range topts.extraSnapshots { 92 snapshotter, err := fn(filepath.Join(dirname, name)) 93 if err != nil { 94 t.Fatal(err) 95 } 96 snapshotters[name] = snapshotter 97 } 98 99 cs, err := local.NewStore(filepath.Join(dirname, "content")) 100 if err != nil { 101 t.Fatal(err) 102 } 103 104 bdb, err := bolt.Open(filepath.Join(dirname, "metadata.db"), 0644, nil) 105 if err != nil { 106 t.Fatal(err) 107 } 108 109 db := NewDB(bdb, cs, snapshotters) 110 if err := db.Init(ctx); err != nil { 111 t.Fatal(err) 112 } 113 114 return ctx, db, func() { 115 bdb.Close() 116 if err := os.RemoveAll(dirname); err != nil { 117 t.Log("failed removing temp dir", err) 118 } 119 cancel() 120 } 121 } 122 123 func TestInit(t *testing.T) { 124 ctx, db, cancel := testEnv(t) 125 defer cancel() 126 127 if err := NewDB(db, nil, nil).Init(ctx); err != nil { 128 t.Fatal(err) 129 } 130 131 version, err := readDBVersion(db, bucketKeyVersion) 132 if err != nil { 133 t.Fatal(err) 134 } 135 if version != dbVersion { 136 t.Fatalf("Unexpected version %d, expected %d", version, dbVersion) 137 } 138 } 139 140 func TestMigrations(t *testing.T) { 141 testRefs := []struct { 142 ref string 143 bref string 144 }{ 145 { 146 ref: "k1", 147 bref: "bk1", 148 }, 149 { 150 ref: strings.Repeat("longerkey", 30), // 270 characters 151 bref: "short", 152 }, 153 { 154 ref: "short", 155 bref: strings.Repeat("longerkey", 30), // 270 characters 156 }, 157 { 158 ref: "emptykey", 159 bref: "", 160 }, 161 } 162 migrationTests := []struct { 163 name string 164 init func(*bolt.Tx) error 165 check func(*bolt.Tx) error 166 }{ 167 { 168 name: "ChildrenKey", 169 init: func(tx *bolt.Tx) error { 170 bkt, err := createSnapshotterBucket(tx, "testing", "testing") 171 if err != nil { 172 return err 173 } 174 175 snapshots := []struct { 176 key string 177 parent string 178 }{ 179 { 180 key: "k1", 181 parent: "", 182 }, 183 { 184 key: "k2", 185 parent: "k1", 186 }, 187 { 188 key: "k2a", 189 parent: "k1", 190 }, 191 { 192 key: "a1", 193 parent: "k2", 194 }, 195 } 196 197 for _, s := range snapshots { 198 sbkt, err := bkt.CreateBucket([]byte(s.key)) 199 if err != nil { 200 return err 201 } 202 if err := sbkt.Put(bucketKeyParent, []byte(s.parent)); err != nil { 203 return err 204 } 205 } 206 207 return nil 208 }, 209 check: func(tx *bolt.Tx) error { 210 bkt := getSnapshotterBucket(tx, "testing", "testing") 211 if bkt == nil { 212 return errors.Wrap(errdefs.ErrNotFound, "snapshots bucket not found") 213 } 214 snapshots := []struct { 215 key string 216 children []string 217 }{ 218 { 219 key: "k1", 220 children: []string{"k2", "k2a"}, 221 }, 222 { 223 key: "k2", 224 children: []string{"a1"}, 225 }, 226 { 227 key: "k2a", 228 children: []string{}, 229 }, 230 { 231 key: "a1", 232 children: []string{}, 233 }, 234 } 235 236 for _, s := range snapshots { 237 sbkt := bkt.Bucket([]byte(s.key)) 238 if sbkt == nil { 239 return errors.Wrap(errdefs.ErrNotFound, "key does not exist") 240 } 241 242 cbkt := sbkt.Bucket(bucketKeyChildren) 243 var cn int 244 if cbkt != nil { 245 cn = cbkt.Stats().KeyN 246 } 247 248 if cn != len(s.children) { 249 return errors.Errorf("unexpected number of children %d, expected %d", cn, len(s.children)) 250 } 251 252 for _, ch := range s.children { 253 if v := cbkt.Get([]byte(ch)); v == nil { 254 return errors.Errorf("missing child record for %s", ch) 255 } 256 } 257 } 258 259 return nil 260 }, 261 }, 262 { 263 name: "IngestUpdate", 264 init: func(tx *bolt.Tx) error { 265 bkt, err := createBucketIfNotExists(tx, bucketKeyVersion, []byte("testing"), bucketKeyObjectContent, deprecatedBucketKeyObjectIngest) 266 if err != nil { 267 return err 268 } 269 270 for _, s := range testRefs { 271 if err := bkt.Put([]byte(s.ref), []byte(s.bref)); err != nil { 272 return err 273 } 274 } 275 276 return nil 277 }, 278 check: func(tx *bolt.Tx) error { 279 bkt := getIngestsBucket(tx, "testing") 280 if bkt == nil { 281 return errors.Wrap(errdefs.ErrNotFound, "ingests bucket not found") 282 } 283 284 for _, s := range testRefs { 285 sbkt := bkt.Bucket([]byte(s.ref)) 286 if sbkt == nil { 287 return errors.Wrap(errdefs.ErrNotFound, "ref does not exist") 288 } 289 290 bref := string(sbkt.Get(bucketKeyRef)) 291 if bref != s.bref { 292 return errors.Errorf("unexpected reference key %q, expected %q", bref, s.bref) 293 } 294 } 295 296 dbkt := getBucket(tx, bucketKeyVersion, []byte("testing"), bucketKeyObjectContent, deprecatedBucketKeyObjectIngest) 297 if dbkt != nil { 298 return errors.New("deprecated ingest bucket still exists") 299 } 300 301 return nil 302 }, 303 }, 304 305 { 306 name: "NoOp", 307 init: func(tx *bolt.Tx) error { 308 return nil 309 }, 310 check: func(tx *bolt.Tx) error { 311 return nil 312 }, 313 }, 314 } 315 316 if len(migrationTests) != len(migrations) { 317 t.Fatal("Each migration must have a test case") 318 } 319 320 for i, mt := range migrationTests { 321 t.Run(mt.name, runMigrationTest(i, mt.init, mt.check)) 322 } 323 } 324 325 func runMigrationTest(i int, init, check func(*bolt.Tx) error) func(t *testing.T) { 326 return func(t *testing.T) { 327 _, db, cancel := testEnv(t) 328 defer cancel() 329 330 if err := db.Update(init); err != nil { 331 t.Fatal(err) 332 } 333 334 if err := db.Update(migrations[i].migrate); err != nil { 335 t.Fatal(err) 336 } 337 338 if err := db.View(check); err != nil { 339 t.Fatal(err) 340 } 341 } 342 } 343 344 func readDBVersion(db *bolt.DB, schema []byte) (int, error) { 345 var version int 346 if err := db.View(func(tx *bolt.Tx) error { 347 bkt := tx.Bucket(schema) 348 if bkt == nil { 349 return errors.Wrap(errdefs.ErrNotFound, "no version bucket") 350 } 351 vb := bkt.Get(bucketKeyDBVersion) 352 if vb == nil { 353 return errors.Wrap(errdefs.ErrNotFound, "no version value") 354 } 355 v, _ := binary.Varint(vb) 356 version = int(v) 357 return nil 358 }); err != nil { 359 return 0, err 360 } 361 return version, nil 362 } 363 364 func TestMetadataCollector(t *testing.T) { 365 mdb, cs, sn, cleanup := newStores(t) 366 defer cleanup() 367 368 var ( 369 ctx = logtest.WithT(context.Background(), t) 370 371 objects = []object{ 372 blob(bytesFor(1), true), 373 blob(bytesFor(2), false), 374 blob(bytesFor(3), true), 375 blob(bytesFor(4), false, "containerd.io/gc.root", time.Now().String()), 376 newSnapshot("1", "", false, false), 377 newSnapshot("2", "1", false, false), 378 newSnapshot("3", "2", false, false), 379 newSnapshot("4", "3", false, false), 380 newSnapshot("5", "3", false, true), 381 container("1", "4"), 382 image("image-1", digestFor(2)), 383 384 // Test lease preservation 385 blob(bytesFor(5), false, "containerd.io/gc.ref.content.0", digestFor(6).String()), 386 blob(bytesFor(6), false), 387 blob(bytesFor(7), false), 388 newSnapshot("6", "", false, false, "containerd.io/gc.ref.content.0", digestFor(7).String()), 389 lease("lease-1", []leases.Resource{ 390 { 391 ID: digestFor(5).String(), 392 Type: "content", 393 }, 394 { 395 ID: "6", 396 Type: "snapshots/native", 397 }, 398 }, false), 399 400 // Test flat lease 401 blob(bytesFor(8), false, "containerd.io/gc.ref.content.0", digestFor(9).String()), 402 blob(bytesFor(9), true), 403 blob(bytesFor(10), true), 404 newSnapshot("7", "", false, false, "containerd.io/gc.ref.content.0", digestFor(10).String()), 405 newSnapshot("8", "7", false, false), 406 newSnapshot("9", "8", false, false), 407 lease("lease-2", []leases.Resource{ 408 { 409 ID: digestFor(8).String(), 410 Type: "content", 411 }, 412 { 413 ID: "9", 414 Type: "snapshots/native", 415 }, 416 }, false, "containerd.io/gc.flat", time.Now().String()), 417 } 418 remaining []gc.Node 419 ) 420 421 if err := mdb.Update(func(tx *bolt.Tx) error { 422 for _, obj := range objects { 423 node, err := create(obj, tx, mdb, cs, sn) 424 if err != nil { 425 return err 426 } 427 if node != nil { 428 remaining = append(remaining, *node) 429 } 430 } 431 return nil 432 }); err != nil { 433 t.Fatalf("Creation failed: %+v", err) 434 } 435 436 if _, err := mdb.GarbageCollect(ctx); err != nil { 437 t.Fatal(err) 438 } 439 440 var actual []gc.Node 441 442 if err := mdb.View(func(tx *bolt.Tx) error { 443 scanFn := func(ctx context.Context, node gc.Node) error { 444 actual = append(actual, node) 445 return nil 446 } 447 return scanAll(ctx, tx, scanFn) 448 }); err != nil { 449 t.Fatal(err) 450 } 451 452 checkNodesEqual(t, actual, remaining) 453 } 454 455 func BenchmarkGarbageCollect(b *testing.B) { 456 b.Run("10-Sets", benchmarkTrigger(10)) 457 b.Run("100-Sets", benchmarkTrigger(100)) 458 b.Run("1000-Sets", benchmarkTrigger(1000)) 459 b.Run("10000-Sets", benchmarkTrigger(10000)) 460 } 461 462 func benchmarkTrigger(n int) func(b *testing.B) { 463 return func(b *testing.B) { 464 mdb, cs, sn, cleanup := newStores(b) 465 defer cleanup() 466 467 objects := []object{} 468 469 // TODO: Allow max to be configurable 470 for i := 0; i < n; i++ { 471 objects = append(objects, 472 blob(bytesFor(int64(i)), false), 473 image(fmt.Sprintf("image-%d", i), digestFor(int64(i))), 474 ) 475 lastSnapshot := 6 476 for j := 0; j <= lastSnapshot; j++ { 477 var parent string 478 key := fmt.Sprintf("snapshot-%d-%d", i, j) 479 if j > 0 { 480 parent = fmt.Sprintf("snapshot-%d-%d", i, j-1) 481 } 482 objects = append(objects, newSnapshot(key, parent, false, false)) 483 } 484 objects = append(objects, container(fmt.Sprintf("container-%d", i), fmt.Sprintf("snapshot-%d-%d", i, lastSnapshot))) 485 486 } 487 488 // TODO: Create set of objects for removal 489 490 var ( 491 ctx = context.Background() 492 493 remaining []gc.Node 494 ) 495 496 if err := mdb.Update(func(tx *bolt.Tx) error { 497 for _, obj := range objects { 498 node, err := create(obj, tx, mdb, cs, sn) 499 if err != nil { 500 return err 501 } 502 if node != nil { 503 remaining = append(remaining, *node) 504 } 505 } 506 return nil 507 }); err != nil { 508 b.Fatalf("Creation failed: %+v", err) 509 } 510 511 // TODO: reset benchmark 512 b.ResetTimer() 513 //b.StopTimer() 514 515 labels := pprof.Labels("worker", "trigger") 516 pprof.Do(ctx, labels, func(ctx context.Context) { 517 for i := 0; i < b.N; i++ { 518 519 // TODO: Add removal objects 520 521 //b.StartTimer() 522 523 if _, err := mdb.GarbageCollect(ctx); err != nil { 524 b.Fatal(err) 525 } 526 527 //b.StopTimer() 528 529 //var actual []gc.Node 530 531 //if err := db.View(func(tx *bolt.Tx) error { 532 // nodeC := make(chan gc.Node) 533 // var scanErr error 534 // go func() { 535 // defer close(nodeC) 536 // scanErr = scanAll(ctx, tx, nodeC) 537 // }() 538 // for node := range nodeC { 539 // actual = append(actual, node) 540 // } 541 // return scanErr 542 //}); err != nil { 543 // t.Fatal(err) 544 //} 545 546 //checkNodesEqual(t, actual, remaining) 547 } 548 }) 549 } 550 } 551 552 func bytesFor(i int64) []byte { 553 r := rand.New(rand.NewSource(i)) 554 var b [256]byte 555 _, err := r.Read(b[:]) 556 if err != nil { 557 panic(err) 558 } 559 return b[:] 560 } 561 562 func digestFor(i int64) digest.Digest { 563 r := rand.New(rand.NewSource(i)) 564 dgstr := digest.SHA256.Digester() 565 _, err := io.Copy(dgstr.Hash(), io.LimitReader(r, 256)) 566 if err != nil { 567 panic(err) 568 } 569 return dgstr.Digest() 570 } 571 572 type object struct { 573 data interface{} 574 removed bool 575 labels map[string]string 576 } 577 578 func create(obj object, tx *bolt.Tx, db *DB, cs content.Store, sn snapshots.Snapshotter) (*gc.Node, error) { 579 var ( 580 node *gc.Node 581 namespace = "test" 582 ctx = WithTransactionContext(namespaces.WithNamespace(context.Background(), namespace), tx) 583 ) 584 585 switch v := obj.data.(type) { 586 case testContent: 587 expected := digest.FromBytes(v.data) 588 w, err := cs.Writer(ctx, 589 content.WithRef("test-ref"), 590 content.WithDescriptor(ocispec.Descriptor{Size: int64(len(v.data)), Digest: expected})) 591 if err != nil { 592 return nil, errors.Wrap(err, "failed to create writer") 593 } 594 if _, err := w.Write(v.data); err != nil { 595 return nil, errors.Wrap(err, "write blob failed") 596 } 597 if err := w.Commit(ctx, int64(len(v.data)), expected, content.WithLabels(obj.labels)); err != nil { 598 return nil, errors.Wrap(err, "failed to commit blob") 599 } 600 if !obj.removed { 601 node = &gc.Node{ 602 Type: ResourceContent, 603 Namespace: namespace, 604 Key: expected.String(), 605 } 606 } 607 case testSnapshot: 608 if v.active { 609 _, err := sn.Prepare(ctx, v.key, v.parent, snapshots.WithLabels(obj.labels)) 610 if err != nil { 611 return nil, err 612 } 613 } else { 614 akey := fmt.Sprintf("%s-active", v.key) 615 _, err := sn.Prepare(ctx, akey, v.parent) 616 if err != nil { 617 return nil, err 618 } 619 if err := sn.Commit(ctx, v.key, akey, snapshots.WithLabels(obj.labels)); err != nil { 620 return nil, err 621 } 622 } 623 if !obj.removed { 624 node = &gc.Node{ 625 Type: ResourceSnapshot, 626 Namespace: namespace, 627 Key: fmt.Sprintf("native/%s", v.key), 628 } 629 } 630 case testImage: 631 image := images.Image{ 632 Name: v.name, 633 Target: v.target, 634 Labels: obj.labels, 635 } 636 637 _, err := NewImageStore(db).Create(ctx, image) 638 if err != nil { 639 return nil, errors.Wrap(err, "failed to create image") 640 } 641 case testContainer: 642 container := containers.Container{ 643 ID: v.id, 644 SnapshotKey: v.snapshot, 645 Snapshotter: "native", 646 Labels: obj.labels, 647 648 Runtime: containers.RuntimeInfo{ 649 Name: "testruntime", 650 }, 651 Spec: &types.Any{}, 652 } 653 _, err := NewContainerStore(db).Create(ctx, container) 654 if err != nil { 655 return nil, err 656 } 657 case testLease: 658 lm := NewLeaseManager(db) 659 660 l, err := lm.Create(ctx, leases.WithID(v.id), leases.WithLabels(obj.labels)) 661 if err != nil { 662 return nil, err 663 } 664 665 for _, ref := range v.refs { 666 if err := lm.AddResource(ctx, l, ref); err != nil { 667 return nil, err 668 } 669 } 670 671 if !obj.removed { 672 node = &gc.Node{ 673 Type: ResourceLease, 674 Namespace: namespace, 675 Key: v.id, 676 } 677 } 678 } 679 680 return node, nil 681 } 682 683 func blob(b []byte, r bool, l ...string) object { 684 return object{ 685 data: testContent{ 686 data: b, 687 }, 688 removed: r, 689 labels: labelmap(l...), 690 } 691 } 692 693 func image(n string, d digest.Digest, l ...string) object { 694 return object{ 695 data: testImage{ 696 name: n, 697 target: ocispec.Descriptor{ 698 MediaType: "irrelevant", 699 Digest: d, 700 Size: 256, 701 }, 702 }, 703 removed: false, 704 labels: labelmap(l...), 705 } 706 } 707 708 func newSnapshot(key, parent string, active, r bool, l ...string) object { 709 return object{ 710 data: testSnapshot{ 711 key: key, 712 parent: parent, 713 active: active, 714 }, 715 removed: r, 716 labels: labelmap(l...), 717 } 718 } 719 720 func container(id, s string, l ...string) object { 721 return object{ 722 data: testContainer{ 723 id: id, 724 snapshot: s, 725 }, 726 removed: false, 727 labels: labelmap(l...), 728 } 729 } 730 731 func lease(id string, refs []leases.Resource, r bool, l ...string) object { 732 return object{ 733 data: testLease{ 734 id: id, 735 refs: refs, 736 }, 737 removed: r, 738 labels: labelmap(l...), 739 } 740 } 741 742 type testContent struct { 743 data []byte 744 } 745 746 type testSnapshot struct { 747 key string 748 parent string 749 active bool 750 } 751 752 type testImage struct { 753 name string 754 target ocispec.Descriptor 755 } 756 757 type testContainer struct { 758 id string 759 snapshot string 760 } 761 762 type testLease struct { 763 id string 764 refs []leases.Resource 765 } 766 767 func newStores(t testing.TB) (*DB, content.Store, snapshots.Snapshotter, func()) { 768 td, err := ioutil.TempDir("", "gc-test-") 769 if err != nil { 770 t.Fatal(err) 771 } 772 db, err := bolt.Open(filepath.Join(td, "meta.db"), 0644, nil) 773 if err != nil { 774 t.Fatal(err) 775 } 776 777 nsn, err := native.NewSnapshotter(filepath.Join(td, "snapshots")) 778 if err != nil { 779 t.Fatal(err) 780 } 781 782 lcs, err := local.NewStore(filepath.Join(td, "content")) 783 if err != nil { 784 t.Fatal(err) 785 } 786 787 mdb := NewDB(db, lcs, map[string]snapshots.Snapshotter{"native": nsn}) 788 789 return mdb, mdb.ContentStore(), mdb.Snapshotter("native"), func() { 790 os.RemoveAll(td) 791 } 792 }