diff --git a/snapshot/btrfs/btrfs.go b/snapshot/btrfs/btrfs.go index bee93ac..ab0904d 100644 --- a/snapshot/btrfs/btrfs.go +++ b/snapshot/btrfs/btrfs.go @@ -2,16 +2,17 @@ package btrfs import ( "context" - "crypto/sha256" "fmt" - "io/ioutil" "os" "path/filepath" - "strings" + "github.com/Sirupsen/logrus" "github.com/docker/containerd" + "github.com/docker/containerd/log" "github.com/docker/containerd/plugin" "github.com/docker/containerd/snapshot" + "github.com/docker/containerd/snapshot/storage" + "github.com/docker/containerd/snapshot/storage/boltdb" "github.com/pkg/errors" "github.com/stevvooe/go-btrfs" ) @@ -25,11 +26,19 @@ func init() { Type: plugin.SnapshotPlugin, Config: &btrfsConfig{}, Init: func(ic *plugin.InitContext) (interface{}, error) { + root := filepath.Join(ic.Root, "snapshot", "btrfs") conf := ic.Config.(*btrfsConfig) if conf.Device == "" { + // TODO: check device for root return nil, errors.Errorf("btrfs requires \"device\" configuration") } - return NewSnapshotter(conf.Device, filepath.Join(ic.Root, "snapshot", "btrfs")) + + ms, err := boltdb.NewMetaStore(ic.Context, filepath.Join(root, "metadata.db")) + if err != nil { + return nil, err + } + + return NewSnapshotter(conf.Device, root, ms) }, }) } @@ -37,30 +46,29 @@ func init() { type Snapshotter struct { device string // maybe we can resolve it with path? root string // root provides paths for internal storage. + ms storage.MetaStore } -func NewSnapshotter(device, root string) (snapshot.Snapshotter, error) { +func NewSnapshotter(device, root string, ms storage.MetaStore) (snapshot.Snapshotter, error) { var ( active = filepath.Join(root, "active") snapshots = filepath.Join(root, "snapshots") - parents = filepath.Join(root, "parents") - index = filepath.Join(root, "index") - names = filepath.Join(root, "names") ) for _, path := range []string{ active, snapshots, - parents, - index, - names, } { if err := os.MkdirAll(path, 0755); err != nil { return nil, err } } - return &Snapshotter{device: device, root: root}, nil + return &Snapshotter{ + device: device, + root: root, + ms: ms, + }, nil } // Stat returns the info for an active or committed snapshot by name or @@ -69,123 +77,71 @@ func NewSnapshotter(device, root string) (snapshot.Snapshotter, error) { // Should be used for parent resolution, existence checks and to discern // the kind of snapshot. func (b *Snapshotter) Stat(ctx context.Context, key string) (snapshot.Info, error) { - // resolve the snapshot out of the index. - target, err := os.Readlink(filepath.Join(b.root, "index", hash(key))) + ctx, t, err := b.ms.TransactionContext(ctx, false) if err != nil { - if !os.IsNotExist(err) { - return snapshot.Info{}, err - } - - return snapshot.Info{}, errors.Errorf("snapshot %v not found", key) + return snapshot.Info{}, err } - - return b.stat(target) + defer t.Rollback() + return b.ms.Stat(ctx, key) } -func (b *Snapshotter) stat(target string) (snapshot.Info, error) { - var ( - parents = filepath.Join(b.root, "parents") - names = filepath.Join(b.root, "names") - namep = filepath.Join(names, filepath.Base(target)) - parentlink = filepath.Join(parents, filepath.Base(target)) - ) - - // grab information about the subvolume - info, err := btrfs.SubvolInfo(target) +// Walk the committed snapshots. +func (b *Snapshotter) Walk(ctx context.Context, fn func(context.Context, snapshot.Info) error) error { + ctx, t, err := b.ms.TransactionContext(ctx, false) if err != nil { - return snapshot.Info{}, err + return err } - - // read the name out of the names! - nameraw, err := ioutil.ReadFile(namep) - if err != nil { - return snapshot.Info{}, err - } - - // resolve the parents path. - parentp, err := os.Readlink(parentlink) - if err != nil { - if !os.IsNotExist(err) { - return snapshot.Info{}, err - } - - // no parent! - } - - var parent string - if parentp != "" { - // okay, grab the basename of the parent and look up its name! - parentnamep := filepath.Join(names, filepath.Base(parentp)) - - p, err := ioutil.ReadFile(parentnamep) - if err != nil { - return snapshot.Info{}, err - } - - parent = string(p) - } - - kind := snapshot.KindCommitted - if strings.HasPrefix(target, filepath.Join(b.root, "active")) { - kind = snapshot.KindActive - } - - return snapshot.Info{ - Name: string(nameraw), - Parent: parent, - Readonly: info.Readonly, - Kind: kind, - }, nil - + defer t.Rollback() + return b.ms.Walk(ctx, fn) } func (b *Snapshotter) Prepare(ctx context.Context, key, parent string) ([]containerd.Mount, error) { - return b.makeActive(key, parent, false) + return b.makeActive(ctx, key, parent, false) } func (b *Snapshotter) View(ctx context.Context, key, parent string) ([]containerd.Mount, error) { - return b.makeActive(key, parent, true) + return b.makeActive(ctx, key, parent, true) } -func (b *Snapshotter) makeActive(key, parent string, readonly bool) ([]containerd.Mount, error) { - var ( - active = filepath.Join(b.root, "active") - snapshots = filepath.Join(b.root, "snapshots") - parents = filepath.Join(b.root, "parents") - index = filepath.Join(b.root, "index") - names = filepath.Join(b.root, "names") - keyh = hash(key) - parenth = hash(parent) - target = filepath.Join(active, keyh) - namep = filepath.Join(names, keyh) - indexlink = filepath.Join(index, keyh) - parentlink = filepath.Join(parents, keyh) - parentp = filepath.Join(snapshots, parenth) // parent must be restricted to snaps - ) - - if parent == "" { - // create new subvolume - // btrfs subvolume create /dir - if err := btrfs.SubvolCreate(target); err != nil { - return nil, err - } - } else { - // btrfs subvolume snapshot /parent /subvol - if err := btrfs.SubvolSnapshot(target, parentp, readonly); err != nil { - return nil, err - } - - if err := os.Symlink(parentp, parentlink); err != nil { - return nil, err - } +func (b *Snapshotter) makeActive(ctx context.Context, key, parent string, readonly bool) ([]containerd.Mount, error) { + ctx, t, err := b.ms.TransactionContext(ctx, true) + if err != nil { + return nil, err } + defer func() { + if err != nil && t != nil { + if rerr := t.Rollback(); rerr != nil { + log.G(ctx).WithError(rerr).Warn("Failure rolling back transaction") + } + } + }() - // write in the name - if err := ioutil.WriteFile(namep, []byte(key), 0644); err != nil { + a, err := b.ms.CreateActive(ctx, key, parent, readonly) + if err != nil { return nil, err } - if err := os.Symlink(target, indexlink); err != nil { + target := filepath.Join(b.root, "active", a.ID) + + if len(a.ParentIDs) == 0 { + // create new subvolume + // btrfs subvolume create /dir + if err = btrfs.SubvolCreate(target); err != nil { + return nil, err + } + } else { + parentp := filepath.Join(b.root, "snapshots", a.ParentIDs[0]) + // btrfs subvolume snapshot /parent /subvol + if err = btrfs.SubvolSnapshot(target, parentp, a.Readonly); err != nil { + return nil, err + } + } + err = t.Commit() + t = nil + if err != nil { + if derr := btrfs.SubvolDelete(target); derr != nil { + log.G(ctx).WithError(derr).WithField("subvolume", target).Error("Failed to delete subvolume") + } return nil, err } @@ -218,82 +174,43 @@ func (b *Snapshotter) mounts(dir string) ([]containerd.Mount, error) { }, nil } -func (b *Snapshotter) Commit(ctx context.Context, name, key string) error { - var ( - active = filepath.Join(b.root, "active") - snapshots = filepath.Join(b.root, "snapshots") - index = filepath.Join(b.root, "index") - parents = filepath.Join(b.root, "parents") - names = filepath.Join(b.root, "names") - keyh = hash(key) - nameh = hash(name) - dir = filepath.Join(active, keyh) - target = filepath.Join(snapshots, nameh) - keynamep = filepath.Join(names, keyh) - namep = filepath.Join(names, nameh) - keyparentlink = filepath.Join(parents, keyh) - parentlink = filepath.Join(parents, nameh) - keyindexlink = filepath.Join(index, keyh) - indexlink = filepath.Join(index, nameh) - ) - - info, err := btrfs.SubvolInfo(dir) +func (b *Snapshotter) Commit(ctx context.Context, name, key string) (err error) { + ctx, t, err := b.ms.TransactionContext(ctx, true) if err != nil { return err } + defer func() { + if err != nil && t != nil { + if rerr := t.Rollback(); rerr != nil { + log.G(ctx).WithError(rerr).Warn("Failure rolling back transaction") + } + } + }() - if info.Readonly { - return fmt.Errorf("may not commit view snapshot %q", dir) - } - - // look up the parent information to make sure we have the right parent link. - parentp, err := os.Readlink(keyparentlink) + id, err := b.ms.Commit(ctx, key, name) if err != nil { - if !os.IsNotExist(err) { - return err + return errors.Wrap(err, "failed to commit") + } + + source := filepath.Join(b.root, "active", id) + target := filepath.Join(b.root, "snapshots", id) + + if err := btrfs.SubvolSnapshot(target, source, true); err != nil { + return err + } + + err = t.Commit() + t = nil + if err != nil { + if derr := btrfs.SubvolDelete(target); derr != nil { + log.G(ctx).WithError(derr).WithField("subvolume", target).Error("Failed to delete subvolume") } - - // we have no parent! - } - - if err := os.MkdirAll(snapshots, 0755); err != nil { return err } - if err := btrfs.SubvolSnapshot(target, dir, true); err != nil { - return err - } - - // remove the key name path as we no longer need it. - if err := os.Remove(keynamep); err != nil { - return err - } - - if err := os.Remove(keyindexlink); err != nil { - return err - } - - if err := ioutil.WriteFile(namep, []byte(name), 0755); err != nil { - return err - } - - if parentp != "" { - // move over the parent link into the commit name. - if err := os.Rename(keyparentlink, parentlink); err != nil { - return err - } - - // TODO(stevvooe): For this to not break horribly, we should really - // start taking a full lock. We are going to move all this metadata - // into common storage, so let's not fret over it for now. - } - - if err := os.Symlink(target, indexlink); err != nil { - return err - } - - if err := btrfs.SubvolDelete(dir); err != nil { - return errors.Wrapf(err, "delete subvol failed on %v", dir) + if derr := btrfs.SubvolDelete(source); derr != nil { + // Log as warning, only needed for cleanup, will not cause name collision + log.G(ctx).WithError(derr).WithField("subvolume", source).Warn("Failed to delete subvolume") } return nil @@ -304,55 +221,91 @@ func (b *Snapshotter) Commit(ctx context.Context, name, key string) error { // // This can be used to recover mounts after calling View or Prepare. func (b *Snapshotter) Mounts(ctx context.Context, key string) ([]containerd.Mount, error) { - dir := filepath.Join(b.root, "active", hash(key)) + ctx, t, err := b.ms.TransactionContext(ctx, false) + if err != nil { + return nil, err + } + a, err := b.ms.GetActive(ctx, key) + t.Rollback() + if err != nil { + return nil, errors.Wrap(err, "failed to get active snapshot") + } + dir := filepath.Join(b.root, "active", a.ID) return b.mounts(dir) } // Remove abandons the transaction identified by key. All resources // associated with the key will be removed. -func (b *Snapshotter) Remove(ctx context.Context, key string) error { - panic("not implemented") -} +func (b *Snapshotter) Remove(ctx context.Context, key string) (err error) { + var ( + source, removed string + readonly bool + ) -// Walk the committed snapshots. -func (b *Snapshotter) Walk(ctx context.Context, fn func(context.Context, snapshot.Info) error) error { - // TODO(stevvooe): Copy-pasted almost verbatim from overlay. Really need to - // unify the metadata for snapshot implementations. - root := filepath.Join(b.root, "index") - return filepath.Walk(root, func(path string, fi os.FileInfo, err error) error { - if err != nil { - return err - } - - if path == root { - return nil - } - - if fi.Mode()&os.ModeSymlink == 0 { - // only follow links - return filepath.SkipDir - } - - target, err := os.Readlink(path) - if err != nil { - if !os.IsNotExist(err) { - return err + ctx, t, err := b.ms.TransactionContext(ctx, true) + if err != nil { + return err + } + defer func() { + if err != nil && t != nil { + if rerr := t.Rollback(); rerr != nil { + log.G(ctx).WithError(rerr).Warn("Failure rolling back transaction") } } - si, err := b.stat(target) + if removed != "" { + if derr := btrfs.SubvolDelete(removed); derr != nil { + log.G(ctx).WithError(derr).WithField("subvolume", removed).Warn("Failed to delete subvolume") + } + } + }() + + id, k, err := b.ms.Remove(ctx, key) + if err != nil { + return errors.Wrap(err, "failed to remove snapshot") + } + + if k == snapshot.KindActive { + source = filepath.Join(b.root, "active", id) + + info, err := btrfs.SubvolInfo(source) if err != nil { + source = "" return err } - if err := fn(ctx, si); err != nil { - return err + readonly = info.Readonly + removed = filepath.Join(b.root, "active", "rm-"+id) + } else { + source = filepath.Join(b.root, "snapshots", id) + removed = filepath.Join(b.root, "snapshots", "rm-"+id) + readonly = true + } + + if err := btrfs.SubvolSnapshot(removed, source, readonly); err != nil { + removed = "" + return err + } + + if err := btrfs.SubvolDelete(source); err != nil { + return errors.Wrapf(err, "failed to remove snapshot %v", source) + } + + err = t.Commit() + t = nil + if err != nil { + // Attempt to restore source + if err1 := btrfs.SubvolSnapshot(source, removed, readonly); err1 != nil { + log.G(ctx).WithFields(logrus.Fields{ + logrus.ErrorKey: err1, + "subvolume": source, + "renamed": removed, + }).Error("Failed to restore subvolume from renamed") + // Keep removed to allow for manual restore + removed = "" } + return err + } - return nil - }) -} - -func hash(k string) string { - return fmt.Sprintf("%x", sha256.Sum224([]byte(k))) + return nil } diff --git a/snapshot/btrfs/btrfs_test.go b/snapshot/btrfs/btrfs_test.go index 3cb776c..9b90b7d 100644 --- a/snapshot/btrfs/btrfs_test.go +++ b/snapshot/btrfs/btrfs_test.go @@ -11,6 +11,7 @@ import ( "github.com/docker/containerd" "github.com/docker/containerd/snapshot" + "github.com/docker/containerd/snapshot/storage/boltdb" "github.com/docker/containerd/snapshot/testsuite" "github.com/docker/containerd/testutil" ) @@ -19,11 +20,14 @@ const ( mib = 1024 * 1024 ) -func TestBtrfs(t *testing.T) { - testutil.RequiresRoot(t) - testsuite.SnapshotterSuite(t, "Btrfs", func(root string) (snapshot.Snapshotter, func(), error) { +func boltSnapshotter(t *testing.T) func(context.Context, string) (snapshot.Snapshotter, func(), error) { + return func(ctx context.Context, root string) (snapshot.Snapshotter, func(), error) { device := setupBtrfsLoopbackDevice(t, root) - snapshotter, err := NewSnapshotter(device.deviceName, root) + store, err := boltdb.NewMetaStore(ctx, filepath.Join(root, "metadata.db")) + if err != nil { + return nil, nil, err + } + snapshotter, err := NewSnapshotter(device.deviceName, root, store) if err != nil { t.Fatal(err) } @@ -31,34 +35,39 @@ func TestBtrfs(t *testing.T) { return snapshotter, func() { device.remove(t) }, nil - }) + } +} + +func TestBtrfs(t *testing.T) { + testutil.RequiresRoot(t) + testsuite.SnapshotterSuite(t, "Btrfs", boltSnapshotter(t)) } func TestBtrfsMounts(t *testing.T) { testutil.RequiresRoot(t) - ctx := context.TODO() + ctx := context.Background() // create temporary directory for mount point mountPoint, err := ioutil.TempDir("", "containerd-btrfs-test") if err != nil { t.Fatal("could not create mount point for btrfs test", err) } + defer os.RemoveAll(mountPoint) t.Log("temporary mount point created", mountPoint) - device := setupBtrfsLoopbackDevice(t, mountPoint) - defer device.remove(t) - - root, err := ioutil.TempDir(device.mountPoint, "TestBtrfsPrepare-") + root, err := ioutil.TempDir(mountPoint, "TestBtrfsPrepare-") if err != nil { t.Fatal(err) } defer os.RemoveAll(root) - target := filepath.Join(root, "test") - b, err := NewSnapshotter(device.deviceName, root) + b, c, err := boltSnapshotter(t)(ctx, root) if err != nil { t.Fatal(err) } + defer c() + + target := filepath.Join(root, "test") mounts, err := b.Prepare(ctx, target, "") if err != nil { t.Fatal(err) diff --git a/snapshot/errors.go b/snapshot/errors.go new file mode 100644 index 0000000..0b6e517 --- /dev/null +++ b/snapshot/errors.go @@ -0,0 +1,44 @@ +package snapshot + +import "github.com/pkg/errors" + +var ( + // ErrSnapshotNotExist is returned when a snapshot cannot be found + ErrSnapshotNotExist = errors.New("snapshot does not exist") + + // ErrSnapshotExist is returned when an operation to create a snapshot + // encounters a snapshot with the same key + ErrSnapshotExist = errors.New("snapshot already exists") + + // ErrSnapshotNotActive is returned when a request which requires an + // active snapshot encounters a non-active snapshot. + ErrSnapshotNotActive = errors.New("snapshot is not active") + + // ErrSnapshotNotCommitted is returned when a request which requires a + // committed snapshot encounters a non-committed snapshot. + ErrSnapshotNotCommitted = errors.New("snapshot is not committed") +) + +// IsNotExist returns whether the error represents that a snapshot +// was not found. +func IsNotExist(err error) bool { + return errors.Cause(err) == ErrSnapshotNotExist +} + +// IsExist returns whether the error represents whether a snapshot +// already exists using a provided key. +func IsExist(err error) bool { + return errors.Cause(err) == ErrSnapshotExist +} + +// IsNotActive returns whether the error represents a request +// for a non active snapshot when an active snapshot is expected. +func IsNotActive(err error) bool { + return errors.Cause(err) == ErrSnapshotNotActive +} + +// IsNotCommitted returns whether the error represents a request +// for a non committed snapshot when a committed snapshot is expected. +func IsNotCommitted(err error) bool { + return errors.Cause(err) == ErrSnapshotNotCommitted +} diff --git a/snapshot/overlay/overlay.go b/snapshot/overlay/overlay.go index bc655e3..37c54fe 100644 --- a/snapshot/overlay/overlay.go +++ b/snapshot/overlay/overlay.go @@ -7,12 +7,13 @@ import ( "os" "path/filepath" "strings" - "sync" "github.com/docker/containerd" + "github.com/docker/containerd/log" "github.com/docker/containerd/plugin" "github.com/docker/containerd/snapshot" - digest "github.com/opencontainers/go-digest" + "github.com/docker/containerd/snapshot/storage" + "github.com/docker/containerd/snapshot/storage/boltdb" "github.com/pkg/errors" ) @@ -20,32 +21,40 @@ func init() { plugin.Register("snapshot-overlay", &plugin.Registration{ Type: plugin.SnapshotPlugin, Init: func(ic *plugin.InitContext) (interface{}, error) { - return NewSnapshotter(filepath.Join(ic.Root, "snapshot", "overlay")) + root := filepath.Join(ic.Root, "snapshot", "overlay") + ms, err := boltdb.NewMetaStore(ic.Context, filepath.Join(root, "metadata.db")) + if err != nil { + return nil, err + } + return NewSnapshotter(root, ms) }, }) } type Snapshotter struct { - root string - links *cache + root string + ms storage.MetaStore } -func NewSnapshotter(root string) (snapshot.Snapshotter, error) { +type activeSnapshot struct { + id string + name string + parentID interface{} + readonly bool +} + +func NewSnapshotter(root string, ms storage.MetaStore) (snapshot.Snapshotter, error) { if err := os.MkdirAll(root, 0700); err != nil { return nil, err } - for _, p := range []string{ - "committed", // committed snapshots - "active", // active snapshots - "index", // snapshots by hashed name - } { - if err := os.MkdirAll(filepath.Join(root, p), 0700); err != nil { - return nil, err - } + + if err := os.MkdirAll(filepath.Join(root, "snapshots"), 0700); err != nil { + return nil, err } + return &Snapshotter{ - root: root, - links: newCache(), + root: root, + ms: ms, }, nil } @@ -55,86 +64,20 @@ func NewSnapshotter(root string) (snapshot.Snapshotter, error) { // Should be used for parent resolution, existence checks and to discern // the kind of snapshot. func (o *Snapshotter) Stat(ctx context.Context, key string) (snapshot.Info, error) { - path, err := o.links.get(filepath.Join(o.root, "index", hash(key))) - if err != nil { - if !os.IsNotExist(err) { - return snapshot.Info{}, err - } - - return snapshot.Info{}, errors.Errorf("snapshot %v not found", key) - } - - // TODO(stevvooe): We don't confirm the name to avoid the lookup cost. - return o.stat(path) -} - -func (o *Snapshotter) stat(path string) (snapshot.Info, error) { - ppath, err := o.links.get(filepath.Join(path, "parent")) - if err != nil { - if !os.IsNotExist(err) { - return snapshot.Info{}, err - } - - // no parent - } - - kp, err := ioutil.ReadFile(filepath.Join(path, "name")) + ctx, t, err := o.ms.TransactionContext(ctx, false) if err != nil { return snapshot.Info{}, err } - - var parent string - if ppath != "" { - p, err := ioutil.ReadFile(filepath.Join(ppath, "name")) - if err != nil { - return snapshot.Info{}, err - } - parent = string(p) - } - - ro := true - kind := snapshot.KindCommitted - if strings.HasPrefix(path, filepath.Join(o.root, "active")) { - // TODO(stevvooe): Maybe there is a better way? - kind = snapshot.KindActive - - // TODO(stevvooe): We haven't introduced this to overlay yet. - // We'll add it when we add tests for it. - ro = false - } - - return snapshot.Info{ - Name: string(kp), - Parent: parent, - Kind: kind, - Readonly: ro, - }, nil + defer t.Rollback() + return o.ms.Stat(ctx, key) } func (o *Snapshotter) Prepare(ctx context.Context, key, parent string) ([]containerd.Mount, error) { - active, err := o.newActiveDir(key, false) - if err != nil { - return nil, err - } - if parent != "" { - if err := active.setParent(parent); err != nil { - return nil, err - } - } - return active.mounts(o.links) + return o.createActive(ctx, key, parent, false) } func (o *Snapshotter) View(ctx context.Context, key, parent string) ([]containerd.Mount, error) { - active, err := o.newActiveDir(key, true) - if err != nil { - return nil, err - } - if parent != "" { - if err := active.setParent(parent); err != nil { - return nil, err - } - } - return active.mounts(o.links) + return o.createActive(ctx, key, parent, true) } // Mounts returns the mounts for the transaction identified by key. Can be @@ -142,263 +85,208 @@ func (o *Snapshotter) View(ctx context.Context, key, parent string) ([]container // // This can be used to recover mounts after calling View or Prepare. func (o *Snapshotter) Mounts(ctx context.Context, key string) ([]containerd.Mount, error) { - active := o.getActive(key) - return active.mounts(o.links) + ctx, t, err := o.ms.TransactionContext(ctx, false) + if err != nil { + return nil, err + } + active, err := o.ms.GetActive(ctx, key) + t.Rollback() + if err != nil { + return nil, errors.Wrap(err, "failed to get active mount") + } + return o.mounts(active), nil } func (o *Snapshotter) Commit(ctx context.Context, name, key string) error { - active := o.getActive(key) - return active.commit(name, o.links) + ctx, t, err := o.ms.TransactionContext(ctx, true) + if err != nil { + return err + } + if _, err := o.ms.Commit(ctx, key, name); err != nil { + if rerr := t.Rollback(); rerr != nil { + log.G(ctx).WithError(rerr).Warn("Failure rolling back transaction") + } + return errors.Wrap(err, "failed to commit snapshot") + } + return t.Commit() } // Remove abandons the transaction identified by key. All resources // associated with the key will be removed. -func (o *Snapshotter) Remove(ctx context.Context, key string) error { - panic("not implemented") +func (o *Snapshotter) Remove(ctx context.Context, key string) (err error) { + ctx, t, err := o.ms.TransactionContext(ctx, true) + if err != nil { + return err + } + defer func() { + if err != nil && t != nil { + if rerr := t.Rollback(); rerr != nil { + log.G(ctx).WithError(rerr).Warn("Failure rolling back transaction") + } + } + }() + + id, _, err := o.ms.Remove(ctx, key) + if err != nil { + return errors.Wrap(err, "failed to remove") + } + + path := filepath.Join(o.root, "snapshots", id) + renamed := filepath.Join(o.root, "snapshots", "rm-"+id) + if err := os.Rename(path, renamed); err != nil { + return errors.Wrap(err, "failed to rename") + } + + err = t.Commit() + t = nil + if err != nil { + if err1 := os.Rename(renamed, path); err1 != nil { + // May cause inconsistent data on disk + log.G(ctx).WithError(err1).WithField("path", renamed).Errorf("Failed to rename after failed commit") + } + return errors.Wrap(err, "failed to commit") + } + if err := os.RemoveAll(renamed); err != nil { + // Must be cleaned up, any "rm-*" could be removed if no active transactions + log.G(ctx).WithError(err).WithField("path", renamed).Warnf("Failed to remove root filesystem") + } + + return nil } // Walk the committed snapshots. func (o *Snapshotter) Walk(ctx context.Context, fn func(context.Context, snapshot.Info) error) error { - root := filepath.Join(o.root, "index") - return filepath.Walk(root, func(path string, fi os.FileInfo, err error) error { - if err != nil { - return err - } - - if path == root { - return nil - } - - if fi.Mode()&os.ModeSymlink == 0 { - // only follow links - return filepath.SkipDir - } - - target, err := o.links.get(path) - if err != nil { - if !os.IsNotExist(err) { - return err - } - } - - si, err := o.stat(target) - if err != nil { - return err - } - - if err := fn(ctx, si); err != nil { - return err - } - - return nil - }) + ctx, t, err := o.ms.TransactionContext(ctx, false) + if err != nil { + return err + } + defer t.Rollback() + return o.ms.Walk(ctx, fn) } -func (o *Snapshotter) newActiveDir(key string, readonly bool) (*activeDir, error) { +func (o *Snapshotter) createActive(ctx context.Context, key, parent string, readonly bool) ([]containerd.Mount, error) { var ( - path = filepath.Join(o.root, "active", hash(key)) - name = filepath.Join(path, "name") - indexlink = filepath.Join(o.root, "index", hash(key)) + path string + snapshotDir = filepath.Join(o.root, "snapshots") ) - a := &activeDir{ - path: path, - committedDir: filepath.Join(o.root, "committed"), - indexlink: indexlink, + + td, err := ioutil.TempDir(snapshotDir, "new-") + if err != nil { + return nil, errors.Wrap(err, "failed to create temp dir") + } + defer func() { + if err != nil { + if td != "" { + if err1 := os.RemoveAll(td); err1 != nil { + err = errors.Wrapf(err, "remove failed: %v", err1) + } + } + if path != "" { + if err1 := os.RemoveAll(path); err1 != nil { + err = errors.Wrapf(err, "failed to remove path: %v", err1) + } + } + } + }() + + if err = os.MkdirAll(filepath.Join(td, "fs"), 0700); err != nil { + return nil, err } if !readonly { - for _, p := range []string{ - "work", - "fs", - } { - if err := os.MkdirAll(filepath.Join(path, p), 0700); err != nil { - a.delete() - return nil, err - } - } - } else { - if err := os.MkdirAll(filepath.Join(path, "fs"), 0700); err != nil { - a.delete() + if err = os.MkdirAll(filepath.Join(td, "work"), 0700); err != nil { return nil, err } } - if err := ioutil.WriteFile(name, []byte(key), 0644); err != nil { - a.delete() + ctx, t, err := o.ms.TransactionContext(ctx, true) + if err != nil { return nil, err } - // link from namespace - if err := os.Symlink(path, indexlink); err != nil { - a.delete() - return nil, err - } - - return a, nil -} - -func (o *Snapshotter) getActive(key string) *activeDir { - return &activeDir{ - path: filepath.Join(o.root, "active", hash(key)), - committedDir: filepath.Join(o.root, "committed"), - indexlink: filepath.Join(o.root, "index", hash(key)), - } -} - -func hash(k string) string { - return digest.FromString(k).Hex() -} - -type activeDir struct { - committedDir string - path string - indexlink string -} - -func (a *activeDir) delete() error { - return os.RemoveAll(a.path) -} - -func (a *activeDir) setParent(name string) error { - return os.Symlink(filepath.Join(a.committedDir, hash(name)), filepath.Join(a.path, "parent")) -} - -func (a *activeDir) commit(name string, c *cache) error { - if _, err := os.Stat(filepath.Join(a.path, "fs")); err != nil { - if os.IsNotExist(err) { - return errors.New("cannot commit view") + active, err := o.ms.CreateActive(ctx, key, parent, readonly) + if err != nil { + if rerr := t.Rollback(); rerr != nil { + log.G(ctx).WithError(rerr).Warn("Failure rolling back transaction") } - return err + return nil, errors.Wrap(err, "failed to create active") } - // TODO(stevvooe): This doesn't quite meet the current model. The new model - // is to copy all of this out and let the transaction continue. We don't - // really have tests for it yet, but this will be the spot to fix it. - // - // Nothing should be removed until remove is called on the active - // transaction. - if err := os.RemoveAll(filepath.Join(a.path, "work")); err != nil { - return err + path = filepath.Join(snapshotDir, active.ID) + if err := os.Rename(td, path); err != nil { + if rerr := t.Rollback(); rerr != nil { + log.G(ctx).WithError(rerr).Warn("Failure rolling back transaction") + } + return nil, errors.Wrap(err, "failed to rename") + } + td = "" + + if err := t.Commit(); err != nil { + return nil, errors.Wrap(err, "commit failed") } - if err := ioutil.WriteFile(filepath.Join(a.path, "name"), []byte(name), 0644); err != nil { - return err - } - - c.invalidate(a.path) // clears parent cache, since we end up moving. - c.invalidate(filepath.Join(a.path, "parent")) - c.invalidate(a.indexlink) - - committed := filepath.Join(a.committedDir, hash(name)) - if err := os.Rename(a.path, committed); err != nil { - return err - } - - if err := os.Remove(a.indexlink); err != nil { - return err - } - - indexlink := filepath.Join(filepath.Dir(a.indexlink), hash(name)) - return os.Symlink(committed, indexlink) + return o.mounts(active), nil } -func (a *activeDir) mounts(c *cache) ([]containerd.Mount, error) { - var ( - parents []string - err error - current = a.path - ) - for { - if current, err = c.get(filepath.Join(current, "parent")); err != nil { - if os.IsNotExist(err) { - break - } - - return nil, err - } - parents = append(parents, filepath.Join(current, "fs")) - } - if len(parents) == 0 { +func (o *Snapshotter) mounts(active storage.Active) []containerd.Mount { + if len(active.ParentIDs) == 0 { // if we only have one layer/no parents then just return a bind mount as overlay // will not work roFlag := "rw" - if _, err := os.Stat(filepath.Join(a.path, "work")); err != nil { - if !os.IsNotExist(err) { - return nil, err - } + if active.Readonly { roFlag = "ro" } return []containerd.Mount{ { - Source: filepath.Join(a.path, "fs"), + Source: o.upperPath(active.ID), Type: "bind", Options: []string{ roFlag, "rbind", }, }, - }, nil + } } var options []string - if _, err := os.Stat(filepath.Join(a.path, "work")); err == nil { + if !active.Readonly { options = append(options, - fmt.Sprintf("workdir=%s", filepath.Join(a.path, "work")), - fmt.Sprintf("upperdir=%s", filepath.Join(a.path, "fs")), + fmt.Sprintf("workdir=%s", o.workPath(active.ID)), + fmt.Sprintf("upperdir=%s", o.upperPath(active.ID)), ) - } else if !os.IsNotExist(err) { - return nil, err - } else if len(parents) == 1 { + } else if len(active.ParentIDs) == 1 { return []containerd.Mount{ { - Source: parents[0], + Source: o.upperPath(active.ParentIDs[0]), Type: "bind", Options: []string{ "ro", "rbind", }, }, - }, nil + } } - options = append(options, fmt.Sprintf("lowerdir=%s", strings.Join(parents, ":"))) + parentPaths := make([]string, len(active.ParentIDs)) + for i := range active.ParentIDs { + parentPaths[i] = o.upperPath(active.ParentIDs[i]) + } + + options = append(options, fmt.Sprintf("lowerdir=%s", strings.Join(parentPaths, ":"))) return []containerd.Mount{ { Type: "overlay", Source: "overlay", Options: options, }, - }, nil -} - -func newCache() *cache { - return &cache{ - links: make(map[string]string), } + } -type cache struct { - mu sync.Mutex - links map[string]string +func (o *Snapshotter) upperPath(id string) string { + return filepath.Join(o.root, "snapshots", id, "fs") } -func (c *cache) get(path string) (string, error) { - c.mu.Lock() - defer c.mu.Unlock() - target, ok := c.links[path] - if !ok { - link, err := os.Readlink(path) - if err != nil { - return "", err - } - c.links[path], target = link, link - } - return target, nil -} - -func (c *cache) invalidate(path string) { - c.mu.Lock() - defer c.mu.Unlock() - - delete(c.links, path) +func (o *Snapshotter) workPath(id string) string { + return filepath.Join(o.root, "snapshots", id, "work") } diff --git a/snapshot/overlay/overlay_test.go b/snapshot/overlay/overlay_test.go index d9f974f..3417760 100644 --- a/snapshot/overlay/overlay_test.go +++ b/snapshot/overlay/overlay_test.go @@ -11,20 +11,27 @@ import ( "github.com/docker/containerd" "github.com/docker/containerd/snapshot" + "github.com/docker/containerd/snapshot/storage/boltdb" "github.com/docker/containerd/snapshot/testsuite" "github.com/docker/containerd/testutil" ) +func boltSnapshotter(ctx context.Context, root string) (snapshot.Snapshotter, func(), error) { + store, err := boltdb.NewMetaStore(ctx, filepath.Join(root, "metadata.db")) + if err != nil { + return nil, nil, err + } + snapshotter, err := NewSnapshotter(root, store) + if err != nil { + return nil, nil, err + } + + return snapshotter, func() {}, nil +} + func TestOverlay(t *testing.T) { testutil.RequiresRoot(t) - testsuite.SnapshotterSuite(t, "Overlay", func(root string) (snapshot.Snapshotter, func(), error) { - snapshotter, err := NewSnapshotter(root) - if err != nil { - t.Fatal(err) - } - - return snapshotter, func() {}, nil - }) + testsuite.SnapshotterSuite(t, "Overlay", boltSnapshotter) } func TestOverlayMounts(t *testing.T) { @@ -34,7 +41,7 @@ func TestOverlayMounts(t *testing.T) { t.Fatal(err) } defer os.RemoveAll(root) - o, err := NewSnapshotter(root) + o, _, err := boltSnapshotter(ctx, root) if err != nil { t.Error(err) return @@ -51,7 +58,7 @@ func TestOverlayMounts(t *testing.T) { if m.Type != "bind" { t.Errorf("mount type should be bind but received %q", m.Type) } - expected := filepath.Join(root, "active", hash("/tmp/test"), "fs") + expected := filepath.Join(root, "snapshots", "1", "fs") if m.Source != expected { t.Errorf("expected source %q but received %q", expected, m.Source) } @@ -70,7 +77,7 @@ func TestOverlayCommit(t *testing.T) { t.Fatal(err) } defer os.RemoveAll(root) - o, err := NewSnapshotter(root) + o, _, err := boltSnapshotter(ctx, root) if err != nil { t.Error(err) return @@ -99,7 +106,7 @@ func TestOverlayOverlayMount(t *testing.T) { t.Fatal(err) } defer os.RemoveAll(root) - o, err := NewSnapshotter(root) + o, _, err := boltSnapshotter(ctx, root) if err != nil { t.Error(err) return @@ -129,11 +136,10 @@ func TestOverlayOverlayMount(t *testing.T) { t.Errorf("expected source %q but received %q", "overlay", m.Source) } var ( - ah = hash("/tmp/layer2") - sh = hash("base") - work = "workdir=" + filepath.Join(root, "active", ah, "work") - upper = "upperdir=" + filepath.Join(root, "active", ah, "fs") - lower = "lowerdir=" + filepath.Join(root, "committed", sh, "fs") + bp = getBasePath(ctx, o, root, "/tmp/layer2") + work = "workdir=" + filepath.Join(bp, "work") + upper = "upperdir=" + filepath.Join(bp, "fs") + lower = "lowerdir=" + getParents(ctx, o, root, "/tmp/layer2")[0] ) for i, v := range []string{ work, @@ -146,6 +152,40 @@ func TestOverlayOverlayMount(t *testing.T) { } } +func getBasePath(ctx context.Context, sn snapshot.Snapshotter, root, key string) string { + o := sn.(*Snapshotter) + ctx, t, err := o.ms.TransactionContext(ctx, false) + if err != nil { + panic(err) + } + defer t.Rollback() + + active, err := o.ms.GetActive(ctx, key) + if err != nil { + panic(err) + } + + return filepath.Join(root, "snapshots", active.ID) +} + +func getParents(ctx context.Context, sn snapshot.Snapshotter, root, key string) []string { + o := sn.(*Snapshotter) + ctx, t, err := o.ms.TransactionContext(ctx, false) + if err != nil { + panic(err) + } + defer t.Rollback() + active, err := o.ms.GetActive(ctx, key) + if err != nil { + panic(err) + } + parents := make([]string, len(active.ParentIDs)) + for i := range active.ParentIDs { + parents[i] = filepath.Join(root, "snapshots", active.ParentIDs[i], "fs") + } + return parents +} + func TestOverlayOverlayRead(t *testing.T) { testutil.RequiresRoot(t) ctx := context.TODO() @@ -154,7 +194,7 @@ func TestOverlayOverlayRead(t *testing.T) { t.Fatal(err) } defer os.RemoveAll(root) - o, err := NewSnapshotter(root) + o, _, err := boltSnapshotter(ctx, root) if err != nil { t.Error(err) return @@ -206,7 +246,7 @@ func TestOverlayView(t *testing.T) { t.Fatal(err) } defer os.RemoveAll(root) - o, err := NewSnapshotter(root) + o, _, err := boltSnapshotter(ctx, root) if err != nil { t.Fatal(err) } @@ -228,7 +268,7 @@ func TestOverlayView(t *testing.T) { if err != nil { t.Fatal(err) } - if err := ioutil.WriteFile(filepath.Join(root, "active", hash(key), "fs", "foo"), []byte("hi, again"), 0660); err != nil { + if err := ioutil.WriteFile(filepath.Join(getParents(ctx, o, root, "/tmp/top")[0], "foo"), []byte("hi, again"), 0660); err != nil { t.Fatal(err) } if err := o.Commit(ctx, "top", key); err != nil { @@ -246,7 +286,7 @@ func TestOverlayView(t *testing.T) { if m.Type != "bind" { t.Errorf("mount type should be bind but received %q", m.Type) } - expected := filepath.Join(root, "committed", hash("base"), "fs") + expected := getParents(ctx, o, root, "/tmp/view1")[0] if m.Source != expected { t.Errorf("expected source %q but received %q", expected, m.Source) } @@ -274,7 +314,8 @@ func TestOverlayView(t *testing.T) { if len(m.Options) != 1 { t.Errorf("expected 1 mount option but got %d", len(m.Options)) } - expected = fmt.Sprintf("lowerdir=%s:%s", filepath.Join(root, "committed", hash("top"), "fs"), filepath.Join(root, "committed", hash("base"), "fs")) + lowers := getParents(ctx, o, root, "/tmp/view2") + expected = fmt.Sprintf("lowerdir=%s:%s", lowers[0], lowers[1]) if m.Options[0] != expected { t.Errorf("expected option %q but received %q", expected, m.Options[0]) } diff --git a/snapshot/storage/boltdb/bolt.go b/snapshot/storage/boltdb/bolt.go new file mode 100644 index 0000000..9219bf1 --- /dev/null +++ b/snapshot/storage/boltdb/bolt.go @@ -0,0 +1,396 @@ +package boltdb + +import ( + "context" + "encoding/binary" + "fmt" + + "github.com/boltdb/bolt" + "github.com/docker/containerd/snapshot" + "github.com/docker/containerd/snapshot/storage" + "github.com/gogo/protobuf/proto" + "github.com/pkg/errors" +) + +var ( + bucketKeyStorageVersion = []byte("v1") + bucketKeySnapshot = []byte("snapshots") + bucketKeyParents = []byte("parents") +) + +type boltFileTransactor struct { + db *bolt.DB + tx *bolt.Tx +} + +type boltMetastore struct { + dbfile string +} + +// NewMetaStore returns a snapshot MetaStore for storage of metadata related to +// a snapshot driver backed by a bolt file database. This implementation is +// strongly consistent and does all metadata changes in a transaction to prevent +// against process crashes causing inconsistent metadata state. +func NewMetaStore(ctx context.Context, dbfile string) (storage.MetaStore, error) { + return &boltMetastore{ + dbfile: dbfile, + }, nil +} + +func (ms *boltFileTransactor) Rollback() error { + defer ms.db.Close() + return ms.tx.Rollback() +} + +func (ms *boltFileTransactor) Commit() error { + defer ms.db.Close() + return ms.tx.Commit() +} + +type transactionKey struct{} + +func (ms *boltMetastore) TransactionContext(ctx context.Context, writable bool) (context.Context, storage.Transactor, error) { + db, err := bolt.Open(ms.dbfile, 0600, nil) + if err != nil { + return ctx, nil, errors.Wrap(err, "failed to open database file") + } + + tx, err := db.Begin(writable) + if err != nil { + return ctx, nil, errors.Wrap(err, "failed to start transaction") + } + + t := &boltFileTransactor{ + db: db, + tx: tx, + } + + ctx = context.WithValue(ctx, transactionKey{}, t) + + return ctx, t, nil +} + +func (ms *boltMetastore) withBucket(ctx context.Context, fn func(context.Context, *bolt.Bucket, *bolt.Bucket) error) error { + t, ok := ctx.Value(transactionKey{}).(*boltFileTransactor) + if !ok { + return errors.Errorf("no transaction in context") + } + bkt := t.tx.Bucket(bucketKeyStorageVersion) + if bkt == nil { + return errors.Wrap(snapshot.ErrSnapshotNotExist, "bucket does not exist") + } + return fn(ctx, bkt.Bucket(bucketKeySnapshot), bkt.Bucket(bucketKeyParents)) +} + +func (ms *boltMetastore) createBucketIfNotExists(ctx context.Context, fn func(context.Context, *bolt.Bucket, *bolt.Bucket) error) error { + t, ok := ctx.Value(transactionKey{}).(*boltFileTransactor) + if !ok { + return errors.Errorf("no transaction in context") + } + + bkt, err := t.tx.CreateBucketIfNotExists(bucketKeyStorageVersion) + if err != nil { + return errors.Wrap(err, "failed to create version bucket") + } + sbkt, err := bkt.CreateBucketIfNotExists(bucketKeySnapshot) + if err != nil { + return errors.Wrap(err, "failed to create snapshots bucket") + } + pbkt, err := bkt.CreateBucketIfNotExists(bucketKeyParents) + if err != nil { + return errors.Wrap(err, "failed to create snapshots bucket") + } + return fn(ctx, sbkt, pbkt) +} + +func fromProtoKind(k Kind) snapshot.Kind { + if k == KindActive { + return snapshot.KindActive + } + return snapshot.KindCommitted +} + +// parentKey returns a composite key of the parent and child identifiers. The +// parts of the key are separated by a zero byte. +func parentKey(parent, child uint64) []byte { + b := make([]byte, binary.Size([]uint64{parent, child})+1) + i := binary.PutUvarint(b, parent) + j := binary.PutUvarint(b[i+1:], child) + return b[0 : i+j+1] +} + +// parentPrefixKey returns the parent part of the composite key with the +// zero byte separator. +func parentPrefixKey(parent uint64) []byte { + b := make([]byte, binary.Size(parent)+1) + i := binary.PutUvarint(b, parent) + return b[0 : i+1] +} + +// getParentPrefix returns the first part of the composite key which +// represents the parent identifier. +func getParentPrefix(b []byte) uint64 { + parent, _ := binary.Uvarint(b) + return parent +} + +func (ms *boltMetastore) Stat(ctx context.Context, key string) (snapshot.Info, error) { + var ss Snapshot + err := ms.withBucket(ctx, func(ctx context.Context, bkt, pbkt *bolt.Bucket) error { + return getSnapshot(bkt, key, &ss) + }) + if err != nil { + return snapshot.Info{}, err + } + + return snapshot.Info{ + Name: key, + Parent: ss.Parent, + Kind: fromProtoKind(ss.Kind), + Readonly: ss.Readonly, + }, nil +} + +func (ms *boltMetastore) Walk(ctx context.Context, fn func(context.Context, snapshot.Info) error) error { + return ms.withBucket(ctx, func(ctx context.Context, bkt, pbkt *bolt.Bucket) error { + return bkt.ForEach(func(k, v []byte) error { + // skip nested buckets + if v == nil { + return nil + } + var ss Snapshot + if err := proto.Unmarshal(v, &ss); err != nil { + return errors.Wrap(err, "failed to unmarshal snapshot") + } + + info := snapshot.Info{ + Name: string(k), + Parent: ss.Parent, + Kind: fromProtoKind(ss.Kind), + Readonly: ss.Readonly, + } + return fn(ctx, info) + }) + }) +} + +func (ms *boltMetastore) CreateActive(ctx context.Context, key, parent string, readonly bool) (a storage.Active, err error) { + err = ms.createBucketIfNotExists(ctx, func(ctx context.Context, bkt, pbkt *bolt.Bucket) error { + var ( + parentS *Snapshot + ) + if parent != "" { + parentS = new(Snapshot) + if err := getSnapshot(bkt, parent, parentS); err != nil { + return errors.Wrap(err, "failed to get parent snapshot") + } + + if parentS.Kind != KindCommitted { + return errors.Wrap(snapshot.ErrSnapshotNotCommitted, "parent is not committed snapshot") + } + } + b := bkt.Get([]byte(key)) + if len(b) != 0 { + return snapshot.ErrSnapshotExist + } + + id, err := bkt.NextSequence() + if err != nil { + return errors.Wrap(err, "unable to get identifier") + } + + ss := Snapshot{ + ID: id, + Parent: parent, + Kind: KindActive, + Readonly: readonly, + } + if err := putSnapshot(bkt, key, &ss); err != nil { + return err + } + + if parentS != nil { + // Store a backlink from the key to the parent. Store the snapshot name + // as the value to allow following the backlink to the snapshot value. + if err := pbkt.Put(parentKey(parentS.ID, ss.ID), []byte(key)); err != nil { + return errors.Wrap(err, "failed to write parent link") + } + + a.ParentIDs, err = ms.parents(bkt, parentS) + if err != nil { + return errors.Wrap(err, "failed to get parent chain") + } + } + + a.ID = fmt.Sprintf("%d", id) + a.Readonly = readonly + + return nil + }) + if err != nil { + return storage.Active{}, err + } + + return +} + +func (ms *boltMetastore) GetActive(ctx context.Context, key string) (a storage.Active, err error) { + err = ms.withBucket(ctx, func(ctx context.Context, bkt, pbkt *bolt.Bucket) error { + b := bkt.Get([]byte(key)) + if len(b) == 0 { + return snapshot.ErrSnapshotNotExist + } + + var ss Snapshot + if err := proto.Unmarshal(b, &ss); err != nil { + return errors.Wrap(err, "failed to unmarshal snapshot") + } + if ss.Kind != KindActive { + return snapshot.ErrSnapshotNotActive + } + + a.ID = fmt.Sprintf("%d", ss.ID) + a.Readonly = ss.Readonly + + if ss.Parent != "" { + var parent Snapshot + if err := getSnapshot(bkt, ss.Parent, &parent); err != nil { + return errors.Wrap(err, "failed to get parent snapshot") + } + + a.ParentIDs, err = ms.parents(bkt, &parent) + if err != nil { + return errors.Wrap(err, "failed to get parent chain") + } + } + return nil + }) + if err != nil { + return storage.Active{}, err + } + + return +} + +func (ms *boltMetastore) parents(bkt *bolt.Bucket, parent *Snapshot) (parents []string, err error) { + for { + parents = append(parents, fmt.Sprintf("%d", parent.ID)) + + if parent.Parent == "" { + return + } + + var ps Snapshot + if err := getSnapshot(bkt, parent.Parent, &ps); err != nil { + return nil, errors.Wrap(err, "failed to get parent snapshot") + } + parent = &ps + } + + return +} + +func (ms *boltMetastore) Remove(ctx context.Context, key string) (id string, k snapshot.Kind, err error) { + err = ms.withBucket(ctx, func(ctx context.Context, bkt, pbkt *bolt.Bucket) error { + var ss Snapshot + b := bkt.Get([]byte(key)) + if len(b) == 0 { + return snapshot.ErrSnapshotNotExist + } + + if err := proto.Unmarshal(b, &ss); err != nil { + return errors.Wrap(err, "failed to unmarshal snapshot") + } + + if pbkt != nil { + k, _ := pbkt.Cursor().Seek(parentPrefixKey(ss.ID)) + if getParentPrefix(k) == ss.ID { + return errors.Errorf("cannot remove snapshot with child") + } + + if ss.Parent != "" { + var ps Snapshot + if err := getSnapshot(bkt, ss.Parent, &ps); err != nil { + return errors.Wrap(err, "failed to get parent snapshot") + } + + if err := pbkt.Delete(parentKey(ps.ID, ss.ID)); err != nil { + return errors.Wrap(err, "failed to delte parent link") + } + } + } + + if err := bkt.Delete([]byte(key)); err != nil { + return errors.Wrap(err, "failed to delete snapshot") + } + + id = fmt.Sprintf("%d", ss.ID) + k = fromProtoKind(ss.Kind) + + return nil + }) + + return +} + +func (ms *boltMetastore) Commit(ctx context.Context, key, name string) (id string, err error) { + err = ms.withBucket(ctx, func(ctx context.Context, bkt, pbkt *bolt.Bucket) error { + b := bkt.Get([]byte(name)) + if len(b) != 0 { + return errors.Wrap(snapshot.ErrSnapshotExist, "committed name already exists") + } + + var ss Snapshot + if err := getSnapshot(bkt, key, &ss); err != nil { + return errors.Wrap(err, "failed to get active snapshot") + } + if ss.Kind != KindActive { + return snapshot.ErrSnapshotNotActive + } + if ss.Readonly { + return errors.Errorf("active snapshot is readonly") + } + + ss.Kind = KindCommitted + ss.Readonly = true + + if err := putSnapshot(bkt, name, &ss); err != nil { + return err + } + if err := bkt.Delete([]byte(key)); err != nil { + return errors.Wrap(err, "failed to delete active") + } + + id = fmt.Sprintf("%d", ss.ID) + + return nil + }) + if err != nil { + return "", err + } + + return +} + +func getSnapshot(bkt *bolt.Bucket, key string, ss *Snapshot) error { + b := bkt.Get([]byte(key)) + if len(b) == 0 { + return snapshot.ErrSnapshotNotExist + } + if err := proto.Unmarshal(b, ss); err != nil { + return errors.Wrap(err, "failed to unmarshal snapshot") + } + return nil +} + +func putSnapshot(bkt *bolt.Bucket, key string, ss *Snapshot) error { + b, err := proto.Marshal(ss) + if err != nil { + return errors.Wrap(err, "failed to marshal snapshot") + } + + if err := bkt.Put([]byte(key), b); err != nil { + return errors.Wrap(err, "failed to save snapshot") + } + return nil +} diff --git a/snapshot/storage/boltdb/bolt_test.go b/snapshot/storage/boltdb/bolt_test.go new file mode 100644 index 0000000..b204ee2 --- /dev/null +++ b/snapshot/storage/boltdb/bolt_test.go @@ -0,0 +1,25 @@ +package boltdb + +import ( + "context" + "path/filepath" + "testing" + + "github.com/docker/containerd/snapshot/storage" + "github.com/docker/containerd/snapshot/storage/testsuite" + + // Does not require root but flag must be defined for snapshot tests + _ "github.com/docker/containerd/testutil" +) + +func TestBoltDB(t *testing.T) { + testsuite.MetaStoreSuite(t, "BoltDB", func(ctx context.Context, root string) (storage.MetaStore, error) { + return NewMetaStore(ctx, filepath.Join(root, "metadata.db")) + }) +} + +func BenchmarkSuite(b *testing.B) { + testsuite.Benchmarks(b, "BoltDBBench", func(ctx context.Context, root string) (storage.MetaStore, error) { + return NewMetaStore(ctx, filepath.Join(root, "metadata.db")) + }) +} diff --git a/snapshot/storage/boltdb/record.pb.go b/snapshot/storage/boltdb/record.pb.go new file mode 100644 index 0000000..cb6bb2d --- /dev/null +++ b/snapshot/storage/boltdb/record.pb.go @@ -0,0 +1,468 @@ +// Code generated by protoc-gen-gogo. +// source: github.com/docker/containerd/snapshot/storage/boltdb/record.proto +// DO NOT EDIT! + +/* + Package boltdb is a generated protocol buffer package. + + It is generated from these files: + github.com/docker/containerd/snapshot/storage/boltdb/record.proto + + It has these top-level messages: + Snapshot +*/ +package boltdb + +import proto "github.com/gogo/protobuf/proto" +import fmt "fmt" +import math "math" +import _ "github.com/gogo/protobuf/gogoproto" + +import strings "strings" +import reflect "reflect" + +import io "io" + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion2 // please upgrade the proto package + +// Kind defines the kind of snapshot. +type Kind int32 + +const ( + // KindActive represents an active snapshot + KindActive Kind = 0 + // KindCommitted represents a committed immutable snapshot + KindCommitted Kind = 1 +) + +var Kind_name = map[int32]string{ + 0: "ACTIVE", + 1: "COMMITTED", +} +var Kind_value = map[string]int32{ + "ACTIVE": 0, + "COMMITTED": 1, +} + +func (x Kind) String() string { + return proto.EnumName(Kind_name, int32(x)) +} +func (Kind) EnumDescriptor() ([]byte, []int) { return fileDescriptorRecord, []int{0} } + +type Snapshot struct { + ID uint64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Parent string `protobuf:"bytes,2,opt,name=parent,proto3" json:"parent,omitempty"` + Kind Kind `protobuf:"varint,4,opt,name=kind,proto3,enum=containerd.v1.Kind" json:"kind,omitempty"` + Readonly bool `protobuf:"varint,5,opt,name=readonly,proto3" json:"readonly,omitempty"` +} + +func (m *Snapshot) Reset() { *m = Snapshot{} } +func (*Snapshot) ProtoMessage() {} +func (*Snapshot) Descriptor() ([]byte, []int) { return fileDescriptorRecord, []int{0} } + +func init() { + proto.RegisterType((*Snapshot)(nil), "containerd.v1.Snapshot") + proto.RegisterEnum("containerd.v1.Kind", Kind_name, Kind_value) +} +func (m *Snapshot) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalTo(dAtA) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *Snapshot) MarshalTo(dAtA []byte) (int, error) { + var i int + _ = i + var l int + _ = l + if m.ID != 0 { + dAtA[i] = 0x8 + i++ + i = encodeVarintRecord(dAtA, i, uint64(m.ID)) + } + if len(m.Parent) > 0 { + dAtA[i] = 0x12 + i++ + i = encodeVarintRecord(dAtA, i, uint64(len(m.Parent))) + i += copy(dAtA[i:], m.Parent) + } + if m.Kind != 0 { + dAtA[i] = 0x20 + i++ + i = encodeVarintRecord(dAtA, i, uint64(m.Kind)) + } + if m.Readonly { + dAtA[i] = 0x28 + i++ + if m.Readonly { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i++ + } + return i, nil +} + +func encodeFixed64Record(dAtA []byte, offset int, v uint64) int { + dAtA[offset] = uint8(v) + dAtA[offset+1] = uint8(v >> 8) + dAtA[offset+2] = uint8(v >> 16) + dAtA[offset+3] = uint8(v >> 24) + dAtA[offset+4] = uint8(v >> 32) + dAtA[offset+5] = uint8(v >> 40) + dAtA[offset+6] = uint8(v >> 48) + dAtA[offset+7] = uint8(v >> 56) + return offset + 8 +} +func encodeFixed32Record(dAtA []byte, offset int, v uint32) int { + dAtA[offset] = uint8(v) + dAtA[offset+1] = uint8(v >> 8) + dAtA[offset+2] = uint8(v >> 16) + dAtA[offset+3] = uint8(v >> 24) + return offset + 4 +} +func encodeVarintRecord(dAtA []byte, offset int, v uint64) int { + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return offset + 1 +} +func (m *Snapshot) Size() (n int) { + var l int + _ = l + if m.ID != 0 { + n += 1 + sovRecord(uint64(m.ID)) + } + l = len(m.Parent) + if l > 0 { + n += 1 + l + sovRecord(uint64(l)) + } + if m.Kind != 0 { + n += 1 + sovRecord(uint64(m.Kind)) + } + if m.Readonly { + n += 2 + } + return n +} + +func sovRecord(x uint64) (n int) { + for { + n++ + x >>= 7 + if x == 0 { + break + } + } + return n +} +func sozRecord(x uint64) (n int) { + return sovRecord(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (this *Snapshot) String() string { + if this == nil { + return "nil" + } + s := strings.Join([]string{`&Snapshot{`, + `ID:` + fmt.Sprintf("%v", this.ID) + `,`, + `Parent:` + fmt.Sprintf("%v", this.Parent) + `,`, + `Kind:` + fmt.Sprintf("%v", this.Kind) + `,`, + `Readonly:` + fmt.Sprintf("%v", this.Readonly) + `,`, + `}`, + }, "") + return s +} +func valueToStringRecord(v interface{}) string { + rv := reflect.ValueOf(v) + if rv.IsNil() { + return "nil" + } + pv := reflect.Indirect(rv).Interface() + return fmt.Sprintf("*%v", pv) +} +func (m *Snapshot) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowRecord + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: Snapshot: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: Snapshot: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field ID", wireType) + } + m.ID = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowRecord + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.ID |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Parent", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowRecord + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthRecord + } + postIndex := iNdEx + intStringLen + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Parent = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 4: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Kind", wireType) + } + m.Kind = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowRecord + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Kind |= (Kind(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + case 5: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Readonly", wireType) + } + var v int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowRecord + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + m.Readonly = bool(v != 0) + default: + iNdEx = preIndex + skippy, err := skipRecord(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthRecord + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipRecord(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowRecord + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowRecord + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + return iNdEx, nil + case 1: + iNdEx += 8 + return iNdEx, nil + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowRecord + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + iNdEx += length + if length < 0 { + return 0, ErrInvalidLengthRecord + } + return iNdEx, nil + case 3: + for { + var innerWire uint64 + var start int = iNdEx + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowRecord + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + innerWire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + innerWireType := int(innerWire & 0x7) + if innerWireType == 4 { + break + } + next, err := skipRecord(dAtA[start:]) + if err != nil { + return 0, err + } + iNdEx = start + next + } + return iNdEx, nil + case 4: + return iNdEx, nil + case 5: + iNdEx += 4 + return iNdEx, nil + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + } + panic("unreachable") +} + +var ( + ErrInvalidLengthRecord = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowRecord = fmt.Errorf("proto: integer overflow") +) + +func init() { + proto.RegisterFile("github.com/docker/containerd/snapshot/storage/boltdb/record.proto", fileDescriptorRecord) +} + +var fileDescriptorRecord = []byte{ + // 314 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x54, 0x8f, 0xcf, 0x4a, 0xf3, 0x40, + 0x14, 0xc5, 0x33, 0x21, 0x5f, 0x68, 0x87, 0xaf, 0xa5, 0x46, 0x29, 0x21, 0x8b, 0x71, 0x70, 0x63, + 0x70, 0x91, 0x41, 0x7d, 0x82, 0xfe, 0x5b, 0x94, 0x52, 0x84, 0x58, 0xdc, 0x27, 0x99, 0x21, 0x1d, + 0xda, 0xce, 0x2d, 0xd3, 0xb1, 0xe0, 0x4a, 0x97, 0xd2, 0x77, 0xe8, 0x4a, 0x9f, 0xc2, 0x27, 0xe8, + 0xd2, 0xa5, 0x2b, 0xb1, 0x79, 0x12, 0x69, 0x5a, 0x14, 0x77, 0xf7, 0xdc, 0xf3, 0xbb, 0xe7, 0x72, + 0x70, 0x2b, 0x97, 0x66, 0x7c, 0x9f, 0x46, 0x19, 0xcc, 0x18, 0x87, 0x6c, 0x22, 0x34, 0xcb, 0x40, + 0x99, 0x44, 0x2a, 0xa1, 0x39, 0x5b, 0xa8, 0x64, 0xbe, 0x18, 0x83, 0x61, 0x0b, 0x03, 0x3a, 0xc9, + 0x05, 0x4b, 0x61, 0x6a, 0x78, 0xca, 0xb4, 0xc8, 0x40, 0xf3, 0x68, 0xae, 0xc1, 0x80, 0x57, 0xfb, + 0x3d, 0x88, 0x96, 0x97, 0xc1, 0x49, 0x0e, 0x39, 0x94, 0x0e, 0xdb, 0x4d, 0x7b, 0xe8, 0xec, 0x11, + 0x57, 0x6e, 0x0f, 0x61, 0x5e, 0x13, 0xdb, 0x92, 0xfb, 0x88, 0xa2, 0xd0, 0x69, 0xbb, 0xc5, 0xe7, + 0xa9, 0xdd, 0xef, 0xc6, 0xb6, 0xe4, 0x5e, 0x13, 0xbb, 0xf3, 0x44, 0x0b, 0x65, 0x7c, 0x9b, 0xa2, + 0xb0, 0x1a, 0x1f, 0x94, 0x77, 0x8e, 0x9d, 0x89, 0x54, 0xdc, 0x77, 0x28, 0x0a, 0xeb, 0x57, 0xc7, + 0xd1, 0x9f, 0x7f, 0xd1, 0x40, 0x2a, 0x1e, 0x97, 0x80, 0x17, 0xe0, 0x8a, 0x16, 0x09, 0x07, 0x35, + 0x7d, 0xf0, 0xff, 0x51, 0x14, 0x56, 0xe2, 0x1f, 0x7d, 0x11, 0x63, 0x67, 0xb0, 0x67, 0xdc, 0x56, + 0x67, 0xd4, 0xbf, 0xeb, 0x35, 0xac, 0xa0, 0xbe, 0x5a, 0x53, 0xbc, 0xdb, 0xb6, 0x32, 0x23, 0x97, + 0xc2, 0xa3, 0xb8, 0xda, 0xb9, 0x19, 0x0e, 0xfb, 0xa3, 0x51, 0xaf, 0xdb, 0x40, 0xc1, 0xd1, 0x6a, + 0x4d, 0x6b, 0x3b, 0xbb, 0x03, 0xb3, 0x99, 0x34, 0x46, 0xf0, 0xe0, 0xff, 0xf3, 0x0b, 0xb1, 0xde, + 0x5e, 0x49, 0x99, 0xd5, 0xf6, 0x37, 0x5b, 0x62, 0x7d, 0x6c, 0x89, 0xf5, 0x54, 0x10, 0xb4, 0x29, + 0x08, 0x7a, 0x2f, 0x08, 0xfa, 0x2a, 0x08, 0x4a, 0xdd, 0xb2, 0xf5, 0xf5, 0x77, 0x00, 0x00, 0x00, + 0xff, 0xff, 0x9c, 0xce, 0xfc, 0xc2, 0x5f, 0x01, 0x00, 0x00, +} diff --git a/snapshot/storage/boltdb/record.proto b/snapshot/storage/boltdb/record.proto new file mode 100644 index 0000000..4d7a214 --- /dev/null +++ b/snapshot/storage/boltdb/record.proto @@ -0,0 +1,24 @@ +syntax = "proto3"; + +package containerd.v1; + +import "gogoproto/gogo.proto"; + +// Kind defines the kind of snapshot. +enum Kind { + option (gogoproto.goproto_enum_prefix) = false; + option (gogoproto.enum_customname) = "Kind"; + + // KindActive represents an active snapshot + ACTIVE = 0 [(gogoproto.enumvalue_customname) = "KindActive"]; + + // KindCommitted represents a committed immutable snapshot + COMMITTED = 1 [(gogoproto.enumvalue_customname) = "KindCommitted"]; +} + +message Snapshot { + uint64 id = 1 [(gogoproto.customname) = "ID"]; + string parent = 2; + Kind kind = 4; + bool readonly = 5; +} diff --git a/snapshot/storage/metastore.go b/snapshot/storage/metastore.go new file mode 100644 index 0000000..f863e7c --- /dev/null +++ b/snapshot/storage/metastore.go @@ -0,0 +1,69 @@ +package storage + +import ( + "context" + + "github.com/docker/containerd/snapshot" +) + +// MetaStore is used to store metadata related to a snapshot driver. The +// MetaStore is intended to store metadata related to name, state and +// parentage. Using the MetaStore is not required to implement a snapshot +// driver but can be used to handle the persistence and transactional +// complexities of a driver implementation. +type MetaStore interface { + // TransactionContext creates a new transaction context. + TransactionContext(ctx context.Context, writable bool) (context.Context, Transactor, error) + + // Stat returns the snapshot stat Info directly from + // the metadata. + Stat(ctx context.Context, key string) (snapshot.Info, error) + + // Walk iterates through all metadata for the stored + // snapshots and calls the provided function for each. + Walk(ctx context.Context, fn func(context.Context, snapshot.Info) error) error + + // CreateActive creates a new active snapshot transaction referenced by + // the provided key. The new active snapshot will have the provided + // parent. If the readonly option is given, the active snapshot will be + // marked as readonly and can only be removed, and not committed. The + // provided context must contain a writable transaction. + CreateActive(ctx context.Context, key, parent string, readonly bool) (Active, error) + + // GetActive returns the metadata for the active snapshot transaction + // referenced by the given key. + GetActive(ctx context.Context, key string) (Active, error) + + // Remove removes a snapshot from the metastore. The provided context + // must contain a writable transaction. The string identifier for the + // snapshot is returned as well as the kind. + Remove(ctx context.Context, key string) (string, snapshot.Kind, error) + + // Commit renames the active snapshot transaction referenced by `key` + // as a committed snapshot referenced by `Name`. The resulting snapshot + // will be committed and readonly. The `key` reference will no longer + // be available for lookup or removal. The returned string identifier + // for the committed snapshot is the same identifier of the original + // active snapshot. + Commit(ctx context.Context, key, name string) (string, error) +} + +// Transactor is used to finalize an active transaction. +type Transactor interface { + // Commit commits any changes made during the transaction. + Commit() error + + // Rollback rolls back any changes made during the transaction. + Rollback() error +} + +// Active hold the metadata for an active snapshot transaction. The ParentIDs +// hold the snapshot identifiers for the committed snapshots this active is +// based on. The ParentIDs are ordered from the lowest base to highest, meaning +// they should be applied in order from the first index to the last index. The +// last index should always be considered the active snapshots immediate parent. +type Active struct { + ID string + ParentIDs []string + Readonly bool +} diff --git a/snapshot/storage/testsuite/bench.go b/snapshot/storage/testsuite/bench.go new file mode 100644 index 0000000..cb104bd --- /dev/null +++ b/snapshot/storage/testsuite/bench.go @@ -0,0 +1,217 @@ +package testsuite + +import ( + "context" + "fmt" + "io/ioutil" + "os" + "testing" + + "github.com/docker/containerd/snapshot/storage" +) + +// Benchmarks returns a benchmark suite using the provided metadata store +// creation method +func Benchmarks(b *testing.B, name string, metaFn func(context.Context, string) (storage.MetaStore, error)) { + b.Run("StatActive", makeBench(b, name, metaFn, statActiveBenchmark)) + b.Run("StatCommitted", makeBench(b, name, metaFn, statCommittedBenchmark)) + b.Run("CreateActive", makeBench(b, name, metaFn, createActiveBenchmark)) + b.Run("Remove", makeBench(b, name, metaFn, removeBenchmark)) + b.Run("Commit", makeBench(b, name, metaFn, commitBenchmark)) + b.Run("GetActive", makeBench(b, name, metaFn, getActiveBenchmark)) + b.Run("WriteTransaction", openCloseWritable(b, name, metaFn)) + b.Run("ReadTransaction", openCloseReadonly(b, name, metaFn)) +} + +// makeBench creates a benchmark with a writable transaction +func makeBench(b *testing.B, name string, metaFn func(context.Context, string) (storage.MetaStore, error), fn func(context.Context, *testing.B, storage.MetaStore)) func(b *testing.B) { + return func(b *testing.B) { + ctx := context.Background() + tmpDir, err := ioutil.TempDir("", "metastore-bench-"+name+"-") + if err != nil { + b.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + ms, err := metaFn(ctx, tmpDir) + if err != nil { + b.Fatal(err) + } + + ctx, t, err := ms.TransactionContext(ctx, true) + if err != nil { + b.Fatal(err) + } + defer t.Commit() + + b.ResetTimer() + fn(ctx, b, ms) + } +} + +func openCloseWritable(b *testing.B, name string, metaFn func(context.Context, string) (storage.MetaStore, error)) func(b *testing.B) { + return func(b *testing.B) { + ctx := context.Background() + tmpDir, err := ioutil.TempDir("", "metastore-bench-"+name+"-") + if err != nil { + b.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + ms, err := metaFn(ctx, tmpDir) + if err != nil { + b.Fatal(err) + } + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _, t, err := ms.TransactionContext(ctx, true) + if err != nil { + b.Fatal(err) + } + if err := t.Commit(); err != nil { + b.Fatal(err) + } + } + } +} + +func openCloseReadonly(b *testing.B, name string, metaFn func(context.Context, string) (storage.MetaStore, error)) func(b *testing.B) { + return func(b *testing.B) { + ctx := context.Background() + tmpDir, err := ioutil.TempDir("", "metastore-bench-"+name+"-") + if err != nil { + b.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + ms, err := metaFn(ctx, tmpDir) + if err != nil { + b.Fatal(err) + } + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _, t, err := ms.TransactionContext(ctx, false) + if err != nil { + b.Fatal(err) + } + if err := t.Rollback(); err != nil { + b.Fatal(err) + } + } + } +} + +func createActiveFromBase(ctx context.Context, ms storage.MetaStore, active, base string) error { + if _, err := ms.CreateActive(ctx, "bottom", "", false); err != nil { + return err + } + if _, err := ms.Commit(ctx, "bottom", base); err != nil { + return err + } + + _, err := ms.CreateActive(ctx, active, base, false) + return err +} + +func statActiveBenchmark(ctx context.Context, b *testing.B, ms storage.MetaStore) { + if err := createActiveFromBase(ctx, ms, "active", "base"); err != nil { + b.Fatal(err) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := ms.Stat(ctx, "active") + if err != nil { + b.Fatal(err) + } + } +} + +func statCommittedBenchmark(ctx context.Context, b *testing.B, ms storage.MetaStore) { + if err := createActiveFromBase(ctx, ms, "active", "base"); err != nil { + b.Fatal(err) + } + if _, err := ms.Commit(ctx, "active", "committed"); err != nil { + b.Fatal(err) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := ms.Stat(ctx, "committed") + if err != nil { + b.Fatal(err) + } + } +} + +func createActiveBenchmark(ctx context.Context, b *testing.B, ms storage.MetaStore) { + for i := 0; i < b.N; i++ { + if _, err := ms.CreateActive(ctx, "active", "", false); err != nil { + b.Fatal(err) + } + b.StopTimer() + if _, _, err := ms.Remove(ctx, "active"); err != nil { + b.Fatal(err) + } + b.StartTimer() + } +} + +func removeBenchmark(ctx context.Context, b *testing.B, ms storage.MetaStore) { + for i := 0; i < b.N; i++ { + b.StopTimer() + if _, err := ms.CreateActive(ctx, "active", "", false); err != nil { + b.Fatal(err) + } + b.StartTimer() + if _, _, err := ms.Remove(ctx, "active"); err != nil { + b.Fatal(err) + } + } +} + +func commitBenchmark(ctx context.Context, b *testing.B, ms storage.MetaStore) { + b.StopTimer() + for i := 0; i < b.N; i++ { + if _, err := ms.CreateActive(ctx, "active", "", false); err != nil { + b.Fatal(err) + } + b.StartTimer() + if _, err := ms.Commit(ctx, "active", "committed"); err != nil { + b.Fatal(err) + } + b.StopTimer() + if _, _, err := ms.Remove(ctx, "committed"); err != nil { + b.Fatal(err) + } + } +} + +func getActiveBenchmark(ctx context.Context, b *testing.B, ms storage.MetaStore) { + var base string + for i := 1; i <= 10; i++ { + if _, err := ms.CreateActive(ctx, "tmp", base, false); err != nil { + b.Fatalf("create active failed: %+v", err) + } + base = fmt.Sprintf("base-%d", i) + if _, err := ms.Commit(ctx, "tmp", base); err != nil { + b.Fatalf("commit failed: %+v", err) + } + + } + + if _, err := ms.CreateActive(ctx, "active", base, false); err != nil { + b.Fatalf("create active failed: %+v", err) + } + b.ResetTimer() + + for i := 0; i < b.N; i++ { + if _, err := ms.GetActive(ctx, "active"); err != nil { + b.Fatal(err) + } + } +} diff --git a/snapshot/storage/testsuite/testsuite.go b/snapshot/storage/testsuite/testsuite.go new file mode 100644 index 0000000..ce300f6 --- /dev/null +++ b/snapshot/storage/testsuite/testsuite.go @@ -0,0 +1,552 @@ +package testsuite + +import ( + "context" + "io/ioutil" + "os" + "testing" + + "github.com/docker/containerd/snapshot" + "github.com/docker/containerd/snapshot/storage" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" +) + +type testFunc func(context.Context, *testing.T, storage.MetaStore) + +type metaFactory func(context.Context, string) (storage.MetaStore, error) + +type populateFunc func(context.Context, storage.MetaStore) error + +// MetaStoreSuite runs a test suite on the metastore given a factory function. +func MetaStoreSuite(t *testing.T, name string, meta func(ctx context.Context, root string) (storage.MetaStore, error)) { + t.Run("Stat", makeTest(t, name, meta, inReadTransaction(testStat, basePopulate))) + t.Run("StatNotExist", makeTest(t, name, meta, inReadTransaction(testStatNotExist, basePopulate))) + t.Run("StatEmptyDB", makeTest(t, name, meta, inReadTransaction(testStatNotExist, nil))) + t.Run("Walk", makeTest(t, name, meta, inReadTransaction(testWalk, basePopulate))) + t.Run("GetActive", makeTest(t, name, meta, testGetActive)) + t.Run("GetActiveNotExist", makeTest(t, name, meta, inReadTransaction(testGetActiveNotExist, basePopulate))) + t.Run("GetActiveCommitted", makeTest(t, name, meta, inReadTransaction(testGetActiveCommitted, basePopulate))) + t.Run("GetActiveEmptyDB", makeTest(t, name, meta, inReadTransaction(testGetActiveNotExist, basePopulate))) + t.Run("CreateActive", makeTest(t, name, meta, inWriteTransaction(testCreateActive))) + t.Run("CreateActiveNotExist", makeTest(t, name, meta, inWriteTransaction(testCreateActiveNotExist))) + t.Run("CreateActiveExist", makeTest(t, name, meta, inWriteTransaction(testCreateActiveExist))) + t.Run("CreateActiveFromActive", makeTest(t, name, meta, inWriteTransaction(testCreateActiveFromActive))) + t.Run("Commit", makeTest(t, name, meta, inWriteTransaction(testCommit))) + t.Run("CommitNotExist", makeTest(t, name, meta, inWriteTransaction(testCommitExist))) + t.Run("CommitExist", makeTest(t, name, meta, inWriteTransaction(testCommitExist))) + t.Run("CommitCommitted", makeTest(t, name, meta, inWriteTransaction(testCommitCommitted))) + t.Run("CommitReadonly", makeTest(t, name, meta, inWriteTransaction(testCommitReadonly))) + t.Run("Remove", makeTest(t, name, meta, inWriteTransaction(testRemove))) + t.Run("RemoveNotExist", makeTest(t, name, meta, inWriteTransaction(testRemoveNotExist))) + t.Run("RemoveWithChildren", makeTest(t, name, meta, inWriteTransaction(testRemoveWithChildren))) +} + +// makeTest creates a testsuite with a writable transaction +func makeTest(t *testing.T, name string, metaFn metaFactory, fn testFunc) func(t *testing.T) { + return func(t *testing.T) { + ctx := context.Background() + tmpDir, err := ioutil.TempDir("", "metastore-test-"+name+"-") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + ms, err := metaFn(ctx, tmpDir) + if err != nil { + t.Fatal(err) + } + + fn(ctx, t, ms) + } +} + +func inReadTransaction(fn testFunc, pf populateFunc) testFunc { + return func(ctx context.Context, t *testing.T, ms storage.MetaStore) { + if pf != nil { + ctx, tx, err := ms.TransactionContext(ctx, true) + if err != nil { + t.Fatal(err) + } + if err := pf(ctx, ms); err != nil { + if rerr := tx.Rollback(); rerr != nil { + t.Logf("Rollback failed: %+v", rerr) + } + t.Fatalf("Populate failed: %+v", err) + } + if err := tx.Commit(); err != nil { + t.Fatalf("Populate commit failed: %+v", err) + } + } + + ctx, tx, err := ms.TransactionContext(ctx, false) + if err != nil { + t.Fatal("Failed start transaction: %+v", err) + } + defer func() { + if err := tx.Rollback(); err != nil { + t.Logf("Rollback failed: %+v", err) + if !t.Failed() { + t.FailNow() + } + } + }() + + fn(ctx, t, ms) + } +} + +func inWriteTransaction(fn testFunc) testFunc { + return func(ctx context.Context, t *testing.T, ms storage.MetaStore) { + ctx, tx, err := ms.TransactionContext(ctx, true) + if err != nil { + t.Fatal("Failed to start transaction: %+v", err) + } + defer func() { + if t.Failed() { + if err := tx.Rollback(); err != nil { + t.Logf("Rollback failed: %+v", err) + } + } else { + if err := tx.Commit(); err != nil { + t.Fatal("Commit failed: %+v", err) + } + } + }() + fn(ctx, t, ms) + } +} + +// basePopulate creates 7 snapshots +// - "committed-1": committed without parent +// - "committed-2": committed with parent "committed-1" +// - "active-1": active without parent +// - "active-2": active with parent "committed-1" +// - "active-3": active with parent "committed-2" +// - "active-4": readonly active without parent" +// - "active-5": readonly active with parent "committed-2" +func basePopulate(ctx context.Context, ms storage.MetaStore) error { + if _, err := ms.CreateActive(ctx, "committed-tmp-1", "", false); err != nil { + return errors.Wrap(err, "failed to create active") + } + if _, err := ms.Commit(ctx, "committed-tmp-1", "committed-1"); err != nil { + return errors.Wrap(err, "failed to create active") + } + if _, err := ms.CreateActive(ctx, "committed-tmp-2", "committed-1", false); err != nil { + return errors.Wrap(err, "failed to create active") + } + if _, err := ms.Commit(ctx, "committed-tmp-2", "committed-2"); err != nil { + return errors.Wrap(err, "failed to create active") + } + if _, err := ms.CreateActive(ctx, "active-1", "", false); err != nil { + return errors.Wrap(err, "failed to create active") + } + if _, err := ms.CreateActive(ctx, "active-2", "committed-1", false); err != nil { + return errors.Wrap(err, "failed to create active") + } + if _, err := ms.CreateActive(ctx, "active-3", "committed-2", false); err != nil { + return errors.Wrap(err, "failed to create active") + } + if _, err := ms.CreateActive(ctx, "active-4", "", true); err != nil { + return errors.Wrap(err, "failed to create active") + } + if _, err := ms.CreateActive(ctx, "active-5", "committed-2", true); err != nil { + return errors.Wrap(err, "failed to create active") + } + return nil +} + +var baseInfo = map[string]snapshot.Info{ + "committed-1": { + Name: "committed-1", + Parent: "", + Kind: snapshot.KindCommitted, + Readonly: true, + }, + "committed-2": { + Name: "committed-2", + Parent: "committed-1", + Kind: snapshot.KindCommitted, + Readonly: true, + }, + "active-1": { + Name: "active-1", + Parent: "", + Kind: snapshot.KindActive, + Readonly: false, + }, + "active-2": { + Name: "active-2", + Parent: "committed-1", + Kind: snapshot.KindActive, + Readonly: false, + }, + "active-3": { + Name: "active-3", + Parent: "committed-2", + Kind: snapshot.KindActive, + Readonly: false, + }, + "active-4": { + Name: "active-4", + Parent: "", + Kind: snapshot.KindActive, + Readonly: true, + }, + "active-5": { + Name: "active-5", + Parent: "committed-2", + Kind: snapshot.KindActive, + Readonly: true, + }, +} + +func assertNotExist(t *testing.T, err error) { + if err == nil { + t.Fatal("Expected not exist error") + } + if !snapshot.IsNotExist(err) { + t.Fatalf("Expected not exist error, got %+v", err) + } +} + +func assertNotActive(t *testing.T, err error) { + if err == nil { + t.Fatal("Expected not active error") + } + if !snapshot.IsNotActive(err) { + t.Fatalf("Expected not active error, got %+v", err) + } +} + +func assertNotCommitted(t *testing.T, err error) { + if err == nil { + t.Fatal("Expected active error") + } + if !snapshot.IsNotCommitted(err) { + t.Fatalf("Expected active error, got %+v", err) + } +} + +func assertExist(t *testing.T, err error) { + if err == nil { + t.Fatal("Expected exist error") + } + if !snapshot.IsExist(err) { + t.Fatalf("Expected exist error, got %+v", err) + } +} + +func testStat(ctx context.Context, t *testing.T, ms storage.MetaStore) { + for key, expected := range baseInfo { + info, err := ms.Stat(ctx, key) + if err != nil { + t.Fatalf("Stat on %v failed: %+v", key, err) + } + assert.Equal(t, expected, info) + } +} + +func testStatNotExist(ctx context.Context, t *testing.T, ms storage.MetaStore) { + _, err := ms.Stat(ctx, "active-not-exist") + assertNotExist(t, err) +} + +func testWalk(ctx context.Context, t *testing.T, ms storage.MetaStore) { + found := map[string]snapshot.Info{} + err := ms.Walk(ctx, func(ctx context.Context, info snapshot.Info) error { + if _, ok := found[info.Name]; ok { + return errors.Errorf("entry already encountered") + } + found[info.Name] = info + return nil + }) + if err != nil { + t.Fatalf("Walk failed: %+v", err) + } + assert.Equal(t, baseInfo, found) +} + +func testGetActive(ctx context.Context, t *testing.T, ms storage.MetaStore) { + activeMap := map[string]storage.Active{} + populate := func(ctx context.Context, ms storage.MetaStore) error { + if _, err := ms.CreateActive(ctx, "committed-tmp-1", "", false); err != nil { + return errors.Wrap(err, "failed to create active") + } + if _, err := ms.Commit(ctx, "committed-tmp-1", "committed-1"); err != nil { + return errors.Wrap(err, "failed to create active") + } + + for _, opts := range []struct { + Name string + Parent string + Readonly bool + }{ + { + Name: "active-1", + }, + { + Name: "active-2", + Parent: "committed-1", + }, + { + Name: "active-3", + Readonly: true, + }, + { + Name: "active-4", + Parent: "committed-1", + Readonly: true, + }, + } { + active, err := ms.CreateActive(ctx, opts.Name, opts.Parent, opts.Readonly) + if err != nil { + return errors.Wrap(err, "failed to create active") + } + activeMap[opts.Name] = active + } + return nil + } + + test := func(ctx context.Context, t *testing.T, ms storage.MetaStore) { + for key, expected := range activeMap { + active, err := ms.GetActive(ctx, key) + if err != nil { + t.Fatal("Failed to get active: %+v", err) + } + assert.Equal(t, expected, active) + } + } + + inReadTransaction(test, populate)(ctx, t, ms) +} + +func testGetActiveCommitted(ctx context.Context, t *testing.T, ms storage.MetaStore) { + _, err := ms.GetActive(ctx, "committed-1") + assertNotActive(t, err) +} + +func testGetActiveNotExist(ctx context.Context, t *testing.T, ms storage.MetaStore) { + _, err := ms.GetActive(ctx, "active-not-exist") + assertNotExist(t, err) +} + +func testCreateActive(ctx context.Context, t *testing.T, ms storage.MetaStore) { + a1, err := ms.CreateActive(ctx, "active-1", "", false) + if err != nil { + t.Fatal(err) + } + if a1.Readonly { + t.Fatal("Expected writable active") + } + + a2, err := ms.CreateActive(ctx, "active-2", "", true) + if err != nil { + t.Fatal(err) + } + if a2.ID == a1.ID { + t.Fatal("Returned active identifiers must be unique") + } + if !a2.Readonly { + t.Fatal("Expected readonly active") + } + + commitID, err := ms.Commit(ctx, "active-1", "committed-1") + if err != nil { + t.Fatal(err) + } + if commitID != a1.ID { + t.Fatal("Snapshot identifier must not change on commit") + } + + a3, err := ms.CreateActive(ctx, "active-3", "committed-1", false) + if err != nil { + t.Fatal(err) + } + if a3.ID == a1.ID { + t.Fatal("Returned active identifiers must be unique") + } + if len(a3.ParentIDs) != 1 { + t.Fatal("Expected 1 parent, got %d", len(a3.ParentIDs)) + } + if a3.ParentIDs[0] != commitID { + t.Fatal("Expected active parent to be same as commit ID") + } + if a3.Readonly { + t.Fatal("Expected writable active") + } + + a4, err := ms.CreateActive(ctx, "active-4", "committed-1", true) + if err != nil { + t.Fatal(err) + } + if a4.ID == a1.ID { + t.Fatal("Returned active identifiers must be unique") + } + if len(a3.ParentIDs) != 1 { + t.Fatal("Expected 1 parent, got %d", len(a3.ParentIDs)) + } + if a3.ParentIDs[0] != commitID { + t.Fatal("Expected active parent to be same as commit ID") + } + if !a4.Readonly { + t.Fatal("Expected readonly active") + } +} + +func testCreateActiveExist(ctx context.Context, t *testing.T, ms storage.MetaStore) { + if err := basePopulate(ctx, ms); err != nil { + t.Fatalf("Populate failed: %+v", err) + } + _, err := ms.CreateActive(ctx, "active-1", "", false) + assertExist(t, err) + _, err = ms.CreateActive(ctx, "committed-1", "", false) + assertExist(t, err) +} + +func testCreateActiveNotExist(ctx context.Context, t *testing.T, ms storage.MetaStore) { + _, err := ms.CreateActive(ctx, "active-1", "does-not-exist", false) + assertNotExist(t, err) +} + +func testCreateActiveFromActive(ctx context.Context, t *testing.T, ms storage.MetaStore) { + if err := basePopulate(ctx, ms); err != nil { + t.Fatalf("Populate failed: %+v", err) + } + _, err := ms.CreateActive(ctx, "active-new", "active-1", false) + assertNotCommitted(t, err) +} + +func testCommit(ctx context.Context, t *testing.T, ms storage.MetaStore) { + a1, err := ms.CreateActive(ctx, "active-1", "", false) + if err != nil { + t.Fatal(err) + } + if a1.Readonly { + t.Fatal("Expected writable active") + } + + commitID, err := ms.Commit(ctx, "active-1", "committed-1") + if err != nil { + t.Fatal(err) + } + if commitID != a1.ID { + t.Fatal("Snapshot identifier must not change on commit") + } + + _, err = ms.GetActive(ctx, "active-1") + assertNotExist(t, err) + _, err = ms.GetActive(ctx, "committed-1") + assertNotActive(t, err) +} + +func testCommitNotExist(ctx context.Context, t *testing.T, ms storage.MetaStore) { + _, err := ms.Commit(ctx, "active-not-exist", "committed-1") + assertNotExist(t, err) +} + +func testCommitExist(ctx context.Context, t *testing.T, ms storage.MetaStore) { + if err := basePopulate(ctx, ms); err != nil { + t.Fatalf("Populate failed: %+v", err) + } + _, err := ms.Commit(ctx, "active-1", "committed-1") + assertExist(t, err) +} + +func testCommitCommitted(ctx context.Context, t *testing.T, ms storage.MetaStore) { + if err := basePopulate(ctx, ms); err != nil { + t.Fatalf("Populate failed: %+v", err) + } + _, err := ms.Commit(ctx, "committed-1", "committed-3") + assertNotActive(t, err) +} + +func testCommitReadonly(ctx context.Context, t *testing.T, ms storage.MetaStore) { + if err := basePopulate(ctx, ms); err != nil { + t.Fatalf("Populate failed: %+v", err) + } + _, err := ms.Commit(ctx, "active-5", "committed-3") + if err == nil { + t.Fatal("Expected error committing readonly active") + } +} + +func testRemove(ctx context.Context, t *testing.T, ms storage.MetaStore) { + a1, err := ms.CreateActive(ctx, "active-1", "", false) + if err != nil { + t.Fatal(err) + } + + commitID, err := ms.Commit(ctx, "active-1", "committed-1") + if err != nil { + t.Fatal(err) + } + if commitID != a1.ID { + t.Fatal("Snapshot identifier must not change on commit") + } + + a2, err := ms.CreateActive(ctx, "active-2", "committed-1", true) + if err != nil { + t.Fatal(err) + } + + a3, err := ms.CreateActive(ctx, "active-3", "committed-1", true) + if err != nil { + t.Fatal(err) + } + + _, _, err = ms.Remove(ctx, "active-1") + assertNotExist(t, err) + + r3, k3, err := ms.Remove(ctx, "active-3") + if err != nil { + t.Fatal(err) + } + if r3 != a3.ID { + t.Fatal("Expected remove ID to match create ID") + } + if k3 != snapshot.KindActive { + t.Fatal("Expected active kind, got %v", k3) + } + + r2, k2, err := ms.Remove(ctx, "active-2") + if err != nil { + t.Fatal(err) + } + if r2 != a2.ID { + t.Fatal("Expected remove ID to match create ID") + } + if k2 != snapshot.KindActive { + t.Fatal("Expected active kind, got %v", k2) + } + + r1, k1, err := ms.Remove(ctx, "committed-1") + if err != nil { + t.Fatal(err) + } + if r1 != commitID { + t.Fatal("Expected remove ID to match commit ID") + } + if k1 != snapshot.KindCommitted { + t.Fatal("Expected committed kind, got %v", k1) + } +} + +func testRemoveWithChildren(ctx context.Context, t *testing.T, ms storage.MetaStore) { + if err := basePopulate(ctx, ms); err != nil { + t.Fatalf("Populate failed: %+v", err) + } + _, _, err := ms.Remove(ctx, "committed-1") + if err == nil { + t.Fatalf("Expected removal of snapshot with children to error") + } + _, _, err = ms.Remove(ctx, "committed-1") + if err == nil { + t.Fatalf("Expected removal of snapshot with children to error") + } +} + +func testRemoveNotExist(ctx context.Context, t *testing.T, ms storage.MetaStore) { + _, _, err := ms.Remove(ctx, "does-not-exist") + assertNotExist(t, err) +} diff --git a/snapshot/testsuite/testsuite.go b/snapshot/testsuite/testsuite.go index 01d0aef..240de78 100644 --- a/snapshot/testsuite/testsuite.go +++ b/snapshot/testsuite/testsuite.go @@ -17,7 +17,7 @@ import ( ) // SnapshotterSuite runs a test suite on the snapshotter given a factory function. -func SnapshotterSuite(t *testing.T, name string, snapshotterFn func(root string) (snapshot.Snapshotter, func(), error)) { +func SnapshotterSuite(t *testing.T, name string, snapshotterFn func(ctx context.Context, root string) (snapshot.Snapshotter, func(), error)) { t.Run("Basic", makeTest(t, name, snapshotterFn, checkSnapshotterBasic)) t.Run("StatActive", makeTest(t, name, snapshotterFn, checkSnapshotterStatActive)) t.Run("StatComitted", makeTest(t, name, snapshotterFn, checkSnapshotterStatCommitted)) @@ -25,8 +25,9 @@ func SnapshotterSuite(t *testing.T, name string, snapshotterFn func(root string) t.Run("PreareViewFailingtest", makeTest(t, name, snapshotterFn, checkSnapshotterPrepareView)) } -func makeTest(t *testing.T, name string, snapshotterFn func(root string) (snapshot.Snapshotter, func(), error), fn func(t *testing.T, snapshotter snapshot.Snapshotter, work string)) func(t *testing.T) { +func makeTest(t *testing.T, name string, snapshotterFn func(ctx context.Context, root string) (snapshot.Snapshotter, func(), error), fn func(ctx context.Context, t *testing.T, snapshotter snapshot.Snapshotter, work string)) func(t *testing.T) { return func(t *testing.T) { + ctx := context.Background() oldumask := syscall.Umask(0) defer syscall.Umask(oldumask) // Make two directories: a snapshotter root and a play area for the tests: @@ -46,7 +47,7 @@ func makeTest(t *testing.T, name string, snapshotterFn func(root string) (snapsh t.Fatal(err) } - snapshotter, cleanup, err := snapshotterFn(root) + snapshotter, cleanup, err := snapshotterFn(ctx, root) if err != nil { t.Fatal(err) } @@ -58,14 +59,12 @@ func makeTest(t *testing.T, name string, snapshotterFn func(root string) (snapsh } defer testutil.DumpDir(t, tmpDir) - fn(t, snapshotter, work) + fn(ctx, t, snapshotter, work) } } // checkSnapshotterBasic tests the basic workflow of a snapshot snapshotter. -func checkSnapshotterBasic(t *testing.T, snapshotter snapshot.Snapshotter, work string) { - ctx := context.TODO() - +func checkSnapshotterBasic(ctx context.Context, t *testing.T, snapshotter snapshot.Snapshotter, work string) { initialApplier := fstest.Apply( fstest.CreateFile("/foo", []byte("foo\n"), 0777), fstest.CreateDir("/a", 0755), @@ -189,11 +188,15 @@ func checkSnapshotterBasic(t *testing.T, snapshotter snapshot.Snapshotter, work fstest.Apply(initialApplier, diffApplier)); err != nil { t.Fatalf("failure reason: %+v", err) } + + assert.NoError(t, snapshotter.Remove(ctx, nextnext)) + assert.Error(t, snapshotter.Remove(ctx, committed)) + assert.NoError(t, snapshotter.Remove(ctx, nextCommitted)) + assert.NoError(t, snapshotter.Remove(ctx, committed)) } // Create a New Layer on top of base layer with Prepare, Stat on new layer, should return Active layer. -func checkSnapshotterStatActive(t *testing.T, snapshotter snapshot.Snapshotter, work string) { - ctx := context.TODO() +func checkSnapshotterStatActive(ctx context.Context, t *testing.T, snapshotter snapshot.Snapshotter, work string) { preparing := filepath.Join(work, "preparing") if err := os.MkdirAll(preparing, 0777); err != nil { t.Fatal(err) @@ -227,8 +230,7 @@ func checkSnapshotterStatActive(t *testing.T, snapshotter snapshot.Snapshotter, } // Commit a New Layer on top of base layer with Prepare & Commit , Stat on new layer, should return Committed layer. -func checkSnapshotterStatCommitted(t *testing.T, snapshotter snapshot.Snapshotter, work string) { - ctx := context.TODO() +func checkSnapshotterStatCommitted(ctx context.Context, t *testing.T, snapshotter snapshot.Snapshotter, work string) { preparing := filepath.Join(work, "preparing") if err := os.MkdirAll(preparing, 0777); err != nil { t.Fatal(err) @@ -289,8 +291,7 @@ func snapshotterPrepareMount(ctx context.Context, snapshotter snapshot.Snapshott } // Given A <- B <- C, B is the parent of C and A is a transitive parent of C (in this case, a "grandparent") -func checkSnapshotterTransitivity(t *testing.T, snapshotter snapshot.Snapshotter, work string) { - ctx := context.TODO() +func checkSnapshotterTransitivity(ctx context.Context, t *testing.T, snapshotter snapshot.Snapshotter, work string) { preparing, err := snapshotterPrepareMount(ctx, snapshotter, "preparing", "", work) if err != nil { t.Fatal(err) @@ -344,9 +345,7 @@ func checkSnapshotterTransitivity(t *testing.T, snapshotter snapshot.Snapshotter } // Creating two layers with Prepare or View with same key must fail. -func checkSnapshotterPrepareView(t *testing.T, snapshotter snapshot.Snapshotter, work string) { - ctx := context.TODO() - +func checkSnapshotterPrepareView(ctx context.Context, t *testing.T, snapshotter snapshot.Snapshotter, work string) { preparing, err := snapshotterPrepareMount(ctx, snapshotter, "preparing", "", work) if err != nil { t.Fatal(err)