docker/graph: a store for filesystem images and the graph of their relationships
This commit is contained in:
parent
1480bff3a9
commit
33f6a0aaf5
8 changed files with 643 additions and 4 deletions
|
@ -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 (
|
|
@ -1,4 +1,4 @@
|
|||
package fs
|
||||
package graph
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
106
graph/changes.go
Normal file
106
graph/changes.go
Normal file
|
@ -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
graph/graph.go
Normal file
143
graph/graph.go
Normal file
|
@ -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
graph/graph_test.go
Normal file
143
graph/graph_test.go
Normal file
|
@ -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
graph/image.go
Normal file
245
graph/image.go
Normal file
|
@ -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,4 +1,4 @@
|
|||
package fs
|
||||
package graph
|
||||
|
||||
import "errors"
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package fs
|
||||
package graph
|
||||
|
||||
import "syscall"
|
||||
|
Loading…
Reference in a new issue