Victor Vieux il y a 12 ans
Parent
commit
ca39f15fa3
43 fichiers modifiés avec 900 ajouts et 309 suppressions
  1. 1 0
      AUTHORS
  2. 3 3
      Makefile
  3. 65 54
      api.go
  4. 9 8
      api_params.go
  5. 38 0
      api_test.go
  6. 55 50
      auth/auth.go
  7. 8 6
      auth/auth_test.go
  8. 20 1
      buildfile.go
  9. 96 23
      buildfile_test.go
  10. 60 34
      commands.go
  11. 1 1
      commands_test.go
  12. 60 40
      container.go
  13. 39 0
      container_test.go
  14. 1 1
      docker/docker.go
  15. 46 17
      docs/sources/api/docker_remote_api.rst
  16. 7 2
      docs/sources/api/docker_remote_api_v1.0.rst
  17. 4 0
      docs/sources/api/docker_remote_api_v1.1.rst
  18. 5 0
      docs/sources/api/docker_remote_api_v1.2.rst
  19. 35 0
      docs/sources/api/docker_remote_api_v1.3.rst
  20. 1 1
      docs/sources/api/index_api.rst
  21. 3 2
      docs/sources/api/registry_index_spec.rst
  22. 1 0
      docs/sources/commandline/command/run.rst
  23. 3 1
      docs/sources/commandline/command/stop.rst
  24. 2 1
      docs/sources/commandline/index.rst
  25. 2 0
      docs/sources/contributing/devenvironment.rst
  26. 0 2
      docs/sources/index.rst
  27. 2 2
      docs/sources/use/basics.rst
  28. 25 17
      docs/sources/use/builder.rst
  29. 1 1
      docs/sources/use/workingwithrepository.rst
  30. 6 7
      docs/theme/docker/layout.html
  31. 5 5
      docs/theme/docker/static/css/main.css
  32. 3 7
      docs/theme/docker/static/css/main.less
  33. BIN
      docs/theme/docker/static/img/docker-top-logo.png
  34. BIN
      docs/theme/docker/static/img/external-link-icon.png
  35. 91 0
      hack/bootcamp/README.md
  36. 5 0
      lxc_template.go
  37. 49 3
      network.go
  38. 6 6
      runtime_test.go
  39. 36 9
      server.go
  40. 40 0
      server_test.go
  41. 1 1
      utils.go
  42. 19 0
      utils/utils.go
  43. 46 4
      utils_test.go

+ 1 - 0
AUTHORS

@@ -76,6 +76,7 @@ Shawn Siefkas <shawn.siefkas@meredith.com>
 Silas Sewell <silas@sewell.org>
 Silas Sewell <silas@sewell.org>
 Solomon Hykes <solomon@dotcloud.com>
 Solomon Hykes <solomon@dotcloud.com>
 Sridhar Ratnakumar <sridharr@activestate.com>
 Sridhar Ratnakumar <sridharr@activestate.com>
+Stefan Praszalowicz <stefan@greplin.com>
 Thatcher Peskens <thatcher@dotcloud.com>
 Thatcher Peskens <thatcher@dotcloud.com>
 Thomas Bikeev <thomas.bikeev@mac.com>
 Thomas Bikeev <thomas.bikeev@mac.com>
 Thomas Hansen <thomas.hansen@gmail.com>
 Thomas Hansen <thomas.hansen@gmail.com>

+ 3 - 3
Makefile

@@ -11,7 +11,7 @@ BUILD_DIR := $(CURDIR)/.gopath
 GOPATH ?= $(BUILD_DIR)
 GOPATH ?= $(BUILD_DIR)
 export GOPATH
 export GOPATH
 
 
-GO_OPTIONS ?=
+GO_OPTIONS ?= -a -ldflags='-w -d'
 ifeq ($(VERBOSE), 1)
 ifeq ($(VERBOSE), 1)
 GO_OPTIONS += -v
 GO_OPTIONS += -v
 endif
 endif
@@ -80,10 +80,10 @@ test:
 	tar --exclude=${BUILD_SRC} -cz . | tar -xz -C ${BUILD_PATH}
 	tar --exclude=${BUILD_SRC} -cz . | tar -xz -C ${BUILD_PATH}
 	GOPATH=${CURDIR}/${BUILD_SRC} go get -d
 	GOPATH=${CURDIR}/${BUILD_SRC} go get -d
 	# Do the test
 	# Do the test
-	sudo -E GOPATH=${CURDIR}/${BUILD_SRC} go test ${GO_OPTIONS}
+	sudo -E GOPATH=${CURDIR}/${BUILD_SRC} CGO_ENABLED=0 go test ${GO_OPTIONS}
 
 
 testall: all
 testall: all
-	@(cd $(DOCKER_DIR); sudo -E go test ./... $(GO_OPTIONS))
+	@(cd $(DOCKER_DIR); CGO_ENABLED=0 sudo -E go test ./... $(GO_OPTIONS))
 
 
 fmt:
 fmt:
 	@gofmt -s -l -w .
 	@gofmt -s -l -w .

+ 65 - 54
api.go

@@ -81,54 +81,15 @@ func getBoolParam(value string) (bool, error) {
 	return ret, nil
 	return ret, nil
 }
 }
 
 
