Merge pull request #635 from dmcgowan/snapshot-metastore-context
Snapshot driver metastore
This commit is contained in:
commit
44d2ef4db3
13 changed files with 2223 additions and 538 deletions
|
@ -2,16 +2,17 @@ package btrfs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
"github.com/docker/containerd"
|
"github.com/docker/containerd"
|
||||||
|
"github.com/docker/containerd/log"
|
||||||
"github.com/docker/containerd/plugin"
|
"github.com/docker/containerd/plugin"
|
||||||
"github.com/docker/containerd/snapshot"
|
"github.com/docker/containerd/snapshot"
|
||||||
|
"github.com/docker/containerd/snapshot/storage"
|
||||||
|
"github.com/docker/containerd/snapshot/storage/boltdb"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/stevvooe/go-btrfs"
|
"github.com/stevvooe/go-btrfs"
|
||||||
)
|
)
|
||||||
|
@ -25,11 +26,19 @@ func init() {
|
||||||
Type: plugin.SnapshotPlugin,
|
Type: plugin.SnapshotPlugin,
|
||||||
Config: &btrfsConfig{},
|
Config: &btrfsConfig{},
|
||||||
Init: func(ic *plugin.InitContext) (interface{}, error) {
|
Init: func(ic *plugin.InitContext) (interface{}, error) {
|
||||||
|
root := filepath.Join(ic.Root, "snapshot", "btrfs")
|
||||||
conf := ic.Config.(*btrfsConfig)
|
conf := ic.Config.(*btrfsConfig)
|
||||||
if conf.Device == "" {
|
if conf.Device == "" {
|
||||||
|
// TODO: check device for root
|
||||||
return nil, errors.Errorf("btrfs requires \"device\" configuration")
|
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 {
|
type Snapshotter struct {
|
||||||
device string // maybe we can resolve it with path?
|
device string // maybe we can resolve it with path?
|
||||||
root string // root provides paths for internal storage.
|
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 (
|
var (
|
||||||
active = filepath.Join(root, "active")
|
active = filepath.Join(root, "active")
|
||||||
snapshots = filepath.Join(root, "snapshots")
|
snapshots = filepath.Join(root, "snapshots")
|
||||||
parents = filepath.Join(root, "parents")
|
|
||||||
index = filepath.Join(root, "index")
|
|
||||||
names = filepath.Join(root, "names")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
for _, path := range []string{
|
for _, path := range []string{
|
||||||
active,
|
active,
|
||||||
snapshots,
|
snapshots,
|
||||||
parents,
|
|
||||||
index,
|
|
||||||
names,
|
|
||||||
} {
|
} {
|
||||||
if err := os.MkdirAll(path, 0755); err != nil {
|
if err := os.MkdirAll(path, 0755); err != nil {
|
||||||
return nil, err
|
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
|
// 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
|
// Should be used for parent resolution, existence checks and to discern
|
||||||
// the kind of snapshot.
|
// the kind of snapshot.
|
||||||
func (b *Snapshotter) Stat(ctx context.Context, key string) (snapshot.Info, error) {
|
func (b *Snapshotter) Stat(ctx context.Context, key string) (snapshot.Info, error) {
|
||||||
// resolve the snapshot out of the index.
|
ctx, t, err := b.ms.TransactionContext(ctx, false)
|
||||||
target, err := os.Readlink(filepath.Join(b.root, "index", hash(key)))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !os.IsNotExist(err) {
|
return snapshot.Info{}, err
|
||||||
return snapshot.Info{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return snapshot.Info{}, errors.Errorf("snapshot %v not found", key)
|
|
||||||
}
|
}
|
||||||
|
defer t.Rollback()
|
||||||
return b.stat(target)
|
return b.ms.Stat(ctx, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Snapshotter) stat(target string) (snapshot.Info, error) {
|
// Walk the committed snapshots.
|
||||||
var (
|
func (b *Snapshotter) Walk(ctx context.Context, fn func(context.Context, snapshot.Info) error) error {
|
||||||
parents = filepath.Join(b.root, "parents")
|
ctx, t, err := b.ms.TransactionContext(ctx, false)
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return snapshot.Info{}, err
|
return err
|
||||||
}
|
}
|
||||||
|
defer t.Rollback()
|
||||||
// read the name out of the names!
|
return b.ms.Walk(ctx, fn)
|
||||||
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
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Snapshotter) Prepare(ctx context.Context, key, parent string) ([]containerd.Mount, error) {
|
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) {
|
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) {
|
func (b *Snapshotter) makeActive(ctx context.Context, key, parent string, readonly bool) ([]containerd.Mount, error) {
|
||||||
var (
|
ctx, t, err := b.ms.TransactionContext(ctx, true)
|
||||||
active = filepath.Join(b.root, "active")
|
if err != nil {
|
||||||
snapshots = filepath.Join(b.root, "snapshots")
|
return nil, err
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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
|
a, err := b.ms.CreateActive(ctx, key, parent, readonly)
|
||||||
if err := ioutil.WriteFile(namep, []byte(key), 0644); err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -218,82 +174,43 @@ func (b *Snapshotter) mounts(dir string) ([]containerd.Mount, error) {
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Snapshotter) Commit(ctx context.Context, name, key string) error {
|
func (b *Snapshotter) Commit(ctx context.Context, name, key string) (err error) {
|
||||||
var (
|
ctx, t, err := b.ms.TransactionContext(ctx, true)
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 {
|
id, err := b.ms.Commit(ctx, key, name)
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !os.IsNotExist(err) {
|
return errors.Wrap(err, "failed to commit")
|
||||||
return err
|
}
|
||||||
|
|
||||||
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := btrfs.SubvolSnapshot(target, dir, true); err != nil {
|
if derr := btrfs.SubvolDelete(source); derr != nil {
|
||||||
return err
|
// 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")
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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.
|
// This can be used to recover mounts after calling View or Prepare.
|
||||||
func (b *Snapshotter) Mounts(ctx context.Context, key string) ([]containerd.Mount, error) {
|
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)
|
return b.mounts(dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove abandons the transaction identified by key. All resources
|
// Remove abandons the transaction identified by key. All resources
|
||||||
// associated with the key will be removed.
|
// associated with the key will be removed.
|
||||||
func (b *Snapshotter) Remove(ctx context.Context, key string) error {
|
func (b *Snapshotter) Remove(ctx context.Context, key string) (err error) {
|
||||||
panic("not implemented")
|
var (
|
||||||
}
|
source, removed string
|
||||||
|
readonly bool
|
||||||
|
)
|
||||||
|
|
||||||
// Walk the committed snapshots.
|
ctx, t, err := b.ms.TransactionContext(ctx, true)
|
||||||
func (b *Snapshotter) Walk(ctx context.Context, fn func(context.Context, snapshot.Info) error) error {
|
if err != nil {
|
||||||
// TODO(stevvooe): Copy-pasted almost verbatim from overlay. Really need to
|
return err
|
||||||
// unify the metadata for snapshot implementations.
|
}
|
||||||
root := filepath.Join(b.root, "index")
|
defer func() {
|
||||||
return filepath.Walk(root, func(path string, fi os.FileInfo, err error) error {
|
if err != nil && t != nil {
|
||||||
if err != nil {
|
if rerr := t.Rollback(); rerr != nil {
|
||||||
return err
|
log.G(ctx).WithError(rerr).Warn("Failure rolling back transaction")
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
if err != nil {
|
||||||
|
source = ""
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := fn(ctx, si); err != nil {
|
readonly = info.Readonly
|
||||||
return err
|
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
|
return nil
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func hash(k string) string {
|
|
||||||
return fmt.Sprintf("%x", sha256.Sum224([]byte(k)))
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
|
|
||||||
"github.com/docker/containerd"
|
"github.com/docker/containerd"
|
||||||
"github.com/docker/containerd/snapshot"
|
"github.com/docker/containerd/snapshot"
|
||||||
|
"github.com/docker/containerd/snapshot/storage/boltdb"
|
||||||
"github.com/docker/containerd/snapshot/testsuite"
|
"github.com/docker/containerd/snapshot/testsuite"
|
||||||
"github.com/docker/containerd/testutil"
|
"github.com/docker/containerd/testutil"
|
||||||
)
|
)
|
||||||
|
@ -19,11 +20,14 @@ const (
|
||||||
mib = 1024 * 1024
|
mib = 1024 * 1024
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBtrfs(t *testing.T) {
|
func boltSnapshotter(t *testing.T) func(context.Context, string) (snapshot.Snapshotter, func(), error) {
|
||||||
testutil.RequiresRoot(t)
|
return func(ctx context.Context, root string) (snapshot.Snapshotter, func(), error) {
|
||||||
testsuite.SnapshotterSuite(t, "Btrfs", func(root string) (snapshot.Snapshotter, func(), error) {
|
|
||||||
device := setupBtrfsLoopbackDevice(t, root)
|
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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -31,34 +35,39 @@ func TestBtrfs(t *testing.T) {
|
||||||
return snapshotter, func() {
|
return snapshotter, func() {
|
||||||
device.remove(t)
|
device.remove(t)
|
||||||
}, nil
|
}, nil
|
||||||
})
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBtrfs(t *testing.T) {
|
||||||
|
testutil.RequiresRoot(t)
|
||||||
|
testsuite.SnapshotterSuite(t, "Btrfs", boltSnapshotter(t))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBtrfsMounts(t *testing.T) {
|
func TestBtrfsMounts(t *testing.T) {
|
||||||
testutil.RequiresRoot(t)
|
testutil.RequiresRoot(t)
|
||||||
ctx := context.TODO()
|
ctx := context.Background()
|
||||||
|
|
||||||
// create temporary directory for mount point
|
// create temporary directory for mount point
|
||||||
mountPoint, err := ioutil.TempDir("", "containerd-btrfs-test")
|
mountPoint, err := ioutil.TempDir("", "containerd-btrfs-test")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal("could not create mount point for btrfs test", err)
|
t.Fatal("could not create mount point for btrfs test", err)
|
||||||
}
|
}
|
||||||
|
defer os.RemoveAll(mountPoint)
|
||||||
t.Log("temporary mount point created", mountPoint)
|
t.Log("temporary mount point created", mountPoint)
|
||||||
|
|
||||||
device := setupBtrfsLoopbackDevice(t, mountPoint)
|
root, err := ioutil.TempDir(mountPoint, "TestBtrfsPrepare-")
|
||||||
defer device.remove(t)
|
|
||||||
|
|
||||||
root, err := ioutil.TempDir(device.mountPoint, "TestBtrfsPrepare-")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(root)
|
defer os.RemoveAll(root)
|
||||||
|
|
||||||
target := filepath.Join(root, "test")
|
b, c, err := boltSnapshotter(t)(ctx, root)
|
||||||
b, err := NewSnapshotter(device.deviceName, root)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
defer c()
|
||||||
|
|
||||||
|
target := filepath.Join(root, "test")
|
||||||
mounts, err := b.Prepare(ctx, target, "")
|
mounts, err := b.Prepare(ctx, target, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|
44
snapshot/errors.go
Normal file
44
snapshot/errors.go
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -7,12 +7,13 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/docker/containerd"
|
"github.com/docker/containerd"
|
||||||
|
"github.com/docker/containerd/log"
|
||||||
"github.com/docker/containerd/plugin"
|
"github.com/docker/containerd/plugin"
|
||||||
"github.com/docker/containerd/snapshot"
|
"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"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -20,32 +21,40 @@ func init() {
|
||||||
plugin.Register("snapshot-overlay", &plugin.Registration{
|
plugin.Register("snapshot-overlay", &plugin.Registration{
|
||||||
Type: plugin.SnapshotPlugin,
|
Type: plugin.SnapshotPlugin,
|
||||||
Init: func(ic *plugin.InitContext) (interface{}, error) {
|
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 {
|
type Snapshotter struct {
|
||||||
root string
|
root string
|
||||||
links *cache
|
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 {
|
if err := os.MkdirAll(root, 0700); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for _, p := range []string{
|
|
||||||
"committed", // committed snapshots
|
if err := os.MkdirAll(filepath.Join(root, "snapshots"), 0700); err != nil {
|
||||||
"active", // active snapshots
|
return nil, err
|
||||||
"index", // snapshots by hashed name
|
|
||||||
} {
|
|
||||||
if err := os.MkdirAll(filepath.Join(root, p), 0700); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Snapshotter{
|
return &Snapshotter{
|
||||||
root: root,
|
root: root,
|
||||||
links: newCache(),
|
ms: ms,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,86 +64,20 @@ func NewSnapshotter(root string) (snapshot.Snapshotter, error) {
|
||||||
// Should be used for parent resolution, existence checks and to discern
|
// Should be used for parent resolution, existence checks and to discern
|
||||||
// the kind of snapshot.
|
// the kind of snapshot.
|
||||||
func (o *Snapshotter) Stat(ctx context.Context, key string) (snapshot.Info, error) {
|
func (o *Snapshotter) Stat(ctx context.Context, key string) (snapshot.Info, error) {
|
||||||
path, err := o.links.get(filepath.Join(o.root, "index", hash(key)))
|
ctx, t, err := o.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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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"))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return snapshot.Info{}, err
|
return snapshot.Info{}, err
|
||||||
}
|
}
|
||||||
|
defer t.Rollback()
|
||||||
var parent string
|
return o.ms.Stat(ctx, key)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *Snapshotter) Prepare(ctx context.Context, key, parent string) ([]containerd.Mount, error) {
|
func (o *Snapshotter) Prepare(ctx context.Context, key, parent string) ([]containerd.Mount, error) {
|
||||||
active, err := o.newActiveDir(key, false)
|
return o.createActive(ctx, key, parent, false)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if parent != "" {
|
|
||||||
if err := active.setParent(parent); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return active.mounts(o.links)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *Snapshotter) View(ctx context.Context, key, parent string) ([]containerd.Mount, error) {
|
func (o *Snapshotter) View(ctx context.Context, key, parent string) ([]containerd.Mount, error) {
|
||||||
active, err := o.newActiveDir(key, true)
|
return o.createActive(ctx, key, parent, true)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if parent != "" {
|
|
||||||
if err := active.setParent(parent); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return active.mounts(o.links)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mounts returns the mounts for the transaction identified by key. Can be
|
// 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.
|
// This can be used to recover mounts after calling View or Prepare.
|
||||||
func (o *Snapshotter) Mounts(ctx context.Context, key string) ([]containerd.Mount, error) {
|
func (o *Snapshotter) Mounts(ctx context.Context, key string) ([]containerd.Mount, error) {
|
||||||
active := o.getActive(key)
|
ctx, t, err := o.ms.TransactionContext(ctx, false)
|
||||||
return active.mounts(o.links)
|
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 {
|
func (o *Snapshotter) Commit(ctx context.Context, name, key string) error {
|
||||||
active := o.getActive(key)
|
ctx, t, err := o.ms.TransactionContext(ctx, true)
|
||||||
return active.commit(name, o.links)
|
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
|
// Remove abandons the transaction identified by key. All resources
|
||||||
// associated with the key will be removed.
|
// associated with the key will be removed.
|
||||||
func (o *Snapshotter) Remove(ctx context.Context, key string) error {
|
func (o *Snapshotter) Remove(ctx context.Context, key string) (err error) {
|
||||||
panic("not implemented")
|
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.
|
// Walk the committed snapshots.
|
||||||
func (o *Snapshotter) Walk(ctx context.Context, fn func(context.Context, snapshot.Info) error) error {
|
func (o *Snapshotter) Walk(ctx context.Context, fn func(context.Context, snapshot.Info) error) error {
|
||||||
root := filepath.Join(o.root, "index")
|
ctx, t, err := o.ms.TransactionContext(ctx, false)
|
||||||
return filepath.Walk(root, func(path string, fi os.FileInfo, err error) error {
|
if err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
return err
|
}
|
||||||
}
|
defer t.Rollback()
|
||||||
|
return o.ms.Walk(ctx, fn)
|
||||||
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
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 (
|
var (
|
||||||
path = filepath.Join(o.root, "active", hash(key))
|
path string
|
||||||
name = filepath.Join(path, "name")
|
snapshotDir = filepath.Join(o.root, "snapshots")
|
||||||
indexlink = filepath.Join(o.root, "index", hash(key))
|
|
||||||
)
|
)
|
||||||
a := &activeDir{
|
|
||||||
path: path,
|
td, err := ioutil.TempDir(snapshotDir, "new-")
|
||||||
committedDir: filepath.Join(o.root, "committed"),
|
if err != nil {
|
||||||
indexlink: indexlink,
|
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 {
|
if !readonly {
|
||||||
for _, p := range []string{
|
if err = os.MkdirAll(filepath.Join(td, "work"), 0700); err != nil {
|
||||||
"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()
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ioutil.WriteFile(name, []byte(key), 0644); err != nil {
|
ctx, t, err := o.ms.TransactionContext(ctx, true)
|
||||||
a.delete()
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// link from namespace
|
active, err := o.ms.CreateActive(ctx, key, parent, readonly)
|
||||||
if err := os.Symlink(path, indexlink); err != nil {
|
if err != nil {
|
||||||
a.delete()
|
if rerr := t.Rollback(); rerr != nil {
|
||||||
return nil, err
|
log.G(ctx).WithError(rerr).Warn("Failure rolling back transaction")
|
||||||
}
|
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
return err
|
return nil, errors.Wrap(err, "failed to create active")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(stevvooe): This doesn't quite meet the current model. The new model
|
path = filepath.Join(snapshotDir, active.ID)
|
||||||
// is to copy all of this out and let the transaction continue. We don't
|
if err := os.Rename(td, path); err != nil {
|
||||||
// really have tests for it yet, but this will be the spot to fix it.
|
if rerr := t.Rollback(); rerr != nil {
|
||||||
//
|
log.G(ctx).WithError(rerr).Warn("Failure rolling back transaction")
|
||||||
// Nothing should be removed until remove is called on the active
|
}
|
||||||
// transaction.
|
return nil, errors.Wrap(err, "failed to rename")
|
||||||
if err := os.RemoveAll(filepath.Join(a.path, "work")); err != nil {
|
}
|
||||||
return err
|
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 o.mounts(active), 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *activeDir) mounts(c *cache) ([]containerd.Mount, error) {
|
func (o *Snapshotter) mounts(active storage.Active) []containerd.Mount {
|
||||||
var (
|
if len(active.ParentIDs) == 0 {
|
||||||
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 {
|
|
||||||
// if we only have one layer/no parents then just return a bind mount as overlay
|
// if we only have one layer/no parents then just return a bind mount as overlay
|
||||||
// will not work
|
// will not work
|
||||||
roFlag := "rw"
|
roFlag := "rw"
|
||||||
if _, err := os.Stat(filepath.Join(a.path, "work")); err != nil {
|
if active.Readonly {
|
||||||
if !os.IsNotExist(err) {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
roFlag = "ro"
|
roFlag = "ro"
|
||||||
}
|
}
|
||||||
|
|
||||||
return []containerd.Mount{
|
return []containerd.Mount{
|
||||||
{
|
{
|
||||||
Source: filepath.Join(a.path, "fs"),
|
Source: o.upperPath(active.ID),
|
||||||
Type: "bind",
|
Type: "bind",
|
||||||
Options: []string{
|
Options: []string{
|
||||||
roFlag,
|
roFlag,
|
||||||
"rbind",
|
"rbind",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, nil
|
}
|
||||||
}
|
}
|
||||||
var options []string
|
var options []string
|
||||||
|
|
||||||
if _, err := os.Stat(filepath.Join(a.path, "work")); err == nil {
|
if !active.Readonly {
|
||||||
options = append(options,
|
options = append(options,
|
||||||
fmt.Sprintf("workdir=%s", filepath.Join(a.path, "work")),
|
fmt.Sprintf("workdir=%s", o.workPath(active.ID)),
|
||||||
fmt.Sprintf("upperdir=%s", filepath.Join(a.path, "fs")),
|
fmt.Sprintf("upperdir=%s", o.upperPath(active.ID)),
|
||||||
)
|
)
|
||||||
} else if !os.IsNotExist(err) {
|
} else if len(active.ParentIDs) == 1 {
|
||||||
return nil, err
|
|
||||||
} else if len(parents) == 1 {
|
|
||||||
return []containerd.Mount{
|
return []containerd.Mount{
|
||||||
{
|
{
|
||||||
Source: parents[0],
|
Source: o.upperPath(active.ParentIDs[0]),
|
||||||
Type: "bind",
|
Type: "bind",
|
||||||
Options: []string{
|
Options: []string{
|
||||||
"ro",
|
"ro",
|
||||||
"rbind",
|
"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{
|
return []containerd.Mount{
|
||||||
{
|
{
|
||||||
Type: "overlay",
|
Type: "overlay",
|
||||||
Source: "overlay",
|
Source: "overlay",
|
||||||
Options: options,
|
Options: options,
|
||||||
},
|
},
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newCache() *cache {
|
|
||||||
return &cache{
|
|
||||||
links: make(map[string]string),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type cache struct {
|
func (o *Snapshotter) upperPath(id string) string {
|
||||||
mu sync.Mutex
|
return filepath.Join(o.root, "snapshots", id, "fs")
|
||||||
links map[string]string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cache) get(path string) (string, error) {
|
func (o *Snapshotter) workPath(id string) string {
|
||||||
c.mu.Lock()
|
return filepath.Join(o.root, "snapshots", id, "work")
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,20 +11,27 @@ import (
|
||||||
|
|
||||||
"github.com/docker/containerd"
|
"github.com/docker/containerd"
|
||||||
"github.com/docker/containerd/snapshot"
|
"github.com/docker/containerd/snapshot"
|
||||||
|
"github.com/docker/containerd/snapshot/storage/boltdb"
|
||||||
"github.com/docker/containerd/snapshot/testsuite"
|
"github.com/docker/containerd/snapshot/testsuite"
|
||||||
"github.com/docker/containerd/testutil"
|
"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) {
|
func TestOverlay(t *testing.T) {
|
||||||
testutil.RequiresRoot(t)
|
testutil.RequiresRoot(t)
|
||||||
testsuite.SnapshotterSuite(t, "Overlay", func(root string) (snapshot.Snapshotter, func(), error) {
|
testsuite.SnapshotterSuite(t, "Overlay", boltSnapshotter)
|
||||||
snapshotter, err := NewSnapshotter(root)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return snapshotter, func() {}, nil
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOverlayMounts(t *testing.T) {
|
func TestOverlayMounts(t *testing.T) {
|
||||||
|
@ -34,7 +41,7 @@ func TestOverlayMounts(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(root)
|
defer os.RemoveAll(root)
|
||||||
o, err := NewSnapshotter(root)
|
o, _, err := boltSnapshotter(ctx, root)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
return
|
return
|
||||||
|
@ -51,7 +58,7 @@ func TestOverlayMounts(t *testing.T) {
|
||||||
if m.Type != "bind" {
|
if m.Type != "bind" {
|
||||||
t.Errorf("mount type should be bind but received %q", m.Type)
|
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 {
|
if m.Source != expected {
|
||||||
t.Errorf("expected source %q but received %q", expected, m.Source)
|
t.Errorf("expected source %q but received %q", expected, m.Source)
|
||||||
}
|
}
|
||||||
|
@ -70,7 +77,7 @@ func TestOverlayCommit(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(root)
|
defer os.RemoveAll(root)
|
||||||
o, err := NewSnapshotter(root)
|
o, _, err := boltSnapshotter(ctx, root)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
return
|
return
|
||||||
|
@ -99,7 +106,7 @@ func TestOverlayOverlayMount(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(root)
|
defer os.RemoveAll(root)
|
||||||
o, err := NewSnapshotter(root)
|
o, _, err := boltSnapshotter(ctx, root)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
return
|
return
|
||||||
|
@ -129,11 +136,10 @@ func TestOverlayOverlayMount(t *testing.T) {
|
||||||
t.Errorf("expected source %q but received %q", "overlay", m.Source)
|
t.Errorf("expected source %q but received %q", "overlay", m.Source)
|
||||||
}
|
}
|
||||||
var (
|
var (
|
||||||
ah = hash("/tmp/layer2")
|
bp = getBasePath(ctx, o, root, "/tmp/layer2")
|
||||||
sh = hash("base")
|
work = "workdir=" + filepath.Join(bp, "work")
|
||||||
work = "workdir=" + filepath.Join(root, "active", ah, "work")
|
upper = "upperdir=" + filepath.Join(bp, "fs")
|
||||||
upper = "upperdir=" + filepath.Join(root, "active", ah, "fs")
|
lower = "lowerdir=" + getParents(ctx, o, root, "/tmp/layer2")[0]
|
||||||
lower = "lowerdir=" + filepath.Join(root, "committed", sh, "fs")
|
|
||||||
)
|
)
|
||||||
for i, v := range []string{
|
for i, v := range []string{
|
||||||
work,
|
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) {
|
func TestOverlayOverlayRead(t *testing.T) {
|
||||||
testutil.RequiresRoot(t)
|
testutil.RequiresRoot(t)
|
||||||
ctx := context.TODO()
|
ctx := context.TODO()
|
||||||
|
@ -154,7 +194,7 @@ func TestOverlayOverlayRead(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(root)
|
defer os.RemoveAll(root)
|
||||||
o, err := NewSnapshotter(root)
|
o, _, err := boltSnapshotter(ctx, root)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
return
|
return
|
||||||
|
@ -206,7 +246,7 @@ func TestOverlayView(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(root)
|
defer os.RemoveAll(root)
|
||||||
o, err := NewSnapshotter(root)
|
o, _, err := boltSnapshotter(ctx, root)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -228,7 +268,7 @@ func TestOverlayView(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
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)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := o.Commit(ctx, "top", key); err != nil {
|
if err := o.Commit(ctx, "top", key); err != nil {
|
||||||
|
@ -246,7 +286,7 @@ func TestOverlayView(t *testing.T) {
|
||||||
if m.Type != "bind" {
|
if m.Type != "bind" {
|
||||||
t.Errorf("mount type should be bind but received %q", m.Type)
|
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 {
|
if m.Source != expected {
|
||||||
t.Errorf("expected source %q but received %q", expected, m.Source)
|
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 {
|
if len(m.Options) != 1 {
|
||||||
t.Errorf("expected 1 mount option but got %d", len(m.Options))
|
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 {
|
if m.Options[0] != expected {
|
||||||
t.Errorf("expected option %q but received %q", expected, m.Options[0])
|
t.Errorf("expected option %q but received %q", expected, m.Options[0])
|
||||||
}
|
}
|
||||||
|
|
396
snapshot/storage/boltdb/bolt.go
Normal file
396
snapshot/storage/boltdb/bolt.go
Normal file
|
@ -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
|
||||||
|
}
|
25
snapshot/storage/boltdb/bolt_test.go
Normal file
25
snapshot/storage/boltdb/bolt_test.go
Normal file
|
@ -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"))
|
||||||
|
})
|
||||||
|
}
|
468
snapshot/storage/boltdb/record.pb.go
Normal file
468
snapshot/storage/boltdb/record.pb.go
Normal file
|
@ -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,
|
||||||
|
}
|
24
snapshot/storage/boltdb/record.proto
Normal file
24
snapshot/storage/boltdb/record.proto
Normal file
|
@ -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;
|
||||||
|
}
|
69
snapshot/storage/metastore.go
Normal file
69
snapshot/storage/metastore.go
Normal file
|
@ -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
|
||||||
|
}
|
217
snapshot/storage/testsuite/bench.go
Normal file
217
snapshot/storage/testsuite/bench.go
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
552
snapshot/storage/testsuite/testsuite.go
Normal file
552
snapshot/storage/testsuite/testsuite.go
Normal file
|
@ -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)
|
||||||
|
}
|
|
@ -17,7 +17,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// SnapshotterSuite runs a test suite on the snapshotter given a factory function.
|
// 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("Basic", makeTest(t, name, snapshotterFn, checkSnapshotterBasic))
|
||||||
t.Run("StatActive", makeTest(t, name, snapshotterFn, checkSnapshotterStatActive))
|
t.Run("StatActive", makeTest(t, name, snapshotterFn, checkSnapshotterStatActive))
|
||||||
t.Run("StatComitted", makeTest(t, name, snapshotterFn, checkSnapshotterStatCommitted))
|
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))
|
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) {
|
return func(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
oldumask := syscall.Umask(0)
|
oldumask := syscall.Umask(0)
|
||||||
defer syscall.Umask(oldumask)
|
defer syscall.Umask(oldumask)
|
||||||
// Make two directories: a snapshotter root and a play area for the tests:
|
// 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)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
snapshotter, cleanup, err := snapshotterFn(root)
|
snapshotter, cleanup, err := snapshotterFn(ctx, root)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -58,14 +59,12 @@ func makeTest(t *testing.T, name string, snapshotterFn func(root string) (snapsh
|
||||||
}
|
}
|
||||||
|
|
||||||
defer testutil.DumpDir(t, tmpDir)
|
defer testutil.DumpDir(t, tmpDir)
|
||||||
fn(t, snapshotter, work)
|
fn(ctx, t, snapshotter, work)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkSnapshotterBasic tests the basic workflow of a snapshot snapshotter.
|
// checkSnapshotterBasic tests the basic workflow of a snapshot snapshotter.
|
||||||
func checkSnapshotterBasic(t *testing.T, snapshotter snapshot.Snapshotter, work string) {
|
func checkSnapshotterBasic(ctx context.Context, t *testing.T, snapshotter snapshot.Snapshotter, work string) {
|
||||||
ctx := context.TODO()
|
|
||||||
|
|
||||||
initialApplier := fstest.Apply(
|
initialApplier := fstest.Apply(
|
||||||
fstest.CreateFile("/foo", []byte("foo\n"), 0777),
|
fstest.CreateFile("/foo", []byte("foo\n"), 0777),
|
||||||
fstest.CreateDir("/a", 0755),
|
fstest.CreateDir("/a", 0755),
|
||||||
|
@ -189,11 +188,15 @@ func checkSnapshotterBasic(t *testing.T, snapshotter snapshot.Snapshotter, work
|
||||||
fstest.Apply(initialApplier, diffApplier)); err != nil {
|
fstest.Apply(initialApplier, diffApplier)); err != nil {
|
||||||
t.Fatalf("failure reason: %+v", err)
|
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.
|
// 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) {
|
func checkSnapshotterStatActive(ctx context.Context, t *testing.T, snapshotter snapshot.Snapshotter, work string) {
|
||||||
ctx := context.TODO()
|
|
||||||
preparing := filepath.Join(work, "preparing")
|
preparing := filepath.Join(work, "preparing")
|
||||||
if err := os.MkdirAll(preparing, 0777); err != nil {
|
if err := os.MkdirAll(preparing, 0777); err != nil {
|
||||||
t.Fatal(err)
|
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.
|
// 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) {
|
func checkSnapshotterStatCommitted(ctx context.Context, t *testing.T, snapshotter snapshot.Snapshotter, work string) {
|
||||||
ctx := context.TODO()
|
|
||||||
preparing := filepath.Join(work, "preparing")
|
preparing := filepath.Join(work, "preparing")
|
||||||
if err := os.MkdirAll(preparing, 0777); err != nil {
|
if err := os.MkdirAll(preparing, 0777); err != nil {
|
||||||
t.Fatal(err)
|
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")
|
// 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) {
|
func checkSnapshotterTransitivity(ctx context.Context, t *testing.T, snapshotter snapshot.Snapshotter, work string) {
|
||||||
ctx := context.TODO()
|
|
||||||
preparing, err := snapshotterPrepareMount(ctx, snapshotter, "preparing", "", work)
|
preparing, err := snapshotterPrepareMount(ctx, snapshotter, "preparing", "", work)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
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.
|
// Creating two layers with Prepare or View with same key must fail.
|
||||||
func checkSnapshotterPrepareView(t *testing.T, snapshotter snapshot.Snapshotter, work string) {
|
func checkSnapshotterPrepareView(ctx context.Context, t *testing.T, snapshotter snapshot.Snapshotter, work string) {
|
||||||
ctx := context.TODO()
|
|
||||||
|
|
||||||
preparing, err := snapshotterPrepareMount(ctx, snapshotter, "preparing", "", work)
|
preparing, err := snapshotterPrepareMount(ctx, snapshotter, "preparing", "", work)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|
Loading…
Reference in a new issue