Merge pull request #496 from stevvooe/unify-snapshot-keyspace
snapshot: clarify active and committed snapshots
This commit is contained in:
commit
696e88c813
6 changed files with 703 additions and 491 deletions
|
@ -6,47 +6,33 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/containerd"
|
"github.com/docker/containerd"
|
||||||
"github.com/docker/containerd/log"
|
"github.com/docker/containerd/snapshot"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/stevvooe/go-btrfs"
|
"github.com/stevvooe/go-btrfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Driver struct {
|
type Snapshotter struct {
|
||||||
device string // maybe we can resolve it with path?
|
device string // maybe we can resolve it with path?
|
||||||
root string // root provides paths for internal storage.
|
root string // root provides paths for internal storage.
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDriver(device, root string) (*Driver, error) {
|
func NewSnapshotter(device, root string) (*Snapshotter, error) {
|
||||||
return &Driver{device: device, root: root}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Driver) Prepare(key, parent string) ([]containerd.Mount, error) {
|
|
||||||
return b.makeActive(key, parent, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Driver) View(key, parent string) ([]containerd.Mount, error) {
|
|
||||||
return b.makeActive(key, parent, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Driver) makeActive(key, parent string, readonly bool) ([]containerd.Mount, error) {
|
|
||||||
var (
|
var (
|
||||||
active = filepath.Join(b.root, "active")
|
active = filepath.Join(root, "active")
|
||||||
parents = filepath.Join(b.root, "parents")
|
snapshots = filepath.Join(root, "snapshots")
|
||||||
snapshots = filepath.Join(b.root, "snapshots")
|
parents = filepath.Join(root, "parents")
|
||||||
names = filepath.Join(b.root, "names")
|
index = filepath.Join(root, "index")
|
||||||
keyh = hash(key)
|
names = filepath.Join(root, "names")
|
||||||
parenth = hash(parent)
|
|
||||||
dir = filepath.Join(active, keyh)
|
|
||||||
namep = filepath.Join(names, keyh)
|
|
||||||
parentlink = filepath.Join(parents, keyh)
|
|
||||||
parentp = filepath.Join(snapshots, parenth)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
for _, path := range []string{
|
for _, path := range []string{
|
||||||
active,
|
active,
|
||||||
|
snapshots,
|
||||||
parents,
|
parents,
|
||||||
|
index,
|
||||||
names,
|
names,
|
||||||
} {
|
} {
|
||||||
if err := os.MkdirAll(path, 0755); err != nil {
|
if err := os.MkdirAll(path, 0755); err != nil {
|
||||||
|
@ -54,15 +40,118 @@ func (b *Driver) makeActive(key, parent string, readonly bool) ([]containerd.Mou
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return &Snapshotter{device: device, 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 (b *Snapshotter) Stat(key string) (snapshot.Info, error) {
|
||||||
|
// resolve the snapshot out of the index.
|
||||||
|
target, err := os.Readlink(filepath.Join(b.root, "index", hash(key)))
|
||||||
|
if err != nil {
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
return snapshot.Info{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return snapshot.Info{}, errors.Errorf("snapshot %v not found", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.stat(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Snapshotter) stat(target string) (snapshot.Info, error) {
|
||||||
|
var (
|
||||||
|
parents = filepath.Join(b.root, "parents")
|
||||||
|
names = filepath.Join(b.root, "names")
|
||||||
|
namep = filepath.Join(names, filepath.Base(target))
|
||||||
|
parentlink = filepath.Join(parents, filepath.Base(target))
|
||||||
|
)
|
||||||
|
|
||||||
|
// grab information about the subvolume
|
||||||
|
info, err := btrfs.SubvolInfo(target)
|
||||||
|
if err != nil {
|
||||||
|
return snapshot.Info{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// read the name out of the names!
|
||||||
|
nameraw, err := ioutil.ReadFile(namep)
|
||||||
|
if err != nil {
|
||||||
|
return snapshot.Info{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolve the parents path.
|
||||||
|
parentp, err := os.Readlink(parentlink)
|
||||||
|
if err != nil {
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
return snapshot.Info{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// no parent!
|
||||||
|
}
|
||||||
|
|
||||||
|
var parent string
|
||||||
|
if parentp != "" {
|
||||||
|
// okay, grab the basename of the parent and look up its name!
|
||||||
|
parentnamep := filepath.Join(names, filepath.Base(parentp))
|
||||||
|
|
||||||
|
p, err := ioutil.ReadFile(parentnamep)
|
||||||
|
if err != nil {
|
||||||
|
return snapshot.Info{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
parent = string(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
kind := snapshot.KindCommitted
|
||||||
|
if strings.HasPrefix(target, filepath.Join(b.root, "active")) {
|
||||||
|
kind = snapshot.KindActive
|
||||||
|
}
|
||||||
|
|
||||||
|
return snapshot.Info{
|
||||||
|
Name: string(nameraw),
|
||||||
|
Parent: parent,
|
||||||
|
Readonly: info.Readonly,
|
||||||
|
Kind: kind,
|
||||||
|
}, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Snapshotter) Prepare(key, parent string) ([]containerd.Mount, error) {
|
||||||
|
return b.makeActive(key, parent, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Snapshotter) View(key, parent string) ([]containerd.Mount, error) {
|
||||||
|
return b.makeActive(key, parent, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Snapshotter) makeActive(key, parent string, readonly bool) ([]containerd.Mount, error) {
|
||||||
|
var (
|
||||||
|
active = filepath.Join(b.root, "active")
|
||||||
|
snapshots = filepath.Join(b.root, "snapshots")
|
||||||
|
parents = filepath.Join(b.root, "parents")
|
||||||
|
index = filepath.Join(b.root, "index")
|
||||||
|
names = filepath.Join(b.root, "names")
|
||||||
|
keyh = hash(key)
|
||||||
|
parenth = hash(parent)
|
||||||
|
target = filepath.Join(active, keyh)
|
||||||
|
namep = filepath.Join(names, keyh)
|
||||||
|
indexlink = filepath.Join(index, keyh)
|
||||||
|
parentlink = filepath.Join(parents, keyh)
|
||||||
|
parentp = filepath.Join(snapshots, parenth) // parent must be restricted to snaps
|
||||||
|
)
|
||||||
|
|
||||||
if parent == "" {
|
if parent == "" {
|
||||||
// create new subvolume
|
// create new subvolume
|
||||||
// btrfs subvolume create /dir
|
// btrfs subvolume create /dir
|
||||||
if err := btrfs.SubvolCreate(dir); err != nil {
|
if err := btrfs.SubvolCreate(target); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// btrfs subvolume snapshot /parent /subvol
|
// btrfs subvolume snapshot /parent /subvol
|
||||||
if err := btrfs.SubvolSnapshot(dir, parentp, readonly); err != nil {
|
if err := btrfs.SubvolSnapshot(target, parentp, readonly); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,14 +160,19 @@ func (b *Driver) makeActive(key, parent string, readonly bool) ([]containerd.Mou
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// write in the name
|
||||||
if err := ioutil.WriteFile(namep, []byte(key), 0644); err != nil {
|
if err := ioutil.WriteFile(namep, []byte(key), 0644); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.mounts(dir)
|
if err := os.Symlink(target, indexlink); err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Driver) mounts(dir string) ([]containerd.Mount, error) {
|
return b.mounts(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Snapshotter) mounts(dir string) ([]containerd.Mount, error) {
|
||||||
var options []string
|
var options []string
|
||||||
|
|
||||||
// get the subvolume id back out for the mount
|
// get the subvolume id back out for the mount
|
||||||
|
@ -104,12 +198,13 @@ func (b *Driver) mounts(dir string) ([]containerd.Mount, error) {
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Driver) Commit(name, key string) error {
|
func (b *Snapshotter) Commit(name, key string) error {
|
||||||
var (
|
var (
|
||||||
active = filepath.Join(b.root, "active")
|
active = filepath.Join(b.root, "active")
|
||||||
snapshots = filepath.Join(b.root, "snapshots")
|
snapshots = filepath.Join(b.root, "snapshots")
|
||||||
names = filepath.Join(b.root, "names")
|
index = filepath.Join(b.root, "index")
|
||||||
parents = filepath.Join(b.root, "parents")
|
parents = filepath.Join(b.root, "parents")
|
||||||
|
names = filepath.Join(b.root, "names")
|
||||||
keyh = hash(key)
|
keyh = hash(key)
|
||||||
nameh = hash(name)
|
nameh = hash(name)
|
||||||
dir = filepath.Join(active, keyh)
|
dir = filepath.Join(active, keyh)
|
||||||
|
@ -118,6 +213,8 @@ func (b *Driver) Commit(name, key string) error {
|
||||||
namep = filepath.Join(names, nameh)
|
namep = filepath.Join(names, nameh)
|
||||||
keyparentlink = filepath.Join(parents, keyh)
|
keyparentlink = filepath.Join(parents, keyh)
|
||||||
parentlink = filepath.Join(parents, nameh)
|
parentlink = filepath.Join(parents, nameh)
|
||||||
|
keyindexlink = filepath.Join(index, keyh)
|
||||||
|
indexlink = filepath.Join(index, nameh)
|
||||||
)
|
)
|
||||||
|
|
||||||
info, err := btrfs.SubvolInfo(dir)
|
info, err := btrfs.SubvolInfo(dir)
|
||||||
|
@ -143,9 +240,7 @@ func (b *Driver) Commit(name, key string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("commit snapshot", target)
|
|
||||||
if err := btrfs.SubvolSnapshot(target, dir, true); err != nil {
|
if err := btrfs.SubvolSnapshot(target, dir, true); err != nil {
|
||||||
fmt.Println("snapshot error")
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -154,6 +249,10 @@ func (b *Driver) Commit(name, key string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := os.Remove(keyindexlink); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if err := ioutil.WriteFile(namep, []byte(name), 0755); err != nil {
|
if err := ioutil.WriteFile(namep, []byte(name), 0755); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -169,6 +268,10 @@ func (b *Driver) Commit(name, key string) error {
|
||||||
// into common storage, so let's not fret over it for now.
|
// into common storage, so let's not fret over it for now.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := os.Symlink(target, indexlink); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if err := btrfs.SubvolDelete(dir); err != nil {
|
if err := btrfs.SubvolDelete(dir); err != nil {
|
||||||
return errors.Wrapf(err, "delete subvol failed on %v", dir)
|
return errors.Wrapf(err, "delete subvol failed on %v", dir)
|
||||||
}
|
}
|
||||||
|
@ -180,82 +283,54 @@ func (b *Driver) Commit(name, key string) error {
|
||||||
// called on an read-write or readonly transaction.
|
// called on an read-write or readonly transaction.
|
||||||
//
|
//
|
||||||
// This can be used to recover mounts after calling View or Prepare.
|
// This can be used to recover mounts after calling View or Prepare.
|
||||||
func (b *Driver) Mounts(key string) ([]containerd.Mount, error) {
|
func (b *Snapshotter) Mounts(key string) ([]containerd.Mount, error) {
|
||||||
dir := filepath.Join(b.root, "active", hash(key))
|
dir := filepath.Join(b.root, "active", hash(key))
|
||||||
return b.mounts(dir)
|
return b.mounts(dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove abandons the transaction identified by key. All resources
|
// Remove abandons the transaction identified by key. All resources
|
||||||
// associated with the key will be removed.
|
// associated with the key will be removed.
|
||||||
func (b *Driver) Remove(key string) error {
|
func (b *Snapshotter) Remove(key string) error {
|
||||||
panic("not implemented")
|
panic("not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parent returns the parent of snapshot identified by name.
|
|
||||||
func (b *Driver) Parent(name string) (string, error) {
|
|
||||||
var (
|
|
||||||
parents = filepath.Join(b.root, "parents")
|
|
||||||
names = filepath.Join(b.root, "names")
|
|
||||||
parentlink = filepath.Join(parents, hash(name))
|
|
||||||
)
|
|
||||||
|
|
||||||
parentp, err := os.Readlink(parentlink)
|
|
||||||
if err != nil {
|
|
||||||
if !os.IsNotExist(err) {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", nil // no parent!
|
|
||||||
}
|
|
||||||
|
|
||||||
// okay, grab the basename of the parent and look up its name!
|
|
||||||
parentnamep := filepath.Join(names, filepath.Base(parentp))
|
|
||||||
|
|
||||||
p, err := ioutil.ReadFile(parentnamep)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return string(p), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exists returns true if the snapshot with name exists.
|
|
||||||
func (b *Driver) Exists(name string) bool {
|
|
||||||
target := filepath.Join(b.root, "snapshots", hash(name))
|
|
||||||
|
|
||||||
if _, err := os.Stat(target); err != nil {
|
|
||||||
if !os.IsNotExist(err) {
|
|
||||||
// TODO(stevvooe): Very rare condition when this fails horribly,
|
|
||||||
// such as an access error. Ideally, Exists is simple, but we may
|
|
||||||
// consider returning an error.
|
|
||||||
log.L.WithError(err).Fatal("error encountered checking for snapshot existence")
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete the snapshot idenfitied by name.
|
|
||||||
//
|
|
||||||
// If name has children, the operation will fail.
|
|
||||||
func (b *Driver) Delete(name string) error {
|
|
||||||
panic("not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(stevvooe): The methods below are still in flux. We'll need to work
|
|
||||||
// out the roles of active and committed snapshots for this to become more
|
|
||||||
// clear.
|
|
||||||
|
|
||||||
// Walk the committed snapshots.
|
// Walk the committed snapshots.
|
||||||
func (b *Driver) Walk(fn func(name string) error) error {
|
func (b *Snapshotter) Walk(fn func(snapshot.Info) error) error {
|
||||||
panic("not implemented")
|
// TODO(stevvooe): Copy-pasted almost verbatim from overlay. Really need to
|
||||||
|
// unify the metadata for snapshot implementations.
|
||||||
|
root := filepath.Join(b.root, "index")
|
||||||
|
return filepath.Walk(root, func(path string, fi os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Active will call fn for each active transaction.
|
if path == root {
|
||||||
func (b *Driver) Active(fn func(key string) error) error {
|
return nil
|
||||||
panic("not implemented")
|
}
|
||||||
|
|
||||||
|
if fi.Mode()&os.ModeSymlink == 0 {
|
||||||
|
// only follow links
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
|
||||||
|
target, err := os.Readlink(path)
|
||||||
|
if err != nil {
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
si, err := b.stat(target)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := fn(si); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func hash(k string) string {
|
func hash(k string) string {
|
||||||
|
|
|
@ -20,14 +20,14 @@ const (
|
||||||
|
|
||||||
func TestBtrfs(t *testing.T) {
|
func TestBtrfs(t *testing.T) {
|
||||||
testutil.RequiresRoot(t)
|
testutil.RequiresRoot(t)
|
||||||
snapshot.DriverSuite(t, "Btrfs", func(root string) (snapshot.Driver, func(), error) {
|
snapshot.SnapshotterSuite(t, "Btrfs", func(root string) (snapshot.Snapshotter, func(), error) {
|
||||||
device := setupBtrfsLoopbackDevice(t, root)
|
device := setupBtrfsLoopbackDevice(t, root)
|
||||||
driver, err := NewDriver(device.deviceName, root)
|
snapshotter, err := NewSnapshotter(device.deviceName, root)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return driver, func() {
|
return snapshotter, func() {
|
||||||
device.remove(t)
|
device.remove(t)
|
||||||
}, nil
|
}, nil
|
||||||
})
|
})
|
||||||
|
@ -53,7 +53,7 @@ func TestBtrfsMounts(t *testing.T) {
|
||||||
defer os.RemoveAll(root)
|
defer os.RemoveAll(root)
|
||||||
|
|
||||||
target := filepath.Join(root, "test")
|
target := filepath.Join(root, "test")
|
||||||
b, err := NewDriver(device.deviceName, root)
|
b, err := NewSnapshotter(device.deviceName, root)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,302 +0,0 @@
|
||||||
package snapshot
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/docker/containerd"
|
|
||||||
"github.com/docker/containerd/testutil"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Driver defines the methods required to implement a snapshot driver for
|
|
||||||
// allocating, snapshotting and mounting abstract filesystems. The model works
|
|
||||||
// by building up sets of changes with parent-child relationships.
|
|
||||||
//
|
|
||||||
// These differ from the concept of the graphdriver in that the Manager has no
|
|
||||||
// knowledge of images, layers or containers. Users simply prepare and commit
|
|
||||||
// directories. We also avoid the integration between graph driver's and the
|
|
||||||
// tar format used to represent the changesets.
|
|
||||||
//
|
|
||||||
// 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.
|
|
||||||
//
|
|
||||||
// For convention, we define the following terms to be used throughout this
|
|
||||||
// interface for driver implementations:
|
|
||||||
//
|
|
||||||
// `name` - refers to a forkable snapshot, typically read only
|
|
||||||
// `key` - refers to an active transaction, either a prepare or view
|
|
||||||
// `parent` - refers to the parent in relation to a name
|
|
||||||
//
|
|
||||||
// TODO(stevvooe): Update this description when things settle.
|
|
||||||
//
|
|
||||||
// Importing a Layer
|
|
||||||
//
|
|
||||||
// To import a layer, we simply have the Manager 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, tmpLocation := getLayerPath(), mkTmpDir() // just a path to layer tar file.
|
|
||||||
//
|
|
||||||
// We then use a Manager to prepare the temporary location as a
|
|
||||||
// snapshot point:
|
|
||||||
//
|
|
||||||
// sm := NewManager()
|
|
||||||
// mounts, err := sm.Prepare(tmpLocation, "")
|
|
||||||
// if err != nil { ... }
|
|
||||||
//
|
|
||||||
// Note that we provide "" as the parent, since we are applying the diff to an
|
|
||||||
// empty directory. We get back a list of mounts from Manager.Prepare.
|
|
||||||
// Before proceeding, we perform all these mounts:
|
|
||||||
//
|
|
||||||
// if err := MountAll(mounts); 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 create a location to
|
|
||||||
// commit the actual diff. For this example, we are just going to use the layer
|
|
||||||
// digest, but in practice, this will probably be the ChainID:
|
|
||||||
//
|
|
||||||
// diffPath := filepath.Join("/layers", digest) // name location for the uncompressed layer digest
|
|
||||||
// if err := sm.Commit(diffPath, tmpLocation); err != nil { ... }
|
|
||||||
//
|
|
||||||
// Now, we have a layer in the Manager that can be accessed with the
|
|
||||||
// opaque diffPath provided during commit.
|
|
||||||
//
|
|
||||||
// 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 diffPath when calling
|
|
||||||
// Manager.Prepare:
|
|
||||||
//
|
|
||||||
// mounts, err := sm.Prepare(tmpLocation, parentDiffPath)
|
|
||||||
//
|
|
||||||
// The diff will be captured at tmpLocation, as the layer is applied.
|
|
||||||
//
|
|
||||||
// Running a Container
|
|
||||||
//
|
|
||||||
// To run a container, we simply provide Manager.Prepare the diffPath
|
|
||||||
// of the image we want to start the container from. After mounting, the
|
|
||||||
// prepared path can be used directly as the container's filesystem:
|
|
||||||
//
|
|
||||||
// mounts, err := sm.Prepare(containerRootFS, imageDiffPath)
|
|
||||||
//
|
|
||||||
// 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 := sm.Commit(newImageDiff, containerRootFS); err != nil { ... }
|
|
||||||
//
|
|
||||||
// Alternatively, for most container runs, Manager.Rollback will be
|
|
||||||
// called to signal Manager to abandon the changes.
|
|
||||||
type Driver interface {
|
|
||||||
// Prepare returns a set of mounts corresponding to an active snapshot
|
|
||||||
// transaction, identified by the provided transaction key.
|
|
||||||
//
|
|
||||||
// If a parent is provided, after performing the mounts, the destination
|
|
||||||
// will start with the content of the parent. Changes to the mounted
|
|
||||||
// destination will be captured in relation to the provided parent. The
|
|
||||||
// default parent, "", is an empty directory.
|
|
||||||
//
|
|
||||||
// The changes may be saved to a new 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(key, parent string) ([]containerd.Mount, error)
|
|
||||||
|
|
||||||
// View behaves identically to Prepare except the result may not be committed
|
|
||||||
// back to the snapshot manager. View returns a readonly view on the
|
|
||||||
// parent, with the transaction 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.
|
|
||||||
//
|
|
||||||
// Commit may not be called on the provided key. To collect the resources
|
|
||||||
// associated with key, Remove must be called with key as the argument.
|
|
||||||
View(key, parent string) ([]containerd.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 driver's other
|
|
||||||
// methods to create subsequent snapshots.
|
|
||||||
//
|
|
||||||
// A snapshot will be created under name with the parent that started the
|
|
||||||
// transaction.
|
|
||||||
//
|
|
||||||
// Commit may be called multiple times on the same key. Snapshots created
|
|
||||||
// in this manner will all reference the parent used to start the
|
|
||||||
// transaction.
|
|
||||||
Commit(name, key string) error
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
Mounts(key string) ([]containerd.Mount, error)
|
|
||||||
|
|
||||||
// Remove abandons the transaction identified by key. All resources
|
|
||||||
// associated with the key will be removed.
|
|
||||||
Remove(key string) error
|
|
||||||
|
|
||||||
// Parent returns the parent of snapshot identified by name.
|
|
||||||
Parent(name string) (string, error)
|
|
||||||
|
|
||||||
// Exists returns true if the snapshot with name exists.
|
|
||||||
Exists(name string) bool
|
|
||||||
|
|
||||||
// Delete the snapshot idenfitied by name.
|
|
||||||
//
|
|
||||||
// If name has children, the operation will fail.
|
|
||||||
Delete(name string) error
|
|
||||||
|
|
||||||
// TODO(stevvooe): The methods below are still in flux. We'll need to work
|
|
||||||
// out the roles of active and committed snapshots for this to become more
|
|
||||||
// clear.
|
|
||||||
|
|
||||||
// Walk the committed snapshots.
|
|
||||||
Walk(fn func(name string) error) error
|
|
||||||
|
|
||||||
// Active will call fn for each active transaction.
|
|
||||||
Active(fn func(key string) error) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// DriverSuite runs a test suite on the driver given a factory function.
|
|
||||||
func DriverSuite(t *testing.T, name string, driverFn func(root string) (Driver, func(), error)) {
|
|
||||||
t.Run("Basic", makeTest(t, name, driverFn, checkDriverBasic))
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeTest(t *testing.T, name string, driverFn func(root string) (Driver, func(), error), fn func(t *testing.T, driver Driver, work string)) func(t *testing.T) {
|
|
||||||
return func(t *testing.T) {
|
|
||||||
// Make two directories: a driver root and a play area for the tests:
|
|
||||||
//
|
|
||||||
// /tmp
|
|
||||||
// work/ -> passed to test functions
|
|
||||||
// root/ -> passed to driver
|
|
||||||
//
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
driver, cleanup, err := driverFn(root)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
work := filepath.Join(tmpDir, "work")
|
|
||||||
if err := os.MkdirAll(work, 0777); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer testutil.DumpDir(t, tmpDir)
|
|
||||||
fn(t, driver, work)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkDriverBasic tests the basic workflow of a snapshot driver.
|
|
||||||
func checkDriverBasic(t *testing.T, driver Driver, work string) {
|
|
||||||
preparing := filepath.Join(work, "preparing")
|
|
||||||
if err := os.MkdirAll(preparing, 0777); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
mounts, err := driver.Prepare(preparing, "")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(mounts) < 1 {
|
|
||||||
t.Fatal("expected mounts to have entries")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := containerd.MountAll(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)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.MkdirAll(filepath.Join(preparing, "a", "b", "c"), 0755); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
committed := filepath.Join(work, "committed")
|
|
||||||
if err := driver.Commit(committed, preparing); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
parent, err := driver.Parent(committed)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, parent, "")
|
|
||||||
|
|
||||||
next := filepath.Join(work, "nextlayer")
|
|
||||||
if err := os.MkdirAll(next, 0777); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
mounts, err = driver.Prepare(next, committed)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := containerd.MountAll(mounts, next); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer testutil.Unmount(t, next)
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.RemoveAll(filepath.Join(next, "a", "b")); err != nil {
|
|
||||||
t.Log(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
nextCommitted := filepath.Join(work, "committed-next")
|
|
||||||
if err := driver.Commit(nextCommitted, next); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
parent, err = driver.Parent(nextCommitted)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, parent, committed)
|
|
||||||
}
|
|
|
@ -9,33 +9,98 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/docker/containerd"
|
"github.com/docker/containerd"
|
||||||
|
"github.com/docker/containerd/snapshot"
|
||||||
digest "github.com/opencontainers/go-digest"
|
digest "github.com/opencontainers/go-digest"
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Driver struct {
|
type Snapshotter struct {
|
||||||
root string
|
root string
|
||||||
cache *cache
|
links *cache
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDriver(root string) (*Driver, error) {
|
func NewSnapshotter(root string) (*Snapshotter, error) {
|
||||||
if err := os.MkdirAll(root, 0700); err != nil {
|
if err := os.MkdirAll(root, 0700); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for _, p := range []string{
|
for _, p := range []string{
|
||||||
"snapshots",
|
"committed", // committed snapshots
|
||||||
"active",
|
"active", // active snapshots
|
||||||
|
"index", // snapshots by hashed name
|
||||||
} {
|
} {
|
||||||
if err := os.MkdirAll(filepath.Join(root, p), 0700); err != nil {
|
if err := os.MkdirAll(filepath.Join(root, p), 0700); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &Driver{
|
return &Snapshotter{
|
||||||
root: root,
|
root: root,
|
||||||
cache: newCache(),
|
links: newCache(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *Driver) Prepare(key, parent string) ([]containerd.Mount, error) {
|
// 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(key string) (snapshot.Info, error) {
|
||||||
|
path, err := o.links.get(filepath.Join(o.root, "index", hash(key)))
|
||||||
|
if err != nil {
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
return snapshot.Info{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return snapshot.Info{}, errors.Errorf("snapshot %v not found", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(stevvooe): We don't confirm the name to avoid the lookup cost.
|
||||||
|
return o.stat(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Snapshotter) stat(path string) (snapshot.Info, error) {
|
||||||
|
ppath, err := o.links.get(filepath.Join(path, "parent"))
|
||||||
|
if err != nil {
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
return snapshot.Info{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// no parent
|
||||||
|
}
|
||||||
|
|
||||||
|
kp, err := ioutil.ReadFile(filepath.Join(path, "name"))
|
||||||
|
if err != nil {
|
||||||
|
return snapshot.Info{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var parent string
|
||||||
|
if ppath != "" {
|
||||||
|
p, err := ioutil.ReadFile(filepath.Join(ppath, "name"))
|
||||||
|
if err != nil {
|
||||||
|
return snapshot.Info{}, err
|
||||||
|
}
|
||||||
|
parent = string(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
ro := true
|
||||||
|
kind := snapshot.KindCommitted
|
||||||
|
if strings.HasPrefix(path, filepath.Join(o.root, "active")) {
|
||||||
|
// TODO(stevvooe): Maybe there is a better way?
|
||||||
|
kind = snapshot.KindActive
|
||||||
|
|
||||||
|
// TODO(stevvooe): We haven't introduced this to overlay yet.
|
||||||
|
// We'll add it when we add tests for it.
|
||||||
|
ro = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return snapshot.Info{
|
||||||
|
Name: string(kp),
|
||||||
|
Parent: parent,
|
||||||
|
Kind: kind,
|
||||||
|
Readonly: ro,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Snapshotter) Prepare(key, parent string) ([]containerd.Mount, error) {
|
||||||
active, err := o.newActiveDir(key)
|
active, err := o.newActiveDir(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -45,10 +110,10 @@ func (o *Driver) Prepare(key, parent string) ([]containerd.Mount, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return o.Mounts(key)
|
return active.mounts(o.links)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *Driver) View(key, parent string) ([]containerd.Mount, error) {
|
func (o *Snapshotter) View(key, parent string) ([]containerd.Mount, error) {
|
||||||
panic("not implemented")
|
panic("not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,70 +121,69 @@ func (o *Driver) View(key, parent string) ([]containerd.Mount, error) {
|
||||||
// called on an read-write or readonly transaction.
|
// called on an read-write or readonly transaction.
|
||||||
//
|
//
|
||||||
// This can be used to recover mounts after calling View or Prepare.
|
// This can be used to recover mounts after calling View or Prepare.
|
||||||
func (o *Driver) Mounts(key string) ([]containerd.Mount, error) {
|
func (o *Snapshotter) Mounts(key string) ([]containerd.Mount, error) {
|
||||||
active := o.getActive(key)
|
active := o.getActive(key)
|
||||||
return active.mounts(o.cache)
|
return active.mounts(o.links)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *Driver) Commit(name, key string) error {
|
func (o *Snapshotter) Commit(name, key string) error {
|
||||||
active := o.getActive(key)
|
active := o.getActive(key)
|
||||||
return active.commit(name, o.cache)
|
return active.commit(name, o.links)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove abandons the transaction identified by key. All resources
|
// Remove abandons the transaction identified by key. All resources
|
||||||
// associated with the key will be removed.
|
// associated with the key will be removed.
|
||||||
func (o *Driver) Remove(key string) error {
|
func (o *Snapshotter) Remove(key string) error {
|
||||||
panic("not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parent returns the parent of snapshot identified by name.
|
|
||||||
func (o *Driver) Parent(name string) (string, error) {
|
|
||||||
ppath, err := o.cache.get(filepath.Join(o.root, "snapshots", hash(name)))
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return "", nil // no parent
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err := ioutil.ReadFile(filepath.Join(ppath, "name"))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return string(p), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exists returns true if the snapshot with name exists.
|
|
||||||
func (o *Driver) Exists(name string) bool {
|
|
||||||
panic("not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete the snapshot idenfitied by name.
|
|
||||||
//
|
|
||||||
// If name has children, the operation will fail.
|
|
||||||
func (o *Driver) Delete(name string) error {
|
|
||||||
panic("not implemented")
|
panic("not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Walk the committed snapshots.
|
// Walk the committed snapshots.
|
||||||
func (o *Driver) Walk(fn func(name string) error) error {
|
func (o *Snapshotter) Walk(fn func(snapshot.Info) error) error {
|
||||||
panic("not implemented")
|
root := filepath.Join(o.root, "index")
|
||||||
|
return filepath.Walk(root, func(path string, fi os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Active will call fn for each active transaction.
|
if path == root {
|
||||||
func (o *Driver) Active(fn func(key string) error) error {
|
return nil
|
||||||
panic("not implemented")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *Driver) newActiveDir(key string) (*activeDir, error) {
|
if fi.Mode()&os.ModeSymlink == 0 {
|
||||||
|
// only follow links
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
|
||||||
|
target, err := o.links.get(path)
|
||||||
|
if err != nil {
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
si, err := o.stat(target)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := fn(si); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Snapshotter) newActiveDir(key string) (*activeDir, error) {
|
||||||
var (
|
var (
|
||||||
path = filepath.Join(o.root, "active", hash(key))
|
path = filepath.Join(o.root, "active", hash(key))
|
||||||
|
name = filepath.Join(path, "name")
|
||||||
|
indexlink = filepath.Join(o.root, "index", hash(key))
|
||||||
)
|
)
|
||||||
a := &activeDir{
|
a := &activeDir{
|
||||||
path: path,
|
path: path,
|
||||||
snapshotsDir: filepath.Join(o.root, "snapshots"),
|
committedDir: filepath.Join(o.root, "committed"),
|
||||||
|
indexlink: indexlink,
|
||||||
}
|
}
|
||||||
for _, p := range []string{
|
for _, p := range []string{
|
||||||
"work",
|
"work",
|
||||||
|
@ -130,13 +194,26 @@ func (o *Driver) newActiveDir(key string) (*activeDir, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := ioutil.WriteFile(name, []byte(key), 0644); err != nil {
|
||||||
|
a.delete()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// link from namespace
|
||||||
|
if err := os.Symlink(path, indexlink); err != nil {
|
||||||
|
a.delete()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return a, nil
|
return a, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *Driver) getActive(key string) *activeDir {
|
func (o *Snapshotter) getActive(key string) *activeDir {
|
||||||
return &activeDir{
|
return &activeDir{
|
||||||
path: filepath.Join(o.root, "active", hash(key)),
|
path: filepath.Join(o.root, "active", hash(key)),
|
||||||
snapshotsDir: filepath.Join(o.root, "snapshots"),
|
committedDir: filepath.Join(o.root, "committed"),
|
||||||
|
indexlink: filepath.Join(o.root, "index", hash(key)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,8 +222,9 @@ func hash(k string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
type activeDir struct {
|
type activeDir struct {
|
||||||
snapshotsDir string
|
committedDir string
|
||||||
path string
|
path string
|
||||||
|
indexlink string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *activeDir) delete() error {
|
func (a *activeDir) delete() error {
|
||||||
|
@ -154,7 +232,7 @@ func (a *activeDir) delete() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *activeDir) setParent(name string) error {
|
func (a *activeDir) setParent(name string) error {
|
||||||
return os.Symlink(filepath.Join(a.snapshotsDir, hash(name)), filepath.Join(a.path, "parent"))
|
return os.Symlink(filepath.Join(a.committedDir, hash(name)), filepath.Join(a.path, "parent"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *activeDir) commit(name string, c *cache) error {
|
func (a *activeDir) commit(name string, c *cache) error {
|
||||||
|
@ -173,7 +251,20 @@ func (a *activeDir) commit(name string, c *cache) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
c.invalidate(a.path) // clears parent cache, since we end up moving.
|
c.invalidate(a.path) // clears parent cache, since we end up moving.
|
||||||
return os.Rename(a.path, filepath.Join(a.snapshotsDir, hash(name)))
|
c.invalidate(filepath.Join(a.path, "parent"))
|
||||||
|
c.invalidate(a.indexlink)
|
||||||
|
|
||||||
|
committed := filepath.Join(a.committedDir, hash(name))
|
||||||
|
if err := os.Rename(a.path, committed); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Remove(a.indexlink); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
indexlink := filepath.Join(filepath.Dir(a.indexlink), hash(name))
|
||||||
|
return os.Symlink(committed, indexlink)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *activeDir) mounts(c *cache) ([]containerd.Mount, error) {
|
func (a *activeDir) mounts(c *cache) ([]containerd.Mount, error) {
|
||||||
|
@ -183,10 +274,11 @@ func (a *activeDir) mounts(c *cache) ([]containerd.Mount, error) {
|
||||||
current = a.path
|
current = a.path
|
||||||
)
|
)
|
||||||
for {
|
for {
|
||||||
if current, err = c.get(current); err != nil {
|
if current, err = c.get(filepath.Join(current, "parent")); err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
parents = append(parents, filepath.Join(current, "fs"))
|
parents = append(parents, filepath.Join(current, "fs"))
|
||||||
|
@ -221,32 +313,32 @@ func (a *activeDir) mounts(c *cache) ([]containerd.Mount, error) {
|
||||||
|
|
||||||
func newCache() *cache {
|
func newCache() *cache {
|
||||||
return &cache{
|
return &cache{
|
||||||
parents: make(map[string]string),
|
links: make(map[string]string),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type cache struct {
|
type cache struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
parents map[string]string
|
links map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cache) get(path string) (string, error) {
|
func (c *cache) get(path string) (string, error) {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
parentRoot, ok := c.parents[path]
|
target, ok := c.links[path]
|
||||||
if !ok {
|
if !ok {
|
||||||
link, err := os.Readlink(filepath.Join(path, "parent"))
|
link, err := os.Readlink(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
c.parents[path], parentRoot = link, link
|
c.links[path], target = link, link
|
||||||
}
|
}
|
||||||
return parentRoot, nil
|
return target, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cache) invalidate(path string) {
|
func (c *cache) invalidate(path string) {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
delete(c.parents, path)
|
delete(c.links, path)
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,12 +13,24 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestOverlay(t *testing.T) {
|
func TestOverlay(t *testing.T) {
|
||||||
|
testutil.RequiresRoot(t)
|
||||||
|
snapshot.SnapshotterSuite(t, "Overlay", func(root string) (snapshot.Snapshotter, func(), error) {
|
||||||
|
snapshotter, err := NewSnapshotter(root)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return snapshotter, func() {}, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOverlayMounts(t *testing.T) {
|
||||||
root, err := ioutil.TempDir("", "overlay")
|
root, err := ioutil.TempDir("", "overlay")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(root)
|
defer os.RemoveAll(root)
|
||||||
o, err := NewDriver(root)
|
o, err := NewSnapshotter(root)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
return
|
return
|
||||||
|
@ -53,7 +65,7 @@ func TestOverlayCommit(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(root)
|
defer os.RemoveAll(root)
|
||||||
o, err := NewDriver(root)
|
o, err := NewSnapshotter(root)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
return
|
return
|
||||||
|
@ -81,7 +93,7 @@ func TestOverlayOverlayMount(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(root)
|
defer os.RemoveAll(root)
|
||||||
o, err := NewDriver(root)
|
o, err := NewSnapshotter(root)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
return
|
return
|
||||||
|
@ -115,7 +127,7 @@ func TestOverlayOverlayMount(t *testing.T) {
|
||||||
sh = hash("base")
|
sh = hash("base")
|
||||||
work = "workdir=" + filepath.Join(root, "active", ah, "work")
|
work = "workdir=" + filepath.Join(root, "active", ah, "work")
|
||||||
upper = "upperdir=" + filepath.Join(root, "active", ah, "fs")
|
upper = "upperdir=" + filepath.Join(root, "active", ah, "fs")
|
||||||
lower = "lowerdir=" + filepath.Join(root, "snapshots", sh, "fs")
|
lower = "lowerdir=" + filepath.Join(root, "committed", sh, "fs")
|
||||||
)
|
)
|
||||||
for i, v := range []string{
|
for i, v := range []string{
|
||||||
work,
|
work,
|
||||||
|
@ -135,7 +147,7 @@ func TestOverlayOverlayRead(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(root)
|
defer os.RemoveAll(root)
|
||||||
o, err := NewDriver(root)
|
o, err := NewSnapshotter(root)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
return
|
return
|
||||||
|
@ -179,15 +191,3 @@ func TestOverlayOverlayRead(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOverlayDriverSuite(t *testing.T) {
|
|
||||||
testutil.RequiresRoot(t)
|
|
||||||
snapshot.DriverSuite(t, "Overlay", func(root string) (snapshot.Driver, func(), error) {
|
|
||||||
driver, err := NewDriver(root)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return driver, func() {}, nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
347
snapshot/snapshotter.go
Normal file
347
snapshot/snapshotter.go
Normal file
|
@ -0,0 +1,347 @@
|
||||||
|
package snapshot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/containerd"
|
||||||
|
"github.com/docker/containerd/testutil"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Kind identifies the kind of snapshot.
|
||||||
|
type Kind int
|
||||||
|
|
||||||
|
// definitions of snapshot kinds
|
||||||
|
const (
|
||||||
|
KindActive Kind = iota
|
||||||
|
KindCommitted
|
||||||
|
)
|
||||||
|
|
||||||
|
// Info provides information about a particular snapshot.
|
||||||
|
type Info struct {
|
||||||
|
Name string // name or key of snapshot
|
||||||
|
Parent string // name of parent snapshot
|
||||||
|
Kind Kind // active or committed snapshot
|
||||||
|
Readonly bool // true if readonly, only valid for active
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 commiting 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:
|
||||||
|
//
|
||||||
|
// `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 Manager 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(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 := MountAll(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(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(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 tmpLocation:
|
||||||
|
//
|
||||||
|
// mounts, err := sm.Prepare(tmpLocation, 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 := sm.Prepare(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 := sm.Commit(newImageSnapshot, containerKey); err != nil { ... }
|
||||||
|
//
|
||||||
|
// Alternatively, for most container runs, Manager.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(key string) (Info, 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(key string) ([]containerd.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(key, parent string) ([]containerd.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(key, parent string) ([]containerd.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.
|
||||||
|
//
|
||||||
|
// Commit may be called multiple times on the same key. Snapshots created
|
||||||
|
// in this manner will all reference the parent used to start the
|
||||||
|
// transaction.
|
||||||
|
Commit(name, key string) 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(key string) error
|
||||||
|
|
||||||
|
// Walk the committed snapshots. For each snapshot in the snapshotter, the
|
||||||
|
// function will be called.
|
||||||
|
Walk(fn func(Info) error) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// SnapshotterSuite runs a test suite on the snapshotter given a factory function.
|
||||||
|
func SnapshotterSuite(t *testing.T, name string, snapshotterFn func(root string) (Snapshotter, func(), error)) {
|
||||||
|
t.Run("Basic", makeTest(t, name, snapshotterFn, checkSnapshotterBasic))
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeTest(t *testing.T, name string, snapshotterFn func(root string) (Snapshotter, func(), error), fn func(t *testing.T, snapshotter Snapshotter, work string)) func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
// 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(root)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
work := filepath.Join(tmpDir, "work")
|
||||||
|
if err := os.MkdirAll(work, 0777); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer testutil.DumpDir(t, tmpDir)
|
||||||
|
fn(t, snapshotter, work)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkSnapshotterBasic tests the basic workflow of a snapshot snapshotter.
|
||||||
|
func checkSnapshotterBasic(t *testing.T, snapshotter Snapshotter, work string) {
|
||||||
|
preparing := filepath.Join(work, "preparing")
|
||||||
|
if err := os.MkdirAll(preparing, 0777); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mounts, err := snapshotter.Prepare(preparing, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(mounts) < 1 {
|
||||||
|
t.Fatal("expected mounts to have entries")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := containerd.MountAll(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)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Join(preparing, "a", "b", "c"), 0755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
committed := filepath.Join(work, "committed")
|
||||||
|
if err := snapshotter.Commit(committed, preparing); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
si, err := snapshotter.Stat(committed)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, si.Parent, "")
|
||||||
|
|
||||||
|
next := filepath.Join(work, "nextlayer")
|
||||||
|
if err := os.MkdirAll(next, 0777); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mounts, err = snapshotter.Prepare(next, committed)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := containerd.MountAll(mounts, next); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer testutil.Unmount(t, next)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.RemoveAll(filepath.Join(next, "a", "b")); err != nil {
|
||||||
|
t.Log(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nextCommitted := filepath.Join(work, "committed-next")
|
||||||
|
if err := snapshotter.Commit(nextCommitted, next); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
si2, err := snapshotter.Stat(nextCommitted)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, si2.Parent, committed)
|
||||||
|
|
||||||
|
expected := map[string]Info{
|
||||||
|
si.Name: si,
|
||||||
|
si2.Name: si2,
|
||||||
|
}
|
||||||
|
walked := map[string]Info{} // walk is not ordered
|
||||||
|
assert.NoError(t, snapshotter.Walk(func(si Info) error {
|
||||||
|
walked[si.Name] = si
|
||||||
|
return nil
|
||||||
|
}))
|
||||||
|
|
||||||
|
assert.Equal(t, expected, walked)
|
||||||
|
}
|
Loading…
Reference in a new issue