-func getAuth(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
-	if version > 1.1 {
-		w.WriteHeader(http.StatusNotFound)
-		return nil
-	}
-	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 {
-		return err
-	}
-	writeJSON(w, b)
-	return nil
-}
-
 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 {
 	authConfig := &auth.AuthConfig{}
 	authConfig := &auth.AuthConfig{}
 	err := json.NewDecoder(r.Body).Decode(authConfig)
 	err := json.NewDecoder(r.Body).Decode(authConfig)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
-	status := ""
-	if version > 1.1 {
-		status, err = auth.Login(authConfig, false)
-		if err != nil {
-			return err
-		}
-	} else {
-		localAuthConfig, err := auth.LoadConfig(srv.runtime.root)
-		if err != nil {
-			if err != auth.ErrConfigFileMissing {
-				return err
-			}
-		}
-		if authConfig.Username == localAuthConfig.Username {
-			authConfig.Password = localAuthConfig.Password
-		}
-
-		newAuthConfig := auth.NewAuthConfig(authConfig.Username, authConfig.Password, authConfig.Email, srv.runtime.root)
-		status, err = auth.Login(newAuthConfig, true)
-		if err != nil {
-			return err
-		}
+	status, err := auth.Login(authConfig)
+	if err != nil {
+		return err
 	}
 	}
 	if status != "" {
 	if status != "" {
 		b, err := json.Marshal(&APIAuth{Status: status})
 		b, err := json.Marshal(&APIAuth{Status: status})
@@ -217,6 +178,64 @@ func getInfo(srv *Server, version float64, w http.ResponseWriter, r *http.Reques
 	return nil
 	return nil
 }
 }
 
 
+func getEvents(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
+	sendEvent := func(wf *utils.WriteFlusher, event *utils.JSONMessage) error {
+		b, err := json.Marshal(event)
+		if err != nil {
+			return fmt.Errorf("JSON error")
+		}
+		_, err = wf.Write(b)
+		if err != nil {
+			// On error, evict the listener
+			utils.Debugf("%s", err)
+			srv.Lock()
+			delete(srv.listeners, r.RemoteAddr)
+			srv.Unlock()
+			return err
+		}
+		return nil
+	}
+
+	if err := parseForm(r); err != nil {
+		return err
+	}
+	listener := make(chan utils.JSONMessage)
+	srv.Lock()
+	srv.listeners[r.RemoteAddr] = listener
+	srv.Unlock()
+	since, err := strconv.ParseInt(r.Form.Get("since"), 10, 0)
+	if err != nil {
+		since = 0
+	}
+	w.Header().Set("Content-Type", "application/json")
+	wf := utils.NewWriteFlusher(w)
+	if since != 0 {
+		// If since, send previous events that happened after the timestamp
+		for _, event := range srv.events {
+			if event.Time >= since {
+				err := sendEvent(wf, &event)
+				if err != nil && err.Error() == "JSON error" {
+					continue
+				}
+				if err != nil {
+					return err
+				}
+			}
+		}
+	}
+	for {
+		event := <-listener
+		err := sendEvent(wf, &event)
+		if err != nil && err.Error() == "JSON error" {
+			continue
+		}
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
 func getImagesHistory(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
 func getImagesHistory(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
 	if vars == nil {
 	if vars == nil {
 		return fmt.Errorf("Missing parameter")
 		return fmt.Errorf("Missing parameter")
@@ -429,16 +448,8 @@ func postImagesInsert(srv *Server, version float64, w http.ResponseWriter, r *ht
 
 
 func postImagesPush(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
 func postImagesPush(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
 	authConfig := &auth.AuthConfig{}
 	authConfig := &auth.AuthConfig{}
-	if version > 1.1 {
-		if err := json.NewDecoder(r.Body).Decode(authConfig); err != nil {
-			return err
-		}
-	} else {
-		localAuthConfig, err := auth.LoadConfig(srv.runtime.root)
-		if err != nil && err != auth.ErrConfigFileMissing {
-			return err
-		}
-		authConfig = localAuthConfig
+	if err := json.NewDecoder(r.Body).Decode(authConfig); err != nil {
+		return err
 	}
 	}
 	if err := parseForm(r); err != nil {
 	if err := parseForm(r); err != nil {
 		return err
 		return err
@@ -854,9 +865,9 @@ func createRouter(srv *Server, logging bool) (*mux.Router, error) {
 
 
 	m := map[string]map[string]func(*Server, float64, http.ResponseWriter, *http.Request, map[string]string) error{
 	m := map[string]map[string]func(*Server, float64, http.ResponseWriter, *http.Request, map[string]string) error{
 		"GET": {
 		"GET": {
-			"/auth":                         getAuth,
-			"/version":                      getVersion,
+			"/events":                       getEvents,
 			"/info":                         getInfo,
 			"/info":                         getInfo,
+			"/version":                      getVersion,
 			"/images/json":                  getImagesJSON,
 			"/images/json":                  getImagesJSON,
 			"/images/viz":                   getImagesViz,
 			"/images/viz":                   getImagesViz,
 			"/images/search":                getImagesSearch,
 			"/images/search":                getImagesSearch,

+ 9 - 8
api_params.go

@@ -17,14 +17,15 @@ type APIImages struct {
 }
 }
 
 
 type APIInfo struct {
 type APIInfo struct {
-	Debug       bool
-	Containers  int
-	Images      int
-	NFd         int    `json:",omitempty"`
-	NGoroutines int    `json:",omitempty"`
-	MemoryLimit bool   `json:",omitempty"`
-	SwapLimit   bool   `json:",omitempty"`
-	LXCVersion  string `json:",omitempty"`
+	Debug           bool
+	Containers      int
+	Images          int
+	NFd             int    `json:",omitempty"`
+	NGoroutines     int    `json:",omitempty"`
+	MemoryLimit     bool   `json:",omitempty"`
+	SwapLimit       bool   `json:",omitempty"`
+	LXCVersion      string `json:",omitempty"`
+	NEventsListener int    `json:",omitempty"`
 }
 }
 
 
 type APITop struct {
 type APITop struct {

+ 38 - 0
api_test.go

@@ -89,6 +89,44 @@ func TestGetInfo(t *testing.T) {
 	}
 	}
 }
 }
 
 
+func TestGetEvents(t *testing.T) {
+	runtime := mkRuntime(t)
+	srv := &Server{
+		runtime:   runtime,
+		events:    make([]utils.JSONMessage, 0, 64),
+		listeners: make(map[string]chan utils.JSONMessage),
+	}
+
+	srv.LogEvent("fakeaction", "fakeid")
+	srv.LogEvent("fakeaction2", "fakeid")
+
+	req, err := http.NewRequest("GET", "/events?since=1", nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	r := httptest.NewRecorder()
+	setTimeout(t, "", 500*time.Millisecond, func() {
+		if err := getEvents(srv, APIVERSION, r, req, nil); err != nil {
+			t.Fatal(err)
+		}
+	})
+
+	dec := json.NewDecoder(r.Body)
+	for i := 0; i < 2; i++ {
+		var jm utils.JSONMessage
+		if err := dec.Decode(&jm); err == io.EOF {
+			break
+		} else if err != nil {
+			t.Fatal(err)
+		}
+		if jm != srv.events[i] {
+			t.Fatalf("Event received it different than expected")
+		}
+	}
+
+}
+
 func TestGetImagesJSON(t *testing.T) {
 func TestGetImagesJSON(t *testing.T) {
 	runtime := mkRuntime(t)
 	runtime := mkRuntime(t)
 	defer nuke(runtime)
 	defer nuke(runtime)

+ 55 - 50
auth/auth.go

@@ -25,19 +25,15 @@ var (
 )
 )
 
 
 type AuthConfig struct {
 type AuthConfig struct {
-	Username string `json:"username"`
-	Password string `json:"password"`
+	Username string `json:"username,omitempty"`
+	Password string `json:"password,omitempty"`
+	Auth     string `json:"auth"`
 	Email    string `json:"email"`
 	Email    string `json:"email"`
-	rootPath string
 }
 }
 
 
-func NewAuthConfig(username, password, email, rootPath string) *AuthConfig {
-	return &AuthConfig{
-		Username: username,
-		Password: password,
-		Email:    email,
-		rootPath: rootPath,
-	}
+type ConfigFile struct {
+	Configs  map[string]AuthConfig `json:"configs,omitempty"`
+	rootPath string
 }
 }
 
 
 func IndexServerAddress() string {
 func IndexServerAddress() string {
@@ -54,61 +50,84 @@ func encodeAuth(authConfig *AuthConfig) string {
 }
 }
 
 
 // decode the auth string
 // decode the auth string
-func decodeAuth(authStr string) (*AuthConfig, error) {
+func decodeAuth(authStr string) (string, string, error) {
 	decLen := base64.StdEncoding.DecodedLen(len(authStr))
 	decLen := base64.StdEncoding.DecodedLen(len(authStr))
 	decoded := make([]byte, decLen)
 	decoded := make([]byte, decLen)
 	authByte := []byte(authStr)
 	authByte := []byte(authStr)
 	n, err := base64.StdEncoding.Decode(decoded, authByte)
 	n, err := base64.StdEncoding.Decode(decoded, authByte)
 	if err != nil {
 	if err != nil {
-		return nil, err
+		return "", "", err
 	}
 	}
 	if n > decLen {
 	if n > decLen {
-		return nil, fmt.Errorf("Something went wrong decoding auth config")
+		return "", "", fmt.Errorf("Something went wrong decoding auth config")
 	}
 	}
 	arr := strings.Split(string(decoded), ":")
 	arr := strings.Split(string(decoded), ":")
 	if len(arr) != 2 {
 	if len(arr) != 2 {
-		return nil, fmt.Errorf("Invalid auth configuration file")
+		return "", "", fmt.Errorf("Invalid auth configuration file")
 	}
 	}
 	password := strings.Trim(arr[1], "\x00")
 	password := strings.Trim(arr[1], "\x00")
-	return &AuthConfig{Username: arr[0], Password: password}, nil
+	return arr[0], password, nil
 }
 }
 
 
 // load up the auth config information and return values
 // load up the auth config information and return values
 // FIXME: use the internal golang config parser
 // FIXME: use the internal golang config parser
-func LoadConfig(rootPath string) (*AuthConfig, error) {
+func LoadConfig(rootPath string) (*ConfigFile, error) {
+	configFile := ConfigFile{Configs: make(map[string]AuthConfig), rootPath: rootPath}
 	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{rootPath: rootPath}, ErrConfigFileMissing
+		return &configFile, ErrConfigFileMissing
 	}
 	}
 	b, err := ioutil.ReadFile(confFile)
 	b, err := ioutil.ReadFile(confFile)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
-	arr := strings.Split(string(b), "\n")
-	if len(arr) < 2 {
-		return nil, fmt.Errorf("The Auth config file is empty")
-	}
-	origAuth := strings.Split(arr[0], " = ")
-	origEmail := strings.Split(arr[1], " = ")
-	authConfig, err := decodeAuth(origAuth[1])
-	if err != nil {
-		return nil, err
+
+	if err := json.Unmarshal(b, &configFile.Configs); err != nil {
+		arr := strings.Split(string(b), "\n")
+		if len(arr) < 2 {
+			return nil, fmt.Errorf("The Auth config file is empty")
+		}
+		authConfig := AuthConfig{}
+		origAuth := strings.Split(arr[0], " = ")
+		authConfig.Username, authConfig.Password, err = decodeAuth(origAuth[1])
+		if err != nil {
+			return nil, err
+		}
+		origEmail := strings.Split(arr[1], " = ")
+		authConfig.Email = origEmail[1]
+		configFile.Configs[IndexServerAddress()] = authConfig
+	} else {
+		for k, authConfig := range configFile.Configs {
+			authConfig.Username, authConfig.Password, err = decodeAuth(authConfig.Auth)
+			if err != nil {
+				return nil, err
+			}
+			authConfig.Auth = ""
+			configFile.Configs[k] = authConfig
+		}
 	}
 	}
-	authConfig.Email = origEmail[1]
-	authConfig.rootPath = rootPath
-	return authConfig, nil
+	return &configFile, nil
 }
 }
 
 
 // save the auth config
 // save the auth config
-func SaveConfig(authConfig *AuthConfig) error {
-	confFile := path.Join(authConfig.rootPath, CONFIGFILE)
-	if len(authConfig.Email) == 0 {
+func SaveConfig(configFile *ConfigFile) error {
+	confFile := path.Join(configFile.rootPath, CONFIGFILE)
+	if len(configFile.Configs) == 0 {
 		os.Remove(confFile)
 		os.Remove(confFile)
 		return nil
 		return nil
 	}
 	}
-	lines := "auth = " + encodeAuth(authConfig) + "\n" + "email = " + authConfig.Email + "\n"
-	b := []byte(lines)
-	err := ioutil.WriteFile(confFile, b, 0600)
+	for k, authConfig := range configFile.Configs {
+		authConfig.Auth = encodeAuth(&authConfig)
+		authConfig.Username = ""
+		authConfig.Password = ""
+		configFile.Configs[k] = authConfig
+	}
+
+	b, err := json.Marshal(configFile.Configs)
+	if err != nil {
+		return err
+	}
+	err = ioutil.WriteFile(confFile, b, 0600)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -116,8 +135,7 @@ func SaveConfig(authConfig *AuthConfig) error {
 }
 }
 
 
 // try to register/login to the registry server
 // try to register/login to the registry server
-func Login(authConfig *AuthConfig, store bool) (string, error) {
-	storeConfig := false
+func Login(authConfig *AuthConfig) (string, error) {
 	client := &http.Client{}
 	client := &http.Client{}
 	reqStatusCode := 0
 	reqStatusCode := 0
 	var status string
 	var status string
@@ -143,7 +161,6 @@ func Login(authConfig *AuthConfig, store bool) (string, error) {
 	if reqStatusCode == 201 {
 	if reqStatusCode == 201 {
 		status = "Account created. Please use the confirmation link we sent" +
 		status = "Account created. Please use the confirmation link we sent" +
 			" to your e-mail to activate it."
 			" to your e-mail to activate it."
-		storeConfig = true
 	} else if reqStatusCode == 403 {
 	} else if reqStatusCode == 403 {
 		return "", fmt.Errorf("Login: Your account hasn't been activated. " +
 		return "", fmt.Errorf("Login: Your account hasn't been activated. " +
 			"Please check your e-mail for a confirmation link.")
 			"Please check your e-mail for a confirmation link.")
@@ -162,14 +179,7 @@ func Login(authConfig *AuthConfig, store bool) (string, error) {
 			}
 			}
 			if resp.StatusCode == 200 {
 			if resp.StatusCode == 200 {
 				status = "Login Succeeded"
 				status = "Login Succeeded"
-				storeConfig = true
 			} else if resp.StatusCode == 401 {
 			} else if resp.StatusCode == 401 {
-				if store {
-					authConfig.Email = ""
-					if err := SaveConfig(authConfig); 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,
@@ -181,10 +191,5 @@ func Login(authConfig *AuthConfig, store bool) (string, error) {
 	} else {
 	} else {
 		return "", fmt.Errorf("Unexpected status code [%d] : %s", reqStatusCode, reqBody)
 		return "", fmt.Errorf("Unexpected status code [%d] : %s", reqStatusCode, reqBody)
 	}
 	}
-	if storeConfig && store {
-		if err := SaveConfig(authConfig); err != nil {
-			return "", err
-		}
-	}
 	return status, nil
 	return status, nil
 }
 }

+ 8 - 6
auth/auth_test.go

@@ -11,7 +11,9 @@ import (
 func TestEncodeAuth(t *testing.T) {
 func TestEncodeAuth(t *testing.T) {
 	newAuthConfig := &AuthConfig{Username: "ken", Password: "test", Email: "test@example.com"}
 	newAuthConfig := &AuthConfig{Username: "ken", Password: "test", Email: "test@example.com"}
 	authStr := encodeAuth(newAuthConfig)
 	authStr := encodeAuth(newAuthConfig)
-	decAuthConfig, err := decodeAuth(authStr)
+	decAuthConfig := &AuthConfig{}
+	var err error
+	decAuthConfig.Username, decAuthConfig.Password, err = decodeAuth(authStr)
 	if err != nil {
 	if err != nil {
 		t.Fatal(err)
 		t.Fatal(err)
 	}
 	}
@@ -29,8 +31,8 @@ func TestEncodeAuth(t *testing.T) {
 func TestLogin(t *testing.T) {
 func TestLogin(t *testing.T) {
 	os.Setenv("DOCKER_INDEX_URL", "https://indexstaging-docker.dotcloud.com")
 	os.Setenv("DOCKER_INDEX_URL", "https://indexstaging-docker.dotcloud.com")
 	defer os.Setenv("DOCKER_INDEX_URL", "")
 	defer os.Setenv("DOCKER_INDEX_URL", "")
-	authConfig := NewAuthConfig("unittester", "surlautrerivejetattendrai", "noise+unittester@dotcloud.com", "/tmp")
-	status, err := Login(authConfig, false)
+	authConfig := &AuthConfig{Username: "unittester", Password: "surlautrerivejetattendrai", Email: "noise+unittester@dotcloud.com"}
+	status, err := Login(authConfig)
 	if err != nil {
 	if err != nil {
 		t.Fatal(err)
 		t.Fatal(err)
 	}
 	}
@@ -49,8 +51,8 @@ func TestCreateAccount(t *testing.T) {
 	}
 	}
 	token := hex.EncodeToString(tokenBuffer)[:12]
 	token := hex.EncodeToString(tokenBuffer)[:12]
 	username := "ut" + token
 	username := "ut" + token
-	authConfig := NewAuthConfig(username, "test42", "docker-ut+"+token+"@example.com", "/tmp")
-	status, err := Login(authConfig, false)
+	authConfig := &AuthConfig{Username: username, Password: "test42", Email: "docker-ut+"+token+"@example.com"}
+	status, err := Login(authConfig)
 	if err != nil {
 	if err != nil {
 		t.Fatal(err)
 		t.Fatal(err)
 	}
 	}
@@ -60,7 +62,7 @@ func TestCreateAccount(t *testing.T) {
 		t.Fatalf("Expected status: \"%s\", found \"%s\" instead.", expectedStatus, status)
 		t.Fatalf("Expected status: \"%s\", found \"%s\" instead.", expectedStatus, status)
 	}
 	}
 
 
-	status, err = Login(authConfig, false)
+	status, err = Login(authConfig)
 	if err == nil {
 	if err == nil {
 		t.Fatalf("Expected error but found nil instead")
 		t.Fatalf("Expected error but found nil instead")
 	}
 	}

+ 20 - 1
buildfile.go

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

+ 96 - 23
buildfile_test.go

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

+ 60 - 34
commands.go

@@ -78,6 +78,7 @@ func (cli *DockerCli) CmdHelp(args ...string) error {
 		{"build", "Build a container from a Dockerfile"},
 		{"build", "Build a container from a Dockerfile"},
 		{"commit", "Create a new image from a container's changes"},
 		{"commit", "Create a new image from a container's changes"},
 		{"diff", "Inspect changes on a container's filesystem"},
 		{"diff", "Inspect changes on a container's filesystem"},
+		{"events", "Get real time events from the server"},
 		{"export", "Stream the contents of a container as a tar archive"},
 		{"export", "Stream the contents of a container as a tar archive"},
 		{"history", "Show the history of an image"},
 		{"history", "Show the history of an image"},
 		{"images", "List images"},
 		{"images", "List images"},
@@ -185,6 +186,9 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
 	} else if utils.IsURL(cmd.Arg(0)) || utils.IsGIT(cmd.Arg(0)) {
 	} else if utils.IsURL(cmd.Arg(0)) || utils.IsGIT(cmd.Arg(0)) {
 		isRemote = true
 		isRemote = true
 	} else {
 	} else {
+		if _, err := os.Stat(cmd.Arg(0)); err != nil {
+			return err
+		}
 		context, err = Tar(cmd.Arg(0), Uncompressed)
 		context, err = Tar(cmd.Arg(0), Uncompressed)
 	}
 	}
 	var body io.Reader
 	var body io.Reader
@@ -310,16 +314,21 @@ func (cli *DockerCli) CmdLogin(args ...string) error {
 		email    string
 		email    string
 	)
 	)
 
 
+	authconfig, ok := cli.configFile.Configs[auth.IndexServerAddress()]
+	if !ok {
+		authconfig = auth.AuthConfig{}
+	}
+
 	if *flUsername == "" {
 	if *flUsername == "" {
-		fmt.Fprintf(cli.out, "Username (%s): ", cli.authConfig.Username)
+		fmt.Fprintf(cli.out, "Username (%s): ", authconfig.Username)
 		username = readAndEchoString(cli.in, cli.out)
 		username = readAndEchoString(cli.in, cli.out)
 		if username == "" {
 		if username == "" {
-			username = cli.authConfig.Username
+			username = authconfig.Username
 		}
 		}
 	} else {
 	} else {
 		username = *flUsername
 		username = *flUsername
 	}
 	}
-	if username != cli.authConfig.Username {
+	if username != authconfig.Username {
 		if *flPassword == "" {
 		if *flPassword == "" {
 			fmt.Fprintf(cli.out, "Password: ")
 			fmt.Fprintf(cli.out, "Password: ")
 			password = readString(cli.in, cli.out)
 			password = readString(cli.in, cli.out)
@@ -331,31 +340,30 @@ func (cli *DockerCli) CmdLogin(args ...string) error {
 		}
 		}
 
 
 		if *flEmail == "" {
 		if *flEmail == "" {
-			fmt.Fprintf(cli.out, "Email (%s): ", cli.authConfig.Email)
+			fmt.Fprintf(cli.out, "Email (%s): ", authconfig.Email)
 			email = readAndEchoString(cli.in, cli.out)
 			email = readAndEchoString(cli.in, cli.out)
 			if email == "" {
 			if email == "" {
-				email = cli.authConfig.Email
+				email = authconfig.Email
 			}
 			}
 		} else {
 		} else {
 			email = *flEmail
 			email = *flEmail
 		}
 		}
 	} else {
 	} else {
-		password = cli.authConfig.Password
-		email = cli.authConfig.Email
+		password = authconfig.Password
+		email = authconfig.Email
 	}
 	}
 	if oldState != nil {
 	if oldState != nil {
 		term.RestoreTerminal(cli.terminalFd, oldState)
 		term.RestoreTerminal(cli.terminalFd, oldState)
 	}
 	}
-	cli.authConfig.Username = username
-	cli.authConfig.Password = password
-	cli.authConfig.Email = email
+	authconfig.Username = username
+	authconfig.Password = password
+	authconfig.Email = email
+	cli.configFile.Configs[auth.IndexServerAddress()] = authconfig
 
 
-	body, statusCode, err := cli.call("POST", "/auth", cli.authConfig)
+	body, statusCode, err := cli.call("POST", "/auth", cli.configFile.Configs[auth.IndexServerAddress()])
 	if statusCode == 401 {
 	if statusCode == 401 {
-		cli.authConfig.Username = ""
-		cli.authConfig.Password = ""
-		cli.authConfig.Email = ""
-		auth.SaveConfig(cli.authConfig)
+		delete(cli.configFile.Configs, auth.IndexServerAddress())
+		auth.SaveConfig(cli.configFile)
 		return err
 		return err
 	}
 	}
 	if err != nil {
 	if err != nil {
@@ -365,10 +373,10 @@ func (cli *DockerCli) CmdLogin(args ...string) error {
 	var out2 APIAuth
 	var out2 APIAuth
 	err = json.Unmarshal(body, &out2)
 	err = json.Unmarshal(body, &out2)
 	if err != nil {
 	if err != nil {
-		auth.LoadConfig(os.Getenv("HOME"))
+		cli.configFile, _ = auth.LoadConfig(os.Getenv("HOME"))
 		return err
 		return err
 	}
 	}
-	auth.SaveConfig(cli.authConfig)
+	auth.SaveConfig(cli.configFile)
 	if out2.Status != "" {
 	if out2.Status != "" {
 		fmt.Fprintf(cli.out, "%s\n", out2.Status)
 		fmt.Fprintf(cli.out, "%s\n", out2.Status)
 	}
 	}
@@ -464,6 +472,7 @@ func (cli *DockerCli) CmdInfo(args ...string) error {
 		fmt.Fprintf(cli.out, "Fds: %d\n", out.NFd)
 		fmt.Fprintf(cli.out, "Fds: %d\n", out.NFd)
 		fmt.Fprintf(cli.out, "Goroutines: %d\n", out.NGoroutines)
 		fmt.Fprintf(cli.out, "Goroutines: %d\n", out.NGoroutines)
 		fmt.Fprintf(cli.out, "LXC Version: %s\n", out.LXCVersion)
 		fmt.Fprintf(cli.out, "LXC Version: %s\n", out.LXCVersion)
+		fmt.Fprintf(cli.out, "EventsListeners: %d\n", out.NEventsListener)
 	}
 	}
 	if !out.MemoryLimit {
 	if !out.MemoryLimit {
 		fmt.Fprintf(cli.err, "WARNING: No memory limit support\n")
 		fmt.Fprintf(cli.err, "WARNING: No memory limit support\n")
@@ -476,7 +485,7 @@ func (cli *DockerCli) CmdInfo(args ...string) error {
 
 
 func (cli *DockerCli) CmdStop(args ...string) error {
 func (cli *DockerCli) CmdStop(args ...string) error {
 	cmd := Subcmd("stop", "[OPTIONS] CONTAINER [CONTAINER...]", "Stop a running container")
 	cmd := Subcmd("stop", "[OPTIONS] CONTAINER [CONTAINER...]", "Stop a running container")
-	nSeconds := cmd.Int("t", 10, "Number of seconds to try to stop for before killing the container. Default=10")
+	nSeconds := cmd.Int("t", 10, "Number of seconds to wait for the container to stop before killing it.")
 	if err := cmd.Parse(args); err != nil {
 	if err := cmd.Parse(args); err != nil {
 		return nil
 		return nil
 	}
 	}
@@ -800,10 +809,10 @@ func (cli *DockerCli) CmdPush(args ...string) error {
 	// Custom repositories can have different rules, and we must also
 	// Custom repositories can have different rules, and we must also
 	// allow pushing by image ID.
 	// allow pushing by image ID.
 	if len(strings.SplitN(name, "/", 2)) == 1 {
 	if len(strings.SplitN(name, "/", 2)) == 1 {
-		return fmt.Errorf("Impossible to push a \"root\" repository. Please rename your repository in <user>/<repo> (ex: %s/%s)", cli.authConfig.Username, name)
+		return fmt.Errorf("Impossible to push a \"root\" repository. Please rename your repository in <user>/<repo> (ex: %s/%s)", cli.configFile.Configs[auth.IndexServerAddress()].Username, name)
 	}
 	}
 
 
-	buf, err := json.Marshal(cli.authConfig)
+	buf, err := json.Marshal(cli.configFile.Configs[auth.IndexServerAddress()])
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -1053,6 +1062,29 @@ func (cli *DockerCli) CmdCommit(args ...string) error {
 	return nil
 	return nil
 }
 }
 
 
+func (cli *DockerCli) CmdEvents(args ...string) error {
+	cmd := Subcmd("events", "[OPTIONS]", "Get real time events from the server")
+	since := cmd.String("since", "", "Show events previously created (used for polling).")
+	if err := cmd.Parse(args); err != nil {
+		return nil
+	}
+
+	if cmd.NArg() != 0 {
+		cmd.Usage()
+		return nil
+	}
+
+	v := url.Values{}
+	if *since != "" {
+		v.Set("since", *since)
+	}
+
+	if err := cli.stream("GET", "/events?"+v.Encode(), nil, cli.out); err != nil {
+		return err
+	}
+	return nil
+}
+
 func (cli *DockerCli) CmdExport(args ...string) error {
 func (cli *DockerCli) CmdExport(args ...string) error {
 	cmd := Subcmd("export", "CONTAINER", "Export the contents of a filesystem as a tar archive")
 	cmd := Subcmd("export", "CONTAINER", "Export the contents of a filesystem as a tar archive")
 	if err := cmd.Parse(args); err != nil {
 	if err := cmd.Parse(args); err != nil {
@@ -1408,11 +1440,11 @@ func (cli *DockerCli) CmdRun(args ...string) error {
 
 
 func (cli *DockerCli) checkIfLogged(action string) error {
 func (cli *DockerCli) checkIfLogged(action string) error {
 	// If condition AND the login failed
 	// If condition AND the login failed
-	if cli.authConfig.Username == "" {
+	if cli.configFile.Configs[auth.IndexServerAddress()].Username == "" {
 		if err := cli.CmdLogin(""); err != nil {
 		if err := cli.CmdLogin(""); err != nil {
 			return err
 			return err
 		}
 		}
-		if cli.authConfig.Username == "" {
+		if cli.configFile.Configs[auth.IndexServerAddress()].Username == "" {
 			return fmt.Errorf("Please login prior to %s. ('docker login')", action)
 			return fmt.Errorf("Please login prior to %s. ('docker login')", action)
 		}
 		}
 	}
 	}
@@ -1507,19 +1539,13 @@ 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" {
 		dec := json.NewDecoder(resp.Body)
 		dec := json.NewDecoder(resp.Body)
 		for {
 		for {
-			var m utils.JSONMessage
-			if err := dec.Decode(&m); err == io.EOF {
+			var jm utils.JSONMessage
+			if err := dec.Decode(&jm); err == io.EOF {
 				break
 				break
 			} else if err != nil {
 			} else if err != nil {
 				return err
 				return err
 			}
 			}
-			if m.Progress != "" {
-				fmt.Fprintf(out, "%s %s\r", m.Status, m.Progress)
-			} else if m.Error != "" {
-				return fmt.Errorf(m.Error)
-			} else {
-				fmt.Fprintf(out, "%s\n", m.Status)
-			}
+			jm.Display(out)
 		}
 		}
 	} else {
 	} else {
 		if _, err := io.Copy(out, resp.Body); err != nil {
 		if _, err := io.Copy(out, resp.Body); err != nil {
@@ -1668,11 +1694,11 @@ func NewDockerCli(in io.ReadCloser, out, err io.Writer, proto, addr string) *Doc
 		err = out
 		err = out
 	}
 	}
 
 
-	authConfig, _ := auth.LoadConfig(os.Getenv("HOME"))
+	configFile, _ := auth.LoadConfig(os.Getenv("HOME"))
 	return &DockerCli{
 	return &DockerCli{
 		proto:      proto,
 		proto:      proto,
 		addr:       addr,
 		addr:       addr,
-		authConfig: authConfig,
+		configFile: configFile,
 		in:         in,
 		in:         in,
 		out:        out,
 		out:        out,
 		err:        err,
 		err:        err,
@@ -1684,7 +1710,7 @@ func NewDockerCli(in io.ReadCloser, out, err io.Writer, proto, addr string) *Doc
 type DockerCli struct {
 type DockerCli struct {
 	proto      string
 	proto      string
 	addr       string
 	addr       string
-	authConfig *auth.AuthConfig
+	configFile *auth.ConfigFile
 	in         io.ReadCloser
 	in         io.ReadCloser
 	out        io.Writer
 	out        io.Writer
 	err        io.Writer
 	err        io.Writer

+ 1 - 1
commands_test.go

@@ -38,7 +38,7 @@ func setTimeout(t *testing.T, msg string, d time.Duration, f func()) {
 		f()
 		f()
 		c <- false
 		c <- false
 	}()
 	}()
-	if <-c {
+	if <-c && msg != "" {
 		t.Fatal(msg)
 		t.Fatal(msg)
 	}
 	}
 }
 }

+ 60 - 40
container.go

@@ -58,25 +58,26 @@ type Container struct {
 }
 }
 
 
 type Config struct {
 type Config struct {
-	Hostname     string
-	User         string
-	Memory       int64 // Memory limit (in bytes)
-	MemorySwap   int64 // Total memory usage (memory + swap); set `-1' to disable swap
-	CpuShares    int64 // CPU shares (relative weight vs. other containers)
-	AttachStdin  bool
-	AttachStdout bool
-	AttachStderr bool
-	PortSpecs    []string
-	Tty          bool // Attach standard streams to a tty, including stdin if it is not closed.
-	OpenStdin    bool // Open stdin
-	StdinOnce    bool // If true, close stdin after the 1 attached client disconnects.
-	Env          []string
-	Cmd          []string
-	Dns          []string
-	Image        string // Name of the image as it was passed by the operator (eg. could be symbolic)
-	Volumes      map[string]struct{}
-	VolumesFrom  string
-	Entrypoint   []string
+	Hostname        string
+	User            string
+	Memory          int64 // Memory limit (in bytes)
+	MemorySwap      int64 // Total memory usage (memory + swap); set `-1' to disable swap
+	CpuShares       int64 // CPU shares (relative weight vs. other containers)
+	AttachStdin     bool
+	AttachStdout    bool
+	AttachStderr    bool
+	PortSpecs       []string
+	Tty             bool // Attach standard streams to a tty, including stdin if it is not closed.
+	OpenStdin       bool // Open stdin
+	StdinOnce       bool // If true, close stdin after the 1 attached client disconnects.
+	Env             []string
+	Cmd             []string
+	Dns             []string
+	Image           string // Name of the image as it was passed by the operator (eg. could be symbolic)
+	Volumes         map[string]struct{}
+	VolumesFrom     string
+	Entrypoint      []string
+	NetworkDisabled bool
 }
 }
 
 
 type HostConfig struct {
 type HostConfig struct {
@@ -94,6 +95,7 @@ func ParseRun(args []string, capabilities *Capabilities) (*Config, *HostConfig,
 	cmd := Subcmd("run", "[OPTIONS] IMAGE [COMMAND] [ARG...]", "Run a command in a new container")
 	cmd := Subcmd("run", "[OPTIONS] IMAGE [COMMAND] [ARG...]", "Run a command in a new container")
 	if len(args) > 0 && args[0] != "--help" {
 	if len(args) > 0 && args[0] != "--help" {
 		cmd.SetOutput(ioutil.Discard)
 		cmd.SetOutput(ioutil.Discard)
+		cmd.Usage = nil
 	}
 	}
 
 
 	flHostname := cmd.String("h", "", "Container host name")
 	flHostname := cmd.String("h", "", "Container host name")
@@ -105,6 +107,7 @@ func ParseRun(args []string, capabilities *Capabilities) (*Config, *HostConfig,
 	flTty := cmd.Bool("t", false, "Allocate a pseudo-tty")
 	flTty := cmd.Bool("t", false, "Allocate a pseudo-tty")
 	flMemory := cmd.Int64("m", 0, "Memory limit (in bytes)")
 	flMemory := cmd.Int64("m", 0, "Memory limit (in bytes)")
 	flContainerIDFile := cmd.String("cidfile", "", "Write the container ID to the file")
 	flContainerIDFile := cmd.String("cidfile", "", "Write the container ID to the file")
+	flNetwork := cmd.Bool("n", true, "Enable networking for this container")
 
 
 	if capabilities != nil && *flMemory > 0 && !capabilities.MemoryLimit {
 	if capabilities != nil && *flMemory > 0 && !capabilities.MemoryLimit {
 		//fmt.Fprintf(stdout, "WARNING: Your kernel does not support memory limit capabilities. Limitation discarded.\n")
 		//fmt.Fprintf(stdout, "WARNING: Your kernel does not support memory limit capabilities. Limitation discarded.\n")
@@ -173,23 +176,24 @@ func ParseRun(args []string, capabilities *Capabilities) (*Config, *HostConfig,
 	}
 	}
 
 
 	config := &Config{
 	config := &Config{
-		Hostname:     *flHostname,
-		PortSpecs:    flPorts,
-		User:         *flUser,
-		Tty:          *flTty,
-		OpenStdin:    *flStdin,
-		Memory:       *flMemory,
-		CpuShares:    *flCpuShares,
-		AttachStdin:  flAttach.Get("stdin"),
-		AttachStdout: flAttach.Get("stdout"),
-		AttachStderr: flAttach.Get("stderr"),
-		Env:          flEnv,
-		Cmd:          runCmd,
-		Dns:          flDns,
-		Image:        image,
-		Volumes:      flVolumes,
-		VolumesFrom:  *flVolumesFrom,
-		Entrypoint:   entrypoint,
+		Hostname:        *flHostname,
+		PortSpecs:       flPorts,
+		User:            *flUser,
+		Tty:             *flTty,
+		NetworkDisabled: !*flNetwork,
+		OpenStdin:       *flStdin,
+		Memory:          *flMemory,
+		CpuShares:       *flCpuShares,
+		AttachStdin:     flAttach.Get("stdin"),
+		AttachStdout:    flAttach.Get("stdout"),
+		AttachStderr:    flAttach.Get("stderr"),
+		Env:             flEnv,
+		Cmd:             runCmd,
+		Dns:             flDns,
+		Image:           image,
+		Volumes:         flVolumes,
+		VolumesFrom:     *flVolumesFrom,
+		Entrypoint:      entrypoint,
 	}
 	}
 	hostConfig := &HostConfig{
 	hostConfig := &HostConfig{
 		Binds:           binds,
 		Binds:           binds,
@@ -510,8 +514,12 @@ func (container *Container) Start(hostConfig *HostConfig) error {
 	if err := container.EnsureMounted(); err != nil {
 	if err := container.EnsureMounted(); err != nil {
 		return err
 		return err
 	}
 	}
-	if err := container.allocateNetwork(); err != nil {
-		return err
+	if container.runtime.networkManager.disabled {
+		container.Config.NetworkDisabled = true
+	} else {
+		if err := container.allocateNetwork(); err != nil {
+			return err
+		}
 	}
 	}
 
 
 	// Make sure the config is compatible with the current kernel
 	// Make sure the config is compatible with the current kernel
@@ -625,7 +633,9 @@ func (container *Container) Start(hostConfig *HostConfig) error {
 	}
 	}
 
 
 	// Networking
 	// Networking
-	params = append(params, "-g", container.network.Gateway.String())
+	if !container.Config.NetworkDisabled {
+		params = append(params, "-g", container.network.Gateway.String())
+	}
 
 
 	// User
 	// User
 	if container.Config.User != "" {
 	if container.Config.User != "" {
@@ -640,6 +650,7 @@ func (container *Container) Start(hostConfig *HostConfig) error {
 	params = append(params,
 	params = append(params,
 		"-e", "HOME=/",
 		"-e", "HOME=/",
 		"-e", "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
 		"-e", "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
+		"-e", "container=lxc",
 	)
 	)
 
 
 	for _, elem := range container.Config.Env {
 	for _, elem := range container.Config.Env {
@@ -726,6 +737,10 @@ func (container *Container) StderrPipe() (io.ReadCloser, error) {
 }
 }
 
 
 func (container *Container) allocateNetwork() error {
 func (container *Container) allocateNetwork() error {
+	if container.Config.NetworkDisabled {
+		return nil
+	}
+
 	iface, err := container.runtime.networkManager.Allocate()
 	iface, err := container.runtime.networkManager.Allocate()
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -752,6 +767,9 @@ func (container *Container) allocateNetwork() error {
 }
 }
 
 
 func (container *Container) releaseNetwork() {
 func (container *Container) releaseNetwork() {
+	if container.Config.NetworkDisabled {
+		return
+	}
 	container.network.Release()
 	container.network.Release()
 	container.network = nil
 	container.network = nil
 	container.NetworkSettings = &NetworkSettings{}
 	container.NetworkSettings = &NetworkSettings{}
@@ -787,7 +805,9 @@ func (container *Container) monitor() {
 		}
 		}
 	}
 	}
 	utils.Debugf("Process finished")
 	utils.Debugf("Process finished")
-
+	if container.runtime != nil && container.runtime.srv != nil {
+		container.runtime.srv.LogEvent("die", container.ShortID())
+	}
 	exitCode := -1
 	exitCode := -1
 	if container.cmd != nil {
 	if container.cmd != nil {
 		exitCode = container.cmd.ProcessState.Sys().(syscall.WaitStatus).ExitStatus()
 		exitCode = container.cmd.ProcessState.Sys().(syscall.WaitStatus).ExitStatus()

+ 39 - 0
container_test.go

@@ -959,6 +959,7 @@ func TestEnv(t *testing.T) {
 	goodEnv := []string{
 	goodEnv := []string{
 		"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
 		"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
 		"HOME=/",
 		"HOME=/",
+		"container=lxc",
 	}
 	}
 	sort.Strings(goodEnv)
 	sort.Strings(goodEnv)
 	if len(goodEnv) != len(actualEnv) {
 	if len(goodEnv) != len(actualEnv) {
@@ -1251,3 +1252,41 @@ func TestRestartWithVolumes(t *testing.T) {
 		t.Fatalf("Expected volume path: %s Actual path: %s", expected, actual)
 		t.Fatalf("Expected volume path: %s Actual path: %s", expected, actual)
 	}
 	}
 }
 }
+
+func TestOnlyLoopbackExistsWhenUsingDisableNetworkOption(t *testing.T) {
+	runtime := mkRuntime(t)
+	defer nuke(runtime)
+
+	config, hc, _, err := ParseRun([]string{"-n=false", GetTestImage(runtime).ID, "ip", "addr", "show"}, nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+	c, err := NewBuilder(runtime).Create(config)
+	if err != nil {
+		t.Fatal(err)
+	}
+	stdout, err := c.StdoutPipe()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	defer runtime.Destroy(c)
+	if err := c.Start(hc); err != nil {
+		t.Fatal(err)
+	}
+	c.WaitTimeout(500 * time.Millisecond)
+	c.Wait()
+	output, err := ioutil.ReadAll(stdout)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	interfaces := regexp.MustCompile(`(?m)^[0-9]+: [a-zA-Z0-9]+`).FindAllString(string(output), -1)
+	if len(interfaces) != 1 {
+		t.Fatalf("Wrong interface count in test container: expected [1: lo], got [%s]", interfaces)
+	}
+	if interfaces[0] != "1: lo" {
+		t.Fatalf("Wrong interface in test container: expected [1: lo], got [%s]", interfaces)
+	}
+
+}

+ 1 - 1
docker/docker.go

@@ -28,7 +28,7 @@ func main() {
 	flDaemon := flag.Bool("d", false, "Daemon mode")
 	flDaemon := flag.Bool("d", false, "Daemon mode")
 	flDebug := flag.Bool("D", false, "Debug mode")
 	flDebug := flag.Bool("D", false, "Debug mode")
 	flAutoRestart := flag.Bool("r", false, "Restart previously running containers")
 	flAutoRestart := flag.Bool("r", false, "Restart previously running containers")
-	bridgeName := flag.String("b", "", "Attach containers to a pre-existing network bridge")
+	bridgeName := flag.String("b", "", "Attach containers to a pre-existing network bridge. Use 'none' to disable container networking")
 	pidfile := flag.String("p", "/var/run/docker.pid", "File containing process PID")
 	pidfile := flag.String("p", "/var/run/docker.pid", "File containing process PID")
 	flGraphPath := flag.String("g", "/var/lib/docker", "Path to graph storage base dir.")
 	flGraphPath := flag.String("g", "/var/lib/docker", "Path to graph storage base dir.")
 	flEnableCors := flag.Bool("api-enable-cors", false, "Enable CORS requests in the remote api.")
 	flEnableCors := flag.Bool("api-enable-cors", false, "Enable CORS requests in the remote api.")

+ 46 - 17
docs/sources/api/docker_remote_api.rst

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

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

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

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

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

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

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

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

@@ -1,3 +1,8 @@
+.. use orphan to suppress "WARNING: document isn't included in any toctree"
+.. per http://sphinx-doc.org/markup/misc.html#file-wide-metadata
+
+:orphan:
+
 :title: Remote API v1.3
 :title: Remote API v1.3
 :description: API Documentation for Docker
 :description: API Documentation for Docker
 :keywords: API, Docker, rcli, REST, documentation
 :keywords: API, Docker, rcli, REST, documentation
@@ -1054,6 +1059,36 @@ Create a new image from a container's changes
         :statuscode 500: server error
         :statuscode 500: server error
 
 
 
 
+Monitor Docker's events
+***********************
+
+.. http:get:: /events
+
+	Get events from docker, either in real time via streaming, or via polling (using `since`)
+
+	**Example request**:
+
+	.. sourcecode:: http
+
+           POST /events?since=1374067924
+
+        **Example response**:
+
+        .. sourcecode:: http
+
+           HTTP/1.1 200 OK
+	   Content-Type: application/json
+
+	   {"status":"create","id":"dfdf82bd3881","time":1374067924}
+	   {"status":"start","id":"dfdf82bd3881","time":1374067924}
+	   {"status":"stop","id":"dfdf82bd3881","time":1374067966}
+	   {"status":"destroy","id":"dfdf82bd3881","time":1374067970}
+
+	:query since: timestamp used for polling
+        :statuscode 200: no error
+        :statuscode 500: server error
+
+
 3. Going further
 3. Going further
 ================
 ================
 
 

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

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

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

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

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

@@ -20,6 +20,7 @@
       -h="": Container host name
       -h="": Container host name
       -i=false: Keep stdin open even if not attached
       -i=false: Keep stdin open even if not attached
       -m=0: Memory limit (in bytes)
       -m=0: Memory limit (in bytes)
+      -n=true: Enable networking for this container
       -p=[]: Map a network port to the container
       -p=[]: Map a network port to the container
       -t=false: Allocate a pseudo-tty
       -t=false: Allocate a pseudo-tty
       -u="": Username or UID
       -u="": Username or UID

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

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

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

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

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

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

+ 0 - 2
docs/sources/index.rst

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

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

@@ -51,7 +51,7 @@ For example:
 .. code-block:: bash
 .. code-block:: bash
 
 
    # Run docker in daemon mode
    # Run docker in daemon mode
-   sudo <path to>/docker -H 0.0.0.0:5555 &
+   sudo <path to>/docker -H 0.0.0.0:5555 -d &
    # Download a base image
    # Download a base image
    docker -H :5555 pull base
    docker -H :5555 pull base
 
 
@@ -61,7 +61,7 @@ on both tcp and a unix socket
 .. code-block:: bash
 .. code-block:: bash
 
 
    # Run docker in daemon mode
    # Run docker in daemon mode
-   sudo <path to>/docker -H tcp://127.0.0.1:4243 -H unix:///var/run/docker.sock
+   sudo <path to>/docker -H tcp://127.0.0.1:4243 -H unix:///var/run/docker.sock -d &
    # Download a base image
    # Download a base image
    docker pull base
    docker pull base
    # OR
    # OR

+ 25 - 17
docs/sources/use/builder.rst

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

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

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

+ 6 - 7
docs/theme/docker/layout.html

@@ -68,19 +68,18 @@
 
 
             <div style="float: right" class="pull-right">
             <div style="float: right" class="pull-right">
                 <ul class="nav">
                 <ul class="nav">
-                    <li id="nav-introduction"><a href="http://www.docker.io/">Introduction</a></li>
+                    <li id="nav-introduction"><a href="http://www.docker.io/">Home</a></li>
+                    <li id="nav-about"><a href="http://www.docker.io/">About</a></li>
+                    <li id="nav-community"><a href="http://www.docker.io/">Community</a></li>
                     <li id="nav-gettingstarted"><a href="http://www.docker.io/gettingstarted/">Getting started</a></li>
                     <li id="nav-gettingstarted"><a href="http://www.docker.io/gettingstarted/">Getting started</a></li>
                     <li id="nav-documentation" class="active"><a href="http://docs.docker.io/en/latest/">Documentation</a></li>
                     <li id="nav-documentation" class="active"><a href="http://docs.docker.io/en/latest/">Documentation</a></li>
                     <li id="nav-blog"><a href="http://blog.docker.io/">Blog</a></li>
                     <li id="nav-blog"><a href="http://blog.docker.io/">Blog</a></li>
+                    <li id="nav-index"><a href="http://index.docker.io/" title="Docker Image Index, find images here">INDEX <img class="inline-icon" src="{{ pathto('_static/img/external-link-icon.png', 1) }}" title="external link"> </a></li>
                 </ul>
                 </ul>
-                <!--<div class="social links" style="float: right; margin-top: 14px; margin-left: 12px">-->
-                    <!--<a class="twitter" href="http://twitter.com/getdocker">Twitter</a>-->
-                    <!--<a class="github" href="https://github.com/dotcloud/docker/">GitHub</a>-->
-                <!--</div>-->
             </div>
             </div>
 
 
             <div style="margin-left: -12px; float: left;">
             <div style="margin-left: -12px; float: left;">
-                <a href="http://www.docker.io"><img style="margin-top: 12px; height: 38px" src="{{ pathto('_static/img/docker-letters-logo.gif', 1) }}"></a>
+                <a href="http://www.docker.io" title="Docker Homepage"><img style="margin-top: 0px; height: 60px; margin-left: 10px;" src="{{ pathto('_static/img/docker-top-logo.png', 1) }}"></a>
             </div>
             </div>
         </div>
         </div>
 
 
@@ -96,7 +95,7 @@
             <div class="pull-right" id="fork-us" style="margin-top: 16px; margin-right: 16px;">
             <div class="pull-right" id="fork-us" style="margin-top: 16px; margin-right: 16px;">
                 <a  href="http://github.com/dotcloud/docker/"><img src="{{ pathto('_static/img/fork-us.png', 1) }}"> Fork us on Github</a>
                 <a  href="http://github.com/dotcloud/docker/"><img src="{{ pathto('_static/img/fork-us.png', 1) }}"> Fork us on Github</a>
             </div>
             </div>
-            <h1 class="pageheader">DOCUMENTATION</h1>
+            <h1 class="pageheader"><a href="http://docs.docker.io/en/latest/" title="Documentation" style="color: white;">DOCUMENTATION</a></h1>
 
 
         </div>
         </div>
     </div>
     </div>

+ 5 - 5
docs/theme/docker/static/css/main.css

@@ -34,12 +34,12 @@ h4 {
 .navbar .nav li a {
 .navbar .nav li a {
   padding: 22px 15px 22px;
   padding: 22px 15px 22px;
 }
 }
-.navbar .brand {
-  padding: 13px 10px 13px 28px ;
-}
 .navbar-dotcloud .container {
 .navbar-dotcloud .container {
   border-bottom: 2px #000000 solid;
   border-bottom: 2px #000000 solid;
 }
 }
+.inline-icon {
+  margin-bottom: 6px;
+}
 /*
 /*
 * Responsive YouTube, Vimeo, Embed, and HTML5 Videos with CSS
 * Responsive YouTube, Vimeo, Embed, and HTML5 Videos with CSS
 * http://www.jonsuh.com
 * http://www.jonsuh.com
@@ -82,7 +82,7 @@ h4 {
 .btn-custom {
 .btn-custom {
   background-color: #292929 !important;
   background-color: #292929 !important;
   background-repeat: repeat-x;
   background-repeat: repeat-x;
-  filter: progid:dximagetransform.microsoft.gradient(startColorstr="#515151", endColorstr="#282828");
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr="#515151", endColorstr="#282828");
   background-image: -khtml-gradient(linear, left top, left bottom, from(#515151), to(#282828));
   background-image: -khtml-gradient(linear, left top, left bottom, from(#515151), to(#282828));
   background-image: -moz-linear-gradient(top, #515151, #282828);
   background-image: -moz-linear-gradient(top, #515151, #282828);
   background-image: -ms-linear-gradient(top, #515151, #282828);
   background-image: -ms-linear-gradient(top, #515151, #282828);
@@ -301,7 +301,7 @@ section.header {
   height: 28px;
   height: 28px;
   line-height: 28px;
   line-height: 28px;
   background-color: #43484c;
   background-color: #43484c;
-  filter: progid:dximagetransform.microsoft.gradient(gradientType=0, startColorstr='#FFFF6E56', endColorstr='#FFED4F35');
+  filter: progid:DXImageTransform.Microsoft.gradient(gradientType=0, startColorstr='#FFFF6E56', endColorstr='#FFED4F35');
   background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #747474), color-stop(100%, #43484c));
   background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #747474), color-stop(100%, #43484c));
   background-image: -webkit-linear-gradient(top, #747474 0%, #43484c 100%);
   background-image: -webkit-linear-gradient(top, #747474 0%, #43484c 100%);
   background-image: -moz-linear-gradient(top, #747474 0%, #43484c 100%);
   background-image: -moz-linear-gradient(top, #747474 0%, #43484c 100%);

+ 3 - 7
docs/theme/docker/static/css/main.less

@@ -53,13 +53,6 @@ h1, h2, h3, h4 {
       padding: 22px 15px 22px;
       padding: 22px 15px 22px;
     }
     }
   }
   }
-
-  .brand {
-    padding: 13px 10px 13px 28px ;
-  // padding-left: 30px;
-
-  }
-
   background-color: white;
   background-color: white;
 }
 }
 
 
@@ -67,6 +60,9 @@ h1, h2, h3, h4 {
   border-bottom: 2px @black solid;
   border-bottom: 2px @black solid;
 }
 }
 
 
+.inline-icon {
+  margin-bottom: 6px;
+}
 
 
 /*
 /*
 * Responsive YouTube, Vimeo, Embed, and HTML5 Videos with CSS
 * Responsive YouTube, Vimeo, Embed, and HTML5 Videos with CSS

BIN
docs/theme/docker/static/img/docker-top-logo.png


BIN
docs/theme/docker/static/img/external-link-icon.png


+ 91 - 0
hack/bootcamp/README.md

@@ -0,0 +1,91 @@
+# Docker maintainer bootcamp
+
+## Introduction: we need more maintainers
+
+Docker is growing incredibly fast. At the time of writing, it has received over 200 contributions from 90 people,
+and its API is used by dozens of 3rd-party tools. Over 1,000 issues have been opened. As the first production deployments
+start going live, the growth will only accelerate.
+
+Also at the time of writing, Docker has 3 full-time maintainers, and 7 part-time subsystem maintainers. If docker
+is going to live up to the expectations, we need more than that.
+
+This document describes a *bootcamp* to guide and train volunteers interested in helping the project, either with individual
+contributions, maintainer work, or both.
+
+This bootcamp is an experiment. If you decide to go through it, consider yourself an alpha-tester. You should expect quirks,
+and report them to us as you encounter them to help us smooth out the process.
+
+
+## How it works
+
+The maintainer bootcamp is a 12-step program - one step for each of the maintainer's responsibilities. The aspiring maintainer must
+validate all 12 steps by 1) studying it, 2) practicing it, and 3) getting endorsed for it.
+
+Steps are all equally important and can be validated in any order. Validating all 12 steps is a pre-requisite for becoming a core
+maintainer, but even 1 step will make you a better contributor!
+
+### List of steps
+
+#### 1) Be a power user
+
+Use docker daily, build cool things with it, know its quirks inside and out.
+
+
+#### 2) Help users
+
+Answer questions on irc, twitter, email, in person.
+
+
+#### 3) Manage the bug tracker
+
+Help triage tickets - ask the right questions, find duplicates, reference relevant resources, know when to close a ticket when necessary, take the time to go over older tickets.
+
+
+#### 4) Improve the documentation
+
+Follow the documentation from scratch regularly and make sure it is still up-to-date. Find and fix inconsistencies. Remove stale information. Find a frequently asked question that is not documented. Simplify the content and the form.
+
+
+#### 5) Evangelize the principles of docker
+
+Understand what the underlying goals and principle of docker are. Explain design decisions based on what docker is, and what it is not. When someone is not using docker, find how docker can be valuable to them. If they are using docker, find how they can use it better.
+
+
+#### 6) Fix bugs
+
+Self-explanatory. Contribute improvements to docker which solve defects. Bugfixes should be well-tested, and prioritized by impact to the user.
+
+
+#### 7) Improve the testing infrastructure
+
+Automated testing is complicated and should be perpetually improved. Invest time to improve the current tooling. Refactor existing tests, create new ones, make testing more accessible to developers, add new testing capabilities (integration tests, mocking, stress test...), improve integration between tests and documentation...
+
+
+#### 8) Contribute features
+
+Improve docker to do more things, or get better at doing the same things. Features should be well-tested, not break existing APIs, respect the project goals. They should make the user's life measurably better. Features should be discussed ahead of time to avoid wasting time and duplicating effort.
+
+
+#### 9) Refactor internals
+
+Improve docker to repay technical debt. Simplify code layout, improve performance, add missing comments, reduce the number of files and functions, rename functions and variables to be more readable, go over FIXMEs, etc.
+
+#### 10) Review and merge contributions
+
+Review pull requests in a timely manner, review code in detail and offer feedback. Keep a high bar without being pedantic. Share the load of testing and merging pull requests.
+
+#### 11) Release
+
+Manage a release of docker from beginning to end. Tests, final review, tags, builds, upload to mirrors, distro packaging, etc.
+
+#### 12) Train other maintainers
+
+Contribute to training other maintainers. Give advice, delegate work, help organize the bootcamp. This also means contribute to the maintainer's manual, look for ways to improve the project organization etc.
+
+### How to study a step
+
+### How to practice a step
+
+### How to get endorsed for a step
+
+

+ 5 - 0
lxc_template.go

@@ -13,6 +13,10 @@ lxc.utsname = {{.Id}}
 {{end}}
 {{end}}
 #lxc.aa_profile = unconfined
 #lxc.aa_profile = unconfined
 
 
+{{if .Config.NetworkDisabled}}
+# network is disabled (-n=false)
+lxc.network.type = empty
+{{else}}
 # network configuration
 # network configuration
 lxc.network.type = veth
 lxc.network.type = veth
 lxc.network.flags = up
 lxc.network.flags = up
@@ -20,6 +24,7 @@ lxc.network.link = {{.NetworkSettings.Bridge}}
 lxc.network.name = eth0
 lxc.network.name = eth0
 lxc.network.mtu = 1500
 lxc.network.mtu = 1500
 lxc.network.ipv4 = {{.NetworkSettings.IPAddress}}/{{.NetworkSettings.IPPrefixLen}}
 lxc.network.ipv4 = {{.NetworkSettings.IPAddress}}/{{.NetworkSettings.IPPrefixLen}}
+{{end}}
 
 
 # root filesystem
 # root filesystem
 {{$ROOTFS := .RootfsPath}}
 {{$ROOTFS := .RootfsPath}}

+ 49 - 3
network.go

@@ -17,6 +17,7 @@ var NetworkBridgeIface string
 
 
 const (
 const (
 	DefaultNetworkBridge = "docker0"
 	DefaultNetworkBridge = "docker0"
+	DisableNetworkBridge = "none"
 	portRangeStart       = 49153
 	portRangeStart       = 49153
 	portRangeEnd         = 65535
 	portRangeEnd         = 65535
 )
 )
@@ -111,10 +112,29 @@ func checkRouteOverlaps(dockerNetwork *net.IPNet) error {
 	return nil
 	return nil
 }
 }
 
 
+// CreateBridgeIface creates a network bridge interface on the host system with the name `ifaceName`,
+// and attempts to configure it with an address which doesn't conflict with any other interface on the host.
+// If it can't find an address which doesn't conflict, it will return an error.
 func CreateBridgeIface(ifaceName string) error {
 func CreateBridgeIface(ifaceName string) error {
-	// FIXME: try more IP ranges
-	// FIXME: try bigger ranges! /24 is too small.
-	addrs := []string{"172.16.42.1/24", "10.0.42.1/24", "192.168.42.1/24"}
+	addrs := []string{
+		// Here we don't follow the convention of using the 1st IP of the range for the gateway.
+		// This is to use the same gateway IPs as the /24 ranges, which predate the /16 ranges.
+		// In theory this shouldn't matter - in practice there's bound to be a few scripts relying
+		// on the internal addressing or other stupid things like that.
+		// The shouldn't, but hey, let's not break them unless we really have to.
+		"172.16.42.1/16",
+		"10.0.42.1/16", // Don't even try using the entire /8, that's too intrusive
+		"10.1.42.1/16",
+		"10.42.42.1/16",
+		"172.16.42.1/24",
+		"172.16.43.1/24",
+		"172.16.44.1/24",
+		"10.0.42.1/24",
+		"10.0.43.1/24",
+		"192.168.42.1/24",
+		"192.168.43.1/24",
+		"192.168.44.1/24",
+	}
 
 
 	var ifaceAddr string
 	var ifaceAddr string
 	for _, addr := range addrs {
 	for _, addr := range addrs {
@@ -453,10 +473,16 @@ type NetworkInterface struct {
 
 
 	manager  *NetworkManager
 	manager  *NetworkManager
 	extPorts []*Nat
 	extPorts []*Nat
+	disabled bool
 }
 }
 
 
 // Allocate an external TCP port and map it to the interface
 // Allocate an external TCP port and map it to the interface
 func (iface *NetworkInterface) AllocatePort(spec string) (*Nat, error) {
 func (iface *NetworkInterface) AllocatePort(spec string) (*Nat, error) {
+
+	if iface.disabled {
+		return nil, fmt.Errorf("Trying to allocate port for interface %v, which is disabled", iface) // FIXME
+	}
+
 	nat, err := parseNat(spec)
 	nat, err := parseNat(spec)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
@@ -552,6 +578,11 @@ func parseNat(spec string) (*Nat, error) {
 
 
 // Release: Network cleanup - release all resources
 // Release: Network cleanup - release all resources
 func (iface *NetworkInterface) Release() {
 func (iface *NetworkInterface) Release() {
+
+	if iface.disabled {
+		return
+	}
+
 	for _, nat := range iface.extPorts {
 	for _, nat := range iface.extPorts {
 		utils.Debugf("Unmaping %v/%v", nat.Proto, nat.Frontend)
 		utils.Debugf("Unmaping %v/%v", nat.Proto, nat.Frontend)
 		if err := iface.manager.portMapper.Unmap(nat.Frontend, nat.Proto); err != nil {
 		if err := iface.manager.portMapper.Unmap(nat.Frontend, nat.Proto); err != nil {
@@ -579,10 +610,17 @@ type NetworkManager struct {
 	tcpPortAllocator *PortAllocator
 	tcpPortAllocator *PortAllocator
 	udpPortAllocator *PortAllocator
 	udpPortAllocator *PortAllocator
 	portMapper       *PortMapper
 	portMapper       *PortMapper
+
+	disabled bool
 }
 }
 
 
 // Allocate a network interface
 // Allocate a network interface
 func (manager *NetworkManager) Allocate() (*NetworkInterface, error) {
 func (manager *NetworkManager) Allocate() (*NetworkInterface, error) {
+
+	if manager.disabled {
+		return &NetworkInterface{disabled: true}, nil
+	}
+
 	ip, err := manager.ipAllocator.Acquire()
 	ip, err := manager.ipAllocator.Acquire()
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
@@ -596,6 +634,14 @@ func (manager *NetworkManager) Allocate() (*NetworkInterface, error) {
 }
 }
 
 
 func newNetworkManager(bridgeIface string) (*NetworkManager, error) {
 func newNetworkManager(bridgeIface string) (*NetworkManager, error) {
+
+	if bridgeIface == DisableNetworkBridge {
+		manager := &NetworkManager{
+			disabled: true,
+		}
+		return manager, nil
+	}
+
 	addr, err := getIfaceAddr(bridgeIface)
 	addr, err := getIfaceAddr(bridgeIface)
 	if err != nil {
 	if err != nil {
 		// If the iface is not found, try to create it
 		// If the iface is not found, try to create it

+ 6 - 6
runtime_test.go

@@ -17,12 +17,12 @@ import (
 )
 )
 
 
 const (
 const (
-	unitTestImageName	= "docker-test-image"
-	unitTestImageID		= "83599e29c455eb719f77d799bc7c51521b9551972f5a850d7ad265bc1b5292f6" // 1.0
-	unitTestNetworkBridge	= "testdockbr0"
-	unitTestStoreBase	= "/var/lib/docker/unit-tests"
-	testDaemonAddr		= "127.0.0.1:4270"
-	testDaemonProto		= "tcp"
+	unitTestImageName     = "docker-test-image"
+	unitTestImageID       = "83599e29c455eb719f77d799bc7c51521b9551972f5a850d7ad265bc1b5292f6" // 1.0
+	unitTestNetworkBridge = "testdockbr0"
+	unitTestStoreBase     = "/var/lib/docker/unit-tests"
+	testDaemonAddr        = "127.0.0.1:4270"
+	testDaemonProto       = "tcp"
 )
 )
 
 
 var globalRuntime *Runtime
 var globalRuntime *Runtime

+ 36 - 9
server.go

@@ -19,6 +19,7 @@ import (
 	"runtime"
 	"runtime"
 	"strings"
 	"strings"
 	"sync"
 	"sync"
+	"time"
 )
 )
 
 
 func (srv *Server) DockerVersion() APIVersion {
 func (srv *Server) DockerVersion() APIVersion {
@@ -32,8 +33,9 @@ func (srv *Server) DockerVersion() APIVersion {
 func (srv *Server) ContainerKill(name string) error {
 func (srv *Server) ContainerKill(name string) error {
 	if container := srv.runtime.Get(name); container != nil {
 	if container := srv.runtime.Get(name); container != nil {
 		if err := container.Kill(); err != nil {
 		if err := container.Kill(); err != nil {
-			return fmt.Errorf("Error restarting container %s: %s", name, err)
+			return fmt.Errorf("Error killing container %s: %s", name, err)
 		}
 		}
+		srv.LogEvent("kill", name)
 	} else {
 	} else {
 		return fmt.Errorf("No such container: %s", name)
 		return fmt.Errorf("No such container: %s", name)
 	}
 	}
@@ -52,6 +54,7 @@ func (srv *Server) ContainerExport(name string, out io.Writer) error {
 		if _, err := io.Copy(out, data); err != nil {
 		if _, err := io.Copy(out, data); err != nil {
 			return err
 			return err
 		}
 		}
+		srv.LogEvent("export", name)
 		return nil
 		return nil
 	}
 	}
 	return fmt.Errorf("No such container: %s", name)
 	return fmt.Errorf("No such container: %s", name)
@@ -217,14 +220,15 @@ func (srv *Server) DockerInfo() *APIInfo {
 	}
 	}
 
 
 	return &APIInfo{
 	return &APIInfo{
-		Containers:  len(srv.runtime.List()),
-		Images:      imgcount,
-		MemoryLimit: srv.runtime.capabilities.MemoryLimit,
-		SwapLimit:   srv.runtime.capabilities.SwapLimit,
-		Debug:       os.Getenv("DEBUG") != "",
-		NFd:         utils.GetTotalUsedFds(),
-		NGoroutines: runtime.NumGoroutine(),
-		LXCVersion:  lxcVersion,
+		Containers:      len(srv.runtime.List()),
+		Images:          imgcount,
+		MemoryLimit:     srv.runtime.capabilities.MemoryLimit,
+		SwapLimit:       srv.runtime.capabilities.SwapLimit,
+		Debug:           os.Getenv("DEBUG") != "",
+		NFd:             utils.GetTotalUsedFds(),
+		NGoroutines:     runtime.NumGoroutine(),
+		LXCVersion:      lxcVersion,
+		NEventsListener: len(srv.events),
 	}
 	}
 }
 }
 
 
@@ -819,6 +823,7 @@ func (srv *Server) ContainerCreate(config *Config) (string, error) {
 		}
 		}
 		return "", err
 		return "", err
 	}
 	}
+	srv.LogEvent("create", container.ShortID())
 	return container.ShortID(), nil
 	return container.ShortID(), nil
 }
 }
 
 
@@ -827,6 +832,7 @@ func (srv *Server) ContainerRestart(name string, t int) error {
 		if err := container.Restart(t); err != nil {
 		if err := container.Restart(t); err != nil {
 			return fmt.Errorf("Error restarting container %s: %s", name, err)
 			return fmt.Errorf("Error restarting container %s: %s", name, err)
 		}
 		}
+		srv.LogEvent("restart", name)
 	} else {
 	} else {
 		return fmt.Errorf("No such container: %s", name)
 		return fmt.Errorf("No such container: %s", name)
 	}
 	}
@@ -846,6 +852,7 @@ func (srv *Server) ContainerDestroy(name string, removeVolume bool) error {
 		if err := srv.runtime.Destroy(container); err != nil {
 		if err := srv.runtime.Destroy(container); err != nil {
 			return fmt.Errorf("Error destroying container %s: %s", name, err)
 			return fmt.Errorf("Error destroying container %s: %s", name, err)
 		}
 		}
+		srv.LogEvent("destroy", name)
 
 
 		if removeVolume {
 		if removeVolume {
 			// Retrieve all volumes from all remaining containers
 			// Retrieve all volumes from all remaining containers
@@ -912,6 +919,7 @@ func (srv *Server) deleteImageAndChildren(id string, imgs *[]APIRmi) error {
 			return err
 			return err
 		}
 		}
 		*imgs = append(*imgs, APIRmi{Deleted: utils.TruncateID(id)})
 		*imgs = append(*imgs, APIRmi{Deleted: utils.TruncateID(id)})
+		srv.LogEvent("delete", utils.TruncateID(id))
 		return nil
 		return nil
 	}
 	}
 	return nil
 	return nil
@@ -955,6 +963,7 @@ func (srv *Server) deleteImage(img *Image, repoName, tag string) ([]APIRmi, erro
 	}
 	}
 	if tagDeleted {
 	if tagDeleted {
 		imgs = append(imgs, APIRmi{Untagged: img.ShortID()})
 		imgs = append(imgs, APIRmi{Untagged: img.ShortID()})
+		srv.LogEvent("untag", img.ShortID())
 	}
 	}
 	if len(srv.runtime.repositories.ByID()[img.ID]) == 0 {
 	if len(srv.runtime.repositories.ByID()[img.ID]) == 0 {
 		if err := srv.deleteImageAndChildren(img.ID, &imgs); err != nil {
 		if err := srv.deleteImageAndChildren(img.ID, &imgs); err != nil {
@@ -1027,6 +1036,7 @@ func (srv *Server) ContainerStart(name string, hostConfig *HostConfig) error {
 		if err := container.Start(hostConfig); err != nil {
 		if err := container.Start(hostConfig); err != nil {
 			return fmt.Errorf("Error starting container %s: %s", name, err)
 			return fmt.Errorf("Error starting container %s: %s", name, err)
 		}
 		}
+		srv.LogEvent("start", name)
 	} else {
 	} else {
 		return fmt.Errorf("No such container: %s", name)
 		return fmt.Errorf("No such container: %s", name)
 	}
 	}
@@ -1038,6 +1048,7 @@ func (srv *Server) ContainerStop(name string, t int) error {
 		if err := container.Stop(t); err != nil {
 		if err := container.Stop(t); err != nil {
 			return fmt.Errorf("Error stopping container %s: %s", name, err)
 			return fmt.Errorf("Error stopping container %s: %s", name, err)
 		}
 		}
+		srv.LogEvent("stop", name)
 	} else {
 	} else {
 		return fmt.Errorf("No such container: %s", name)
 		return fmt.Errorf("No such container: %s", name)
 	}
 	}
@@ -1171,15 +1182,31 @@ func NewServer(flGraphPath string, autoRestart, enableCors bool, dns ListOpts) (
 		enableCors:  enableCors,
 		enableCors:  enableCors,
 		pullingPool: make(map[string]struct{}),
 		pullingPool: make(map[string]struct{}),
 		pushingPool: make(map[string]struct{}),
 		pushingPool: make(map[string]struct{}),
+		events:      make([]utils.JSONMessage, 0, 64), //only keeps the 64 last events
+		listeners:   make(map[string]chan utils.JSONMessage),
 	}
 	}
 	runtime.srv = srv
 	runtime.srv = srv
 	return srv, nil
 	return srv, nil
 }
 }
 
 
+func (srv *Server) LogEvent(action, id string) {
+	now := time.Now().Unix()
+	jm := utils.JSONMessage{Status: action, ID: id, Time: now}
+	srv.events = append(srv.events, jm)
+	for _, c := range srv.listeners {
+		select { // non blocking channel
+		case c <- jm:
+		default:
+		}
+	}
+}
+
 type Server struct {
 type Server struct {
 	sync.Mutex
 	sync.Mutex
 	runtime     *Runtime
 	runtime     *Runtime
 	enableCors  bool
 	enableCors  bool
 	pullingPool map[string]struct{}
 	pullingPool map[string]struct{}
 	pushingPool map[string]struct{}
 	pushingPool map[string]struct{}
+	events      []utils.JSONMessage
+	listeners   map[string]chan utils.JSONMessage
 }
 }

+ 40 - 0
server_test.go

@@ -1,7 +1,9 @@
 package docker
 package docker
 
 
 import (
 import (
+	"github.com/dotcloud/docker/utils"
 	"testing"
 	"testing"
+	"time"
 )
 )
 
 
 func TestContainerTagImageDelete(t *testing.T) {
 func TestContainerTagImageDelete(t *testing.T) {
@@ -163,3 +165,41 @@ func TestRunWithTooLowMemoryLimit(t *testing.T) {
 	}
 	}
 
 
 }
 }
+
+func TestLogEvent(t *testing.T) {
+	runtime := mkRuntime(t)
+	srv := &Server{
+		runtime:   runtime,
+		events:    make([]utils.JSONMessage, 0, 64),
+		listeners: make(map[string]chan utils.JSONMessage),
+	}
+
+	srv.LogEvent("fakeaction", "fakeid")
+
+	listener := make(chan utils.JSONMessage)
+	srv.Lock()
+	srv.listeners["test"] = listener
+	srv.Unlock()
+
+	srv.LogEvent("fakeaction2", "fakeid")
+
+	if len(srv.events) != 2 {
+		t.Fatalf("Expected 2 events, found %d", len(srv.events))
+	}
+	go func() {
+		time.Sleep(200 * time.Millisecond)
+		srv.LogEvent("fakeaction3", "fakeid")
+		time.Sleep(200 * time.Millisecond)
+		srv.LogEvent("fakeaction4", "fakeid")
+	}()
+
+	setTimeout(t, "Listening for events timed out", 2*time.Second, func() {
+		for i := 2; i < 4; i++ {
+			event := <-listener
+			if event != srv.events[i] {
+				t.Fatalf("Event received it different than expected")
+			}
+		}
+	})
+
+}

+ 1 - 1
utils.go

@@ -78,7 +78,7 @@ func MergeConfig(userConf, imageConf *Config) {
 			imageNat, _ := parseNat(imagePortSpec)
 			imageNat, _ := parseNat(imagePortSpec)
 			for _, userPortSpec := range userConf.PortSpecs {
 			for _, userPortSpec := range userConf.PortSpecs {
 				userNat, _ := parseNat(userPortSpec)
 				userNat, _ := parseNat(userPortSpec)
-				if imageNat.Proto == userNat.Proto && imageNat.Frontend == userNat.Frontend {
+				if imageNat.Proto == userNat.Proto && imageNat.Backend == userNat.Backend {
 					found = true
 					found = true
 				}
 				}
 			}
 			}

+ 19 - 0
utils/utils.go

@@ -611,8 +611,27 @@ type JSONMessage struct {
 	Status   string `json:"status,omitempty"`
 	Status   string `json:"status,omitempty"`
 	Progress string `json:"progress,omitempty"`
 	Progress string `json:"progress,omitempty"`
 	Error    string `json:"error,omitempty"`
 	Error    string `json:"error,omitempty"`
+	ID	 string `json:"id,omitempty"`
+	Time	 int64 `json:"time,omitempty"`
 }
 }
 
 
+func (jm *JSONMessage) Display(out io.Writer) (error) {
+	if jm.Time != 0 {
+		fmt.Fprintf(out, "[%s] ", time.Unix(jm.Time, 0))
+	}
+	if jm.Progress != "" {
+		fmt.Fprintf(out, "%s %s\r", jm.Status, jm.Progress)
+	} else if jm.Error != "" {
+		return fmt.Errorf(jm.Error)
+	} else if jm.ID != "" {
+		fmt.Fprintf(out, "%s: %s\n", jm.ID, jm.Status)
+	} else {
+		fmt.Fprintf(out, "%s\n", jm.Status)
+	}
+	return nil
+}
+
+
 type StreamFormatter struct {
 type StreamFormatter struct {
 	json bool
 	json bool
 	used bool
 	used bool

+ 46 - 4
utils_test.go

@@ -155,7 +155,7 @@ func TestMergeConfig(t *testing.T) {
 	volumesUser["/test3"] = struct{}{}
 	volumesUser["/test3"] = struct{}{}
 	configUser := &Config{
 	configUser := &Config{
 		Dns:       []string{"3.3.3.3"},
 		Dns:       []string{"3.3.3.3"},
-		PortSpecs: []string{"2222:3333", "3333:3333"},
+		PortSpecs: []string{"3333:2222", "3333:3333"},
 		Env:       []string{"VAR2=3", "VAR3=3"},
 		Env:       []string{"VAR2=3", "VAR3=3"},
 		Volumes:   volumesUser,
 		Volumes:   volumesUser,
 	}
 	}
@@ -172,11 +172,11 @@ func TestMergeConfig(t *testing.T) {
 	}
 	}
 
 
 	if len(configUser.PortSpecs) != 3 {
 	if len(configUser.PortSpecs) != 3 {
-		t.Fatalf("Expected 3 portSpecs, 1111:1111, 2222:3333 and 3333:3333, found %d", len(configUser.PortSpecs))
+		t.Fatalf("Expected 3 portSpecs, 1111:1111, 3333:2222 and 3333:3333, found %d", len(configUser.PortSpecs))
 	}
 	}
 	for _, portSpecs := range configUser.PortSpecs {
 	for _, portSpecs := range configUser.PortSpecs {
-		if portSpecs != "1111:1111" && portSpecs != "2222:3333" && portSpecs != "3333:3333" {
-			t.Fatalf("Expected 1111:1111 or 2222:3333 or 3333:3333, found %s", portSpecs)
+		if portSpecs != "1111:1111" && portSpecs != "3333:2222" && portSpecs != "3333:3333" {
+			t.Fatalf("Expected 1111:1111 or 3333:2222 or 3333:3333, found %s", portSpecs)
 		}
 		}
 	}
 	}
 	if len(configUser.Env) != 3 {
 	if len(configUser.Env) != 3 {
@@ -197,3 +197,45 @@ func TestMergeConfig(t *testing.T) {
 		}
 		}
 	}
 	}
 }
 }
+
+func TestMergeConfigPublicPortNotHonored(t *testing.T) {
+	volumesImage := make(map[string]struct{})
+	volumesImage["/test1"] = struct{}{}
+	volumesImage["/test2"] = struct{}{}
+	configImage := &Config{
+		Dns:       []string{"1.1.1.1", "2.2.2.2"},
+		PortSpecs: []string{"1111", "2222"},
+		Env:       []string{"VAR1=1", "VAR2=2"},
+		Volumes:   volumesImage,
+	}
+
+	volumesUser := make(map[string]struct{})
+	volumesUser["/test3"] = struct{}{}
+	configUser := &Config{
+		Dns:       []string{"3.3.3.3"},
+		PortSpecs: []string{"1111:3333"},
+		Env:       []string{"VAR2=3", "VAR3=3"},
+		Volumes:   volumesUser,
+	}
+
+	MergeConfig(configUser, configImage)
+
+	contains := func(a []string, expect string) bool {
+		for _, p := range a {
+			if p == expect {
+				return true
+			}
+		}
+		return false
+	}
+
+	if !contains(configUser.PortSpecs, "2222") {
+		t.Logf("Expected '2222' Ports: %v", configUser.PortSpecs)
+		t.Fail()
+	}
+
+	if !contains(configUser.PortSpecs, "1111:3333") {
+		t.Logf("Expected '1111:3333' Ports: %v", configUser.PortSpecs)
+		t.Fail()
+	}
+}