containerd/snapshot/driver.go

303 lines
10 KiB
Go

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)
}