containerd/layers.go
Stephen J Day c2da97c4d1 containerkit: layer manipulator overlay poc
A light weight overlay implementation that emits working mounts is
demonstrated. One can prepare and commit changes. The diffs are
correctly held on disk.

The next step from here is to implement the changes methods and ensure
that we can work with the docker registry API.

Signed-off-by: Stephen J Day <stephen.day@docker.com>
2016-09-26 21:43:58 -07:00

291 lines
8.8 KiB
Go

package containerkit
import (
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
)
var (
errNotImplemented = errors.New("not implemented")
)
// LayerManipulator provides an API for allocating, snapshotting and mounting
// abstract, layer-based filesytems. The model works by building up sets of
// directories with parent-child relationships.
//
// These differ from the concept of the graphdriver in that the
// LayerManipulator 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 LayerManipulator 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 LayerManipulator to prepare the temporary location as a
// snapshot point:
//
// lm := NewLayerManipulator()
// 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 LayerManipulator.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 LayerManipulator 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
// LayerManipulator.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 LayerManipulator.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,
// LayerManipulator.Commit is called:
//
// if err := lm.Commit(newImageDiff, containerRootFS); err != nil { ... }
//
// Alternatively, for most container runs, LayerManipulator.Rollback will be
// called to signal LayerManipulator 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): LayerManipulator should be an interface with several
// implementations, similar to graphdriver.
type LayerManipulator 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 NewLayerManipulator(root string) (*LayerManipulator, error) {
if err := os.MkdirAll(root, 0777); err != nil {
return nil, err
}
return &LayerManipulator{
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.
//
// Once the writes have completed, LayerManipulator.Commit or
// LayerManipulator.Rollback should be called on dst.
func (lm *LayerManipulator) Prepare(dst, parent string) ([]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
}
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 []Mount{
{
Type: "overlay",
Source: "none",
Target: dst,
Options: opts,
},
}, nil
}
// 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 *LayerManipulator) 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 *LayerManipulator) 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 *LayerManipulator) 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
}
// Changes returns the list of changes from the diff's parent.
func (lm *LayerManipulator) Changes(diff string) ([]Change, error) {
return nil, errNotImplemented
}