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: // // 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. // // TODO(stevvooe): Consider an alternate API that provides an active object to // represent the lifecycle: // // work, err := sm.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 (sm *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(sm.root, "diff-") if err != nil { return nil, err } opts = append(opts, "upperdir="+upperdir) workdir, err := ioutil.TempDir(sm.root, "work-") if err != nil { return nil, err } opts = append(opts, "workdir="+workdir) empty := filepath.Join(sm.root, "empty") if err := os.MkdirAll(empty, 0777); err != nil { return nil, err } // TODO(stevvooe): Write this metadata to disk to make it useful. sm.active[dst] = activeLayer{ parent: parent, upperdir: upperdir, workdir: workdir, } var parents []string for parent != "" { parents = append(parents, parent) parent = sm.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 snapshot 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 (sm *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 snapshot // manager'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 (sm *Manager) Commit(diff, dst string) error { active, ok := sm.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 } sm.parents[diff] = active.parent delete(sm.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 (sm *Manager) Rollback(dst string) error { active, ok := sm.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(sm.active, dst) return err } // Parent returns the parent of the layer at diff. func (sm *Manager) Parent(diff string) string { return sm.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 (sm *Manager) Changes(diff string) ([]Change, error) { panic("not implemented") }