containerd/snapshot/manager.go
Michael Crosby a7a6270f2a Remove shelling out to mount
Also remove the target from the Mount struct because it should not be
used at all.  The target can be variable and set by a caller, not by the
snapshot drivers.

Signed-off-by: Michael Crosby <crosbymichael@gmail.com>
2017-01-18 11:10:31 -08:00

306 lines
9.3 KiB
Go

package snapshot
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/docker/containerd"
)
// Manager provides an API for allocating, snapshotting and mounting
// abstract, layer-based filesystems. The model works by building up sets of
// directories with parent-child relationships.
//
// These differ from the concept of the graphdriver in that the
// Manager has no knowledge of images 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.
//
// 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:
//
// lm := NewManager()
// mounts, err := lm.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):
//
// 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 := lm.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 := lm.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 := lm.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 := lm.Commit(newImageDiff, containerRootFS); err != nil { ... }
//
// Alternatively, for most container runs, Manager.Rollback will be
// called to signal Manager to abandon the changes.
//
// TODO(stevvooe): Consider an alternate API that provides an active object to
// represent the lifecycle:
//
// work, err := lm.Prepare(dst, parent)
// mountAll(work.Mounts())
// work.Commit() || work.Rollback()
//
// TODO(stevvooe): Manager should be an interface with several
// implementations, similar to graphdriver.
type Manager struct {
root string // root provides paths for internal storage.
// just a simple overlay implementation.
active map[string]activeLayer
parents map[string]string // diff to parent for all committed
}
type activeLayer struct {
parent string
upperdir string
workdir string
}
func NewManager(root string) (*Manager, error) {
if err := os.MkdirAll(root, 0777); err != nil {
return nil, err
}
return &Manager{
root: root,
active: make(map[string]activeLayer),
parents: make(map[string]string),
}, nil
}
// Prepare returns a set of mounts such that dst can be used as a location for
// reading and writing data. If parent is provided, the dst will be setup to
// capture changes between dst and parent. The "default" parent, "", is an
// empty directory.
//
// If the caller intends to write data to dst, they should perform all mounts
// provided before doing so. The location defined by dst should be used as the
// working directory for any associated activity, such as running a container
// or importing a layer.
//
// The implementation may choose to write data directly to dst, opting to
// return no mounts instead.
//
// Once the writes have completed, Manager.Commit or
// Manager.Rollback should be called on dst.
func (lm *Manager) Prepare(dst, parent string) ([]containerd.Mount, error) {
// we want to build up lowerdir, upperdir and workdir options for the
// overlay mount.
//
// lowerdir is a list of parent diffs, ordered from top to bottom (base
// layer to the "right").
//
// upperdir will become the diff location. This will be renamed to the
// location provided in commit.
//
// workdir needs to be there but it is not really clear why.
var opts []string
upperdir, err := ioutil.TempDir(lm.root, "diff-")
if err != nil {
return nil, err
}
opts = append(opts, "upperdir="+upperdir)
workdir, err := ioutil.TempDir(lm.root, "work-")
if err != nil {
return nil, err
}
opts = append(opts, "workdir="+workdir)
empty := filepath.Join(lm.root, "empty")
if err := os.MkdirAll(empty, 0777); err != nil {
return nil, err
}
// TODO(stevvooe): Write this metadata to disk to make it useful.
lm.active[dst] = activeLayer{
parent: parent,
upperdir: upperdir,
workdir: workdir,
}
var parents []string
for parent != "" {
parents = append(parents, parent)
parent = lm.Parent(parent)
}
if len(parents) == 0 {
parents = []string{empty}
}
opts = append(opts, "lowerdir="+strings.Join(parents, ","))
return []containerd.Mount{
{
Type: "overlay",
Source: "none",
Options: opts,
},
}, nil
}
// View behaves identically to Prepare except the result may not be committed
// back to the snappshot manager.
//
// Whether or not these are readonly mounts is implementation specific, but the
// caller may write to dst freely.
//
// Calling Commit on dst will result in an error. Calling Rollback on dst
// should be done to cleanup resources.
func (lm *Manager) View(dst, parent string) ([]containerd.Mount, error) {
panic("not implemented")
}
// Commit captures the changes between dst and its parent into the path
// provided by diff. The path diff can then be used with the layer
// manipulator's other methods to access the diff content.
//
// The contents of diff are opaque to the caller and may be specific to the
// implementation of the layer backend.
func (lm *Manager) Commit(diff, dst string) error {
active, ok := lm.active[dst]
if !ok {
return fmt.Errorf("%q must be an active layer", dst)
}
// move upperdir into the diff dir
if err := os.Rename(active.upperdir, diff); err != nil {
return err
}
// Clean up the working directory; we may not want to do this if we want to
// support re-entrant calls to Commit.
if err := os.RemoveAll(active.workdir); err != nil {
return err
}
lm.parents[diff] = active.parent
delete(lm.active, dst) // remove from active, again, consider not doing this to support multiple commits.
// note that allowing multiple commits would require copy for overlay.
return nil
}
// Rollback can be called after prepare if the caller would like to abandon the
// changeset.
func (lm *Manager) Rollback(dst string) error {
active, ok := lm.active[dst]
if !ok {
return fmt.Errorf("%q must be an active layer", dst)
}
var err error
err = os.RemoveAll(active.upperdir)
err = os.RemoveAll(active.workdir)
delete(lm.active, dst)
return err
}
// Parent returns the parent of the layer at diff.
func (lm *Manager) Parent(diff string) string {
return lm.parents[diff]
}
type ChangeKind int
const (
ChangeKindAdd = iota
ChangeKindModify
ChangeKindDelete
)
func (k ChangeKind) String() string {
switch k {
case ChangeKindAdd:
return "add"
case ChangeKindModify:
return "modify"
case ChangeKindDelete:
return "delete"
default:
return ""
}
}
// Change represents single change between a diff and its parent.
//
// TODO(stevvooe): There are some cool tricks we can do with this type. If we
// provide the path to the resource from both the diff and its parent, for
// example, we can have the differ actually decide the granularity represented
// in the final changeset.
type Change struct {
Kind ChangeKind
Path string
}
// TODO(stevvooe): Make this change emit through a Walk-like interface. We can
// see this patten used in several tar'ing methods in pkg/archive.
// Changes returns the list of changes from the diff's parent.
func (lm *Manager) Changes(diff string) ([]Change, error) {
panic("not implemented")
}