diff --git a/container.go b/container.go index 2bddb9c5d2..1c4b58fd33 100644 --- a/container.go +++ b/container.go @@ -11,6 +11,8 @@ import ( "path" "syscall" "time" + "strings" + "bytes" ) type Container struct { @@ -30,6 +32,9 @@ type Container struct { cmd *exec.Cmd stdout *writeBroadcaster stderr *writeBroadcaster + + stdoutLog *bytes.Buffer + stderrLog *bytes.Buffer } type Config struct { @@ -51,7 +56,15 @@ func createContainer(id string, root string, command string, args []string, laye lxcConfigPath: path.Join(root, "config.lxc"), stdout: newWriteBroadcaster(), stderr: newWriteBroadcaster(), + 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 @@ -73,14 +86,68 @@ func loadContainer(containerPath string) (*Container, error) { container := &Container{ stdout: newWriteBroadcaster(), stderr: newWriteBroadcaster(), + stdoutLog: new(bytes.Buffer), + stderrLog: new(bytes.Buffer), } if err := json.Unmarshal(data, container); err != nil { return nil, err } + if err := container.Filesystem.createMountPoints(); err != nil { + return nil, err + } container.State = newState() return container, nil } + +func (container *Container) Cmd() *exec.Cmd { + return container.cmd +} + +func (container *Container) loadUserData() (map[string]string, error) { + jsonData, err := ioutil.ReadFile(path.Join(container.Root, "userdata.json")) + if err != nil { + if os.IsNotExist(err) { + return make(map[string]string), nil + } + return nil, err + } + data := make(map[string]string) + if err := json.Unmarshal(jsonData, &data); err != nil { + return nil, err + } + return data, nil +} + +func (container *Container) saveUserData(data map[string]string) error { + jsonData, err := json.Marshal(data) + if err != nil { + return err + } + return ioutil.WriteFile(path.Join(container.Root, "userdata.json"), jsonData, 0700) +} + +func (container *Container) SetUserData(key, value string) error { + data, err := container.loadUserData() + if err != nil { + return err + } + data[key] = value + return container.saveUserData(data) +} + +func (container *Container) GetUserData(key string) (string) { + data, err := container.loadUserData() + if err != nil { + return "" + } + if value, exists := data[key]; exists { + return value + } + return "" +} + + func (container *Container) save() (err error) { data, err := json.Marshal(container) if err != nil { @@ -103,7 +170,7 @@ func (container *Container) generateLXCConfig() error { } func (container *Container) Start() error { - if err := container.Filesystem.Mount(); err != nil { + if err := container.Filesystem.EnsureMounted(); err != nil { return err } @@ -156,12 +223,21 @@ func (container *Container) StdoutPipe() (io.ReadCloser, error) { return newBufReader(reader), nil } +func (container *Container) StdoutLog() io.Reader { + return strings.NewReader(container.stdoutLog.String()) +} + + func (container *Container) StderrPipe() (io.ReadCloser, error) { reader, writer := io.Pipe() container.stderr.AddWriter(writer) return newBufReader(reader), nil } +func (container *Container) StderrLog() io.Reader { + return strings.NewReader(container.stderrLog.String()) +} + func (container *Container) monitor() { // Wait for the program to exit container.cmd.Wait() diff --git a/dockerd/dockerd.go b/dockerd/dockerd.go index 6ee17bb45b..30c50ecf38 100644 --- a/dockerd/dockerd.go +++ b/dockerd/dockerd.go @@ -1,33 +1,31 @@ package main import ( + "github.com/dotcloud/docker" "github.com/dotcloud/docker/rcli" - "github.com/dotcloud/docker/fake" "github.com/dotcloud/docker/future" "bufio" "errors" "log" "io" - "io/ioutil" - "os/exec" "flag" "fmt" - "github.com/kr/pty" "strings" - "bytes" "text/tabwriter" "sort" "os" "time" "net/http" + "encoding/json" + "bytes" ) -func (docker *Docker) Name() string { +func (srv *Server) Name() string { return "docker" } -func (docker *Docker) Help() string { +func (srv *Server) Help() string { help := "Usage: docker COMMAND [arg...]\n\nA self-sufficient runtime for linux containers.\n\nCommands:\n" for _, cmd := range [][]interface{}{ {"run", "Run a command in a container"}, @@ -42,6 +40,7 @@ func (docker *Docker) Help() string { {"commit", "Save the state of a container"}, {"attach", "Attach to the standard inputs and outputs of a running container"}, {"info", "Display system-wide information"}, + {"tar", "Stream the contents of a container as a tar archive"}, {"web", "Generate a web UI"}, } { help += fmt.Sprintf(" %-10.10s%s\n", cmd...) @@ -50,7 +49,176 @@ func (docker *Docker) Help() string { } -func (docker *Docker) CmdList(stdin io.ReadCloser, stdout io.Writer, args ...string) error { +func (srv *Server) CmdStop(stdin io.ReadCloser, stdout io.Writer, args ...string) error { + cmd := rcli.Subcmd(stdout, "stop", "[OPTIONS] NAME", "Stop a running container") + if err := cmd.Parse(args); err != nil { + cmd.Usage() + return nil + } + if cmd.NArg() < 1 { + cmd.Usage() + return nil + } + for _, name := range cmd.Args() { + if container := srv.docker.Get(name); container != nil { + if err := container.Stop(); err != nil { + return err + } + fmt.Fprintln(stdout, container.Id) + } else { + return errors.New("No such container: " + name) + } + } + return nil +} + +func (srv *Server) CmdUmount(stdin io.ReadCloser, stdout io.Writer, args ...string) error { + cmd := rcli.Subcmd(stdout, "umount", "[OPTIONS] NAME", "umount a container's filesystem (debug only)") + if err := cmd.Parse(args); err != nil { + cmd.Usage() + return nil + } + if cmd.NArg() < 1 { + cmd.Usage() + return nil + } + for _, name := range cmd.Args() { + if container, exists := srv.findContainer(name); exists { + if err := container.Filesystem.Umount(); err != nil { + return err + } + fmt.Fprintln(stdout, container.Id) + } else { + return errors.New("No such container: " + name) + } + } + return nil +} + +func (srv *Server) CmdMount(stdin io.ReadCloser, stdout io.Writer, args ...string) error { + cmd := rcli.Subcmd(stdout, "umount", "[OPTIONS] NAME", "mount a container's filesystem (debug only)") + if err := cmd.Parse(args); err != nil { + cmd.Usage() + return nil + } + if cmd.NArg() < 1 { + cmd.Usage() + return nil + } + for _, name := range cmd.Args() { + if container, exists := srv.findContainer(name); exists { + if err := container.Filesystem.Mount(); err != nil { + return err + } + fmt.Fprintln(stdout, container.Id) + } else { + return errors.New("No such container: " + name) + } + } + return nil +} + +func (srv *Server) CmdCat(stdin io.ReadCloser, stdout io.Writer, args ...string) error { + cmd := rcli.Subcmd(stdout, "cat", "[OPTIONS] CONTAINER PATH", "write the contents of a container's file to standard output") + if err := cmd.Parse(args); err != nil { + cmd.Usage() + return nil + } + if cmd.NArg() < 2 { + cmd.Usage() + return nil + } + name, path := cmd.Arg(0), cmd.Arg(1) + if container, exists := srv.findContainer(name); exists { + if f, err := container.Filesystem.OpenFile(path, os.O_RDONLY, 0); err != nil { + return err + } else if _, err := io.Copy(stdout, f); err != nil { + return err + } + return nil + } + return errors.New("No such container: " + name) +} + +func (srv *Server) CmdWrite(stdin io.ReadCloser, stdout io.Writer, args ...string) error { + cmd := rcli.Subcmd(stdout, "write", "[OPTIONS] CONTAINER PATH", "write the contents of standard input to a container's file") + if err := cmd.Parse(args); err != nil { + cmd.Usage() + return nil + } + if cmd.NArg() < 2 { + cmd.Usage() + return nil + } + name, path := cmd.Arg(0), cmd.Arg(1) + if container, exists := srv.findContainer(name); exists { + 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 { + return err + } + return nil + } + return errors.New("No such container: " + name) +} + + +func (srv *Server) CmdLs(stdin io.ReadCloser, stdout io.Writer, args ...string) error { + cmd := rcli.Subcmd(stdout, "ls", "[OPTIONS] CONTAINER PATH", "List the contents of a container's directory") + if err := cmd.Parse(args); err != nil { + cmd.Usage() + return nil + } + if cmd.NArg() < 2 { + cmd.Usage() + return nil + } + name, path := cmd.Arg(0), cmd.Arg(1) + if container, exists := srv.findContainer(name); exists { + if files, err := container.Filesystem.ReadDir(path); err != nil { + return err + } else { + for _, f := range files { + fmt.Fprintln(stdout, f.Name()) + } + } + return nil + } + return errors.New("No such container: " + name) +} + +func (srv *Server) CmdInspect(stdin io.ReadCloser, stdout io.Writer, args ...string) error { + cmd := rcli.Subcmd(stdout, "inspect", "[OPTIONS] CONTAINER", "Return low-level information on a container") + if err := cmd.Parse(args); err != nil { + cmd.Usage() + return nil + } + if cmd.NArg() < 1 { + cmd.Usage() + return nil + } + name := cmd.Arg(0) + if container, exists := srv.findContainer(name); exists { + data, err := json.Marshal(container) + if err != nil { + return err + } + indented := new(bytes.Buffer) + if err = json.Indent(indented, data, "", " "); err != nil { + return err + } + if _, err := io.Copy(stdout, indented); err != nil { + return err + } + return nil + } + 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") @@ -64,32 +232,33 @@ func (docker *Docker) CmdList(stdin io.ReadCloser, stdout io.Writer, args ...str nameFilter = flags.Arg(0) } var names []string - for name := range docker.containersByName { + 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\tSIZE\tCHANGES\tRUNNING\tCOMMAND\n") + 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 *docker.containersByName[name] { + for idx, container := range *srv.containersByName[name] { if *limit > 0 && idx >= *limit { break } if !*quiet { for idx, field := range []string{ - /* NAME */ container.Name, + /* NAME */ container.GetUserData("name"), /* ID */ container.Id, /* CREATED */ future.HumanDuration(time.Now().Sub(container.Created)) + " ago", - /* SOURCE */ container.Source, - /* SIZE */ fmt.Sprintf("%.1fM", float32(container.Size) / 1024 / 1024), - /* CHANGES */ fmt.Sprintf("%.1fM", float32(container.BytesChanged) / 1024 / 1024), - /* RUNNING */ fmt.Sprintf("%v", container.Running), - /* COMMAND */ container.CmdString(), + /* 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)) @@ -109,33 +278,33 @@ func (docker *Docker) CmdList(stdin io.ReadCloser, stdout io.Writer, args ...str return nil } -func (docker *Docker) findContainer(name string) (*Container, bool) { +func (srv *Server) findContainer(name string) (*docker.Container, bool) { // 1: look for container by ID - if container, exists := docker.containers[name]; exists { + 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 := docker.containersByName[name]; exists { + if containers, exists := srv.containersByName[name]; exists { return (*containers)[0], true } return nil, false } -func (docker *Docker) CmdRm(stdin io.ReadCloser, stdout io.Writer, args ...string) error { +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 := docker.rm(name); err != nil { + if _, err := srv.rm(name); err != nil { fmt.Fprintln(stdout, "Error: " + err.Error()) } } return nil } -func (docker *Docker) CmdPull(stdin io.ReadCloser, stdout io.Writer, args ...string) error { +func (srv *Server) CmdPull(stdin io.ReadCloser, stdout io.Writer, args ...string) error { if len(args) < 1 { return errors.New("Not enough arguments") } @@ -143,31 +312,42 @@ func (docker *Docker) CmdPull(stdin io.ReadCloser, stdout io.Writer, args ...str if err != nil { return err } - layer, err := docker.layers.AddLayer(resp.Body, stdout) + layer, err := srv.layers.AddLayer(resp.Body, stdout) if err != nil { return err } - docker.addContainer(args[0], "download", 0) - fmt.Fprintln(stdout, layer.Id()) + container, err := srv.addContainer(layer.Id(), []string{layer.Path}, args[0], "download") + if err != nil { + return err + } + fmt.Fprintln(stdout, container.Id) return nil } -func (docker *Docker) CmdPut(stdin io.ReadCloser, stdout io.Writer, args ...string) error { +func (srv *Server) CmdPut(stdin io.ReadCloser, stdout io.Writer, args ...string) error { if len(args) < 1 { return errors.New("Not enough arguments") } fmt.Printf("Adding layer\n") - layer, err := docker.layers.AddLayer(stdin, stdout) + layer, err := srv.layers.AddLayer(stdin, stdout) if err != nil { return err } - docker.addContainer(args[0], "upload", 0) - fmt.Fprintln(stdout, layer.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 (docker *Docker) CmdCommit(stdin io.ReadCloser, stdout io.Writer, args ...string) error { +func (srv *Server) CmdCommit(stdin io.ReadCloser, stdout io.Writer, args ...string) error { flags := rcli.Subcmd(stdout, "fork", "[OPTIONS] CONTAINER [DEST]", "Duplicate a container") @@ -183,15 +363,18 @@ func (docker *Docker) CmdCommit(stdin io.ReadCloser, stdout io.Writer, args ...s if dstName == "" { dstName = srcName } - if src, exists := docker.findContainer(srcName); exists { - dst := docker.addContainer(dstName, "snapshot:" + src.Id, src.Size) - fmt.Fprintln(stdout, dst.Id) + /* + 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) return nil } + */ return errors.New("No such container: " + srcName) } -func (docker *Docker) CmdTar(stdin io.ReadCloser, stdout io.Writer, args ...string) error { +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") @@ -199,70 +382,67 @@ func (docker *Docker) CmdTar(stdin io.ReadCloser, stdout io.Writer, args ...stri return nil } name := flags.Arg(0) - if _, exists := docker.findContainer(name); exists { + if container, exists := srv.findContainer(name); exists { + data, err := container.Filesystem.Tar() + if err != nil { + return err + } // Stream the entire contents of the container (basically a volatile snapshot) - return fake.WriteFakeTar(stdout) + if _, err := io.Copy(stdout, data); err != nil { + return err + } + return nil } return errors.New("No such container: " + name) } -func (docker *Docker) CmdDiff(stdin io.ReadCloser, stdout io.Writer, args ...string) error { +func (srv *Server) CmdDiff(stdin io.ReadCloser, stdout io.Writer, args ...string) error { flags := rcli.Subcmd(stdout, "diff", "CONTAINER [OPTIONS]", "Inspect changes on a container's filesystem") - fl_diff := flags.Bool("d", true, "Show changes in diff format") - fl_bytes := flags.Bool("b", false, "Show how many bytes have been changed") - fl_list := flags.Bool("l", false, "Show a list of changed files") if err := flags.Parse(args); err != nil { return nil } if flags.NArg() < 1 { return errors.New("Not enough arguments") } - if container, exists := docker.findContainer(flags.Arg(0)); !exists { + if container, exists := srv.findContainer(flags.Arg(0)); !exists { return errors.New("No such container") - } else if *fl_bytes { - fmt.Fprintf(stdout, "%d\n", container.BytesChanged) - } else if *fl_list { - // FAKE - fmt.Fprintf(stdout, strings.Join([]string{ - "/etc/postgres/pg.conf", - "/etc/passwd", - "/var/lib/postgres", - "/usr/bin/postgres", - "/usr/bin/psql", - "/var/log/postgres", - "/var/log/postgres/postgres.log", - "/var/log/postgres/postgres.log.0", - "/var/log/postgres/postgres.log.1.gz"}, "\n")) - } else if *fl_diff { - // Achievement unlocked: embed a diff of your code as a string in your code - fmt.Fprintf(stdout, ` -diff --git a/dockerd/dockerd.go b/dockerd/dockerd.go -index 2dae694..e43caca 100644 ---- a/dockerd/dockerd.go -+++ b/dockerd/dockerd.go -@@ -158,6 +158,7 @@ func (docker *Docker) CmdDiff(stdin io.ReadCloser, stdout io.Writer, args ...str - flags := rcli.Subcmd(stdout, - "diff", "CONTAINER [OPTIONS]", - "Inspect changes on a container's filesystem") -+ fl_diff := flags.Bool("d", true, "Show changes in diff format") - fl_bytes := flags.Bool("b", false, "Show how many bytes have been changes") - fl_list := flags.Bool("l", false, "Show a list of changed files") - fl_download := flags.Bool("d", false, "Download the changes as gzipped tar stream") -`) - return nil } else { - flags.Usage() - return nil + changes, err := container.Filesystem.Changes() + if err != nil { + return err + } + for _, change := range changes { + fmt.Fprintln(stdout, change.String()) + } } return nil } +func (srv *Server) CmdReset(stdin io.ReadCloser, stdout io.Writer, args ...string) error { + flags := rcli.Subcmd(stdout, + "reset", "CONTAINER [OPTIONS]", + "Reset changes to a container's filesystem") + if err := flags.Parse(args); err != nil { + return nil + } + if flags.NArg() < 1 { + return errors.New("Not enough arguments") + } + for _, name := range flags.Args() { + if container, exists := srv.findContainer(name); exists { + if err := container.Filesystem.Reset(); err != nil { + return errors.New("Reset " + container.Id + ": " + err.Error()) + } + } + } + return nil +} // ByDate wraps an array of layers so they can be sorted by date (most recent first) -type ByDate []*Container +type ByDate []*docker.Container func (c *ByDate) Len() int { return len(*c) @@ -280,7 +460,7 @@ func (c *ByDate) Swap(i, j int) { containers[j] = tmp } -func (c *ByDate) Add(container *Container) { +func (c *ByDate) Add(container *docker.Container) { *c = append(*c, container) sort.Sort(c) } @@ -294,46 +474,38 @@ func (c *ByDate) Del(id string) { } -func (docker *Docker) addContainer(name string, source string, size uint) *Container { - if size == 0 { - size = fake.RandomContainerSize() +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 } - c := &Container{ - Id: future.RandomId(), - Name: name, - Created: time.Now(), - Source: source, - Size: size, - stdinLog: new(bytes.Buffer), - stdoutLog: new(bytes.Buffer), + if err := c.SetUserData("name", name); err != nil { + srv.docker.Destroy(c) + return nil, err } - docker.containers[c.Id] = c - if _, exists := docker.containersByName[c.Name]; !exists { - docker.containersByName[c.Name] = new(ByDate) + if _, exists := srv.containersByName[name]; !exists { + srv.containersByName[name] = new(ByDate) } - docker.containersByName[c.Name].Add(c) - return c - + srv.containersByName[name].Add(c) + return c, nil } -func (docker *Docker) rm(id string) (*Container, error) { - if container, exists := docker.containers[id]; exists { - if container.Running { - return nil, errors.New("Container is running: " + id) - } else { - // Remove from name lookup - docker.containersByName[container.Name].Del(container.Id) - // Remove from id lookup - delete(docker.containers, container.Id) - return container, 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) } - return nil, errors.New(fmt.Sprintf("No such container: %s", 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 (docker *Docker) CmdLogs(stdin io.ReadCloser, stdout io.Writer, args ...string) error { +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") if err := flags.Parse(args); err != nil { return nil @@ -343,19 +515,23 @@ func (docker *Docker) CmdLogs(stdin io.ReadCloser, stdout io.Writer, args ...str return nil } name := flags.Arg(0) - if container, exists := docker.findContainer(name); exists { + if container, exists := srv.findContainer(name); exists { if _, err := io.Copy(stdout, container.StdoutLog()); err != nil { return err } + if _, err := io.Copy(stdout, container.StderrLog()); err != nil { + return err + } return nil } return errors.New("No such container: " + flags.Arg(0)) } -func (docker *Docker) CmdRun(stdin io.ReadCloser, stdout io.Writer, args ...string) error { + +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") fl_attach := flags.Bool("a", false, "Attach stdin and stdout") - fl_tty := flags.Bool("t", false, "Allocate a pseudo-tty") + //fl_tty := flags.Bool("t", false, "Allocate a pseudo-tty") if err := flags.Parse(args); err != nil { return nil } @@ -364,62 +540,60 @@ func (docker *Docker) CmdRun(stdin io.ReadCloser, stdout io.Writer, args ...stri return nil } name, cmd := flags.Arg(0), flags.Args()[1:] - if container, exists := docker.findContainer(name); exists { - if container.Running { - return errors.New("Already running: " + name) - } + 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 { - return container.Run(cmd[0], cmd[1:], stdin, stdout, *fl_tty) + 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 { - go container.Run(cmd[0], cmd[1:], ioutil.NopCloser(new(bytes.Buffer)), ioutil.Discard, *fl_tty) - fmt.Fprintln(stdout, container.Id) - return nil + if output, err := container.Output(); err != nil { + return err + } else { + fmt.Printf("-->|%s|\n", output) + } } + return nil } return errors.New("No such container: " + name) } -func startCommand(cmd *exec.Cmd, interactive bool) (io.WriteCloser, io.ReadCloser, error) { - if interactive { - term, err := pty.Start(cmd) - if err != nil { - return nil, nil, err - } - return term, term, nil - } - stdin, err := cmd.StdinPipe() - if err != nil { - return nil, nil, err - } - stdout, err := cmd.StdoutPipe() - if err != nil { - return nil, nil, err - } - if err := cmd.Start(); err != nil { - return nil, nil, err - } - return stdin, stdout, nil -} - - func main() { future.Seed() flag.Parse() - docker, err := New() + d, err := New() if err != nil { log.Fatal(err) } go func() { - if err := rcli.ListenAndServeHTTP(":8080", docker); err != nil { + if err := rcli.ListenAndServeHTTP(":8080", d); err != nil { log.Fatal(err) } }() - if err := rcli.ListenAndServeTCP(":4242", docker); err != nil { + if err := rcli.ListenAndServeTCP(":4242", d); err != nil { log.Fatal(err) } } -func New() (*Docker, error) { +func New() (*Server, error) { store, err := future.NewStore("/var/lib/docker/layers") if err != nil { return nil, err @@ -427,20 +601,32 @@ func New() (*Docker, error) { if err := store.Init(); err != nil { return nil, err } - return &Docker{ + d, err := docker.New() + if err != nil { + return nil, err + } + srv := &Server{ containersByName: make(map[string]*ByDate), - containers: make(map[string]*Container), layers: store, - }, nil + 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) + } + log.Printf("Done building index\n") + return srv, nil } - -func (docker *Docker) CmdMirror(stdin io.ReadCloser, stdout io.Writer, args ...string) error { +func (srv *Server) CmdMirror(stdin io.ReadCloser, stdout io.Writer, args ...string) error { _, err := io.Copy(stdout, stdin) return err } -func (docker *Docker) CmdDebug(stdin io.ReadCloser, stdout io.Writer, args ...string) error { +func (srv *Server) CmdDebug(stdin io.ReadCloser, stdout io.Writer, args ...string) error { for { if line, err := bufio.NewReader(stdin).ReadString('\n'); err == nil { fmt.Printf("--- %s", line) @@ -456,7 +642,7 @@ func (docker *Docker) CmdDebug(stdin io.ReadCloser, stdout io.Writer, args ...st return nil } -func (docker *Docker) CmdWeb(stdin io.ReadCloser, stdout io.Writer, args ...string) error { +func (srv *Server) CmdWeb(stdin io.ReadCloser, stdout io.Writer, args ...string) error { flags := rcli.Subcmd(stdout, "web", "[OPTIONS]", "A web UI for docker") showurl := flags.Bool("u", false, "Return the URL of the web UI") if err := flags.Parse(args); err != nil { @@ -475,85 +661,9 @@ func (docker *Docker) CmdWeb(stdin io.ReadCloser, stdout io.Writer, args ...stri } -type Docker struct { - containers map[string]*Container +type Server struct { containersByName map[string]*ByDate layers *future.Store -} - -type Container struct { - Id string - Name string - Created time.Time - Source string - Size uint - FilesChanged uint - BytesChanged uint - Running bool - Cmd string - Args []string - stdoutLog *bytes.Buffer - stdinLog *bytes.Buffer -} - -func (c *Container) Run(command string, args []string, stdin io.ReadCloser, stdout io.Writer, tty bool) error { - // Not thread-safe - if c.Running { - return errors.New("Already running") - } - c.Cmd = command - c.Args = args - // Reset logs - c.stdoutLog.Reset() - c.stdinLog.Reset() - cmd := exec.Command(c.Cmd, c.Args...) - cmd_stdin, cmd_stdout, err := startCommand(cmd, tty) - if err != nil { - return err - } - c.Running = true - // ADD FAKE RANDOM CHANGES - c.FilesChanged = fake.RandomFilesChanged() - c.BytesChanged = fake.RandomBytesChanged() - copy_out := future.Go(func() error { - _, err := io.Copy(io.MultiWriter(stdout, c.stdoutLog), cmd_stdout) - return err - }) - future.Go(func() error { - _, err := io.Copy(io.MultiWriter(cmd_stdin, c.stdinLog), stdin) - cmd_stdin.Close() - stdin.Close() - return err - }) - wait := future.Go(func() error { - err := cmd.Wait() - c.Running = false - return err - }) - if err := <-copy_out; err != nil { - if c.Running { - return err - } - } - if err := <-wait; err != nil { - if status, ok := err.(*exec.ExitError); ok { - fmt.Fprintln(stdout, status) - return nil - } - return err - } - return nil -} - -func (c *Container) StdoutLog() io.Reader { - return strings.NewReader(c.stdoutLog.String()) -} - -func (c *Container) StdinLog() io.Reader { - return strings.NewReader(c.stdinLog.String()) -} - -func (c *Container) CmdString() string { - return strings.Join(append([]string{c.Cmd}, c.Args...), " ") + docker *docker.Docker } diff --git a/fake/fake.go b/fake/fake.go index 0e0de77507..990d84a0b5 100644 --- a/fake/fake.go +++ b/fake/fake.go @@ -5,6 +5,10 @@ import ( "math/rand" "io" "archive/tar" + "github.com/dotcloud/docker" + "os/exec" + "strings" + "github.com/kr/pty" ) @@ -47,3 +51,51 @@ func RandomFilesChanged() uint { func RandomContainerSize() uint { return uint(rand.Int31n(142 * 1024 * 1024)) } + +func ContainerRunning() bool { + return false +} + +type Container struct { + *docker.Container + Name string + Source string + Size uint + FilesChanged uint + BytesChanged uint +} + +func NewContainer(c *docker.Container) *Container { + return &Container{ + Container: c, + Name: c.GetUserData("name"), + } +} +func (c *Container) CmdString() string { + return strings.Join(append([]string{c.Path}, c.Args...), " ") +} + + +func startCommand(cmd *exec.Cmd, interactive bool) (io.WriteCloser, io.ReadCloser, error) { + if interactive { + term, err := pty.Start(cmd) + if err != nil { + return nil, nil, err + } + return term, term, nil + } + stdin, err := cmd.StdinPipe() + if err != nil { + return nil, nil, err + } + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, nil, err + } + if err := cmd.Start(); err != nil { + return nil, nil, err + } + return stdin, stdout, nil +} + + diff --git a/filesystem.go b/filesystem.go index 1e4679fc04..030625a150 100644 --- a/filesystem.go +++ b/filesystem.go @@ -8,6 +8,8 @@ import ( "strings" "syscall" "time" + "io" + "io/ioutil" ) type Filesystem struct { @@ -94,6 +96,24 @@ func (fs *Filesystem) IsMounted() bool { return mntpointSt.Dev != parentSt.Dev } +// Tar returns the contents of the filesystem as an uncompressed tar stream +func (fs *Filesystem) Tar() (io.Reader, error) { + if err := fs.EnsureMounted(); err != nil { + return nil, err + } + return Tar(fs.RootFS) +} + + +func (fs *Filesystem) EnsureMounted() error { + if !fs.IsMounted() { + if err := fs.Mount(); err != nil { + return err + } + } + return nil +} + type ChangeType int const ( @@ -107,6 +127,16 @@ type Change struct { 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 (fs *Filesystem) Changes() ([]Change, error) { var changes []Change err := filepath.Walk(fs.RWPath, func(path string, f os.FileInfo, err error) error { @@ -179,6 +209,35 @@ func (fs *Filesystem) Changes() ([]Change, error) { return changes, nil } +// Reset removes all changes to the filesystem, reverting it to its initial state. +func (fs *Filesystem) Reset() error { + if err := os.RemoveAll(fs.RWPath); err != nil { + return err + } + // We removed the RW directory itself along with its content: let's re-create an empty one. + if err := fs.createMountPoints(); err != nil { + return err + } + return nil +} + +// Open opens the named file for reading. +func (fs *Filesystem) OpenFile(path string, flag int, perm os.FileMode) (*os.File, error) { + if err := fs.EnsureMounted(); err != nil { + return nil, err + } + return os.OpenFile(filepath.Join(fs.RootFS, path), flag, perm) +} + +// ReadDir reads the directory named by dirname, relative to the Filesystem's root, +// and returns a list of sorted directory entries +func (fs *Filesystem) ReadDir(dirname string) ([]os.FileInfo, error) { + if err := fs.EnsureMounted(); err != nil { + return nil, err + } + return ioutil.ReadDir(filepath.Join(fs.RootFS, dirname)) +} + func newFilesystem(rootfs string, rwpath string, layers []string) *Filesystem { return &Filesystem{ RootFS: rootfs, diff --git a/utils.go b/utils.go index 63b88956c5..c1e6695f05 100644 --- a/utils.go +++ b/utils.go @@ -5,8 +5,38 @@ import ( "container/list" "io" "sync" + "os/exec" ) +// Tar generates a tar archive from a filesystem path, and returns it as a stream. +// Path must point to a directory. + +func Tar(path string) (io.Reader, error) { + cmd := exec.Command("tar", "-C", path, "-c", ".") + output, err := cmd.StdoutPipe() + if err != nil { + return nil, err + } + if err := cmd.Start(); err != nil { + return nil, err + } + // FIXME: errors will not be passed because we don't wait for the command. + // Instead, consumers will hit EOF right away. + // This can be fixed by waiting for the process to exit, or for the first write + // on stdout, whichever comes first. + return output, nil +} + +type nopWriteCloser struct { + io.Writer +} + +func (w *nopWriteCloser) Close() error { return nil } + +func NopWriteCloser(w io.Writer) io.WriteCloser { + return &nopWriteCloser{w} +} + type bufReader struct { buf *bytes.Buffer reader io.Reader