rename snapshot->snapshots pkg

Signed-off-by: Jess Valarezo <valarezo.jessica@gmail.com>
This commit is contained in:
Jess Valarezo
2017-11-22 15:53:16 -05:00
parent 61c8fe2307
commit 9885edfc44
39 changed files with 367 additions and 367 deletions

377
snapshots/btrfs/btrfs.go Normal file
View File

@@ -0,0 +1,377 @@
// +build linux,!no_btrfs
package btrfs
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/containerd/btrfs"
"github.com/containerd/containerd/log"
"github.com/containerd/containerd/mount"
"github.com/containerd/containerd/platforms"
"github.com/containerd/containerd/plugin"
"github.com/containerd/containerd/snapshots"
"github.com/containerd/containerd/snapshots/storage"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
func init() {
plugin.Register(&plugin.Registration{
ID: "btrfs",
Type: plugin.SnapshotPlugin,
InitFn: func(ic *plugin.InitContext) (interface{}, error) {
ic.Meta.Platforms = []ocispec.Platform{platforms.DefaultSpec()}
ic.Meta.Exports = map[string]string{"root": ic.Root}
return NewSnapshotter(ic.Root)
},
})
}
type snapshotter struct {
device string // device of the root
root string // root provides paths for internal storage.
ms *storage.MetaStore
}
// NewSnapshotter returns a Snapshotter using btrfs. Uses the provided
// root directory for snapshots and stores the metadata in
// a file in the provided root.
// root needs to be a mount point of btrfs.
func NewSnapshotter(root string) (snapshots.Snapshotter, error) {
// If directory does not exist, create it
if _, err := os.Stat(root); err != nil {
if !os.IsNotExist(err) {
return nil, err
}
if err := os.Mkdir(root, 0755); err != nil {
return nil, err
}
}
mnt, err := mount.Lookup(root)
if err != nil {
return nil, err
}
if mnt.FSType != "btrfs" {
return nil, fmt.Errorf("path %s must be a btrfs filesystem to be used with the btrfs snapshotter", root)
}
var (
active = filepath.Join(root, "active")
view = filepath.Join(root, "view")
snapshots = filepath.Join(root, "snapshots")
)
for _, path := range []string{
active,
view,
snapshots,
} {
if err := os.Mkdir(path, 0755); err != nil && !os.IsExist(err) {
return nil, err
}
}
ms, err := storage.NewMetaStore(filepath.Join(root, "metadata.db"))
if err != nil {
return nil, err
}
return &snapshotter{
device: mnt.Source,
root: root,
ms: ms,
}, nil
}
// Stat returns the info for an active or committed snapshot by name or
// key.
//
// Should be used for parent resolution, existence checks and to discern
// the kind of snapshot.
func (b *snapshotter) Stat(ctx context.Context, key string) (snapshots.Info, error) {
ctx, t, err := b.ms.TransactionContext(ctx, false)
if err != nil {
return snapshots.Info{}, err
}
defer t.Rollback()
_, info, _, err := storage.GetInfo(ctx, key)
if err != nil {
return snapshots.Info{}, err
}
return info, nil
}
func (b *snapshotter) Update(ctx context.Context, info snapshots.Info, fieldpaths ...string) (snapshots.Info, error) {
ctx, t, err := b.ms.TransactionContext(ctx, true)
if err != nil {
return snapshots.Info{}, err
}
info, err = storage.UpdateInfo(ctx, info, fieldpaths...)
if err != nil {
t.Rollback()
return snapshots.Info{}, err
}
if err := t.Commit(); err != nil {
return snapshots.Info{}, err
}
return info, nil
}
// Usage retrieves the disk usage of the top-level snapshot.
func (b *snapshotter) Usage(ctx context.Context, key string) (snapshots.Usage, error) {
panic("not implemented")
// TODO(stevvooe): Btrfs has a quota model where data can be exclusive to a
// snapshot or shared among other resources. We may find that this is the
// correct value to reoprt but the stability of the implementation is under
// question.
//
// In general, this has impact on the model we choose for reporting usage.
// Ideally, the value should allow aggregration. For overlay, this is
// simple since we can scan the diff directory to get a unique value. This
// breaks down when start looking the behavior when data is shared between
// snapshots, such as that for btrfs.
}
// Walk the committed snapshots.
func (b *snapshotter) Walk(ctx context.Context, fn func(context.Context, snapshots.Info) error) error {
ctx, t, err := b.ms.TransactionContext(ctx, false)
if err != nil {
return err
}
defer t.Rollback()
return storage.WalkInfo(ctx, fn)
}
func (b *snapshotter) Prepare(ctx context.Context, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) {
return b.makeSnapshot(ctx, snapshots.KindActive, key, parent, opts)
}
func (b *snapshotter) View(ctx context.Context, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) {
return b.makeSnapshot(ctx, snapshots.KindView, key, parent, opts)
}
func (b *snapshotter) makeSnapshot(ctx context.Context, kind snapshots.Kind, key, parent string, opts []snapshots.Opt) ([]mount.Mount, error) {
ctx, t, err := b.ms.TransactionContext(ctx, true)
if err != nil {
return nil, err
}
defer func() {
if err != nil && t != nil {
if rerr := t.Rollback(); rerr != nil {
log.G(ctx).WithError(rerr).Warn("Failure rolling back transaction")
}
}
}()
s, err := storage.CreateSnapshot(ctx, kind, key, parent, opts...)
if err != nil {
return nil, err
}
target := filepath.Join(b.root, strings.ToLower(s.Kind.String()), s.ID)
if len(s.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", s.ParentIDs[0])
var readonly bool
if kind == snapshots.KindView {
readonly = true
}
// btrfs subvolume snapshot /parent /subvol
if err = btrfs.SubvolSnapshot(target, parentp, 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 b.mounts(target, s)
}
func (b *snapshotter) mounts(dir string, s storage.Snapshot) ([]mount.Mount, error) {
var options []string
// get the subvolume id back out for the mount
sid, err := btrfs.SubvolID(dir)
if err != nil {
return nil, err
}
options = append(options, fmt.Sprintf("subvolid=%d", sid))
if s.Kind != snapshots.KindActive {
options = append(options, "ro")
}
return []mount.Mount{
{
Type: "btrfs",
Source: b.device,
// NOTE(stevvooe): While it would be nice to use to uuids for
// mounts, they don't work reliably if the uuids are missing.
Options: options,
},
}, nil
}
func (b *snapshotter) Commit(ctx context.Context, name, key string, opts ...snapshots.Opt) (err error) {
ctx, t, err := b.ms.TransactionContext(ctx, true)
if err != nil {
return err
}
defer func() {
if err != nil && t != nil {
if rerr := t.Rollback(); rerr != nil {
log.G(ctx).WithError(rerr).Warn("Failure rolling back transaction")
}
}
}()
id, err := storage.CommitActive(ctx, key, name, snapshots.Usage{}, opts...) // TODO(stevvooe): Resolve a usage value for btrfs
if err != nil {
return errors.Wrap(err, "failed to commit")
}
source := filepath.Join(b.root, "active", id)
target := filepath.Join(b.root, "snapshots", id)
if err := btrfs.SubvolSnapshot(target, source, true); err != nil {
return err
}
err = t.Commit()
t = nil
if err != nil {
if derr := btrfs.SubvolDelete(target); derr != nil {
log.G(ctx).WithError(derr).WithField("subvolume", target).Error("Failed to delete subvolume")
}
return err
}
if derr := btrfs.SubvolDelete(source); derr != nil {
// Log as warning, only needed for cleanup, will not cause name collision
log.G(ctx).WithError(derr).WithField("subvolume", source).Warn("Failed to delete subvolume")
}
return nil
}
// Mounts returns the mounts for the transaction identified by key. Can be
// called on an read-write or readonly transaction.
//
// This can be used to recover mounts after calling View or Prepare.
func (b *snapshotter) Mounts(ctx context.Context, key string) ([]mount.Mount, error) {
ctx, t, err := b.ms.TransactionContext(ctx, false)
if err != nil {
return nil, err
}
s, err := storage.GetSnapshot(ctx, key)
t.Rollback()
if err != nil {
return nil, errors.Wrap(err, "failed to get active snapshot")
}
dir := filepath.Join(b.root, strings.ToLower(s.Kind.String()), s.ID)
return b.mounts(dir, s)
}
// Remove abandons the transaction identified by key. All resources
// associated with the key will be removed.
func (b *snapshotter) Remove(ctx context.Context, key string) (err error) {
var (
source, removed string
readonly bool
)
ctx, t, err := b.ms.TransactionContext(ctx, true)
if err != nil {
return err
}
defer func() {
if err != nil && t != nil {
if rerr := t.Rollback(); rerr != nil {
log.G(ctx).WithError(rerr).Warn("Failure rolling back transaction")
}
}
if removed != "" {
if derr := btrfs.SubvolDelete(removed); derr != nil {
log.G(ctx).WithError(derr).WithField("subvolume", removed).Warn("Failed to delete subvolume")
}
}
}()
id, k, err := storage.Remove(ctx, key)
if err != nil {
return errors.Wrap(err, "failed to remove snapshot")
}
switch k {
case snapshots.KindView:
source = filepath.Join(b.root, "view", id)
removed = filepath.Join(b.root, "view", "rm-"+id)
readonly = true
case snapshots.KindActive:
source = filepath.Join(b.root, "active", id)
removed = filepath.Join(b.root, "active", "rm-"+id)
case snapshots.KindCommitted:
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
}
// Close closes the snapshotter
func (b *snapshotter) Close() error {
return b.ms.Close()
}

View File

@@ -0,0 +1,155 @@
// +build linux,!no_btrfs
package btrfs
import (
"context"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"github.com/containerd/containerd/mount"
"github.com/containerd/containerd/snapshots"
"github.com/containerd/containerd/snapshots/testsuite"
"github.com/containerd/containerd/testutil"
"github.com/pkg/errors"
"golang.org/x/sys/unix"
)
func boltSnapshotter(t *testing.T) func(context.Context, string) (snapshots.Snapshotter, func() error, error) {
mkbtrfs, err := exec.LookPath("mkfs.btrfs")
if err != nil {
t.Skipf("could not find mkfs.btrfs: %v", err)
}
// TODO: Check for btrfs in /proc/module and skip if not loaded
return func(ctx context.Context, root string) (snapshots.Snapshotter, func() error, error) {
deviceName, cleanupDevice, err := testutil.NewLoopback(100 << 20) // 100 MB
if err != nil {
return nil, nil, err
}
if out, err := exec.Command(mkbtrfs, deviceName).CombinedOutput(); err != nil {
return nil, nil, errors.Wrapf(err, "failed to make btrfs filesystem (out: %q)", out)
}
if out, err := exec.Command("mount", deviceName, root).CombinedOutput(); err != nil {
return nil, nil, errors.Wrapf(err, "failed to mount device %s (out: %q)", deviceName, out)
}
snapshotter, err := NewSnapshotter(root)
if err != nil {
return nil, nil, errors.Wrap(err, "failed to create new snapshotter")
}
return snapshotter, func() error {
if err := snapshotter.Close(); err != nil {
return err
}
err := mount.UnmountAll(root, unix.MNT_DETACH)
if cerr := cleanupDevice(); cerr != nil {
err = errors.Wrap(cerr, "device cleanup failed")
}
return err
}, nil
}
}
func TestBtrfs(t *testing.T) {
testutil.RequiresRoot(t)
testsuite.SnapshotterSuite(t, "Btrfs", boltSnapshotter(t))
}
func TestBtrfsMounts(t *testing.T) {
testutil.RequiresRoot(t)
ctx := context.Background()
// create temporary directory for mount point
mountPoint, err := ioutil.TempDir("", "containerd-btrfs-test")
if err != nil {
t.Fatal("could not create mount point for btrfs test", err)
}
defer os.RemoveAll(mountPoint)
t.Log("temporary mount point created", mountPoint)
root, err := ioutil.TempDir(mountPoint, "TestBtrfsPrepare-")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(root)
b, c, err := boltSnapshotter(t)(ctx, root)
if err != nil {
t.Fatal(err)
}
defer c()
target := filepath.Join(root, "test")
mounts, err := b.Prepare(ctx, target, "")
if err != nil {
t.Fatal(err)
}
t.Log(mounts)
for _, mount := range mounts {
if mount.Type != "btrfs" {
t.Fatalf("wrong mount type: %v != btrfs", mount.Type)
}
// assumes the first, maybe incorrect in the future
if !strings.HasPrefix(mount.Options[0], "subvolid=") {
t.Fatalf("no subvolid option in %v", mount.Options)
}
}
if err := os.MkdirAll(target, 0755); err != nil {
t.Fatal(err)
}
if err := mount.All(mounts, target); err != nil {
t.Fatal(err)
}
defer testutil.Unmount(t, target)
// write in some data
if err := ioutil.WriteFile(filepath.Join(target, "foo"), []byte("content"), 0777); err != nil {
t.Fatal(err)
}
// TODO(stevvooe): We don't really make this with the driver, but that
// might prove annoying in practice.
if err := os.MkdirAll(filepath.Join(root, "snapshots"), 0755); err != nil {
t.Fatal(err)
}
if err := b.Commit(ctx, filepath.Join(root, "snapshots/committed"), filepath.Join(root, "test")); err != nil {
t.Fatal(err)
}
target = filepath.Join(root, "test2")
mounts, err = b.Prepare(ctx, target, filepath.Join(root, "snapshots/committed"))
if err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(target, 0755); err != nil {
t.Fatal(err)
}
if err := mount.All(mounts, target); err != nil {
t.Fatal(err)
}
defer testutil.Unmount(t, target)
// TODO(stevvooe): Verify contents of "foo"
if err := ioutil.WriteFile(filepath.Join(target, "bar"), []byte("content"), 0777); err != nil {
t.Fatal(err)
}
if err := b.Commit(ctx, filepath.Join(root, "snapshots/committed2"), target); err != nil {
t.Fatal(err)
}
}

329
snapshots/naive/naive.go Normal file
View File

@@ -0,0 +1,329 @@
package naive
import (
"context"
"io/ioutil"
"os"
"path/filepath"
"github.com/containerd/containerd/fs"
"github.com/containerd/containerd/log"
"github.com/containerd/containerd/mount"
"github.com/containerd/containerd/platforms"
"github.com/containerd/containerd/plugin"
"github.com/containerd/containerd/snapshots"
"github.com/containerd/containerd/snapshots/storage"
"github.com/pkg/errors"
)
func init() {
plugin.Register(&plugin.Registration{
Type: plugin.SnapshotPlugin,
ID: "naive",
InitFn: func(ic *plugin.InitContext) (interface{}, error) {
ic.Meta.Platforms = append(ic.Meta.Platforms, platforms.DefaultSpec())
return NewSnapshotter(ic.Root)
},
})
}
type snapshotter struct {
root string
ms *storage.MetaStore
}
// NewSnapshotter returns a Snapshotter which copies layers on the underlying
// file system. A metadata file is stored under the root.
func NewSnapshotter(root string) (snapshots.Snapshotter, error) {
if err := os.MkdirAll(root, 0700); err != nil {
return nil, err
}
ms, err := storage.NewMetaStore(filepath.Join(root, "metadata.db"))
if err != nil {
return nil, err
}
if err := os.Mkdir(filepath.Join(root, "snapshots"), 0700); err != nil && !os.IsExist(err) {
return nil, err
}
return &snapshotter{
root: root,
ms: ms,
}, nil
}
// Stat returns the info for an active or committed snapshot by name or
// key.
//
// Should be used for parent resolution, existence checks and to discern
// the kind of snapshot.
func (o *snapshotter) Stat(ctx context.Context, key string) (snapshots.Info, error) {
ctx, t, err := o.ms.TransactionContext(ctx, false)
if err != nil {
return snapshots.Info{}, err
}
defer t.Rollback()
_, info, _, err := storage.GetInfo(ctx, key)
if err != nil {
return snapshots.Info{}, err
}
return info, nil
}
func (o *snapshotter) Update(ctx context.Context, info snapshots.Info, fieldpaths ...string) (snapshots.Info, error) {
ctx, t, err := o.ms.TransactionContext(ctx, true)
if err != nil {
return snapshots.Info{}, err
}
info, err = storage.UpdateInfo(ctx, info, fieldpaths...)
if err != nil {
t.Rollback()
return snapshots.Info{}, err
}
if err := t.Commit(); err != nil {
return snapshots.Info{}, err
}
return info, nil
}
func (o *snapshotter) Usage(ctx context.Context, key string) (snapshots.Usage, error) {
ctx, t, err := o.ms.TransactionContext(ctx, false)
if err != nil {
return snapshots.Usage{}, err
}
defer t.Rollback()
id, info, usage, err := storage.GetInfo(ctx, key)
if err != nil {
return snapshots.Usage{}, err
}
if info.Kind == snapshots.KindActive {
du, err := fs.DiskUsage(o.getSnapshotDir(id))
if err != nil {
return snapshots.Usage{}, err
}
usage = snapshots.Usage(du)
}
return usage, nil
}
func (o *snapshotter) Prepare(ctx context.Context, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) {
return o.createSnapshot(ctx, snapshots.KindActive, key, parent, opts)
}
func (o *snapshotter) View(ctx context.Context, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) {
return o.createSnapshot(ctx, snapshots.KindView, key, parent, opts)
}
// Mounts returns the mounts for the transaction identified by key. Can be
// called on an read-write or readonly transaction.
//
// This can be used to recover mounts after calling View or Prepare.
func (o *snapshotter) Mounts(ctx context.Context, key string) ([]mount.Mount, error) {
ctx, t, err := o.ms.TransactionContext(ctx, false)
if err != nil {
return nil, err
}
s, err := storage.GetSnapshot(ctx, key)
t.Rollback()
if err != nil {
return nil, errors.Wrap(err, "failed to get snapshot mount")
}
return o.mounts(s), nil
}
func (o *snapshotter) Commit(ctx context.Context, name, key string, opts ...snapshots.Opt) error {
ctx, t, err := o.ms.TransactionContext(ctx, true)
if err != nil {
return err
}
id, _, _, err := storage.GetInfo(ctx, key)
if err != nil {
return err
}
usage, err := fs.DiskUsage(o.getSnapshotDir(id))
if err != nil {
return err
}
if _, err := storage.CommitActive(ctx, key, name, snapshots.Usage(usage), opts...); err != nil {
if rerr := t.Rollback(); rerr != nil {
log.G(ctx).WithError(rerr).Warn("Failure rolling back transaction")
}
return errors.Wrap(err, "failed to commit snapshot")
}
return t.Commit()
}
// Remove abandons the transaction identified by key. All resources
// associated with the key will be removed.
func (o *snapshotter) Remove(ctx context.Context, key string) (err error) {
ctx, t, err := o.ms.TransactionContext(ctx, true)
if err != nil {
return err
}
defer func() {
if err != nil && t != nil {
if rerr := t.Rollback(); rerr != nil {
log.G(ctx).WithError(rerr).Warn("Failure rolling back transaction")
}
}
}()
id, _, err := storage.Remove(ctx, key)
if err != nil {
return errors.Wrap(err, "failed to remove")
}
path := o.getSnapshotDir(id)
renamed := filepath.Join(o.root, "snapshots", "rm-"+id)
if err := os.Rename(path, renamed); err != nil {
if !os.IsNotExist(err) {
return errors.Wrap(err, "failed to rename")
}
renamed = ""
}
err = t.Commit()
t = nil
if err != nil {
if renamed != "" {
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 renamed != "" {
if err := os.RemoveAll(renamed); err != nil {
// Must be cleaned up, any "rm-*" could be removed if no active transactions
log.G(ctx).WithError(err).WithField("path", renamed).Warnf("Failed to remove root filesystem")
}
}
return nil
}
// Walk the committed snapshots.
func (o *snapshotter) Walk(ctx context.Context, fn func(context.Context, snapshots.Info) error) error {
ctx, t, err := o.ms.TransactionContext(ctx, false)
if err != nil {
return err
}
defer t.Rollback()
return storage.WalkInfo(ctx, fn)
}
func (o *snapshotter) createSnapshot(ctx context.Context, kind snapshots.Kind, key, parent string, opts []snapshots.Opt) ([]mount.Mount, error) {
var (
err error
path, td string
)
if kind == snapshots.KindActive || parent == "" {
td, err = ioutil.TempDir(filepath.Join(o.root, "snapshots"), "new-")
if err != nil {
return nil, errors.Wrap(err, "failed to create temp dir")
}
defer func() {
if err != nil {
if td != "" {
if err1 := os.RemoveAll(td); err1 != nil {
err = errors.Wrapf(err, "remove failed: %v", err1)
}
}
if path != "" {
if err1 := os.RemoveAll(path); err1 != nil {
err = errors.Wrapf(err, "failed to remove path: %v", err1)
}
}
}
}()
}
ctx, t, err := o.ms.TransactionContext(ctx, true)
if err != nil {
return nil, err
}
s, err := storage.CreateSnapshot(ctx, kind, key, parent, opts...)
if err != nil {
if rerr := t.Rollback(); rerr != nil {
log.G(ctx).WithError(rerr).Warn("Failure rolling back transaction")
}
return nil, errors.Wrap(err, "failed to create snapshot")
}
if td != "" {
if len(s.ParentIDs) > 0 {
parent := o.getSnapshotDir(s.ParentIDs[0])
if err := fs.CopyDir(td, parent); err != nil {
return nil, errors.Wrap(err, "copying of parent failed")
}
}
path = o.getSnapshotDir(s.ID)
if err := os.Rename(td, path); err != nil {
if rerr := t.Rollback(); rerr != nil {
log.G(ctx).WithError(rerr).Warn("Failure rolling back transaction")
}
return nil, errors.Wrap(err, "failed to rename")
}
td = ""
}
if err := t.Commit(); err != nil {
return nil, errors.Wrap(err, "commit failed")
}
return o.mounts(s), nil
}
func (o *snapshotter) getSnapshotDir(id string) string {
return filepath.Join(o.root, "snapshots", id)
}
func (o *snapshotter) mounts(s storage.Snapshot) []mount.Mount {
var (
roFlag string
source string
)
if s.Kind == snapshots.KindView {
roFlag = "ro"
} else {
roFlag = "rw"
}
if len(s.ParentIDs) == 0 || s.Kind == snapshots.KindActive {
source = o.getSnapshotDir(s.ID)
} else {
source = o.getSnapshotDir(s.ParentIDs[0])
}
return []mount.Mount{
{
Source: source,
Type: "bind",
Options: []string{
roFlag,
"rbind",
},
},
}
}
// Close closes the snapshotter
func (o *snapshotter) Close() error {
return o.ms.Close()
}

View File

@@ -0,0 +1,28 @@
package naive
import (
"context"
"runtime"
"testing"
"github.com/containerd/containerd/snapshots"
"github.com/containerd/containerd/snapshots/testsuite"
"github.com/containerd/containerd/testutil"
)
func newSnapshotter(ctx context.Context, root string) (snapshots.Snapshotter, func() error, error) {
snapshotter, err := NewSnapshotter(root)
if err != nil {
return nil, nil, err
}
return snapshotter, func() error { return snapshotter.Close() }, nil
}
func TestNaive(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("snapshotter not implemented on windows")
}
testutil.RequiresRoot(t)
testsuite.SnapshotterSuite(t, "Naive", newSnapshotter)
}

View File

@@ -0,0 +1,401 @@
// +build linux
package overlay
import (
"context"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"syscall"
"github.com/containerd/containerd/fs"
"github.com/containerd/containerd/log"
"github.com/containerd/containerd/mount"
"github.com/containerd/containerd/platforms"
"github.com/containerd/containerd/plugin"
"github.com/containerd/containerd/snapshots"
"github.com/containerd/containerd/snapshots/storage"
"github.com/pkg/errors"
)
func init() {
plugin.Register(&plugin.Registration{
Type: plugin.SnapshotPlugin,
ID: "overlayfs",
InitFn: func(ic *plugin.InitContext) (interface{}, error) {
ic.Meta.Platforms = append(ic.Meta.Platforms, platforms.DefaultSpec())
ic.Meta.Exports["root"] = ic.Root
return NewSnapshotter(ic.Root)
},
})
}
type snapshotter struct {
root string
ms *storage.MetaStore
}
// NewSnapshotter returns a Snapshotter which uses overlayfs. The overlayfs
// diffs are stored under the provided root. A metadata file is stored under
// the root.
func NewSnapshotter(root string) (snapshots.Snapshotter, error) {
if err := os.MkdirAll(root, 0700); err != nil {
return nil, err
}
supportsDType, err := fs.SupportsDType(root)
if err != nil {
return nil, err
}
if !supportsDType {
return nil, fmt.Errorf("%s does not support d_type. If the backing filesystem is xfs, please reformat with ftype=1 to enable d_type support", root)
}
ms, err := storage.NewMetaStore(filepath.Join(root, "metadata.db"))
if err != nil {
return nil, err
}
if err := os.Mkdir(filepath.Join(root, "snapshots"), 0700); err != nil && !os.IsExist(err) {
return nil, err
}
return &snapshotter{
root: root,
ms: ms,
}, nil
}
// Stat returns the info for an active or committed snapshot by name or
// key.
//
// Should be used for parent resolution, existence checks and to discern
// the kind of snapshot.
func (o *snapshotter) Stat(ctx context.Context, key string) (snapshots.Info, error) {
ctx, t, err := o.ms.TransactionContext(ctx, false)
if err != nil {
return snapshots.Info{}, err
}
defer t.Rollback()
_, info, _, err := storage.GetInfo(ctx, key)
if err != nil {
return snapshots.Info{}, err
}
return info, nil
}
func (o *snapshotter) Update(ctx context.Context, info snapshots.Info, fieldpaths ...string) (snapshots.Info, error) {
ctx, t, err := o.ms.TransactionContext(ctx, true)
if err != nil {
return snapshots.Info{}, err
}
info, err = storage.UpdateInfo(ctx, info, fieldpaths...)
if err != nil {
t.Rollback()
return snapshots.Info{}, err
}
if err := t.Commit(); err != nil {
return snapshots.Info{}, err
}
return info, nil
}
// Usage returns the resources taken by the snapshot identified by key.
//
// For active snapshots, this will scan the usage of the overlay "diff" (aka
// "upper") directory and may take some time.
//
// For committed snapshots, the value is returned from the metadata database.
func (o *snapshotter) Usage(ctx context.Context, key string) (snapshots.Usage, error) {
ctx, t, err := o.ms.TransactionContext(ctx, false)
if err != nil {
return snapshots.Usage{}, err
}
id, info, usage, err := storage.GetInfo(ctx, key)
t.Rollback() // transaction no longer needed at this point.
if err != nil {
return snapshots.Usage{}, err
}
upperPath := o.upperPath(id)
if info.Kind == snapshots.KindActive {
du, err := fs.DiskUsage(upperPath)
if err != nil {
// TODO(stevvooe): Consider not reporting an error in this case.
return snapshots.Usage{}, err
}
usage = snapshots.Usage(du)
}
return usage, nil
}
func (o *snapshotter) Prepare(ctx context.Context, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) {
return o.createSnapshot(ctx, snapshots.KindActive, key, parent, opts)
}
func (o *snapshotter) View(ctx context.Context, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) {
return o.createSnapshot(ctx, snapshots.KindView, key, parent, opts)
}
// Mounts returns the mounts for the transaction identified by key. Can be
// called on an read-write or readonly transaction.
//
// This can be used to recover mounts after calling View or Prepare.
func (o *snapshotter) Mounts(ctx context.Context, key string) ([]mount.Mount, error) {
ctx, t, err := o.ms.TransactionContext(ctx, false)
if err != nil {
return nil, err
}
s, err := storage.GetSnapshot(ctx, key)
t.Rollback()
if err != nil {
return nil, errors.Wrap(err, "failed to get active mount")
}
return o.mounts(s), nil
}
func (o *snapshotter) Commit(ctx context.Context, name, key string, opts ...snapshots.Opt) error {
ctx, t, err := o.ms.TransactionContext(ctx, true)
if err != nil {
return err
}
defer func() {
if err != nil {
if rerr := t.Rollback(); rerr != nil {
log.G(ctx).WithError(rerr).Warn("Failure rolling back transaction")
}
}
}()
// grab the existing id
id, _, _, err := storage.GetInfo(ctx, key)
if err != nil {
return err
}
usage, err := fs.DiskUsage(o.upperPath(id))
if err != nil {
return err
}
if _, err = storage.CommitActive(ctx, key, name, snapshots.Usage(usage), opts...); err != nil {
return errors.Wrap(err, "failed to commit snapshot")
}
return t.Commit()
}
// Remove abandons the transaction identified by key. All resources
// associated with the key will be removed.
func (o *snapshotter) Remove(ctx context.Context, key string) (err error) {
ctx, t, err := o.ms.TransactionContext(ctx, true)
if err != nil {
return err
}
defer func() {
if err != nil && t != nil {
if rerr := t.Rollback(); rerr != nil {
log.G(ctx).WithError(rerr).Warn("Failure rolling back transaction")
}
}
}()
id, _, err := storage.Remove(ctx, key)
if err != nil {
return errors.Wrap(err, "failed to remove")
}
path := filepath.Join(o.root, "snapshots", id)
renamed := filepath.Join(o.root, "snapshots", "rm-"+id)
if err := os.Rename(path, renamed); err != nil {
return errors.Wrap(err, "failed to rename")
}
err = t.Commit()
t = nil
if err != nil {
if err1 := os.Rename(renamed, path); err1 != nil {
// May cause inconsistent data on disk
log.G(ctx).WithError(err1).WithField("path", renamed).Errorf("Failed to rename after failed commit")
}
return errors.Wrap(err, "failed to commit")
}
if err := os.RemoveAll(renamed); err != nil {
// Must be cleaned up, any "rm-*" could be removed if no active transactions
log.G(ctx).WithError(err).WithField("path", renamed).Warnf("Failed to remove root filesystem")
}
return nil
}
// Walk the committed snapshots.
func (o *snapshotter) Walk(ctx context.Context, fn func(context.Context, snapshots.Info) error) error {
ctx, t, err := o.ms.TransactionContext(ctx, false)
if err != nil {
return err
}
defer t.Rollback()
return storage.WalkInfo(ctx, fn)
}
func (o *snapshotter) createSnapshot(ctx context.Context, kind snapshots.Kind, key, parent string, opts []snapshots.Opt) ([]mount.Mount, error) {
var (
path string
snapshotDir = filepath.Join(o.root, "snapshots")
)
td, err := ioutil.TempDir(snapshotDir, "new-")
if err != nil {
return nil, errors.Wrap(err, "failed to create temp dir")
}
defer func() {
if err != nil {
if td != "" {
if err1 := os.RemoveAll(td); err1 != nil {
err = errors.Wrapf(err, "remove failed: %v", err1)
}
}
if path != "" {
if err1 := os.RemoveAll(path); err1 != nil {
err = errors.Wrapf(err, "failed to remove path: %v", err1)
}
}
}
}()
fs := filepath.Join(td, "fs")
if err = os.MkdirAll(fs, 0755); err != nil {
return nil, err
}
if kind == snapshots.KindActive {
if err = os.MkdirAll(filepath.Join(td, "work"), 0711); err != nil {
return nil, err
}
}
ctx, t, err := o.ms.TransactionContext(ctx, true)
if err != nil {
return nil, err
}
s, err := storage.CreateSnapshot(ctx, kind, key, parent, opts...)
if err != nil {
if rerr := t.Rollback(); rerr != nil {
log.G(ctx).WithError(rerr).Warn("Failure rolling back transaction")
}
return nil, errors.Wrap(err, "failed to create active")
}
if len(s.ParentIDs) > 0 {
st, err := os.Stat(filepath.Join(o.upperPath(s.ParentIDs[0])))
if err != nil {
if rerr := t.Rollback(); rerr != nil {
log.G(ctx).WithError(rerr).Warn("Failure rolling back transaction")
}
return nil, errors.Wrap(err, "failed to stat parent")
}
stat := st.Sys().(*syscall.Stat_t)
if err := os.Lchown(fs, int(stat.Uid), int(stat.Gid)); err != nil {
if rerr := t.Rollback(); rerr != nil {
log.G(ctx).WithError(rerr).Warn("Failure rolling back transaction")
}
return nil, errors.Wrap(err, "failed to chown")
}
}
path = filepath.Join(snapshotDir, s.ID)
if err = os.Rename(td, path); err != nil {
if rerr := t.Rollback(); rerr != nil {
log.G(ctx).WithError(rerr).Warn("Failure rolling back transaction")
}
return nil, errors.Wrap(err, "failed to rename")
}
td = ""
if err = t.Commit(); err != nil {
return nil, errors.Wrap(err, "commit failed")
}
return o.mounts(s), nil
}
func (o *snapshotter) mounts(s storage.Snapshot) []mount.Mount {
if len(s.ParentIDs) == 0 {
// if we only have one layer/no parents then just return a bind mount as overlay
// will not work
roFlag := "rw"
if s.Kind == snapshots.KindView {
roFlag = "ro"
}
return []mount.Mount{
{
Source: o.upperPath(s.ID),
Type: "bind",
Options: []string{
roFlag,
"rbind",
},
},
}
}
var options []string
if s.Kind == snapshots.KindActive {
options = append(options,
fmt.Sprintf("workdir=%s", o.workPath(s.ID)),
fmt.Sprintf("upperdir=%s", o.upperPath(s.ID)),
)
} else if len(s.ParentIDs) == 1 {
return []mount.Mount{
{
Source: o.upperPath(s.ParentIDs[0]),
Type: "bind",
Options: []string{
"ro",
"rbind",
},
},
}
}
parentPaths := make([]string, len(s.ParentIDs))
for i := range s.ParentIDs {
parentPaths[i] = o.upperPath(s.ParentIDs[i])
}
options = append(options, fmt.Sprintf("lowerdir=%s", strings.Join(parentPaths, ":")))
return []mount.Mount{
{
Type: "overlay",
Source: "overlay",
Options: options,
},
}
}
func (o *snapshotter) upperPath(id string) string {
return filepath.Join(o.root, "snapshots", id, "fs")
}
func (o *snapshotter) workPath(id string) string {
return filepath.Join(o.root, "snapshots", id, "work")
}
// Close closes the snapshotter
func (o *snapshotter) Close() error {
return o.ms.Close()
}

View File

@@ -0,0 +1,320 @@
// +build linux
package overlay
import (
"context"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"syscall"
"testing"
"github.com/containerd/containerd/mount"
"github.com/containerd/containerd/snapshots"
"github.com/containerd/containerd/snapshots/storage"
"github.com/containerd/containerd/snapshots/testsuite"
"github.com/containerd/containerd/testutil"
)
func newSnapshotter(ctx context.Context, root string) (snapshots.Snapshotter, func() error, error) {
snapshotter, err := NewSnapshotter(root)
if err != nil {
return nil, nil, err
}
return snapshotter, func() error { return snapshotter.Close() }, nil
}
func TestOverlay(t *testing.T) {
testutil.RequiresRoot(t)
testsuite.SnapshotterSuite(t, "Overlay", newSnapshotter)
}
func TestOverlayMounts(t *testing.T) {
ctx := context.TODO()
root, err := ioutil.TempDir("", "overlay")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(root)
o, _, err := newSnapshotter(ctx, root)
if err != nil {
t.Error(err)
return
}
mounts, err := o.Prepare(ctx, "/tmp/test", "")
if err != nil {
t.Error(err)
return
}
if len(mounts) != 1 {
t.Errorf("should only have 1 mount but received %d", len(mounts))
}
m := mounts[0]
if m.Type != "bind" {
t.Errorf("mount type should be bind but received %q", m.Type)
}
expected := filepath.Join(root, "snapshots", "1", "fs")
if m.Source != expected {
t.Errorf("expected source %q but received %q", expected, m.Source)
}
if m.Options[0] != "rw" {
t.Errorf("expected mount option rw but received %q", m.Options[0])
}
if m.Options[1] != "rbind" {
t.Errorf("expected mount option rbind but received %q", m.Options[1])
}
}
func TestOverlayCommit(t *testing.T) {
ctx := context.TODO()
root, err := ioutil.TempDir("", "overlay")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(root)
o, _, err := newSnapshotter(ctx, root)
if err != nil {
t.Error(err)
return
}
key := "/tmp/test"
mounts, err := o.Prepare(ctx, key, "")
if err != nil {
t.Error(err)
return
}
m := mounts[0]
if err := ioutil.WriteFile(filepath.Join(m.Source, "foo"), []byte("hi"), 0660); err != nil {
t.Error(err)
return
}
if err := o.Commit(ctx, "base", key); err != nil {
t.Error(err)
return
}
}
func TestOverlayOverlayMount(t *testing.T) {
ctx := context.TODO()
root, err := ioutil.TempDir("", "overlay")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(root)
o, _, err := newSnapshotter(ctx, root)
if err != nil {
t.Error(err)
return
}
key := "/tmp/test"
if _, err = o.Prepare(ctx, key, ""); err != nil {
t.Error(err)
return
}
if err := o.Commit(ctx, "base", key); err != nil {
t.Error(err)
return
}
var mounts []mount.Mount
if mounts, err = o.Prepare(ctx, "/tmp/layer2", "base"); err != nil {
t.Error(err)
return
}
if len(mounts) != 1 {
t.Errorf("should only have 1 mount but received %d", len(mounts))
}
m := mounts[0]
if m.Type != "overlay" {
t.Errorf("mount type should be overlay but received %q", m.Type)
}
if m.Source != "overlay" {
t.Errorf("expected source %q but received %q", "overlay", m.Source)
}
var (
bp = getBasePath(ctx, o, root, "/tmp/layer2")
work = "workdir=" + filepath.Join(bp, "work")
upper = "upperdir=" + filepath.Join(bp, "fs")
lower = "lowerdir=" + getParents(ctx, o, root, "/tmp/layer2")[0]
)
for i, v := range []string{
work,
upper,
lower,
} {
if m.Options[i] != v {
t.Errorf("expected %q but received %q", v, m.Options[i])
}
}
}
func getBasePath(ctx context.Context, sn snapshots.Snapshotter, root, key string) string {
o := sn.(*snapshotter)
ctx, t, err := o.ms.TransactionContext(ctx, false)
if err != nil {
panic(err)
}
defer t.Rollback()
s, err := storage.GetSnapshot(ctx, key)
if err != nil {
panic(err)
}
return filepath.Join(root, "snapshots", s.ID)
}
func getParents(ctx context.Context, sn snapshots.Snapshotter, root, key string) []string {
o := sn.(*snapshotter)
ctx, t, err := o.ms.TransactionContext(ctx, false)
if err != nil {
panic(err)
}
defer t.Rollback()
s, err := storage.GetSnapshot(ctx, key)
if err != nil {
panic(err)
}
parents := make([]string, len(s.ParentIDs))
for i := range s.ParentIDs {
parents[i] = filepath.Join(root, "snapshots", s.ParentIDs[i], "fs")
}
return parents
}
func TestOverlayOverlayRead(t *testing.T) {
testutil.RequiresRoot(t)
ctx := context.TODO()
root, err := ioutil.TempDir("", "overlay")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(root)
o, _, err := newSnapshotter(ctx, root)
if err != nil {
t.Error(err)
return
}
key := "/tmp/test"
mounts, err := o.Prepare(ctx, key, "")
if err != nil {
t.Error(err)
return
}
m := mounts[0]
if err := ioutil.WriteFile(filepath.Join(m.Source, "foo"), []byte("hi"), 0660); err != nil {
t.Error(err)
return
}
if err := o.Commit(ctx, "base", key); err != nil {
t.Error(err)
return
}
if mounts, err = o.Prepare(ctx, "/tmp/layer2", "base"); err != nil {
t.Error(err)
return
}
dest := filepath.Join(root, "dest")
if err := os.Mkdir(dest, 0700); err != nil {
t.Error(err)
return
}
if err := mount.All(mounts, dest); err != nil {
t.Error(err)
return
}
defer syscall.Unmount(dest, 0)
data, err := ioutil.ReadFile(filepath.Join(dest, "foo"))
if err != nil {
t.Error(err)
return
}
if e := string(data); e != "hi" {
t.Errorf("expected file contents hi but got %q", e)
return
}
}
func TestOverlayView(t *testing.T) {
ctx := context.TODO()
root, err := ioutil.TempDir("", "overlay")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(root)
o, _, err := newSnapshotter(ctx, root)
if err != nil {
t.Fatal(err)
}
key := "/tmp/base"
mounts, err := o.Prepare(ctx, key, "")
if err != nil {
t.Fatal(err)
}
m := mounts[0]
if err := ioutil.WriteFile(filepath.Join(m.Source, "foo"), []byte("hi"), 0660); err != nil {
t.Fatal(err)
}
if err := o.Commit(ctx, "base", key); err != nil {
t.Fatal(err)
}
key = "/tmp/top"
_, err = o.Prepare(ctx, key, "base")
if err != nil {
t.Fatal(err)
}
if err := ioutil.WriteFile(filepath.Join(getParents(ctx, o, root, "/tmp/top")[0], "foo"), []byte("hi, again"), 0660); err != nil {
t.Fatal(err)
}
if err := o.Commit(ctx, "top", key); err != nil {
t.Fatal(err)
}
mounts, err = o.View(ctx, "/tmp/view1", "base")
if err != nil {
t.Fatal(err)
}
if len(mounts) != 1 {
t.Fatalf("should only have 1 mount but received %d", len(mounts))
}
m = mounts[0]
if m.Type != "bind" {
t.Errorf("mount type should be bind but received %q", m.Type)
}
expected := getParents(ctx, o, root, "/tmp/view1")[0]
if m.Source != expected {
t.Errorf("expected source %q but received %q", expected, m.Source)
}
if m.Options[0] != "ro" {
t.Errorf("expected mount option ro but received %q", m.Options[0])
}
if m.Options[1] != "rbind" {
t.Errorf("expected mount option rbind but received %q", m.Options[1])
}
mounts, err = o.View(ctx, "/tmp/view2", "top")
if err != nil {
t.Fatal(err)
}
if len(mounts) != 1 {
t.Fatalf("should only have 1 mount but received %d", len(mounts))
}
m = mounts[0]
if m.Type != "overlay" {
t.Errorf("mount type should be overlay but received %q", m.Type)
}
if m.Source != "overlay" {
t.Errorf("mount source should be overlay but received %q", m.Source)
}
if len(m.Options) != 1 {
t.Errorf("expected 1 mount option but got %d", len(m.Options))
}
lowers := getParents(ctx, o, root, "/tmp/view2")
expected = fmt.Sprintf("lowerdir=%s:%s", lowers[0], lowers[1])
if m.Options[0] != expected {
t.Errorf("expected option %q but received %q", expected, m.Options[0])
}
}

316
snapshots/snapshotter.go Normal file
View File

@@ -0,0 +1,316 @@
package snapshots
import (
"context"
"encoding/json"
"strings"
"time"
"github.com/containerd/containerd/mount"
)
// Kind identifies the kind of snapshot.
type Kind uint8
// definitions of snapshot kinds
const (
KindUnknown Kind = iota
KindView
KindActive
KindCommitted
)
// ParseKind parses the provided string into a Kind
//
// If the string cannot be parsed KindUnknown is returned
func ParseKind(s string) Kind {
s = strings.ToLower(s)
switch s {
case "view":
return KindView
case "active":
return KindActive
case "committed":
return KindCommitted
}
return KindUnknown
}
// String returns the string representation of the Kind
func (k Kind) String() string {
switch k {
case KindView:
return "View"
case KindActive:
return "Active"
case KindCommitted:
return "Committed"
}
return "Unknown"
}
// MarshalJSON the Kind to JSON
func (k Kind) MarshalJSON() ([]byte, error) {
return json.Marshal(k.String())
}
// UnmarshalJSON the Kind from JSON
func (k *Kind) UnmarshalJSON(b []byte) error {
var s string
if err := json.Unmarshal(b, &s); err != nil {
return err
}
*k = ParseKind(s)
return nil
}
// Info provides information about a particular snapshot.
// JSON marshallability is supported for interactive with tools like ctr,
type Info struct {
Kind Kind // active or committed snapshot
Name string // name or key of snapshot
Parent string `json:",omitempty"` // name of parent snapshot
Labels map[string]string `json:",omitempty"` // Labels for snapshot
Created time.Time `json:",omitempty"` // Created time
Updated time.Time `json:",omitempty"` // Last update time
}
// Usage defines statistics for disk resources consumed by the snapshot.
//
// These resources only include the resources consumed by the snapshot itself
// and does not include resources usage by the parent.
type Usage struct {
Inodes int64 // number of inodes in use.
Size int64 // provides usage, in bytes, of snapshot
}
// Add the provided usage to the current usage
func (u *Usage) Add(other Usage) {
u.Size += other.Size
// TODO(stevvooe): assumes independent inodes, but provides and upper
// bound. This should be pretty close, assuming the inodes for a
// snapshot are roughly unique to it. Don't trust this assumption.
u.Inodes += other.Inodes
}
// Snapshotter defines the methods required to implement a snapshot snapshotter for
// allocating, snapshotting and mounting filesystem changesets. The model works
// by building up sets of changes with parent-child relationships.
//
// A snapshot represents a filesystem state. Every snapshot has a parent, where
// the empty parent is represented by the empty string. A diff can be taken
// between a parent and its snapshot to generate a classic layer.
//
// An active snapshot is created by calling `Prepare`. After mounting, changes
// can be made to the snapshot. The act of committing creates a committed
// snapshot. The committed snapshot will get the parent of active snapshot. The
// committed snapshot can then be used as a parent. Active snapshots can never
// act as a parent.
//
// Snapshots are best understood by their lifecycle. Active snapshots are
// always created with Prepare or View. Committed snapshots are always created
// with Commit. Active snapshots never become committed snapshots and vice
// versa. All snapshots may be removed.
//
// For consistency, we define the following terms to be used throughout this
// interface for snapshotter implementations:
//
// `ctx` - refers to a context.Context
// `key` - refers to an active snapshot
// `name` - refers to a committed snapshot
// `parent` - refers to the parent in relation
//
// Most methods take various combinations of these identifiers. Typically,
// `name` and `parent` will be used in cases where a method *only* takes
// committed snapshots. `key` will be used to refer to active snapshots in most
// cases, except where noted. All variables used to access snapshots use the
// same key space. For example, an active snapshot may not share the same key
// with a committed snapshot.
//
// We cover several examples below to demonstrate the utility of a snapshot
// snapshotter.
//
// Importing a Layer
//
// To import a layer, we simply have the Snapshotter provide a list of
// mounts to be applied such that our dst will capture a changeset. We start
// out by getting a path to the layer tar file and creating a temp location to
// unpack it to:
//
// layerPath, tmpDir := getLayerPath(), mkTmpDir() // just a path to layer tar file.
//
// We start by using a Snapshotter to Prepare a new snapshot transaction, using a
// key and descending from the empty parent "":
//
// mounts, err := snapshotter.Prepare(ctx, key, "")
// if err != nil { ... }
//
// We get back a list of mounts from Snapshotter.Prepare, with the key identifying
// the active snapshot. Mount this to the temporary location with the
// following:
//
// if err := mount.All(mounts, tmpDir); err != nil { ... }
//
// Once the mounts are performed, our temporary location is ready to capture
// a diff. In practice, this works similar to a filesystem transaction. The
// next step is to unpack the layer. We have a special function unpackLayer
// that applies the contents of the layer to target location and calculates the
// DiffID of the unpacked layer (this is a requirement for docker
// implementation):
//
// layer, err := os.Open(layerPath)
// if err != nil { ... }
// digest, err := unpackLayer(tmpLocation, layer) // unpack into layer location
// if err != nil { ... }
//
// When the above completes, we should have a filesystem the represents the
// contents of the layer. Careful implementations should verify that digest
// matches the expected DiffID. When completed, we unmount the mounts:
//
// unmount(mounts) // optional, for now
//
// Now that we've verified and unpacked our layer, we commit the active
// snapshot to a name. For this example, we are just going to use the layer
// digest, but in practice, this will probably be the ChainID:
//
// if err := snapshotter.Commit(ctx, digest.String(), key); err != nil { ... }
//
// Now, we have a layer in the Snapshotter that can be accessed with the digest
// provided during commit. Once you have committed the snapshot, the active
// snapshot can be removed with the following:
//
// snapshotter.Remove(ctx, key)
//
// Importing the Next Layer
//
// Making a layer depend on the above is identical to the process described
// above except that the parent is provided as parent when calling
// Manager.Prepare, assuming a clean, unique key identifier:
//
// mounts, err := snapshotter.Prepare(ctx, key, parentDigest)
//
// We then mount, apply and commit, as we did above. The new snapshot will be
// based on the content of the previous one.
//
// Running a Container
//
// To run a container, we simply provide Snapshotter.Prepare the committed image
// snapshot as the parent. After mounting, the prepared path can
// be used directly as the container's filesystem:
//
// mounts, err := snapshotter.Prepare(ctx, containerKey, imageRootFSChainID)
//
// The returned mounts can then be passed directly to the container runtime. If
// one would like to create a new image from the filesystem, Manager.Commit is
// called:
//
// if err := snapshotter.Commit(ctx, newImageSnapshot, containerKey); err != nil { ... }
//
// Alternatively, for most container runs, Snapshotter.Remove will be called to
// signal the Snapshotter to abandon the changes.
type Snapshotter interface {
// Stat returns the info for an active or committed snapshot by name or
// key.
//
// Should be used for parent resolution, existence checks and to discern
// the kind of snapshot.
Stat(ctx context.Context, key string) (Info, error)
// Update updates the info for a snapshot.
//
// Only mutable properties of a snapshot may be updated.
Update(ctx context.Context, info Info, fieldpaths ...string) (Info, error)
// Usage returns the resource usage of an active or committed snapshot
// excluding the usage of parent snapshots.
//
// The running time of this call for active snapshots is dependent on
// implementation, but may be proportional to the size of the resource.
// Callers should take this into consideration. Implementations should
// attempt to honer context cancellation and avoid taking locks when making
// the calculation.
Usage(ctx context.Context, key string) (Usage, error)
// Mounts returns the mounts for the active snapshot transaction identified
// by key. Can be called on an read-write or readonly transaction. This is
// available only for active snapshots.
//
// This can be used to recover mounts after calling View or Prepare.
Mounts(ctx context.Context, key string) ([]mount.Mount, error)
// Prepare creates an active snapshot identified by key descending from the
// provided parent. The returned mounts can be used to mount the snapshot
// to capture changes.
//
// If a parent is provided, after performing the mounts, the destination
// will start with the content of the parent. The parent must be a
// committed snapshot. Changes to the mounted destination will be captured
// in relation to the parent. The default parent, "", is an empty
// directory.
//
// The changes may be saved to a committed snapshot by calling Commit. When
// one is done with the transaction, Remove should be called on the key.
//
// Multiple calls to Prepare or View with the same key should fail.
Prepare(ctx context.Context, key, parent string, opts ...Opt) ([]mount.Mount, error)
// View behaves identically to Prepare except the result may not be
// committed back to the snapshot snapshotter. View returns a readonly view on
// the parent, with the active snapshot being tracked by the given key.
//
// This method operates identically to Prepare, except that Mounts returned
// may have the readonly flag set. Any modifications to the underlying
// filesystem will be ignored. Implementations may perform this in a more
// efficient manner that differs from what would be attempted with
// `Prepare`.
//
// Commit may not be called on the provided key and will return an error.
// To collect the resources associated with key, Remove must be called with
// key as the argument.
View(ctx context.Context, key, parent string, opts ...Opt) ([]mount.Mount, error)
// Commit captures the changes between key and its parent into a snapshot
// identified by name. The name can then be used with the snapshotter's other
// methods to create subsequent snapshots.
//
// A committed snapshot will be created under name with the parent of the
// active snapshot.
//
// After commit, the snapshot identified by key is removed.
Commit(ctx context.Context, name, key string, opts ...Opt) error
// Remove the committed or active snapshot by the provided key.
//
// All resources associated with the key will be removed.
//
// If the snapshot is a parent of another snapshot, its children must be
// removed before proceeding.
Remove(ctx context.Context, key string) error
// Walk all snapshots in the snapshotter. For each snapshot in the
// snapshotter, the function will be called.
Walk(ctx context.Context, fn func(context.Context, Info) error) error
// Close releases the internal resources.
//
// Close is expected to be called on the end of the lifecycle of the snapshotter,
// but not mandatory.
//
// Close returns nil when it is already closed.
Close() error
}
// Opt allows setting mutable snapshot properties on creation
type Opt func(info *Info) error
// WithLabels adds labels to a created snapshot
func WithLabels(labels map[string]string) Opt {
return func(info *Info) error {
info.Labels = labels
return nil
}
}

570
snapshots/storage/bolt.go Normal file
View File

@@ -0,0 +1,570 @@
package storage
import (
"context"
"encoding/binary"
"fmt"
"strings"
"time"
"github.com/boltdb/bolt"
"github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/metadata/boltutil"
"github.com/containerd/containerd/snapshots"
"github.com/pkg/errors"
)
var (
bucketKeyStorageVersion = []byte("v1")
bucketKeySnapshot = []byte("snapshots")
bucketKeyParents = []byte("parents")
bucketKeyID = []byte("id")
bucketKeyParent = []byte("parent")
bucketKeyKind = []byte("kind")
bucketKeyInodes = []byte("inodes")
bucketKeySize = []byte("size")
// ErrNoTransaction is returned when an operation is attempted with
// a context which is not inside of a transaction.
ErrNoTransaction = errors.New("no transaction in context")
)
// 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
}
// GetInfo returns the snapshot Info directly from the metadata. Requires a
// context with a storage transaction.
func GetInfo(ctx context.Context, key string) (string, snapshots.Info, snapshots.Usage, error) {
var (
id uint64
su snapshots.Usage
si = snapshots.Info{
Name: key,
}
)
err := withSnapshotBucket(ctx, key, func(ctx context.Context, bkt, pbkt *bolt.Bucket) error {
getUsage(bkt, &su)
return readSnapshot(bkt, &id, &si)
})
if err != nil {
return "", snapshots.Info{}, snapshots.Usage{}, err
}
return fmt.Sprintf("%d", id), si, su, nil
}
// UpdateInfo updates an existing snapshot info's data
func UpdateInfo(ctx context.Context, info snapshots.Info, fieldpaths ...string) (snapshots.Info, error) {
updated := snapshots.Info{
Name: info.Name,
}
err := withBucket(ctx, func(ctx context.Context, bkt, pbkt *bolt.Bucket) error {
sbkt := bkt.Bucket([]byte(info.Name))
if sbkt == nil {
return errors.Wrap(errdefs.ErrNotFound, "snapshot does not exist")
}
if err := readSnapshot(sbkt, nil, &updated); err != nil {
return err
}
if len(fieldpaths) > 0 {
for _, path := range fieldpaths {
if strings.HasPrefix(path, "labels.") {
if updated.Labels == nil {
updated.Labels = map[string]string{}
}
key := strings.TrimPrefix(path, "labels.")
updated.Labels[key] = info.Labels[key]
continue
}
switch path {
case "labels":
updated.Labels = info.Labels
default:
return errors.Wrapf(errdefs.ErrInvalidArgument, "cannot update %q field on snapshot %q", path, info.Name)
}
}
} else {
// Set mutable fields
updated.Labels = info.Labels
}
updated.Updated = time.Now().UTC()
if err := boltutil.WriteTimestamps(sbkt, updated.Created, updated.Updated); err != nil {
return err
}
return boltutil.WriteLabels(sbkt, updated.Labels)
})
if err != nil {
return snapshots.Info{}, err
}
return updated, nil
}
// WalkInfo iterates through all metadata Info for the stored snapshots and
// calls the provided function for each. Requires a context with a storage
// transaction.
func WalkInfo(ctx context.Context, fn func(context.Context, snapshots.Info) error) error {
return withBucket(ctx, func(ctx context.Context, bkt, pbkt *bolt.Bucket) error {
return bkt.ForEach(func(k, v []byte) error {
// skip non buckets
if v != nil {
return nil
}
var (
sbkt = bkt.Bucket(k)
si = snapshots.Info{
Name: string(k),
}
)
if err := readSnapshot(sbkt, nil, &si); err != nil {
return err
}
return fn(ctx, si)
})
})
}
// GetSnapshot returns the metadata for the active or view snapshot transaction
// referenced by the given key. Requires a context with a storage transaction.
func GetSnapshot(ctx context.Context, key string) (s Snapshot, err error) {
err = withBucket(ctx, func(ctx context.Context, bkt, pbkt *bolt.Bucket) error {
sbkt := bkt.Bucket([]byte(key))
if sbkt == nil {
return errors.Wrap(errdefs.ErrNotFound, "snapshot does not exist")
}
s.ID = fmt.Sprintf("%d", readID(sbkt))
s.Kind = readKind(sbkt)
if s.Kind != snapshots.KindActive && s.Kind != snapshots.KindView {
return errors.Wrapf(errdefs.ErrFailedPrecondition, "requested snapshot %v not active or view", key)
}
if parentKey := sbkt.Get(bucketKeyParent); len(parentKey) > 0 {
spbkt := bkt.Bucket(parentKey)
if spbkt == nil {
return errors.Wrap(errdefs.ErrNotFound, "parent does not exist")
}
s.ParentIDs, err = parents(bkt, spbkt, readID(spbkt))
if err != nil {
return errors.Wrap(err, "failed to get parent chain")
}
}
return nil
})
if err != nil {
return Snapshot{}, err
}
return
}
// CreateSnapshot inserts a record for an active or view snapshot with the provided parent.
func CreateSnapshot(ctx context.Context, kind snapshots.Kind, key, parent string, opts ...snapshots.Opt) (s Snapshot, err error) {
switch kind {
case snapshots.KindActive, snapshots.KindView:
default:
return Snapshot{}, errors.Wrapf(errdefs.ErrInvalidArgument, "snapshot type %v invalid; only snapshots of type Active or View can be created", kind)
}
var base snapshots.Info
for _, opt := range opts {
if err := opt(&base); err != nil {
return Snapshot{}, err
}
}
err = createBucketIfNotExists(ctx, func(ctx context.Context, bkt, pbkt *bolt.Bucket) error {
var (
spbkt *bolt.Bucket
)
if parent != "" {
spbkt = bkt.Bucket([]byte(parent))
if spbkt == nil {
return errors.Wrap(errdefs.ErrNotFound, "missing parent bucket")
}
if readKind(spbkt) != snapshots.KindCommitted {
return errors.Wrap(errdefs.ErrInvalidArgument, "parent is not committed snapshot")
}
}
sbkt, err := bkt.CreateBucket([]byte(key))
if err != nil {
if err == bolt.ErrBucketExists {
err = errors.Wrapf(errdefs.ErrAlreadyExists, "snapshot %v", key)
}
return err
}
id, err := bkt.NextSequence()
if err != nil {
return errors.Wrap(err, "unable to get identifier")
}
t := time.Now().UTC()
si := snapshots.Info{
Parent: parent,
Kind: kind,
Labels: base.Labels,
Created: t,
Updated: t,
}
if err := putSnapshot(sbkt, id, si); err != nil {
return err
}
if spbkt != nil {
pid := readID(spbkt)
// 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(pid, id), []byte(key)); err != nil {
return errors.Wrap(err, "failed to write parent link")
}
s.ParentIDs, err = parents(bkt, spbkt, pid)
if err != nil {
return errors.Wrap(err, "failed to get parent chain")
}
}
s.ID = fmt.Sprintf("%d", id)
s.Kind = kind
return nil
})
if err != nil {
return Snapshot{}, err
}
return
}
// Remove removes a snapshot from the metastore. The string identifier for the
// snapshot is returned as well as the kind. The provided context must contain a
// writable transaction.
func Remove(ctx context.Context, key string) (string, snapshots.Kind, error) {
var (
id uint64
si snapshots.Info
)
if err := withBucket(ctx, func(ctx context.Context, bkt, pbkt *bolt.Bucket) error {
sbkt := bkt.Bucket([]byte(key))
if sbkt == nil {
return errors.Wrapf(errdefs.ErrNotFound, "snapshot %v", key)
}
if err := readSnapshot(sbkt, &id, &si); err != nil {
errors.Wrapf(err, "failed to read snapshot %s", key)
}
if pbkt != nil {
k, _ := pbkt.Cursor().Seek(parentPrefixKey(id))
if getParentPrefix(k) == id {
return errors.Wrap(errdefs.ErrFailedPrecondition, "cannot remove snapshot with child")
}
if si.Parent != "" {
spbkt := bkt.Bucket([]byte(si.Parent))
if spbkt == nil {
return errors.Wrapf(errdefs.ErrNotFound, "snapshot %v", key)
}
if err := pbkt.Delete(parentKey(readID(spbkt), id)); err != nil {
return errors.Wrap(err, "failed to delete parent link")
}
}
}
if err := bkt.DeleteBucket([]byte(key)); err != nil {
return errors.Wrap(err, "failed to delete snapshot")
}
return nil
}); err != nil {
return "", 0, err
}
return fmt.Sprintf("%d", id), si.Kind, nil
}
// CommitActive 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. The provided context
// must contain a writable transaction.
func CommitActive(ctx context.Context, key, name string, usage snapshots.Usage, opts ...snapshots.Opt) (string, error) {
var (
id uint64
base snapshots.Info
)
for _, opt := range opts {
if err := opt(&base); err != nil {
return "", err
}
}
if err := withBucket(ctx, func(ctx context.Context, bkt, pbkt *bolt.Bucket) error {
dbkt, err := bkt.CreateBucket([]byte(name))
if err != nil {
if err == bolt.ErrBucketExists {
err = errdefs.ErrAlreadyExists
}
return errors.Wrapf(err, "committed snapshot %v", name)
}
sbkt := bkt.Bucket([]byte(key))
if sbkt == nil {
return errors.Wrap(errdefs.ErrNotFound, "failed to get active snapshot")
}
var si snapshots.Info
if err := readSnapshot(sbkt, &id, &si); err != nil {
return errors.Wrap(err, "failed to read snapshot")
}
if si.Kind != snapshots.KindActive {
return errors.Wrapf(errdefs.ErrFailedPrecondition, "snapshot %v is not active", name)
}
si.Kind = snapshots.KindCommitted
si.Created = time.Now().UTC()
si.Updated = si.Created
// Replace labels, do not inherit
si.Labels = base.Labels
if err := putSnapshot(dbkt, id, si); err != nil {
return err
}
if err := putUsage(dbkt, usage); err != nil {
return err
}
if err := bkt.DeleteBucket([]byte(key)); err != nil {
return errors.Wrap(err, "failed to delete active")
}
if si.Parent != "" {
spbkt := bkt.Bucket([]byte(si.Parent))
if spbkt == nil {
return errors.Wrap(errdefs.ErrNotFound, "missing parent")
}
pid := readID(spbkt)
// Updates parent back link to use new key
if err := pbkt.Put(parentKey(pid, id), []byte(name)); err != nil {
return errors.Wrap(err, "failed to update parent link")
}
}
return nil
}); err != nil {
return "", err
}
return fmt.Sprintf("%d", id), nil
}
func withSnapshotBucket(ctx context.Context, key string, fn func(context.Context, *bolt.Bucket, *bolt.Bucket) error) error {
tx, ok := ctx.Value(transactionKey{}).(*bolt.Tx)
if !ok {
return ErrNoTransaction
}
bkt := tx.Bucket(bucketKeyStorageVersion)
if bkt == nil {
return errors.Wrap(errdefs.ErrNotFound, "bucket does not exist")
}
bkt = bkt.Bucket(bucketKeySnapshot)
if bkt == nil {
return errors.Wrap(errdefs.ErrNotFound, "snapshots bucket does not exist")
}
bkt = bkt.Bucket([]byte(key))
if bkt == nil {
return errors.Wrap(errdefs.ErrNotFound, "snapshot does not exist")
}
return fn(ctx, bkt, bkt.Bucket(bucketKeyParents))
}
func withBucket(ctx context.Context, fn func(context.Context, *bolt.Bucket, *bolt.Bucket) error) error {
tx, ok := ctx.Value(transactionKey{}).(*bolt.Tx)
if !ok {
return ErrNoTransaction
}
bkt := tx.Bucket(bucketKeyStorageVersion)
if bkt == nil {
return errors.Wrap(errdefs.ErrNotFound, "bucket does not exist")
}
return fn(ctx, bkt.Bucket(bucketKeySnapshot), bkt.Bucket(bucketKeyParents))
}
func createBucketIfNotExists(ctx context.Context, fn func(context.Context, *bolt.Bucket, *bolt.Bucket) error) error {
tx, ok := ctx.Value(transactionKey{}).(*bolt.Tx)
if !ok {
return ErrNoTransaction
}
bkt, err := 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 parents(bkt, pbkt *bolt.Bucket, parent uint64) (parents []string, err error) {
for {
parents = append(parents, fmt.Sprintf("%d", parent))
parentKey := pbkt.Get(bucketKeyParent)
if len(parentKey) == 0 {
return
}
pbkt = bkt.Bucket(parentKey)
if pbkt == nil {
return nil, errors.Wrap(errdefs.ErrNotFound, "missing parent")
}
parent = readID(pbkt)
}
}
func readKind(bkt *bolt.Bucket) (k snapshots.Kind) {
kind := bkt.Get(bucketKeyKind)
if len(kind) == 1 {
k = snapshots.Kind(kind[0])
}
return
}
func readID(bkt *bolt.Bucket) uint64 {
id, _ := binary.Uvarint(bkt.Get(bucketKeyID))
return id
}
func readSnapshot(bkt *bolt.Bucket, id *uint64, si *snapshots.Info) error {
if id != nil {
*id = readID(bkt)
}
if si != nil {
si.Kind = readKind(bkt)
si.Parent = string(bkt.Get(bucketKeyParent))
if err := boltutil.ReadTimestamps(bkt, &si.Created, &si.Updated); err != nil {
return err
}
labels, err := boltutil.ReadLabels(bkt)
if err != nil {
return err
}
si.Labels = labels
}
return nil
}
func putSnapshot(bkt *bolt.Bucket, id uint64, si snapshots.Info) error {
idEncoded, err := encodeID(id)
if err != nil {
return err
}
updates := [][2][]byte{
{bucketKeyID, idEncoded},
{bucketKeyKind, []byte{byte(si.Kind)}},
}
if si.Parent != "" {
updates = append(updates, [2][]byte{bucketKeyParent, []byte(si.Parent)})
}
for _, v := range updates {
if err := bkt.Put(v[0], v[1]); err != nil {
return err
}
}
if err := boltutil.WriteTimestamps(bkt, si.Created, si.Updated); err != nil {
return err
}
return boltutil.WriteLabels(bkt, si.Labels)
}
func getUsage(bkt *bolt.Bucket, usage *snapshots.Usage) {
usage.Inodes, _ = binary.Varint(bkt.Get(bucketKeyInodes))
usage.Size, _ = binary.Varint(bkt.Get(bucketKeySize))
}
func putUsage(bkt *bolt.Bucket, usage snapshots.Usage) error {
for _, v := range []struct {
key []byte
value int64
}{
{bucketKeyInodes, usage.Inodes},
{bucketKeySize, usage.Size},
} {
e, err := encodeSize(v.value)
if err != nil {
return err
}
if err := bkt.Put(v.key, e); err != nil {
return err
}
}
return nil
}
func encodeSize(size int64) ([]byte, error) {
var (
buf [binary.MaxVarintLen64]byte
sizeEncoded = buf[:]
)
sizeEncoded = sizeEncoded[:binary.PutVarint(sizeEncoded, size)]
if len(sizeEncoded) == 0 {
return nil, fmt.Errorf("failed encoding size = %v", size)
}
return sizeEncoded, nil
}
func encodeID(id uint64) ([]byte, error) {
var (
buf [binary.MaxVarintLen64]byte
idEncoded = buf[:]
)
idEncoded = idEncoded[:binary.PutUvarint(idEncoded, id)]
if len(idEncoded) == 0 {
return nil, fmt.Errorf("failed encoding id = %v", id)
}
return idEncoded, nil
}

View File

@@ -0,0 +1,22 @@
package storage
import (
"path/filepath"
"testing"
// Does not require root but flag must be defined for snapshot tests
_ "github.com/containerd/containerd/testutil"
)
func TestMetastore(t *testing.T) {
MetaStoreSuite(t, "Metastore", func(root string) (*MetaStore, error) {
return NewMetaStore(filepath.Join(root, "metadata.db"))
})
}
func BenchmarkSuite(b *testing.B) {
Benchmarks(b, "BoltDBBench", func(root string) (*MetaStore, error) {
return NewMetaStore(filepath.Join(root, "metadata.db"))
})
}

View File

@@ -0,0 +1,99 @@
// Package storage provides a metadata storage implementation for snapshot
// drivers. Drive implementations are responsible for starting and managing
// transactions using the defined context creator. This storage package uses
// BoltDB for storing metadata. Access to the raw boltdb transaction is not
// provided, but the stored object is provided by the proto subpackage.
package storage
import (
"context"
"sync"
"github.com/boltdb/bolt"
"github.com/containerd/containerd/snapshots"
"github.com/pkg/errors"
)
// Transactor is used to finalize an active transaction.
type Transactor interface {
// Commit commits any changes made during the transaction. On error a
// caller is expected to clean up any resources which would have relied
// on data mutated as part of this transaction. Only writable
// transactions can commit, non-writable must call Rollback.
Commit() error
// Rollback rolls back any changes made during the transaction. This
// must be called on all non-writable transactions and aborted writable
// transaction.
Rollback() error
}
// Snapshot hold the metadata for an active or view snapshot transaction. The
// ParentIDs hold the snapshot identifiers for the committed snapshots this
// active or view 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 Snapshot struct {
Kind snapshots.Kind
ID string
ParentIDs []string
}
// 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 struct {
dbfile string
dbL sync.Mutex
db *bolt.DB
}
// 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(dbfile string) (*MetaStore, error) {
return &MetaStore{
dbfile: dbfile,
}, nil
}
type transactionKey struct{}
// TransactionContext creates a new transaction context. The writable value
// should be set to true for transactions which are expected to mutate data.
func (ms *MetaStore) TransactionContext(ctx context.Context, writable bool) (context.Context, Transactor, error) {
ms.dbL.Lock()
if ms.db == nil {
db, err := bolt.Open(ms.dbfile, 0600, nil)
if err != nil {
ms.dbL.Unlock()
return ctx, nil, errors.Wrap(err, "failed to open database file")
}
ms.db = db
}
ms.dbL.Unlock()
tx, err := ms.db.Begin(writable)
if err != nil {
return ctx, nil, errors.Wrap(err, "failed to start transaction")
}
ctx = context.WithValue(ctx, transactionKey{}, tx)
return ctx, tx, nil
}
// Close closes the metastore and any underlying database connections
func (ms *MetaStore) Close() error {
ms.dbL.Lock()
defer ms.dbL.Unlock()
if ms.db == nil {
return nil
}
return ms.db.Close()
}

