diff --git a/snapshot/naive/naive.go b/snapshot/naive/naive.go index 1f495df..2c2c4d9 100644 --- a/snapshot/naive/naive.go +++ b/snapshot/naive/naive.go @@ -1,117 +1,263 @@ package naive import ( - "fmt" + "context" "io/ioutil" "os" "path/filepath" "github.com/containerd/containerd" "github.com/containerd/containerd/fs" + "github.com/containerd/containerd/log" + "github.com/containerd/containerd/plugin" + "github.com/containerd/containerd/snapshot" + "github.com/containerd/containerd/snapshot/storage" "github.com/pkg/errors" ) -type Naive struct { +func init() { + plugin.Register("snapshot-naive", &plugin.Registration{ + Type: plugin.SnapshotPlugin, + Init: func(ic *plugin.InitContext) (interface{}, error) { + return NewSnapshotter(filepath.Join(ic.Root, "snapshot", "naive")) + }, + }) +} + +type snapshotter struct { root string - - // TODO(stevvooe): Very lazy copying from the overlay driver. We'll have to - // put *all* of this on disk in an easy to access format. - active map[string]activeNaiveSnapshot - parents map[string]string // mirror of what is on disk + ms *storage.MetaStore } -type activeNaiveSnapshot struct { - parent string - metadata string -} - -func NewNaive(root string) (*Naive, error) { - if err := os.MkdirAll(root, 0777); err != nil { +// NewSnapshotter returns a Snapshotter which copies layers on the underlying +// file system. A metadata file is stored under the root. +func NewSnapshotter(root string) (snapshot.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 } - // TODO(stevvooe): Recover active transactions. + if err := os.Mkdir(filepath.Join(root, "snapshots"), 0700); err != nil { + return nil, err + } - return &Naive{ - root: root, - active: make(map[string]activeNaiveSnapshot), - parents: make(map[string]string), + return &snapshotter{ + root: root, + ms: ms, }, nil } -// Prepare works per the snapshot specification. +// Stat returns the info for an active or committed snapshot by name or +// key. // -// For the naive driver, the data is checked out directly into dst and no -// mounts are returned. -func (n *Naive) Prepare(dst, parent string) ([]containerd.Mount, error) { - metadataRoot, err := ioutil.TempDir(n.root, "active-") +// Should be used for parent resolution, existence checks and to discern +// the kind of snapshot. +func (o *snapshotter) Stat(ctx context.Context, key string) (snapshot.Info, error) { + ctx, t, err := o.ms.TransactionContext(ctx, false) if err != nil { - return nil, errors.Wrap(err, "failed to created transaction dir") + return snapshot.Info{}, err } - - // TODO(stevvooe): Write in driver metadata so it can be identified, - // probably part of common manager type. - - if err := ioutil.WriteFile(filepath.Join(metadataRoot, "target"), []byte(dst), 0777); err != nil { - return nil, errors.Wrap(err, "failed to write target to disk") - } - - if parent != "" { - if _, ok := n.parents[parent]; !ok { - return nil, errors.Wrap(err, "specified parent does not exist") - } - - if err := ioutil.WriteFile(filepath.Join(metadataRoot, "parent"), []byte(parent), 0777); err != nil { - return nil, errors.Wrap(err, "error specifying parent") - } - - // Now, we copy the parent filesystem, just a directory, into dst. - if err := fs.CopyDir(dst, filepath.Join(parent, "data")); err != nil { - return nil, errors.Wrap(err, "copying of parent failed") - } - } - - n.active[dst] = activeNaiveSnapshot{ - parent: parent, - metadata: metadataRoot, - } - - return nil, nil // no mounts!! + defer t.Rollback() + return storage.GetInfo(ctx, key) } -// Commit just moves the metadata directory to the diff location. -func (n *Naive) Commit(diff, dst string) error { - active, ok := n.active[dst] - if !ok { - return errors.Errorf("%v is not an active transaction", dst) +func (o *snapshotter) Prepare(ctx context.Context, key, parent string) ([]containerd.Mount, error) { + return o.createActive(ctx, key, parent, false) +} + +func (o *snapshotter) View(ctx context.Context, key, parent string) ([]containerd.Mount, error) { + return o.createActive(ctx, key, parent, true) +} + +// 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) ([]containerd.Mount, error) { + ctx, t, err := o.ms.TransactionContext(ctx, false) + if err != nil { + return nil, err + } + active, err := storage.GetActive(ctx, key) + t.Rollback() + if err != nil { + return nil, errors.Wrap(err, "failed to get active mount") + } + return o.mounts(active), nil +} + +func (o *snapshotter) Commit(ctx context.Context, name, key string) error { + ctx, t, err := o.ms.TransactionContext(ctx, true) + if err != nil { + return err + } + if _, err := storage.CommitActive(ctx, key, name); err != nil { + if rerr := t.Rollback(); rerr != nil { + log.G(ctx).WithError(rerr).Warn("Failure rolling back transaction") + } + return errors.Wrap(err, "failed to commit snapshot") + } + return t.Commit() +} + +// Remove abandons the transaction identified by key. All resources +// associated with the key will be removed. +func (o *snapshotter) Remove(ctx context.Context, key string) (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") } - // Move the data into our metadata directory, we could probably save disk - // space if we just saved the diff, but let's get something working. - if err := fs.CopyDir(filepath.Join(active.metadata, "data"), dst); err != nil { - return errors.Wrap(err, "copying of parent failed") + 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 = "" } - if err := os.Rename(active.metadata, diff); err != nil { - return errors.Wrap(err, "failed to rename metadata into diff") + 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") + } } - - n.parents[diff] = active.parent - delete(n.active, dst) return nil } -func (n *Naive) Rollback(dst string) error { - active, ok := n.active[dst] - if !ok { - return fmt.Errorf("%q must be an active snapshot", dst) +// Walk the committed snapshots. +func (o *snapshotter) Walk(ctx context.Context, fn func(context.Context, snapshot.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) createActive(ctx context.Context, key, parent string, readonly bool) ([]containerd.Mount, error) { + var ( + err error + path, td string + ) + + if !readonly || 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) + } + } + } + }() } - delete(n.active, dst) - return os.RemoveAll(active.metadata) + ctx, t, err := o.ms.TransactionContext(ctx, true) + if err != nil { + return nil, err + } + + active, err := storage.CreateActive(ctx, key, parent, readonly) + if err != nil { + if rerr := t.Rollback(); rerr != nil { + log.G(ctx).WithError(rerr).Warn("Failure rolling back transaction") + } + return nil, errors.Wrap(err, "failed to create active") + } + + if td != "" { + if len(active.ParentIDs) > 0 { + parent := o.getSnapshotDir(active.ParentIDs[0]) + if err := fs.CopyDir(td, parent); err != nil { + return nil, errors.Wrap(err, "copying of parent failed") + } + } + + path = o.getSnapshotDir(active.ID) + if err := os.Rename(td, path); err != nil { + if rerr := t.Rollback(); rerr != nil { + log.G(ctx).WithError(rerr).Warn("Failure rolling back transaction") + } + return nil, errors.Wrap(err, "failed to rename") + } + td = "" + } + + if err := t.Commit(); err != nil { + return nil, errors.Wrap(err, "commit failed") + } + + return o.mounts(active), nil } -func (n *Naive) Parent(diff string) string { - return n.parents[diff] +func (o *snapshotter) getSnapshotDir(id string) string { + return filepath.Join(o.root, "snapshots", id) +} + +func (o *snapshotter) mounts(active storage.Active) []containerd.Mount { + var ( + roFlag string + source string + ) + + if active.Readonly { + roFlag = "ro" + } else { + roFlag = "rw" + } + + if len(active.ParentIDs) == 0 || !active.Readonly { + source = o.getSnapshotDir(active.ID) + } else { + source = o.getSnapshotDir(active.ParentIDs[0]) + } + + return []containerd.Mount{ + { + Source: source, + Type: "bind", + Options: []string{ + roFlag, + "rbind", + }, + }, + } } diff --git a/snapshot/naive/naive_test.go b/snapshot/naive/naive_test.go index 390c1c0..67ac2cc 100644 --- a/snapshot/naive/naive_test.go +++ b/snapshot/naive/naive_test.go @@ -1,92 +1,24 @@ package naive import ( - "io/ioutil" - "os" - "path/filepath" + "context" "testing" - "github.com/containerd/containerd" + "github.com/containerd/containerd/snapshot" + "github.com/containerd/containerd/snapshot/testsuite" "github.com/containerd/containerd/testutil" ) -func TestSnapshotNaiveBasic(t *testing.T) { - testutil.RequiresRoot(t) - tmpDir, err := ioutil.TempDir("", "test-naive-") +func newSnapshotter(ctx context.Context, root string) (snapshot.Snapshotter, func(), error) { + snapshotter, err := NewSnapshotter(root) if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tmpDir) - - t.Log(tmpDir) - root := filepath.Join(tmpDir, "root") - - n, err := NewNaive(root) - if err != nil { - t.Fatal(err) + return nil, nil, err } - preparing := filepath.Join(tmpDir, "preparing") - if err := os.MkdirAll(preparing, 0777); err != nil { - t.Fatal(err) - } - - mounts, err := n.Prepare(preparing, "") - if err != nil { - t.Fatal(err) - } - - if err := containerd.MountAll(mounts, preparing); err != nil { - t.Fatal(err) - } - - if err := ioutil.WriteFile(filepath.Join(preparing, "foo"), []byte("foo\n"), 0777); err != nil { - t.Fatal(err) - } - - os.MkdirAll(preparing+"/a/b/c", 0755) - - // defer os.Remove(filepath.Join(tmpDir, "foo")) - - committed := filepath.Join(n.root, "committed") - - if err := n.Commit(committed, preparing); err != nil { - t.Fatal(err) - } - - if n.Parent(preparing) != "" { - t.Fatalf("parent of new layer should be empty, got n.Parent(%q) == %q", preparing, n.Parent(preparing)) - } - - next := filepath.Join(tmpDir, "nextlayer") - if err := os.MkdirAll(next, 0777); err != nil { - t.Fatal(err) - } - - mounts, err = n.Prepare(next, committed) - if err != nil { - t.Fatal(err) - } - if err := containerd.MountAll(mounts, next); err != nil { - t.Fatal(err) - } - - if err := ioutil.WriteFile(filepath.Join(next, "bar"), []byte("bar\n"), 0777); err != nil { - t.Fatal(err) - } - - // also, change content of foo to bar - if err := ioutil.WriteFile(filepath.Join(next, "foo"), []byte("bar\n"), 0777); err != nil { - t.Fatal(err) - } - - os.RemoveAll(next + "/a/b") - nextCommitted := filepath.Join(n.root, "committed-next") - if err := n.Commit(nextCommitted, next); err != nil { - t.Fatal(err) - } - - if n.Parent(nextCommitted) != committed { - t.Fatalf("parent of new layer should be %q, got n.Parent(%q) == %q (%#v)", committed, next, n.Parent(next), n.parents) - } + return snapshotter, func() {}, nil +} + +func TestNaive(t *testing.T) { + testutil.RequiresRoot(t) + testsuite.SnapshotterSuite(t, "Naive", newSnapshotter) }