Victor Vieux пре 12 година
родитељ
комит
ca902b6be4
48 измењених фајлова са 944 додато и 387 уклоњено
  1. 1 0
      AUTHORS
  2. 9 0
      CHANGELOG.md
  3. 1 0
      MAINTAINERS
  4. 63 14
      api.go
  5. 9 10
      api_test.go
  6. 3 0
      archive.go
  7. 15 4
      auth/auth.go
  8. 63 76
      buildfile.go
  9. 138 96
      commands.go
  10. 8 14
      contrib/mkimage-debian.sh
  11. 4 0
      docs/Makefile
  12. 11 9
      docs/README.md
  13. 52 7
      docs/sources/api/docker_remote_api.rst
  14. 2 2
      docs/sources/api/index.rst
  15. 0 1
      docs/sources/api/registry_api.rst
  16. 21 6
      docs/sources/commandline/command/build.rst
  17. 0 25
      docs/sources/concepts/buildingblocks.rst
  18. 0 1
      docs/sources/concepts/index.rst
  19. 2 2
      docs/sources/concepts/introduction.rst
  20. 2 2
      docs/sources/contributing/devenvironment.rst
  21. 2 2
      docs/sources/examples/couchdb_data_volumes.rst
  22. 2 1
      docs/sources/examples/index.rst
  23. 236 0
      docs/sources/examples/nodejs_web_app.rst
  24. 2 2
      docs/sources/examples/python_web_app.rst
  25. 1 1
      docs/sources/examples/running_examples.rst
  26. 3 3
      docs/sources/examples/running_redis_service.rst
  27. 49 4
      docs/sources/examples/running_ssh_service.rst
  28. 2 2
      docs/sources/index.rst
  29. 18 0
      docs/sources/installation/archlinux.rst
  30. 2 2
      docs/sources/use/basics.rst
  31. 8 2
      docs/sources/use/builder.rst
  32. 4 4
      docs/sources/use/workingwithrepository.rst
  33. 1 1
      docs/website/index.html
  34. 3 1
      graph.go
  35. 14 6
      hack/dockerbuilder/Dockerfile
  36. 2 1
      hack/dockerbuilder/dockerbuilder
  37. 0 3
      hack/dockerbuilder/fake_initctl
  38. 2 0
      hack/infrastructure/MAINTAINERS
  39. 5 0
      hack/infrastructure/README.md
  40. 1 0
      image.go
  41. 11 0
      packaging/debian/changelog
  42. 12 0
      packaging/ubuntu/changelog
  43. 1 2
      packaging/ubuntu/docker.upstart
  44. 3 0
      registry/registry.go
  45. 41 14
      runtime_test.go
  46. 56 53
      server.go
  47. 2 0
      term/MAINTAINERS
  48. 57 14
      utils/utils.go

+ 1 - 0
AUTHORS

@@ -15,6 +15,7 @@ Brian McCallister <brianm@skife.org>
 Bruno Bigras <bigras.bruno@gmail.com>
 Bruno Bigras <bigras.bruno@gmail.com>
 Caleb Spare <cespare@gmail.com>
 Caleb Spare <cespare@gmail.com>
 Charles Hooper <charles.hooper@dotcloud.com>
 Charles Hooper <charles.hooper@dotcloud.com>
+Daniel Gasienica <daniel@gasienica.ch>
 Daniel Mizyrycki <daniel.mizyrycki@dotcloud.com>
 Daniel Mizyrycki <daniel.mizyrycki@dotcloud.com>
 Daniel Robinson <gottagetmac@gmail.com>
 Daniel Robinson <gottagetmac@gmail.com>
 Daniel Von Fange <daniel@leancoder.com>
 Daniel Von Fange <daniel@leancoder.com>

+ 9 - 0
CHANGELOG.md

@@ -1,5 +1,14 @@
 # Changelog
 # Changelog
 
 
+## 0.3.4 (2013-05-30)
+ + Builder: 'docker build' builds a container, layer by layer, from a source repository containing a Dockerfile
+ + Builder: 'docker build -t FOO' applies the tag FOO to the newly built container.
+ + Runtime: interactive TTYs correctly handle window resize
+ * Runtime: fix how configuration is merged between layers
+ + Remote API: split stdout and stderr on 'docker run'
+ + Remote API: optionally listen on a different IP and port (use at your own risk)
+ * Documentation: improved install instructions.
+
 ## 0.3.3 (2013-05-23)
 ## 0.3.3 (2013-05-23)
  - Registry: Fix push regression
  - Registry: Fix push regression
  - Various bugfixes
  - Various bugfixes

+ 1 - 0
MAINTAINERS

@@ -1,4 +1,5 @@
 Solomon Hykes <solomon@dotcloud.com>
 Solomon Hykes <solomon@dotcloud.com>
 Guillaume Charmes <guillaume@dotcloud.com>
 Guillaume Charmes <guillaume@dotcloud.com>
+Victor Vieux <victor@dotcloud.com>
 api.go: Victor Vieux <victor@dotcloud.com>
 api.go: Victor Vieux <victor@dotcloud.com>
 Vagrantfile: Daniel Mizyrycki <daniel@dotcloud.com>
 Vagrantfile: Daniel Mizyrycki <daniel@dotcloud.com>

+ 63 - 14
api.go

@@ -47,6 +47,8 @@ func httpError(w http.ResponseWriter, err error) {
 		http.Error(w, err.Error(), http.StatusBadRequest)
 		http.Error(w, err.Error(), http.StatusBadRequest)
 	} else if strings.HasPrefix(err.Error(), "Conflict") {
 	} else if strings.HasPrefix(err.Error(), "Conflict") {
 		http.Error(w, err.Error(), http.StatusConflict)
 		http.Error(w, err.Error(), http.StatusConflict)
+	} else if strings.HasPrefix(err.Error(), "Impossible") {
+		http.Error(w, err.Error(), http.StatusNotAcceptable)
 	} else {
 	} else {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 	}
 	}
