Prechádzať zdrojové kódy

docker/graph: a store for filesystem images and the graph of their relationships

Solomon Hykes 12 rokov pred
rodič
commit
33f6a0aaf5
8 zmenil súbory, kde vykonal 643 pridanie a 4 odobranie
  1. 3 1
      graph/archive.go
  2. 1 1
      graph/archive_test.go
  3. 106 0
      graph/changes.go
  4. 143 0
      graph/graph.go
  5. 143 0
      graph/graph_test.go
  6. 245 0
      graph/image.go
  7. 1 1
      graph/mount_darwin.go
  8. 1 1
      graph/mount_linux.go

+ 3 - 1
fs/archive.go → graph/archive.go

@@ -1,4 +1,4 @@
-package fs
+package graph
 
 
 import (
 import (
 	"errors"
 	"errors"
@@ -7,6 +7,8 @@ import (
 	"os/exec"
 	"os/exec"
 )
 )
 
 
+type Archive io.Reader
+
 type Compression uint32
 type Compression uint32
 
 
 const (
 const (

+ 1 - 1
fs/archive_test.go → graph/archive_test.go

@@ -1,4 +1,4 @@
-package fs
+package graph
 
 
 import (
 import (
 	"io/ioutil"
 	"io/ioutil"

+ 106 - 0
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
+}

+ 143 - 0
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)
+}

+ 143 - 0
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
+}

+ 245 - 0
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
+}

+ 1 - 1
fs/mount_darwin.go → graph/mount_darwin.go

@@ -1,4 +1,4 @@
-package fs
+package graph
 
 
 import "errors"
 import "errors"
 
 

+ 1 - 1
fs/mount_linux.go → graph/mount_linux.go

@@ -1,4 +1,4 @@
-package fs
+package graph
 
 
 import "syscall"
 import "syscall"