Browse Source

Merge branch 'master' into 1237-improve_docker_top-feature

Conflicts:
	docs/sources/api/docker_remote_api.rst
Victor Vieux 12 years ago
parent
commit
c81662eae4

+ 20 - 1
buildfile.go

@@ -7,6 +7,7 @@ import (
 	"github.com/dotcloud/docker/utils"
 	"io"
 	"io/ioutil"
+	"net/url"
 	"os"
 	"path"
 	"reflect"
@@ -201,6 +202,24 @@ func (b *buildFile) addRemote(container *Container, orig, dest string) error {
 	}
 	defer file.Body.Close()
 
+	// If the destination is a directory, figure out the filename.
+	if strings.HasSuffix(dest, "/") {
+		u, err := url.Parse(orig)
+		if err != nil {
+			return err
+		}
+		path := u.Path
+		if strings.HasSuffix(path, "/") {
+			path = path[:len(path)-1]
+		}
+		parts := strings.Split(path, "/")
+		filename := parts[len(parts)-1]
+		if filename == "" {
+			return fmt.Errorf("cannot determine filename from url: %s", u)
+		}
+		dest = dest + filename
+	}
+
 	return container.Inject(file.Body, dest)
 }
 
@@ -208,7 +227,7 @@ func (b *buildFile) addContext(container *Container, orig, dest string) error {
 	origPath := path.Join(b.context, orig)
 	destPath := path.Join(container.RootfsPath(), dest)
 	// Preserve the trailing '/'
-	if dest[len(dest)-1] == '/' {
+	if strings.HasSuffix(dest, "/") {
 		destPath = destPath + "/"
 	}
 	fi, err := os.Stat(origPath)

+ 96 - 23
buildfile_test.go

@@ -3,13 +3,17 @@ package docker
 import (
 	"fmt"
 	"io/ioutil"
+	"net"
+	"net/http"
+	"net/http/httptest"
+	"strings"
 	"testing"
 )
 
 // mkTestContext generates a build context from the contents of the provided dockerfile.
 // This context is suitable for use as an argument to BuildFile.Build()
 func mkTestContext(dockerfile string, files [][2]string, t *testing.T) Archive {
-	context, err := mkBuildContext(fmt.Sprintf(dockerfile, unitTestImageID), files)
+	context, err := mkBuildContext(dockerfile, files)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -22,6 +26,8 @@ type testContextTemplate struct {
 	dockerfile string
 	// Additional files in the context, eg [][2]string{"./passwd", "gordon"}
 	files [][2]string
+	// Additional remote files to host on a local HTTP server.
+	remoteFiles [][2]string
 }
 
 // A table of all the contexts to build and test.
@@ -29,27 +35,31 @@ type testContextTemplate struct {
 var testContexts = []testContextTemplate{
 	{
 		`
-from   %s
+from   {IMAGE}
 run    sh -c 'echo root:testpass > /tmp/passwd'
 run    mkdir -p /var/run/sshd
 run    [ "$(cat /tmp/passwd)" = "root:testpass" ]
 run    [ "$(ls -d /var/run/sshd)" = "/var/run/sshd" ]
 `,
 		nil,
+		nil,
 	},
 
 	{
 		`
-from %s
+from {IMAGE}
 add foo /usr/lib/bla/bar
-run [ "$(cat /usr/lib/bla/bar)" = 'hello world!' ]
+run [ "$(cat /usr/lib/bla/bar)" = 'hello' ]
+add http://{SERVERADDR}/baz /usr/lib/baz/quux
+run [ "$(cat /usr/lib/baz/quux)" = 'world!' ]
 `,
-		[][2]string{{"foo", "hello world!"}},
+		[][2]string{{"foo", "hello"}},
+		[][2]string{{"/baz", "world!"}},
 	},
 
 	{
 		`
-from %s
+from {IMAGE}
 add f /
 run [ "$(cat /f)" = "hello" ]
 add f /abc
@@ -71,38 +81,86 @@ run [ "$(cat /somewheeeere/over/the/rainbooow/ga)" = "bu" ]
 			{"f", "hello"},
 			{"d/ga", "bu"},
 		},
+		nil,
+	},
+
+	{
+		`
+from {IMAGE}
+add http://{SERVERADDR}/x /a/b/c
+run [ "$(cat /a/b/c)" = "hello" ]
+add http://{SERVERADDR}/x?foo=bar /
+run [ "$(cat /x)" = "hello" ]
+add http://{SERVERADDR}/x /d/
+run [ "$(cat /d/x)" = "hello" ]
+add http://{SERVERADDR} /e
+run [ "$(cat /e)" = "blah" ]
+`,
+		nil,
+		[][2]string{{"/x", "hello"}, {"/", "blah"}},
 	},
 
 	{
 		`
-from %s
+from   {IMAGE}
 env    FOO BAR
 run    [ "$FOO" = "BAR" ]
 `,
 		nil,
+		nil,
 	},
 
 	{
 		`
-from %s
+from {IMAGE}
 ENTRYPOINT /bin/echo
 CMD Hello world
 `,
 		nil,
+		nil,
 	},
 
 	{
 		`
-from %s
+from {IMAGE}
 VOLUME /test
 CMD Hello world
 `,
 		nil,
+		nil,
 	},
 }
 
 // FIXME: test building with 2 successive overlapping ADD commands
 
+func constructDockerfile(template string, ip net.IP, port string) string {
+	serverAddr := fmt.Sprintf("%s:%s", ip, port)
+	replacer := strings.NewReplacer("{IMAGE}", unitTestImageID, "{SERVERADDR}", serverAddr)
+	return replacer.Replace(template)
+}
+
+func mkTestingFileServer(files [][2]string) (*httptest.Server, error) {
+	mux := http.NewServeMux()
+	for _, file := range files {
+		name, contents := file[0], file[1]
+		mux.HandleFunc(name, func(w http.ResponseWriter, r *http.Request) {
+			w.Write([]byte(contents))
+		})
+	}
+
+	// This is how httptest.NewServer sets up a net.Listener, except that our listener must accept remote
+	// connections (from the container).
+	listener, err := net.Listen("tcp", ":0")
+	if err != nil {
+		return nil, err
+	}
+
+	s := httptest.NewUnstartedServer(mux)
+	s.Listener = listener
+	s.Start()
+	return s, nil
+}
+
 func TestBuild(t *testing.T) {
 	for _, ctx := range testContexts {
 		buildImage(ctx, t)
@@ -121,9 +179,24 @@ func buildImage(context testContextTemplate, t *testing.T) *Image {
 		pullingPool: make(map[string]struct{}),
 		pushingPool: make(map[string]struct{}),
 	}
-	buildfile := NewBuildFile(srv, ioutil.Discard, false)
 
-	id, err := buildfile.Build(mkTestContext(context.dockerfile, context.files, t))
+	httpServer, err := mkTestingFileServer(context.remoteFiles)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer httpServer.Close()
+
+	idx := strings.LastIndex(httpServer.URL, ":")
+	if idx < 0 {
+		t.Fatalf("could not get port from test http server address %s", httpServer.URL)
+	}
+	port := httpServer.URL[idx+1:]
+
+	ip := runtime.networkManager.bridgeNetwork.IP
+	dockerfile := constructDockerfile(context.dockerfile, ip, port)
+
+	buildfile := NewBuildFile(srv, ioutil.Discard, false)
+	id, err := buildfile.Build(mkTestContext(dockerfile, context.files, t))
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -137,10 +210,10 @@ func buildImage(context testContextTemplate, t *testing.T) *Image {
 
 func TestVolume(t *testing.T) {
 	img := buildImage(testContextTemplate{`
-        from %s
+        from {IMAGE}
         volume /test
         cmd Hello world
-    `, nil}, t)
+    `, nil, nil}, t)
 
 	if len(img.Config.Volumes) == 0 {
 		t.Fail()
@@ -154,9 +227,9 @@ func TestVolume(t *testing.T) {
 
 func TestBuildMaintainer(t *testing.T) {
 	img := buildImage(testContextTemplate{`
-        from %s
+        from {IMAGE}
         maintainer dockerio
-    `, nil}, t)
+    `, nil, nil}, t)
 
 	if img.Author != "dockerio" {
 		t.Fail()
@@ -165,10 +238,10 @@ func TestBuildMaintainer(t *testing.T) {
 
 func TestBuildEnv(t *testing.T) {
 	img := buildImage(testContextTemplate{`
-        from %s
+        from {IMAGE}
         env port 4243
         `,
-		nil}, t)
+		nil, nil}, t)
 
 	if img.Config.Env[0] != "port=4243" {
 		t.Fail()
@@ -177,10 +250,10 @@ func TestBuildEnv(t *testing.T) {
 
 func TestBuildCmd(t *testing.T) {
 	img := buildImage(testContextTemplate{`
-        from %s
+        from {IMAGE}
         cmd ["/bin/echo", "Hello World"]
         `,
-		nil}, t)
+		nil, nil}, t)
 
 	if img.Config.Cmd[0] != "/bin/echo" {
 		t.Log(img.Config.Cmd[0])
@@ -194,10 +267,10 @@ func TestBuildCmd(t *testing.T) {
 
 func TestBuildExpose(t *testing.T) {
 	img := buildImage(testContextTemplate{`
-        from %s
+        from {IMAGE}
         expose 4243
         `,
-		nil}, t)
+		nil, nil}, t)
 
 	if img.Config.PortSpecs[0] != "4243" {
 		t.Fail()
@@ -206,10 +279,10 @@ func TestBuildExpose(t *testing.T) {
 
 func TestBuildEntrypoint(t *testing.T) {
 	img := buildImage(testContextTemplate{`
-        from %s
+        from {IMAGE}
         entrypoint ["/bin/echo"]
         `,
-		nil}, t)
+		nil, nil}, t)
 
 	if img.Config.Entrypoint[0] != "/bin/echo" {
 	}

+ 19 - 5
commands.go

@@ -475,7 +475,7 @@ func (cli *DockerCli) CmdInfo(args ...string) error {
 
 func (cli *DockerCli) CmdStop(args ...string) error {
 	cmd := Subcmd("stop", "[OPTIONS] CONTAINER [CONTAINER...]", "Stop a running container")
-	nSeconds := cmd.Int("t", 10, "Number of seconds to try to stop for before killing the container. Default=10")
+	nSeconds := cmd.Int("t", 10, "Number of seconds to wait for the container to stop before killing it.")
 	if err := cmd.Parse(args); err != nil {
 		return nil
 	}
@@ -1110,10 +1110,7 @@ func (cli *DockerCli) CmdLogs(args ...string) error {
 		return nil
 	}
 
-	if err := cli.hijack("POST", "/containers/"+cmd.Arg(0)+"/attach?logs=1&stdout=1", false, nil, cli.out); err != nil {
-		return err
-	}
-	if err := cli.hijack("POST", "/containers/"+cmd.Arg(0)+"/attach?logs=1&stderr=1", false, nil, cli.err); err != nil {
+	if err := cli.hijack("POST", "/containers/"+cmd.Arg(0)+"/attach?logs=1&stdout=1&stderr=1", false, nil, cli.out); err != nil {
 		return err
 	}
 	return nil
@@ -1316,6 +1313,18 @@ func (cli *DockerCli) CmdRun(args ...string) error {
 		return nil
 	}
 
+	var containerIDFile *os.File
+	if len(hostConfig.ContainerIDFile) > 0 {
+		if _, err := ioutil.ReadFile(hostConfig.ContainerIDFile); err == nil {
+			return fmt.Errorf("cid file found, make sure the other container isn't running or delete %s", hostConfig.ContainerIDFile)
+		}
+		containerIDFile, err = os.Create(hostConfig.ContainerIDFile)
+		if err != nil {
+			return fmt.Errorf("failed to create the container ID file: %s", err)
+		}
+		defer containerIDFile.Close()
+	}
+
 	//create the container
 	body, statusCode, err := cli.call("POST", "/containers/create", config)
 	//if image not found try to pull it
@@ -1346,6 +1355,11 @@ func (cli *DockerCli) CmdRun(args ...string) error {
 	for _, warning := range runResult.Warnings {
 		fmt.Fprintf(cli.err, "WARNING: %s\n", warning)
 	}
+	if len(hostConfig.ContainerIDFile) > 0 {
+		if _, err = containerIDFile.WriteString(runResult.ID); err != nil {
+			return fmt.Errorf("failed to write the container ID to the file: %s", err)
+		}
+	}
 
 	//start the container
 	if _, _, err = cli.call("POST", "/containers/"+runResult.ID+"/start", hostConfig); err != nil {

+ 6 - 6
commands_test.go

@@ -59,7 +59,6 @@ func assertPipe(input, output string, r io.Reader, w io.Writer, count int) error
 	return nil
 }
 
-
 // TestRunHostname checks that 'docker run -h' correctly sets a custom hostname
 func TestRunHostname(t *testing.T) {
 	stdout, stdoutPipe := io.Pipe()
@@ -91,7 +90,6 @@ func TestRunHostname(t *testing.T) {
 
 }
 
-
 // TestAttachStdin checks attaching to stdin without stdout and stderr.
 // 'docker run -i -a stdin' should sends the client's stdin to the command,
 // then detach from it and print the container id.
@@ -144,15 +142,17 @@ func TestRunAttachStdin(t *testing.T) {
 	})
 
 	// Check logs
-	if cmdLogs, err := container.ReadLog("stdout"); err != nil {
+	if cmdLogs, err := container.ReadLog("json"); err != nil {
 		t.Fatal(err)
 	} else {
 		if output, err := ioutil.ReadAll(cmdLogs); err != nil {
 			t.Fatal(err)
 		} else {
-			expectedLog := "hello\nhi there\n"
-			if string(output) != expectedLog {
-				t.Fatalf("Unexpected logs: should be '%s', not '%s'\n", expectedLog, output)
+			expectedLogs := []string{"{\"log\":\"hello\\n\",\"stream\":\"stdout\"", "{\"log\":\"hi there\\n\",\"stream\":\"stdout\""}
+			for _, expectedLog := range expectedLogs {
+				if !strings.Contains(string(output), expectedLog) {
+					t.Fatalf("Unexpected logs: should contains '%s', it is not '%s'\n", expectedLog, output)
+				}
 			}
 		}
 	}

+ 11 - 6
container.go

@@ -80,7 +80,8 @@ type Config struct {
 }
 
 type HostConfig struct {
-	Binds []string
+	Binds           []string
+	ContainerIDFile string
 }
 
 type BindMap struct {
@@ -93,6 +94,7 @@ func ParseRun(args []string, capabilities *Capabilities) (*Config, *HostConfig,
 	cmd := Subcmd("run", "[OPTIONS] IMAGE [COMMAND] [ARG...]", "Run a command in a new container")
 	if len(args) > 0 && args[0] != "--help" {
 		cmd.SetOutput(ioutil.Discard)
+		cmd.Usage = nil
 	}
 
 	flHostname := cmd.String("h", "", "Container host name")
@@ -103,6 +105,7 @@ func ParseRun(args []string, capabilities *Capabilities) (*Config, *HostConfig,
 	flStdin := cmd.Bool("i", false, "Keep stdin open even if not attached")
 	flTty := cmd.Bool("t", false, "Allocate a pseudo-tty")
 	flMemory := cmd.Int64("m", 0, "Memory limit (in bytes)")
+	flContainerIDFile := cmd.String("cidfile", "", "Write the container ID to the file")
 
 	if capabilities != nil && *flMemory > 0 && !capabilities.MemoryLimit {
 		//fmt.Fprintf(stdout, "WARNING: Your kernel does not support memory limit capabilities. Limitation discarded.\n")
@@ -190,7 +193,8 @@ func ParseRun(args []string, capabilities *Capabilities) (*Config, *HostConfig,
 		Entrypoint:   entrypoint,
 	}
 	hostConfig := &HostConfig{
-		Binds: binds,
+		Binds:           binds,
+		ContainerIDFile: *flContainerIDFile,
 	}
 
 	if capabilities != nil && *flMemory > 0 && !capabilities.SwapLimit {
@@ -637,6 +641,7 @@ func (container *Container) Start(hostConfig *HostConfig) error {
 	params = append(params,
 		"-e", "HOME=/",
 		"-e", "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
+		"-e", "container=lxc",
 	)
 
 	for _, elem := range container.Config.Env {
@@ -650,10 +655,10 @@ func (container *Container) Start(hostConfig *HostConfig) error {
 	container.cmd = exec.Command("lxc-start", params...)
 
 	// Setup logging of stdout and stderr to disk
-	if err := container.runtime.LogToDisk(container.stdout, container.logPath("stdout")); err != nil {
+	if err := container.runtime.LogToDisk(container.stdout, container.logPath("json"), "stdout"); err != nil {
 		return err
 	}
-	if err := container.runtime.LogToDisk(container.stderr, container.logPath("stderr")); err != nil {
+	if err := container.runtime.LogToDisk(container.stderr, container.logPath("json"), "stderr"); err != nil {
 		return err
 	}
 
@@ -712,13 +717,13 @@ func (container *Container) StdinPipe() (io.WriteCloser, error) {
 
 func (container *Container) StdoutPipe() (io.ReadCloser, error) {
 	reader, writer := io.Pipe()
-	container.stdout.AddWriter(writer)
+	container.stdout.AddWriter(writer, "")
 	return utils.NewBufReader(reader), nil
 }
 
 func (container *Container) StderrPipe() (io.ReadCloser, error) {
 	reader, writer := io.Pipe()
-	container.stderr.AddWriter(writer)
+	container.stderr.AddWriter(writer, "")
 	return utils.NewBufReader(reader), nil
 }
 

+ 13 - 104
container_test.go

@@ -39,16 +39,11 @@ func TestIDFormat(t *testing.T) {
 func TestMultipleAttachRestart(t *testing.T) {
 	runtime := mkRuntime(t)
 	defer nuke(runtime)
-	container, err := NewBuilder(runtime).Create(
-		&Config{
-			Image: GetTestImage(runtime).ID,
-			Cmd: []string{"/bin/sh", "-c",
-				"i=1; while [ $i -le 5 ]; do i=`expr $i + 1`;  echo hello; done"},
-		},
+	container, hostConfig, _ := mkContainer(
+		runtime,
+		[]string{"_", "/bin/sh", "-c", "i=1; while [ $i -le 5 ]; do i=`expr $i + 1`;  echo hello; done"},
+		t,
 	)
-	if err != nil {
-		t.Fatal(err)
-	}
 	defer runtime.Destroy(container)
 
 	// Simulate 3 client attaching to the container and stop/restart
@@ -65,7 +60,6 @@ func TestMultipleAttachRestart(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	hostConfig := &HostConfig{}
 	if err := container.Start(hostConfig); err != nil {
 		t.Fatal(err)
 	}
@@ -140,19 +134,8 @@ func TestMultipleAttachRestart(t *testing.T) {
 func TestDiff(t *testing.T) {
 	runtime := mkRuntime(t)
 	defer nuke(runtime)
-
-	builder := NewBuilder(runtime)
-
 	// Create a container and remove a file
-	container1, err := builder.Create(
-		&Config{
-			Image: GetTestImage(runtime).ID,
-			Cmd:   []string{"/bin/rm", "/etc/passwd"},
-		},
-	)
-	if err != nil {
-		t.Fatal(err)
-	}
+	container1, _, _ := mkContainer(runtime, []string{"_", "/bin/rm", "/etc/passwd"}, t)
 	defer runtime.Destroy(container1)
 
 	if err := container1.Run(); err != nil {
@@ -185,15 +168,7 @@ func TestDiff(t *testing.T) {
 	}
 
 	// Create a new container from the commited image
-	container2, err := builder.Create(
-		&Config{
-			Image: img.ID,
-			Cmd:   []string{"cat", "/etc/passwd"},
-		},
-	)
-	if err != nil {
-		t.Fatal(err)
-	}
+	container2, _, _ := mkContainer(runtime, []string{img.ID, "cat", "/etc/passwd"}, t)
 	defer runtime.Destroy(container2)
 
 	if err := container2.Run(); err != nil {
@@ -212,15 +187,7 @@ func TestDiff(t *testing.T) {
 	}
 
 	// Create a new containere
-	container3, err := builder.Create(
-		&Config{
-			Image: GetTestImage(runtime).ID,
-			Cmd:   []string{"rm", "/bin/httpd"},
-		},
-	)
-	if err != nil {
-		t.Fatal(err)
-	}
+	container3, _, _ := mkContainer(runtime, []string{"_", "rm", "/bin/httpd"}, t)
 	defer runtime.Destroy(container3)
 
 	if err := container3.Run(); err != nil {
@@ -246,17 +213,7 @@ func TestDiff(t *testing.T) {
 func TestCommitAutoRun(t *testing.T) {
 	runtime := mkRuntime(t)
 	defer nuke(runtime)
-
-	builder := NewBuilder(runtime)
-	container1, err := builder.Create(
-		&Config{
-			Image: GetTestImage(runtime).ID,
-			Cmd:   []string{"/bin/sh", "-c", "echo hello > /world"},
-		},
-	)
-	if err != nil {
-		t.Fatal(err)
-	}
+	container1, _, _ := mkContainer(runtime, []string{"_", "/bin/sh", "-c", "echo hello > /world"}, t)
 	defer runtime.Destroy(container1)
 
 	if container1.State.Running {
@@ -279,14 +236,7 @@ func TestCommitAutoRun(t *testing.T) {
 	}
 
 	// FIXME: Make a TestCommit that stops here and check docker.root/layers/img.id/world
-	container2, err := builder.Create(
-		&Config{
-			Image: img.ID,
-		},
-	)
-	if err != nil {
-		t.Fatal(err)
-	}
+	container2, hostConfig, _ := mkContainer(runtime, []string{img.ID}, t)
 	defer runtime.Destroy(container2)
 	stdout, err := container2.StdoutPipe()
 	if err != nil {
@@ -296,7 +246,6 @@ func TestCommitAutoRun(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	hostConfig := &HostConfig{}
 	if err := container2.Start(hostConfig); err != nil {
 		t.Fatal(err)
 	}
@@ -324,17 +273,7 @@ func TestCommitRun(t *testing.T) {
 	runtime := mkRuntime(t)
 	defer nuke(runtime)
 
-	builder := NewBuilder(runtime)
-
-	container1, err := builder.Create(
-		&Config{
-			Image: GetTestImage(runtime).ID,
-			Cmd:   []string{"/bin/sh", "-c", "echo hello > /world"},
-		},
-	)
-	if err != nil {
-		t.Fatal(err)
-	}
+	container1, hostConfig, _ := mkContainer(runtime, []string{"_", "/bin/sh", "-c", "echo hello > /world"}, t)
 	defer runtime.Destroy(container1)
 
 	if container1.State.Running {
@@ -357,16 +296,7 @@ func TestCommitRun(t *testing.T) {
 	}
 
 	// FIXME: Make a TestCommit that stops here and check docker.root/layers/img.id/world
-
-	container2, err := builder.Create(
-		&Config{
-			Image: img.ID,
-			Cmd:   []string{"cat", "/world"},
-		},
-	)
-	if err != nil {
-		t.Fatal(err)
-	}
+	container2, hostConfig, _ := mkContainer(runtime, []string{img.ID, "cat", "/world"}, t)
 	defer runtime.Destroy(container2)
 	stdout, err := container2.StdoutPipe()
 	if err != nil {
@@ -376,7 +306,6 @@ func TestCommitRun(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	hostConfig := &HostConfig{}
 	if err := container2.Start(hostConfig); err != nil {
 		t.Fatal(err)
 	}
@@ -403,18 +332,7 @@ func TestCommitRun(t *testing.T) {
 func TestStart(t *testing.T) {
 	runtime := mkRuntime(t)
 	defer nuke(runtime)
-	container, err := NewBuilder(runtime).Create(
-		&Config{
-			Image:     GetTestImage(runtime).ID,
-			Memory:    33554432,
-			CpuShares: 1000,
-			Cmd:       []string{"/bin/cat"},
-			OpenStdin: true,
-		},
-	)
-	if err != nil {
-		t.Fatal(err)
-	}
+	container, hostConfig, _ := mkContainer(runtime, []string{"-m", "33554432", "-c", "1000", "-i", "_", "/bin/cat"}, t)
 	defer runtime.Destroy(container)
 
 	cStdin, err := container.StdinPipe()
@@ -422,7 +340,6 @@ func TestStart(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	hostConfig := &HostConfig{}
 	if err := container.Start(hostConfig); err != nil {
 		t.Fatal(err)
 	}
@@ -445,15 +362,7 @@ func TestStart(t *testing.T) {
 func TestRun(t *testing.T) {
 	runtime := mkRuntime(t)
 	defer nuke(runtime)
-	container, err := NewBuilder(runtime).Create(
-		&Config{
-			Image: GetTestImage(runtime).ID,
-			Cmd:   []string{"ls", "-al"},
-		},
-	)
-	if err != nil {
-		t.Fatal(err)
-	}
+	container, _, _ := mkContainer(runtime, []string{"_", "ls", "-al"}, t)
 	defer runtime.Destroy(container)
 
 	if container.State.Running {

+ 45 - 20
docs/sources/api/docker_remote_api.rst

@@ -2,6 +2,9 @@
 :description: API Documentation for Docker
 :keywords: API, Docker, rcli, REST, documentation
 
+.. COMMENT use http://pythonhosted.org/sphinxcontrib-httpdomain/ to
+.. document the REST API.
+
 =================
 Docker Remote API
 =================
@@ -13,15 +16,23 @@ Docker Remote API
 
 - The Remote API is replacing rcli
 - Default port in the docker deamon is 4243 
-- The API tends to be REST, but for some complex commands, like attach or pull, the HTTP connection is hijacked to transport stdout stdin and stderr
-- Since API version 1.2, the auth configuration is now handled client side, so the client has to send the authConfig as POST in /images/(name)/push
+- The API tends to be REST, but for some complex commands, like attach
+  or pull, the HTTP connection is hijacked to transport stdout stdin
+  and stderr
+- Since API version 1.2, the auth configuration is now handled client
+  side, so the client has to send the authConfig as POST in
+  /images/(name)/push
 
 2. Versions
 ===========
 
-The current verson of the API is 1.4
-Calling /images/<name>/insert is the same as calling /v1.4/images/<name>/insert
-You can still call an old version of the api using /v1.0/images/<name>/insert
+The current verson of the API is 1.3
+
+Calling /images/<name>/insert is the same as calling
+/v1.3/images/<name>/insert 
+
+You can still call an old version of the api using
+/v1.0/images/<name>/insert
 
 :doc:`docker_remote_api_v1.3`
 *****************************
@@ -29,9 +40,9 @@ You can still call an old version of the api using /v1.0/images/<name>/insert
 What's new
 ----------
 
-Listing processes (/top):
+.. http:get:: /containers/(id)/top
 
-- You can now use ps args with docker top, like `docker top <container_id> aux`
+   **New!** You can now use ps args with docker top, like `docker top <container_id> aux`
 
 :doc:`docker_remote_api_v1.3`
 *****************************
@@ -41,19 +52,21 @@ docker v0.5.0 51f6c4a_
 What's new
 ----------
 
-Listing processes (/top):
-
-- List the processes inside a container
+.. http:get:: /containers/(id)/top
 
+   List the processes running inside a container.
 
 Builder (/build):
 
 - Simplify the upload of the build context
-- Simply stream a tarball instead of multipart upload with 4 intermediary buffers
+- Simply stream a tarball instead of multipart upload with 4
+  intermediary buffers
 - Simpler, less memory usage, less disk usage and faster
 
-.. Note::
-The /build improvements are not reverse-compatible. Pre 1.3 clients will break on /build.
+.. Warning::
+
+  The /build improvements are not reverse-compatible. Pre 1.3 clients
+  will break on /build.
 
 List containers (/containers/json):
 
@@ -61,7 +74,8 @@ List containers (/containers/json):
 
 Start containers (/containers/<id>/start):
 
-- You can now pass host-specific configuration (e.g. bind mounts) in the POST body for start calls 
+- You can now pass host-specific configuration (e.g. bind mounts) in
+  the POST body for start calls
 
 :doc:`docker_remote_api_v1.2`
 *****************************
@@ -72,14 +86,25 @@ What's new
 ----------
 
 The auth configuration is now handled by the client.
-The client should send it's authConfig as POST on each call of /images/(name)/push
 
-.. http:get:: /auth is now deprecated
-.. http:post:: /auth only checks the configuration but doesn't store it on the server
+The client should send it's authConfig as POST on each call of
+/images/(name)/push
+
+.. http:get:: /auth 
+
+  **Deprecated.**
+
+.. http:post:: /auth 
+
+  Only checks the configuration but doesn't store it on the server
+
+  Deleting an image is now improved, will only untag the image if it
+  has chidren and remove all the untagged parents if has any.
 
-Deleting an image is now improved, will only untag the image if it has chidrens and remove all the untagged parents if has any.
+.. http:post:: /images/<name>/delete 
 
-.. http:post:: /images/<name>/delete now returns a JSON with the list of images deleted/untagged
+  Now returns a JSON structure with the list of images
+  deleted/untagged.
 
 
 :doc:`docker_remote_api_v1.1`
@@ -94,7 +119,7 @@ What's new
 .. http:post:: /images/(name)/insert
 .. http:post:: /images/(name)/push
 
-Uses json stream instead of HTML hijack, it looks like this:
+   Uses json stream instead of HTML hijack, it looks like this:
 
         .. sourcecode:: http
 

+ 7 - 2
docs/sources/api/docker_remote_api_v1.0.rst

@@ -1,3 +1,8 @@
+.. use orphan to suppress "WARNING: document isn't included in any toctree"
+.. per http://sphinx-doc.org/markup/misc.html#file-wide-metadata
+
+:orphan:
+
 :title: Remote API v1.0
 :description: API Documentation for Docker
 :keywords: API, Docker, rcli, REST, documentation
@@ -300,8 +305,8 @@ Start a container
 	:statuscode 500: server error
 
 
-Stop a contaier
-***************
+Stop a container
+****************
 
 .. http:post:: /containers/(id)/stop
 

+ 4 - 0
docs/sources/api/docker_remote_api_v1.1.rst

@@ -1,3 +1,7 @@
+.. use orphan to suppress "WARNING: document isn't included in any toctree"
+.. per http://sphinx-doc.org/markup/misc.html#file-wide-metadata
+
+:orphan:
 
 :title: Remote API v1.1
 :description: API Documentation for Docker

+ 5 - 0
docs/sources/api/docker_remote_api_v1.2.rst

@@ -1,3 +1,8 @@
+.. use orphan to suppress "WARNING: document isn't included in any toctree"
+.. per http://sphinx-doc.org/markup/misc.html#file-wide-metadata
+
+:orphan:
+
 :title: Remote API v1.2
 :description: API Documentation for Docker
 :keywords: API, Docker, rcli, REST, documentation

+ 5 - 0
docs/sources/api/docker_remote_api_v1.3.rst

@@ -1,3 +1,8 @@
+.. use orphan to suppress "WARNING: document isn't included in any toctree"
+.. per http://sphinx-doc.org/markup/misc.html#file-wide-metadata
+
+:orphan:
+
 :title: Remote API v1.3
 :description: API Documentation for Docker
 :keywords: API, Docker, rcli, REST, documentation

+ 1 - 1
docs/sources/api/index_api.rst

@@ -452,7 +452,7 @@ User Register
          "username": "foobar"'}
 
     :jsonparameter email: valid email address, that needs to be confirmed
-    :jsonparameter username: min 4 character, max 30 characters, must match the regular expression [a-z0-9_].
+    :jsonparameter username: min 4 character, max 30 characters, must match the regular expression [a-z0-9\_].
     :jsonparameter password: min 5 characters
 
     **Example Response**:

+ 3 - 2
docs/sources/api/registry_index_spec.rst

@@ -367,7 +367,8 @@ POST /v1/users
     {"email": "sam@dotcloud.com", "password": "toto42", "username": "foobar"'}
 
 **Validation**:
-    - **username** : min 4 character, max 30 characters, must match the regular expression [a-z0-9_].
+    - **username**: min 4 character, max 30 characters, must match the regular
+      expression [a-z0-9\_].
     - **password**: min 5 characters
 
 **Valid**: return HTTP 200
@@ -566,4 +567,4 @@ Next request::
 ---------------------
 
 - 1.0 : May 6th 2013 : initial release 
-- 1.1 : June 1st 2013 : Added Delete Repository and way to handle new source namespace.
+- 1.1 : June 1st 2013 : Added Delete Repository and way to handle new source namespace.

+ 11 - 0
docs/sources/commandline/command/run.rst

@@ -14,6 +14,7 @@
 
       -a=map[]: Attach to stdin, stdout or stderr.
       -c=0: CPU shares (relative weight)
+      -cidfile="": Write the container ID to the file
       -d=false: Detached mode: leave the container running in the background
       -e=[]: Set environment variables
       -h="": Container host name
@@ -26,3 +27,13 @@
       -v=[]: Create a bind mount with: [host-dir]:[container-dir]:[rw|ro]. If "host-dir" is missing, then docker creates a new volume.
       -volumes-from="": Mount all volumes from the given container.
       -entrypoint="": Overwrite the default entrypoint set by the image.
+
+
+Examples
+--------
+
+.. code-block:: bash
+
+    docker run -cidfile /tmp/docker_test.cid ubuntu echo "test"
+
+| This will create a container and print "test" to the console. The cidfile flag makes docker attempt to create a new file and write the container ID to it. If the file exists already, docker will return an error. Docker will close this file when docker run exits.

+ 3 - 1
docs/sources/commandline/command/stop.rst

@@ -8,6 +8,8 @@
 
 ::
 
-    Usage: docker stop [OPTIONS] NAME
+    Usage: docker stop [OPTIONS] CONTAINER [CONTAINER...]
 
     Stop a running container
+
+      -t=10: Number of seconds to wait for the container to stop before killing it.

+ 2 - 1
docs/sources/commandline/index.rst

@@ -37,5 +37,6 @@ Contents:
   start   <command/start>
   stop    <command/stop>
   tag     <command/tag>
+  top     <command/top>
   version <command/version>
-  wait    <command/wait>
+  wait    <command/wait>

+ 2 - 0
docs/sources/contributing/devenvironment.rst

@@ -46,11 +46,13 @@ in a standard build environment.
 You can run an interactive session in the newly built container:
 
 ::
+
     docker run -i -t docker bash
 
 
 To extract the binaries from the container:
 
 ::
+
     docker run docker sh -c 'cat $(which docker)' > docker-build && chmod +x docker-build
 

+ 0 - 2
docs/sources/index.rst

@@ -2,8 +2,6 @@
 :description: An overview of the Docker Documentation
 :keywords: containers, lxc, concepts, explanation
 
-.. _introduction:
-
 Welcome
 =======
 

+ 24 - 16
docs/sources/use/builder.rst

@@ -1,6 +1,6 @@
-:title: Dockerfile Builder
-:description: Docker Builder specifes a simple DSL which allows you to automate the steps you would normally manually take to create an image.
-:keywords: builder, docker, Docker Builder, automation, image creation
+:title: Dockerfiles for Images
+:description: Dockerfiles use a simple DSL which allows you to automate the steps you would normally manually take to create an image.
+:keywords: builder, docker, Dockerfile, automation, image creation
 
 ==================
 Dockerfile Builder
@@ -30,7 +30,7 @@ build succeeds:
 
     ``docker build -t shykes/myapp .``
 
-Docker will run your steps one-by-one, committing the result if necessary, 
+Docker will run your steps one-by-one, committing the result if necessary,
 before finally outputting the ID of your new image.
 
 2. Format
@@ -43,7 +43,7 @@ The Dockerfile format is quite simple:
     # Comment
     INSTRUCTION arguments
 
-The Instruction is not case-sensitive, however convention is for them to be 
+The Instruction is not case-sensitive, however convention is for them to be
 UPPERCASE in order to distinguish them from arguments more easily.
 
 Docker evaluates the instructions in a Dockerfile in order. **The first
@@ -106,7 +106,7 @@ The ``CMD`` instruction sets the command to be executed when running
 the image.  This is functionally equivalent to running ``docker commit
 -run '{"Cmd": <command>}'`` outside the builder.
 
-.. note:: 
+.. note::
     Don't confuse `RUN` with `CMD`. `RUN` actually runs a
     command and commits the result; `CMD` does not execute anything at
     build time, but specifies the intended command for the image.
@@ -131,7 +131,7 @@ value ``<value>``. This value will be passed to all future ``RUN``
 instructions. This is functionally equivalent to prefixing the command
 with ``<key>=<value>``
 
-.. note:: 
+.. note::
     The environment variables will persist when a container is run
     from the resulting image.
 
@@ -152,16 +152,24 @@ destination container.
 
 The copy obeys the following rules:
 
+* If ``<src>`` is a URL and ``<dest>`` does not end with a trailing slash,
+  then a file is downloaded from the URL and copied to ``<dest>``.
+* If ``<src>`` is a URL and ``<dest>`` does end with a trailing slash,
+  then the filename is inferred from the URL and the file is downloaded to
+  ``<dest>/<filename>``. For instance, ``ADD http://example.com/foobar /``
+  would create the file ``/foobar``. The URL must have a nontrivial path
+  so that an appropriate filename can be discovered in this case
+  (``http://example.com`` will not work).
 * If ``<src>`` is a directory, the entire directory is copied,
   including filesystem metadata.
 * If ``<src>``` is a tar archive in a recognized compression format
   (identity, gzip, bzip2 or xz), it is unpacked as a directory.
 
   When a directory is copied or unpacked, it has the same behavior as
-  ``tar -x``: the result is the union of 
+  ``tar -x``: the result is the union of
 
   1. whatever existed at the destination path and
-  2. the contents of the source tree, 
+  2. the contents of the source tree,
 
   with conflicts resolved in favor of 2) on a file-by-file basis.
 
@@ -177,7 +185,7 @@ The copy obeys the following rules:
   with mode 0700, uid and gid 0.
 
 3.8 ENTRYPOINT
--------------
+--------------
 
     ``ENTRYPOINT /bin/echo``
 
@@ -203,14 +211,14 @@ container created from the image.
     # Nginx
     #
     # VERSION               0.0.1
-    
+
     FROM      ubuntu
     MAINTAINER Guillaume J. Charmes "guillaume@dotcloud.com"
-    
+
     # make sure the package repository is up to date
     RUN echo "deb http://archive.ubuntu.com/ubuntu precise main universe" > /etc/apt/sources.list
     RUN apt-get update
-    
+
     RUN apt-get install -y inotify-tools nginx apache2 openssh-server
 
 .. code-block:: bash
@@ -218,12 +226,12 @@ container created from the image.
     # Firefox over VNC
     #
     # VERSION               0.3
-    
+
     FROM ubuntu
     # make sure the package repository is up to date
     RUN echo "deb http://archive.ubuntu.com/ubuntu precise main universe" > /etc/apt/sources.list
     RUN apt-get update
-    
+
     # Install vnc, xvfb in order to create a 'fake' display and firefox
     RUN apt-get install -y x11vnc xvfb firefox
     RUN mkdir /.vnc
@@ -231,7 +239,7 @@ container created from the image.
     RUN x11vnc -storepasswd 1234 ~/.vnc/passwd
     # Autostart firefox (might not be the best way, but it does the trick)
     RUN bash -c 'echo "firefox" >> /.bashrc'
-    
+
     EXPOSE 5900
     CMD    ["x11vnc", "-forever", "-usepw", "-create"]
 

+ 1 - 1
docs/sources/use/workingwithrepository.rst

@@ -119,7 +119,7 @@ your container to an image within your username namespace.
 
 
 Pushing a container to its repository
-------------------------------------
+-------------------------------------
 
 In order to push an image to its repository you need to have committed
 your container to a named image (see above)

+ 2 - 2
runtime.go

@@ -167,12 +167,12 @@ func (runtime *Runtime) Register(container *Container) error {
 	return nil
 }
 
-func (runtime *Runtime) LogToDisk(src *utils.WriteBroadcaster, dst string) error {
+func (runtime *Runtime) LogToDisk(src *utils.WriteBroadcaster, dst, stream string) error {
 	log, err := os.OpenFile(dst, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0600)
 	if err != nil {
 		return err
 	}
-	src.AddWriter(log)
+	src.AddWriter(log, stream)
 	return nil
 }
 

+ 8 - 64
runtime_test.go

@@ -5,7 +5,6 @@ import (
 	"fmt"
 	"github.com/dotcloud/docker/utils"
 	"io"
-	"io/ioutil"
 	"log"
 	"net"
 	"os"
@@ -247,36 +246,13 @@ func TestGet(t *testing.T) {
 	runtime := mkRuntime(t)
 	defer nuke(runtime)
 
-	builder := NewBuilder(runtime)
-
-	container1, err := builder.Create(&Config{
-		Image: GetTestImage(runtime).ID,
-		Cmd:   []string{"ls", "-al"},
-	},
-	)
-	if err != nil {
-		t.Fatal(err)
-	}
+	container1, _, _ := mkContainer(runtime, []string{"_", "ls", "-al"}, t)
 	defer runtime.Destroy(container1)
 
-	container2, err := builder.Create(&Config{
-		Image: GetTestImage(runtime).ID,
-		Cmd:   []string{"ls", "-al"},
-	},
-	)
-	if err != nil {
-		t.Fatal(err)
-	}
+	container2, _, _ := mkContainer(runtime, []string{"_", "ls", "-al"}, t)
 	defer runtime.Destroy(container2)
 
-	container3, err := builder.Create(&Config{
-		Image: GetTestImage(runtime).ID,
-		Cmd:   []string{"ls", "-al"},
-	},
-	)
-	if err != nil {
-		t.Fatal(err)
-	}
+	container3, _, _ := mkContainer(runtime, []string{"_", "ls", "-al"}, t)
 	defer runtime.Destroy(container3)
 
 	if runtime.Get(container1.ID) != container1 {
@@ -431,46 +407,14 @@ func TestAllocateUDPPortLocalhost(t *testing.T) {
 }
 
 func TestRestore(t *testing.T) {
-
-	root, err := ioutil.TempDir("", "docker-test")
-	if err != nil {
-		t.Fatal(err)
-	}
-	if err := os.Remove(root); err != nil {
-		t.Fatal(err)
-	}
-	if err := utils.CopyDirectory(unitTestStoreBase, root); err != nil {
-		t.Fatal(err)
-	}
-
-	runtime1, err := NewRuntimeFromDirectory(root, false)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	builder := NewBuilder(runtime1)
-
+	runtime1 := mkRuntime(t)
+	defer nuke(runtime1)
 	// Create a container with one instance of docker
-	container1, err := builder.Create(&Config{
-		Image: GetTestImage(runtime1).ID,
-		Cmd:   []string{"ls", "-al"},
-	},
-	)
-	if err != nil {
-		t.Fatal(err)
-	}
+	container1, _, _ := mkContainer(runtime1, []string{"_", "ls", "-al"}, t)
 	defer runtime1.Destroy(container1)
 
 	// Create a second container meant to be killed
-	container2, err := builder.Create(&Config{
-		Image:     GetTestImage(runtime1).ID,
-		Cmd:       []string{"/bin/cat"},
-		OpenStdin: true,
-	},
-	)
-	if err != nil {
-		t.Fatal(err)
-	}
+	container2, _, _ := mkContainer(runtime1, []string{"-i", "_", "/bin/cat"}, t)
 	defer runtime1.Destroy(container2)
 
 	// Start the container non blocking
@@ -505,7 +449,7 @@ func TestRestore(t *testing.T) {
 
 	// Here are are simulating a docker restart - that is, reloading all containers
 	// from scratch
-	runtime2, err := NewRuntimeFromDirectory(root, false)
+	runtime2, err := NewRuntimeFromDirectory(runtime1.root, false)
 	if err != nil {
 		t.Fatal(err)
 	}

+ 35 - 13
server.go

@@ -2,6 +2,7 @@ package docker
 
 import (
 	"bufio"
+	"encoding/json"
 	"errors"
 	"fmt"
 	"github.com/dotcloud/docker/auth"
@@ -1054,20 +1055,41 @@ func (srv *Server) ContainerAttach(name string, logs, stream, stdin, stdout, std
 	}
 	//logs
 	if logs {
-		if stdout {
-			cLog, err := container.ReadLog("stdout")
-			if err != nil {
-				utils.Debugf("Error reading logs (stdout): %s", err)
-			} else if _, err := io.Copy(out, cLog); err != nil {
-				utils.Debugf("Error streaming logs (stdout): %s", err)
+		cLog, err := container.ReadLog("json")
+		if err != nil && os.IsNotExist(err) {
+			// Legacy logs
+			utils.Debugf("Old logs format")
+			if stdout {
+				cLog, err := container.ReadLog("stdout")
+				if err != nil {
+					utils.Debugf("Error reading logs (stdout): %s", err)
+				} else if _, err := io.Copy(out, cLog); err != nil {
+					utils.Debugf("Error streaming logs (stdout): %s", err)
+				}
 			}
-		}
-		if stderr {
-			cLog, err := container.ReadLog("stderr")
-			if err != nil {
-				utils.Debugf("Error reading logs (stderr): %s", err)
-			} else if _, err := io.Copy(out, cLog); err != nil {
-				utils.Debugf("Error streaming logs (stderr): %s", err)
+			if stderr {
+				cLog, err := container.ReadLog("stderr")
+				if err != nil {
+					utils.Debugf("Error reading logs (stderr): %s", err)
+				} else if _, err := io.Copy(out, cLog); err != nil {
+					utils.Debugf("Error streaming logs (stderr): %s", err)
+				}
+			}
+		} else if err != nil {
+			utils.Debugf("Error reading logs (json): %s", err)
+		} else {
+			dec := json.NewDecoder(cLog)
+			for {
+				var l utils.JSONLog
+				if err := dec.Decode(&l); err == io.EOF {
+					break
+				} else if err != nil {
+					utils.Debugf("Error streaming logs: %s", err)
+					break
+				}
+				if (l.Stream == "stdout" && stdout) || (l.Stream == "stderr" && stderr) {
+					fmt.Fprintf(out, "%s", l.Log)
+				}
 			}
 		}
 	}

+ 40 - 16
utils/utils.go

@@ -248,30 +248,54 @@ func (r *bufReader) Close() error {
 
 type WriteBroadcaster struct {
 	sync.Mutex
-	writers map[io.WriteCloser]struct{}
+	buf     *bytes.Buffer
+	writers map[StreamWriter]bool
 }
 
-func (w *WriteBroadcaster) AddWriter(writer io.WriteCloser) {
-	w.Lock()
-	w.writers[writer] = struct{}{}
-	w.Unlock()
+type StreamWriter struct {
+	wc     io.WriteCloser
+	stream string
 }
 
-// FIXME: Is that function used?
-// FIXME: This relies on the concrete writer type used having equality operator
-func (w *WriteBroadcaster) RemoveWriter(writer io.WriteCloser) {
+func (w *WriteBroadcaster) AddWriter(writer io.WriteCloser, stream string) {
 	w.Lock()
-	delete(w.writers, writer)
+	sw := StreamWriter{wc: writer, stream: stream}
+	w.writers[sw] = true
 	w.Unlock()
 }
 
+type JSONLog struct {
+	Log     string    `json:"log,omitempty"`
+	Stream  string    `json:"stream,omitempty"`
+	Created time.Time `json:"time"`
+}
+
 func (w *WriteBroadcaster) Write(p []byte) (n int, err error) {
 	w.Lock()
 	defer w.Unlock()
-	for writer := range w.writers {
-		if n, err := writer.Write(p); err != nil || n != len(p) {
+	w.buf.Write(p)
+	for sw := range w.writers {
+		lp := p
+		if sw.stream != "" {
+			lp = nil
+			for {
+				line, err := w.buf.ReadString('\n')
+				if err != nil {
+					w.buf.Write([]byte(line))
+					break
+				}
+				b, err := json.Marshal(&JSONLog{Log: line, Stream: sw.stream, Created: time.Now()})
+				if err != nil {
+					// On error, evict the writer
+					delete(w.writers, sw)
+					continue
+				}
+				lp = append(lp, b...)
+			}
+		}
+		if n, err := sw.wc.Write(lp); err != nil || n != len(lp) {
 			// On error, evict the writer
-			delete(w.writers, writer)
+			delete(w.writers, sw)
 		}
 	}
 	return len(p), nil
@@ -280,15 +304,15 @@ func (w *WriteBroadcaster) Write(p []byte) (n int, err error) {
 func (w *WriteBroadcaster) CloseWriters() error {
 	w.Lock()
 	defer w.Unlock()
-	for writer := range w.writers {
-		writer.Close()
+	for sw := range w.writers {
+		sw.wc.Close()
 	}
-	w.writers = make(map[io.WriteCloser]struct{})
+	w.writers = make(map[StreamWriter]bool)
 	return nil
 }
 
 func NewWriteBroadcaster() *WriteBroadcaster {
-	return &WriteBroadcaster{writers: make(map[io.WriteCloser]struct{})}
+	return &WriteBroadcaster{writers: make(map[StreamWriter]bool), buf: bytes.NewBuffer(nil)}
 }
 
 func GetTotalUsedFds() int {

+ 9 - 22
utils/utils_test.go

@@ -60,9 +60,9 @@ func TestWriteBroadcaster(t *testing.T) {
 
 	// Test 1: Both bufferA and bufferB should contain "foo"
 	bufferA := &dummyWriter{}
-	writer.AddWriter(bufferA)
+	writer.AddWriter(bufferA, "")
 	bufferB := &dummyWriter{}
-	writer.AddWriter(bufferB)
+	writer.AddWriter(bufferB, "")
 	writer.Write([]byte("foo"))
 
 	if bufferA.String() != "foo" {
@@ -76,7 +76,7 @@ func TestWriteBroadcaster(t *testing.T) {
 	// Test2: bufferA and bufferB should contain "foobar",
 	// while bufferC should only contain "bar"
 	bufferC := &dummyWriter{}
-	writer.AddWriter(bufferC)
+	writer.AddWriter(bufferC, "")
 	writer.Write([]byte("bar"))
 
 	if bufferA.String() != "foobar" {
@@ -91,35 +91,22 @@ func TestWriteBroadcaster(t *testing.T) {
 		t.Errorf("Buffer contains %v", bufferC.String())
 	}
 
-	// Test3: Test removal
-	writer.RemoveWriter(bufferB)
-	writer.Write([]byte("42"))
-	if bufferA.String() != "foobar42" {
-		t.Errorf("Buffer contains %v", bufferA.String())
-	}
-	if bufferB.String() != "foobar" {
-		t.Errorf("Buffer contains %v", bufferB.String())
-	}
-	if bufferC.String() != "bar42" {
-		t.Errorf("Buffer contains %v", bufferC.String())
-	}
-
-	// Test4: Test eviction on failure
+	// Test3: Test eviction on failure
 	bufferA.failOnWrite = true
 	writer.Write([]byte("fail"))
-	if bufferA.String() != "foobar42" {
+	if bufferA.String() != "foobar" {
 		t.Errorf("Buffer contains %v", bufferA.String())
 	}
-	if bufferC.String() != "bar42fail" {
+	if bufferC.String() != "barfail" {
 		t.Errorf("Buffer contains %v", bufferC.String())
 	}
 	// Even though we reset the flag, no more writes should go in there
 	bufferA.failOnWrite = false
 	writer.Write([]byte("test"))
-	if bufferA.String() != "foobar42" {
+	if bufferA.String() != "foobar" {
 		t.Errorf("Buffer contains %v", bufferA.String())
 	}
-	if bufferC.String() != "bar42failtest" {
+	if bufferC.String() != "barfailtest" {
 		t.Errorf("Buffer contains %v", bufferC.String())
 	}
 
@@ -141,7 +128,7 @@ func TestRaceWriteBroadcaster(t *testing.T) {
 	writer := NewWriteBroadcaster()
 	c := make(chan bool)
 	go func() {
-		writer.AddWriter(devNullCloser(0))
+		writer.AddWriter(devNullCloser(0), "")
 		c <- true
 	}()
 	writer.Write([]byte("hello"))

+ 10 - 3
utils_test.go

@@ -84,18 +84,25 @@ func readFile(src string, t *testing.T) (content string) {
 }
 
 // Create a test container from the given runtime `r` and run arguments `args`.
-// The image name (eg. the XXX in []string{"-i", "-t", "XXX", "bash"}, is dynamically replaced by the current test image.
+// If the image name is "_", (eg. []string{"-i", "-t", "_", "bash"}, it is
+// dynamically replaced by the current test image.
 // The caller is responsible for destroying the container.
 // Call t.Fatal() at the first error.
 func mkContainer(r *Runtime, args []string, t *testing.T) (*Container, *HostConfig, error) {
 	config, hostConfig, _, err := ParseRun(args, nil)
+	defer func() {
+		if err != nil && t != nil {
+			t.Fatal(err)
+		}
+	}()
 	if err != nil {
 		return nil, nil, err
 	}
-	config.Image = GetTestImage(r).ID
+	if config.Image == "_" {
+		config.Image = GetTestImage(r).ID
+	}
 	c, err := NewBuilder(r).Create(config)
 	if err != nil {
-		t.Fatal(err)
 		return nil, nil, err
 	}
 	return c, hostConfig, nil