From 33f6a0aaf58734f39af564123ca2c0056a3f1bd4 Mon Sep 17 00:00:00 2001 From: Solomon Hykes Date: Mon, 18 Mar 2013 00:15:35 -0700 Subject: [PATCH] docker/graph: a store for filesystem images and the graph of their relationships --- {fs => graph}/archive.go | 4 +- {fs => graph}/archive_test.go | 2 +- graph/changes.go | 106 +++++++++++++++ graph/graph.go | 143 ++++++++++++++++++++ graph/graph_test.go | 143 ++++++++++++++++++++ graph/image.go | 245 ++++++++++++++++++++++++++++++++++ {fs => graph}/mount_darwin.go | 2 +- {fs => graph}/mount_linux.go | 2 +- 8 files changed, 643 insertions(+), 4 deletions(-) rename {fs => graph}/archive.go (97%) rename {fs => graph}/archive_test.go (99%) create mode 100644 graph/changes.go create mode 100644 graph/graph.go create mode 100644 graph/graph_test.go create mode 100644 graph/image.go rename {fs => graph}/mount_darwin.go (92%) rename {fs => graph}/mount_linux.go (92%) diff --git a/fs/archive.go b/graph/archive.go similarity index 97% rename from fs/archive.go rename to graph/archive.go index f43cc64b66..b6281bf4b5 100644 --- a/fs/archive.go +++ b/graph/archive.go @@ -1,4 +1,4 @@ -package fs +package graph import ( "errors" @@ -7,6 +7,8 @@ import ( "os/exec" ) +type Archive io.Reader + type Compression uint32 const ( diff --git a/fs/archive_test.go b/graph/archive_test.go similarity index 99% rename from fs/archive_test.go rename to graph/archive_test.go index b182a1563e..c158537a6e 100644 --- a/fs/archive_test.go +++ b/graph/archive_test.go @@ -1,4 +1,4 @@ -package fs +package graph import ( "io/ioutil" diff --git a/graph/changes.go b/graph/changes.go new file mode 100644 index 0000000000..eebf7657e7 --- /dev/null +++ b/graph/changes.go @@ -0,0 +1,106 @@ +package graph + +import ( + "fmt" + "os" + "path/filepath" + "strings" +) + +type ChangeType int + +const ( + ChangeModify = iota + ChangeAdd + ChangeDelete +) + +type Change struct { + Path string + Kind ChangeType +} + +func (change *Change) String() string { + var kind string + switch change.Kind { + case ChangeModify: + kind = "C" + case ChangeAdd: + kind = "A" + case ChangeDelete: + kind = "D" + } + return fmt.Sprintf("%s %s", kind, change.Path) +} + +func Changes(layers []string, rw string) ([]Change, error) { + var changes []Change + err := filepath.Walk(rw, func(path string, f os.FileInfo, err error) error { + if err != nil { + return err + } + + // Rebase path + path, err = filepath.Rel(rw, path) + if err != nil { + return err + } + path = filepath.Join("/", path) + + // Skip root + if path == "/" { + return nil + } + + // Skip AUFS metadata + if matched, err := filepath.Match("/.wh..wh.*", path); err != nil || matched { + return err + } + + change := Change{ + Path: path, + } + + // Find out what kind of modification happened + file := filepath.Base(path) + // If there is a whiteout, then the file was removed + if strings.HasPrefix(file, ".wh.") { + originalFile := strings.TrimLeft(file, ".wh.") + change.Path = filepath.Join(filepath.Dir(path), originalFile) + change.Kind = ChangeDelete + } else { + // Otherwise, the file was added + change.Kind = ChangeAdd + + // ...Unless it already existed in a top layer, in which case, it's a modification + for _, layer := range layers { + stat, err := os.Stat(filepath.Join(layer, path)) + if err != nil && !os.IsNotExist(err) { + return err + } + if err == nil { + // The file existed in the top layer, so that's a modification + + // However, if it's a directory, maybe it wasn't actually modified. + // If you modify /foo/bar/baz, then /foo will be part of the changed files only because it's the parent of bar + if stat.IsDir() && f.IsDir() { + if f.Size() == stat.Size() && f.Mode() == stat.Mode() && f.ModTime() == stat.ModTime() { + // Both directories are the same, don't record the change + return nil + } + } + change.Kind = ChangeModify + break + } + } + } + + // Record change + changes = append(changes, change) + return nil + }) + if err != nil { + return nil, err + } + return changes, nil +} diff --git a/graph/graph.go b/graph/graph.go new file mode 100644 index 0000000000..7e5e884c8e --- /dev/null +++ b/graph/graph.go @@ -0,0 +1,143 @@ +package graph + +import ( + "fmt" + "io/ioutil" + "os" + "path" + "path/filepath" + "time" +) + +type Graph struct { + Root string +} + +func New(root string) (*Graph, error) { + abspath, err := filepath.Abs(root) + if err != nil { + return nil, err + } + // Create the root directory if it doesn't exists + if err := os.Mkdir(root, 0700); err != nil && !os.IsExist(err) { + return nil, err + } + return &Graph{ + Root: abspath, + }, nil +} + +func (graph *Graph) Exists(id string) bool { + if _, err := graph.Get(id); err != nil { + return false + } + return true +} + +func (graph *Graph) Get(id string) (*Image, error) { + img, err := LoadImage(graph.imageRoot(id)) + if err != nil { + return nil, err + } + if img.Id != id { + return nil, fmt.Errorf("Image stored at '%s' has wrong id '%s'", id, img.Id) + } + return img, nil +} + +func (graph *Graph) Create(layerData Archive, parent, comment string) (*Image, error) { + img := &Image{ + Id: GenerateId(), + Parent: parent, + Comment: comment, + Created: time.Now(), + } + if err := graph.Register(layerData, img); err != nil { + return nil, err + } + return img, nil +} + +func (graph *Graph) Register(layerData Archive, img *Image) error { + if err := ValidateId(img.Id); err != nil { + return err + } + // (This is a convenience to save time. Race conditions are taken care of by os.Rename) + if graph.Exists(img.Id) { + return fmt.Errorf("Image %s already exists", img.Id) + } + tmp, err := graph.Mktemp(img.Id) + defer os.RemoveAll(tmp) + if err != nil { + return fmt.Errorf("Mktemp failed: %s", err) + } + if err := StoreImage(img, layerData, tmp); err != nil { + return err + } + // Commit + if err := os.Rename(tmp, graph.imageRoot(img.Id)); err != nil { + return err + } + img.graph = graph + return nil +} + +func (graph *Graph) Mktemp(id string) (string, error) { + tmp, err := New(path.Join(graph.Root, ":tmp:")) + if err != nil { + return "", fmt.Errorf("Couldn't create temp: %s", err) + } + if tmp.Exists(id) { + return "", fmt.Errorf("Image %d already exists", id) + } + return tmp.imageRoot(id), nil +} + +func (graph *Graph) Garbage() (*Graph, error) { + return New(path.Join(graph.Root, ":garbage:")) +} + +func (graph *Graph) Delete(id string) error { + garbage, err := graph.Garbage() + if err != nil { + return err + } + return os.Rename(graph.imageRoot(id), garbage.imageRoot(id)) +} + +func (graph *Graph) Undelete(id string) error { + garbage, err := graph.Garbage() + if err != nil { + return err + } + return os.Rename(garbage.imageRoot(id), graph.imageRoot(id)) +} + +func (graph *Graph) GarbageCollect() error { + garbage, err := graph.Garbage() + if err != nil { + return err + } + return os.RemoveAll(garbage.Root) +} + +func (graph *Graph) All() ([]*Image, error) { + files, err := ioutil.ReadDir(graph.Root) + if err != nil { + return nil, err + } + var images []*Image + for _, st := range files { + if img, err := graph.Get(st.Name()); err != nil { + // Skip image + continue + } else { + images = append(images, img) + } + } + return images, nil +} + +func (graph *Graph) imageRoot(id string) string { + return path.Join(graph.Root, id) +} diff --git a/graph/graph_test.go b/graph/graph_test.go new file mode 100644 index 0000000000..8487aaf1df --- /dev/null +++ b/graph/graph_test.go @@ -0,0 +1,143 @@ +package graph + +import ( + "github.com/dotcloud/docker/fake" + "io/ioutil" + "os" + "path" + "testing" + "time" +) + +func TestInit(t *testing.T) { + graph := tempGraph(t) + defer os.RemoveAll(graph.Root) + // Root should exist + if _, err := os.Stat(graph.Root); err != nil { + t.Fatal(err) + } + // All() should be empty + if l, err := graph.All(); err != nil { + t.Fatal(err) + } else if len(l) != 0 { + t.Fatalf("List() should return %d, not %d", 0, len(l)) + } +} + +// FIXME: Do more extensive tests (ex: create multiple, delete, recreate; +// create multiple, check the amount of images and paths, etc..) +func TestCreate(t *testing.T) { + graph := tempGraph(t) + defer os.RemoveAll(graph.Root) + archive, err := fake.FakeTar() + if err != nil { + t.Fatal(err) + } + image, err := graph.Create(archive, "", "Testing") + if err != nil { + t.Fatal(err) + } + if err := ValidateId(image.Id); err != nil { + t.Fatal(err) + } + if image.Comment != "Testing" { + t.Fatalf("Wrong comment: should be '%s', not '%s'", "Testing", image.Comment) + } + if images, err := graph.All(); err != nil { + t.Fatal(err) + } else if l := len(images); l != 1 { + t.Fatalf("Wrong number of images. Should be %d, not %d", 1, l) + } +} + +func TestRegister(t *testing.T) { + graph := tempGraph(t) + defer os.RemoveAll(graph.Root) + archive, err := fake.FakeTar() + if err != nil { + t.Fatal(err) + } + image := &Image{ + Id: GenerateId(), + Comment: "testing", + Created: time.Now(), + } + err = graph.Register(archive, image) + if err != nil { + t.Fatal(err) + } + if images, err := graph.All(); err != nil { + t.Fatal(err) + } else if l := len(images); l != 1 { + t.Fatalf("Wrong number of images. Should be %d, not %d", 1, l) + } + if resultImg, err := graph.Get(image.Id); err != nil { + t.Fatal(err) + } else { + if resultImg.Id != image.Id { + t.Fatalf("Wrong image ID. Should be '%s', not '%s'", image.Id, resultImg.Id) + } + if resultImg.Comment != image.Comment { + t.Fatalf("Wrong image comment. Should be '%s', not '%s'", image.Comment, resultImg.Comment) + } + } +} + +func TestMount(t *testing.T) { + graph := tempGraph(t) + defer os.RemoveAll(graph.Root) + archive, err := fake.FakeTar() + if err != nil { + t.Fatal(err) + } + image, err := graph.Create(archive, "", "Testing") + if err != nil { + t.Fatal(err) + } + tmp, err := ioutil.TempDir("", "docker-test-graph-mount-") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmp) + rootfs := path.Join(tmp, "rootfs") + if err := os.MkdirAll(rootfs, 0700); err != nil { + t.Fatal(err) + } + rw := path.Join(tmp, "rw") + if err := os.MkdirAll(rw, 0700); err != nil { + t.Fatal(err) + } + if err := image.Mount(rootfs, rw); err != nil { + t.Fatal(err) + } + // FIXME: test for mount contents + defer func() { + if err := Unmount(rootfs); err != nil { + t.Error(err) + } + }() +} + +/* + * HELPER FUNCTIONS + */ + +func tempGraph(t *testing.T) *Graph { + tmp, err := ioutil.TempDir("", "docker-graph-") + if err != nil { + t.Fatal(err) + } + graph, err := New(tmp) + if err != nil { + t.Fatal(err) + } + return graph +} + +func testArchive(t *testing.T) Archive { + archive, err := fake.FakeTar() + if err != nil { + t.Fatal(err) + } + return archive +} diff --git a/graph/image.go b/graph/image.go new file mode 100644 index 0000000000..ca03461530 --- /dev/null +++ b/graph/image.go @@ -0,0 +1,245 @@ +package graph + +import ( + "encoding/json" + "fmt" + "github.com/dotcloud/docker/future" + "io/ioutil" + "os" + "path" + "strings" + "syscall" + "time" +) + +type Image struct { + Id string + Parent string + Comment string + Created time.Time + graph *Graph +} + +func LoadImage(root string) (*Image, error) { + // Load the json data + jsonData, err := ioutil.ReadFile(jsonPath(root)) + if err != nil { + return nil, err + } + var img Image + if err := json.Unmarshal(jsonData, &img); err != nil { + return nil, err + } + if err := ValidateId(img.Id); err != nil { + return nil, err + } + // Check that the filesystem layer exists + if stat, err := os.Stat(layerPath(root)); err != nil { + if os.IsNotExist(err) { + return nil, fmt.Errorf("Couldn't load image %s: no filesystem layer", img.Id) + } else { + return nil, err + } + } else if !stat.IsDir() { + return nil, fmt.Errorf("Couldn't load image %s: %s is not a directory", img.Id, layerPath(root)) + } + return &img, nil +} + +func StoreImage(img *Image, layerData Archive, root string) error { + // Check that root doesn't already exist + if _, err := os.Stat(root); err == nil { + return fmt.Errorf("Image %s already exists", img.Id) + } else if !os.IsNotExist(err) { + return err + } + // Store the layer + layer := layerPath(root) + if err := os.MkdirAll(layer, 0700); err != nil { + return err + } + if err := Untar(layerData, layer); err != nil { + return err + } + // Store the json ball + jsonData, err := json.Marshal(img) + if err != nil { + return err + } + if err := ioutil.WriteFile(jsonPath(root), jsonData, 0600); err != nil { + return err + } + return nil +} + +func layerPath(root string) string { + return path.Join(root, "layer") +} + +func jsonPath(root string) string { + return path.Join(root, "json") +} + +func MountAUFS(ro []string, rw string, target string) error { + // FIXME: Now mount the layers + rwBranch := fmt.Sprintf("%v=rw", rw) + roBranches := "" + for _, layer := range ro { + roBranches += fmt.Sprintf("%v=ro:", layer) + } + branches := fmt.Sprintf("br:%v:%v", rwBranch, roBranches) + return mount("none", target, "aufs", 0, branches) +} + +func Unmount(target string) error { + if err := syscall.Unmount(target, 0); err != nil { + return err + } + // Even though we just unmounted the filesystem, AUFS will prevent deleting the mntpoint + // for some time. We'll just keep retrying until it succeeds. + for retries := 0; retries < 1000; retries++ { + err := os.Remove(target) + if err == nil { + // rm mntpoint succeeded + return nil + } + if os.IsNotExist(err) { + // mntpoint doesn't exist anymore. Success. + return nil + } + // fmt.Printf("(%v) Remove %v returned: %v\n", retries, target, err) + time.Sleep(10 * time.Millisecond) + } + return fmt.Errorf("Umount: Failed to umount %v", target) +} + +func (image *Image) Mount(root, rw string) error { + layers, err := image.layers() + if err != nil { + return err + } + // FIXME: @creack shouldn't we do this after going over changes? + if err := MountAUFS(layers, rw, root); err != nil { + return err + } + // FIXME: Create tests for deletion + // FIXME: move this part to change.go + // Retrieve the changeset from the parent and apply it to the container + // - Retrieve the changes + changes, err := Changes(layers, layers[0]) + if err != nil { + return err + } + // Iterate on changes + for _, c := range changes { + // If there is a delete + if c.Kind == ChangeDelete { + // Make sure the directory exists + file_path, file_name := path.Dir(c.Path), path.Base(c.Path) + if err := os.MkdirAll(path.Join(rw, file_path), 0755); err != nil { + return err + } + // And create the whiteout (we just need to create empty file, discard the return) + if _, err := os.Create(path.Join(path.Join(rw, file_path), + ".wh."+path.Base(file_name))); err != nil { + return err + } + } + } + return nil +} + +func ValidateId(id string) error { + if id == "" { + return fmt.Errorf("Image id can't be empty") + } + if strings.Contains(id, ":") { + return fmt.Errorf("Invalid character in image id: ':'") + } + return nil +} + +func GenerateId() string { + future.Seed() + return future.RandomId() +} + +// Image includes convenience proxy functions to its graph +// These functions will return an error if the image is not registered +// (ie. if image.graph == nil) + +func (img *Image) History() ([]*Image, error) { + var parents []*Image + if err := img.WalkHistory( + func(img *Image) { + parents = append(parents, img) + }, + ); err != nil { + return nil, err + } + return parents, nil +} + +// layers returns all the filesystem layers needed to mount an image +func (img *Image) layers() ([]string, error) { + var list []string + var e error + if err := img.WalkHistory( + func(img *Image) { + if layer, err := img.layer(); err != nil { + e = err + } else if layer != "" { + list = append(list, layer) + } + }, + ); err != nil { + return nil, err + } else if e != nil { // Did an error occur inside the handler? + return nil, e + } + if len(list) == 0 { + return nil, fmt.Errorf("No layer found for image %s\n", img.Id) + } + return list, nil +} + +func (img *Image) WalkHistory(handler func(*Image)) error { + var err error + currentImg := img + for currentImg != nil { + if handler != nil { + handler(currentImg) + } + currentImg, err = currentImg.GetParent() + if err != nil { + return fmt.Errorf("Error while getting parent image: %v", err) + } + } + return nil +} + +func (img *Image) GetParent() (*Image, error) { + if img.Parent == "" { + return nil, nil + } + if img.graph == nil { + return nil, fmt.Errorf("Can't lookup parent of unregistered image") + } + return img.graph.Get(img.Parent) +} + +func (img *Image) root() (string, error) { + if img.graph == nil { + return "", fmt.Errorf("Can't lookup root of unregistered image") + } + return img.graph.imageRoot(img.Id), nil +} + +// Return the path of an image's layer +func (img *Image) layer() (string, error) { + root, err := img.root() + if err != nil { + return "", err + } + return layerPath(root), nil +} diff --git a/fs/mount_darwin.go b/graph/mount_darwin.go similarity index 92% rename from fs/mount_darwin.go rename to graph/mount_darwin.go index 540d6f7691..1fb24390b3 100644 --- a/fs/mount_darwin.go +++ b/graph/mount_darwin.go @@ -1,4 +1,4 @@ -package fs +package graph import "errors" diff --git a/fs/mount_linux.go b/graph/mount_linux.go similarity index 92% rename from fs/mount_linux.go rename to graph/mount_linux.go index b36888f75c..fd849821cf 100644 --- a/fs/mount_linux.go +++ b/graph/mount_linux.go @@ -1,4 +1,4 @@ -package fs +package graph import "syscall"