snapshot: clarify active and committed snapshots
After receiving feedback on the `snapshot.Driver` interface, it was found that the behavior of active and committed snapshots was confusing. We attempt to clean this up by doing the following: 1. Define the concept of "active" and "committed" snapshots and their lifecycle relationship. Active snapshots can be created from a parent. Committed snapshots can only be created from active snapshots. 2. Only committed snapshots can be a parent. 3. Unify the keyspace of snapshots. For common operations, such as removal and stat, we only have a single method that works for both active and committed snapshots. For methods that take one or the other, the restriction is called out. `Remove` and `Delete` are consolidated for this purpose. 4. Define the `Info` data type to include name, parent, kind and readonly state. This allows us to collect `Exists` and `Parent` into a single method `Stat` and simplifies the `Walk` method, eliding `Active`. 5. The `Driver` has been renamed to `Snapshotter` due to the overuse of the term `Driver`. Effectively, we now have snapshots that are either active or committed. Signed-off-by: Stephen J Day <stephen.day@docker.com>
This commit is contained in:
parent
42a17f9391
commit
ab08944aa7
2 changed files with 347 additions and 302 deletions
|
@ -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)
|
|
||||||
}
|
|
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