diff --git a/mount.go b/mount.go index a9c7a14..1ae4264 100644 --- a/mount.go +++ b/mount.go @@ -4,6 +4,7 @@ import ( "os" "os/exec" "strings" + "syscall" ) // Mount is the lingua franca of the containerkit. A mount represents a @@ -48,3 +49,67 @@ func MountAll(mounts ...Mount) error { return nil } + +func MountFS(mounts []Mount, target string) error { + for _, m := range mounts { + flags, data := parseMountOptions(m.Options) + if err := syscall.Mount(m.Source, target, m.Type, uintptr(flags), data); err != nil { + return err + } + } + return nil +} + +// parseMountOptions takes fstab style mount options and parses them for +// use with a standard mount() syscall +func parseMountOptions(options []string) (int, string) { + var ( + flag int + data []string + ) + flags := map[string]struct { + clear bool + flag int + }{ + "async": {true, syscall.MS_SYNCHRONOUS}, + "atime": {true, syscall.MS_NOATIME}, + "bind": {false, syscall.MS_BIND}, + "defaults": {false, 0}, + "dev": {true, syscall.MS_NODEV}, + "diratime": {true, syscall.MS_NODIRATIME}, + "dirsync": {false, syscall.MS_DIRSYNC}, + "exec": {true, syscall.MS_NOEXEC}, + "mand": {false, syscall.MS_MANDLOCK}, + "noatime": {false, syscall.MS_NOATIME}, + "nodev": {false, syscall.MS_NODEV}, + "nodiratime": {false, syscall.MS_NODIRATIME}, + "noexec": {false, syscall.MS_NOEXEC}, + "nomand": {true, syscall.MS_MANDLOCK}, + "norelatime": {true, syscall.MS_RELATIME}, + "nostrictatime": {true, syscall.MS_STRICTATIME}, + "nosuid": {false, syscall.MS_NOSUID}, + "rbind": {false, syscall.MS_BIND | syscall.MS_REC}, + "relatime": {false, syscall.MS_RELATIME}, + "remount": {false, syscall.MS_REMOUNT}, + "ro": {false, syscall.MS_RDONLY}, + "rw": {true, syscall.MS_RDONLY}, + "strictatime": {false, syscall.MS_STRICTATIME}, + "suid": {true, syscall.MS_NOSUID}, + "sync": {false, syscall.MS_SYNCHRONOUS}, + } + for _, o := range options { + // If the option does not exist in the flags table or the flag + // is not supported on the platform, + // then it is a data value for a specific fs type + if f, exists := flags[o]; exists && f.flag != 0 { + if f.clear { + flag &= ^f.flag + } else { + flag |= f.flag + } + } else { + data = append(data, o) + } + } + return flag, strings.Join(data, ",") +} diff --git a/snapshot/overlayfs.go b/snapshot/overlayfs.go new file mode 100644 index 0000000..8802d7d --- /dev/null +++ b/snapshot/overlayfs.go @@ -0,0 +1,157 @@ +package snapshot + +import ( + "crypto/md5" + "encoding/hex" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/docker/containerd" +) + +func NewOverlayfs(root string) (*Overlayfs, error) { + if err := os.MkdirAll(root, 0700); err != nil { + return nil, err + } + for _, p := range []string{ + "snapshots", + "active", + } { + if err := os.MkdirAll(filepath.Join(root, p), 0700); err != nil { + return nil, err + } + } + return &Overlayfs{ + root: root, + }, nil +} + +type Overlayfs struct { + root string +} + +func (o *Overlayfs) Prepare(key string, parentName string) ([]containerd.Mount, error) { + if err := validKey(key); err != nil { + return nil, err + } + active, err := o.newActiveDir(key) + if err != nil { + return nil, err + } + if parentName != "" { + if err := active.setParent(parentName); err != nil { + return nil, err + } + } + return active.mounts() +} + +func (o *Overlayfs) Commit(key string, name string) error { + active := o.getActive(key) + return active.commit(name) +} + +func (o *Overlayfs) newActiveDir(key string) (*activeDir, error) { + var ( + hash = hash(key) + path = filepath.Join(o.root, "active", hash) + ) + a := &activeDir{ + path: path, + snapshotsDir: filepath.Join(o.root, "snapshots"), + } + for _, p := range []string{ + "work", + "fs", + } { + if err := os.MkdirAll(filepath.Join(path, p), 0700); err != nil { + a.delete() + return nil, err + } + } + return a, nil +} + +func (o *Overlayfs) getActive(key string) *activeDir { + return &activeDir{ + path: filepath.Join(o.root, "active", hash(key)), + snapshotsDir: filepath.Join(o.root, "snapshots"), + } +} + +func validKey(key string) error { + _, err := filepath.Abs(key) + return err +} + +func hash(k string) string { + h := md5.New() + h.Write([]byte(k)) + return hex.EncodeToString(h.Sum(nil)) +} + +type activeDir struct { + snapshotsDir string + path string +} + +func (a *activeDir) delete() error { + return os.RemoveAll(a.path) +} + +func (a *activeDir) setParent(name string) error { + return os.Symlink(filepath.Join(a.snapshotsDir, name), filepath.Join(a.path, "parent")) +} + +func (a *activeDir) commit(name string) error { + if err := os.RemoveAll(filepath.Join(a.path, "work")); err != nil { + return err + } + return os.Rename(a.path, filepath.Join(a.snapshotsDir, name)) +} + +func (a *activeDir) mounts() ([]containerd.Mount, error) { + var ( + parentLink = filepath.Join(a.path, "parent") + parents []string + ) + for { + snapshot, err := os.Readlink(parentLink) + if err != nil { + if os.IsNotExist(err) { + break + } + return nil, err + } + parents = append(parents, filepath.Join(snapshot, "fs")) + parentLink = filepath.Join(snapshot, "parent") + } + if len(parents) == 0 { + // if we only have one layer/no parents then just return a bind mount as overlay + // will not work + return []containerd.Mount{ + { + Source: filepath.Join(a.path, "fs"), + Type: "bind", + Options: []string{ + "rw", + "rbind", + }, + }, + }, nil + } + options := []string{ + fmt.Sprintf("workdir=%s", filepath.Join(a.path, "work")), + fmt.Sprintf("upperdir=%s", filepath.Join(a.path, "fs")), + fmt.Sprintf("lowerdir=%s", strings.Join(parents, ":")), + } + return []containerd.Mount{ + { + Type: "overlay", + Source: "overlay", + Options: options, + }, + }, nil +} diff --git a/snapshot/overlayfs_test.go b/snapshot/overlayfs_test.go new file mode 100644 index 0000000..ecbef33 --- /dev/null +++ b/snapshot/overlayfs_test.go @@ -0,0 +1,180 @@ +package snapshot + +import ( + "io/ioutil" + "os" + "path/filepath" + "syscall" + "testing" + + "github.com/docker/containerd" +) + +func TestOverlayfs(t *testing.T) { + root, err := ioutil.TempDir("", "overlay") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(root) + o, err := NewOverlayfs(root) + if err != nil { + t.Error(err) + return + } + mounts, err := o.Prepare("/tmp/test", "") + if err != nil { + t.Error(err) + return + } + if len(mounts) != 1 { + t.Errorf("should only have 1 mount but received %d", len(mounts)) + } + m := mounts[0] + if m.Type != "bind" { + t.Errorf("mount type should be bind but received %q", m.Type) + } + expected := filepath.Join(root, "active", hash("/tmp/test"), "fs") + if m.Source != expected { + t.Errorf("expected source %q but received %q", expected, m.Source) + } + if m.Options[0] != "rw" { + t.Errorf("expected mount option rw but received %q", m.Options[0]) + } + if m.Options[1] != "rbind" { + t.Errorf("expected mount option rbind but received %q", m.Options[1]) + } +} + +func TestOverlayfsCommit(t *testing.T) { + root, err := ioutil.TempDir("", "overlay") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(root) + o, err := NewOverlayfs(root) + if err != nil { + t.Error(err) + return + } + key := "/tmp/test" + mounts, err := o.Prepare(key, "") + if err != nil { + t.Error(err) + return + } + m := mounts[0] + if err := ioutil.WriteFile(filepath.Join(m.Source, "foo"), []byte("hi"), 0660); err != nil { + t.Error(err) + return + } + if err := o.Commit(key, "base"); err != nil { + t.Error(err) + return + } +} + +func TestOverlayfsOverlayMount(t *testing.T) { + root, err := ioutil.TempDir("", "overlay") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(root) + o, err := NewOverlayfs(root) + if err != nil { + t.Error(err) + return + } + key := "/tmp/test" + mounts, err := o.Prepare(key, "") + if err != nil { + t.Error(err) + return + } + if err := o.Commit(key, "base"); err != nil { + t.Error(err) + return + } + if mounts, err = o.Prepare("/tmp/layer2", "base"); err != nil { + t.Error(err) + return + } + if len(mounts) != 1 { + t.Errorf("should only have 1 mount but received %d", len(mounts)) + } + m := mounts[0] + if m.Type != "overlay" { + t.Errorf("mount type should be overlay but received %q", m.Type) + } + if m.Source != "overlay" { + t.Errorf("expected source %q but received %q", "overlay", m.Source) + } + var ( + hash = hash("/tmp/layer2") + work = "workdir=" + filepath.Join(root, "active", hash, "work") + upper = "upperdir=" + filepath.Join(root, "active", hash, "fs") + lower = "lowerdir=" + filepath.Join(root, "snapshots", "base", "fs") + ) + for i, v := range []string{ + work, + upper, + lower, + } { + if m.Options[i] != v { + t.Errorf("expected %q but received %q", v, m.Options[i]) + } + } +} + +func TestOverlayfsOverlayRead(t *testing.T) { + if os.Getuid() != 0 { + t.Skip("not running as root") + } + root, err := ioutil.TempDir("", "overlay") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(root) + o, err := NewOverlayfs(root) + if err != nil { + t.Error(err) + return + } + key := "/tmp/test" + mounts, err := o.Prepare(key, "") + if err != nil { + t.Error(err) + return + } + m := mounts[0] + if err := ioutil.WriteFile(filepath.Join(m.Source, "foo"), []byte("hi"), 0660); err != nil { + t.Error(err) + return + } + if err := o.Commit(key, "base"); err != nil { + t.Error(err) + return + } + if mounts, err = o.Prepare("/tmp/layer2", "base"); err != nil { + t.Error(err) + return + } + dest := filepath.Join(root, "dest") + if err := os.Mkdir(dest, 0700); err != nil { + t.Error(err) + return + } + if err := containerd.MountFS(mounts, dest); err != nil { + t.Error(err) + return + } + defer syscall.Unmount(dest, 0) + data, err := ioutil.ReadFile(filepath.Join(dest, "foo")) + if err != nil { + t.Error(err) + return + } + if e := string(data); e != "hi" { + t.Errorf("expected file contents hi but got %q", e) + return + } +}