@@ -69,7 +71,16 @@ func getBoolParam(value string) (bool, error) {
 }
 }
 
 
 func getAuth(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
 func getAuth(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
-	b, err := json.Marshal(srv.registry.GetAuthConfig(false))
+	// FIXME: Handle multiple login at once
+	// FIXME: return specific error code if config file missing?
+	authConfig, err := auth.LoadConfig(srv.runtime.root)
+	if err != nil {
+		if err != auth.ErrConfigFileMissing {
+			return err
+		}
+		authConfig = &auth.AuthConfig{}
+	}
+	b, err := json.Marshal(&auth.AuthConfig{Username: authConfig.Username, Email: authConfig.Email})
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -78,11 +89,19 @@ func getAuth(srv *Server, version float64, w http.ResponseWriter, r *http.Reques
 }
 }
 
 
 func postAuth(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
 func postAuth(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
+	// FIXME: Handle multiple login at once
 	config := &auth.AuthConfig{}
 	config := &auth.AuthConfig{}
 	if err := json.NewDecoder(r.Body).Decode(config); err != nil {
 	if err := json.NewDecoder(r.Body).Decode(config); err != nil {
 		return err
 		return err
 	}
 	}
-	authConfig := srv.registry.GetAuthConfig(true)
+
+	authConfig, err := auth.LoadConfig(srv.runtime.root)
+	if err != nil {
+		if err != auth.ErrConfigFileMissing {
+			return err
+		}
+		authConfig = &auth.AuthConfig{}
+	}
 	if config.Username == authConfig.Username {
 	if config.Username == authConfig.Username {
 		config.Password = authConfig.Password
 		config.Password = authConfig.Password
 	}
 	}
@@ -92,7 +111,6 @@ func postAuth(srv *Server, version float64, w http.ResponseWriter, r *http.Reque
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
-	srv.registry.ResetClient(newAuthConfig)
 
 
 	if status != "" {
 	if status != "" {
 		b, err := json.Marshal(&ApiAuth{Status: status})
 		b, err := json.Marshal(&ApiAuth{Status: status})
@@ -298,16 +316,25 @@ func postImagesCreate(srv *Server, version float64, w http.ResponseWriter, r *ht
 	tag := r.Form.Get("tag")
 	tag := r.Form.Get("tag")
 	repo := r.Form.Get("repo")
 	repo := r.Form.Get("repo")
 
 
+	if version > 1.0 {
+		w.Header().Set("Content-Type", "application/json")
+	}
+	sf := utils.NewStreamFormatter(version > 1.0)
 	if image != "" { //pull
 	if image != "" { //pull
 		registry := r.Form.Get("registry")
 		registry := r.Form.Get("registry")
-		if version > 1.0 {
-			w.Header().Set("Content-Type", "application/json")
-		}
-		if err := srv.ImagePull(image, tag, registry, w, version > 1.0); err != nil {
+		if err := srv.ImagePull(image, tag, registry, w, sf); err != nil {
+			if sf.Used() {
+				w.Write(sf.FormatError(err))
+				return nil
+			}
 			return err
 			return err
 		}
 		}
 	} else { //import
 	} else { //import
-		if err := srv.ImageImport(src, repo, tag, r.Body, w); err != nil {
+		if err := srv.ImageImport(src, repo, tag, r.Body, w, sf); err != nil {
+			if sf.Used() {
+				w.Write(sf.FormatError(err))
+				return nil
+			}
 			return err
 			return err
 		}
 		}
 	}
 	}
@@ -343,10 +370,16 @@ func postImagesInsert(srv *Server, version float64, w http.ResponseWriter, r *ht
 		return fmt.Errorf("Missing parameter")
 		return fmt.Errorf("Missing parameter")
 	}
 	}
 	name := vars["name"]
 	name := vars["name"]
-
-	imgId, err := srv.ImageInsert(name, url, path, w)
+	if version > 1.0 {
+		w.Header().Set("Content-Type", "application/json")
+	}
+	sf := utils.NewStreamFormatter(version > 1.0)
+	imgId, err := srv.ImageInsert(name, url, path, w, sf)
 	if err != nil {
 	if err != nil {
-		return err
+		if sf.Used() {
+			w.Write(sf.FormatError(err))
+			return nil
+		}
 	}
 	}
 	b, err := json.Marshal(&ApiId{Id: imgId})
 	b, err := json.Marshal(&ApiId{Id: imgId})
 	if err != nil {
 	if err != nil {
@@ -366,8 +399,15 @@ func postImagesPush(srv *Server, version float64, w http.ResponseWriter, r *http
 		return fmt.Errorf("Missing parameter")
 		return fmt.Errorf("Missing parameter")
 	}
 	}
 	name := vars["name"]
 	name := vars["name"]
-
-	if err := srv.ImagePush(name, registry, w); err != nil {
+	if version > 1.0 {
+		w.Header().Set("Content-Type", "application/json")
+	}
+	sf := utils.NewStreamFormatter(version > 1.0)
+	if err := srv.ImagePush(name, registry, w, sf); err != nil {
+		if sf.Used() {
+			w.Write(sf.FormatError(err))
+			return nil
+		}
 		return err
 		return err
 	}
 	}
 	return nil
 	return nil
@@ -652,6 +692,13 @@ func postBuild(srv *Server, version float64, w http.ResponseWriter, r *http.Requ
 	if err := r.ParseMultipartForm(4096); err != nil {
 	if err := r.ParseMultipartForm(4096); err != nil {
 		return err
 		return err
 	}
 	}
+	remote := r.FormValue("t")
+	tag := ""
+	if strings.Contains(remote, ":") {
+		remoteParts := strings.Split(remote, ":")
+		tag = remoteParts[1]
+		remote = remoteParts[0]
+	}
 
 
 	dockerfile, _, err := r.FormFile("Dockerfile")
 	dockerfile, _, err := r.FormFile("Dockerfile")
 	if err != nil {
 	if err != nil {
@@ -666,8 +713,10 @@ func postBuild(srv *Server, version float64, w http.ResponseWriter, r *http.Requ
 	}
 	}
 
 
 	b := NewBuildFile(srv, utils.NewWriteFlusher(w))
 	b := NewBuildFile(srv, utils.NewWriteFlusher(w))
-	if _, err := b.Build(dockerfile, context); err != nil {
+	if id, err := b.Build(dockerfile, context); err != nil {
 		fmt.Fprintf(w, "Error build: %s\n", err)
 		fmt.Fprintf(w, "Error build: %s\n", err)
+	} else if remote != "" {
+		srv.runtime.repositories.Set(remote, tag, id, false)
 	}
 	}
 	return nil
 	return nil
 }
 }

+ 9 - 10
api_test.go

@@ -26,8 +26,7 @@ func TestGetAuth(t *testing.T) {
 	defer nuke(runtime)
 	defer nuke(runtime)
 
 
 	srv := &Server{
 	srv := &Server{
-		runtime:  runtime,
-		registry: registry.NewRegistry(runtime.root),
+		runtime: runtime,
 	}
 	}
 
 
 	r := httptest.NewRecorder()
 	r := httptest.NewRecorder()
@@ -56,7 +55,7 @@ func TestGetAuth(t *testing.T) {
 		t.Fatalf("%d OK or 0 expected, received %d\n", http.StatusOK, r.Code)
 		t.Fatalf("%d OK or 0 expected, received %d\n", http.StatusOK, r.Code)
 	}
 	}
 
 
-	newAuthConfig := srv.registry.GetAuthConfig(false)
+	newAuthConfig := registry.NewRegistry(runtime.root).GetAuthConfig(false)
 	if newAuthConfig.Username != authConfig.Username ||
 	if newAuthConfig.Username != authConfig.Username ||
 		newAuthConfig.Email != authConfig.Email {
 		newAuthConfig.Email != authConfig.Email {
 		t.Fatalf("The auth configuration hasn't been set correctly")
 		t.Fatalf("The auth configuration hasn't been set correctly")
@@ -247,8 +246,7 @@ func TestGetImagesSearch(t *testing.T) {
 	defer nuke(runtime)
 	defer nuke(runtime)
 
 
 	srv := &Server{
 	srv := &Server{
-		runtime:  runtime,
-		registry: registry.NewRegistry(runtime.root),
+		runtime: runtime,
 	}
 	}
 
 
 	r := httptest.NewRecorder()
 	r := httptest.NewRecorder()
@@ -504,15 +502,16 @@ func TestPostAuth(t *testing.T) {
 	defer nuke(runtime)
 	defer nuke(runtime)
 
 
 	srv := &Server{
 	srv := &Server{
-		runtime:  runtime,
-		registry: registry.NewRegistry(runtime.root),
+		runtime: runtime,
 	}
 	}
 
 
-	authConfigOrig := &auth.AuthConfig{
+	config := &auth.AuthConfig{
 		Username: "utest",
 		Username: "utest",
 		Email:    "utest@yopmail.com",
 		Email:    "utest@yopmail.com",
 	}
 	}
-	srv.registry.ResetClient(authConfigOrig)
+
+	authStr := auth.EncodeAuth(config)
+	auth.SaveConfig(runtime.root, authStr, config.Email)
 
 
 	r := httptest.NewRecorder()
 	r := httptest.NewRecorder()
 	if err := getAuth(srv, API_VERSION, r, nil, nil); err != nil {
 	if err := getAuth(srv, API_VERSION, r, nil, nil); err != nil {
@@ -524,7 +523,7 @@ func TestPostAuth(t *testing.T) {
 		t.Fatal(err)
 		t.Fatal(err)
 	}
 	}
 
 
-	if authConfig.Username != authConfigOrig.Username || authConfig.Email != authConfigOrig.Email {
+	if authConfig.Username != config.Username || authConfig.Email != config.Email {
 		t.Errorf("The retrieve auth mismatch with the one set.")
 		t.Errorf("The retrieve auth mismatch with the one set.")
 	}
 	}
 }
 }

+ 3 - 0
archive.go

@@ -54,6 +54,9 @@ func Tar(path string, compression Compression) (io.Reader, error) {
 func Untar(archive io.Reader, path string) error {
 func Untar(archive io.Reader, path string) error {
 	cmd := exec.Command("bsdtar", "-f", "-", "-C", path, "-x")
 	cmd := exec.Command("bsdtar", "-f", "-", "-C", path, "-x")
 	cmd.Stdin = archive
 	cmd.Stdin = archive
+	// Hardcode locale environment for predictable outcome regardless of host configuration.
+	//   (see https://github.com/dotcloud/docker/issues/355)
+	cmd.Env = []string{"LANG=en_US.utf-8", "LC_ALL=en_US.utf-8"}
 	output, err := cmd.CombinedOutput()
 	output, err := cmd.CombinedOutput()
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf("%s: %s", err, output)
 		return fmt.Errorf("%s: %s", err, output)

+ 15 - 4
auth/auth.go

@@ -3,6 +3,7 @@ package auth
 import (
 import (
 	"encoding/base64"
 	"encoding/base64"
 	"encoding/json"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"fmt"
 	"io/ioutil"
 	"io/ioutil"
 	"net/http"
 	"net/http"
@@ -17,6 +18,12 @@ const CONFIGFILE = ".dockercfg"
 // the registry server we want to login against
 // the registry server we want to login against
 const INDEX_SERVER = "https://index.docker.io/v1"
 const INDEX_SERVER = "https://index.docker.io/v1"
 
 
+//const INDEX_SERVER = "http://indexstaging-docker.dotcloud.com/"
+
+var (
+	ErrConfigFileMissing error = errors.New("The Auth config file is missing")
+)
+
 type AuthConfig struct {
 type AuthConfig struct {
 	Username string `json:"username"`
 	Username string `json:"username"`
 	Password string `json:"password"`
 	Password string `json:"password"`
@@ -75,7 +82,7 @@ func DecodeAuth(authStr string) (*AuthConfig, error) {
 func LoadConfig(rootPath string) (*AuthConfig, error) {
 func LoadConfig(rootPath string) (*AuthConfig, error) {
 	confFile := path.Join(rootPath, CONFIGFILE)
 	confFile := path.Join(rootPath, CONFIGFILE)
 	if _, err := os.Stat(confFile); err != nil {
 	if _, err := os.Stat(confFile); err != nil {
-		return &AuthConfig{}, fmt.Errorf("The Auth config file is missing")
+		return nil, ErrConfigFileMissing
 	}
 	}
 	b, err := ioutil.ReadFile(confFile)
 	b, err := ioutil.ReadFile(confFile)
 	if err != nil {
 	if err != nil {
@@ -97,7 +104,7 @@ func LoadConfig(rootPath string) (*AuthConfig, error) {
 }
 }
 
 
 // save the auth config
 // save the auth config
-func saveConfig(rootPath, authStr string, email string) error {
+func SaveConfig(rootPath, authStr string, email string) error {
 	confFile := path.Join(rootPath, CONFIGFILE)
 	confFile := path.Join(rootPath, CONFIGFILE)
 	if len(email) == 0 {
 	if len(email) == 0 {
 		os.Remove(confFile)
 		os.Remove(confFile)
@@ -161,7 +168,9 @@ func Login(authConfig *AuthConfig) (string, error) {
 				status = "Login Succeeded\n"
 				status = "Login Succeeded\n"
 				storeConfig = true
 				storeConfig = true
 			} else if resp.StatusCode == 401 {
 			} else if resp.StatusCode == 401 {
-				saveConfig(authConfig.rootPath, "", "")
+				if err := SaveConfig(authConfig.rootPath, "", ""); err != nil {
+					return "", err
+				}
 				return "", fmt.Errorf("Wrong login/password, please try again")
 				return "", fmt.Errorf("Wrong login/password, please try again")
 			} else {
 			} else {
 				return "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body,
 				return "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body,
@@ -175,7 +184,9 @@ func Login(authConfig *AuthConfig) (string, error) {
 	}
 	}
 	if storeConfig {
 	if storeConfig {
 		authStr := EncodeAuth(authConfig)
 		authStr := EncodeAuth(authConfig)
-		saveConfig(authConfig.rootPath, authStr, authConfig.Email)
+		if err := SaveConfig(authConfig.rootPath, authStr, authConfig.Email); err != nil {
+			return "", err
+		}
 	}
 	}
 	return status, nil
 	return status, nil
 }
 }

+ 63 - 76
buildfile.go

@@ -32,8 +32,6 @@ type buildFile struct {
 	tmpContainers map[string]struct{}
 	tmpContainers map[string]struct{}
 	tmpImages     map[string]struct{}
 	tmpImages     map[string]struct{}
 
 
-	needCommit bool
-
 	out io.Writer
 	out io.Writer
 }
 }
 
 
@@ -63,7 +61,7 @@ func (b *buildFile) CmdFrom(name string) error {
 				remote = name
 				remote = name
 			}
 			}
 
 
-			if err := b.srv.ImagePull(remote, tag, "", b.out, false); err != nil {
+			if err := b.srv.ImagePull(remote, tag, "", b.out, utils.NewStreamFormatter(false)); err != nil {
 				return err
 				return err
 			}
 			}
 
 
@@ -81,9 +79,8 @@ func (b *buildFile) CmdFrom(name string) error {
 }
 }
 
 
 func (b *buildFile) CmdMaintainer(name string) error {
 func (b *buildFile) CmdMaintainer(name string) error {
-	b.needCommit = true
 	b.maintainer = name
 	b.maintainer = name
-	return nil
+	return b.commit("", b.config.Cmd, fmt.Sprintf("MAINTAINER %s", name))
 }
 }
 
 
 func (b *buildFile) CmdRun(args string) error {
 func (b *buildFile) CmdRun(args string) error {
@@ -95,28 +92,34 @@ func (b *buildFile) CmdRun(args string) error {
 		return err
 		return err
 	}
 	}
 
 
-	cmd, env := b.config.Cmd, b.config.Env
+	cmd := b.config.Cmd
 	b.config.Cmd = nil
 	b.config.Cmd = nil
 	MergeConfig(b.config, config)
 	MergeConfig(b.config, config)
 
 
-	if cache, err := b.srv.ImageGetCached(b.image, config); err != nil {
+	utils.Debugf("Command to be executed: %v", b.config.Cmd)
+
+	if cache, err := b.srv.ImageGetCached(b.image, b.config); err != nil {
 		return err
 		return err
 	} else if cache != nil {
 	} else if cache != nil {
-		utils.Debugf("Use cached version")
+		utils.Debugf("[BUILDER] Use cached version")
 		b.image = cache.Id
 		b.image = cache.Id
 		return nil
 		return nil
+	} else {
+		utils.Debugf("[BUILDER] Cache miss")
 	}
 	}
 
 
 	cid, err := b.run()
 	cid, err := b.run()
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
-	b.config.Cmd, b.config.Env = cmd, env
-	return b.commit(cid)
+	if err := b.commit(cid, cmd, "run"); err != nil {
+		return err
+	}
+	b.config.Cmd = cmd
+	return nil
 }
 }
 
 
 func (b *buildFile) CmdEnv(args string) error {
 func (b *buildFile) CmdEnv(args string) error {
-	b.needCommit = true
 	tmp := strings.SplitN(args, " ", 2)
 	tmp := strings.SplitN(args, " ", 2)
 	if len(tmp) != 2 {
 	if len(tmp) != 2 {
 		return fmt.Errorf("Invalid ENV format")
 		return fmt.Errorf("Invalid ENV format")
@@ -131,60 +134,34 @@ func (b *buildFile) CmdEnv(args string) error {
 		}
 		}
 	}
 	}
 	b.config.Env = append(b.config.Env, key+"="+value)
 	b.config.Env = append(b.config.Env, key+"="+value)
-	return nil
+	return b.commit("", b.config.Cmd, fmt.Sprintf("ENV %s=%s", key, value))
 }
 }
 
 
 func (b *buildFile) CmdCmd(args string) error {
 func (b *buildFile) CmdCmd(args string) error {
-	b.needCommit = true
 	var cmd []string
 	var cmd []string
 	if err := json.Unmarshal([]byte(args), &cmd); err != nil {
 	if err := json.Unmarshal([]byte(args), &cmd); err != nil {
 		utils.Debugf("Error unmarshalling: %s, using /bin/sh -c", err)
 		utils.Debugf("Error unmarshalling: %s, using /bin/sh -c", err)
-		b.config.Cmd = []string{"/bin/sh", "-c", args}
-	} else {
-		b.config.Cmd = cmd
+		cmd = []string{"/bin/sh", "-c", args}
+	}
+	if err := b.commit("", cmd, fmt.Sprintf("CMD %v", cmd)); err != nil {
+		return err
 	}
 	}
+	b.config.Cmd = cmd
 	return nil
 	return nil
 }
 }
 
 
 func (b *buildFile) CmdExpose(args string) error {
 func (b *buildFile) CmdExpose(args string) error {
 	ports := strings.Split(args, " ")
 	ports := strings.Split(args, " ")
 	b.config.PortSpecs = append(ports, b.config.PortSpecs...)
 	b.config.PortSpecs = append(ports, b.config.PortSpecs...)
-	return nil
+	return b.commit("", b.config.Cmd, fmt.Sprintf("EXPOSE %v", ports))
 }
 }
 
 
 func (b *buildFile) CmdInsert(args string) error {
 func (b *buildFile) CmdInsert(args string) error {
-	if b.image == "" {
-		return fmt.Errorf("Please provide a source image with `from` prior to insert")
-	}
-	tmp := strings.SplitN(args, " ", 2)
-	if len(tmp) != 2 {
-		return fmt.Errorf("Invalid INSERT format")
-	}
-	sourceUrl := strings.Trim(tmp[0], " ")
-	destPath := strings.Trim(tmp[1], " ")
-
-	file, err := utils.Download(sourceUrl, b.out)
-	if err != nil {
-		return err
-	}
-	defer file.Body.Close()
-
-	b.config.Cmd = []string{"echo", "INSERT", sourceUrl, "in", destPath}
-	cid, err := b.run()
-	if err != nil {
-		return err
-	}
-
-	container := b.runtime.Get(cid)
-	if container == nil {
-		return fmt.Errorf("An error occured while creating the container")
-	}
-
-	if err := container.Inject(file.Body, destPath); err != nil {
-		return err
-	}
+	return fmt.Errorf("INSERT has been deprecated. Please use ADD instead")
+}
 
 
-	return b.commit(cid)
+func (b *buildFile) CmdCopy(args string) error {
+	return fmt.Errorf("COPY has been deprecated. Please use ADD instead")
 }
 }
 
 
 func (b *buildFile) CmdAdd(args string) error {
 func (b *buildFile) CmdAdd(args string) error {
@@ -193,12 +170,13 @@ func (b *buildFile) CmdAdd(args string) error {
 	}
 	}
 	tmp := strings.SplitN(args, " ", 2)
 	tmp := strings.SplitN(args, " ", 2)
 	if len(tmp) != 2 {
 	if len(tmp) != 2 {
-		return fmt.Errorf("Invalid INSERT format")
+		return fmt.Errorf("Invalid ADD format")
 	}
 	}
 	orig := strings.Trim(tmp[0], " ")
 	orig := strings.Trim(tmp[0], " ")
 	dest := strings.Trim(tmp[1], " ")
 	dest := strings.Trim(tmp[1], " ")
 
 
-	b.config.Cmd = []string{"echo", "PUSH", orig, "in", dest}
+	cmd := b.config.Cmd
+	b.config.Cmd = []string{"/bin/sh", "-c", fmt.Sprintf("#(nop) ADD %s in %s", orig, dest)}
 	cid, err := b.run()
 	cid, err := b.run()
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -208,19 +186,23 @@ func (b *buildFile) CmdAdd(args string) error {
 	if container == nil {
 	if container == nil {
 		return fmt.Errorf("Error while creating the container (CmdAdd)")
 		return fmt.Errorf("Error while creating the container (CmdAdd)")
 	}
 	}
-
-	if err := os.MkdirAll(path.Join(container.rwPath(), dest), 0700); err != nil {
+	if err := container.EnsureMounted(); err != nil {
 		return err
 		return err
 	}
 	}
+	defer container.Unmount()
 
 
 	origPath := path.Join(b.context, orig)
 	origPath := path.Join(b.context, orig)
-	destPath := path.Join(container.rwPath(), dest)
+	destPath := path.Join(container.RootfsPath(), dest)
 
 
 	fi, err := os.Stat(origPath)
 	fi, err := os.Stat(origPath)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 	if fi.IsDir() {
 	if fi.IsDir() {
+		if err := os.MkdirAll(destPath, 0700); err != nil {
+			return err
+		}
+
 		files, err := ioutil.ReadDir(path.Join(b.context, orig))
 		files, err := ioutil.ReadDir(path.Join(b.context, orig))
 		if err != nil {
 		if err != nil {
 			return err
 			return err
@@ -231,12 +213,18 @@ func (b *buildFile) CmdAdd(args string) error {
 			}
 			}
 		}
 		}
 	} else {
 	} else {
+		if err := os.MkdirAll(path.Dir(destPath), 0700); err != nil {
+			return err
+		}
 		if err := utils.CopyDirectory(origPath, destPath); err != nil {
 		if err := utils.CopyDirectory(origPath, destPath); err != nil {
 			return err
 			return err
 		}
 		}
 	}
 	}
-
-	return b.commit(cid)
+	if err := b.commit(cid, cmd, fmt.Sprintf("ADD %s in %s", orig, dest)); err != nil {
+		return err
+	}
+	b.config.Cmd = cmd
+	return nil
 }
 }
 
 
 func (b *buildFile) run() (string, error) {
 func (b *buildFile) run() (string, error) {
@@ -265,20 +253,30 @@ func (b *buildFile) run() (string, error) {
 	return c.Id, nil
 	return c.Id, nil
 }
 }
 
 
-func (b *buildFile) commit(id string) error {
+// Commit the container <id> with the autorun command <autoCmd>
+func (b *buildFile) commit(id string, autoCmd []string, comment string) error {
 	if b.image == "" {
 	if b.image == "" {
 		return fmt.Errorf("Please provide a source image with `from` prior to commit")
 		return fmt.Errorf("Please provide a source image with `from` prior to commit")
 	}
 	}
 	b.config.Image = b.image
 	b.config.Image = b.image
 	if id == "" {
 	if id == "" {
-		cmd := b.config.Cmd
-		b.config.Cmd = []string{"true"}
+		b.config.Cmd = []string{"/bin/sh", "-c", "#(nop) " + comment}
+
+		if cache, err := b.srv.ImageGetCached(b.image, b.config); err != nil {
+			return err
+		} else if cache != nil {
+			utils.Debugf("[BUILDER] Use cached version")
+			b.image = cache.Id
+			return nil
+		} else {
+			utils.Debugf("[BUILDER] Cache miss")
+		}
+
 		if cid, err := b.run(); err != nil {
 		if cid, err := b.run(); err != nil {
 			return err
 			return err
 		} else {
 		} else {
 			id = cid
 			id = cid
 		}
 		}
-		b.config.Cmd = cmd
 	}
 	}
 
 
 	container := b.runtime.Get(id)
 	container := b.runtime.Get(id)
@@ -286,20 +284,20 @@ func (b *buildFile) commit(id string) error {
 		return fmt.Errorf("An error occured while creating the container")
 		return fmt.Errorf("An error occured while creating the container")
 	}
 	}
 
 
+	// Note: Actually copy the struct
+	autoConfig := *b.config
+	autoConfig.Cmd = autoCmd
 	// Commit the container
 	// Commit the container
-	image, err := b.builder.Commit(container, "", "", "", b.maintainer, nil)
+	image, err := b.builder.Commit(container, "", "", "", b.maintainer, &autoConfig)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 	b.tmpImages[image.Id] = struct{}{}
 	b.tmpImages[image.Id] = struct{}{}
 	b.image = image.Id
 	b.image = image.Id
-	b.needCommit = false
 	return nil
 	return nil
 }
 }
 
 
 func (b *buildFile) Build(dockerfile, context io.Reader) (string, error) {
 func (b *buildFile) Build(dockerfile, context io.Reader) (string, error) {
-	defer b.clearTmp(b.tmpContainers, b.tmpImages)
-
 	if context != nil {
 	if context != nil {
 		name, err := ioutil.TempDir("/tmp", "docker-build")
 		name, err := ioutil.TempDir("/tmp", "docker-build")
 		if err != nil {
 		if err != nil {
@@ -337,6 +335,7 @@ func (b *buildFile) Build(dockerfile, context io.Reader) (string, error) {
 		method, exists := reflect.TypeOf(b).MethodByName("Cmd" + strings.ToUpper(instruction[:1]) + strings.ToLower(instruction[1:]))
 		method, exists := reflect.TypeOf(b).MethodByName("Cmd" + strings.ToUpper(instruction[:1]) + strings.ToLower(instruction[1:]))
 		if !exists {
 		if !exists {
 			fmt.Fprintf(b.out, "Skipping unknown instruction %s\n", strings.ToUpper(instruction))
 			fmt.Fprintf(b.out, "Skipping unknown instruction %s\n", strings.ToUpper(instruction))
+			continue
 		}
 		}
 		ret := method.Func.Call([]reflect.Value{reflect.ValueOf(b), reflect.ValueOf(arguments)})[0].Interface()
 		ret := method.Func.Call([]reflect.Value{reflect.ValueOf(b), reflect.ValueOf(arguments)})[0].Interface()
 		if ret != nil {
 		if ret != nil {
@@ -345,22 +344,10 @@ func (b *buildFile) Build(dockerfile, context io.Reader) (string, error) {
 
 
 		fmt.Fprintf(b.out, "===> %v\n", b.image)
 		fmt.Fprintf(b.out, "===> %v\n", b.image)
 	}
 	}
-	if b.needCommit {
-		if err := b.commit(""); err != nil {
-			return "", err
-		}
-	}
 	if b.image != "" {
 	if b.image != "" {
-		// The build is successful, keep the temporary containers and images
-		for i := range b.tmpImages {
-			delete(b.tmpImages, i)
-		}
-		fmt.Fprintf(b.out, "Build success.\n Image id:\n%s\n", b.image)
+		fmt.Fprintf(b.out, "Build successful.\n===> %s\n", b.image)
 		return b.image, nil
 		return b.image, nil
 	}
 	}
-	for i := range b.tmpContainers {
-		delete(b.tmpContainers, i)
-	}
 	return "", fmt.Errorf("An error occured during the build\n")
 	return "", fmt.Errorf("An error occured during the build\n")
 }
 }
 
 

+ 138 - 96
commands.go

@@ -17,6 +17,7 @@ import (
 	"net/url"
 	"net/url"
 	"os"
 	"os"
 	"os/signal"
 	"os/signal"
+	"path"
 	"path/filepath"
 	"path/filepath"
 	"reflect"
 	"reflect"
 	"strconv"
 	"strconv"
@@ -27,7 +28,7 @@ import (
 	"unicode"
 	"unicode"
 )
 )
 
 
-const VERSION = "0.3.3"
+const VERSION = "0.3.4"
 
 
 var (
 var (
 	GIT_COMMIT string
 	GIT_COMMIT string
@@ -73,37 +74,37 @@ func (cli *DockerCli) CmdHelp(args ...string) error {
 		}
 		}
 	}
 	}
 	help := fmt.Sprintf("Usage: docker [OPTIONS] COMMAND [arg...]\n  -H=\"%s:%d\": Host:port to bind/connect to\n\nA self-sufficient runtime for linux containers.\n\nCommands:\n", cli.host, cli.port)
 	help := fmt.Sprintf("Usage: docker [OPTIONS] COMMAND [arg...]\n  -H=\"%s:%d\": Host:port to bind/connect to\n\nA self-sufficient runtime for linux containers.\n\nCommands:\n", cli.host, cli.port)
-	for cmd, description := range map[string]string{
-		"attach":  "Attach to a running container",
-		"build":   "Build a container from a Dockerfile",
-		"commit":  "Create a new image from a container's changes",
-		"diff":    "Inspect changes on a container's filesystem",
-		"export":  "Stream the contents of a container as a tar archive",
-		"history": "Show the history of an image",
-		"images":  "List images",
-		"import":  "Create a new filesystem image from the contents of a tarball",
-		"info":    "Display system-wide information",
-		"insert":  "Insert a file in an image",
-		"inspect": "Return low-level information on a container",
-		"kill":    "Kill a running container",
-		"login":   "Register or Login to the docker registry server",
-		"logs":    "Fetch the logs of a container",
-		"port":    "Lookup the public-facing port which is NAT-ed to PRIVATE_PORT",
-		"ps":      "List containers",
-		"pull":    "Pull an image or a repository from the docker registry server",
-		"push":    "Push an image or a repository to the docker registry server",
-		"restart": "Restart a running container",
-		"rm":      "Remove a container",
-		"rmi":     "Remove an image",
-		"run":     "Run a command in a new container",
-		"search":  "Search for an image in the docker index",
-		"start":   "Start a stopped container",
-		"stop":    "Stop a running container",
-		"tag":     "Tag an image into a repository",
-		"version": "Show the docker version information",
-		"wait":    "Block until a container stops, then print its exit code",
+	for _, command := range [][2]string{
+		{"attach", "Attach to a running container"},
+		{"build", "Build a container from a Dockerfile"},
+		{"commit", "Create a new image from a container's changes"},
+		{"diff", "Inspect changes on a container's filesystem"},
+		{"export", "Stream the contents of a container as a tar archive"},
+		{"history", "Show the history of an image"},
+		{"images", "List images"},
+		{"import", "Create a new filesystem image from the contents of a tarball"},
+		{"info", "Display system-wide information"},
+		{"insert", "Insert a file in an image"},
+		{"inspect", "Return low-level information on a container"},
+		{"kill", "Kill a running container"},
+		{"login", "Register or Login to the docker registry server"},
+		{"logs", "Fetch the logs of a container"},
+		{"port", "Lookup the public-facing port which is NAT-ed to PRIVATE_PORT"},
+		{"ps", "List containers"},
+		{"pull", "Pull an image or a repository from the docker registry server"},
+		{"push", "Push an image or a repository to the docker registry server"},
+		{"restart", "Restart a running container"},
+		{"rm", "Remove a container"},
+		{"rmi", "Remove an image"},
+		{"run", "Run a command in a new container"},
+		{"search", "Search for an image in the docker index"},
+		{"start", "Start a stopped container"},
+		{"stop", "Stop a running container"},
+		{"tag", "Tag an image into a repository"},
+		{"version", "Show the docker version information"},
+		{"wait", "Block until a container stops, then print its exit code"},
 	} {
 	} {
-		help += fmt.Sprintf("    %-10.10s%s\n", cmd, description)
+		help += fmt.Sprintf("    %-10.10s%s\n", command[0], command[1])
 	}
 	}
 	fmt.Println(help)
 	fmt.Println(help)
 	return nil
 	return nil
@@ -130,16 +131,20 @@ func (cli *DockerCli) CmdInsert(args ...string) error {
 }
 }
 
 
 func (cli *DockerCli) CmdBuild(args ...string) error {
 func (cli *DockerCli) CmdBuild(args ...string) error {
-	cmd := Subcmd("build", "[OPTIONS] [CONTEXT]", "Build an image from a Dockerfile")
-	fileName := cmd.String("f", "Dockerfile", "Use `file` as Dockerfile. Can be '-' for stdin")
+	cmd := Subcmd("build", "[OPTIONS] PATH | -", "Build a new container image from the source code at PATH")
+	tag := cmd.String("t", "", "Tag to be applied to the resulting image in case of success")
 	if err := cmd.Parse(args); err != nil {
 	if err := cmd.Parse(args); err != nil {
 		return nil
 		return nil
 	}
 	}
+	if cmd.NArg() != 1 {
+		cmd.Usage()
+		return nil
+	}
 
 
 	var (
 	var (
-		file          io.ReadCloser
 		multipartBody io.Reader
 		multipartBody io.Reader
-		err           error
+		file          io.ReadCloser
+		contextPath   string
 	)
 	)
 
 
 	// Init the needed component for the Multipart
 	// Init the needed component for the Multipart
@@ -148,27 +153,19 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
 	w := multipart.NewWriter(buff)
 	w := multipart.NewWriter(buff)
 	boundary := strings.NewReader("\r\n--" + w.Boundary() + "--\r\n")
 	boundary := strings.NewReader("\r\n--" + w.Boundary() + "--\r\n")
 
 
-	// Create a FormFile multipart for the Dockerfile
-	if *fileName == "-" {
+	compression := Bzip2
+
+	if cmd.Arg(0) == "-" {
 		file = os.Stdin
 		file = os.Stdin
 	} else {
 	} else {
-		file, err = os.Open(*fileName)
-		if err != nil {
+		// Send Dockerfile from arg/Dockerfile (deprecate later)
+		if f, err := os.Open(path.Join(cmd.Arg(0), "Dockerfile")); err != nil {
 			return err
 			return err
+		} else {
+			file = f
 		}
 		}
-		defer file.Close()
-	}
-	if wField, err := w.CreateFormFile("Dockerfile", *fileName); err != nil {
-		return err
-	} else {
-		io.Copy(wField, file)
-	}
-	multipartBody = io.MultiReader(multipartBody, boundary)
-
-	compression := Bzip2
-
-	// Create a FormFile multipart for the context if needed
-	if cmd.Arg(0) != "" {
+		// Send context from arg
+		// Create a FormFile multipart for the context if needed
 		// FIXME: Use NewTempArchive in order to have the size and avoid too much memory usage?
 		// FIXME: Use NewTempArchive in order to have the size and avoid too much memory usage?
 		context, err := Tar(cmd.Arg(0), compression)
 		context, err := Tar(cmd.Arg(0), compression)
 		if err != nil {
 		if err != nil {
@@ -183,19 +180,28 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
 			return err
 			return err
 		} else {
 		} else {
 			// FIXME: Find a way to have a progressbar for the upload too
 			// FIXME: Find a way to have a progressbar for the upload too
-			io.Copy(wField, utils.ProgressReader(ioutil.NopCloser(context), -1, os.Stdout, "Caching Context %v/%v (%v)\r", false))
+			sf := utils.NewStreamFormatter(false)
+			io.Copy(wField, utils.ProgressReader(ioutil.NopCloser(context), -1, os.Stdout, sf.FormatProgress("Caching Context", "%v/%v (%v)"), sf))
 		}
 		}
-
 		multipartBody = io.MultiReader(multipartBody, boundary)
 		multipartBody = io.MultiReader(multipartBody, boundary)
 	}
 	}
+	// Create a FormFile multipart for the Dockerfile
+	if wField, err := w.CreateFormFile("Dockerfile", "Dockerfile"); err != nil {
+		return err
+	} else {
+		io.Copy(wField, file)
+	}
+	multipartBody = io.MultiReader(multipartBody, boundary)
 
 
+	v := &url.Values{}
+	v.Set("t", *tag)
 	// Send the multipart request with correct content-type
 	// Send the multipart request with correct content-type
-	req, err := http.NewRequest("POST", fmt.Sprintf("http://%s:%d%s", cli.host, cli.port, "/build"), multipartBody)
+	req, err := http.NewRequest("POST", fmt.Sprintf("http://%s:%d%s?%s", cli.host, cli.port, "/build", v.Encode()), multipartBody)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 	req.Header.Set("Content-Type", w.FormDataContentType())
 	req.Header.Set("Content-Type", w.FormDataContentType())
-	if cmd.Arg(0) != "" {
+	if contextPath != "" {
 		req.Header.Set("X-Docker-Context-Compression", compression.Flag())
 		req.Header.Set("X-Docker-Context-Compression", compression.Flag())
 		fmt.Println("Uploading Context...")
 		fmt.Println("Uploading Context...")
 	}
 	}
@@ -366,12 +372,10 @@ func (cli *DockerCli) CmdWait(args ...string) error {
 // 'docker version': show version information
 // 'docker version': show version information
 func (cli *DockerCli) CmdVersion(args ...string) error {
 func (cli *DockerCli) CmdVersion(args ...string) error {
 	cmd := Subcmd("version", "", "Show the docker version information.")
 	cmd := Subcmd("version", "", "Show the docker version information.")
-	fmt.Println(len(args))
 	if err := cmd.Parse(args); err != nil {
 	if err := cmd.Parse(args); err != nil {
 		return nil
 		return nil
 	}
 	}
 
 
-	fmt.Println(cmd.NArg())
 	if cmd.NArg() > 0 {
 	if cmd.NArg() > 0 {
 		cmd.Usage()
 		cmd.Usage()
 		return nil
 		return nil
@@ -882,9 +886,9 @@ func (cli *DockerCli) CmdPs(args ...string) error {
 	for _, out := range outs {
 	for _, out := range outs {
 		if !*quiet {
 		if !*quiet {
 			if *noTrunc {
 			if *noTrunc {
-				fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s ago\t%s\n", out.Id, out.Image, out.Command, utils.HumanDuration(time.Now().Sub(time.Unix(out.Created, 0))), out.Status, out.Ports)
+				fmt.Fprintf(w, "%s\t%s\t%s\t%s ago\t%s\t%s\n", out.Id, out.Image, out.Command, utils.HumanDuration(time.Now().Sub(time.Unix(out.Created, 0))), out.Status, out.Ports)
 			} else {
 			} else {
-				fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s ago\t%s\n", utils.TruncateId(out.Id), out.Image, utils.Trunc(out.Command, 20), utils.HumanDuration(time.Now().Sub(time.Unix(out.Created, 0))), out.Status, out.Ports)
+				fmt.Fprintf(w, "%s\t%s\t%s\t%s ago\t%s\t%s\n", utils.TruncateId(out.Id), out.Image, utils.Trunc(out.Command, 20), utils.HumanDuration(time.Now().Sub(time.Unix(out.Created, 0))), out.Status, out.Ports)
 			}
 			}
 		} else {
 		} else {
 			if *noTrunc {
 			if *noTrunc {
@@ -996,12 +1000,10 @@ func (cli *DockerCli) CmdLogs(args ...string) error {
 		return nil
 		return nil
 	}
 	}
 
 
-	v := url.Values{}
-	v.Set("logs", "1")
-	v.Set("stdout", "1")
-	v.Set("stderr", "1")
-
-	if err := cli.hijack("POST", "/containers/"+cmd.Arg(0)+"/attach?"+v.Encode(), false); err != nil {
+	if err := cli.stream("POST", "/containers/"+cmd.Arg(0)+"/attach?logs=1&stdout=1", nil, os.Stdout); err != nil {
+		return err
+	}
+	if err := cli.stream("POST", "/containers/"+cmd.Arg(0)+"/attach?logs=1&stderr=1", nil, os.Stderr); err != nil {
 		return err
 		return err
 	}
 	}
 	return nil
 	return nil
@@ -1028,15 +1030,35 @@ func (cli *DockerCli) CmdAttach(args ...string) error {
 		return err
 		return err
 	}
 	}
 
 
+	splitStderr := container.Config.Tty
+
+	connections := 1
+	if splitStderr {
+		connections += 1
+	}
+	chErrors := make(chan error, connections)
+	cli.monitorTtySize(cmd.Arg(0))
+	if splitStderr {
+		go func() {
+			chErrors <- cli.hijack("POST", "/containers/"+cmd.Arg(0)+"/attach?stream=1&stderr=1", false, nil, os.Stderr)
+		}()
+	}
 	v := url.Values{}
 	v := url.Values{}
 	v.Set("stream", "1")
 	v.Set("stream", "1")
-	v.Set("stdout", "1")
-	v.Set("stderr", "1")
 	v.Set("stdin", "1")
 	v.Set("stdin", "1")
-
-	cli.monitorTtySize(cmd.Arg(0))
-	if err := cli.hijack("POST", "/containers/"+cmd.Arg(0)+"/attach?"+v.Encode(), container.Config.Tty); err != nil {
-		return err
+	v.Set("stdout", "1")
+	if !splitStderr {
+		v.Set("stderr", "1")
+	}
+	go func() {
+		chErrors <- cli.hijack("POST", "/containers/"+cmd.Arg(0)+"/attach?"+v.Encode(), container.Config.Tty, os.Stdin, os.Stdout)
+	}()
+	for connections > 0 {
+		err := <-chErrors
+		if err != nil {
+			return err
+		}
+		connections -= 1
 	}
 	}
 	return nil
 	return nil
 }
 }
@@ -1200,19 +1222,14 @@ func (cli *DockerCli) CmdRun(args ...string) error {
 		fmt.Fprintln(os.Stderr, "WARNING: ", warning)
 		fmt.Fprintln(os.Stderr, "WARNING: ", warning)
 	}
 	}
 
 
-	v := url.Values{}
-	v.Set("logs", "1")
-	v.Set("stream", "1")
+	splitStderr := !config.Tty
 
 
-	if config.AttachStdin {
-		v.Set("stdin", "1")
+	connections := 0
+	if config.AttachStdin || config.AttachStdout || (!splitStderr && config.AttachStderr) {
+		connections += 1
 	}
 	}
-	if config.AttachStdout {
-		v.Set("stdout", "1")
-	}
-	if config.AttachStderr {
-		v.Set("stderr", "1")
-
+	if splitStderr && config.AttachStderr {
+		connections += 1
 	}
 	}
 
 
 	//start the container
 	//start the container
@@ -1221,10 +1238,38 @@ func (cli *DockerCli) CmdRun(args ...string) error {
 		return err
 		return err
 	}
 	}
 
 
-	if config.AttachStdin || config.AttachStdout || config.AttachStderr {
+	if connections > 0 {
+		chErrors := make(chan error, connections)
 		cli.monitorTtySize(out.Id)
 		cli.monitorTtySize(out.Id)
-		if err := cli.hijack("POST", "/containers/"+out.Id+"/attach?"+v.Encode(), config.Tty); err != nil {
-			return err
+
+		if splitStderr && config.AttachStderr {
+			go func() {
+				chErrors <- cli.hijack("POST", "/containers/"+out.Id+"/attach?logs=1&stream=1&stderr=1", config.Tty, nil, os.Stderr)
+			}()
+		}
+
+		v := url.Values{}
+		v.Set("logs", "1")
+		v.Set("stream", "1")
+
+		if config.AttachStdin {
+			v.Set("stdin", "1")
+		}
+		if config.AttachStdout {
+			v.Set("stdout", "1")
+		}
+		if !splitStderr && config.AttachStderr {
+			v.Set("stderr", "1")
+		}
+		go func() {
+			chErrors <- cli.hijack("POST", "/containers/"+out.Id+"/attach?"+v.Encode(), config.Tty, os.Stdin, os.Stdout)
+		}()
+		for connections > 0 {
+			err := <-chErrors
+			if err != nil {
+				return err
+			}
+			connections -= 1
 		}
 		}
 	}
 	}
 	if !config.AttachStdout && !config.AttachStderr {
 	if !config.AttachStdout && !config.AttachStderr {
@@ -1334,13 +1379,9 @@ func (cli *DockerCli) stream(method, path string, in io.Reader, out io.Writer) e
 	}
 	}
 
 
 	if resp.Header.Get("Content-Type") == "application/json" {
 	if resp.Header.Get("Content-Type") == "application/json" {
-		type Message struct {
-			Status   string `json:"status,omitempty"`
-			Progress string `json:"progress,omitempty"`
-		}
 		dec := json.NewDecoder(resp.Body)
 		dec := json.NewDecoder(resp.Body)
 		for {
 		for {
-			var m Message
+			var m utils.JsonMessage
 			if err := dec.Decode(&m); err == io.EOF {
 			if err := dec.Decode(&m); err == io.EOF {
 				break
 				break
 			} else if err != nil {
 			} else if err != nil {
@@ -1348,6 +1389,8 @@ func (cli *DockerCli) stream(method, path string, in io.Reader, out io.Writer) e
 			}
 			}
 			if m.Progress != "" {
 			if m.Progress != "" {
 				fmt.Fprintf(out, "Downloading %s\r", m.Progress)
 				fmt.Fprintf(out, "Downloading %s\r", m.Progress)
+			} else if m.Error != "" {
+				return fmt.Errorf(m.Error)
 			} else {
 			} else {
 				fmt.Fprintf(out, "%s\n", m.Status)
 				fmt.Fprintf(out, "%s\n", m.Status)
 			}
 			}
@@ -1360,7 +1403,7 @@ func (cli *DockerCli) stream(method, path string, in io.Reader, out io.Writer) e
 	return nil
 	return nil
 }
 }
 
 
-func (cli *DockerCli) hijack(method, path string, setRawTerminal bool) error {
+func (cli *DockerCli) hijack(method, path string, setRawTerminal bool, in *os.File, out io.Writer) error {
 	req, err := http.NewRequest(method, fmt.Sprintf("/v%g%s", API_VERSION, path), nil)
 	req, err := http.NewRequest(method, fmt.Sprintf("/v%g%s", API_VERSION, path), nil)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -1378,20 +1421,19 @@ func (cli *DockerCli) hijack(method, path string, setRawTerminal bool) error {
 	defer rwc.Close()
 	defer rwc.Close()
 
 
 	receiveStdout := utils.Go(func() error {
 	receiveStdout := utils.Go(func() error {
-		_, err := io.Copy(os.Stdout, br)
+		_, err := io.Copy(out, br)
 		return err
 		return err
 	})
 	})
 
 
-	if setRawTerminal && term.IsTerminal(int(os.Stdin.Fd())) && os.Getenv("NORAW") == "" {
+	if in != nil && setRawTerminal && term.IsTerminal(int(in.Fd())) && os.Getenv("NORAW") == "" {
 		if oldState, err := term.SetRawTerminal(); err != nil {
 		if oldState, err := term.SetRawTerminal(); err != nil {
 			return err
 			return err
 		} else {
 		} else {
 			defer term.RestoreTerminal(oldState)
 			defer term.RestoreTerminal(oldState)
 		}
 		}
 	}
 	}
-
 	sendStdin := utils.Go(func() error {
 	sendStdin := utils.Go(func() error {
-		_, err := io.Copy(rwc, os.Stdin)
+		_, err := io.Copy(rwc, in)
 		if err := rwc.(*net.TCPConn).CloseWrite(); err != nil {
 		if err := rwc.(*net.TCPConn).CloseWrite(); err != nil {
 			fmt.Fprintf(os.Stderr, "Couldn't send EOF: %s\n", err)
 			fmt.Fprintf(os.Stderr, "Couldn't send EOF: %s\n", err)
 		}
 		}

+ 8 - 14
contrib/mkimage-debian.sh

@@ -2,18 +2,15 @@
 set -e
 set -e
 
 
 # these should match the names found at http://www.debian.org/releases/
 # these should match the names found at http://www.debian.org/releases/
-stableSuite='squeeze'
-testingSuite='wheezy'
+stableSuite='wheezy'
+testingSuite='jessie'
 unstableSuite='sid'
 unstableSuite='sid'
 
 
-# if suite is equal to this, it gets the "latest" tag
-latestSuite="$testingSuite"
-
 variant='minbase'
 variant='minbase'
 include='iproute,iputils-ping'
 include='iproute,iputils-ping'
 
 
 repo="$1"
 repo="$1"
-suite="${2:-$latestSuite}"
+suite="${2:-$stableSuite}"
 mirror="${3:-}" # stick to the default debootstrap mirror if one is not provided
 mirror="${3:-}" # stick to the default debootstrap mirror if one is not provided
 
 
 if [ ! "$repo" ]; then
 if [ ! "$repo" ]; then
@@ -41,17 +38,14 @@ img=$(sudo tar -c . | docker import -)
 # tag suite
 # tag suite
 docker tag $img $repo $suite
 docker tag $img $repo $suite
 
 
-if [ "$suite" = "$latestSuite" ]; then
-	# tag latest
-	docker tag $img $repo latest
-fi
-
 # test the image
 # test the image
 docker run -i -t $repo:$suite echo success
 docker run -i -t $repo:$suite echo success
 
 
-# unstable's version numbers match testing (since it's mostly just a sandbox for testing), so it doesn't get a version number tag
-if [ "$suite" != "$unstableSuite" -a "$suite" != 'unstable' ]; then
-	# tag the specific version
+if [ "$suite" = "$stableSuite" -o "$suite" = 'stable' ]; then
+	# tag latest
+	docker tag $img $repo latest
+	
+	# tag the specific debian release version
 	ver=$(docker run $repo:$suite cat /etc/debian_version)
 	ver=$(docker run $repo:$suite cat /etc/debian_version)
 	docker tag $img $repo $ver
 	docker tag $img $repo $ver
 fi
 fi

+ 4 - 0
docs/Makefile

@@ -6,6 +6,7 @@ SPHINXOPTS    =
 SPHINXBUILD   = sphinx-build
 SPHINXBUILD   = sphinx-build
 PAPER         =
 PAPER         =
 BUILDDIR      = _build
 BUILDDIR      = _build
+PYTHON        = python
 
 
 # Internal variables.
 # Internal variables.
 PAPEROPT_a4     = -D latex_paper_size=a4
 PAPEROPT_a4     = -D latex_paper_size=a4
@@ -38,6 +39,7 @@ help:
 #	@echo "  linkcheck  to check all external links for integrity"
 #	@echo "  linkcheck  to check all external links for integrity"
 #	@echo "  doctest    to run all doctests embedded in the documentation (if enabled)"
 #	@echo "  doctest    to run all doctests embedded in the documentation (if enabled)"
 	@echo "  docs       to build the docs and copy the static files to the outputdir"
 	@echo "  docs       to build the docs and copy the static files to the outputdir"
+	@echo "  server     to serve the docs in your browser under \`http://localhost:8000\`"
 	@echo "  publish    to publish the app to dotcloud"
 	@echo "  publish    to publish the app to dotcloud"
 
 
 clean:
 clean:
@@ -49,6 +51,8 @@ docs:
 	@echo
 	@echo
 	@echo "Build finished. The documentation pages are now in $(BUILDDIR)/html."
 	@echo "Build finished. The documentation pages are now in $(BUILDDIR)/html."
 
 
+server:
+	@cd $(BUILDDIR)/html; $(PYTHON) -m SimpleHTTPServer 8000
 
 
 site:
 site:
 	cp -r website $(BUILDDIR)/
 	cp -r website $(BUILDDIR)/

+ 11 - 9
docs/README.md

@@ -14,20 +14,22 @@ Installation
 ------------
 ------------
 
 
 * Work in your own fork of the code, we accept pull requests.
 * Work in your own fork of the code, we accept pull requests.
-* Install sphinx: ``pip install sphinx``
-* Install sphinx httpdomain contrib package ``sphinxcontrib-httpdomain``
+* Install sphinx: `pip install sphinx`
+    * Mac OS X: `[sudo] pip-2.7 install sphinx`)
+* Install sphinx httpdomain contrib package: `pip install sphinxcontrib-httpdomain`
+    * Mac OS X: `[sudo] pip-2.7 install sphinxcontrib-httpdomain`
 * If pip is not available you can probably install it using your favorite package manager as **python-pip**
 * If pip is not available you can probably install it using your favorite package manager as **python-pip**
 
 
 Usage
 Usage
 -----
 -----
-* change the .rst files with your favorite editor to your liking
-* run *make docs* to clean up old files and generate new ones
-* your static website can now be found in the _build dir
-* to preview what you have generated, cd into _build/html and then run 'python -m SimpleHTTPServer 8000'
+* Change the `.rst` files with your favorite editor to your liking.
+* Run `make docs` to clean up old files and generate new ones.
+* Your static website can now be found in the `_build` directory.
+* To preview what you have generated run `make server` and open <http://localhost:8000/> in your favorite browser.
 
 
-Working using github's file editor
+Working using GitHub's file editor
 ----------------------------------
 ----------------------------------
-Alternatively, for small changes and typo's you might want to use github's built in file editor. It allows
+Alternatively, for small changes and typo's you might want to use GitHub's built in file editor. It allows
 you to preview your changes right online. Just be carefull not to create many commits.
 you to preview your changes right online. Just be carefull not to create many commits.
 
 
 Images
 Images
@@ -72,4 +74,4 @@ Guides on using sphinx
 
 
 * Code examples
 * Code examples
 
 
-  Start without $, so it's easy to copy and paste.
+  Start without $, so it's easy to copy and paste.

+ 52 - 7
docs/sources/api/docker_remote_api.rst

@@ -15,10 +15,17 @@ Docker Remote API
 - Default port in the docker deamon is 4243 
 - 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
 - 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
 
 
-2. Endpoints
+2. Version
+==========
+
+The current verson of the API is 1.1
+Calling /images/<name>/insert is the same as calling /v1.1/images/<name>/insert
+You can still call an old version of the api using /v1.0/images/<name>/insert
+
+3. Endpoints
 ============
 ============
 
 
-2.1 Containers
+3.1 Containers
 --------------
 --------------
 
 
 List containers
 List containers
@@ -132,6 +139,7 @@ Create a container
 	:jsonparam config: the container's configuration
 	:jsonparam config: the container's configuration
 	:statuscode 201: no error
 	:statuscode 201: no error
 	:statuscode 404: no such container
 	:statuscode 404: no such container
+	:statuscode 406: impossible to attach (container not running)
 	:statuscode 500: server error
 	:statuscode 500: server error
 
 
 
 
@@ -459,7 +467,7 @@ Remove a container
         :statuscode 500: server error
         :statuscode 500: server error
 
 
 
 
-2.2 Images
+3.2 Images
 ----------
 ----------
 
 
 List Images
 List Images
@@ -548,7 +556,19 @@ Create an image
 
 
            POST /images/create?fromImage=base HTTP/1.1
            POST /images/create?fromImage=base HTTP/1.1
 
 
-        **Example response**:
+        **Example response v1.1**:
+
+        .. sourcecode:: http
+
+           HTTP/1.1 200 OK
+	   Content-Type: application/json
+
+	   {"status":"Pulling..."}
+	   {"progress":"1/? (n/a)"}
+	   {"error":"Invalid..."}
+	   ...
+
+        **Example response v1.0**:
 
 
         .. sourcecode:: http
         .. sourcecode:: http
 
 
@@ -579,7 +599,19 @@ Insert a file in a image
 
 
            POST /images/test/insert?path=/usr&url=myurl HTTP/1.1
            POST /images/test/insert?path=/usr&url=myurl HTTP/1.1
 
 
-	**Example response**:
+	**Example response v1.1**:
+
+        .. sourcecode:: http
+
+           HTTP/1.1 200 OK
+	   Content-Type: application/json
+
+	   {"status":"Inserting..."}
+	   {"progress":"1/? (n/a)"}
+	   {"error":"Invalid..."}
+	   ...
+
+	**Example response v1.0**:
 
 
         .. sourcecode:: http
         .. sourcecode:: http
 
 
@@ -694,7 +726,19 @@ Push an image on the registry
 
 
 	    POST /images/test/push HTTP/1.1
 	    POST /images/test/push HTTP/1.1
 
 
-	 **Example response**:
+	 **Example response v1.1**:
+
+        .. sourcecode:: http
+
+           HTTP/1.1 200 OK
+	   Content-Type: application/json
+
+	   {"status":"Pushing..."}
+	   {"progress":"1/? (n/a)"}
+	   {"error":"Invalid..."}
+	   ...
+
+	 **Example response v1.0**:
 
 
         .. sourcecode:: http
         .. sourcecode:: http
 
 
@@ -817,7 +861,7 @@ Search images
 	   :statuscode 500: server error
 	   :statuscode 500: server error
 
 
 
 
-2.3 Misc
+3.3 Misc
 --------
 --------
 
 
 Build an image from Dockerfile via stdin
 Build an image from Dockerfile via stdin
@@ -843,6 +887,7 @@ Build an image from Dockerfile via stdin
 	   
 	   
 	   {{ STREAM }}
 	   {{ STREAM }}
 
 
+	:query t: tag to be applied to the resulting image in case of success
 	:statuscode 200: no error
 	:statuscode 200: no error
         :statuscode 500: server error
         :statuscode 500: server error
 
 

+ 2 - 2
docs/sources/api/index.rst

@@ -2,8 +2,8 @@
 :description: docker documentation
 :description: docker documentation
 :keywords: docker, ipa, documentation
 :keywords: docker, ipa, documentation
 
 
-API's
-=============
+APIs
+====
 
 
 This following :
 This following :
 
 

+ 0 - 1
docs/sources/api/registry_api.rst

@@ -246,7 +246,6 @@ The Index has two main purposes (along with its fancy social features):
 
 
 - Resolve short names (to avoid passing absolute URLs all the time)
 - Resolve short names (to avoid passing absolute URLs all the time)
    - username/projectname -> \https://registry.docker.io/users/<username>/repositories/<projectname>/
    - username/projectname -> \https://registry.docker.io/users/<username>/repositories/<projectname>/
-   - team/projectname -> \https://registry.docker.io/team/<team>/repositories/<projectname>/
 - Authenticate a user as a repos owner (for a central referenced repository)
 - Authenticate a user as a repos owner (for a central referenced repository)
 
 
 3.1 Without an Index
 3.1 Without an Index

+ 21 - 6
docs/sources/commandline/command/build.rst

@@ -2,12 +2,27 @@
 :description: Build a new image from the Dockerfile passed via stdin
 :description: Build a new image from the Dockerfile passed via stdin
 :keywords: build, docker, container, documentation
 :keywords: build, docker, container, documentation
 
 
-========================================================
-``build`` -- Build a container from Dockerfile via stdin
-========================================================
+================================================
+``build`` -- Build a container from a Dockerfile
+================================================
 
 
 ::
 ::
 
 
-    Usage: docker build -
-    Example: cat Dockerfile | docker build -
-    Build a new image from the Dockerfile passed via stdin
+    Usage: docker build [OPTIONS] PATH | -
+    Build a new container image from the source code at PATH
+      -t="": Tag to be applied to the resulting image in case of success.
+
+Examples
+--------
+
+.. code-block:: bash
+
+    docker build .
+
+This will take the local Dockerfile
+
+.. code-block:: bash
+
+    docker build -
+
+This will read a Dockerfile form Stdin without context

+ 0 - 25
docs/sources/concepts/buildingblocks.rst

@@ -1,25 +0,0 @@
-:title: Building Blocks
-:description: An introduction to docker and standard containers?
-:keywords: containers, lxc, concepts, explanation
-
-
-Building blocks
-===============
-
-.. _images:
-
-Images
-------
-An original container image. These are stored on disk and are comparable with what you normally expect from a stopped virtual machine image. Images are stored (and retrieved from) repository
-
-Images are stored on your local file system under /var/lib/docker/graph
-
-
-.. _containers:
-
-Containers
-----------
-A container is a local version of an image. It can be running or stopped, The equivalent would be a virtual machine instance.
-
-Containers are stored on your local file system under /var/lib/docker/containers
-

+ 0 - 1
docs/sources/concepts/index.rst

@@ -13,5 +13,4 @@ Contents:
    :maxdepth: 1
    :maxdepth: 1
 
 
    ../index
    ../index
-   buildingblocks
 
 

+ 2 - 2
docs/sources/concepts/introduction.rst

@@ -5,8 +5,8 @@
 Introduction
 Introduction
 ============
 ============
 
 
-Docker - The Linux container runtime
-------------------------------------
+Docker -- The Linux container runtime
+-------------------------------------
 
 
 Docker complements LXC with a high-level API which operates at the process level. It runs unix processes with strong guarantees of isolation and repeatability across servers.
 Docker complements LXC with a high-level API which operates at the process level. It runs unix processes with strong guarantees of isolation and repeatability across servers.
 
 

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

@@ -1,8 +1,8 @@
-:title: Setting up a dev environment
+:title: Setting Up a Dev Environment
 :description: Guides on how to contribute to docker
 :description: Guides on how to contribute to docker
 :keywords: Docker, documentation, developers, contributing, dev environment
 :keywords: Docker, documentation, developers, contributing, dev environment
 
 
-Setting up a dev environment
+Setting Up a Dev Environment
 ============================
 ============================
 
 
 Instructions that have been verified to work on Ubuntu 12.10,
 Instructions that have been verified to work on Ubuntu 12.10,

+ 2 - 2
docs/sources/examples/couchdb_data_volumes.rst

@@ -4,8 +4,8 @@
 
 
 .. _running_couchdb_service:
 .. _running_couchdb_service:
 
 
-Create a CouchDB service
-========================
+CouchDB Service
+===============
 
 
 .. include:: example_header.inc
 .. include:: example_header.inc
 
 

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

@@ -1,6 +1,6 @@
 :title: Docker Examples
 :title: Docker Examples
 :description: Examples on how to use Docker
 :description: Examples on how to use Docker
-:keywords: docker, hello world, examples
+:keywords: docker, hello world, node, nodejs, python, couch, couchdb, redis, ssh, sshd, examples
 
 
 
 
 
 
@@ -16,6 +16,7 @@ Contents:
    hello_world
    hello_world
    hello_world_daemon
    hello_world_daemon
    python_web_app
    python_web_app
+   nodejs_web_app
    running_redis_service
    running_redis_service
    running_ssh_service
    running_ssh_service
    couchdb_data_volumes
    couchdb_data_volumes

+ 236 - 0
docs/sources/examples/nodejs_web_app.rst

@@ -0,0 +1,236 @@
+:title: Running a Node.js app on CentOS
+:description: Installing and running a Node.js app on CentOS
+:keywords: docker, example, package installation, node, centos
+
+.. _nodejs_web_app:
+
+Node.js Web App
+===============
+
+.. include:: example_header.inc
+
+The goal of this example is to show you how you can build your own docker images
+from a parent image using a ``Dockerfile`` . We will do that by making a simple
+Node.js hello world web application running on CentOS. You can get the full
+source code at https://github.com/gasi/docker-node-hello.
+
+Create Node.js app
+++++++++++++++++++
+
+First, create a ``package.json`` file that describes your app and its
+dependencies:
+
+.. code-block:: json
+
+    {
+      "name": "docker-centos-hello",
+      "private": true,
+      "version": "0.0.1",
+      "description": "Node.js Hello World app on CentOS using docker",
+      "author": "Daniel Gasienica <daniel@gasienica.ch>",
+      "dependencies": {
+        "express": "3.2.4"
+      }
+    }
+
+Then, create an ``index.js`` file that defines a web app using the
+`Express.js <http://expressjs.com/>`_ framework:
+
+.. code-block:: javascript
+
+    var express = require('express');
+
+    // Constants
+    var PORT = 8080;
+
+    // App
+    var app = express();
+    app.get('/', function (req, res) {
+      res.send('Hello World\n');
+    });
+
+    app.listen(PORT)
+    console.log('Running on http://localhost:' + PORT);
+
+
+In the next steps, we’ll look at how you can run this app inside a CentOS
+container using docker. First, you’ll need to build a docker image of your app.
+
+Creating a ``Dockerfile``
++++++++++++++++++++++++++
+
+Create an empty file called ``Dockerfile``:
+
+.. code-block:: bash
+
+    touch Dockerfile
+
+Open the ``Dockerfile`` in your favorite text editor and add the following line
+that defines the version of docker the image requires to build
+(this example uses docker 0.3.4):
+
+.. code-block:: bash
+
+    # DOCKER-VERSION 0.3.4
+
+Next, define the parent image you want to use to build your own image on top of.
+Here, we’ll use `CentOS <https://index.docker.io/_/centos/>`_ (tag: ``6.4``)
+available on the `docker index`_:
+
+.. code-block:: bash
+
+    FROM    centos:6.4
+
+Since we’re building a Node.js app, you’ll have to install Node.js as well as
+npm on your CentOS image. Node.js is required to run your app and npm to install
+your app’s dependencies defined in ``package.json``.
+To install the right package for CentOS, we’ll use the instructions from the
+`Node.js wiki`_:
+
+.. code-block:: bash
+
+    # Enable EPEL for Node.js
+    RUN     rpm -Uvh http://download.fedoraproject.org/pub/epel/6/i386/epel-release-6-8.noarch.rpm
+    # Install Node.js and npm
+    RUN     yum install -y npm-1.2.17-5.el6
+
+To bundle your app’s source code inside the docker image, use the ``ADD``
+command:
+
+.. code-block:: bash
+
+    # Bundle app source
+    ADD . /src
+
+Install your app dependencies using npm:
+
+.. code-block:: bash
+
+    # Install app dependencies
+    RUN cd /src; npm install
+
+Your app binds to port ``8080`` so you’ll use the ``EXPOSE`` command to have it
+mapped by the docker daemon:
+
+.. code-block:: bash
+
+    EXPOSE  8080
+
+Last but not least, define the command to run your app using ``CMD`` which
+defines your runtime, i.e. ``node``, and the path to our app, i.e.
+``src/index.js`` (see the step where we added the source to the container):
+
+.. code-block:: bash
+
+    CMD ["node", "/src/index.js"]
+
+Your ``Dockerfile`` should now look like this:
+
+.. code-block:: bash
+
+
+    # DOCKER-VERSION 0.3.4
+    FROM    centos:6.4
+
+    # Enable EPEL for Node.js
+    RUN     rpm -Uvh http://download.fedoraproject.org/pub/epel/6/i386/epel-release-6-8.noarch.rpm
+    # Install Node.js and npm
+    RUN     yum install -y npm-1.2.17-5.el6
+
+    # Bundle app source
+    ADD . /src
+    # Install app dependencies
+    RUN cd /src; npm install
+
+    EXPOSE  8080
+    CMD ["node", "/src/index.js"]
+
+
+Building your image
++++++++++++++++++++
+
+Go to the directory that has your ``Dockerfile`` and run the following command
+to build a docker image. The ``-t`` flag let’s you tag your image so it’s easier
+to find later using the ``docker images`` command:
+
+.. code-block:: bash
+
+    docker build -t <your username>/centos-node-hello .
+
+Your image will now be listed by docker:
+
+.. code-block:: bash
+
+    docker images
+
+    > # Example
+    > REPOSITORY                 TAG       ID              CREATED
+    > centos                     6.4       539c0211cd76    8 weeks ago
+    > gasi/centos-node-hello     latest    d64d3505b0d2    2 hours ago
+
+
+Run the image
++++++++++++++
+
+Running your image with ``-d`` runs the container in detached mode, leaving the
+container running in the background. Run the image you previously built:
+
+.. code-block:: bash
+
+    docker run -d <your username>/centos-node-hello
+
+Print the output of your app:
+
+.. code-block:: bash
+
+    # Get container ID
+    docker ps
+
+    # Print app output
+    docker logs <container id>
+
+    > # Example
+    > Running on http://localhost:8080
+
+
+Test
+++++
+
+To test your app, get the the port of your app that docker mapped:
+
+.. code-block:: bash
+
+    docker ps
+
+    > # Example
+    > ID            IMAGE                          COMMAND              ...   PORTS
+    > ecce33b30ebf  gasi/centos-node-hello:latest  node /src/index.js         49160->8080
+
+In the example above, docker mapped the ``8080`` port of the container to
+``49160``.
+
+Now you can call your app using ``curl`` (install if needed via:
+``sudo apt-get install curl``):
+
+.. code-block:: bash
+
+    curl -i localhost:49160
+
+    > HTTP/1.1 200 OK
+    > X-Powered-By: Express
+    > Content-Type: text/html; charset=utf-8
+    > Content-Length: 12
+    > Date: Sun, 02 Jun 2013 03:53:22 GMT
+    > Connection: keep-alive
+    >
+    > Hello World
+
+We hope this tutorial helped you get up and running with Node.js and CentOS on
+docker. You can get the full source code at
+https://github.com/gasi/docker-node-hello.
+
+Continue to :ref:`running_redis_service`.
+
+
+.. _Node.js wiki: https://github.com/joyent/node/wiki/Installing-Node.js-via-package-manager#rhelcentosscientific-linux-6
+.. _docker index: https://index.docker.io/

+ 2 - 2
docs/sources/examples/python_web_app.rst

@@ -4,8 +4,8 @@
 
 
 .. _python_web_app:
 .. _python_web_app:
 
 
-Building a python web app
-=========================
+Python Web App
+==============
 
 
 .. include:: example_header.inc
 .. include:: example_header.inc
 
 

+ 1 - 1
docs/sources/examples/running_examples.rst

@@ -4,7 +4,7 @@
 
 
 .. _running_examples:
 .. _running_examples:
 
 
-Running The Examples
+Running the Examples
 --------------------
 --------------------
 
 
 All the examples assume your machine is running the docker daemon. To run the docker daemon in the background, simply type:
 All the examples assume your machine is running the docker daemon. To run the docker daemon in the background, simply type:

+ 3 - 3
docs/sources/examples/running_redis_service.rst

@@ -4,8 +4,8 @@
 
 
 .. _running_redis_service:
 .. _running_redis_service:
 
 
-Create a redis service
-======================
+Redis Service
+=============
 
 
 .. include:: example_header.inc
 .. include:: example_header.inc
 
 
@@ -34,7 +34,7 @@ Snapshot the installation
 
 
 .. code-block:: bash
 .. code-block:: bash
 
 
-    docker ps -a  # grab the container id (this will be the last one in the list)
+    docker ps -a  # grab the container id (this will be the first one in the list)
     docker commit <container_id> <your username>/redis
     docker commit <container_id> <your username>/redis
 
 
 Run the service
 Run the service

+ 49 - 4
docs/sources/examples/running_ssh_service.rst

@@ -4,8 +4,8 @@
 
 
 .. _running_ssh_service:
 .. _running_ssh_service:
 
 
-Create an ssh daemon service
-============================
+SSH Daemon Service
+==================
 
 
 .. include:: example_header.inc
 .. include:: example_header.inc
 
 
@@ -20,8 +20,7 @@ minutes and not entirely smooth, but gives you a good idea.
     <div style="margin-top:10px;">
     <div style="margin-top:10px;">
       <iframe width="800" height="400" src="http://ascii.io/a/2637/raw" frameborder="0"></iframe>
       <iframe width="800" height="400" src="http://ascii.io/a/2637/raw" frameborder="0"></iframe>
     </div>
     </div>
-
-
+	
 You can also get this sshd container by using
 You can also get this sshd container by using
 ::
 ::
 
 
@@ -30,3 +29,49 @@ You can also get this sshd container by using
 
 
 The password is 'screencast'
 The password is 'screencast'
 
 
+**Video's Transcription:**
+
+.. code-block:: bash
+
+	 # Hello! We are going to try and install openssh on a container and run it as a servic 
+	 # let's pull base to get a base ubuntu image. 
+	 $ docker pull base
+	 # I had it so it was quick
+	 # now let's connect using -i for interactive and with -t for terminal 
+	 # we execute /bin/bash to get a prompt.
+	 $ docker run -i -t base /bin/bash
+	 # now let's commit it 
+	 # which container was it?
+	 $ docker ps -a |more
+	 $ docker commit a30a3a2f2b130749995f5902f079dc6ad31ea0621fac595128ec59c6da07feea dhrp/sshd 
+	 # I gave the name dhrp/sshd for the container
+	 # now we can run it again 
+	 $ docker run -d dhrp/sshd /usr/sbin/sshd -D # D for daemon mode 
+	 # is it running?
+	 $ docker ps
+	 # yes!
+	 # let's stop it 
+	 $ docker stop 0ebf7cec294755399d063f4b1627980d4cbff7d999f0bc82b59c300f8536a562
+	 $ docker ps
+	 # and reconnect, but now open a port to it
+	 $ docker run -d -p 22 dhrp/sshd /usr/sbin/sshd -D 
+	 $ docker port b2b407cf22cf8e7fa3736fa8852713571074536b1d31def3fdfcd9fa4fd8c8c5 22
+	 # it has now given us a port to connect to
+	 # we have to connect using a public ip of our host
+	 $ hostname
+	 $ ifconfig
+	 $ ssh root@192.168.33.10 -p 49153
+	 # Ah! forgot to set root passwd
+	 $ docker commit b2b407cf22cf8e7fa3736fa8852713571074536b1d31def3fdfcd9fa4fd8c8c5 dhrp/sshd 
+	 $ docker ps -a
+	 $ docker run -i -t dhrp/sshd /bin/bash
+	 $ passwd
+	 $ exit
+	 $ docker commit 9e863f0ca0af31c8b951048ba87641d67c382d08d655c2e4879c51410e0fedc1 dhrp/sshd
+	 $ docker run -d -p 22 dhrp/sshd /usr/sbin/sshd -D
+	 $ docker port a0aaa9558c90cf5c7782648df904a82365ebacce523e4acc085ac1213bfe2206 22
+	 $ ifconfig
+	 $ ssh root@192.168.33.10 -p 49154
+	 # Thanks for watching, Thatcher thatcher@dotcloud.com
+
+

+ 2 - 2
docs/sources/index.rst

@@ -7,8 +7,8 @@
 Introduction
 Introduction
 ============
 ============
 
 
-Docker - The Linux container runtime
-------------------------------------
+Docker -- The Linux container runtime
+-------------------------------------
 
 
 Docker complements LXC with a high-level API which operates at the process level. It runs unix processes with strong guarantees of isolation and repeatability across servers.
 Docker complements LXC with a high-level API which operates at the process level. It runs unix processes with strong guarantees of isolation and repeatability across servers.
 
 

+ 18 - 0
docs/sources/installation/archlinux.rst

@@ -67,3 +67,21 @@ To start on system boot:
 ::
 ::
 
 
     sudo systemctl enable docker
     sudo systemctl enable docker
+    
+Network Configuration
+---------------------
+
+IPv4 packet forwarding is disabled by default on Arch, so internet access from inside
+the container may not work.
+
+To enable the forwarding, run as root on the host system:
+
+::
+
+    sysctl net.ipv4.ip_forward=1
+    
+And, to make it persistent across reboots, enable it on the host's **/etc/sysctl.conf**:
+
+::
+
+    net.ipv4.ip_forward=1

+ 2 - 2
docs/sources/use/basics.rst

@@ -3,8 +3,8 @@
 :keywords: Examples, Usage, basic commands, docker, documentation, examples
 :keywords: Examples, Usage, basic commands, docker, documentation, examples
 
 
 
 
-The basics
-=============
+The Basics
+==========
 
 
 Starting Docker
 Starting Docker
 ---------------
 ---------------

+ 8 - 2
docs/sources/use/builder.rst

@@ -125,8 +125,14 @@ curl was installed within the image.
 .. note::
 .. note::
     The path must include the file name.
     The path must include the file name.
 
 
-.. note::
-    This instruction has temporarily disabled
+2.8 ADD
+-------
+
+    ``ADD <src> <dest>``
+
+The `ADD` instruction will insert the files from the `<src>` path of the context into `<dest>` path 
+of the container.
+The context must be set in order to use this instruction. (see examples)
 
 
 3. Dockerfile Examples
 3. Dockerfile Examples
 ======================
 ======================

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

@@ -4,8 +4,8 @@
 
 
 .. _working_with_the_repository:
 .. _working_with_the_repository:
 
 
-Working with the repository
-============================
+Working with the Repository
+===========================
 
 
 
 
 Top-level repositories and user repositories
 Top-level repositories and user repositories
@@ -14,9 +14,9 @@ Top-level repositories and user repositories
 Generally, there are two types of repositories: Top-level repositories which are controlled by the people behind
 Generally, there are two types of repositories: Top-level repositories which are controlled by the people behind
 Docker, and user repositories.
 Docker, and user repositories.
 
 
-* Top-level repositories can easily be recognized by not having a / (slash) in their name. These repositories can
+* Top-level repositories can easily be recognized by not having a ``/`` (slash) in their name. These repositories can
   generally be trusted.
   generally be trusted.
-* User repositories always come in the form of <username>/<repo_name>. This is what your published images will look like.
+* User repositories always come in the form of ``<username>/<repo_name>``. This is what your published images will look like.
 * User images are not checked, it is therefore up to you whether or not you trust the creator of this image.
 * User images are not checked, it is therefore up to you whether or not you trust the creator of this image.
 
 
 
 

+ 1 - 1
docs/website/index.html

@@ -270,7 +270,7 @@
                     <li>Filesystem isolation: each process container runs in a completely separate root filesystem.</li>
                     <li>Filesystem isolation: each process container runs in a completely separate root filesystem.</li>
                     <li>Resource isolation: system resources like cpu and memory can be allocated differently to each process container, using cgroups.</li>
                     <li>Resource isolation: system resources like cpu and memory can be allocated differently to each process container, using cgroups.</li>
                     <li>Network isolation: each process container runs in its own network namespace, with a virtual interface and IP address of its own.</li>
                     <li>Network isolation: each process container runs in its own network namespace, with a virtual interface and IP address of its own.</li>
-                    <li>Copy-on-write: root filesystems are created using copy-on-write, which makes deployment extremeley fast, memory-cheap and disk-cheap.</li>
+                    <li>Copy-on-write: root filesystems are created using copy-on-write, which makes deployment extremely fast, memory-cheap and disk-cheap.</li>
                     <li>Logging: the standard streams (stdout/stderr/stdin) of each process container is collected and logged for real-time or batch retrieval.</li>
                     <li>Logging: the standard streams (stdout/stderr/stdin) of each process container is collected and logged for real-time or batch retrieval.</li>
                     <li>Change management: changes to a container's filesystem can be committed into a new image and re-used to create more containers. No templating or manual configuration required.</li>
                     <li>Change management: changes to a container's filesystem can be committed into a new image and re-used to create more containers. No templating or manual configuration required.</li>
                     <li>Interactive shell: docker can allocate a pseudo-tty and attach to the standard input of any container, for example to run a throwaway interactive shell.</li>
                     <li>Interactive shell: docker can allocate a pseudo-tty and attach to the standard input of any container, for example to run a throwaway interactive shell.</li>

+ 3 - 1
graph.go

@@ -107,6 +107,7 @@ func (graph *Graph) Create(layerData Archive, container *Container, comment, aut
 		DockerVersion: VERSION,
 		DockerVersion: VERSION,
 		Author:        author,
 		Author:        author,
 		Config:        config,
 		Config:        config,
+		Architecture:  "x86_64",
 	}
 	}
 	if container != nil {
 	if container != nil {
 		img.Parent = container.Image
 		img.Parent = container.Image
@@ -165,7 +166,8 @@ func (graph *Graph) TempLayerArchive(id string, compression Compression, output
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
-	return NewTempArchive(utils.ProgressReader(ioutil.NopCloser(archive), 0, output, "Buffering to disk %v/%v (%v)", false), tmp.Root)
+	sf := utils.NewStreamFormatter(false)
+	return NewTempArchive(utils.ProgressReader(ioutil.NopCloser(archive), 0, output, sf.FormatProgress("Buffering to disk", "%v/%v (%v)"), sf), tmp.Root)
 }
 }
 
 
 // Mktemp creates a temporary sub-directory inside the graph's filesystem.
 // Mktemp creates a temporary sub-directory inside the graph's filesystem.

+ 14 - 6
hack/dockerbuilder/Dockerfile

@@ -1,23 +1,31 @@
 # This will build a container capable of producing an official binary build of docker and
 # This will build a container capable of producing an official binary build of docker and
 # uploading it to S3
 # uploading it to S3
+from	ubuntu:12.04
 maintainer	Solomon Hykes <solomon@dotcloud.com>
 maintainer	Solomon Hykes <solomon@dotcloud.com>
-from	ubuntu:12.10
+# Workaround the upstart issue
+run dpkg-divert --local --rename --add /sbin/initctl
+run ln -s /bin/true /sbin/initctl
+# Enable universe and gophers PPA
+run	DEBIAN_FRONTEND=noninteractive apt-get install -y -q python-software-properties
+run	add-apt-repository "deb http://archive.ubuntu.com/ubuntu $(lsb_release -sc) universe"
+run	add-apt-repository -y ppa:gophers/go/ubuntu
 run	apt-get update
 run	apt-get update
+# Packages required to checkout, build and upload docker
 run	DEBIAN_FRONTEND=noninteractive apt-get install -y -q s3cmd
 run	DEBIAN_FRONTEND=noninteractive apt-get install -y -q s3cmd
 run	DEBIAN_FRONTEND=noninteractive apt-get install -y -q curl
 run	DEBIAN_FRONTEND=noninteractive apt-get install -y -q curl
-# Packages required to checkout and build docker
 run	curl -s -o /go.tar.gz https://go.googlecode.com/files/go1.1.linux-amd64.tar.gz
 run	curl -s -o /go.tar.gz https://go.googlecode.com/files/go1.1.linux-amd64.tar.gz
 run	tar -C /usr/local -xzf /go.tar.gz
 run	tar -C /usr/local -xzf /go.tar.gz
-run	echo "export PATH=$PATH:/usr/local/go/bin" > /.bashrc
-run	echo "export PATH=$PATH:/usr/local/go/bin" > /.bash_profile
+run	echo "export PATH=/usr/local/go/bin:$PATH" > /.bashrc
+run	echo "export PATH=/usr/local/go/bin:$PATH" > /.bash_profile
 run	DEBIAN_FRONTEND=noninteractive apt-get install -y -q git
 run	DEBIAN_FRONTEND=noninteractive apt-get install -y -q git
 run	DEBIAN_FRONTEND=noninteractive apt-get install -y -q build-essential
 run	DEBIAN_FRONTEND=noninteractive apt-get install -y -q build-essential
 # Packages required to build an ubuntu package
 # Packages required to build an ubuntu package
+run	DEBIAN_FRONTEND=noninteractive apt-get install -y -q golang-stable
 run	DEBIAN_FRONTEND=noninteractive apt-get install -y -q debhelper
 run	DEBIAN_FRONTEND=noninteractive apt-get install -y -q debhelper
 run	DEBIAN_FRONTEND=noninteractive apt-get install -y -q autotools-dev
 run	DEBIAN_FRONTEND=noninteractive apt-get install -y -q autotools-dev
-copy	fake_initctl	/usr/local/bin/initctl
 run	apt-get install -y -q devscripts
 run	apt-get install -y -q devscripts
-add	.	/src
+# Copy dockerbuilder files into the container
+add	.       /src
 run	cp /src/dockerbuilder /usr/local/bin/ && chmod +x /usr/local/bin/dockerbuilder
 run	cp /src/dockerbuilder /usr/local/bin/ && chmod +x /usr/local/bin/dockerbuilder
 run	cp /src/s3cfg /.s3cfg
 run	cp /src/s3cfg /.s3cfg
 cmd	["dockerbuilder"]
 cmd	["dockerbuilder"]

+ 2 - 1
hack/dockerbuilder/dockerbuilder

@@ -2,7 +2,7 @@
 set -x
 set -x
 set -e
 set -e
 
 
-export PATH=$PATH:/usr/local/go/bin
+export PATH=/usr/local/go/bin:$PATH
 
 
 PACKAGE=github.com/dotcloud/docker
 PACKAGE=github.com/dotcloud/docker
 
 
@@ -36,5 +36,6 @@ else
 fi
 fi
 
 
 if [ -z "$NO_UBUNTU" ]; then
 if [ -z "$NO_UBUNTU" ]; then
+	export PATH=`echo $PATH | sed 's#/usr/local/go/bin:##g'`
 	(cd packaging/ubuntu && make ubuntu)
 	(cd packaging/ubuntu && make ubuntu)
 fi
 fi

+ 0 - 3
hack/dockerbuilder/fake_initctl

@@ -1,3 +0,0 @@
-#!/bin/sh
-
-echo Whatever you say, man

+ 2 - 0
hack/infrastructure/MAINTAINERS

@@ -0,0 +1,2 @@
+Ken Cochrane <ken@dotcloud.com>
+Jerome Petazzoni <jerome@dotcloud.com>

+ 5 - 0
hack/infrastructure/README.md

@@ -0,0 +1,5 @@
+# Docker project infrastructure
+
+This directory holds all information about the technical infrastructure of the docker project; servers, dns, email, and all the corresponding tools and configuration.
+
+Obviously credentials should not be stored in this repo, but how to obtain and use them should be documented here.

+ 1 - 0
image.go

@@ -27,6 +27,7 @@ type Image struct {
 	DockerVersion   string    `json:"docker_version,omitempty"`
 	DockerVersion   string    `json:"docker_version,omitempty"`
 	Author          string    `json:"author,omitempty"`
 	Author          string    `json:"author,omitempty"`
 	Config          *Config   `json:"config,omitempty"`
 	Config          *Config   `json:"config,omitempty"`
+	Architecture    string    `json:"architecture,omitempty"`
 	graph           *Graph
 	graph           *Graph
 }
 }
 
 

+ 11 - 0
packaging/debian/changelog

@@ -1,3 +1,14 @@
+lxc-docker (0.3.4-1) UNRELEASED; urgency=low
+  - Builder: 'docker build' builds a container, layer by layer, from a source repository containing a Dockerfile
+  - Builder: 'docker build -t FOO' applies the tag FOO to the newly built container.
+  - Runtime: interactive TTYs correctly handle window resize
+  - Runtime: fix how configuration is merged between layers
+  - Remote API: split stdout and stderr on 'docker run'
+  - Remote API: optionally listen on a different IP and port (use at your own risk)
+  - Documentation: improved install instructions.
+
+ -- dotCloud <ops@dotcloud.com>  Thu, 30 May 2013 00:00:00 -0700
+
 lxc-docker (0.3.2-1) UNRELEASED; urgency=low
 lxc-docker (0.3.2-1) UNRELEASED; urgency=low
   - Runtime: Store the actual archive on commit
   - Runtime: Store the actual archive on commit
   - Registry: Improve the checksum process
   - Registry: Improve the checksum process

+ 12 - 0
packaging/ubuntu/changelog

@@ -1,3 +1,15 @@
+lxc-docker (0.3.4-1) precise; urgency=low
+  - Builder: 'docker build' builds a container, layer by layer, from a source repository containing a Dockerfile
+  - Builder: 'docker build -t FOO' applies the tag FOO to the newly built container.
+  - Runtime: interactive TTYs correctly handle window resize
+  - Runtime: fix how configuration is merged between layers
+  - Remote API: split stdout and stderr on 'docker run'
+  - Remote API: optionally listen on a different IP and port (use at your own risk)
+  - Documentation: improved install instructions.
+
+ -- dotCloud <ops@dotcloud.com>  Thu, 30 May 2013 00:00:00 -0700
+
+
 lxc-docker (0.3.3-1) precise; urgency=low
 lxc-docker (0.3.3-1) precise; urgency=low
   - Registry: Fix push regression
   - Registry: Fix push regression
   - Various bugfixes
   - Various bugfixes

+ 1 - 2
packaging/ubuntu/docker.upstart

@@ -5,6 +5,5 @@ stop on starting rc RUNLEVEL=[016]
 respawn
 respawn
 
 
 script
 script
-    # FIXME: docker should not depend on the system having en_US.UTF-8
-    LC_ALL='en_US.UTF-8' /usr/bin/docker -d
+    /usr/bin/docker -d
 end script
 end script

+ 3 - 0
registry/registry.go

@@ -330,6 +330,9 @@ func (r *Registry) PushImageJsonIndex(remote string, imgList []*ImgData, validat
 	if validate {
 	if validate {
 		suffix = "images"
 		suffix = "images"
 	}
 	}
+
+	utils.Debugf("Image list pushed to index:\n%s\n", imgListJson)
+
 	req, err := http.NewRequest("PUT", auth.IndexServerAddress()+"/repositories/"+remote+"/"+suffix, bytes.NewReader(imgListJson))
 	req, err := http.NewRequest("PUT", auth.IndexServerAddress()+"/repositories/"+remote+"/"+suffix, bytes.NewReader(imgListJson))
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err

+ 41 - 14
runtime_test.go

@@ -2,13 +2,15 @@ package docker
 
 
 import (
 import (
 	"fmt"
 	"fmt"
-	"github.com/dotcloud/docker/registry"
 	"github.com/dotcloud/docker/utils"
 	"github.com/dotcloud/docker/utils"
 	"io"
 	"io"
 	"io/ioutil"
 	"io/ioutil"
+	"log"
 	"net"
 	"net"
 	"os"
 	"os"
 	"os/user"
 	"os/user"
+	"strconv"
+	"strings"
 	"sync"
 	"sync"
 	"testing"
 	"testing"
 	"time"
 	"time"
@@ -63,11 +65,10 @@ func init() {
 
 
 	// Create the "Server"
 	// Create the "Server"
 	srv := &Server{
 	srv := &Server{
-		runtime:  runtime,
-		registry: registry.NewRegistry(runtime.root),
+		runtime: runtime,
 	}
 	}
 	// Retrieve the Image
 	// Retrieve the Image
-	if err := srv.ImagePull(unitTestImageName, "", "", os.Stdout, false); err != nil {
+	if err := srv.ImagePull(unitTestImageName, "", "", os.Stdout, utils.NewStreamFormatter(false)); err != nil {
 		panic(err)
 		panic(err)
 	}
 	}
 }
 }
@@ -279,24 +280,50 @@ func TestGet(t *testing.T) {
 
 
 }
 }
 
 
-// Run a container with a TCP port allocated, and test that it can receive connections on localhost
-func TestAllocatePortLocalhost(t *testing.T) {
-	runtime, err := newTestRuntime()
-	if err != nil {
-		t.Fatal(err)
-	}
+func findAvailalblePort(runtime *Runtime, port int) (*Container, error) {
+	strPort := strconv.Itoa(port)
 	container, err := NewBuilder(runtime).Create(&Config{
 	container, err := NewBuilder(runtime).Create(&Config{
 		Image:     GetTestImage(runtime).Id,
 		Image:     GetTestImage(runtime).Id,
-		Cmd:       []string{"sh", "-c", "echo well hello there | nc -l -p 5555"},
-		PortSpecs: []string{"5555"},
+		Cmd:       []string{"sh", "-c", "echo well hello there | nc -l -p " + strPort},
+		PortSpecs: []string{strPort},
 	},
 	},
 	)
 	)
 	if err != nil {
 	if err != nil {
-		t.Fatal(err)
+		return nil, err
 	}
 	}
 	if err := container.Start(); err != nil {
 	if err := container.Start(); err != nil {
+		if strings.Contains(err.Error(), "address already in use") {
+			return nil, nil
+		}
+		return nil, err
+	}
+	return container, nil
+}
+
+// Run a container with a TCP port allocated, and test that it can receive connections on localhost
+func TestAllocatePortLocalhost(t *testing.T) {
+	runtime, err := newTestRuntime()
+	if err != nil {
 		t.Fatal(err)
 		t.Fatal(err)
 	}
 	}
+	port := 5554
+
+	var container *Container
+	for {
+		port += 1
+		log.Println("Trying port", port)
+		t.Log("Trying port", port)
+		container, err = findAvailalblePort(runtime, port)
+		if container != nil {
+			break
+		}
+		if err != nil {
+			t.Fatal(err)
+		}
+		log.Println("Port", port, "already in use")
+		t.Log("Port", port, "already in use")
+	}
+
 	defer container.Kill()
 	defer container.Kill()
 
 
 	setTimeout(t, "Waiting for the container to be started timed out", 2*time.Second, func() {
 	setTimeout(t, "Waiting for the container to be started timed out", 2*time.Second, func() {
@@ -310,7 +337,7 @@ func TestAllocatePortLocalhost(t *testing.T) {
 
 
 	conn, err := net.Dial("tcp",
 	conn, err := net.Dial("tcp",
 		fmt.Sprintf(
 		fmt.Sprintf(
-			"localhost:%s", container.NetworkSettings.PortMapping["5555"],
+			"localhost:%s", container.NetworkSettings.PortMapping[strconv.Itoa(port)],
 		),
 		),
 	)
 	)
 	if err != nil {
 	if err != nil {

+ 56 - 53
server.go

@@ -50,7 +50,8 @@ func (srv *Server) ContainerExport(name string, out io.Writer) error {
 }
 }
 
 
 func (srv *Server) ImagesSearch(term string) ([]ApiSearch, error) {
 func (srv *Server) ImagesSearch(term string) ([]ApiSearch, error) {
-	results, err := srv.registry.SearchRepositories(term)
+
+	results, err := registry.NewRegistry(srv.runtime.root).SearchRepositories(term)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
@@ -68,7 +69,7 @@ func (srv *Server) ImagesSearch(term string) ([]ApiSearch, error) {
 	return outs, nil
 	return outs, nil
 }
 }
 
 
-func (srv *Server) ImageInsert(name, url, path string, out io.Writer) (string, error) {
+func (srv *Server) ImageInsert(name, url, path string, out io.Writer, sf *utils.StreamFormatter) (string, error) {
 	out = utils.NewWriteFlusher(out)
 	out = utils.NewWriteFlusher(out)
 	img, err := srv.runtime.repositories.LookupImage(name)
 	img, err := srv.runtime.repositories.LookupImage(name)
 	if err != nil {
 	if err != nil {
@@ -92,7 +93,7 @@ func (srv *Server) ImageInsert(name, url, path string, out io.Writer) (string, e
 		return "", err
 		return "", err
 	}
 	}
 
 
-	if err := c.Inject(utils.ProgressReader(file.Body, int(file.ContentLength), out, "Downloading %v/%v (%v)\r", false), path); err != nil {
+	if err := c.Inject(utils.ProgressReader(file.Body, int(file.ContentLength), out, sf.FormatProgress("Downloading", "%v/%v (%v)"), sf), path); err != nil {
 		return "", err
 		return "", err
 	}
 	}
 	// FIXME: Handle custom repo, tag comment, author
 	// FIXME: Handle custom repo, tag comment, author
@@ -100,7 +101,7 @@ func (srv *Server) ImageInsert(name, url, path string, out io.Writer) (string, e
 	if err != nil {
 	if err != nil {
 		return "", err
 		return "", err
 	}
 	}
-	fmt.Fprintf(out, "%s\n", img.Id)
+	out.Write(sf.FormatStatus(img.Id))
 	return img.ShortId(), nil
 	return img.ShortId(), nil
 }
 }
 
 
@@ -292,8 +293,8 @@ func (srv *Server) ContainerTag(name, repo, tag string, force bool) error {
 	return nil
 	return nil
 }
 }
 
 
-func (srv *Server) pullImage(out io.Writer, imgId, registry string, token []string, json bool) error {
-	history, err := srv.registry.GetRemoteHistory(imgId, registry, token)
+func (srv *Server) pullImage(r *registry.Registry, out io.Writer, imgId, endpoint string, token []string, sf *utils.StreamFormatter) error {
+	history, err := r.GetRemoteHistory(imgId, endpoint, token)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -302,8 +303,8 @@ func (srv *Server) pullImage(out io.Writer, imgId, registry string, token []stri
 	// FIXME: Launch the getRemoteImage() in goroutines
 	// FIXME: Launch the getRemoteImage() in goroutines
 	for _, id := range history {
 	for _, id := range history {
 		if !srv.runtime.graph.Exists(id) {
 		if !srv.runtime.graph.Exists(id) {
-			fmt.Fprintf(out, utils.FormatStatus("Pulling %s metadata", json), id)
-			imgJson, err := srv.registry.GetRemoteImageJson(id, registry, token)
+			out.Write(sf.FormatStatus("Pulling %s metadata", id))
+			imgJson, err := r.GetRemoteImageJson(id, endpoint, token)
 			if err != nil {
 			if err != nil {
 				// FIXME: Keep goging in case of error?
 				// FIXME: Keep goging in case of error?
 				return err
 				return err
@@ -314,12 +315,12 @@ func (srv *Server) pullImage(out io.Writer, imgId, registry string, token []stri
 			}
 			}
 
 
 			// Get the layer
 			// Get the layer
-			fmt.Fprintf(out, utils.FormatStatus("Pulling %s fs layer", json), id)
-			layer, contentLength, err := srv.registry.GetRemoteImageLayer(img.Id, registry, token)
+			out.Write(sf.FormatStatus("Pulling %s fs layer", id))
+			layer, contentLength, err := r.GetRemoteImageLayer(img.Id, endpoint, token)
 			if err != nil {
 			if err != nil {
 				return err
 				return err
 			}
 			}
-			if err := srv.runtime.graph.Register(utils.ProgressReader(layer, contentLength, out, utils.FormatProgress("%v/%v (%v)", json), json), false, img); err != nil {
+			if err := srv.runtime.graph.Register(utils.ProgressReader(layer, contentLength, out, sf.FormatProgress("Downloading", "%v/%v (%v)"), sf), false, img); err != nil {
 				return err
 				return err
 			}
 			}
 		}
 		}
@@ -327,9 +328,9 @@ func (srv *Server) pullImage(out io.Writer, imgId, registry string, token []stri
 	return nil
 	return nil
 }
 }
 
 
-func (srv *Server) pullRepository(out io.Writer, remote, askedTag string, json bool) error {
-	fmt.Fprintf(out, utils.FormatStatus("Pulling repository %s from %s", json), remote, auth.IndexServerAddress())
-	repoData, err := srv.registry.GetRepositoryData(remote)
+func (srv *Server) pullRepository(r *registry.Registry, out io.Writer, remote, askedTag string, sf *utils.StreamFormatter) error {
+	out.Write(sf.FormatStatus("Pulling repository %s from %s", remote, auth.IndexServerAddress()))
+	repoData, err := r.GetRepositoryData(remote)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -341,7 +342,7 @@ func (srv *Server) pullRepository(out io.Writer, remote, askedTag string, json b
 	}
 	}
 
 
 	utils.Debugf("Retrieving the tag list")
 	utils.Debugf("Retrieving the tag list")
-	tagsList, err := srv.registry.GetRemoteTags(repoData.Endpoints, remote, repoData.Tokens)
+	tagsList, err := r.GetRemoteTags(repoData.Endpoints, remote, repoData.Tokens)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -365,11 +366,11 @@ func (srv *Server) pullRepository(out io.Writer, remote, askedTag string, json b
 			utils.Debugf("(%s) does not match %s (id: %s), skipping", img.Tag, askedTag, img.Id)
 			utils.Debugf("(%s) does not match %s (id: %s), skipping", img.Tag, askedTag, img.Id)
 			continue
 			continue
 		}
 		}
-		fmt.Fprintf(out, utils.FormatStatus("Pulling image %s (%s) from %s", json), img.Id, img.Tag, remote)
+		out.Write(sf.FormatStatus("Pulling image %s (%s) from %s", img.Id, img.Tag, remote))
 		success := false
 		success := false
 		for _, ep := range repoData.Endpoints {
 		for _, ep := range repoData.Endpoints {
-			if err := srv.pullImage(out, img.Id, "https://"+ep+"/v1", repoData.Tokens, json); err != nil {
-				fmt.Fprintf(out, utils.FormatStatus("Error while retrieving image for tag: %s (%s); checking next endpoint\n", json), askedTag, err)
+			if err := srv.pullImage(r, out, img.Id, "https://"+ep+"/v1", repoData.Tokens, sf); err != nil {
+				out.Write(sf.FormatStatus("Error while retrieving image for tag: %s (%s); checking next endpoint", askedTag, err))
 				continue
 				continue
 			}
 			}
 			success = true
 			success = true
@@ -394,16 +395,17 @@ func (srv *Server) pullRepository(out io.Writer, remote, askedTag string, json b
 	return nil
 	return nil
 }
 }
 
 
-func (srv *Server) ImagePull(name, tag, registry string, out io.Writer, json bool) error {
+func (srv *Server) ImagePull(name, tag, endpoint string, out io.Writer, sf *utils.StreamFormatter) error {
+	r := registry.NewRegistry(srv.runtime.root)
 	out = utils.NewWriteFlusher(out)
 	out = utils.NewWriteFlusher(out)
-	if registry != "" {
-		if err := srv.pullImage(out, name, registry, nil, json); err != nil {
+	if endpoint != "" {
+		if err := srv.pullImage(r, out, name, endpoint, nil, sf); err != nil {
 			return err
 			return err
 		}
 		}
 		return nil
 		return nil
 	}
 	}
 
 
-	if err := srv.pullRepository(out, name, tag, json); err != nil {
+	if err := srv.pullRepository(r, out, name, tag, sf); err != nil {
 		return err
 		return err
 	}
 	}
 
 
@@ -476,53 +478,52 @@ func (srv *Server) getImageList(localRepo map[string]string) ([]*registry.ImgDat
 	return imgList, nil
 	return imgList, nil
 }
 }
 
 
-func (srv *Server) pushRepository(out io.Writer, name string, localRepo map[string]string) error {
+func (srv *Server) pushRepository(r *registry.Registry, out io.Writer, name string, localRepo map[string]string, sf *utils.StreamFormatter) error {
 	out = utils.NewWriteFlusher(out)
 	out = utils.NewWriteFlusher(out)
-	fmt.Fprintf(out, "Processing checksums\n")
+	out.Write(sf.FormatStatus("Processing checksums"))
 	imgList, err := srv.getImageList(localRepo)
 	imgList, err := srv.getImageList(localRepo)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
-	fmt.Fprintf(out, "Sending image list\n")
+	out.Write(sf.FormatStatus("Sending image list"))
 
 
-	repoData, err := srv.registry.PushImageJsonIndex(name, imgList, false)
+	repoData, err := r.PushImageJsonIndex(name, imgList, false)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 
 
-	// FIXME: Send only needed images
 	for _, ep := range repoData.Endpoints {
 	for _, ep := range repoData.Endpoints {
-		fmt.Fprintf(out, "Pushing repository %s to %s (%d tags)\r\n", name, ep, len(localRepo))
+		out.Write(sf.FormatStatus("Pushing repository %s to %s (%d tags)", name, ep, len(localRepo)))
 		// For each image within the repo, push them
 		// For each image within the repo, push them
 		for _, elem := range imgList {
 		for _, elem := range imgList {
 			if _, exists := repoData.ImgList[elem.Id]; exists {
 			if _, exists := repoData.ImgList[elem.Id]; exists {
-				fmt.Fprintf(out, "Image %s already on registry, skipping\n", name)
+				out.Write(sf.FormatStatus("Image %s already on registry, skipping", name))
 				continue
 				continue
 			}
 			}
-			if err := srv.pushImage(out, name, elem.Id, ep, repoData.Tokens); err != nil {
+			if err := srv.pushImage(r, out, name, elem.Id, ep, repoData.Tokens, sf); err != nil {
 				// FIXME: Continue on error?
 				// FIXME: Continue on error?
 				return err
 				return err
 			}
 			}
-			fmt.Fprintf(out, "Pushing tags for rev [%s] on {%s}\n", elem.Id, ep+"/users/"+name+"/"+elem.Tag)
-			if err := srv.registry.PushRegistryTag(name, elem.Id, elem.Tag, ep, repoData.Tokens); err != nil {
+			out.Write(sf.FormatStatus("Pushing tags for rev [%s] on {%s}", elem.Id, ep+"/users/"+name+"/"+elem.Tag))
+			if err := r.PushRegistryTag(name, elem.Id, elem.Tag, ep, repoData.Tokens); err != nil {
 				return err
 				return err
 			}
 			}
 		}
 		}
 	}
 	}
 
 
-	if _, err := srv.registry.PushImageJsonIndex(name, imgList, true); err != nil {
+	if _, err := r.PushImageJsonIndex(name, imgList, true); err != nil {
 		return err
 		return err
 	}
 	}
 	return nil
 	return nil
 }
 }
 
 
-func (srv *Server) pushImage(out io.Writer, remote, imgId, ep string, token []string) error {
+func (srv *Server) pushImage(r *registry.Registry, out io.Writer, remote, imgId, ep string, token []string, sf *utils.StreamFormatter) error {
 	out = utils.NewWriteFlusher(out)
 	out = utils.NewWriteFlusher(out)
 	jsonRaw, err := ioutil.ReadFile(path.Join(srv.runtime.graph.Root, imgId, "json"))
 	jsonRaw, err := ioutil.ReadFile(path.Join(srv.runtime.graph.Root, imgId, "json"))
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf("Error while retreiving the path for {%s}: %s", imgId, err)
 		return fmt.Errorf("Error while retreiving the path for {%s}: %s", imgId, err)
 	}
 	}
-	fmt.Fprintf(out, "Pushing %s\r\n", imgId)
+	out.Write(sf.FormatStatus("Pushing %s", imgId))
 
 
 	// Make sure we have the image's checksum
 	// Make sure we have the image's checksum
 	checksum, err := srv.getChecksum(imgId)
 	checksum, err := srv.getChecksum(imgId)
@@ -535,9 +536,9 @@ func (srv *Server) pushImage(out io.Writer, remote, imgId, ep string, token []st
 	}
 	}
 
 
 	// Send the json
 	// Send the json
-	if err := srv.registry.PushImageJsonRegistry(imgData, jsonRaw, ep, token); err != nil {
+	if err := r.PushImageJsonRegistry(imgData, jsonRaw, ep, token); err != nil {
 		if err == registry.ErrAlreadyExists {
 		if err == registry.ErrAlreadyExists {
-			fmt.Fprintf(out, "Image %s already uploaded ; skipping\n", imgData.Id)
+			out.Write(sf.FormatStatus("Image %s already uploaded ; skipping", imgData.Id))
 			return nil
 			return nil
 		}
 		}
 		return err
 		return err
@@ -570,20 +571,22 @@ func (srv *Server) pushImage(out io.Writer, remote, imgId, ep string, token []st
 	}
 	}
 
 
 	// Send the layer
 	// Send the layer
-	if err := srv.registry.PushImageLayerRegistry(imgData.Id, utils.ProgressReader(layerData, int(layerData.Size), out, "", false), ep, token); err != nil {
+	if err := r.PushImageLayerRegistry(imgData.Id, utils.ProgressReader(layerData, int(layerData.Size), out, sf.FormatProgress("", "%v/%v (%v)"), sf), ep, token); err != nil {
 		return err
 		return err
 	}
 	}
 	return nil
 	return nil
 }
 }
 
 
-func (srv *Server) ImagePush(name, registry string, out io.Writer) error {
+func (srv *Server) ImagePush(name, endpoint string, out io.Writer, sf *utils.StreamFormatter) error {
 	out = utils.NewWriteFlusher(out)
 	out = utils.NewWriteFlusher(out)
 	img, err := srv.runtime.graph.Get(name)
 	img, err := srv.runtime.graph.Get(name)
+	r := registry.NewRegistry(srv.runtime.root)
+
 	if err != nil {
 	if err != nil {
-		fmt.Fprintf(out, "The push refers to a repository [%s] (len: %d)\n", name, len(srv.runtime.repositories.Repositories[name]))
+		out.Write(sf.FormatStatus("The push refers to a repository [%s] (len: %d)", name, len(srv.runtime.repositories.Repositories[name])))
 		// If it fails, try to get the repository
 		// If it fails, try to get the repository
 		if localRepo, exists := srv.runtime.repositories.Repositories[name]; exists {
 		if localRepo, exists := srv.runtime.repositories.Repositories[name]; exists {
-			if err := srv.pushRepository(out, name, localRepo); err != nil {
+			if err := srv.pushRepository(r, out, name, localRepo, sf); err != nil {
 				return err
 				return err
 			}
 			}
 			return nil
 			return nil
@@ -591,14 +594,14 @@ func (srv *Server) ImagePush(name, registry string, out io.Writer) error {
 
 
 		return err
 		return err
 	}
 	}
-	fmt.Fprintf(out, "The push refers to an image: [%s]\n", name)
-	if err := srv.pushImage(out, name, img.Id, registry, nil); err != nil {
+	out.Write(sf.FormatStatus("The push refers to an image: [%s]", name))
+	if err := srv.pushImage(r, out, name, img.Id, endpoint, nil, sf); err != nil {
 		return err
 		return err
 	}
 	}
 	return nil
 	return nil
 }
 }
 
 
-func (srv *Server) ImageImport(src, repo, tag string, in io.Reader, out io.Writer) error {
+func (srv *Server) ImageImport(src, repo, tag string, in io.Reader, out io.Writer, sf *utils.StreamFormatter) error {
 	var archive io.Reader
 	var archive io.Reader
 	var resp *http.Response
 	var resp *http.Response
 
 
@@ -607,21 +610,21 @@ func (srv *Server) ImageImport(src, repo, tag string, in io.Reader, out io.Write
 	} else {
 	} else {
 		u, err := url.Parse(src)
 		u, err := url.Parse(src)
 		if err != nil {
 		if err != nil {
-			fmt.Fprintf(out, "Error: %s\n", err)
+			return err
 		}
 		}
 		if u.Scheme == "" {
 		if u.Scheme == "" {
 			u.Scheme = "http"
 			u.Scheme = "http"
 			u.Host = src
 			u.Host = src
 			u.Path = ""
 			u.Path = ""
 		}
 		}
-		fmt.Fprintf(out, "Downloading from %s\n", u)
+		out.Write(sf.FormatStatus("Downloading from %s", u))
 		// Download with curl (pretty progress bar)
 		// Download with curl (pretty progress bar)
 		// If curl is not available, fallback to http.Get()
 		// If curl is not available, fallback to http.Get()
 		resp, err = utils.Download(u.String(), out)
 		resp, err = utils.Download(u.String(), out)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
-		archive = utils.ProgressReader(resp.Body, int(resp.ContentLength), out, "Importing %v/%v (%v)\r", false)
+		archive = utils.ProgressReader(resp.Body, int(resp.ContentLength), out, sf.FormatProgress("Importing", "%v/%v (%v)"), sf)
 	}
 	}
 	img, err := srv.runtime.graph.Create(archive, nil, "Imported from "+src, "", nil)
 	img, err := srv.runtime.graph.Create(archive, nil, "Imported from "+src, "", nil)
 	if err != nil {
 	if err != nil {
@@ -633,7 +636,7 @@ func (srv *Server) ImageImport(src, repo, tag string, in io.Reader, out io.Write
 			return err
 			return err
 		}
 		}
 	}
 	}
-	fmt.Fprintf(out, "%s\n", img.ShortId())
+	out.Write(sf.FormatStatus(img.ShortId()))
 	return nil
 	return nil
 }
 }
 
 
@@ -883,7 +886,6 @@ func (srv *Server) ContainerAttach(name string, logs, stream, stdin, stdout, std
 	if container == nil {
 	if container == nil {
 		return fmt.Errorf("No such container: %s", name)
 		return fmt.Errorf("No such container: %s", name)
 	}
 	}
-
 	//logs
 	//logs
 	if logs {
 	if logs {
 		if stdout {
 		if stdout {
@@ -909,6 +911,9 @@ func (srv *Server) ContainerAttach(name string, logs, stream, stdin, stdout, std
 		if container.State.Ghost {
 		if container.State.Ghost {
 			return fmt.Errorf("Impossible to attach to a ghost container")
 			return fmt.Errorf("Impossible to attach to a ghost container")
 		}
 		}
+		if !container.State.Running {
+			return fmt.Errorf("Impossible to attach to a stopped container, start it first")
+		}
 
 
 		var (
 		var (
 			cStdin           io.ReadCloser
 			cStdin           io.ReadCloser
@@ -967,14 +972,12 @@ func NewServer(autoRestart bool) (*Server, error) {
 		return nil, err
 		return nil, err
 	}
 	}
 	srv := &Server{
 	srv := &Server{
-		runtime:  runtime,
-		registry: registry.NewRegistry(runtime.root),
+		runtime: runtime,
 	}
 	}
 	runtime.srv = srv
 	runtime.srv = srv
 	return srv, nil
 	return srv, nil
 }
 }
 
 
 type Server struct {
 type Server struct {
-	runtime  *Runtime
-	registry *registry.Registry
+	runtime *Runtime
 }
 }

+ 2 - 0
term/MAINTAINERS

@@ -0,0 +1,2 @@
+Guillaume Charmes <guillaume@dotcloud.com>
+Solomon Hykes <solomon@dotcloud.com>

+ 57 - 14
utils/utils.go

@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"bytes"
 	"crypto/sha256"
 	"crypto/sha256"
 	"encoding/hex"
 	"encoding/hex"
+	"encoding/json"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
 	"index/suffixarray"
 	"index/suffixarray"
@@ -69,7 +70,7 @@ type progressReader struct {
 	readProgress int           // How much has been read so far (bytes)
 	readProgress int           // How much has been read so far (bytes)
 	lastUpdate   int           // How many bytes read at least update
 	lastUpdate   int           // How many bytes read at least update
 	template     string        // Template to print. Default "%v/%v (%v)"
 	template     string        // Template to print. Default "%v/%v (%v)"
-	json         bool
+	sf *StreamFormatter
 }
 }
 
 
 func (r *progressReader) Read(p []byte) (n int, err error) {
 func (r *progressReader) Read(p []byte) (n int, err error) {
@@ -93,7 +94,7 @@ func (r *progressReader) Read(p []byte) (n int, err error) {
 	}
 	}
 	// Send newline when complete
 	// Send newline when complete
 	if err != nil {
 	if err != nil {
-		fmt.Fprintf(r.output, FormatStatus("", r.json))
+		r.output.Write(r.sf.FormatStatus(""))
 	}
 	}
 
 
 	return read, err
 	return read, err
@@ -101,11 +102,12 @@ func (r *progressReader) Read(p []byte) (n int, err error) {
 func (r *progressReader) Close() error {
 func (r *progressReader) Close() error {
 	return io.ReadCloser(r.reader).Close()
 	return io.ReadCloser(r.reader).Close()
 }
 }
-func ProgressReader(r io.ReadCloser, size int, output io.Writer, template string, json bool) *progressReader {
-	if template == "" {
-		template = "%v/%v (%v)\r"
+func ProgressReader(r io.ReadCloser, size int, output io.Writer, template []byte, sf *StreamFormatter) *progressReader {
+      	tpl := string(template)
+	if tpl == "" {
+		tpl = string(sf.FormatProgress("", "%v/%v (%v)"))
 	}
 	}
-	return &progressReader{r, NewWriteFlusher(output), size, 0, 0, template, json}
+	return &progressReader{r, NewWriteFlusher(output), size, 0, 0, tpl, sf}
 }
 }
 
 
 // HumanDuration returns a human-readable approximation of a duration
 // HumanDuration returns a human-readable approximation of a duration
@@ -564,16 +566,57 @@ func NewWriteFlusher(w io.Writer) *WriteFlusher {
 	return &WriteFlusher{w: w, flusher: flusher}
 	return &WriteFlusher{w: w, flusher: flusher}
 }
 }
 
 
-func FormatStatus(str string, json bool) string {
-	if json {
-		return "{\"status\" : \"" + str + "\"}"
+type JsonMessage struct {
+	Status   string `json:"status,omitempty"`
+	Progress string `json:"progress,omitempty"`
+	Error    string `json:"error,omitempty"`
+}
+
+type StreamFormatter struct {
+	json bool
+	used bool
+}
+
+func NewStreamFormatter(json bool) *StreamFormatter {
+	return &StreamFormatter{json, false}
+}
+
+func (sf *StreamFormatter) FormatStatus(format string, a ...interface{}) []byte {
+	sf.used = true
+	str := fmt.Sprintf(format, a...)
+	if sf.json {
+		b, err := json.Marshal(&JsonMessage{Status:str});
+		if err != nil {
+			return sf.FormatError(err)
+		}
+		return b
 	}
 	}
-	return str + "\r\n"
+	return []byte(str + "\r\n")
 }
 }
 
 
-func FormatProgress(str string, json bool) string {
-	if json {
-		return "{\"progress\" : \"" + str + "\"}"
+func (sf *StreamFormatter) FormatError(err error) []byte {
+	sf.used = true
+	if sf.json {
+		if b, err := json.Marshal(&JsonMessage{Error:err.Error()}); err == nil {
+			return b
+		}
+		return []byte("{\"error\":\"format error\"}")
 	}
 	}
-	return "Downloading " + str + "\r"
+	return []byte("Error: " + err.Error() + "\r\n")
+}
+
+func (sf *StreamFormatter) FormatProgress(action, str string) []byte {
+	sf.used = true
+	if sf.json {
+		b, err := json.Marshal(&JsonMessage{Progress:str})
+		if err != nil {
+                        return nil
+                }
+		return b
+	}
+	return []byte(action + " " + str + "\r")
+}
+
+func (sf *StreamFormatter) Used() bool {
+	return sf.used
 }
 }