View File

@@ -0,0 +1,217 @@
package storage
import (
"context"
"fmt"
"io/ioutil"
"os"
"testing"
"github.com/containerd/containerd/snapshots"
)
// Benchmarks returns a benchmark suite using the provided metadata store
// creation method
func Benchmarks(b *testing.B, name string, metaFn metaFactory) {
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 metaFactory, fn func(context.Context, *testing.B, *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(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 metaFactory) 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(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 metaFactory) 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(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 *MetaStore, active, base string) error {
if _, err := CreateSnapshot(ctx, snapshots.KindActive, "bottom", ""); err != nil {
return err
}
if _, err := CommitActive(ctx, "bottom", base, snapshots.Usage{}); err != nil {
return err
}
_, err := CreateSnapshot(ctx, snapshots.KindActive, active, base)
return err
}
func statActiveBenchmark(ctx context.Context, b *testing.B, ms *MetaStore) {
if err := createActiveFromBase(ctx, ms, "active", "base"); err != nil {
b.Fatal(err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _, _, err := GetInfo(ctx, "active")
if err != nil {
b.Fatal(err)
}
}
}
func statCommittedBenchmark(ctx context.Context, b *testing.B, ms *MetaStore) {
if err := createActiveFromBase(ctx, ms, "active", "base"); err != nil {
b.Fatal(err)
}
if _, err := CommitActive(ctx, "active", "committed", snapshots.Usage{}); err != nil {
b.Fatal(err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _, _, err := GetInfo(ctx, "committed")
if err != nil {
b.Fatal(err)
}
}
}
func createActiveBenchmark(ctx context.Context, b *testing.B, ms *MetaStore) {
for i := 0; i < b.N; i++ {
if _, err := CreateSnapshot(ctx, snapshots.KindActive, "active", ""); err != nil {
b.Fatal(err)
}
b.StopTimer()
if _, _, err := Remove(ctx, "active"); err != nil {
b.Fatal(err)
}
b.StartTimer()
}
}
func removeBenchmark(ctx context.Context, b *testing.B, ms *MetaStore) {
for i := 0; i < b.N; i++ {
b.StopTimer()
if _, err := CreateSnapshot(ctx, snapshots.KindActive, "active", ""); err != nil {
b.Fatal(err)
}
b.StartTimer()
if _, _, err := Remove(ctx, "active"); err != nil {
b.Fatal(err)
}
}
}
func commitBenchmark(ctx context.Context, b *testing.B, ms *MetaStore) {
b.StopTimer()
for i := 0; i < b.N; i++ {
if _, err := CreateSnapshot(ctx, snapshots.KindActive, "active", ""); err != nil {
b.Fatal(err)
}
b.StartTimer()
if _, err := CommitActive(ctx, "active", "committed", snapshots.Usage{}); err != nil {
b.Fatal(err)
}
b.StopTimer()
if _, _, err := Remove(ctx, "committed"); err != nil {
b.Fatal(err)
}
}
}
func getActiveBenchmark(ctx context.Context, b *testing.B, ms *MetaStore) {
var base string
for i := 1; i <= 10; i++ {
if _, err := CreateSnapshot(ctx, snapshots.KindActive, "tmp", base); err != nil {
b.Fatalf("create active failed: %+v", err)
}
base = fmt.Sprintf("base-%d", i)
if _, err := CommitActive(ctx, "tmp", base, snapshots.Usage{}); err != nil {
b.Fatalf("commit failed: %+v", err)
}
}
if _, err := CreateSnapshot(ctx, snapshots.KindActive, "active", base); err != nil {
b.Fatalf("create active failed: %+v", err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
if _, err := GetSnapshot(ctx, "active"); err != nil {
b.Fatal(err)
}
}
}

View File

@@ -0,0 +1,634 @@
package storage
import (
"context"
"fmt"
"io/ioutil"
"os"
"testing"
"time"
"github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/snapshots"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)
type testFunc func(context.Context, *testing.T, *MetaStore)
type metaFactory func(string) (*MetaStore, error)
type populateFunc func(context.Context, *MetaStore) error
// MetaStoreSuite runs a test suite on the metastore given a factory function.
func MetaStoreSuite(t *testing.T, name string, meta func(root string) (*MetaStore, error)) {
t.Run("GetInfo", makeTest(t, name, meta, inReadTransaction(testGetInfo, basePopulate)))
t.Run("GetInfoNotExist", makeTest(t, name, meta, inReadTransaction(testGetInfoNotExist, basePopulate)))
t.Run("GetInfoEmptyDB", makeTest(t, name, meta, inReadTransaction(testGetInfoNotExist, nil)))
t.Run("Walk", makeTest(t, name, meta, inReadTransaction(testWalk, basePopulate)))
t.Run("GetSnapshot", makeTest(t, name, meta, testGetSnapshot))
t.Run("GetSnapshotNotExist", makeTest(t, name, meta, inReadTransaction(testGetSnapshotNotExist, basePopulate)))
t.Run("GetSnapshotCommitted", makeTest(t, name, meta, inReadTransaction(testGetSnapshotCommitted, basePopulate)))
t.Run("GetSnapshotEmptyDB", makeTest(t, name, meta, inReadTransaction(testGetSnapshotNotExist, 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("CommitViewFails", makeTest(t, name, meta, inWriteTransaction(testCommitViewFails)))
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)))
t.Run("ParentIDs", makeTest(t, name, meta, inWriteTransaction(testParents)))
}
// 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(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 *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.Fatalf("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 *MetaStore) {
ctx, tx, err := ms.TransactionContext(ctx, true)
if err != nil {
t.Fatalf("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.Fatalf("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 *MetaStore) error {
if _, err := CreateSnapshot(ctx, snapshots.KindActive, "committed-tmp-1", ""); err != nil {
return errors.Wrap(err, "failed to create active")
}
if _, err := CommitActive(ctx, "committed-tmp-1", "committed-1", snapshots.Usage{Size: 1}); err != nil {
return errors.Wrap(err, "failed to create active")
}
if _, err := CreateSnapshot(ctx, snapshots.KindActive, "committed-tmp-2", "committed-1"); err != nil {
return errors.Wrap(err, "failed to create active")
}
if _, err := CommitActive(ctx, "committed-tmp-2", "committed-2", snapshots.Usage{Size: 2}); err != nil {
return errors.Wrap(err, "failed to create active")
}
if _, err := CreateSnapshot(ctx, snapshots.KindActive, "active-1", ""); err != nil {
return errors.Wrap(err, "failed to create active")
}
if _, err := CreateSnapshot(ctx, snapshots.KindActive, "active-2", "committed-1"); err != nil {
return errors.Wrap(err, "failed to create active")
}
if _, err := CreateSnapshot(ctx, snapshots.KindActive, "active-3", "committed-2"); err != nil {
return errors.Wrap(err, "failed to create active")
}
if _, err := CreateSnapshot(ctx, snapshots.KindView, "view-1", ""); err != nil {
return errors.Wrap(err, "failed to create active")
}
if _, err := CreateSnapshot(ctx, snapshots.KindView, "view-2", "committed-2"); err != nil {
return errors.Wrap(err, "failed to create active")
}
return nil
}
var baseInfo = map[string]snapshots.Info{
"committed-1": {
Name: "committed-1",
Parent: "",
Kind: snapshots.KindCommitted,
},
"committed-2": {
Name: "committed-2",
Parent: "committed-1",
Kind: snapshots.KindCommitted,
},
"active-1": {
Name: "active-1",
Parent: "",
Kind: snapshots.KindActive,
},
"active-2": {
Name: "active-2",
Parent: "committed-1",
Kind: snapshots.KindActive,
},
"active-3": {
Name: "active-3",
Parent: "committed-2",
Kind: snapshots.KindActive,
},
"view-1": {
Name: "view-1",
Parent: "",
Kind: snapshots.KindView,
},
"view-2": {
Name: "view-2",
Parent: "committed-2",
Kind: snapshots.KindView,
},
}
func assertNotExist(t *testing.T, err error) {
if err == nil {
t.Fatal("Expected not exist error")
}
if !errdefs.IsNotFound(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 !errdefs.IsFailedPrecondition(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 !errdefs.IsInvalidArgument(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 !errdefs.IsAlreadyExists(err) {
t.Fatalf("Expected exist error, got %+v", err)
}
}
func testGetInfo(ctx context.Context, t *testing.T, ms *MetaStore) {
for key, expected := range baseInfo {
_, info, _, err := GetInfo(ctx, key)
if err != nil {
t.Fatalf("GetInfo on %v failed: %+v", key, err)
}
// TODO: Check timestamp range
info.Created = time.Time{}
info.Updated = time.Time{}
assert.Equal(t, expected, info)
}
}
func testGetInfoNotExist(ctx context.Context, t *testing.T, ms *MetaStore) {
_, _, _, err := GetInfo(ctx, "active-not-exist")
assertNotExist(t, err)
}
func testWalk(ctx context.Context, t *testing.T, ms *MetaStore) {
found := map[string]snapshots.Info{}
err := WalkInfo(ctx, func(ctx context.Context, info snapshots.Info) error {
if _, ok := found[info.Name]; ok {
return errors.Errorf("entry already encountered")
}
// TODO: Check time range
info.Created = time.Time{}
info.Updated = time.Time{}
found[info.Name] = info
return nil
})
if err != nil {
t.Fatalf("Walk failed: %+v", err)
}
assert.Equal(t, baseInfo, found)
}
func testGetSnapshot(ctx context.Context, t *testing.T, ms *MetaStore) {
snapshotMap := map[string]Snapshot{}
populate := func(ctx context.Context, ms *MetaStore) error {
if _, err := CreateSnapshot(ctx, snapshots.KindActive, "committed-tmp-1", ""); err != nil {
return errors.Wrap(err, "failed to create active")
}
if _, err := CommitActive(ctx, "committed-tmp-1", "committed-1", snapshots.Usage{}); err != nil {
return errors.Wrap(err, "failed to create active")
}
for _, opts := range []struct {
Kind snapshots.Kind
Name string
Parent string
}{
{
Name: "active-1",
Kind: snapshots.KindActive,
},
{
Name: "active-2",
Parent: "committed-1",
Kind: snapshots.KindActive,
},
{
Name: "view-1",
Kind: snapshots.KindView,
},
{
Name: "view-2",
Parent: "committed-1",
Kind: snapshots.KindView,
},
} {
active, err := CreateSnapshot(ctx, opts.Kind, opts.Name, opts.Parent)
if err != nil {
return errors.Wrap(err, "failed to create active")
}
snapshotMap[opts.Name] = active
}
return nil
}
test := func(ctx context.Context, t *testing.T, ms *MetaStore) {
for key, expected := range snapshotMap {
s, err := GetSnapshot(ctx, key)
if err != nil {
t.Fatalf("Failed to get active: %+v", err)
}
assert.Equal(t, expected, s)
}
}
inReadTransaction(test, populate)(ctx, t, ms)
}
func testGetSnapshotCommitted(ctx context.Context, t *testing.T, ms *MetaStore) {
_, err := GetSnapshot(ctx, "committed-1")
assertNotActive(t, err)
}
func testGetSnapshotNotExist(ctx context.Context, t *testing.T, ms *MetaStore) {
_, err := GetSnapshot(ctx, "active-not-exist")
assertNotExist(t, err)
}
func testCreateActive(ctx context.Context, t *testing.T, ms *MetaStore) {
a1, err := CreateSnapshot(ctx, snapshots.KindActive, "active-1", "")
if err != nil {
t.Fatal(err)
}
if a1.Kind != snapshots.KindActive {
t.Fatal("Expected writable active")
}
a2, err := CreateSnapshot(ctx, snapshots.KindView, "view-1", "")
if err != nil {
t.Fatal(err)
}
if a2.ID == a1.ID {
t.Fatal("Returned active identifiers must be unique")
}
if a2.Kind != snapshots.KindView {
t.Fatal("Expected a view")
}
commitID, err := CommitActive(ctx, "active-1", "committed-1", snapshots.Usage{})
if err != nil {
t.Fatal(err)
}
if commitID != a1.ID {
t.Fatal("Snapshot identifier must not change on commit")
}
a3, err := CreateSnapshot(ctx, snapshots.KindActive, "active-3", "committed-1")
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.Fatalf("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.Kind != snapshots.KindActive {
t.Fatal("Expected writable active")
}
a4, err := CreateSnapshot(ctx, snapshots.KindView, "view-2", "committed-1")
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.Fatalf("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.Kind != snapshots.KindView {
t.Fatal("Expected a view")
}
}
func testCreateActiveExist(ctx context.Context, t *testing.T, ms *MetaStore) {
if err := basePopulate(ctx, ms); err != nil {
t.Fatalf("Populate failed: %+v", err)
}
_, err := CreateSnapshot(ctx, snapshots.KindActive, "active-1", "")
assertExist(t, err)
_, err = CreateSnapshot(ctx, snapshots.KindActive, "committed-1", "")
assertExist(t, err)
}
func testCreateActiveNotExist(ctx context.Context, t *testing.T, ms *MetaStore) {
_, err := CreateSnapshot(ctx, snapshots.KindActive, "active-1", "does-not-exist")
assertNotExist(t, err)
}
func testCreateActiveFromActive(ctx context.Context, t *testing.T, ms *MetaStore) {
if err := basePopulate(ctx, ms); err != nil {
t.Fatalf("Populate failed: %+v", err)
}
_, err := CreateSnapshot(ctx, snapshots.KindActive, "active-new", "active-1")
assertNotCommitted(t, err)
}
func testCommit(ctx context.Context, t *testing.T, ms *MetaStore) {
a1, err := CreateSnapshot(ctx, snapshots.KindActive, "active-1", "")
if err != nil {
t.Fatal(err)
}
if a1.Kind != snapshots.KindActive {
t.Fatal("Expected writable active")
}
commitID, err := CommitActive(ctx, "active-1", "committed-1", snapshots.Usage{})
if err != nil {
t.Fatal(err)
}
if commitID != a1.ID {
t.Fatal("Snapshot identifier must not change on commit")
}
_, err = GetSnapshot(ctx, "active-1")
assertNotExist(t, err)
_, err = GetSnapshot(ctx, "committed-1")
assertNotActive(t, err)
}
func testCommitExist(ctx context.Context, t *testing.T, ms *MetaStore) {
if err := basePopulate(ctx, ms); err != nil {
t.Fatalf("Populate failed: %+v", err)
}
_, err := CommitActive(ctx, "active-1", "committed-1", snapshots.Usage{})
assertExist(t, err)
}
func testCommitCommitted(ctx context.Context, t *testing.T, ms *MetaStore) {
if err := basePopulate(ctx, ms); err != nil {
t.Fatalf("Populate failed: %+v", err)
}
_, err := CommitActive(ctx, "committed-1", "committed-3", snapshots.Usage{})
assertNotActive(t, err)
}
func testCommitViewFails(ctx context.Context, t *testing.T, ms *MetaStore) {
if err := basePopulate(ctx, ms); err != nil {
t.Fatalf("Populate failed: %+v", err)
}
_, err := CommitActive(ctx, "view-1", "committed-3", snapshots.Usage{})
if err == nil {
t.Fatal("Expected error committing readonly active")
}
}
func testRemove(ctx context.Context, t *testing.T, ms *MetaStore) {
a1, err := CreateSnapshot(ctx, snapshots.KindActive, "active-1", "")
if err != nil {
t.Fatal(err)
}
commitID, err := CommitActive(ctx, "active-1", "committed-1", snapshots.Usage{})
if err != nil {
t.Fatal(err)
}
if commitID != a1.ID {
t.Fatal("Snapshot identifier must not change on commit")
}
a2, err := CreateSnapshot(ctx, snapshots.KindView, "view-1", "committed-1")
if err != nil {
t.Fatal(err)
}
a3, err := CreateSnapshot(ctx, snapshots.KindView, "view-2", "committed-1")
if err != nil {
t.Fatal(err)
}
_, _, err = Remove(ctx, "active-1")
assertNotExist(t, err)
r3, k3, err := Remove(ctx, "view-2")
if err != nil {
t.Fatal(err)
}
if r3 != a3.ID {
t.Fatal("Expected remove ID to match create ID")
}
if k3 != snapshots.KindView {
t.Fatalf("Expected view kind, got %v", k3)
}
r2, k2, err := Remove(ctx, "view-1")
if err != nil {
t.Fatal(err)
}
if r2 != a2.ID {
t.Fatal("Expected remove ID to match create ID")
}
if k2 != snapshots.KindView {
t.Fatalf("Expected view kind, got %v", k2)
}
r1, k1, err := Remove(ctx, "committed-1")
if err != nil {
t.Fatal(err)
}
if r1 != commitID {
t.Fatal("Expected remove ID to match commit ID")
}
if k1 != snapshots.KindCommitted {
t.Fatalf("Expected committed kind, got %v", k1)
}
}
func testRemoveWithChildren(ctx context.Context, t *testing.T, ms *MetaStore) {
if err := basePopulate(ctx, ms); err != nil {
t.Fatalf("Populate failed: %+v", err)
}
_, _, err := Remove(ctx, "committed-1")
if err == nil {
t.Fatalf("Expected removal of snapshot with children to error")
}
_, _, err = 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 *MetaStore) {
_, _, err := Remove(ctx, "does-not-exist")
assertNotExist(t, err)
}
func testParents(ctx context.Context, t *testing.T, ms *MetaStore) {
if err := basePopulate(ctx, ms); err != nil {
t.Fatalf("Populate failed: %+v", err)
}
testcases := []struct {
Name string
Parents int
}{
{"committed-1", 0},
{"committed-2", 1},
{"active-1", 0},
{"active-2", 1},
{"active-3", 2},
{"view-1", 0},
{"view-2", 2},
}
for _, tc := range testcases {
name := tc.Name
expectedID := ""
expectedParents := []string{}
for i := tc.Parents; i >= 0; i-- {
sid, info, _, err := GetInfo(ctx, name)
if err != nil {
t.Fatalf("Failed to get snapshot %s: %v", tc.Name, err)
}
var (
id string
parents []string
)
if info.Kind == snapshots.KindCommitted {
// When commited, create view and resolve from view
nid := fmt.Sprintf("test-%s-%d", tc.Name, i)
s, err := CreateSnapshot(ctx, snapshots.KindView, nid, name)
if err != nil {
t.Fatalf("Failed to get snapshot %s: %v", tc.Name, err)
}
if len(s.ParentIDs) != i+1 {
t.Fatalf("Unexpected number of parents for view of %s: %d, expected %d", name, len(s.ParentIDs), i+1)
}
id = s.ParentIDs[0]
parents = s.ParentIDs[1:]
} else {
s, err := GetSnapshot(ctx, name)
if err != nil {
t.Fatalf("Failed to get snapshot %s: %v", tc.Name, err)
}
if len(s.ParentIDs) != i {
t.Fatalf("Unexpected number of parents for %s: %d, expected %d", name, len(s.ParentIDs), i)
}
id = s.ID
parents = s.ParentIDs
}
if sid != id {
t.Fatalf("Info ID mismatched resolved snapshot ID for %s, %s vs %s", name, sid, id)
}
if expectedID != "" {
if id != expectedID {
t.Errorf("Unexpected ID of parent: %s, expected %s", id, expectedID)
}
}
if len(expectedParents) > 0 {
for j := range expectedParents {
if parents[j] != expectedParents[j] {
t.Errorf("Unexpected ID in parent array at %d: %s, expected %s", j, parents[j], expectedParents[j])
}
}
}
if i > 0 {
name = info.Parent
expectedID = parents[0]
expectedParents = parents[1:]
}
}
}
}

View File

@@ -0,0 +1,154 @@
package testsuite
import (
"context"
"fmt"
"io/ioutil"
"math/rand"
"os"
"github.com/containerd/containerd/fs/fstest"
"github.com/containerd/containerd/mount"
"github.com/containerd/containerd/snapshots"
"github.com/pkg/errors"
)
func applyToMounts(m []mount.Mount, work string, a fstest.Applier) (err error) {
td, err := ioutil.TempDir(work, "prepare")
if err != nil {
return errors.Wrap(err, "failed to create temp dir")
}
defer os.RemoveAll(td)
if err := mount.All(m, td); err != nil {
return errors.Wrap(err, "failed to mount")
}
defer func() {
if err1 := mount.UnmountAll(td, umountflags); err == nil {
err = errors.Wrap(err1, "failed to unmount")
}
}()
return a.Apply(td)
}
// createSnapshot creates a new snapshot in the snapshotter
// given an applier to run on top of the given parent.
func createSnapshot(ctx context.Context, sn snapshots.Snapshotter, parent, work string, a fstest.Applier) (string, error) {
n := fmt.Sprintf("%p-%d", a, rand.Int())
prepare := fmt.Sprintf("%s-prepare", n)
m, err := sn.Prepare(ctx, prepare, parent, opt)
if err != nil {
return "", errors.Wrap(err, "failed to prepare snapshot")
}
if err := applyToMounts(m, work, a); err != nil {
return "", errors.Wrap(err, "failed to apply")
}
if err := sn.Commit(ctx, n, prepare, opt); err != nil {
return "", errors.Wrap(err, "failed to commit")
}
return n, nil
}
func checkSnapshot(ctx context.Context, sn snapshots.Snapshotter, work, name, check string) (err error) {
td, err := ioutil.TempDir(work, "check")
if err != nil {
return errors.Wrap(err, "failed to create temp dir")
}
defer func() {
if err1 := os.RemoveAll(td); err == nil {
err = errors.Wrapf(err1, "failed to remove temporary directory %s", td)
}
}()
view := fmt.Sprintf("%s-view", name)
m, err := sn.View(ctx, view, name, opt)
if err != nil {
return errors.Wrap(err, "failed to create view")
}
defer func() {
if err1 := sn.Remove(ctx, view); err == nil {
err = errors.Wrap(err1, "failed to remove view")
}
}()
if err := mount.All(m, td); err != nil {
return errors.Wrap(err, "failed to mount")
}
defer func() {
if err1 := mount.UnmountAll(td, umountflags); err == nil {
err = errors.Wrap(err1, "failed to unmount view")
}
}()
if err := fstest.CheckDirectoryEqual(check, td); err != nil {
return errors.Wrap(err, "check directory failed")
}
return nil
}
// checkSnapshots creates a new chain of snapshots in the given snapshotter
// using the provided appliers, checking each snapshot created in a view
// against the changes applied to a single directory.
func checkSnapshots(ctx context.Context, sn snapshots.Snapshotter, work string, as ...fstest.Applier) error {
td, err := ioutil.TempDir(work, "flat")
if err != nil {
return errors.Wrap(err, "failed to create temp dir")
}
defer os.RemoveAll(td)
var parentID string
for i, a := range as {
s, err := createSnapshot(ctx, sn, parentID, work, a)
if err != nil {
return errors.Wrapf(err, "failed to create snapshot %d", i+1)
}
if err := a.Apply(td); err != nil {
return errors.Wrapf(err, "failed to apply to check directory on %d", i+1)
}
if err := checkSnapshot(ctx, sn, work, s, td); err != nil {
return errors.Wrapf(err, "snapshot check failed on snapshot %d", i+1)
}
parentID = s
}
return nil
}
// checkInfo checks that the infos are the same
func checkInfo(si1, si2 snapshots.Info) error {
if si1.Kind != si2.Kind {
return errors.Errorf("Expected kind %v, got %v", si1.Kind, si2.Kind)
}
if si1.Name != si2.Name {
return errors.Errorf("Expected name %v, got %v", si1.Name, si2.Name)
}
if si1.Parent != si2.Parent {
return errors.Errorf("Expected Parent %v, got %v", si1.Parent, si2.Parent)
}
if len(si1.Labels) != len(si2.Labels) {
return errors.Errorf("Expected %d labels, got %d", len(si1.Labels), len(si2.Labels))
}
for k, l1 := range si1.Labels {
l2 := si2.Labels[k]
if l1 != l2 {
return errors.Errorf("Expected label %v, got %v", l1, l2)
}
}
if si1.Created != si2.Created {
return errors.Errorf("Expected Created %v, got %v", si1.Created, si2.Created)
}
if si1.Updated != si2.Updated {
return errors.Errorf("Expected Updated %v, got %v", si1.Updated, si2.Updated)
}
return nil
}

View File

@@ -0,0 +1,5 @@
package testsuite
import "golang.org/x/sys/unix"
const umountflags int = unix.MNT_DETACH

View File

@@ -0,0 +1,5 @@
// +build !linux
package testsuite
const umountflags int = 0

View File

@@ -0,0 +1,226 @@
package testsuite
import (
"context"
"fmt"
"strings"
"testing"
"time"
"github.com/containerd/containerd/fs/fstest"
"github.com/containerd/containerd/snapshots"
)
// Checks which cover former issues found in older layering models.
//
// NOTE: In older models, applying with tar was used to create read only layers,
// however with the snapshot model read only layers are created just using
// mounts and commits. Read write layers are a separate type of snapshot which
// is not committed, avoiding any confusion in the snapshotter about whether
// a snapshot will be mutated in the future.
// checkLayerFileUpdate tests the update of a single file in an upper layer
// Cause of issue was originally related to tar, snapshot should be able to
// avoid such issues by not relying on tar to create layers.
// See https://github.com/docker/docker/issues/21555
func checkLayerFileUpdate(ctx context.Context, t *testing.T, sn snapshots.Snapshotter, work string) {
l1Init := fstest.Apply(
fstest.CreateDir("/etc", 0700),
fstest.CreateFile("/etc/hosts", []byte("mydomain 10.0.0.1"), 0644),
fstest.CreateFile("/etc/profile", []byte("PATH=/usr/bin"), 0644),
)
l2Init := fstest.Apply(
fstest.CreateFile("/etc/hosts", []byte("mydomain 10.0.0.2"), 0644),
fstest.CreateFile("/etc/profile", []byte("PATH=/usr/bin"), 0666),
fstest.CreateDir("/root", 0700),
fstest.CreateFile("/root/.bashrc", []byte("PATH=/usr/sbin:/usr/bin"), 0644),
)
var sleepTime time.Duration
// run 5 times to account for sporadic failure
for i := 0; i < 5; i++ {
time.Sleep(sleepTime)
if err := checkSnapshots(ctx, sn, work, l1Init, l2Init); err != nil {
t.Fatalf("Check snapshots failed: %+v", err)
}
// Sleep until next second boundary before running again
nextTime := time.Now()
sleepTime = time.Unix(nextTime.Unix()+1, 0).Sub(nextTime)
}
}
// checkRemoveDirectoryInLowerLayer
// See https://github.com/docker/docker/issues/25244
func checkRemoveDirectoryInLowerLayer(ctx context.Context, t *testing.T, sn snapshots.Snapshotter, work string) {
l1Init := fstest.Apply(
fstest.CreateDir("/lib", 0700),
fstest.CreateFile("/lib/hidden", []byte{}, 0644),
)
l2Init := fstest.Apply(
fstest.RemoveAll("/lib"),
fstest.CreateDir("/lib", 0700),
fstest.CreateFile("/lib/not-hidden", []byte{}, 0644),
)
l3Init := fstest.Apply(
fstest.CreateFile("/lib/newfile", []byte{}, 0644),
)
if err := checkSnapshots(ctx, sn, work, l1Init, l2Init, l3Init); err != nil {
t.Fatalf("Check snapshots failed: %+v", err)
}
}
// checkChown
// See https://github.com/docker/docker/issues/20240 aufs
// See https://github.com/docker/docker/issues/24913 overlay
// see https://github.com/docker/docker/issues/28391 overlay2
func checkChown(ctx context.Context, t *testing.T, sn snapshots.Snapshotter, work string) {
l1Init := fstest.Apply(
fstest.CreateDir("/opt", 0700),
fstest.CreateDir("/opt/a", 0700),
fstest.CreateDir("/opt/a/b", 0700),
fstest.CreateFile("/opt/a/b/file.txt", []byte("hello"), 0644),
)
l2Init := fstest.Apply(
fstest.Chown("/opt", 1, 1),
fstest.Chown("/opt/a", 1, 1),
fstest.Chown("/opt/a/b", 1, 1),
fstest.Chown("/opt/a/b/file.txt", 1, 1),
)
if err := checkSnapshots(ctx, sn, work, l1Init, l2Init); err != nil {
t.Fatalf("Check snapshots failed: %+v", err)
}
}
// checkRename
// https://github.com/docker/docker/issues/25409
func checkRename(ctx context.Context, t *testing.T, sn snapshots.Snapshotter, work string) {
t.Skip("rename test still fails on some kernels with overlay")
l1Init := fstest.Apply(
fstest.CreateDir("/dir1", 0700),
fstest.CreateDir("/somefiles", 0700),
fstest.CreateFile("/somefiles/f1", []byte("was here first!"), 0644),
fstest.CreateFile("/somefiles/f2", []byte("nothing interesting"), 0644),
)
l2Init := fstest.Apply(
fstest.Rename("/dir1", "/dir2"),
fstest.CreateFile("/somefiles/f1-overwrite", []byte("new content 1"), 0644),
fstest.Rename("/somefiles/f1-overwrite", "/somefiles/f1"),
fstest.Rename("/somefiles/f2", "/somefiles/f3"),
)
if err := checkSnapshots(ctx, sn, work, l1Init, l2Init); err != nil {
t.Fatalf("Check snapshots failed: %+v", err)
}
}
// checkDirectoryPermissionOnCommit
// https://github.com/docker/docker/issues/27298
func checkDirectoryPermissionOnCommit(ctx context.Context, t *testing.T, sn snapshots.Snapshotter, work string) {
l1Init := fstest.Apply(
fstest.CreateDir("/dir1", 0700),
fstest.CreateDir("/dir2", 0700),
fstest.CreateDir("/dir3", 0700),
fstest.CreateDir("/dir4", 0700),
fstest.CreateFile("/dir4/f1", []byte("..."), 0644),
fstest.CreateDir("/dir5", 0700),
fstest.CreateFile("/dir5/f1", []byte("..."), 0644),
fstest.Chown("/dir1", 1, 1),
fstest.Chown("/dir2", 1, 1),
fstest.Chown("/dir3", 1, 1),
fstest.Chown("/dir5", 1, 1),
fstest.Chown("/dir5/f1", 1, 1),
)
l2Init := fstest.Apply(
fstest.Chown("/dir2", 0, 0),
fstest.RemoveAll("/dir3"),
fstest.Chown("/dir4", 1, 1),
fstest.Chown("/dir4/f1", 1, 1),
)
l3Init := fstest.Apply(
fstest.CreateDir("/dir3", 0700),
fstest.Chown("/dir3", 1, 1),
fstest.RemoveAll("/dir5"),
fstest.CreateDir("/dir5", 0700),
fstest.Chown("/dir5", 1, 1),
)
if err := checkSnapshots(ctx, sn, work, l1Init, l2Init, l3Init); err != nil {
t.Fatalf("Check snapshots failed: %+v", err)
}
}
// checkStatInWalk ensures that a stat can be called during a walk
func checkStatInWalk(ctx context.Context, t *testing.T, sn snapshots.Snapshotter, work string) {
prefix := "stats-in-walk-"
if err := createNamedSnapshots(ctx, sn, prefix); err != nil {
t.Fatal(err)
}
err := sn.Walk(ctx, func(ctx context.Context, si snapshots.Info) error {
if !strings.HasPrefix(si.Name, prefix) {
// Only stat snapshots from this test
return nil
}
si2, err := sn.Stat(ctx, si.Name)
if err != nil {
return err
}
return checkInfo(si, si2)
})
if err != nil {
t.Fatal(err)
}
}
func createNamedSnapshots(ctx context.Context, snapshotter snapshots.Snapshotter, ns string) error {
c1 := fmt.Sprintf("%sc1", ns)
c2 := fmt.Sprintf("%sc2", ns)
if _, err := snapshotter.Prepare(ctx, c1+"-a", "", opt); err != nil {
return err
}
if err := snapshotter.Commit(ctx, c1, c1+"-a", opt); err != nil {
return err
}
if _, err := snapshotter.Prepare(ctx, c2+"-a", c1, opt); err != nil {
return err
}
if err := snapshotter.Commit(ctx, c2, c2+"-a", opt); err != nil {
return err
}
if _, err := snapshotter.Prepare(ctx, fmt.Sprintf("%sa1", ns), c2, opt); err != nil {
return err
}
if _, err := snapshotter.View(ctx, fmt.Sprintf("%sv1", ns), c2, opt); err != nil {
return err
}
return nil
}
// More issues to test
//
// checkRemoveAfterCommit
// See https://github.com/docker/docker/issues/24309
//
// checkUnixDomainSockets
// See https://github.com/docker/docker/issues/12080
//
// checkDirectoryInodeStability
// See https://github.com/docker/docker/issues/19647
//
// checkOpenFileInodeStability
// See https://github.com/docker/docker/issues/12327
//
// checkGetCWD
// See https://github.com/docker/docker/issues/19082
//
// checkChmod
// See https://github.com/docker/machine/issues/3327
//
// checkRemoveInWalk
// Allow mutations during walk without deadlocking

View File

@@ -0,0 +1,826 @@
package testsuite
import (
"context"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
"time"
"github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/fs/fstest"
"github.com/containerd/containerd/mount"
"github.com/containerd/containerd/namespaces"
"github.com/containerd/containerd/snapshots"
"github.com/containerd/containerd/testutil"
"github.com/stretchr/testify/assert"
)
// SnapshotterSuite runs a test suite on the snapshotter given a factory function.
func SnapshotterSuite(t *testing.T, name string, snapshotterFn func(ctx context.Context, root string) (snapshots.Snapshotter, func() error, error)) {
restoreMask := clearMask()
defer restoreMask()
t.Run("Basic", makeTest(name, snapshotterFn, checkSnapshotterBasic))
t.Run("StatActive", makeTest(name, snapshotterFn, checkSnapshotterStatActive))
t.Run("StatComitted", makeTest(name, snapshotterFn, checkSnapshotterStatCommitted))
t.Run("TransitivityTest", makeTest(name, snapshotterFn, checkSnapshotterTransitivity))
t.Run("PreareViewFailingtest", makeTest(name, snapshotterFn, checkSnapshotterPrepareView))
t.Run("Update", makeTest(name, snapshotterFn, checkUpdate))
t.Run("Remove", makeTest(name, snapshotterFn, checkRemove))
t.Run("LayerFileupdate", makeTest(name, snapshotterFn, checkLayerFileUpdate))
t.Run("RemoveDirectoryInLowerLayer", makeTest(name, snapshotterFn, checkRemoveDirectoryInLowerLayer))
t.Run("Chown", makeTest(name, snapshotterFn, checkChown))
t.Run("DirectoryPermissionOnCommit", makeTest(name, snapshotterFn, checkDirectoryPermissionOnCommit))
t.Run("RemoveIntermediateSnapshot", makeTest(name, snapshotterFn, checkRemoveIntermediateSnapshot))
t.Run("DeletedFilesInChildSnapshot", makeTest(name, snapshotterFn, checkDeletedFilesInChildSnapshot))
t.Run("MoveFileFromLowerLayer", makeTest(name, snapshotterFn, checkFileFromLowerLayer))
t.Run("Rename", makeTest(name, snapshotterFn, checkRename))
t.Run("ViewReadonly", makeTest(name, snapshotterFn, checkSnapshotterViewReadonly))
t.Run("StatInWalk", makeTest(name, snapshotterFn, checkStatInWalk))
t.Run("CloseTwice", makeTest(name, snapshotterFn, closeTwice))
}
func makeTest(name string, snapshotterFn func(ctx context.Context, root string) (snapshots.Snapshotter, func() error, error), fn func(ctx context.Context, t *testing.T, snapshotter snapshots.Snapshotter, work string)) func(t *testing.T) {
return func(t *testing.T) {
t.Parallel()
ctx := context.Background()
ctx = namespaces.WithNamespace(ctx, "testsuite")
// Make two directories: a snapshotter root and a play area for the tests:
//
// /tmp
// work/ -> passed to test functions
// root/ -> passed to snapshotter
//
tmpDir, err := ioutil.TempDir("", "snapshot-suite-"+name+"-")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
root := filepath.Join(tmpDir, "root")
if err := os.MkdirAll(root, 0777); err != nil {
t.Fatal(err)
}
snapshotter, cleanup, err := snapshotterFn(ctx, root)
if err != nil {
t.Fatalf("Failed to initialize snapshotter: %+v", err)
}
defer func() {
if cleanup != nil {
if err := cleanup(); err != nil {
t.Errorf("Cleanup failed: %v", err)
}
}
}()
work := filepath.Join(tmpDir, "work")
if err := os.MkdirAll(work, 0777); err != nil {
t.Fatal(err)
}
defer testutil.DumpDir(t, tmpDir)
fn(ctx, t, snapshotter, work)
}
}
var opt = snapshots.WithLabels(map[string]string{
"containerd.io/gc.root": time.Now().UTC().Format(time.RFC3339),
})
// checkSnapshotterBasic tests the basic workflow of a snapshot snapshotter.
func checkSnapshotterBasic(ctx context.Context, t *testing.T, snapshotter snapshots.Snapshotter, work string) {
initialApplier := fstest.Apply(
fstest.CreateFile("/foo", []byte("foo\n"), 0777),
fstest.CreateDir("/a", 0755),
fstest.CreateDir("/a/b", 0755),
fstest.CreateDir("/a/b/c", 0755),
)
diffApplier := fstest.Apply(
fstest.CreateFile("/bar", []byte("bar\n"), 0777),
// also, change content of foo to bar
fstest.CreateFile("/foo", []byte("bar\n"), 0777),
fstest.RemoveAll("/a/b"),
)
preparing := filepath.Join(work, "preparing")
if err := os.MkdirAll(preparing, 0777); err != nil {
t.Fatalf("failure reason: %+v", err)
}
mounts, err := snapshotter.Prepare(ctx, preparing, "", opt)
if err != nil {
t.Fatalf("failure reason: %+v", err)
}
if len(mounts) < 1 {
t.Fatal("expected mounts to have entries")
}
if err := mount.All(mounts, preparing); err != nil {
t.Fatalf("failure reason: %+v", err)
}
defer testutil.Unmount(t, preparing)
if err := initialApplier.Apply(preparing); err != nil {
t.Fatalf("failure reason: %+v", err)
}
committed := filepath.Join(work, "committed")
if err := snapshotter.Commit(ctx, committed, preparing, opt); err != nil {
t.Fatalf("failure reason: %+v", err)
}
si, err := snapshotter.Stat(ctx, committed)
if err != nil {
t.Fatalf("failure reason: %+v", err)
}
assert.Equal(t, "", si.Parent)
assert.Equal(t, snapshots.KindCommitted, si.Kind)
_, err = snapshotter.Stat(ctx, preparing)
if err == nil {
t.Fatalf("%s should no longer be available after Commit", preparing)
}
next := filepath.Join(work, "nextlayer")
if err := os.MkdirAll(next, 0777); err != nil {
t.Fatalf("failure reason: %+v", err)
}
mounts, err = snapshotter.Prepare(ctx, next, committed, opt)
if err != nil {
t.Fatalf("failure reason: %+v", err)
}
if err := mount.All(mounts, next); err != nil {
t.Fatalf("failure reason: %+v", err)
}
defer testutil.Unmount(t, next)
if err := fstest.CheckDirectoryEqualWithApplier(next, initialApplier); err != nil {
t.Fatalf("failure reason: %+v", err)
}
if err := diffApplier.Apply(next); err != nil {
t.Fatalf("failure reason: %+v", err)
}
ni, err := snapshotter.Stat(ctx, next)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, committed, ni.Parent)
assert.Equal(t, snapshots.KindActive, ni.Kind)
nextCommitted := filepath.Join(work, "committed-next")
if err := snapshotter.Commit(ctx, nextCommitted, next, opt); err != nil {
t.Fatalf("failure reason: %+v", err)
}
si2, err := snapshotter.Stat(ctx, nextCommitted)
if err != nil {
t.Fatalf("failure reason: %+v", err)
}
assert.Equal(t, committed, si2.Parent)
assert.Equal(t, snapshots.KindCommitted, si2.Kind)
_, err = snapshotter.Stat(ctx, next)
if err == nil {
t.Fatalf("%s should no longer be available after Commit", next)
}
expected := map[string]snapshots.Info{
si.Name: si,
si2.Name: si2,
}
walked := map[string]snapshots.Info{} // walk is not ordered
assert.NoError(t, snapshotter.Walk(ctx, func(ctx context.Context, si snapshots.Info) error {
walked[si.Name] = si
return nil
}))
for ek, ev := range expected {
av, ok := walked[ek]
if !ok {
t.Errorf("Missing stat for %v", ek)
continue
}
assert.Equal(t, ev, av)
}
nextnext := filepath.Join(work, "nextnextlayer")
if err := os.MkdirAll(nextnext, 0777); err != nil {
t.Fatalf("failure reason: %+v", err)
}
mounts, err = snapshotter.View(ctx, nextnext, nextCommitted, opt)
if err != nil {
t.Fatalf("failure reason: %+v", err)
}
if err := mount.All(mounts, nextnext); err != nil {
t.Fatalf("failure reason: %+v", err)
}
if err := fstest.CheckDirectoryEqualWithApplier(nextnext,
fstest.Apply(initialApplier, diffApplier)); err != nil {
testutil.Unmount(t, nextnext)
t.Fatalf("failure reason: %+v", err)
}
testutil.Unmount(t, nextnext)
assert.NoError(t, snapshotter.Remove(ctx, nextnext))
assert.Error(t, snapshotter.Remove(ctx, committed))
assert.NoError(t, snapshotter.Remove(ctx, nextCommitted))
assert.NoError(t, snapshotter.Remove(ctx, committed))
}
// Create a New Layer on top of base layer with Prepare, Stat on new layer, should return Active layer.
func checkSnapshotterStatActive(ctx context.Context, t *testing.T, snapshotter snapshots.Snapshotter, work string) {
preparing := filepath.Join(work, "preparing")
if err := os.MkdirAll(preparing, 0777); err != nil {
t.Fatal(err)
}
mounts, err := snapshotter.Prepare(ctx, preparing, "", opt)
if err != nil {
t.Fatal(err)
}
if len(mounts) < 1 {
t.Fatal("expected mounts to have entries")
}
if err = mount.All(mounts, preparing); err != nil {
t.Fatal(err)
}
defer testutil.Unmount(t, preparing)
if err = ioutil.WriteFile(filepath.Join(preparing, "foo"), []byte("foo\n"), 0777); err != nil {
t.Fatal(err)
}
si, err := snapshotter.Stat(ctx, preparing)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, si.Name, preparing)
assert.Equal(t, snapshots.KindActive, si.Kind)
assert.Equal(t, "", si.Parent)
}
// Commit a New Layer on top of base layer with Prepare & Commit , Stat on new layer, should return Committed layer.
func checkSnapshotterStatCommitted(ctx context.Context, t *testing.T, snapshotter snapshots.Snapshotter, work string) {
preparing := filepath.Join(work, "preparing")
if err := os.MkdirAll(preparing, 0777); err != nil {
t.Fatal(err)
}
mounts, err := snapshotter.Prepare(ctx, preparing, "", opt)
if err != nil {
t.Fatal(err)
}
if len(mounts) < 1 {
t.Fatal("expected mounts to have entries")
}
if err = mount.All(mounts, preparing); err != nil {
t.Fatal(err)
}
defer testutil.Unmount(t, preparing)
if err = ioutil.WriteFile(filepath.Join(preparing, "foo"), []byte("foo\n"), 0777); err != nil {
t.Fatal(err)
}
committed := filepath.Join(work, "committed")
if err = snapshotter.Commit(ctx, committed, preparing, opt); err != nil {
t.Fatal(err)
}
si, err := snapshotter.Stat(ctx, committed)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, si.Name, committed)
assert.Equal(t, snapshots.KindCommitted, si.Kind)
assert.Equal(t, "", si.Parent)
}
func snapshotterPrepareMount(ctx context.Context, snapshotter snapshots.Snapshotter, diffPathName string, parent string, work string) (string, error) {
preparing := filepath.Join(work, diffPathName)
if err := os.MkdirAll(preparing, 0777); err != nil {
return "", err
}
mounts, err := snapshotter.Prepare(ctx, preparing, parent, opt)
if err != nil {
return "", err
}
if len(mounts) < 1 {
return "", fmt.Errorf("expected mounts to have entries")
}
if err = mount.All(mounts, preparing); err != nil {
return "", err
}
return preparing, nil
}
// 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(ctx context.Context, t *testing.T, snapshotter snapshots.Snapshotter, work string) {
preparing, err := snapshotterPrepareMount(ctx, snapshotter, "preparing", "", work)
if err != nil {
t.Fatal(err)
}
defer testutil.Unmount(t, preparing)
if err = ioutil.WriteFile(filepath.Join(preparing, "foo"), []byte("foo\n"), 0777); err != nil {
t.Fatal(err)
}
snapA := filepath.Join(work, "snapA")
if err = snapshotter.Commit(ctx, snapA, preparing, opt); err != nil {
t.Fatal(err)
}
next, err := snapshotterPrepareMount(ctx, snapshotter, "next", snapA, work)
if err != nil {
t.Fatal(err)
}
defer testutil.Unmount(t, next)
if err = ioutil.WriteFile(filepath.Join(next, "foo"), []byte("foo bar\n"), 0777); err != nil {
t.Fatal(err)
}
snapB := filepath.Join(work, "snapB")
if err = snapshotter.Commit(ctx, snapB, next, opt); err != nil {
t.Fatal(err)
}
siA, err := snapshotter.Stat(ctx, snapA)
if err != nil {
t.Fatal(err)
}
siB, err := snapshotter.Stat(ctx, snapB)
if err != nil {
t.Fatal(err)
}
siParentB, err := snapshotter.Stat(ctx, siB.Parent)
if err != nil {
t.Fatal(err)
}
// Test the transivity
assert.Equal(t, "", siA.Parent)
assert.Equal(t, snapA, siB.Parent)
assert.Equal(t, "", siParentB.Parent)
}
// Creating two layers with Prepare or View with same key must fail.
func checkSnapshotterPrepareView(ctx context.Context, t *testing.T, snapshotter snapshots.Snapshotter, work string) {
preparing, err := snapshotterPrepareMount(ctx, snapshotter, "preparing", "", work)
if err != nil {
t.Fatal(err)
}
defer testutil.Unmount(t, preparing)
snapA := filepath.Join(work, "snapA")
if err = snapshotter.Commit(ctx, snapA, preparing, opt); err != nil {
t.Fatal(err)
}
// Prepare & View with same key
newLayer := filepath.Join(work, "newlayer")
if err = os.MkdirAll(preparing, 0777); err != nil {
t.Fatal(err)
}
// Prepare & View with same key
_, err = snapshotter.Prepare(ctx, newLayer, snapA, opt)
if err != nil {
t.Fatal(err)
}
_, err = snapshotter.View(ctx, newLayer, snapA, opt)
//must be err != nil
assert.NotNil(t, err)
// Two Prepare with same key
prepLayer := filepath.Join(work, "prepLayer")
if err = os.MkdirAll(preparing, 0777); err != nil {
t.Fatal(err)
}
_, err = snapshotter.Prepare(ctx, prepLayer, snapA, opt)
if err != nil {
t.Fatal(err)
}
_, err = snapshotter.Prepare(ctx, prepLayer, snapA, opt)
//must be err != nil
assert.NotNil(t, err)
// Two View with same key
viewLayer := filepath.Join(work, "viewLayer")
if err = os.MkdirAll(preparing, 0777); err != nil {
t.Fatal(err)
}
_, err = snapshotter.View(ctx, viewLayer, snapA, opt)
if err != nil {
t.Fatal(err)
}
_, err = snapshotter.View(ctx, viewLayer, snapA, opt)
//must be err != nil
assert.NotNil(t, err)
}
// Deletion of files/folder of base layer in new layer, On Commit, those files should not be visible.
func checkDeletedFilesInChildSnapshot(ctx context.Context, t *testing.T, snapshotter snapshots.Snapshotter, work string) {
l1Init := fstest.Apply(
fstest.CreateFile("/foo", []byte("foo\n"), 0777),
fstest.CreateFile("/foobar", []byte("foobar\n"), 0777),
)
l2Init := fstest.Apply(
fstest.RemoveAll("/foobar"),
)
l3Init := fstest.Apply()
if err := checkSnapshots(ctx, snapshotter, work, l1Init, l2Init, l3Init); err != nil {
t.Fatalf("Check snapshots failed: %+v", err)
}
}
//Create three layers. Deleting intermediate layer must fail.
func checkRemoveIntermediateSnapshot(ctx context.Context, t *testing.T, snapshotter snapshots.Snapshotter, work string) {
base, err := snapshotterPrepareMount(ctx, snapshotter, "base", "", work)
if err != nil {
t.Fatal(err)
}
defer testutil.Unmount(t, base)
committedBase := filepath.Join(work, "committed-base")
if err = snapshotter.Commit(ctx, committedBase, base, opt); err != nil {
t.Fatal(err)
}
// Create intermediate layer
intermediate := filepath.Join(work, "intermediate")
if _, err = snapshotter.Prepare(ctx, intermediate, committedBase, opt); err != nil {
t.Fatal(err)
}
committedInter := filepath.Join(work, "committed-inter")
if err = snapshotter.Commit(ctx, committedInter, intermediate, opt); err != nil {
t.Fatal(err)
}
// Create top layer
topLayer := filepath.Join(work, "toplayer")
if _, err = snapshotter.Prepare(ctx, topLayer, committedInter, opt); err != nil {
t.Fatal(err)
}
// Deletion of intermediate layer must fail.
err = snapshotter.Remove(ctx, committedInter)
if err == nil {
t.Fatal("intermediate layer removal should fail.")
}
//Removal from toplayer to base should not fail.
err = snapshotter.Remove(ctx, topLayer)
if err != nil {
t.Fatal(err)
}
err = snapshotter.Remove(ctx, committedInter)
if err != nil {
t.Fatal(err)
}
err = snapshotter.Remove(ctx, committedBase)
if err != nil {
t.Fatal(err)
}
}
// baseTestSnapshots creates a base set of snapshots for tests, each snapshot is empty
// Tests snapshots:
// c1 - committed snapshot, no parent
// c2 - commited snapshot, c1 is parent
// a1 - active snapshot, c2 is parent
// a1 - active snapshot, no parent
// v1 - view snapshot, v1 is parent
// v2 - view snapshot, no parent
func baseTestSnapshots(ctx context.Context, snapshotter snapshots.Snapshotter) error {
if _, err := snapshotter.Prepare(ctx, "c1-a", "", opt); err != nil {
return err
}
if err := snapshotter.Commit(ctx, "c1", "c1-a", opt); err != nil {
return err
}
if _, err := snapshotter.Prepare(ctx, "c2-a", "c1", opt); err != nil {
return err
}
if err := snapshotter.Commit(ctx, "c2", "c2-a", opt); err != nil {
return err
}
if _, err := snapshotter.Prepare(ctx, "a1", "c2", opt); err != nil {
return err
}
if _, err := snapshotter.Prepare(ctx, "a2", "", opt); err != nil {
return err
}
if _, err := snapshotter.View(ctx, "v1", "c2", opt); err != nil {
return err
}
if _, err := snapshotter.View(ctx, "v2", "", opt); err != nil {
return err
}
return nil
}
func checkUpdate(ctx context.Context, t *testing.T, snapshotter snapshots.Snapshotter, work string) {
t1 := time.Now().UTC()
if err := baseTestSnapshots(ctx, snapshotter); err != nil {
t.Fatalf("Failed to create base snapshots: %v", err)
}
t2 := time.Now().UTC()
testcases := []struct {
name string
kind snapshots.Kind
parent string
}{
{
name: "c1",
kind: snapshots.KindCommitted,
},
{
name: "c2",
kind: snapshots.KindCommitted,
parent: "c1",
},
{
name: "a1",
kind: snapshots.KindActive,
parent: "c2",
},
{
name: "a2",
kind: snapshots.KindActive,
},
{
name: "v1",
kind: snapshots.KindView,
parent: "c2",
},
{
name: "v2",
kind: snapshots.KindView,
},
}
for _, tc := range testcases {
st, err := snapshotter.Stat(ctx, tc.name)
if err != nil {
t.Fatalf("Failed to stat %s: %v", tc.name, err)
}
if st.Created.Before(t1) || st.Created.After(t2) {
t.Errorf("(%s) wrong created time %s: expected between %s and %s", tc.name, st.Created, t1, t2)
continue
}
if st.Created != st.Updated {
t.Errorf("(%s) unexpected updated time %s: expected %s", tc.name, st.Updated, st.Created)
continue
}
if st.Kind != tc.kind {
t.Errorf("(%s) unexpected kind %s, expected %s", tc.name, st.Kind, tc.kind)
continue
}
if st.Parent != tc.parent {
t.Errorf("(%s) unexpected parent %q, expected %q", tc.name, st.Parent, tc.parent)
continue
}
if st.Name != tc.name {
t.Errorf("(%s) unexpected name %q, expected %q", tc.name, st.Name, tc.name)
continue
}
createdAt := st.Created
rootTime := time.Now().UTC().Format(time.RFC3339)
expected := map[string]string{
"l1": "v1",
"l2": "v2",
"l3": "v3",
// Keep root label
"containerd.io/gc.root": rootTime,
}
st.Parent = "doesnotexist"
st.Labels = expected
u1 := time.Now().UTC()
st, err = snapshotter.Update(ctx, st)
if err != nil {
t.Fatalf("Failed to update %s: %v", tc.name, err)
}
u2 := time.Now().UTC()
if st.Created != createdAt {
t.Errorf("(%s) wrong created time %s: expected %s", tc.name, st.Created, createdAt)
continue
}
if st.Updated.Before(u1) || st.Updated.After(u2) {
t.Errorf("(%s) wrong updated time %s: expected between %s and %s", tc.name, st.Updated, u1, u2)
continue
}
if st.Kind != tc.kind {
t.Errorf("(%s) unexpected kind %s, expected %s", tc.name, st.Kind, tc.kind)
continue
}
if st.Parent != tc.parent {
t.Errorf("(%s) unexpected parent %q, expected %q", tc.name, st.Parent, tc.parent)
continue
}
if st.Name != tc.name {
t.Errorf("(%s) unexpected name %q, expected %q", tc.name, st.Name, tc.name)
continue
}
assertLabels(t, st.Labels, expected)
expected = map[string]string{
"l1": "updated",
"l3": "v3",
"containerd.io/gc.root": rootTime,
}
st.Labels = map[string]string{
"l1": "updated",
"l4": "v4",
}
st, err = snapshotter.Update(ctx, st, "labels.l1", "labels.l2")
if err != nil {
t.Fatalf("Failed to update %s: %v", tc.name, err)
}
assertLabels(t, st.Labels, expected)
expected = map[string]string{
"l4": "v4",
"containerd.io/gc.root": rootTime,
}
st.Labels = expected
st, err = snapshotter.Update(ctx, st, "labels")
if err != nil {
t.Fatalf("Failed to update %s: %v", tc.name, err)
}
assertLabels(t, st.Labels, expected)
// Test failure received when providing immutable field path
st.Parent = "doesnotexist"
st, err = snapshotter.Update(ctx, st, "parent")
if err == nil {
t.Errorf("Expected error updating with immutable field path")
} else if !errdefs.IsInvalidArgument(err) {
t.Fatalf("Unexpected error updating %s: %+v", tc.name, err)
}
}
}
func assertLabels(t *testing.T, actual, expected map[string]string) {
if len(actual) != len(expected) {
t.Fatalf("Label size mismatch: %d vs %d\n\tActual: %#v\n\tExpected: %#v", len(actual), len(expected), actual, expected)
}
for k, v := range expected {
if a := actual[k]; v != a {
t.Errorf("Wrong label value for %s, got %q, expected %q", k, a, v)
}
}
if t.Failed() {
t.FailNow()
}
}
func checkRemove(ctx context.Context, t *testing.T, snapshotter snapshots.Snapshotter, work string) {
if _, err := snapshotter.Prepare(ctx, "committed-a", "", opt); err != nil {
t.Fatal(err)
}
if err := snapshotter.Commit(ctx, "committed-1", "committed-a", opt); err != nil {
t.Fatal(err)
}
if _, err := snapshotter.Prepare(ctx, "reuse-1", "committed-1", opt); err != nil {
t.Fatal(err)
}
if err := snapshotter.Remove(ctx, "reuse-1"); err != nil {
t.Fatal(err)
}
if _, err := snapshotter.View(ctx, "reuse-1", "committed-1", opt); err != nil {
t.Fatal(err)
}
if err := snapshotter.Remove(ctx, "reuse-1"); err != nil {
t.Fatal(err)
}
if _, err := snapshotter.Prepare(ctx, "reuse-1", "", opt); err != nil {
t.Fatal(err)
}
if err := snapshotter.Remove(ctx, "committed-1"); err != nil {
t.Fatal(err)
}
if err := snapshotter.Commit(ctx, "commited-1", "reuse-1", opt); err != nil {
t.Fatal(err)
}
}
// checkSnapshotterViewReadonly ensures a KindView snapshot to be mounted as a read-only filesystem.
// This function is called only when WithTestViewReadonly is true.
func checkSnapshotterViewReadonly(ctx context.Context, t *testing.T, snapshotter snapshots.Snapshotter, work string) {
preparing := filepath.Join(work, "preparing")
if _, err := snapshotter.Prepare(ctx, preparing, "", opt); err != nil {
t.Fatal(err)
}
committed := filepath.Join(work, "commited")
if err := snapshotter.Commit(ctx, committed, preparing, opt); err != nil {
t.Fatal(err)
}
view := filepath.Join(work, "view")
m, err := snapshotter.View(ctx, view, committed, opt)
if err != nil {
t.Fatal(err)
}
viewMountPoint := filepath.Join(work, "view-mount")
if err := os.MkdirAll(viewMountPoint, 0777); err != nil {
t.Fatal(err)
}
// Just checking the option string of m is not enough, we need to test real mount. (#1368)
if err := mount.All(m, viewMountPoint); err != nil {
t.Fatal(err)
}
testfile := filepath.Join(viewMountPoint, "testfile")
if err := ioutil.WriteFile(testfile, []byte("testcontent"), 0777); err != nil {
t.Logf("write to %q failed with %v (EROFS is expected but can be other error code)", testfile, err)
} else {
t.Fatalf("write to %q should fail (EROFS) but did not fail", testfile)
}
testutil.Unmount(t, viewMountPoint)
assert.NoError(t, snapshotter.Remove(ctx, view))
assert.NoError(t, snapshotter.Remove(ctx, committed))
}
// Move files from base layer to new location in intermediate layer.
// Verify if the file at source is deleted and copied to new location.
func checkFileFromLowerLayer(ctx context.Context, t *testing.T, snapshotter snapshots.Snapshotter, work string) {
l1Init := fstest.Apply(
fstest.CreateDir("/dir1", 0700),
fstest.CreateFile("/dir1/f1", []byte("Hello"), 0644),
fstest.CreateDir("dir2", 0700),
fstest.CreateFile("dir2/f2", []byte("..."), 0644),
)
l2Init := fstest.Apply(
fstest.CreateDir("/dir3", 0700),
fstest.CreateFile("/dir3/f1", []byte("Hello"), 0644),
fstest.RemoveAll("/dir1"),
fstest.Link("dir2/f2", "dir3/f2"),
fstest.RemoveAll("dir2/f2"),
)
if err := checkSnapshots(ctx, snapshotter, work, l1Init, l2Init); err != nil {
t.Fatalf("Check snapshots failed: %+v", err)
}
}
func closeTwice(ctx context.Context, t *testing.T, snapshotter snapshots.Snapshotter, work string) {
// do some dummy ops to modify the snapshotter internal state
if _, err := snapshotter.Prepare(ctx, "dummy", ""); err != nil {
t.Fatal(err)
}
if err := snapshotter.Commit(ctx, "dummy-1", "dummy"); err != nil {
t.Fatal(err)
}
if err := snapshotter.Remove(ctx, "dummy-1"); err != nil {
t.Fatal(err)
}
if err := snapshotter.Close(); err != nil {
t.Fatalf("The first close failed: %+v", err)
}
if err := snapshotter.Close(); err != nil {
t.Fatalf("The second close failed: %+v", err)
}
}

View File

@@ -0,0 +1,12 @@
// +build !windows
package testsuite
import "syscall"
func clearMask() func() {
oldumask := syscall.Umask(0)
return func() {
syscall.Umask(oldumask)
}
}

View File

@@ -0,0 +1,5 @@
package testsuite
func clearMask() func() {
return func() {}
}

View File

@@ -0,0 +1,91 @@
// +build windows
package windows
import (
"context"
"github.com/containerd/containerd/mount"
"github.com/containerd/containerd/plugin"
"github.com/containerd/containerd/snapshots"
"github.com/pkg/errors"
)
var (
// ErrNotImplemented is returned when an action is not implemented
ErrNotImplemented = errors.New("not implemented")
)
func init() {
plugin.Register(&plugin.Registration{
Type: plugin.SnapshotPlugin,
ID: "windows",
InitFn: func(ic *plugin.InitContext) (interface{}, error) {
return NewSnapshotter(ic.Root)
},
})
}
type snapshotter struct {
root string
}
// NewSnapshotter returns a new windows snapshotter
func NewSnapshotter(root string) (snapshots.Snapshotter, error) {
return &snapshotter{
root: root,
}, nil
}
// Stat returns the info for an active or committed snapshot by name or
// key.
//
// Should be used for parent resolution, existence checks and to discern
// the kind of snapshot.
func (o *snapshotter) Stat(ctx context.Context, key string) (snapshots.Info, error) {
panic("not implemented")
}
func (o *snapshotter) Update(ctx context.Context, info snapshots.Info, fieldpaths ...string) (snapshots.Info, error) {
panic("not implemented")
}
func (o *snapshotter) Usage(ctx context.Context, key string) (snapshots.Usage, error) {
panic("not implemented")
}
func (o *snapshotter) Prepare(ctx context.Context, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) {
panic("not implemented")
}
func (o *snapshotter) View(ctx context.Context, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) {
panic("not implemented")
}
// Mounts returns the mounts for the transaction identified by key. Can be
// called on an read-write or readonly transaction.
//
// This can be used to recover mounts after calling View or Prepare.
func (o *snapshotter) Mounts(ctx context.Context, key string) ([]mount.Mount, error) {
panic("not implemented")
}
func (o *snapshotter) Commit(ctx context.Context, name, key string, opts ...snapshots.Opt) error {
panic("not implemented")
}
// Remove abandons the transaction identified by key. All resources
// associated with the key will be removed.
func (o *snapshotter) Remove(ctx context.Context, key string) error {
panic("not implemented")
}
// Walk the committed snapshots.
func (o *snapshotter) Walk(ctx context.Context, fn func(context.Context, snapshots.Info) error) error {
panic("not implemented")
}
// Close closes the snapshotter
func (o *snapshotter) Close() error {
panic("not implemented")
}