Prechádzať zdrojové kódy

Experimenting with a UI which differentiates images and containers

Solomon Hykes 12 rokov pred
rodič
commit
904b0ab52b
5 zmenil súbory, kde vykonal 573 pridanie a 260 odobranie
  1. 3 4
      container.go
  2. 197 214
      dockerd/dockerd.go
  3. 316 0
      image/image.go
  4. 46 42
      image/layers.go
  5. 11 0
      state.go

+ 3 - 4
container.go

@@ -59,16 +59,15 @@ func createContainer(id string, root string, command string, args []string, laye
 		stdoutLog:	new(bytes.Buffer),
 		stderrLog:	new(bytes.Buffer),
 	}
-	if err := container.Filesystem.createMountPoints(); err != nil {
-		return nil, err
-	}
-
 	container.stdout.AddWriter(NopWriteCloser(container.stdoutLog))
 	container.stderr.AddWriter(NopWriteCloser(container.stderrLog))
 
 	if err := os.Mkdir(root, 0700); err != nil {
 		return nil, err
 	}
+	if err := container.Filesystem.createMountPoints(); err != nil {
+		return nil, err
+	}
 	if err := container.save(); err != nil {
 		return nil, err
 	}

+ 197 - 214
dockerd/dockerd.go

@@ -3,6 +3,7 @@ package main
 import (
 	"github.com/dotcloud/docker"
 	"github.com/dotcloud/docker/rcli"
+	"github.com/dotcloud/docker/image"
 	"github.com/dotcloud/docker/future"
 	"bufio"
 	"errors"
@@ -12,7 +13,6 @@ import (
 	"fmt"
 	"strings"
 	"text/tabwriter"
-	"sort"
 	"os"
 	"time"
 	"net/http"
@@ -60,7 +60,7 @@ func (srv *Server) CmdStop(stdin io.ReadCloser, stdout io.Writer, args ...string
 		return nil
 	}
 	for _, name := range cmd.Args() {
-		if container := srv.docker.Get(name); container != nil {
+		if container := srv.containers.Get(name); container != nil {
 			if err := container.Stop(); err != nil {
 				return err
 			}
@@ -83,7 +83,7 @@ func (srv *Server) CmdUmount(stdin io.ReadCloser, stdout io.Writer, args ...stri
 		return nil
 	}
 	for _, name := range cmd.Args() {
-		if container, exists := srv.findContainer(name); exists {
+		if container := srv.containers.Get(name); container != nil {
 			if err := container.Filesystem.Umount(); err != nil {
 				return err
 			}
@@ -106,7 +106,7 @@ func (srv *Server) CmdMount(stdin io.ReadCloser, stdout io.Writer, args ...strin
 		return nil
 	}
 	for _, name := range cmd.Args() {
-		if container, exists := srv.findContainer(name); exists {
+		if container := srv.containers.Get(name); container != nil {
 			if err := container.Filesystem.Mount(); err != nil {
 				return err
 			}
@@ -129,7 +129,7 @@ func (srv *Server) CmdCat(stdin io.ReadCloser, stdout io.Writer, args ...string)
 		return nil
 	}
 	name, path := cmd.Arg(0), cmd.Arg(1)
-	if container, exists := srv.findContainer(name); exists {
+	if container := srv.containers.Get(name); container != nil {
 		if f, err := container.Filesystem.OpenFile(path, os.O_RDONLY, 0); err != nil {
 			return err
 		} else if _, err := io.Copy(stdout, f); err != nil {
@@ -151,7 +151,7 @@ func (srv *Server) CmdWrite(stdin io.ReadCloser, stdout io.Writer, args ...strin
 		return nil
 	}
 	name, path := cmd.Arg(0), cmd.Arg(1)
-	if container, exists := srv.findContainer(name); exists {
+	if container := srv.containers.Get(name); container != nil {
 		if f, err := container.Filesystem.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0600); err != nil {
 			return err
 		} else if _, err := io.Copy(f, stdin); err != nil {
@@ -174,7 +174,7 @@ func (srv *Server) CmdLs(stdin io.ReadCloser, stdout io.Writer, args ...string)
 		return nil
 	}
 	name, path := cmd.Arg(0), cmd.Arg(1)
-	if container, exists := srv.findContainer(name); exists {
+	if container := srv.containers.Get(name); container != nil {
 		if files, err := container.Filesystem.ReadDir(path); err != nil {
 			return err
 		} else {
@@ -198,7 +198,7 @@ func (srv *Server) CmdInspect(stdin io.ReadCloser, stdout io.Writer, args ...str
 		return nil
 	}
 	name := cmd.Arg(0)
-	if container, exists := srv.findContainer(name); exists {
+	if container := srv.containers.Get(name); container != nil {
 		data, err := json.Marshal(container)
 		if err != nil {
 			return err
@@ -215,12 +215,54 @@ func (srv *Server) CmdInspect(stdin io.ReadCloser, stdout io.Writer, args ...str
 	return errors.New("No such container: " + name)
 }
 
+func (srv *Server) CmdRm(stdin io.ReadCloser, stdout io.Writer, args ...string) error {
+	flags := rcli.Subcmd(stdout, "rm", "[OPTIONS] CONTAINER", "Remove a container")
+	if err := flags.Parse(args); err != nil {
+		return nil
+	}
+	for _, name := range flags.Args() {
+		container := srv.containers.Get(name)
+		if container == nil {
+			return errors.New("No such container: " + name)
+		}
+		if err := srv.containers.Destroy(container); err != nil {
+			fmt.Fprintln(stdout, "Error destroying container " + name + ": " + err.Error())
+		}
+	}
+	return nil
+}
 
+func (srv *Server) CmdPull(stdin io.ReadCloser, stdout io.Writer, args ...string) error {
+	if len(args) < 1 {
+		return errors.New("Not enough arguments")
+	}
+	resp, err := http.Get(args[0])
+	if err != nil {
+		return err
+	}
+	img, err := srv.images.Import(args[0], resp.Body, stdout, nil)
+	if err != nil {
+		return err
+	}
+	fmt.Fprintln(stdout, img.Id)
+	return nil
+}
 
+func (srv *Server) CmdPut(stdin io.ReadCloser, stdout io.Writer, args ...string) error {
+	if len(args) < 1 {
+		return errors.New("Not enough arguments")
+	}
+	img, err := srv.images.Import(args[0], stdin, stdout, nil)
+	if err != nil {
+		return err
+	}
+	fmt.Fprintln(stdout, img.Id)
+	return nil
+}
 
-func (srv *Server) CmdList(stdin io.ReadCloser, stdout io.Writer, args ...string) error {
-	flags := rcli.Subcmd(stdout, "list", "[OPTIONS] [NAME]", "List containers")
-	limit := flags.Int("l", 0, "Only show the N most recent versions of each name")
+func (srv *Server) CmdImages(stdin io.ReadCloser, stdout io.Writer, args ...string) error {
+	flags := rcli.Subcmd(stdout, "images", "[OPTIONS] [NAME]", "List images")
+	limit := flags.Int("l", 0, "Only show the N most recent versions of each image")
 	quiet := flags.Bool("q", false, "only show numeric IDs")
 	flags.Parse(args)
 	if flags.NArg() > 1 {
@@ -231,34 +273,28 @@ func (srv *Server) CmdList(stdin io.ReadCloser, stdout io.Writer, args ...string
 	if flags.NArg() == 1 {
 		nameFilter = flags.Arg(0)
 	}
-	var names []string
-	for name := range srv.containersByName {
-		names = append(names, name)
-	}
-	sort.Strings(names)
 	w := tabwriter.NewWriter(stdout, 20, 1, 3, ' ', 0)
 	if (!*quiet) {
-		fmt.Fprintf(w, "NAME\tID\tCREATED\tSOURCE\tRUNNING\tMOUNTED\tCOMMAND\tPID\tEXIT\n")
+		fmt.Fprintf(w, "NAME\tID\tCREATED\tPARENT\n")
 	}
-	for _, name := range names {
+	for _, name := range srv.images.Names() {
 		if nameFilter != "" && nameFilter != name {
 			continue
 		}
-		for idx, container := range *srv.containersByName[name] {
+		for idx, img := range *srv.images.ByName[name] {
 			if *limit > 0 && idx >= *limit {
 				break
 			}
 			if !*quiet {
+				id := img.Id
+				if !img.IdIsFinal() {
+					id += "..."
+				}
 				for idx, field := range []string{
-					/* NAME */	container.GetUserData("name"),
-					/* ID */	container.Id,
-					/* CREATED */	future.HumanDuration(time.Now().Sub(container.Created)) + " ago",
-					/* SOURCE */	container.GetUserData("source"),
-					/* RUNNING */	fmt.Sprintf("%v", container.State.Running),
-					/* MOUNTED */	fmt.Sprintf("%v", container.Filesystem.IsMounted()),
-					/* COMMAND */	fmt.Sprintf("%s %s", container.Path, strings.Join(container.Args, " ")),
-					/* PID */	fmt.Sprintf("%v", container.State.Pid),
-					/* EXIT CODE */	fmt.Sprintf("%v", container.State.ExitCode),
+					/* NAME */	name,
+					/* ID */	id,
+					/* CREATED */	future.HumanDuration(time.Now().Sub(img.Created)) + " ago",
+					/* PARENT */	img.Parent,
 				} {
 					if idx == 0 {
 						w.Write([]byte(field))
@@ -268,7 +304,7 @@ func (srv *Server) CmdList(stdin io.ReadCloser, stdout io.Writer, args ...string
 				}
 				w.Write([]byte{'\n'})
 			} else {
-				stdout.Write([]byte(container.Id + "\n"))
+				stdout.Write([]byte(img.Id + "\n"))
 			}
 		}
 	}
@@ -276,113 +312,120 @@ func (srv *Server) CmdList(stdin io.ReadCloser, stdout io.Writer, args ...string
 		w.Flush()
 	}
 	return nil
-}
 
-func (srv *Server) findContainer(name string) (*docker.Container, bool) {
-	// 1: look for container by ID
-	if container := srv.docker.Get(name); container != nil {
-		return container, true
-	}
-	// 2: look for a container by name (and pick the most recent)
-	if containers, exists := srv.containersByName[name]; exists {
-		return (*containers)[0], true
-	}
-	return nil, false
 }
 
-
-func (srv *Server) CmdRm(stdin io.ReadCloser, stdout io.Writer, args ...string) error {
-	flags := rcli.Subcmd(stdout, "rm", "[OPTIONS] CONTAINER", "Remove a container")
-	if err := flags.Parse(args); err != nil {
+func (srv *Server) CmdContainers(stdin io.ReadCloser, stdout io.Writer, args ...string) error {
+	cmd := rcli.Subcmd(stdout,
+		"containers", "[OPTIONS]",
+		"List containers")
+	quiet := cmd.Bool("q", false, "Only display numeric IDs")
+	if err := cmd.Parse(args); err != nil {
 		return nil
 	}
-	for _, name := range flags.Args() {
-		if _, err := srv.rm(name); err != nil {
-			fmt.Fprintln(stdout, "Error: " + err.Error())
+	w := tabwriter.NewWriter(stdout, 20, 1, 3, ' ', 0)
+	if (!*quiet) {
+		fmt.Fprintf(w, "ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\n")
+	}
+	for _, container := range srv.containers.List() {
+		if !*quiet {
+			for idx, field := range[]string {
+				/* ID */	container.Id,
+				/* IMAGE */	container.GetUserData("image"),
+				/* COMMAND */	fmt.Sprintf("%s %s", container.Path, strings.Join(container.Args, " ")),
+				/* CREATED */	future.HumanDuration(time.Now().Sub(container.Created)) + " ago",
+				/* STATUS */	container.State.String(),
+			} {
+				if idx == 0 {
+					w.Write([]byte(field))
+				} else {
+					w.Write([]byte("\t" + field))
+				}
+			}
+			w.Write([]byte{'\n'})
+		} else {
+			stdout.Write([]byte(container.Id + "\n"))
 		}
 	}
+	if (!*quiet) {
+		w.Flush()
+	}
 	return nil
 }
 
-func (srv *Server) CmdPull(stdin io.ReadCloser, stdout io.Writer, args ...string) error {
-	if len(args) < 1 {
-		return errors.New("Not enough arguments")
-	}
-	resp, err := http.Get(args[0])
-	if err != nil {
-		return err
-	}
-	layer, err := srv.layers.AddLayer(resp.Body, stdout)
-	if err != nil {
-		return err
+func (srv *Server) CmdLayers(stdin io.ReadCloser, stdout io.Writer, args ...string) error {
+	flags := rcli.Subcmd(stdout,
+		"layers", "[OPTIONS]",
+		"List filesystem layers (debug only)")
+	if err := flags.Parse(args); err != nil {
+		return nil
 	}
-	container, err := srv.addContainer(layer.Id(), []string{layer.Path}, args[0], "download")
-	if err != nil {
-		return err
+	for _, layer := range srv.images.Layers.List() {
+		fmt.Fprintln(stdout, layer)
 	}
-	fmt.Fprintln(stdout, container.Id)
 	return nil
 }
 
-func (srv *Server) CmdPut(stdin io.ReadCloser, stdout io.Writer, args ...string) error {
-	if len(args) < 1 {
-		return errors.New("Not enough arguments")
+
+func (srv *Server) CmdCp(stdin io.ReadCloser, stdout io.Writer, args ...string) error {
+	flags := rcli.Subcmd(stdout,
+		"cp", "[OPTIONS] IMAGE NAME",
+		"Create a copy of IMAGE and call it NAME")
+	if err := flags.Parse(args); err != nil {
+		return nil
 	}
-	fmt.Printf("Adding layer\n")
-	layer, err := srv.layers.AddLayer(stdin, stdout)
-	if err != nil {
+	if newImage, err := srv.images.Copy(flags.Arg(0), flags.Arg(1)); err != nil {
 		return err
+	} else {
+		fmt.Fprintln(stdout, newImage.Id)
 	}
-	id := layer.Id()
-	if !srv.docker.Exists(id) {
-		log.Println("Creating new container: " + id)
-		log.Printf("%v\n", srv.docker.List())
-		_, err := srv.addContainer(id, []string{layer.Path}, args[0], "upload")
-		if err != nil {
-			return err
-		}
-	}
-	fmt.Fprintln(stdout, id)
 	return nil
 }
 
-
 func (srv *Server) CmdCommit(stdin io.ReadCloser, stdout io.Writer, args ...string) error {
 	flags := rcli.Subcmd(stdout,
-		"fork", "[OPTIONS] CONTAINER [DEST]",
-		"Duplicate a container")
-	// FIXME "-r" to reset changes in the new container
+		"commit", "[OPTIONS] CONTAINER [DEST]",
+		"Create a new image from a container's changes")
 	if err := flags.Parse(args); err != nil {
 		return nil
 	}
-	srcName, dstName := flags.Arg(0), flags.Arg(1)
-	if srcName == "" {
+	containerName, imgName := flags.Arg(0), flags.Arg(1)
+	if containerName == "" || imgName == "" {
 		flags.Usage()
 		return nil
 	}
-	if dstName == "" {
-		dstName = srcName
-	}
-	/*
-	if src, exists := srv.findContainer(srcName); exists {
-		baseLayer := src.Filesystem.Layers[0]
-		//dst := srv.addContainer(dstName, "snapshot:" + src.Id, src.Size)
-		//fmt.Fprintln(stdout, dst.Id)
+	if container := srv.containers.Get(containerName); container != nil {
+		// FIXME: freeze the container before copying it to avoid data corruption?
+		rwTar, err := docker.Tar(container.Filesystem.RWPath)
+		if err != nil {
+			return err
+		}
+		// Create a new image from the container's base layers + a new layer from container changes
+		parentImg := srv.images.Find(container.GetUserData("image"))
+		img, err := srv.images.Import(imgName, rwTar, stdout, parentImg)
+		if err != nil {
+			return err
+		}
+		fmt.Fprintln(stdout, img.Id)
 		return nil
 	}
-	*/
-	return errors.New("No such container: " + srcName)
+	return errors.New("No such container: " + containerName)
 }
 
+
 func (srv *Server) CmdTar(stdin io.ReadCloser, stdout io.Writer, args ...string) error {
 	flags := rcli.Subcmd(stdout,
 		"tar", "CONTAINER",
 		"Stream the contents of a container as a tar archive")
+	fl_sparse := flags.Bool("s", false, "Generate a sparse tar stream (top layer + reference to bottom layers)")
 	if err := flags.Parse(args); err != nil {
 		return nil
 	}
+	if *fl_sparse {
+		return errors.New("Sparse mode not yet implemented") // FIXME
+	}
 	name := flags.Arg(0)
-	if container, exists := srv.findContainer(name); exists {
+	if container := srv.containers.Get(name); container != nil {
 		data, err := container.Filesystem.Tar()
 		if err != nil {
 			return err
@@ -406,7 +449,7 @@ func (srv *Server) CmdDiff(stdin io.ReadCloser, stdout io.Writer, args ...string
 	if flags.NArg() < 1 {
 		return errors.New("Not enough arguments")
 	}
-	if container, exists := srv.findContainer(flags.Arg(0)); !exists {
+	if container := srv.containers.Get(flags.Arg(0)); container == nil {
 		return errors.New("No such container")
 	} else {
 		changes, err := container.Filesystem.Changes()
@@ -431,7 +474,7 @@ func (srv *Server) CmdReset(stdin io.ReadCloser, stdout io.Writer, args ...strin
 		return errors.New("Not enough arguments")
 	}
 	for _, name := range flags.Args() {
-		if container, exists := srv.findContainer(name); exists {
+		if container := srv.containers.Get(name); container != nil {
 			if err := container.Filesystem.Reset(); err != nil {
 				return errors.New("Reset " + container.Id + ": " + err.Error())
 			}
@@ -440,70 +483,6 @@ func (srv *Server) CmdReset(stdin io.ReadCloser, stdout io.Writer, args ...strin
 	return nil
 }
 
-// ByDate wraps an array of layers so they can be sorted by date (most recent first)
-
-type ByDate []*docker.Container
-
-func (c *ByDate) Len() int {
-	return len(*c)
-}
-
-func (c *ByDate) Less(i, j int) bool {
-	containers := *c
-	return containers[j].Created.Before(containers[i].Created)
-}
-
-func (c *ByDate) Swap(i, j int) {
-	containers := *c
-	tmp := containers[i]
-	containers[i] = containers[j]
-	containers[j] = tmp
-}
-
-func (c *ByDate) Add(container *docker.Container) {
-	*c = append(*c, container)
-	sort.Sort(c)
-}
-
-func (c *ByDate) Del(id string) {
-	for idx, container := range *c {
-		if container.Id == id {
-			*c = append((*c)[:idx], (*c)[idx + 1:]...)
-		}
-	}
-}
-
-
-func (srv *Server) addContainer(id string, layers []string, name string, source string) (*docker.Container, error) {
-	c, err := srv.docker.Create(id, "", nil, layers, &docker.Config{Hostname: id, Ram: 512 * 1024 * 1024})
-	if err != nil {
-		return nil, err
-	}
-	if err := c.SetUserData("name", name); err != nil {
-		srv.docker.Destroy(c)
-		return nil, err
-	}
-	if _, exists := srv.containersByName[name]; !exists {
-		srv.containersByName[name] = new(ByDate)
-	}
-	srv.containersByName[name].Add(c)
-	return c, nil
-}
-
-
-func (srv *Server) rm(id string) (*docker.Container, error) {
-	container := srv.docker.Get(id)
-	if container == nil {
-		return nil, errors.New("No such continer: " + id)
-	}
-	// Remove from name lookup
-	srv.containersByName[container.GetUserData("name")].Del(container.Id)
-	if err := srv.docker.Destroy(container); err != nil {
-		return container, err
-	}
-	return container, nil
-}
-
 
 func (srv *Server) CmdLogs(stdin io.ReadCloser, stdout io.Writer, args ...string) error {
 	flags := rcli.Subcmd(stdout, "logs", "[OPTIONS] CONTAINER", "Fetch the logs of a container")
@@ -515,7 +494,7 @@ func (srv *Server) CmdLogs(stdin io.ReadCloser, stdout io.Writer, args ...string
 		return nil
 	}
 	name := flags.Arg(0)
-	if container, exists := srv.findContainer(name); exists {
+	if container := srv.containers.Get(name); container != nil {
 		if _, err := io.Copy(stdout, container.StdoutLog()); err != nil {
 			return err
 		}
@@ -528,8 +507,21 @@ func (srv *Server) CmdLogs(stdin io.ReadCloser, stdout io.Writer, args ...string
 }
 
 
+func (srv *Server) CreateContainer(img *image.Image, cmd string, args ...string) (*docker.Container, error) {
+	id := future.RandomId()
+	container, err := srv.containers.Create(id, cmd, args, img.Layers, &docker.Config{Hostname: id})
+	if err != nil {
+		return nil, err
+	}
+	if err := container.SetUserData("image", img.Id); err != nil {
+		srv.containers.Destroy(container)
+		return nil, errors.New("Error setting container userdata: " + err.Error())
+	}
+	return container, nil
+}
+
 func (srv *Server) CmdRun(stdin io.ReadCloser, stdout io.Writer, args ...string) error {
-	flags := rcli.Subcmd(stdout, "run", "[OPTIONS] CONTAINER COMMAND [ARG...]", "Run a command in a container")
+	flags := rcli.Subcmd(stdout, "run", "[OPTIONS] IMAGE COMMAND [ARG...]", "Run a command in a new container")
 	fl_attach := flags.Bool("a", false, "Attach stdin and stdout")
 	//fl_tty := flags.Bool("t", false, "Allocate a pseudo-tty")
 	if err := flags.Parse(args); err != nil {
@@ -540,40 +532,44 @@ func (srv *Server) CmdRun(stdin io.ReadCloser, stdout io.Writer, args ...string)
 		return nil
 	}
 	name, cmd := flags.Arg(0), flags.Args()[1:]
-	if container, exists := srv.findContainer(name); exists {
-		log.Printf("Running container %#v\n", container)
-		container.Path = cmd[0]
-		container.Args  = cmd[1:]
-		if *fl_attach {
-			cmd_stdout, err := container.StdoutPipe()
-			if err != nil {
-				return err
-			}
-			cmd_stderr, err := container.StderrPipe()
-			if err != nil {
-				return err
-			}
-			if err := container.Start(); err != nil {
-				return err
-			}
-			sending_stdout := future.Go(func() error { _, err := io.Copy(stdout, cmd_stdout); return err })
-			sending_stderr := future.Go(func() error { _, err := io.Copy(stdout, cmd_stderr); return err })
-			err_sending_stdout := <-sending_stdout
-			err_sending_stderr := <-sending_stderr
-			if err_sending_stdout != nil {
-				return err_sending_stdout
-			}
-			return err_sending_stderr
-		} else {
-			if output, err := container.Output(); err != nil {
-				return err
-			} else {
-				fmt.Printf("-->|%s|\n", output)
-			}
+	// Find the image
+	img := srv.images.Find(name)
+	if img == nil {
+		return errors.New("No such image: " + name)
+	}
+	// Create new container
+	container, err := srv.CreateContainer(img, cmd[0], cmd[1:]...)
+	if err != nil {
+		return errors.New("Error creating container: " + err.Error())
+	}
+	// Run the container
+	if *fl_attach {
+		cmd_stdout, err := container.StdoutPipe()
+		if err != nil {
+			return err
 		}
-		return nil
+		cmd_stderr, err := container.StderrPipe()
+		if err != nil {
+			return err
+		}
+		if err := container.Start(); err != nil {
+			return err
+		}
+		sending_stdout := future.Go(func() error { _, err := io.Copy(stdout, cmd_stdout); return err })
+		sending_stderr := future.Go(func() error { _, err := io.Copy(stdout, cmd_stderr); return err })
+		err_sending_stdout := <-sending_stdout
+		err_sending_stderr := <-sending_stderr
+		if err_sending_stdout != nil {
+			return err_sending_stdout
+		}
+		return err_sending_stderr
+	} else {
+		if err := container.Start(); err != nil {
+			return err
+		}
+		fmt.Fprintln(stdout, container.Id)
 	}
-	return errors.New("No such container: " + name)
+	return nil
 }
 
 func main() {
@@ -594,30 +590,18 @@ func main() {
 }
 
 func New() (*Server, error) {
-	store, err := future.NewStore("/var/lib/docker/layers")
+	images, err := image.New("/var/lib/docker/images")
 	if err != nil {
 		return nil, err
 	}
-	if err := store.Init(); err != nil {
-		return nil, err
-	}
-	d, err := docker.New()
+	containers, err := docker.New()
 	if err != nil {
 		return nil, err
 	}
 	srv := &Server{
-		containersByName: make(map[string]*ByDate),
-		layers: store,
-		docker: d,
-	}
-	for _, container := range srv.docker.List() {
-		name := container.GetUserData("name")
-		if _, exists := srv.containersByName[name]; !exists {
-			srv.containersByName[name] = new(ByDate)
-		}
-		srv.containersByName[name].Add(container)
+		images: images,
+		containers: containers,
 	}
-	log.Printf("Done building index\n")
 	return srv, nil
 }
 
@@ -662,8 +646,7 @@ func (srv *Server) CmdWeb(stdin io.ReadCloser, stdout io.Writer, args ...string)
 
 
 type Server struct {
-	containersByName	map[string]*ByDate
-	layers			*future.Store
-	docker			*docker.Docker
+	containers	*docker.Docker
+	images		*image.Store
 }
 

+ 316 - 0
image/image.go

@@ -0,0 +1,316 @@
+package image
+
+import (
+	"io"
+	"io/ioutil"
+	"encoding/json"
+	"time"
+	"path"
+	"path/filepath"
+	"errors"
+	"sort"
+	"os"
+	"github.com/dotcloud/docker/future"
+	"strings"
+)
+
+
+type Store struct {
+	*Index
+	Root	string
+	Layers	*LayerStore
+}
+
+
+func New(root string) (*Store, error) {
+	abspath, err := filepath.Abs(root)
+	if err != nil {
+		return nil, err
+	}
+	layers, err := NewLayerStore(path.Join(root, "layers"))
+	if err != nil {
+		return nil, err
+	}
+	if err := layers.Init(); err != nil {
+		return nil, err
+	}
+	return &Store{
+		Root: abspath,
+		Index: NewIndex(path.Join(root, "index.json")),
+		Layers: layers,
+	}, nil
+}
+
+
+func (store *Store) Import(name string, archive io.Reader, stderr io.Writer, parent *Image) (*Image, error) {
+	layer, err := store.Layers.AddLayer(archive, stderr)
+	if err != nil {
+		return nil, err
+	}
+	layers := []string{layer}
+	if parent != nil {
+		layers = append(parent.Layers, layers...)
+	}
+	var parentId string
+	if parent != nil {
+		parentId = parent.Id
+	}
+	return store.Create(name, parentId, layers...)
+}
+
+func (store *Store) Create(name string, source string, layers ...string) (*Image, error) {
+	image, err := NewImage(name, layers, source)
+	if err != nil {
+		return nil, err
+	}
+	if err := store.Index.Add(name, image); err != nil {
+		return nil, err
+	}
+	return image, nil
+}
+
+
+// Index
+
+type Index struct {
+	Path	string
+	ByName	map[string]*History
+	ById	map[string]*Image
+}
+
+func NewIndex(path string) *Index {
+	return &Index{
+		Path: path,
+		ByName: make(map[string]*History),
+		ById: make(map[string]*Image),
+	}
+}
+
+func (index *Index) Exists(id string) bool {
+	_, exists := index.ById[id]
+	return exists
+}
+
+func (index *Index) Find(idOrName string) *Image {
+	// Load
+	if err := index.load(); err != nil {
+		return nil
+	}
+	// Lookup by ID
+	if image, exists := index.ById[idOrName]; exists {
+		return image
+	}
+	// Lookup by name
+	if history, exists := index.ByName[idOrName]; exists && history.Len() > 0 {
+		return (*history)[0]
+	}
+	return nil
+}
+
+func (index *Index) Add(name string, image *Image) error {
+	// Load
+	if err := index.load(); err != nil {
+		return err
+	}
+	if _, exists := index.ByName[name]; !exists {
+		index.ByName[name] = new(History)
+	} else {
+		// If this image is already the latest version, don't add it.
+		if (*index.ByName[name])[0].Id == image.Id {
+			return nil
+		}
+	}
+	index.ByName[name].Add(image)
+	index.ById[image.Id] = image
+	// Save
+	if err := index.save(); err != nil {
+		return err
+	}
+	return nil
+}
+
+func (index *Index) Copy(srcNameOrId, dstName string) (*Image, error) {
+	if srcNameOrId == "" || dstName == "" {
+		return nil, errors.New("Illegal image name")
+	}
+	// Load
+	if err := index.load(); err != nil {
+		return nil, err
+	}
+	src := index.Find(srcNameOrId)
+	if src == nil {
+		return nil, errors.New("No such image: " + srcNameOrId)
+	}
+	if index.Find(dstName) != nil {
+		return nil, errors.New(dstName + ": image already exists.")
+	}
+	dst, err := NewImage(dstName, src.Layers, src.Id)
+	if err != nil {
+		return nil, err
+	}
+	if err := index.Add(dstName, dst); err != nil {
+		return nil, err
+	}
+	// Save
+	if err := index.save(); err != nil {
+		return nil, err
+	}
+	return dst, nil
+}
+
+func (index *Index) Rename(oldName, newName string) error {
+	// Load
+	if err := index.load(); err != nil {
+		return err
+	}
+	if _, exists := index.ByName[oldName]; !exists {
+		return errors.New("Can't rename " + oldName + ": no such image.")
+	}
+	if _, exists := index.ByName[newName]; exists {
+		return errors.New("Can't rename to " + newName + ": name is already in use.")
+	}
+	index.ByName[newName] = index.ByName[oldName]
+	delete(index.ByName, oldName)
+	// Change the ID of all images, since they include the name
+	for _, image := range *index.ByName[newName] {
+		if id, err := generateImageId(newName, image.Layers); err != nil {
+			return err
+		} else {
+			oldId := image.Id
+			image.Id = id
+			index.ById[id] = image
+			delete(index.ById, oldId)
+		}
+	}
+	// Save
+	if err := index.save(); err != nil {
+		return err
+	}
+	return nil
+}
+
+func (index *Index) Names() []string {
+	if err := index.load(); err != nil {
+		return []string{}
+	}
+	var names[]string
+	for name := range index.ByName {
+		names = append(names, name)
+	}
+	sort.Strings(names)
+	return names
+}
+
+func (index *Index) load() error {
+	jsonData, err := ioutil.ReadFile(index.Path)
+	if err != nil {
+		if os.IsNotExist(err) {
+			return nil
+		}
+		return err
+	}
+	path := index.Path
+	if err := json.Unmarshal(jsonData, index); err != nil {
+		return err
+	}
+	index.Path = path
+	return nil
+}
+
+func (index *Index) save() error {
+	jsonData, err := json.Marshal(index)
+	if err != nil {
+		return err
+	}
+	if err := ioutil.WriteFile(index.Path, jsonData, 0600); err != nil {
+		return err
+	}
+	return nil
+}
+
+// History wraps an array of images so they can be sorted by date (most recent first)
+
+type History []*Image
+
+func (history *History) Len() int {
+	return len(*history)
+}
+
+func (history *History) Less(i, j int) bool {
+	images := *history
+	return images[j].Created.Before(images[i].Created)
+}
+
+func (history *History) Swap(i, j int) {
+	images := *history
+	tmp := images[i]
+	images[i] = images[j]
+	images[j] = tmp
+}
+
+func (history *History) Add(image *Image) {
+	*history = append(*history, image)
+	sort.Sort(history)
+}
+
+func (history *History) Del(id string) {
+	for idx, image := range *history {
+		if image.Id == id {
+			*history = append((*history)[:idx], (*history)[idx + 1:]...)
+		}
+	}
+}
+
+type Image struct {
+	Id	string		// Globally unique identifier
+	Layers	[]string	// Absolute paths
+	Created	time.Time
+	Parent	string
+}
+
+func (image *Image) IdParts() (string, string) {
+	if len(image.Id) < 8 {
+		return "", image.Id
+	}
+	hash := image.Id[len(image.Id)-8:len(image.Id)]
+	name := image.Id[:len(image.Id)-9]
+	return name, hash
+}
+
+func (image *Image) IdIsFinal() bool {
+	return len(image.Layers) == 1
+}
+
+func generateImageId(name string, layers []string) (string, error) {
+	if len(layers) == 0 {
+		return "", errors.New("No layers provided.")
+	}
+	var hash string
+	if len(layers) == 1 {
+		hash = path.Base(layers[0])
+	} else {
+		var ids string
+		for _, layer := range layers {
+			ids += path.Base(layer)
+		}
+		if h, err := future.ComputeId(strings.NewReader(ids)); err != nil  {
+			return "", err
+		} else {
+			hash = h
+		}
+	}
+	return name + ":" + hash, nil
+}
+
+func NewImage(name string, layers []string, parent string) (*Image, error) {
+	id, err := generateImageId(name, layers)
+	if err != nil {
+		return nil, err
+	}
+	return &Image{
+		Id:		id,
+		Layers:		layers,
+		Created:	time.Now(),
+		Parent:		parent,
+	}, nil
+}

+ 46 - 42
future/layers.go → image/layers.go

@@ -1,38 +1,52 @@
-package future
+package image
 
 import (
 	"errors"
 	"path"
 	"path/filepath"
 	"io"
+	"io/ioutil"
 	"os"
 	"os/exec"
+	"github.com/dotcloud/docker/future"
 )
 
-type Store struct {
+type LayerStore struct {
 	Root	string
 }
 
-
-func NewStore(root string) (*Store, error) {
+func NewLayerStore(root string) (*LayerStore, error) {
 	abspath, err := filepath.Abs(root)
 	if err != nil {
 		return nil, err
 	}
-	return &Store{
+	return &LayerStore{
 		Root: abspath,
 	}, nil
 }
 
-func (store *Store) Get(id string) (*Layer, bool) {
-	layer := &Layer{Path: store.layerPath(id)}
-	if !layer.Exists() {
-		return nil, false
+func (store *LayerStore) List() []string {
+	files, err := ioutil.ReadDir(store.Root)
+	if err != nil {
+		return []string{}
+	}
+	var layers []string
+	for _, st := range files {
+		if st.IsDir() {
+			layers = append(layers, path.Join(store.Root, st.Name()))
+		}
+	}
+	return layers
+}
+
+func (store *LayerStore) Get(id string) string {
+	if !store.Exists(id) {
+		return ""
 	}
-	return layer, true
+	return store.layerPath(id)
 }
 
-func (store *Store) Exists() (bool, error) {
+func (store *LayerStore) rootExists() (bool, error) {
 	if stat, err := os.Stat(store.Root); err != nil {
 		if os.IsNotExist(err) {
 			return false, nil
@@ -44,8 +58,8 @@ func (store *Store) Exists() (bool, error) {
 	return true, nil
 }
 
-func (store *Store) Init() error {
-	if exists, err := store.Exists(); err != nil {
+func (store *LayerStore) Init() error {
+	if exists, err := store.rootExists(); err != nil {
 		return err
 	} else if exists {
 		return nil
@@ -54,8 +68,8 @@ func (store *Store) Init() error {
 }
 
 
-func (store *Store) Mktemp() (string, error) {
-	tmpName := RandomId()
+func (store *LayerStore) Mktemp() (string, error) {
+	tmpName := future.RandomId()
 	tmpPath := path.Join(store.Root, "tmp-" + tmpName)
 	if err := os.Mkdir(tmpPath, 0700); err != nil {
 		return "", err
@@ -63,73 +77,63 @@ func (store *Store) Mktemp() (string, error) {
 	return tmpPath, nil
 }
 
-func (store *Store) layerPath(id string) string {
+func (store *LayerStore) layerPath(id string) string {
 	return path.Join(store.Root, id)
 }
 
 
-func (store *Store) AddLayer(archive io.Reader, stderr io.Writer) (*Layer, error) {
+func (store *LayerStore) AddLayer(archive io.Reader, stderr io.Writer) (string, error) {
 	tmp, err := store.Mktemp()
 	defer os.RemoveAll(tmp)
 	if err != nil {
-		return nil, err
+		return "", err
 	}
 	untarCmd := exec.Command("tar", "-C", tmp, "-x")
 	untarW, err := untarCmd.StdinPipe()
 	if err != nil {
-		return nil, err
+		return "", err
 	}
 	untarStderr, err := untarCmd.StderrPipe()
 	if err != nil {
-		return nil, err
+		return "", err
 	}
 	go io.Copy(stderr, untarStderr)
 	untarStdout, err := untarCmd.StdoutPipe()
 	if err != nil {
-		return nil, err
+		return "", err
 	}
 	go io.Copy(stderr, untarStdout)
 	untarCmd.Start()
 	hashR, hashW := io.Pipe()
-	job_copy := Go(func() error {
+	job_copy := future.Go(func() error {
 		_, err := io.Copy(io.MultiWriter(hashW, untarW), archive)
 		hashW.Close()
 		untarW.Close()
 		return err
 	})
-	id, err := ComputeId(hashR)
+	id, err := future.ComputeId(hashR)
 	if err != nil {
-		return nil, err
+		return "", err
 	}
 	if err := untarCmd.Wait(); err != nil {
-		return nil, err
+		return "", err
 	}
 	if err := <-job_copy; err != nil {
-		return nil, err
+		return "", err
 	}
-	layer := &Layer{Path: store.layerPath(id)}
-	if !layer.Exists() {
-		if err := os.Rename(tmp, layer.Path); err != nil {
-			return nil, err
+	layer := store.layerPath(id)
+	if !store.Exists(id) {
+		if err := os.Rename(tmp, layer); err != nil {
+			return "", err
 		}
 	}
-
 	return layer, nil
 }
 
-
-type Layer struct {
-	Path	string
-}
-
-func (layer *Layer) Exists() bool {
-	st, err := os.Stat(layer.Path)
+func (store *LayerStore) Exists(id string) bool {
+	st, err := os.Stat(store.layerPath(id))
 	if err != nil {
 		return false
 	}
 	return st.IsDir()
 }
-
-func (layer *Layer) Id() string {
-	return path.Base(layer.Path)
-}

+ 11 - 0
state.go

@@ -3,6 +3,8 @@ package docker
 import (
 	"sync"
 	"time"
+	"fmt"
+	"github.com/dotcloud/docker/future"
 )
 
 type State struct {
@@ -15,6 +17,7 @@ type State struct {
 	stateChangeCond *sync.Cond
 }
 
+
 func newState() *State {
 	lock := new(sync.Mutex)
 	return &State{
@@ -23,6 +26,14 @@ func newState() *State {
 	}
 }
 
+// String returns a human-readable description of the state
+func (s *State) String() string {
+	if s.Running {
+		return fmt.Sprintf("Running for %s", future.HumanDuration(time.Now().Sub(s.StartedAt)))
+	}
+	return fmt.Sprintf("Exited with %d", s.ExitCode)
+}
+
 func (s *State) setRunning(pid int) {
 	s.Running = true
 	s.ExitCode = 0