rename snapshot->snapshots pkg
Signed-off-by: Jess Valarezo <valarezo.jessica@gmail.com>
This commit is contained in:
377
snapshots/btrfs/btrfs.go
Normal file
377
snapshots/btrfs/btrfs.go
Normal 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()
|
||||
}
|
||||
155
snapshots/btrfs/btrfs_test.go
Normal file
155
snapshots/btrfs/btrfs_test.go
Normal 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
329
snapshots/naive/naive.go
Normal 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()
|
||||
}
|
||||
28
snapshots/naive/naive_test.go
Normal file
28
snapshots/naive/naive_test.go
Normal 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)
|
||||
}
|
||||
401
snapshots/overlay/overlay.go
Normal file
401
snapshots/overlay/overlay.go
Normal 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()
|
||||
}
|
||||
320
snapshots/overlay/overlay_test.go
Normal file
320
snapshots/overlay/overlay_test.go
Normal 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
316
snapshots/snapshotter.go
Normal 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
570
snapshots/storage/bolt.go
Normal 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
|
||||
}
|
||||
22
snapshots/storage/bolt_test.go
Normal file
22
snapshots/storage/bolt_test.go
Normal 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"))
|
||||
})
|
||||
}
|
||||
99
snapshots/storage/metastore.go
Normal file
99
snapshots/storage/metastore.go
Normal 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()
|
||||
}
|
||||
217
snapshots/storage/metastore_bench_test.go
Normal file
217
snapshots/storage/metastore_bench_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
634
snapshots/storage/metastore_test.go
Normal file
634
snapshots/storage/metastore_test.go
Normal 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:]
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
154
snapshots/testsuite/helpers.go
Normal file
154
snapshots/testsuite/helpers.go
Normal 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
|
||||
}
|
||||
5
snapshots/testsuite/helpers_linux.go
Normal file
5
snapshots/testsuite/helpers_linux.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package testsuite
|
||||
|
||||
import "golang.org/x/sys/unix"
|
||||
|
||||
const umountflags int = unix.MNT_DETACH
|
||||
5
snapshots/testsuite/helpers_other.go
Normal file
5
snapshots/testsuite/helpers_other.go
Normal file
@@ -0,0 +1,5 @@
|
||||
// +build !linux
|
||||
|
||||
package testsuite
|
||||
|
||||
const umountflags int = 0
|
||||
226
snapshots/testsuite/issues.go
Normal file
226
snapshots/testsuite/issues.go
Normal 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
|
||||
826
snapshots/testsuite/testsuite.go
Normal file
826
snapshots/testsuite/testsuite.go
Normal 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)
|
||||
}
|
||||
}
|
||||
12
snapshots/testsuite/testsuite_unix.go
Normal file
12
snapshots/testsuite/testsuite_unix.go
Normal file
@@ -0,0 +1,12 @@
|
||||
// +build !windows
|
||||
|
||||
package testsuite
|
||||
|
||||
import "syscall"
|
||||
|
||||
func clearMask() func() {
|
||||
oldumask := syscall.Umask(0)
|
||||
return func() {
|
||||
syscall.Umask(oldumask)
|
||||
}
|
||||
}
|
||||
5
snapshots/testsuite/testsuite_windows.go
Normal file
5
snapshots/testsuite/testsuite_windows.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package testsuite
|
||||
|
||||
func clearMask() func() {
|
||||
return func() {}
|
||||
}
|
||||
91
snapshots/windows/windows.go
Normal file
91
snapshots/windows/windows.go
Normal 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")
|
||||
}
|
||||
Reference in New Issue
Block a user