diff --git a/differ/differ.go b/differ/differ.go index 047c5a60c..1738110e3 100644 --- a/differ/differ.go +++ b/differ/differ.go @@ -10,7 +10,6 @@ import ( "github.com/containerd/containerd/content" "github.com/containerd/containerd/mount" "github.com/containerd/containerd/plugin" - "github.com/containerd/containerd/snapshot" digest "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" @@ -23,35 +22,28 @@ func init() { ID: "base-diff", Requires: []plugin.PluginType{ plugin.ContentPlugin, - plugin.SnapshotPlugin, }, Init: func(ic *plugin.InitContext) (interface{}, error) { c, err := ic.Get(plugin.ContentPlugin) if err != nil { return nil, err } - s, err := ic.Get(plugin.SnapshotPlugin) - if err != nil { - return nil, err - } - return newBaseDiff(c.(content.Store), s.(snapshot.Snapshotter)) + return newBaseDiff(c.(content.Store)) }, }) } type BaseDiff struct { - store content.Store - snapshotter snapshot.Snapshotter + store content.Store } var _ plugin.Differ = &BaseDiff{} var emptyDesc = ocispec.Descriptor{} -func newBaseDiff(store content.Store, snapshotter snapshot.Snapshotter) (*BaseDiff, error) { +func newBaseDiff(store content.Store) (*BaseDiff, error) { return &BaseDiff{ - store: store, - snapshotter: snapshotter, + store: store, }, nil } diff --git a/metadata/bolt.go b/metadata/bolt.go new file mode 100644 index 000000000..221dba5a2 --- /dev/null +++ b/metadata/bolt.go @@ -0,0 +1,40 @@ +package metadata + +import ( + "context" + + "github.com/boltdb/bolt" + "github.com/pkg/errors" +) + +type transactionKey struct{} + +// WithTransactionContext returns a new context holding the provided +// bolt transaction. Functions which require a bolt transaction will +// first check to see if a transaction is already created on the +// context before creating their own. +func WithTransactionContext(ctx context.Context, tx *bolt.Tx) context.Context { + return context.WithValue(ctx, transactionKey{}, tx) +} + +// view gets a bolt db transaction either from the context +// or starts a new one with the provided bolt database. +func view(ctx context.Context, db *bolt.DB, fn func(*bolt.Tx) error) error { + tx, ok := ctx.Value(transactionKey{}).(*bolt.Tx) + if !ok { + return db.View(fn) + } + return fn(tx) +} + +// update gets a writable bolt db transaction either from the context +// or starts a new one with the provided bolt database. +func update(ctx context.Context, db *bolt.DB, fn func(*bolt.Tx) error) error { + tx, ok := ctx.Value(transactionKey{}).(*bolt.Tx) + if !ok { + return db.Update(fn) + } else if !tx.Writable() { + return errors.Wrap(bolt.ErrTxNotWritable, "unable to use transaction from context") + } + return fn(tx) +} diff --git a/metadata/buckets.go b/metadata/buckets.go index 536d04011..202e0f1d5 100644 --- a/metadata/buckets.go +++ b/metadata/buckets.go @@ -32,6 +32,7 @@ var ( bucketKeyObjectIndexes = []byte("indexes") // reserved bucketKeyObjectImages = []byte("images") // stores image objects bucketKeyObjectContainers = []byte("containers") // stores container objects + bucketKeyObjectSnapshots = []byte("snapshots") // stores snapshot references bucketKeyDigest = []byte("digest") bucketKeyMediaType = []byte("mediatype") @@ -125,3 +126,15 @@ func getContainersBucket(tx *bolt.Tx, namespace string) *bolt.Bucket { func getContainerBucket(tx *bolt.Tx, namespace, id string) *bolt.Bucket { return getBucket(tx, bucketKeyVersion, []byte(namespace), bucketKeyObjectContainers, []byte(id)) } + +func createSnapshotterBucket(tx *bolt.Tx, namespace, snapshotter string) (*bolt.Bucket, error) { + bkt, err := createBucketIfNotExists(tx, bucketKeyVersion, []byte(namespace), bucketKeyObjectSnapshots, []byte(snapshotter)) + if err != nil { + return nil, err + } + return bkt, nil +} + +func getSnapshotterBucket(tx *bolt.Tx, namespace, snapshotter string) *bolt.Bucket { + return getBucket(tx, bucketKeyVersion, []byte(namespace), bucketKeyObjectSnapshots, []byte(snapshotter)) +} diff --git a/metadata/snapshot.go b/metadata/snapshot.go new file mode 100644 index 000000000..0ac3133ef --- /dev/null +++ b/metadata/snapshot.go @@ -0,0 +1,272 @@ +package metadata + +import ( + "context" + "fmt" + "strings" + + "github.com/boltdb/bolt" + "github.com/containerd/containerd/errdefs" + "github.com/containerd/containerd/mount" + "github.com/containerd/containerd/namespaces" + "github.com/containerd/containerd/snapshot" + "github.com/pkg/errors" +) + +type snapshotter struct { + snapshot.Snapshotter + name string + db *bolt.DB +} + +// NewSnapshotter returns a new Snapshotter which namespaces the given snapshot +// using the provided name and metadata store. +func NewSnapshotter(db *bolt.DB, name string, sn snapshot.Snapshotter) snapshot.Snapshotter { + return &snapshotter{ + Snapshotter: sn, + name: name, + db: db, + } +} + +func snapshotKey(id uint64, namespace, key string) string { + return fmt.Sprintf("%s/%d/%s", namespace, id, key) +} + +func trimName(key string) string { + parts := strings.SplitN(key, "/", 3) + if len(parts) < 3 { + return "" + } + return parts[2] +} + +func getKey(tx *bolt.Tx, ns, name, key string) string { + bkt := getSnapshotterBucket(tx, ns, name) + if bkt == nil { + return "" + } + v := bkt.Get([]byte(key)) + if len(v) == 0 { + return "" + } + return string(v) +} + +func (s *snapshotter) resolveKey(ctx context.Context, key string) (string, error) { + ns, err := namespaces.NamespaceRequired(ctx) + if err != nil { + return "", err + } + + var id string + if err := view(ctx, s.db, func(tx *bolt.Tx) error { + id = getKey(tx, ns, s.name, key) + if id == "" { + return errors.Wrapf(errdefs.ErrNotFound, "snapshot %v does not exist", key) + } + return nil + }); err != nil { + return "", err + } + + return id, nil +} + +func (s *snapshotter) Stat(ctx context.Context, key string) (snapshot.Info, error) { + bkey, err := s.resolveKey(ctx, key) + if err != nil { + return snapshot.Info{}, err + } + info, err := s.Snapshotter.Stat(ctx, bkey) + if err != nil { + return snapshot.Info{}, err + } + info.Name = trimName(info.Name) + if info.Parent != "" { + info.Parent = trimName(info.Parent) + } + + return info, nil +} + +func (s *snapshotter) Usage(ctx context.Context, key string) (snapshot.Usage, error) { + bkey, err := s.resolveKey(ctx, key) + if err != nil { + return snapshot.Usage{}, err + } + return s.Snapshotter.Usage(ctx, bkey) +} + +func (s *snapshotter) Mounts(ctx context.Context, key string) ([]mount.Mount, error) { + bkey, err := s.resolveKey(ctx, key) + if err != nil { + return nil, err + } + return s.Snapshotter.Mounts(ctx, bkey) +} + +func (s *snapshotter) Prepare(ctx context.Context, key, parent string) ([]mount.Mount, error) { + return s.createSnapshot(ctx, key, parent, false) +} + +func (s *snapshotter) View(ctx context.Context, key, parent string) ([]mount.Mount, error) { + return s.createSnapshot(ctx, key, parent, true) +} + +func (s *snapshotter) createSnapshot(ctx context.Context, key, parent string, readonly bool) ([]mount.Mount, error) { + ns, err := namespaces.NamespaceRequired(ctx) + if err != nil { + return nil, err + } + + var m []mount.Mount + if err := update(ctx, s.db, func(tx *bolt.Tx) error { + bkt, err := createSnapshotterBucket(tx, ns, s.name) + if err != nil { + return err + } + + bkey := string(bkt.Get([]byte(key))) + if bkey != "" { + return errors.Wrapf(errdefs.ErrAlreadyExists, "snapshot %v already exists", key) + } + var bparent string + if parent != "" { + bparent = string(bkt.Get([]byte(parent))) + if bparent == "" { + return errors.Wrapf(errdefs.ErrNotFound, "snapshot %v does not exist", parent) + } + } + + sid, err := bkt.NextSequence() + if err != nil { + return err + } + bkey = snapshotKey(sid, ns, key) + if err := bkt.Put([]byte(key), []byte(bkey)); err != nil { + return err + } + + // TODO: Consider doing this outside of transaction to lessen + // metadata lock time + if readonly { + m, err = s.Snapshotter.View(ctx, bkey, bparent) + } else { + m, err = s.Snapshotter.Prepare(ctx, bkey, bparent) + } + return err + }); err != nil { + return nil, err + } + return m, nil +} + +func (s *snapshotter) Commit(ctx context.Context, name, key string) error { + ns, err := namespaces.NamespaceRequired(ctx) + if err != nil { + return err + } + + return update(ctx, s.db, func(tx *bolt.Tx) error { + bkt := getSnapshotterBucket(tx, ns, s.name) + if bkt == nil { + return errors.Wrapf(errdefs.ErrNotFound, "snapshot %v does not exist", key) + } + + nameKey := string(bkt.Get([]byte(name))) + if nameKey != "" { + return errors.Wrapf(errdefs.ErrAlreadyExists, "snapshot %v already exists", name) + } + + bkey := string(bkt.Get([]byte(key))) + if bkey == "" { + return errors.Wrapf(errdefs.ErrNotFound, "snapshot %v does not exist", key) + } + + sid, err := bkt.NextSequence() + if err != nil { + return err + } + nameKey = snapshotKey(sid, ns, name) + if err := bkt.Put([]byte(name), []byte(nameKey)); err != nil { + return err + } + if err := bkt.Delete([]byte(key)); err != nil { + return err + } + + // TODO: Consider doing this outside of transaction to lessen + // metadata lock time + return s.Snapshotter.Commit(ctx, nameKey, bkey) + }) + +} + +func (s *snapshotter) Remove(ctx context.Context, key string) error { + ns, err := namespaces.NamespaceRequired(ctx) + if err != nil { + return err + } + + return update(ctx, s.db, func(tx *bolt.Tx) error { + bkt := getSnapshotterBucket(tx, ns, s.name) + if bkt == nil { + return errors.Wrapf(errdefs.ErrNotFound, "snapshot %v does not exist", key) + } + + bkey := string(bkt.Get([]byte(key))) + if bkey == "" { + return errors.Wrapf(errdefs.ErrNotFound, "snapshot %v does not exist", key) + } + if err := bkt.Delete([]byte(key)); err != nil { + return err + } + + return s.Snapshotter.Remove(ctx, bkey) + }) +} + +func (s *snapshotter) Walk(ctx context.Context, fn func(context.Context, snapshot.Info) error) error { + ns, err := namespaces.NamespaceRequired(ctx) + if err != nil { + return err + } + + var keys []string + + if err := view(ctx, s.db, func(tx *bolt.Tx) error { + bkt := getSnapshotterBucket(tx, ns, s.name) + if bkt == nil { + return nil + } + + bkt.ForEach(func(k, v []byte) error { + if len(v) > 0 { + keys = append(keys, string(v)) + } + return nil + }) + + return nil + }); err != nil { + return err + } + + for _, k := range keys { + info, err := s.Snapshotter.Stat(ctx, k) + if err != nil { + return err + } + + info.Name = trimName(info.Name) + if info.Parent != "" { + info.Parent = trimName(info.Parent) + } + if err := fn(ctx, info); err != nil { + return err + } + } + + return nil +} diff --git a/services/snapshot/service.go b/services/snapshot/service.go index 8a0b8f836..c1d9f2ffe 100644 --- a/services/snapshot/service.go +++ b/services/snapshot/service.go @@ -12,6 +12,7 @@ import ( "github.com/containerd/containerd/mount" "github.com/containerd/containerd/plugin" "github.com/containerd/containerd/snapshot" + "github.com/containerd/containerd/snapshot/namespaced" protoempty "github.com/golang/protobuf/ptypes/empty" "golang.org/x/net/context" "google.golang.org/grpc" @@ -44,7 +45,7 @@ type service struct { func newService(snapshotter snapshot.Snapshotter, evts events.Poster) (*service, error) { return &service{ - snapshotter: snapshotter, + snapshotter: namespaced.NewSnapshotter(snapshotter), emitter: evts, }, nil } diff --git a/snapshot/storage/bolt.go b/snapshot/storage/bolt.go index ab3697888..f6dc80161 100644 --- a/snapshot/storage/bolt.go +++ b/snapshot/storage/bolt.go @@ -298,6 +298,17 @@ func CommitActive(ctx context.Context, key, name string, usage snapshot.Usage) ( if err := bkt.Delete([]byte(key)); err != nil { return errors.Wrap(err, "failed to delete active") } + if ss.Parent != "" { + var ps db.Snapshot + if err := getSnapshot(bkt, ss.Parent, &ps); err != nil { + return errors.Wrap(err, "failed to get parent snapshot") + } + + // Updates parent back link to use new key + if err := pbkt.Put(parentKey(ps.ID, ss.ID), []byte(name)); err != nil { + return errors.Wrap(err, "failed to update parent link") + } + } id = fmt.Sprintf("%d", ss.ID)