From 904b0ab52b065025acbdba6fdc056ef1dff04d44 Mon Sep 17 00:00:00 2001 From: Solomon Hykes Date: Sun, 27 Jan 2013 15:42:42 -0800 Subject: [PATCH] Experimenting with a UI which differentiates images and containers --- container.go | 7 +- dockerd/dockerd.go | 461 +++++++++++++++++------------------- image/image.go | 316 ++++++++++++++++++++++++ {future => image}/layers.go | 88 +++---- state.go | 11 + 5 files changed, 598 insertions(+), 285 deletions(-) create mode 100644 image/image.go rename {future => image}/layers.go (51%) diff --git a/container.go b/container.go index 1c4b58fd33..fb75b8f69a 100644 --- a/container.go +++ b/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 } diff --git a/dockerd/dockerd.go b/dockerd/dockerd.go index 30c50ecf38..c9e334f6d9 100644 --- a/dockerd/dockerd.go +++ b/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,90 +215,18 @@ func (srv *Server) CmdInspect(stdin io.ReadCloser, stdout io.Writer, args ...str return errors.New("No such container: " + name) } - - - -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") - quiet := flags.Bool("q", false, "only show numeric IDs") - flags.Parse(args) - if flags.NArg() > 1 { - flags.Usage() - return nil - } - var nameFilter 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") - } - for _, name := range names { - if nameFilter != "" && nameFilter != name { - continue - } - for idx, container := range *srv.containersByName[name] { - if *limit > 0 && idx >= *limit { - break - } - if !*quiet { - 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), - } { - 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) 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 { return nil } for _, name := range flags.Args() { - if _, err := srv.rm(name); err != nil { - fmt.Fprintln(stdout, "Error: " + err.Error()) + 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 @@ -312,15 +240,11 @@ func (srv *Server) CmdPull(stdin io.ReadCloser, stdout io.Writer, args ...string if err != nil { return err } - layer, err := srv.layers.AddLayer(resp.Body, stdout) + img, err := srv.images.Import(args[0], resp.Body, stdout, nil) if err != nil { return err } - container, err := srv.addContainer(layer.Id(), []string{layer.Path}, args[0], "download") - if err != nil { - return err - } - fmt.Fprintln(stdout, container.Id) + fmt.Fprintln(stdout, img.Id) return nil } @@ -328,61 +252,180 @@ func (srv *Server) CmdPut(stdin io.ReadCloser, stdout io.Writer, args ...string) if len(args) < 1 { return errors.New("Not enough arguments") } - fmt.Printf("Adding layer\n") - layer, err := srv.layers.AddLayer(stdin, stdout) + img, err := srv.images.Import(args[0], stdin, stdout, nil) if err != nil { return err } - 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, img.Id) + return nil +} + +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 { + flags.Usage() + return nil + } + var nameFilter string + if flags.NArg() == 1 { + nameFilter = flags.Arg(0) + } + w := tabwriter.NewWriter(stdout, 20, 1, 3, ' ', 0) + if (!*quiet) { + fmt.Fprintf(w, "NAME\tID\tCREATED\tPARENT\n") + } + for _, name := range srv.images.Names() { + if nameFilter != "" && nameFilter != name { + continue + } + 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 */ name, + /* ID */ id, + /* CREATED */ future.HumanDuration(time.Now().Sub(img.Created)) + " ago", + /* PARENT */ img.Parent, + } { + if idx == 0 { + w.Write([]byte(field)) + } else { + w.Write([]byte("\t" + field)) + } + } + w.Write([]byte{'\n'}) + } else { + stdout.Write([]byte(img.Id + "\n")) + } } } - fmt.Fprintln(stdout, id) + if (!*quiet) { + w.Flush() + } + return 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 + } + 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) 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 + } + for _, layer := range srv.images.Layers.List() { + fmt.Fprintln(stdout, layer) + } return nil } -func (srv *Server) CmdCommit(stdin io.ReadCloser, stdout io.Writer, args ...string) error { +func (srv *Server) CmdCp(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 + "cp", "[OPTIONS] IMAGE NAME", + "Create a copy of IMAGE and call it NAME") if err := flags.Parse(args); err != nil { return nil } - srcName, dstName := flags.Arg(0), flags.Arg(1) - if srcName == "" { + if newImage, err := srv.images.Copy(flags.Arg(0), flags.Arg(1)); err != nil { + return err + } else { + fmt.Fprintln(stdout, newImage.Id) + } + return nil +} + +func (srv *Server) CmdCommit(stdin io.ReadCloser, stdout io.Writer, args ...string) error { + flags := rcli.Subcmd(stdout, + "commit", "[OPTIONS] CONTAINER [DEST]", + "Create a new image from a container's changes") + if err := flags.Parse(args); err != nil { + return nil + } + 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) - } - } - return nil + // Find the image + img := srv.images.Find(name) + if img == nil { + return errors.New("No such image: " + name) } - return errors.New("No such container: " + 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 + } + 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 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, + images: images, + containers: containers, } - 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) - } - 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 } diff --git a/image/image.go b/image/image.go new file mode 100644 index 0000000000..f2f39a929b --- /dev/null +++ b/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 +} diff --git a/future/layers.go b/image/layers.go similarity index 51% rename from future/layers.go rename to image/layers.go index b85802dc4f..3b43eed2e9 100644 --- a/future/layers.go +++ b/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{} } - return layer, true + var layers []string + for _, st := range files { + if st.IsDir() { + layers = append(layers, path.Join(store.Root, st.Name())) + } + } + return layers } -func (store *Store) Exists() (bool, error) { +func (store *LayerStore) Get(id string) string { + if !store.Exists(id) { + return "" + } + return store.layerPath(id) +} + +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) -} diff --git a/state.go b/state.go index 9b15af0758..bdd1d9cd2c 100644 --- a/state.go +++ b/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