Browse code

bump master

Victor Vieux authored on 2013/07/24 22:28:01
Showing 43 changed files
... ...
@@ -76,6 +76,7 @@ Shawn Siefkas <shawn.siefkas@meredith.com>
76 76
 Silas Sewell <silas@sewell.org>
77 77
 Solomon Hykes <solomon@dotcloud.com>
78 78
 Sridhar Ratnakumar <sridharr@activestate.com>
79
+Stefan Praszalowicz <stefan@greplin.com>
79 80
 Thatcher Peskens <thatcher@dotcloud.com>
80 81
 Thomas Bikeev <thomas.bikeev@mac.com>
81 82
 Thomas Hansen <thomas.hansen@gmail.com>
... ...
@@ -11,7 +11,7 @@ BUILD_DIR := $(CURDIR)/.gopath
11 11
 GOPATH ?= $(BUILD_DIR)
12 12
 export GOPATH
13 13
 
14
-GO_OPTIONS ?=
14
+GO_OPTIONS ?= -a -ldflags='-w -d'
15 15
 ifeq ($(VERBOSE), 1)
16 16
 GO_OPTIONS += -v
17 17
 endif
... ...
@@ -80,10 +80,10 @@ test:
80 80
 	tar --exclude=${BUILD_SRC} -cz . | tar -xz -C ${BUILD_PATH}
81 81
 	GOPATH=${CURDIR}/${BUILD_SRC} go get -d
82 82
 	# Do the test
83
-	sudo -E GOPATH=${CURDIR}/${BUILD_SRC} go test ${GO_OPTIONS}
83
+	sudo -E GOPATH=${CURDIR}/${BUILD_SRC} CGO_ENABLED=0 go test ${GO_OPTIONS}
84 84
 
85 85
 testall: all
86
-	@(cd $(DOCKER_DIR); sudo -E go test ./... $(GO_OPTIONS))
86
+	@(cd $(DOCKER_DIR); CGO_ENABLED=0 sudo -E go test ./... $(GO_OPTIONS))
87 87
 
88 88
 fmt:
89 89
 	@gofmt -s -l -w .
... ...
@@ -81,54 +81,15 @@ func getBoolParam(value string) (bool, error) {
81 81
 	return ret, nil
82 82
 }
83 83
 
84
-func getAuth(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
85
-	if version > 1.1 {
86
-		w.WriteHeader(http.StatusNotFound)
87
-		return nil
88
-	}
89
-	authConfig, err := auth.LoadConfig(srv.runtime.root)
90
-	if err != nil {
91
-		if err != auth.ErrConfigFileMissing {
92
-			return err
93
-		}
94
-		authConfig = &auth.AuthConfig{}
95
-	}
96
-	b, err := json.Marshal(&auth.AuthConfig{Username: authConfig.Username, Email: authConfig.Email})
97
-	if err != nil {
98
-		return err
99
-	}
100
-	writeJSON(w, b)
101
-	return nil
102
-}
103
-
104 84
 func postAuth(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
105 85
 	authConfig := &auth.AuthConfig{}
106 86
 	err := json.NewDecoder(r.Body).Decode(authConfig)
107 87
 	if err != nil {
108 88
 		return err
109 89
 	}
110
-	status := ""
111
-	if version > 1.1 {
112
-		status, err = auth.Login(authConfig, false)
113
-		if err != nil {
114
-			return err
115
-		}
116
-	} else {
117
-		localAuthConfig, err := auth.LoadConfig(srv.runtime.root)
118
-		if err != nil {
119
-			if err != auth.ErrConfigFileMissing {
120
-				return err
121
-			}
122
-		}
123
-		if authConfig.Username == localAuthConfig.Username {
124
-			authConfig.Password = localAuthConfig.Password
125
-		}
126
-
127
-		newAuthConfig := auth.NewAuthConfig(authConfig.Username, authConfig.Password, authConfig.Email, srv.runtime.root)
128
-		status, err = auth.Login(newAuthConfig, true)
129
-		if err != nil {
130
-			return err
131
-		}
90
+	status, err := auth.Login(authConfig)
91
+	if err != nil {
92
+		return err
132 93
 	}
133 94
 	if status != "" {
134 95
 		b, err := json.Marshal(&APIAuth{Status: status})
... ...
@@ -217,6 +178,64 @@ func getInfo(srv *Server, version float64, w http.ResponseWriter, r *http.Reques
217 217
 	return nil
218 218
 }
219 219
 
220
+func getEvents(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
221
+	sendEvent := func(wf *utils.WriteFlusher, event *utils.JSONMessage) error {
222
+		b, err := json.Marshal(event)
223
+		if err != nil {
224
+			return fmt.Errorf("JSON error")
225
+		}
226
+		_, err = wf.Write(b)
227
+		if err != nil {
228
+			// On error, evict the listener
229
+			utils.Debugf("%s", err)
230
+			srv.Lock()
231
+			delete(srv.listeners, r.RemoteAddr)
232
+			srv.Unlock()
233
+			return err
234
+		}
235
+		return nil
236
+	}
237
+
238
+	if err := parseForm(r); err != nil {
239
+		return err
240
+	}
241
+	listener := make(chan utils.JSONMessage)
242
+	srv.Lock()
243
+	srv.listeners[r.RemoteAddr] = listener
244
+	srv.Unlock()
245
+	since, err := strconv.ParseInt(r.Form.Get("since"), 10, 0)
246
+	if err != nil {
247
+		since = 0
248
+	}
249
+	w.Header().Set("Content-Type", "application/json")
250
+	wf := utils.NewWriteFlusher(w)
251
+	if since != 0 {
252
+		// If since, send previous events that happened after the timestamp
253
+		for _, event := range srv.events {
254
+			if event.Time >= since {
255
+				err := sendEvent(wf, &event)
256
+				if err != nil && err.Error() == "JSON error" {
257
+					continue
258
+				}
259
+				if err != nil {
260
+					return err
261
+				}
262
+			}
263
+		}
264
+	}
265
+	for {
266
+		event := <-listener
267
+		err := sendEvent(wf, &event)
268
+		if err != nil && err.Error() == "JSON error" {
269
+			continue
270
+		}
271
+		if err != nil {
272
+			return err
273
+		}
274
+	}
275
+	return nil
276
+}
277
+
220 278
 func getImagesHistory(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
221 279
 	if vars == nil {
222 280
 		return fmt.Errorf("Missing parameter")
... ...
@@ -429,16 +448,8 @@ func postImagesInsert(srv *Server, version float64, w http.ResponseWriter, r *ht
429 429
 
430 430
 func postImagesPush(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
431 431
 	authConfig := &auth.AuthConfig{}
432
-	if version > 1.1 {
433
-		if err := json.NewDecoder(r.Body).Decode(authConfig); err != nil {
434
-			return err
435
-		}
436
-	} else {
437
-		localAuthConfig, err := auth.LoadConfig(srv.runtime.root)
438
-		if err != nil && err != auth.ErrConfigFileMissing {
439
-			return err
440
-		}
441
-		authConfig = localAuthConfig
432
+	if err := json.NewDecoder(r.Body).Decode(authConfig); err != nil {
433
+		return err
442 434
 	}
443 435
 	if err := parseForm(r); err != nil {
444 436
 		return err
... ...
@@ -854,9 +865,9 @@ func createRouter(srv *Server, logging bool) (*mux.Router, error) {
854 854
 
855 855
 	m := map[string]map[string]func(*Server, float64, http.ResponseWriter, *http.Request, map[string]string) error{
856 856
 		"GET": {
857
-			"/auth":                         getAuth,
858
-			"/version":                      getVersion,
857
+			"/events":                       getEvents,
859 858
 			"/info":                         getInfo,
859
+			"/version":                      getVersion,
860 860
 			"/images/json":                  getImagesJSON,
861 861
 			"/images/viz":                   getImagesViz,
862 862
 			"/images/search":                getImagesSearch,
... ...
@@ -17,14 +17,15 @@ type APIImages struct {
17 17
 }
18 18
 
19 19
 type APIInfo struct {
20
-	Debug       bool
21
-	Containers  int
22
-	Images      int
23
-	NFd         int    `json:",omitempty"`
24
-	NGoroutines int    `json:",omitempty"`
25
-	MemoryLimit bool   `json:",omitempty"`
26
-	SwapLimit   bool   `json:",omitempty"`
27
-	LXCVersion  string `json:",omitempty"`
20
+	Debug           bool
21
+	Containers      int
22
+	Images          int
23
+	NFd             int    `json:",omitempty"`
24
+	NGoroutines     int    `json:",omitempty"`
25
+	MemoryLimit     bool   `json:",omitempty"`
26
+	SwapLimit       bool   `json:",omitempty"`
27
+	LXCVersion      string `json:",omitempty"`
28
+	NEventsListener int    `json:",omitempty"`
28 29
 }
29 30
 
30 31
 type APITop struct {
... ...
@@ -89,6 +89,44 @@ func TestGetInfo(t *testing.T) {
89 89
 	}
90 90
 }
91 91
 
92
+func TestGetEvents(t *testing.T) {
93
+	runtime := mkRuntime(t)
94
+	srv := &Server{
95
+		runtime:   runtime,
96
+		events:    make([]utils.JSONMessage, 0, 64),
97
+		listeners: make(map[string]chan utils.JSONMessage),
98
+	}
99
+
100
+	srv.LogEvent("fakeaction", "fakeid")
101
+	srv.LogEvent("fakeaction2", "fakeid")
102
+
103
+	req, err := http.NewRequest("GET", "/events?since=1", nil)
104
+	if err != nil {
105
+		t.Fatal(err)
106
+	}
107
+
108
+	r := httptest.NewRecorder()
109
+	setTimeout(t, "", 500*time.Millisecond, func() {
110
+		if err := getEvents(srv, APIVERSION, r, req, nil); err != nil {
111
+			t.Fatal(err)
112
+		}
113
+	})
114
+
115
+	dec := json.NewDecoder(r.Body)
116
+	for i := 0; i < 2; i++ {
117
+		var jm utils.JSONMessage
118
+		if err := dec.Decode(&jm); err == io.EOF {
119
+			break
120
+		} else if err != nil {
121
+			t.Fatal(err)
122
+		}
123
+		if jm != srv.events[i] {
124
+			t.Fatalf("Event received it different than expected")
125
+		}
126
+	}
127
+
128
+}
129
+
92 130
 func TestGetImagesJSON(t *testing.T) {
93 131
 	runtime := mkRuntime(t)
94 132
 	defer nuke(runtime)
... ...
@@ -25,19 +25,15 @@ var (
25 25
 )
26 26
 
27 27
 type AuthConfig struct {
28
-	Username string `json:"username"`
29
-	Password string `json:"password"`
28
+	Username string `json:"username,omitempty"`
29
+	Password string `json:"password,omitempty"`
30
+	Auth     string `json:"auth"`
30 31
 	Email    string `json:"email"`
31
-	rootPath string
32 32
 }
33 33
 
34
-func NewAuthConfig(username, password, email, rootPath string) *AuthConfig {
35
-	return &AuthConfig{
36
-		Username: username,
37
-		Password: password,
38
-		Email:    email,
39
-		rootPath: rootPath,
40
-	}
34
+type ConfigFile struct {
35
+	Configs  map[string]AuthConfig `json:"configs,omitempty"`
36
+	rootPath string
41 37
 }
42 38
 
43 39
 func IndexServerAddress() string {
... ...
@@ -54,61 +50,84 @@ func encodeAuth(authConfig *AuthConfig) string {
54 54
 }
55 55
 
56 56
 // decode the auth string
57
-func decodeAuth(authStr string) (*AuthConfig, error) {
57
+func decodeAuth(authStr string) (string, string, error) {
58 58
 	decLen := base64.StdEncoding.DecodedLen(len(authStr))
59 59
 	decoded := make([]byte, decLen)
60 60
 	authByte := []byte(authStr)
61 61
 	n, err := base64.StdEncoding.Decode(decoded, authByte)
62 62
 	if err != nil {
63
-		return nil, err
63
+		return "", "", err
64 64
 	}
65 65
 	if n > decLen {
66
-		return nil, fmt.Errorf("Something went wrong decoding auth config")
66
+		return "", "", fmt.Errorf("Something went wrong decoding auth config")
67 67
 	}
68 68
 	arr := strings.Split(string(decoded), ":")
69 69
 	if len(arr) != 2 {
70
-		return nil, fmt.Errorf("Invalid auth configuration file")
70
+		return "", "", fmt.Errorf("Invalid auth configuration file")
71 71
 	}
72 72
 	password := strings.Trim(arr[1], "\x00")
73
-	return &AuthConfig{Username: arr[0], Password: password}, nil
73
+	return arr[0], password, nil
74 74
 }
75 75
 
76 76
 // load up the auth config information and return values
77 77
 // FIXME: use the internal golang config parser
78
-func LoadConfig(rootPath string) (*AuthConfig, error) {
78
+func LoadConfig(rootPath string) (*ConfigFile, error) {
79
+	configFile := ConfigFile{Configs: make(map[string]AuthConfig), rootPath: rootPath}
79 80
 	confFile := path.Join(rootPath, CONFIGFILE)
80 81
 	if _, err := os.Stat(confFile); err != nil {
81
-		return &AuthConfig{rootPath: rootPath}, ErrConfigFileMissing
82
+		return &configFile, ErrConfigFileMissing
82 83
 	}
83 84
 	b, err := ioutil.ReadFile(confFile)
84 85
 	if err != nil {
85 86
 		return nil, err
86 87
 	}
87
-	arr := strings.Split(string(b), "\n")
88
-	if len(arr) < 2 {
89
-		return nil, fmt.Errorf("The Auth config file is empty")
90
-	}
91
-	origAuth := strings.Split(arr[0], " = ")
92
-	origEmail := strings.Split(arr[1], " = ")
93
-	authConfig, err := decodeAuth(origAuth[1])
94
-	if err != nil {
95
-		return nil, err
88
+
89
+	if err := json.Unmarshal(b, &configFile.Configs); err != nil {
90
+		arr := strings.Split(string(b), "\n")
91
+		if len(arr) < 2 {
92
+			return nil, fmt.Errorf("The Auth config file is empty")
93
+		}
94
+		authConfig := AuthConfig{}
95
+		origAuth := strings.Split(arr[0], " = ")
96
+		authConfig.Username, authConfig.Password, err = decodeAuth(origAuth[1])
97
+		if err != nil {
98
+			return nil, err
99
+		}
100
+		origEmail := strings.Split(arr[1], " = ")
101
+		authConfig.Email = origEmail[1]
102
+		configFile.Configs[IndexServerAddress()] = authConfig
103
+	} else {
104
+		for k, authConfig := range configFile.Configs {
105
+			authConfig.Username, authConfig.Password, err = decodeAuth(authConfig.Auth)
106
+			if err != nil {
107
+				return nil, err
108
+			}
109
+			authConfig.Auth = ""
110
+			configFile.Configs[k] = authConfig
111
+		}
96 112
 	}
97
-	authConfig.Email = origEmail[1]
98
-	authConfig.rootPath = rootPath
99
-	return authConfig, nil
113
+	return &configFile, nil
100 114
 }
101 115
 
102 116
 // save the auth config
103
-func SaveConfig(authConfig *AuthConfig) error {
104
-	confFile := path.Join(authConfig.rootPath, CONFIGFILE)
105
-	if len(authConfig.Email) == 0 {
117
+func SaveConfig(configFile *ConfigFile) error {
118
+	confFile := path.Join(configFile.rootPath, CONFIGFILE)
119
+	if len(configFile.Configs) == 0 {
106 120
 		os.Remove(confFile)
107 121
 		return nil
108 122
 	}
109
-	lines := "auth = " + encodeAuth(authConfig) + "\n" + "email = " + authConfig.Email + "\n"
110
-	b := []byte(lines)
111
-	err := ioutil.WriteFile(confFile, b, 0600)
123
+	for k, authConfig := range configFile.Configs {
124
+		authConfig.Auth = encodeAuth(&authConfig)
125
+		authConfig.Username = ""
126
+		authConfig.Password = ""
127
+		configFile.Configs[k] = authConfig
128
+	}
129
+
130
+	b, err := json.Marshal(configFile.Configs)
131
+	if err != nil {
132
+		return err
133
+	}
134
+	err = ioutil.WriteFile(confFile, b, 0600)
112 135
 	if err != nil {
113 136
 		return err
114 137
 	}
... ...
@@ -116,8 +135,7 @@ func SaveConfig(authConfig *AuthConfig) error {
116 116
 }
117 117
 
118 118
 // try to register/login to the registry server
119
-func Login(authConfig *AuthConfig, store bool) (string, error) {
120
-	storeConfig := false
119
+func Login(authConfig *AuthConfig) (string, error) {
121 120
 	client := &http.Client{}
122 121
 	reqStatusCode := 0
123 122
 	var status string
... ...
@@ -143,7 +161,6 @@ func Login(authConfig *AuthConfig, store bool) (string, error) {
143 143
 	if reqStatusCode == 201 {
144 144
 		status = "Account created. Please use the confirmation link we sent" +
145 145
 			" to your e-mail to activate it."
146
-		storeConfig = true
147 146
 	} else if reqStatusCode == 403 {
148 147
 		return "", fmt.Errorf("Login: Your account hasn't been activated. " +
149 148
 			"Please check your e-mail for a confirmation link.")
... ...
@@ -162,14 +179,7 @@ func Login(authConfig *AuthConfig, store bool) (string, error) {
162 162
 			}
163 163
 			if resp.StatusCode == 200 {
164 164
 				status = "Login Succeeded"
165
-				storeConfig = true
166 165
 			} else if resp.StatusCode == 401 {
167
-				if store {
168
-					authConfig.Email = ""
169
-					if err := SaveConfig(authConfig); err != nil {
170
-						return "", err
171
-					}
172
-				}
173 166
 				return "", fmt.Errorf("Wrong login/password, please try again")
174 167
 			} else {
175 168
 				return "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body,
... ...
@@ -181,10 +191,5 @@ func Login(authConfig *AuthConfig, store bool) (string, error) {
181 181
 	} else {
182 182
 		return "", fmt.Errorf("Unexpected status code [%d] : %s", reqStatusCode, reqBody)
183 183
 	}
184
-	if storeConfig && store {
185
-		if err := SaveConfig(authConfig); err != nil {
186
-			return "", err
187
-		}
188
-	}
189 184
 	return status, nil
190 185
 }
... ...
@@ -11,7 +11,9 @@ import (
11 11
 func TestEncodeAuth(t *testing.T) {
12 12
 	newAuthConfig := &AuthConfig{Username: "ken", Password: "test", Email: "test@example.com"}
13 13
 	authStr := encodeAuth(newAuthConfig)
14
-	decAuthConfig, err := decodeAuth(authStr)
14
+	decAuthConfig := &AuthConfig{}
15
+	var err error
16
+	decAuthConfig.Username, decAuthConfig.Password, err = decodeAuth(authStr)
15 17
 	if err != nil {
16 18
 		t.Fatal(err)
17 19
 	}
... ...
@@ -29,8 +31,8 @@ func TestEncodeAuth(t *testing.T) {
29 29
 func TestLogin(t *testing.T) {
30 30
 	os.Setenv("DOCKER_INDEX_URL", "https://indexstaging-docker.dotcloud.com")
31 31
 	defer os.Setenv("DOCKER_INDEX_URL", "")
32
-	authConfig := NewAuthConfig("unittester", "surlautrerivejetattendrai", "noise+unittester@dotcloud.com", "/tmp")
33
-	status, err := Login(authConfig, false)
32
+	authConfig := &AuthConfig{Username: "unittester", Password: "surlautrerivejetattendrai", Email: "noise+unittester@dotcloud.com"}
33
+	status, err := Login(authConfig)
34 34
 	if err != nil {
35 35
 		t.Fatal(err)
36 36
 	}
... ...
@@ -49,8 +51,8 @@ func TestCreateAccount(t *testing.T) {
49 49
 	}
50 50
 	token := hex.EncodeToString(tokenBuffer)[:12]
51 51
 	username := "ut" + token
52
-	authConfig := NewAuthConfig(username, "test42", "docker-ut+"+token+"@example.com", "/tmp")
53
-	status, err := Login(authConfig, false)
52
+	authConfig := &AuthConfig{Username: username, Password: "test42", Email: "docker-ut+"+token+"@example.com"}
53
+	status, err := Login(authConfig)
54 54
 	if err != nil {
55 55
 		t.Fatal(err)
56 56
 	}
... ...
@@ -60,7 +62,7 @@ func TestCreateAccount(t *testing.T) {
60 60
 		t.Fatalf("Expected status: \"%s\", found \"%s\" instead.", expectedStatus, status)
61 61
 	}
62 62
 
63
-	status, err = Login(authConfig, false)
63
+	status, err = Login(authConfig)
64 64
 	if err == nil {
65 65
 		t.Fatalf("Expected error but found nil instead")
66 66
 	}
... ...
@@ -7,6 +7,7 @@ import (
7 7
 	"github.com/dotcloud/docker/utils"
8 8
 	"io"
9 9
 	"io/ioutil"
10
+	"net/url"
10 11
 	"os"
11 12
 	"path"
12 13
 	"reflect"
... ...
@@ -201,6 +202,24 @@ func (b *buildFile) addRemote(container *Container, orig, dest string) error {
201 201
 	}
202 202
 	defer file.Body.Close()
203 203
 
204
+	// If the destination is a directory, figure out the filename.
205
+	if strings.HasSuffix(dest, "/") {
206
+		u, err := url.Parse(orig)
207
+		if err != nil {
208
+			return err
209
+		}
210
+		path := u.Path
211
+		if strings.HasSuffix(path, "/") {
212
+			path = path[:len(path)-1]
213
+		}
214
+		parts := strings.Split(path, "/")
215
+		filename := parts[len(parts)-1]
216
+		if filename == "" {
217
+			return fmt.Errorf("cannot determine filename from url: %s", u)
218
+		}
219
+		dest = dest + filename
220
+	}
221
+
204 222
 	return container.Inject(file.Body, dest)
205 223
 }
206 224
 
... ...
@@ -208,7 +227,7 @@ func (b *buildFile) addContext(container *Container, orig, dest string) error {
208 208
 	origPath := path.Join(b.context, orig)
209 209
 	destPath := path.Join(container.RootfsPath(), dest)
210 210
 	// Preserve the trailing '/'
211
-	if dest[len(dest)-1] == '/' {
211
+	if strings.HasSuffix(dest, "/") {
212 212
 		destPath = destPath + "/"
213 213
 	}
214 214
 	fi, err := os.Stat(origPath)
... ...
@@ -3,13 +3,17 @@ package docker
3 3
 import (
4 4
 	"fmt"
5 5
 	"io/ioutil"
6
+	"net"
7
+	"net/http"
8
+	"net/http/httptest"
9
+	"strings"
6 10
 	"testing"
7 11
 )
8 12
 
9 13
 // mkTestContext generates a build context from the contents of the provided dockerfile.
10 14
 // This context is suitable for use as an argument to BuildFile.Build()
11 15
 func mkTestContext(dockerfile string, files [][2]string, t *testing.T) Archive {
12
-	context, err := mkBuildContext(fmt.Sprintf(dockerfile, unitTestImageID), files)
16
+	context, err := mkBuildContext(dockerfile, files)
13 17
 	if err != nil {
14 18
 		t.Fatal(err)
15 19
 	}
... ...
@@ -22,6 +26,8 @@ type testContextTemplate struct {
22 22
 	dockerfile string
23 23
 	// Additional files in the context, eg [][2]string{"./passwd", "gordon"}
24 24
 	files [][2]string
25
+	// Additional remote files to host on a local HTTP server.
26
+	remoteFiles [][2]string
25 27
 }
26 28
 
27 29
 // A table of all the contexts to build and test.
... ...
@@ -29,27 +35,31 @@ type testContextTemplate struct {
29 29
 var testContexts = []testContextTemplate{
30 30
 	{
31 31
 		`
32
-from   %s
32
+from   {IMAGE}
33 33
 run    sh -c 'echo root:testpass > /tmp/passwd'
34 34
 run    mkdir -p /var/run/sshd
35 35
 run    [ "$(cat /tmp/passwd)" = "root:testpass" ]
36 36
 run    [ "$(ls -d /var/run/sshd)" = "/var/run/sshd" ]
37 37
 `,
38 38
 		nil,
39
+		nil,
39 40
 	},
40 41
 
41 42
 	{
42 43
 		`
43
-from %s
44
+from {IMAGE}
44 45
 add foo /usr/lib/bla/bar
45
-run [ "$(cat /usr/lib/bla/bar)" = 'hello world!' ]
46
+run [ "$(cat /usr/lib/bla/bar)" = 'hello' ]
47
+add http://{SERVERADDR}/baz /usr/lib/baz/quux
48
+run [ "$(cat /usr/lib/baz/quux)" = 'world!' ]
46 49
 `,
47
-		[][2]string{{"foo", "hello world!"}},
50
+		[][2]string{{"foo", "hello"}},
51
+		[][2]string{{"/baz", "world!"}},
48 52
 	},
49 53
 
50 54
 	{
51 55
 		`
52
-from %s
56
+from {IMAGE}
53 57
 add f /
54 58
 run [ "$(cat /f)" = "hello" ]
55 59
 add f /abc
... ...
@@ -71,38 +81,86 @@ run [ "$(cat /somewheeeere/over/the/rainbooow/ga)" = "bu" ]
71 71
 			{"f", "hello"},
72 72
 			{"d/ga", "bu"},
73 73
 		},
74
+		nil,
75
+	},
76
+
77
+	{
78
+		`
79
+from {IMAGE}
80
+add http://{SERVERADDR}/x /a/b/c
81
+run [ "$(cat /a/b/c)" = "hello" ]
82
+add http://{SERVERADDR}/x?foo=bar /
83
+run [ "$(cat /x)" = "hello" ]
84
+add http://{SERVERADDR}/x /d/
85
+run [ "$(cat /d/x)" = "hello" ]
86
+add http://{SERVERADDR} /e
87
+run [ "$(cat /e)" = "blah" ]
88
+`,
89
+		nil,
90
+		[][2]string{{"/x", "hello"}, {"/", "blah"}},
74 91
 	},
75 92
 
76 93
 	{
77 94
 		`
78
-from %s
95
+from   {IMAGE}
79 96
 env    FOO BAR
80 97
 run    [ "$FOO" = "BAR" ]
81 98
 `,
82 99
 		nil,
100
+		nil,
83 101
 	},
84 102
 
85 103
 	{
86 104
 		`
87
-from %s
105
+from {IMAGE}
88 106
 ENTRYPOINT /bin/echo
89 107
 CMD Hello world
90 108
 `,
91 109
 		nil,
110
+		nil,
92 111
 	},
93 112
 
94 113
 	{
95 114
 		`
96
-from %s
115
+from {IMAGE}
97 116
 VOLUME /test
98 117
 CMD Hello world
99 118
 `,
100 119
 		nil,
120
+		nil,
101 121
 	},
102 122
 }
103 123
 
104 124
 // FIXME: test building with 2 successive overlapping ADD commands
105 125
 
126
+func constructDockerfile(template string, ip net.IP, port string) string {
127
+	serverAddr := fmt.Sprintf("%s:%s", ip, port)
128
+	replacer := strings.NewReplacer("{IMAGE}", unitTestImageID, "{SERVERADDR}", serverAddr)
129
+	return replacer.Replace(template)
130
+}
131
+
132
+func mkTestingFileServer(files [][2]string) (*httptest.Server, error) {
133
+	mux := http.NewServeMux()
134
+	for _, file := range files {
135
+		name, contents := file[0], file[1]
136
+		mux.HandleFunc(name, func(w http.ResponseWriter, r *http.Request) {
137
+			w.Write([]byte(contents))
138
+		})
139
+	}
140
+
141
+	// This is how httptest.NewServer sets up a net.Listener, except that our listener must accept remote
142
+	// connections (from the container).
143
+	listener, err := net.Listen("tcp", ":0")
144
+	if err != nil {
145
+		return nil, err
146
+	}
147
+
148
+	s := httptest.NewUnstartedServer(mux)
149
+	s.Listener = listener
150
+	s.Start()
151
+	return s, nil
152
+}
153
+
106 154
 func TestBuild(t *testing.T) {
107 155
 	for _, ctx := range testContexts {
108 156
 		buildImage(ctx, t)
... ...
@@ -121,9 +179,24 @@ func buildImage(context testContextTemplate, t *testing.T) *Image {
121 121
 		pullingPool: make(map[string]struct{}),
122 122
 		pushingPool: make(map[string]struct{}),
123 123
 	}
124
-	buildfile := NewBuildFile(srv, ioutil.Discard, false)
125 124
 
126
-	id, err := buildfile.Build(mkTestContext(context.dockerfile, context.files, t))
125
+	httpServer, err := mkTestingFileServer(context.remoteFiles)
126
+	if err != nil {
127
+		t.Fatal(err)
128
+	}
129
+	defer httpServer.Close()
130
+
131
+	idx := strings.LastIndex(httpServer.URL, ":")
132
+	if idx < 0 {
133
+		t.Fatalf("could not get port from test http server address %s", httpServer.URL)
134
+	}
135
+	port := httpServer.URL[idx+1:]
136
+
137
+	ip := runtime.networkManager.bridgeNetwork.IP
138
+	dockerfile := constructDockerfile(context.dockerfile, ip, port)
139
+
140
+	buildfile := NewBuildFile(srv, ioutil.Discard, false)
141
+	id, err := buildfile.Build(mkTestContext(dockerfile, context.files, t))
127 142
 	if err != nil {
128 143
 		t.Fatal(err)
129 144
 	}
... ...
@@ -137,10 +210,10 @@ func buildImage(context testContextTemplate, t *testing.T) *Image {
137 137
 
138 138
 func TestVolume(t *testing.T) {
139 139
 	img := buildImage(testContextTemplate{`
140
-        from %s
140
+        from {IMAGE}
141 141
         volume /test
142 142
         cmd Hello world
143
-    `, nil}, t)
143
+    `, nil, nil}, t)
144 144
 
145 145
 	if len(img.Config.Volumes) == 0 {
146 146
 		t.Fail()
... ...
@@ -154,9 +227,9 @@ func TestVolume(t *testing.T) {
154 154
 
155 155
 func TestBuildMaintainer(t *testing.T) {
156 156
 	img := buildImage(testContextTemplate{`
157
-        from %s
157
+        from {IMAGE}
158 158
         maintainer dockerio
159
-    `, nil}, t)
159
+    `, nil, nil}, t)
160 160
 
161 161
 	if img.Author != "dockerio" {
162 162
 		t.Fail()
... ...
@@ -165,10 +238,10 @@ func TestBuildMaintainer(t *testing.T) {
165 165
 
166 166
 func TestBuildEnv(t *testing.T) {
167 167
 	img := buildImage(testContextTemplate{`
168
-        from %s
168
+        from {IMAGE}
169 169
         env port 4243
170 170
         `,
171
-		nil}, t)
171
+		nil, nil}, t)
172 172
 
173 173
 	if img.Config.Env[0] != "port=4243" {
174 174
 		t.Fail()
... ...
@@ -177,10 +250,10 @@ func TestBuildEnv(t *testing.T) {
177 177
 
178 178
 func TestBuildCmd(t *testing.T) {
179 179
 	img := buildImage(testContextTemplate{`
180
-        from %s
180
+        from {IMAGE}
181 181
         cmd ["/bin/echo", "Hello World"]
182 182
         `,
183
-		nil}, t)
183
+		nil, nil}, t)
184 184
 
185 185
 	if img.Config.Cmd[0] != "/bin/echo" {
186 186
 		t.Log(img.Config.Cmd[0])
... ...
@@ -194,10 +267,10 @@ func TestBuildCmd(t *testing.T) {
194 194
 
195 195
 func TestBuildExpose(t *testing.T) {
196 196
 	img := buildImage(testContextTemplate{`
197
-        from %s
197
+        from {IMAGE}
198 198
         expose 4243
199 199
         `,
200
-		nil}, t)
200
+		nil, nil}, t)
201 201
 
202 202
 	if img.Config.PortSpecs[0] != "4243" {
203 203
 		t.Fail()
... ...
@@ -206,10 +279,10 @@ func TestBuildExpose(t *testing.T) {
206 206
 
207 207
 func TestBuildEntrypoint(t *testing.T) {
208 208
 	img := buildImage(testContextTemplate{`
209
-        from %s
209
+        from {IMAGE}
210 210
         entrypoint ["/bin/echo"]
211 211
         `,
212
-		nil}, t)
212
+		nil, nil}, t)
213 213
 
214 214
 	if img.Config.Entrypoint[0] != "/bin/echo" {
215 215
 	}
... ...
@@ -78,6 +78,7 @@ func (cli *DockerCli) CmdHelp(args ...string) error {
78 78
 		{"build", "Build a container from a Dockerfile"},
79 79
 		{"commit", "Create a new image from a container's changes"},
80 80
 		{"diff", "Inspect changes on a container's filesystem"},
81
+		{"events", "Get real time events from the server"},
81 82
 		{"export", "Stream the contents of a container as a tar archive"},
82 83
 		{"history", "Show the history of an image"},
83 84
 		{"images", "List images"},
... ...
@@ -185,6 +186,9 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
185 185
 	} else if utils.IsURL(cmd.Arg(0)) || utils.IsGIT(cmd.Arg(0)) {
186 186
 		isRemote = true
187 187
 	} else {
188
+		if _, err := os.Stat(cmd.Arg(0)); err != nil {
189
+			return err
190
+		}
188 191
 		context, err = Tar(cmd.Arg(0), Uncompressed)
189 192
 	}
190 193
 	var body io.Reader
... ...
@@ -310,16 +314,21 @@ func (cli *DockerCli) CmdLogin(args ...string) error {
310 310
 		email    string
311 311
 	)
312 312
 
313
+	authconfig, ok := cli.configFile.Configs[auth.IndexServerAddress()]
314
+	if !ok {
315
+		authconfig = auth.AuthConfig{}
316
+	}
317
+
313 318
 	if *flUsername == "" {
314
-		fmt.Fprintf(cli.out, "Username (%s): ", cli.authConfig.Username)
319
+		fmt.Fprintf(cli.out, "Username (%s): ", authconfig.Username)
315 320
 		username = readAndEchoString(cli.in, cli.out)
316 321
 		if username == "" {
317
-			username = cli.authConfig.Username
322
+			username = authconfig.Username
318 323
 		}
319 324
 	} else {
320 325
 		username = *flUsername
321 326
 	}
322
-	if username != cli.authConfig.Username {
327
+	if username != authconfig.Username {
323 328
 		if *flPassword == "" {
324 329
 			fmt.Fprintf(cli.out, "Password: ")
325 330
 			password = readString(cli.in, cli.out)
... ...
@@ -331,31 +340,30 @@ func (cli *DockerCli) CmdLogin(args ...string) error {
331 331
 		}
332 332
 
333 333
 		if *flEmail == "" {
334
-			fmt.Fprintf(cli.out, "Email (%s): ", cli.authConfig.Email)
334
+			fmt.Fprintf(cli.out, "Email (%s): ", authconfig.Email)
335 335
 			email = readAndEchoString(cli.in, cli.out)
336 336
 			if email == "" {
337
-				email = cli.authConfig.Email
337
+				email = authconfig.Email
338 338
 			}
339 339
 		} else {
340 340
 			email = *flEmail
341 341
 		}
342 342
 	} else {
343
-		password = cli.authConfig.Password
344
-		email = cli.authConfig.Email
343
+		password = authconfig.Password
344
+		email = authconfig.Email
345 345
 	}
346 346
 	if oldState != nil {
347 347
 		term.RestoreTerminal(cli.terminalFd, oldState)
348 348
 	}
349
-	cli.authConfig.Username = username
350
-	cli.authConfig.Password = password
351
-	cli.authConfig.Email = email
349
+	authconfig.Username = username
350
+	authconfig.Password = password
351
+	authconfig.Email = email
352
+	cli.configFile.Configs[auth.IndexServerAddress()] = authconfig
352 353
 
353
-	body, statusCode, err := cli.call("POST", "/auth", cli.authConfig)
354
+	body, statusCode, err := cli.call("POST", "/auth", cli.configFile.Configs[auth.IndexServerAddress()])
354 355
 	if statusCode == 401 {
355
-		cli.authConfig.Username = ""
356
-		cli.authConfig.Password = ""
357
-		cli.authConfig.Email = ""
358
-		auth.SaveConfig(cli.authConfig)
356
+		delete(cli.configFile.Configs, auth.IndexServerAddress())
357
+		auth.SaveConfig(cli.configFile)
359 358
 		return err
360 359
 	}
361 360
 	if err != nil {
... ...
@@ -365,10 +373,10 @@ func (cli *DockerCli) CmdLogin(args ...string) error {
365 365
 	var out2 APIAuth
366 366
 	err = json.Unmarshal(body, &out2)
367 367
 	if err != nil {
368
-		auth.LoadConfig(os.Getenv("HOME"))
368
+		cli.configFile, _ = auth.LoadConfig(os.Getenv("HOME"))
369 369
 		return err
370 370
 	}
371
-	auth.SaveConfig(cli.authConfig)
371
+	auth.SaveConfig(cli.configFile)
372 372
 	if out2.Status != "" {
373 373
 		fmt.Fprintf(cli.out, "%s\n", out2.Status)
374 374
 	}
... ...
@@ -464,6 +472,7 @@ func (cli *DockerCli) CmdInfo(args ...string) error {
464 464
 		fmt.Fprintf(cli.out, "Fds: %d\n", out.NFd)
465 465
 		fmt.Fprintf(cli.out, "Goroutines: %d\n", out.NGoroutines)
466 466
 		fmt.Fprintf(cli.out, "LXC Version: %s\n", out.LXCVersion)
467
+		fmt.Fprintf(cli.out, "EventsListeners: %d\n", out.NEventsListener)
467 468
 	}
468 469
 	if !out.MemoryLimit {
469 470
 		fmt.Fprintf(cli.err, "WARNING: No memory limit support\n")
... ...
@@ -476,7 +485,7 @@ func (cli *DockerCli) CmdInfo(args ...string) error {
476 476
 
477 477
 func (cli *DockerCli) CmdStop(args ...string) error {
478 478
 	cmd := Subcmd("stop", "[OPTIONS] CONTAINER [CONTAINER...]", "Stop a running container")
479
-	nSeconds := cmd.Int("t", 10, "Number of seconds to try to stop for before killing the container. Default=10")
479
+	nSeconds := cmd.Int("t", 10, "Number of seconds to wait for the container to stop before killing it.")
480 480
 	if err := cmd.Parse(args); err != nil {
481 481
 		return nil
482 482
 	}
... ...
@@ -800,10 +809,10 @@ func (cli *DockerCli) CmdPush(args ...string) error {
800 800
 	// Custom repositories can have different rules, and we must also
801 801
 	// allow pushing by image ID.
802 802
 	if len(strings.SplitN(name, "/", 2)) == 1 {
803
-		return fmt.Errorf("Impossible to push a \"root\" repository. Please rename your repository in <user>/<repo> (ex: %s/%s)", cli.authConfig.Username, name)
803
+		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)
804 804
 	}
805 805
 
806
-	buf, err := json.Marshal(cli.authConfig)
806
+	buf, err := json.Marshal(cli.configFile.Configs[auth.IndexServerAddress()])
807 807
 	if err != nil {
808 808
 		return err
809 809
 	}
... ...
@@ -1053,6 +1062,29 @@ func (cli *DockerCli) CmdCommit(args ...string) error {
1053 1053
 	return nil
1054 1054
 }
1055 1055
 
1056
+func (cli *DockerCli) CmdEvents(args ...string) error {
1057
+	cmd := Subcmd("events", "[OPTIONS]", "Get real time events from the server")
1058
+	since := cmd.String("since", "", "Show events previously created (used for polling).")
1059
+	if err := cmd.Parse(args); err != nil {
1060
+		return nil
1061
+	}
1062
+
1063
+	if cmd.NArg() != 0 {
1064
+		cmd.Usage()
1065
+		return nil
1066
+	}
1067
+
1068
+	v := url.Values{}
1069
+	if *since != "" {
1070
+		v.Set("since", *since)
1071
+	}
1072
+
1073
+	if err := cli.stream("GET", "/events?"+v.Encode(), nil, cli.out); err != nil {
1074
+		return err
1075
+	}
1076
+	return nil
1077
+}
1078
+
1056 1079
 func (cli *DockerCli) CmdExport(args ...string) error {
1057 1080
 	cmd := Subcmd("export", "CONTAINER", "Export the contents of a filesystem as a tar archive")
1058 1081
 	if err := cmd.Parse(args); err != nil {
... ...
@@ -1408,11 +1440,11 @@ func (cli *DockerCli) CmdRun(args ...string) error {
1408 1408
 
1409 1409
 func (cli *DockerCli) checkIfLogged(action string) error {
1410 1410
 	// If condition AND the login failed
1411
-	if cli.authConfig.Username == "" {
1411
+	if cli.configFile.Configs[auth.IndexServerAddress()].Username == "" {
1412 1412
 		if err := cli.CmdLogin(""); err != nil {
1413 1413
 			return err
1414 1414
 		}
1415
-		if cli.authConfig.Username == "" {
1415
+		if cli.configFile.Configs[auth.IndexServerAddress()].Username == "" {
1416 1416
 			return fmt.Errorf("Please login prior to %s. ('docker login')", action)
1417 1417
 		}
1418 1418
 	}
... ...
@@ -1507,19 +1539,13 @@ func (cli *DockerCli) stream(method, path string, in io.Reader, out io.Writer) e
1507 1507
 	if resp.Header.Get("Content-Type") == "application/json" {
1508 1508
 		dec := json.NewDecoder(resp.Body)
1509 1509
 		for {
1510
-			var m utils.JSONMessage
1511
-			if err := dec.Decode(&m); err == io.EOF {
1510
+			var jm utils.JSONMessage
1511
+			if err := dec.Decode(&jm); err == io.EOF {
1512 1512
 				break
1513 1513
 			} else if err != nil {
1514 1514
 				return err
1515 1515
 			}
1516
-			if m.Progress != "" {
1517
-				fmt.Fprintf(out, "%s %s\r", m.Status, m.Progress)
1518
-			} else if m.Error != "" {
1519
-				return fmt.Errorf(m.Error)
1520
-			} else {
1521
-				fmt.Fprintf(out, "%s\n", m.Status)
1522
-			}
1516
+			jm.Display(out)
1523 1517
 		}
1524 1518
 	} else {
1525 1519
 		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
1668 1668
 		err = out
1669 1669
 	}
1670 1670
 
1671
-	authConfig, _ := auth.LoadConfig(os.Getenv("HOME"))
1671
+	configFile, _ := auth.LoadConfig(os.Getenv("HOME"))
1672 1672
 	return &DockerCli{
1673 1673
 		proto:      proto,
1674 1674
 		addr:       addr,
1675
-		authConfig: authConfig,
1675
+		configFile: configFile,
1676 1676
 		in:         in,
1677 1677
 		out:        out,
1678 1678
 		err:        err,
... ...
@@ -1684,7 +1710,7 @@ func NewDockerCli(in io.ReadCloser, out, err io.Writer, proto, addr string) *Doc
1684 1684
 type DockerCli struct {
1685 1685
 	proto      string
1686 1686
 	addr       string
1687
-	authConfig *auth.AuthConfig
1687
+	configFile *auth.ConfigFile
1688 1688
 	in         io.ReadCloser
1689 1689
 	out        io.Writer
1690 1690
 	err        io.Writer
... ...
@@ -38,7 +38,7 @@ func setTimeout(t *testing.T, msg string, d time.Duration, f func()) {
38 38
 		f()
39 39
 		c <- false
40 40
 	}()
41
-	if <-c {
41
+	if <-c && msg != "" {
42 42
 		t.Fatal(msg)
43 43
 	}
44 44
 }
... ...
@@ -58,25 +58,26 @@ type Container struct {
58 58
 }
59 59
 
60 60
 type Config struct {
61
-	Hostname     string
62
-	User         string
63
-	Memory       int64 // Memory limit (in bytes)
64
-	MemorySwap   int64 // Total memory usage (memory + swap); set `-1' to disable swap
65
-	CpuShares    int64 // CPU shares (relative weight vs. other containers)
66
-	AttachStdin  bool
67
-	AttachStdout bool
68
-	AttachStderr bool
69
-	PortSpecs    []string
70
-	Tty          bool // Attach standard streams to a tty, including stdin if it is not closed.
71
-	OpenStdin    bool // Open stdin
72
-	StdinOnce    bool // If true, close stdin after the 1 attached client disconnects.
73
-	Env          []string
74
-	Cmd          []string
75
-	Dns          []string
76
-	Image        string // Name of the image as it was passed by the operator (eg. could be symbolic)
77
-	Volumes      map[string]struct{}
78
-	VolumesFrom  string
79
-	Entrypoint   []string
61
+	Hostname        string
62
+	User            string
63
+	Memory          int64 // Memory limit (in bytes)
64
+	MemorySwap      int64 // Total memory usage (memory + swap); set `-1' to disable swap
65
+	CpuShares       int64 // CPU shares (relative weight vs. other containers)
66
+	AttachStdin     bool
67
+	AttachStdout    bool
68
+	AttachStderr    bool
69
+	PortSpecs       []string
70
+	Tty             bool // Attach standard streams to a tty, including stdin if it is not closed.
71
+	OpenStdin       bool // Open stdin
72
+	StdinOnce       bool // If true, close stdin after the 1 attached client disconnects.
73
+	Env             []string
74
+	Cmd             []string
75
+	Dns             []string
76
+	Image           string // Name of the image as it was passed by the operator (eg. could be symbolic)
77
+	Volumes         map[string]struct{}
78
+	VolumesFrom     string
79
+	Entrypoint      []string
80
+	NetworkDisabled bool
80 81
 }
81 82
 
82 83
 type HostConfig struct {
... ...
@@ -94,6 +95,7 @@ func ParseRun(args []string, capabilities *Capabilities) (*Config, *HostConfig,
94 94
 	cmd := Subcmd("run", "[OPTIONS] IMAGE [COMMAND] [ARG...]", "Run a command in a new container")
95 95
 	if len(args) > 0 && args[0] != "--help" {
96 96
 		cmd.SetOutput(ioutil.Discard)
97
+		cmd.Usage = nil
97 98
 	}
98 99
 
99 100
 	flHostname := cmd.String("h", "", "Container host name")
... ...
@@ -105,6 +107,7 @@ func ParseRun(args []string, capabilities *Capabilities) (*Config, *HostConfig,
105 105
 	flTty := cmd.Bool("t", false, "Allocate a pseudo-tty")
106 106
 	flMemory := cmd.Int64("m", 0, "Memory limit (in bytes)")
107 107
 	flContainerIDFile := cmd.String("cidfile", "", "Write the container ID to the file")
108
+	flNetwork := cmd.Bool("n", true, "Enable networking for this container")
108 109
 
109 110
 	if capabilities != nil && *flMemory > 0 && !capabilities.MemoryLimit {
110 111
 		//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,
173 173
 	}
174 174
 
175 175
 	config := &Config{
176
-		Hostname:     *flHostname,
177
-		PortSpecs:    flPorts,
178
-		User:         *flUser,
179
-		Tty:          *flTty,
180
-		OpenStdin:    *flStdin,
181
-		Memory:       *flMemory,
182
-		CpuShares:    *flCpuShares,
183
-		AttachStdin:  flAttach.Get("stdin"),
184
-		AttachStdout: flAttach.Get("stdout"),
185
-		AttachStderr: flAttach.Get("stderr"),
186
-		Env:          flEnv,
187
-		Cmd:          runCmd,
188
-		Dns:          flDns,
189
-		Image:        image,
190
-		Volumes:      flVolumes,
191
-		VolumesFrom:  *flVolumesFrom,
192
-		Entrypoint:   entrypoint,
176
+		Hostname:        *flHostname,
177
+		PortSpecs:       flPorts,
178
+		User:            *flUser,
179
+		Tty:             *flTty,
180
+		NetworkDisabled: !*flNetwork,
181
+		OpenStdin:       *flStdin,
182
+		Memory:          *flMemory,
183
+		CpuShares:       *flCpuShares,
184
+		AttachStdin:     flAttach.Get("stdin"),
185
+		AttachStdout:    flAttach.Get("stdout"),
186
+		AttachStderr:    flAttach.Get("stderr"),
187
+		Env:             flEnv,
188
+		Cmd:             runCmd,
189
+		Dns:             flDns,
190
+		Image:           image,
191
+		Volumes:         flVolumes,
192
+		VolumesFrom:     *flVolumesFrom,
193
+		Entrypoint:      entrypoint,
193 194
 	}
194 195
 	hostConfig := &HostConfig{
195 196
 		Binds:           binds,
... ...
@@ -510,8 +514,12 @@ func (container *Container) Start(hostConfig *HostConfig) error {
510 510
 	if err := container.EnsureMounted(); err != nil {
511 511
 		return err
512 512
 	}
513
-	if err := container.allocateNetwork(); err != nil {
514
-		return err
513
+	if container.runtime.networkManager.disabled {
514
+		container.Config.NetworkDisabled = true
515
+	} else {
516
+		if err := container.allocateNetwork(); err != nil {
517
+			return err
518
+		}
515 519
 	}
516 520
 
517 521
 	// Make sure the config is compatible with the current kernel
... ...
@@ -625,7 +633,9 @@ func (container *Container) Start(hostConfig *HostConfig) error {
625 625
 	}
626 626
 
627 627
 	// Networking
628
-	params = append(params, "-g", container.network.Gateway.String())
628
+	if !container.Config.NetworkDisabled {
629
+		params = append(params, "-g", container.network.Gateway.String())
630
+	}
629 631
 
630 632
 	// User
631 633
 	if container.Config.User != "" {
... ...
@@ -640,6 +650,7 @@ func (container *Container) Start(hostConfig *HostConfig) error {
640 640
 	params = append(params,
641 641
 		"-e", "HOME=/",
642 642
 		"-e", "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
643
+		"-e", "container=lxc",
643 644
 	)
644 645
 
645 646
 	for _, elem := range container.Config.Env {
... ...
@@ -726,6 +737,10 @@ func (container *Container) StderrPipe() (io.ReadCloser, error) {
726 726
 }
727 727
 
728 728
 func (container *Container) allocateNetwork() error {
729
+	if container.Config.NetworkDisabled {
730
+		return nil
731
+	}
732
+
729 733
 	iface, err := container.runtime.networkManager.Allocate()
730 734
 	if err != nil {
731 735
 		return err
... ...
@@ -752,6 +767,9 @@ func (container *Container) allocateNetwork() error {
752 752
 }
753 753
 
754 754
 func (container *Container) releaseNetwork() {
755
+	if container.Config.NetworkDisabled {
756
+		return
757
+	}
755 758
 	container.network.Release()
756 759
 	container.network = nil
757 760
 	container.NetworkSettings = &NetworkSettings{}
... ...
@@ -787,7 +805,9 @@ func (container *Container) monitor() {
787 787
 		}
788 788
 	}
789 789
 	utils.Debugf("Process finished")
790
-
790
+	if container.runtime != nil && container.runtime.srv != nil {
791
+		container.runtime.srv.LogEvent("die", container.ShortID())
792
+	}
791 793
 	exitCode := -1
792 794
 	if container.cmd != nil {
793 795
 		exitCode = container.cmd.ProcessState.Sys().(syscall.WaitStatus).ExitStatus()
... ...
@@ -959,6 +959,7 @@ func TestEnv(t *testing.T) {
959 959
 	goodEnv := []string{
960 960
 		"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
961 961
 		"HOME=/",
962
+		"container=lxc",
962 963
 	}
963 964
 	sort.Strings(goodEnv)
964 965
 	if len(goodEnv) != len(actualEnv) {
... ...
@@ -1251,3 +1252,41 @@ func TestRestartWithVolumes(t *testing.T) {
1251 1251
 		t.Fatalf("Expected volume path: %s Actual path: %s", expected, actual)
1252 1252
 	}
1253 1253
 }
1254
+
1255
+func TestOnlyLoopbackExistsWhenUsingDisableNetworkOption(t *testing.T) {
1256
+	runtime := mkRuntime(t)
1257
+	defer nuke(runtime)
1258
+
1259
+	config, hc, _, err := ParseRun([]string{"-n=false", GetTestImage(runtime).ID, "ip", "addr", "show"}, nil)
1260
+	if err != nil {
1261
+		t.Fatal(err)
1262
+	}
1263
+	c, err := NewBuilder(runtime).Create(config)
1264
+	if err != nil {
1265
+		t.Fatal(err)
1266
+	}
1267
+	stdout, err := c.StdoutPipe()
1268
+	if err != nil {
1269
+		t.Fatal(err)
1270
+	}
1271
+
1272
+	defer runtime.Destroy(c)
1273
+	if err := c.Start(hc); err != nil {
1274
+		t.Fatal(err)
1275
+	}
1276
+	c.WaitTimeout(500 * time.Millisecond)
1277
+	c.Wait()
1278
+	output, err := ioutil.ReadAll(stdout)
1279
+	if err != nil {
1280
+		t.Fatal(err)
1281
+	}
1282
+
1283
+	interfaces := regexp.MustCompile(`(?m)^[0-9]+: [a-zA-Z0-9]+`).FindAllString(string(output), -1)
1284
+	if len(interfaces) != 1 {
1285
+		t.Fatalf("Wrong interface count in test container: expected [1: lo], got [%s]", interfaces)
1286
+	}
1287
+	if interfaces[0] != "1: lo" {
1288
+		t.Fatalf("Wrong interface in test container: expected [1: lo], got [%s]", interfaces)
1289
+	}
1290
+
1291
+}
... ...
@@ -28,7 +28,7 @@ func main() {
28 28
 	flDaemon := flag.Bool("d", false, "Daemon mode")
29 29
 	flDebug := flag.Bool("D", false, "Debug mode")
30 30
 	flAutoRestart := flag.Bool("r", false, "Restart previously running containers")
31
-	bridgeName := flag.String("b", "", "Attach containers to a pre-existing network bridge")
31
+	bridgeName := flag.String("b", "", "Attach containers to a pre-existing network bridge. Use 'none' to disable container networking")
32 32
 	pidfile := flag.String("p", "/var/run/docker.pid", "File containing process PID")
33 33
 	flGraphPath := flag.String("g", "/var/lib/docker", "Path to graph storage base dir.")
34 34
 	flEnableCors := flag.Bool("api-enable-cors", false, "Enable CORS requests in the remote api.")
... ...
@@ -2,6 +2,9 @@
2 2
 :description: API Documentation for Docker
3 3
 :keywords: API, Docker, rcli, REST, documentation
4 4
 
5
+.. COMMENT use http://pythonhosted.org/sphinxcontrib-httpdomain/ to
6
+.. document the REST API.
7
+
5 8
 =================
6 9
 Docker Remote API
7 10
 =================
... ...
@@ -13,15 +16,23 @@ Docker Remote API
13 13
 
14 14
 - The Remote API is replacing rcli
15 15
 - Default port in the docker deamon is 4243 
16
-- 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
17
-- 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
16
+- The API tends to be REST, but for some complex commands, like attach
17
+  or pull, the HTTP connection is hijacked to transport stdout stdin
18
+  and stderr
19
+- Since API version 1.2, the auth configuration is now handled client
20
+  side, so the client has to send the authConfig as POST in
21
+  /images/(name)/push
18 22
 
19 23
 2. Versions
20 24
 ===========
21 25
 
22
-The current verson of the API is 1.3
23
-Calling /images/<name>/insert is the same as calling /v1.3/images/<name>/insert
24
-You can still call an old version of the api using /v1.0/images/<name>/insert
26
+The current verson of the API is 1.3 
27
+
28
+Calling /images/<name>/insert is the same as calling
29
+/v1.3/images/<name>/insert 
30
+
31
+You can still call an old version of the api using
32
+/v1.0/images/<name>/insert
25 33
 
26 34
 :doc:`docker_remote_api_v1.3`
27 35
 *****************************
... ...
@@ -29,19 +40,25 @@ You can still call an old version of the api using /v1.0/images/<name>/insert
29 29
 What's new
30 30
 ----------
31 31
 
32
-Listing processes (/top):
32
+.. http:get:: /containers/(id)/top
33 33
 
34
-- List the processes inside a container
34
+   **New!** List the processes running inside a container.
35 35
 
36
+.. http:get:: /events:
37
+
38
+   **New!** Monitor docker's events via streaming or via polling
36 39
 
37 40
 Builder (/build):
38 41
 
39 42
 - Simplify the upload of the build context
40
-- Simply stream a tarball instead of multipart upload with 4 intermediary buffers
43
+- Simply stream a tarball instead of multipart upload with 4
44
+  intermediary buffers
41 45
 - Simpler, less memory usage, less disk usage and faster
42 46
 
43
-.. Note::
44
-The /build improvements are not reverse-compatible. Pre 1.3 clients will break on /build.
47
+.. Warning::
48
+
49
+  The /build improvements are not reverse-compatible. Pre 1.3 clients
50
+  will break on /build.
45 51
 
46 52
 List containers (/containers/json):
47 53
 
... ...
@@ -49,7 +66,8 @@ List containers (/containers/json):
49 49
 
50 50
 Start containers (/containers/<id>/start):
51 51
 
52
-- You can now pass host-specific configuration (e.g. bind mounts) in the POST body for start calls 
52
+- You can now pass host-specific configuration (e.g. bind mounts) in
53
+  the POST body for start calls
53 54
 
54 55
 :doc:`docker_remote_api_v1.2`
55 56
 *****************************
... ...
@@ -60,14 +78,25 @@ What's new
60 60
 ----------
61 61
 
62 62
 The auth configuration is now handled by the client.
63
-The client should send it's authConfig as POST on each call of /images/(name)/push
64 63
 
65
-.. http:get:: /auth is now deprecated
66
-.. http:post:: /auth only checks the configuration but doesn't store it on the server
64
+The client should send it's authConfig as POST on each call of
65
+/images/(name)/push
66
+
67
+.. http:get:: /auth 
68
+
69
+  **Deprecated.**
70
+
71
+.. http:post:: /auth 
72
+
73
+  Only checks the configuration but doesn't store it on the server
74
+
75
+  Deleting an image is now improved, will only untag the image if it
76
+  has chidren and remove all the untagged parents if has any.
67 77
 
68
-Deleting an image is now improved, will only untag the image if it has chidrens and remove all the untagged parents if has any.
78
+.. http:post:: /images/<name>/delete 
69 79
 
70
-.. http:post:: /images/<name>/delete now returns a JSON with the list of images deleted/untagged
80
+  Now returns a JSON structure with the list of images
81
+  deleted/untagged.
71 82
 
72 83
 
73 84
 :doc:`docker_remote_api_v1.1`
... ...
@@ -82,7 +111,7 @@ What's new
82 82
 .. http:post:: /images/(name)/insert
83 83
 .. http:post:: /images/(name)/push
84 84
 
85
-Uses json stream instead of HTML hijack, it looks like this:
85
+   Uses json stream instead of HTML hijack, it looks like this:
86 86
 
87 87
         .. sourcecode:: http
88 88
 
... ...
@@ -1,3 +1,8 @@
1
+.. use orphan to suppress "WARNING: document isn't included in any toctree"
2
+.. per http://sphinx-doc.org/markup/misc.html#file-wide-metadata
3
+
4
+:orphan:
5
+
1 6
 :title: Remote API v1.0
2 7
 :description: API Documentation for Docker
3 8
 :keywords: API, Docker, rcli, REST, documentation
... ...
@@ -300,8 +305,8 @@ Start a container
300 300
 	:statuscode 500: server error
301 301
 
302 302
 
303
-Stop a contaier
304
-***************
303
+Stop a container
304
+****************
305 305
 
306 306
 .. http:post:: /containers/(id)/stop
307 307
 
... ...
@@ -1,3 +1,7 @@
1
+.. use orphan to suppress "WARNING: document isn't included in any toctree"
2
+.. per http://sphinx-doc.org/markup/misc.html#file-wide-metadata
3
+
4
+:orphan:
1 5
 
2 6
 :title: Remote API v1.1
3 7
 :description: API Documentation for Docker
... ...
@@ -1,3 +1,8 @@
1
+.. use orphan to suppress "WARNING: document isn't included in any toctree"
2
+.. per http://sphinx-doc.org/markup/misc.html#file-wide-metadata
3
+
4
+:orphan:
5
+
1 6
 :title: Remote API v1.2
2 7
 :description: API Documentation for Docker
3 8
 :keywords: API, Docker, rcli, REST, documentation
... ...
@@ -1,3 +1,8 @@
1
+.. use orphan to suppress "WARNING: document isn't included in any toctree"
2
+.. per http://sphinx-doc.org/markup/misc.html#file-wide-metadata
3
+
4
+:orphan:
5
+
1 6
 :title: Remote API v1.3
2 7
 :description: API Documentation for Docker
3 8
 :keywords: API, Docker, rcli, REST, documentation
... ...
@@ -1054,6 +1059,36 @@ Create a new image from a container's changes
1054 1054
         :statuscode 500: server error
1055 1055
 
1056 1056
 
1057
+Monitor Docker's events
1058
+***********************
1059
+
1060
+.. http:get:: /events
1061
+
1062
+	Get events from docker, either in real time via streaming, or via polling (using `since`)
1063
+
1064
+	**Example request**:
1065
+
1066
+	.. sourcecode:: http
1067
+
1068
+           POST /events?since=1374067924
1069
+
1070
+        **Example response**:
1071
+
1072
+        .. sourcecode:: http
1073
+
1074
+           HTTP/1.1 200 OK
1075
+	   Content-Type: application/json
1076
+
1077
+	   {"status":"create","id":"dfdf82bd3881","time":1374067924}
1078
+	   {"status":"start","id":"dfdf82bd3881","time":1374067924}
1079
+	   {"status":"stop","id":"dfdf82bd3881","time":1374067966}
1080
+	   {"status":"destroy","id":"dfdf82bd3881","time":1374067970}
1081
+
1082
+	:query since: timestamp used for polling
1083
+        :statuscode 200: no error
1084
+        :statuscode 500: server error
1085
+
1086
+
1057 1087
 3. Going further
1058 1088
 ================
1059 1089
 
... ...
@@ -452,7 +452,7 @@ User Register
452 452
          "username": "foobar"'}
453 453
 
454 454
     :jsonparameter email: valid email address, that needs to be confirmed
455
-    :jsonparameter username: min 4 character, max 30 characters, must match the regular expression [a-z0-9_].
455
+    :jsonparameter username: min 4 character, max 30 characters, must match the regular expression [a-z0-9\_].
456 456
     :jsonparameter password: min 5 characters
457 457
 
458 458
     **Example Response**:
... ...
@@ -367,7 +367,8 @@ POST /v1/users
367 367
     {"email": "sam@dotcloud.com", "password": "toto42", "username": "foobar"'}
368 368
 
369 369
 **Validation**:
370
-    - **username** : min 4 character, max 30 characters, must match the regular expression [a-z0-9_].
370
+    - **username**: min 4 character, max 30 characters, must match the regular
371
+      expression [a-z0-9\_].
371 372
     - **password**: min 5 characters
372 373
 
373 374
 **Valid**: return HTTP 200
... ...
@@ -566,4 +567,4 @@ Next request::
566 566
 ---------------------
567 567
 
568 568
 - 1.0 : May 6th 2013 : initial release 
569
-- 1.1 : June 1st 2013 : Added Delete Repository and way to handle new source namespace.
570 569
\ No newline at end of file
570
+- 1.1 : June 1st 2013 : Added Delete Repository and way to handle new source namespace.
... ...
@@ -20,6 +20,7 @@
20 20
       -h="": Container host name
21 21
       -i=false: Keep stdin open even if not attached
22 22
       -m=0: Memory limit (in bytes)
23
+      -n=true: Enable networking for this container
23 24
       -p=[]: Map a network port to the container
24 25
       -t=false: Allocate a pseudo-tty
25 26
       -u="": Username or UID
... ...
@@ -8,6 +8,8 @@
8 8
 
9 9
 ::
10 10
 
11
-    Usage: docker stop [OPTIONS] NAME
11
+    Usage: docker stop [OPTIONS] CONTAINER [CONTAINER...]
12 12
 
13 13
     Stop a running container
14
+
15
+      -t=10: Number of seconds to wait for the container to stop before killing it.
... ...
@@ -37,5 +37,6 @@ Contents:
37 37
   start   <command/start>
38 38
   stop    <command/stop>
39 39
   tag     <command/tag>
40
+  top     <command/top>
40 41
   version <command/version>
41
-  wait    <command/wait>
42 42
\ No newline at end of file
43
+  wait    <command/wait>
... ...
@@ -46,11 +46,13 @@ in a standard build environment.
46 46
 You can run an interactive session in the newly built container:
47 47
 
48 48
 ::
49
+
49 50
     docker run -i -t docker bash
50 51
 
51 52
 
52 53
 To extract the binaries from the container:
53 54
 
54 55
 ::
56
+
55 57
     docker run docker sh -c 'cat $(which docker)' > docker-build && chmod +x docker-build
56 58
 
... ...
@@ -2,8 +2,6 @@
2 2
 :description: An overview of the Docker Documentation
3 3
 :keywords: containers, lxc, concepts, explanation
4 4
 
5
-.. _introduction:
6
-
7 5
 Welcome
8 6
 =======
9 7
 
... ...
@@ -51,7 +51,7 @@ For example:
51 51
 .. code-block:: bash
52 52
 
53 53
    # Run docker in daemon mode
54
-   sudo <path to>/docker -H 0.0.0.0:5555 &
54
+   sudo <path to>/docker -H 0.0.0.0:5555 -d &
55 55
    # Download a base image
56 56
    docker -H :5555 pull base
57 57
 
... ...
@@ -61,7 +61,7 @@ on both tcp and a unix socket
61 61
 .. code-block:: bash
62 62
 
63 63
    # Run docker in daemon mode
64
-   sudo <path to>/docker -H tcp://127.0.0.1:4243 -H unix:///var/run/docker.sock
64
+   sudo <path to>/docker -H tcp://127.0.0.1:4243 -H unix:///var/run/docker.sock -d &
65 65
    # Download a base image
66 66
    docker pull base
67 67
    # OR
... ...
@@ -1,6 +1,6 @@
1
-:title: Dockerfile Builder
2
-:description: Docker Builder specifes a simple DSL which allows you to automate the steps you would normally manually take to create an image.
3
-:keywords: builder, docker, Docker Builder, automation, image creation
1
+:title: Dockerfiles for Images
2
+:description: Dockerfiles use a simple DSL which allows you to automate the steps you would normally manually take to create an image.
3
+:keywords: builder, docker, Dockerfile, automation, image creation
4 4
 
5 5
 ==================
6 6
 Dockerfile Builder
... ...
@@ -30,7 +30,7 @@ build succeeds:
30 30
 
31 31
     ``docker build -t shykes/myapp .``
32 32
 
33
-Docker will run your steps one-by-one, committing the result if necessary, 
33
+Docker will run your steps one-by-one, committing the result if necessary,
34 34
 before finally outputting the ID of your new image.
35 35
 
36 36
 2. Format
... ...
@@ -43,7 +43,7 @@ The Dockerfile format is quite simple:
43 43
     # Comment
44 44
     INSTRUCTION arguments
45 45
 
46
-The Instruction is not case-sensitive, however convention is for them to be 
46
+The Instruction is not case-sensitive, however convention is for them to be
47 47
 UPPERCASE in order to distinguish them from arguments more easily.
48 48
 
49 49
 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
106 106
 the image.  This is functionally equivalent to running ``docker commit
107 107
 -run '{"Cmd": <command>}'`` outside the builder.
108 108
 
109
-.. note:: 
109
+.. note::
110 110
     Don't confuse `RUN` with `CMD`. `RUN` actually runs a
111 111
     command and commits the result; `CMD` does not execute anything at
112 112
     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``
131 131
 instructions. This is functionally equivalent to prefixing the command
132 132
 with ``<key>=<value>``
133 133
 
134
-.. note:: 
134
+.. note::
135 135
     The environment variables will persist when a container is run
136 136
     from the resulting image.
137 137
 
... ...
@@ -152,16 +152,24 @@ destination container.
152 152
 
153 153
 The copy obeys the following rules:
154 154
 
155
+* If ``<src>`` is a URL and ``<dest>`` does not end with a trailing slash,
156
+  then a file is downloaded from the URL and copied to ``<dest>``.
157
+* If ``<src>`` is a URL and ``<dest>`` does end with a trailing slash,
158
+  then the filename is inferred from the URL and the file is downloaded to
159
+  ``<dest>/<filename>``. For instance, ``ADD http://example.com/foobar /``
160
+  would create the file ``/foobar``. The URL must have a nontrivial path
161
+  so that an appropriate filename can be discovered in this case
162
+  (``http://example.com`` will not work).
155 163
 * If ``<src>`` is a directory, the entire directory is copied,
156 164
   including filesystem metadata.
157 165
 * If ``<src>``` is a tar archive in a recognized compression format
158 166
   (identity, gzip, bzip2 or xz), it is unpacked as a directory.
159 167
 
160 168
   When a directory is copied or unpacked, it has the same behavior as
161
-  ``tar -x``: the result is the union of 
169
+  ``tar -x``: the result is the union of
162 170
 
163 171
   1. whatever existed at the destination path and
164
-  2. the contents of the source tree, 
172
+  2. the contents of the source tree,
165 173
 
166 174
   with conflicts resolved in favor of 2) on a file-by-file basis.
167 175
 
... ...
@@ -177,9 +185,9 @@ The copy obeys the following rules:
177 177
   with mode 0700, uid and gid 0.
178 178
 
179 179
 3.8 ENTRYPOINT
180
+--------------
180 181
 
181
-    ``ENTRYPOINT /bin/echo``
182
+    ``ENTRYPOINT ["/bin/echo"]``
182 183
 
183 184
 The ``ENTRYPOINT`` instruction adds an entry command that will not be
184 185
 overwritten when arguments are passed to docker run, unlike the
... ...
@@ -203,14 +211,14 @@ container created from the image.
203 203
     # Nginx
204 204
     #
205 205
     # VERSION               0.0.1
206
-    
206
+
207 207
     FROM      ubuntu
208 208
     MAINTAINER Guillaume J. Charmes "guillaume@dotcloud.com"
209
-    
209
+
210 210
     # make sure the package repository is up to date
211 211
     RUN echo "deb http://archive.ubuntu.com/ubuntu precise main universe" > /etc/apt/sources.list
212 212
     RUN apt-get update
213
-    
213
+
214 214
     RUN apt-get install -y inotify-tools nginx apache2 openssh-server
215 215
 
216 216
 .. code-block:: bash
... ...
@@ -218,12 +226,12 @@ container created from the image.
218 218
     # Firefox over VNC
219 219
     #
220 220
     # VERSION               0.3
221
-    
221
+
222 222
     FROM ubuntu
223 223
     # make sure the package repository is up to date
224 224
     RUN echo "deb http://archive.ubuntu.com/ubuntu precise main universe" > /etc/apt/sources.list
225 225
     RUN apt-get update
226
-    
226
+
227 227
     # Install vnc, xvfb in order to create a 'fake' display and firefox
228 228
     RUN apt-get install -y x11vnc xvfb firefox
229 229
     RUN mkdir /.vnc
... ...
@@ -231,7 +239,7 @@ container created from the image.
231 231
     RUN x11vnc -storepasswd 1234 ~/.vnc/passwd
232 232
     # Autostart firefox (might not be the best way, but it does the trick)
233 233
     RUN bash -c 'echo "firefox" >> /.bashrc'
234
-    
234
+
235 235
     EXPOSE 5900
236 236
     CMD    ["x11vnc", "-forever", "-usepw", "-create"]
237 237
 
... ...
@@ -119,7 +119,7 @@ your container to an image within your username namespace.
119 119
 
120 120
 
121 121
 Pushing a container to its repository
122
+-------------------------------------
122 123
 
123 124
 In order to push an image to its repository you need to have committed
124 125
 your container to a named image (see above)
... ...
@@ -68,19 +68,18 @@
68 68
 
69 69
             <div style="float: right" class="pull-right">
70 70
                 <ul class="nav">
71
-                    <li id="nav-introduction"><a href="http://www.docker.io/">Introduction</a></li>
71
+                    <li id="nav-introduction"><a href="http://www.docker.io/">Home</a></li>
72
+                    <li id="nav-about"><a href="http://www.docker.io/">About</a></li>
73
+                    <li id="nav-community"><a href="http://www.docker.io/">Community</a></li>
72 74
                     <li id="nav-gettingstarted"><a href="http://www.docker.io/gettingstarted/">Getting started</a></li>
73 75
                     <li id="nav-documentation" class="active"><a href="http://docs.docker.io/en/latest/">Documentation</a></li>
74 76
                     <li id="nav-blog"><a href="http://blog.docker.io/">Blog</a></li>
77
+                    <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>
75 78
                 </ul>
76
-                <!--<div class="social links" style="float: right; margin-top: 14px; margin-left: 12px">-->
77
-                    <!--<a class="twitter" href="http://twitter.com/getdocker">Twitter</a>-->
78
-                    <!--<a class="github" href="https://github.com/dotcloud/docker/">GitHub</a>-->
79
-                <!--</div>-->
80 79
             </div>
81 80
 
82 81
             <div style="margin-left: -12px; float: left;">
83
-                <a href="http://www.docker.io"><img style="margin-top: 12px; height: 38px" src="{{ pathto('_static/img/docker-letters-logo.gif', 1) }}"></a>
82
+                <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>
84 83
             </div>
85 84
         </div>
86 85
 
... ...
@@ -96,7 +95,7 @@
96 96
             <div class="pull-right" id="fork-us" style="margin-top: 16px; margin-right: 16px;">
97 97
                 <a  href="http://github.com/dotcloud/docker/"><img src="{{ pathto('_static/img/fork-us.png', 1) }}"> Fork us on Github</a>
98 98
             </div>
99
-            <h1 class="pageheader">DOCUMENTATION</h1>
99
+            <h1 class="pageheader"><a href="http://docs.docker.io/en/latest/" title="Documentation" style="color: white;">DOCUMENTATION</a></h1>
100 100
 
101 101
         </div>
102 102
     </div>
... ...
@@ -34,12 +34,12 @@ h4 {
34 34
 .navbar .nav li a {
35 35
   padding: 22px 15px 22px;
36 36
 }
37
-.navbar .brand {
38
-  padding: 13px 10px 13px 28px ;
39
-}
40 37
 .navbar-dotcloud .container {
41 38
   border-bottom: 2px #000000 solid;
42 39
 }
40
+.inline-icon {
41
+  margin-bottom: 6px;
42
+}
43 43
 /*
44 44
 * Responsive YouTube, Vimeo, Embed, and HTML5 Videos with CSS
45 45
 * http://www.jonsuh.com
... ...
@@ -82,7 +82,7 @@ h4 {
82 82
 .btn-custom {
83 83
   background-color: #292929 !important;
84 84
   background-repeat: repeat-x;
85
-  filter: progid:dximagetransform.microsoft.gradient(startColorstr="#515151", endColorstr="#282828");
85
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr="#515151", endColorstr="#282828");
86 86
   background-image: -khtml-gradient(linear, left top, left bottom, from(#515151), to(#282828));
87 87
   background-image: -moz-linear-gradient(top, #515151, #282828);
88 88
   background-image: -ms-linear-gradient(top, #515151, #282828);
... ...
@@ -301,7 +301,7 @@ section.header {
301 301
   height: 28px;
302 302
   line-height: 28px;
303 303
   background-color: #43484c;
304
-  filter: progid:dximagetransform.microsoft.gradient(gradientType=0, startColorstr='#FFFF6E56', endColorstr='#FFED4F35');
304
+  filter: progid:DXImageTransform.Microsoft.gradient(gradientType=0, startColorstr='#FFFF6E56', endColorstr='#FFED4F35');
305 305
   background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #747474), color-stop(100%, #43484c));
306 306
   background-image: -webkit-linear-gradient(top, #747474 0%, #43484c 100%);
307 307
   background-image: -moz-linear-gradient(top, #747474 0%, #43484c 100%);
... ...
@@ -53,13 +53,6 @@ h1, h2, h3, h4 {
53 53
       padding: 22px 15px 22px;
54 54
     }
55 55
   }
56
-
57
-  .brand {
58
-    padding: 13px 10px 13px 28px ;
59
-  // padding-left: 30px;
60
-
61
-  }
62
-
63 56
   background-color: white;
64 57
 }
65 58
 
... ...
@@ -67,6 +60,9 @@ h1, h2, h3, h4 {
67 67
   border-bottom: 2px @black solid;
68 68
 }
69 69
 
70
+.inline-icon {
71
+  margin-bottom: 6px;
72
+}
70 73
 
71 74
 /*
72 75
 * Responsive YouTube, Vimeo, Embed, and HTML5 Videos with CSS
73 76
new file mode 100644
74 77
Binary files /dev/null and b/docs/theme/docker/static/img/docker-top-logo.png differ
75 78
new file mode 100644
76 79
Binary files /dev/null and b/docs/theme/docker/static/img/external-link-icon.png differ
77 80
new file mode 100644
... ...
@@ -0,0 +1,91 @@
0
+# Docker maintainer bootcamp
1
+
2
+## Introduction: we need more maintainers
3
+
4
+Docker is growing incredibly fast. At the time of writing, it has received over 200 contributions from 90 people,
5
+and its API is used by dozens of 3rd-party tools. Over 1,000 issues have been opened. As the first production deployments
6
+start going live, the growth will only accelerate.
7
+
8
+Also at the time of writing, Docker has 3 full-time maintainers, and 7 part-time subsystem maintainers. If docker
9
+is going to live up to the expectations, we need more than that.
10
+
11
+This document describes a *bootcamp* to guide and train volunteers interested in helping the project, either with individual
12
+contributions, maintainer work, or both.
13
+
14
+This bootcamp is an experiment. If you decide to go through it, consider yourself an alpha-tester. You should expect quirks,
15
+and report them to us as you encounter them to help us smooth out the process.
16
+
17
+
18
+## How it works
19
+
20
+The maintainer bootcamp is a 12-step program - one step for each of the maintainer's responsibilities. The aspiring maintainer must
21
+validate all 12 steps by 1) studying it, 2) practicing it, and 3) getting endorsed for it.
22
+
23
+Steps are all equally important and can be validated in any order. Validating all 12 steps is a pre-requisite for becoming a core
24
+maintainer, but even 1 step will make you a better contributor!
25
+
26
+### List of steps
27
+
28
+#### 1) Be a power user
29
+
30
+Use docker daily, build cool things with it, know its quirks inside and out.
31
+
32
+
33
+#### 2) Help users
34
+
35
+Answer questions on irc, twitter, email, in person.
36
+
37
+
38
+#### 3) Manage the bug tracker
39
+
40
+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.
41
+
42
+
43
+#### 4) Improve the documentation
44
+
45
+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.
46
+
47
+
48
+#### 5) Evangelize the principles of docker
49
+
50
+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.
51
+
52
+
53
+#### 6) Fix bugs
54
+
55
+Self-explanatory. Contribute improvements to docker which solve defects. Bugfixes should be well-tested, and prioritized by impact to the user.
56
+
57
+
58
+#### 7) Improve the testing infrastructure
59
+
60
+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...
61
+
62
+
63
+#### 8) Contribute features
64
+
65
+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.
66
+
67
+
68
+#### 9) Refactor internals
69
+
70
+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.
71
+
72
+#### 10) Review and merge contributions
73
+
74
+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.
75
+
76
+#### 11) Release
77
+
78
+Manage a release of docker from beginning to end. Tests, final review, tags, builds, upload to mirrors, distro packaging, etc.
79
+
80
+#### 12) Train other maintainers
81
+
82
+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.
83
+
84
+### How to study a step
85
+
86
+### How to practice a step
87
+
88
+### How to get endorsed for a step
89
+
90
+
... ...
@@ -13,6 +13,10 @@ lxc.utsname = {{.Id}}
13 13
 {{end}}
14 14
 #lxc.aa_profile = unconfined
15 15
 
16
+{{if .Config.NetworkDisabled}}
17
+# network is disabled (-n=false)
18
+lxc.network.type = empty
19
+{{else}}
16 20
 # network configuration
17 21
 lxc.network.type = veth
18 22
 lxc.network.flags = up
... ...
@@ -20,6 +24,7 @@ lxc.network.link = {{.NetworkSettings.Bridge}}
20 20
 lxc.network.name = eth0
21 21
 lxc.network.mtu = 1500
22 22
 lxc.network.ipv4 = {{.NetworkSettings.IPAddress}}/{{.NetworkSettings.IPPrefixLen}}
23
+{{end}}
23 24
 
24 25
 # root filesystem
25 26
 {{$ROOTFS := .RootfsPath}}
... ...
@@ -17,6 +17,7 @@ var NetworkBridgeIface string
17 17
 
18 18
 const (
19 19
 	DefaultNetworkBridge = "docker0"
20
+	DisableNetworkBridge = "none"
20 21
 	portRangeStart       = 49153
21 22
 	portRangeEnd         = 65535
22 23
 )
... ...
@@ -111,10 +112,29 @@ func checkRouteOverlaps(dockerNetwork *net.IPNet) error {
111 111
 	return nil
112 112
 }
113 113
 
114
+// CreateBridgeIface creates a network bridge interface on the host system with the name `ifaceName`,
115
+// and attempts to configure it with an address which doesn't conflict with any other interface on the host.
116
+// If it can't find an address which doesn't conflict, it will return an error.
114 117
 func CreateBridgeIface(ifaceName string) error {
115
-	// FIXME: try more IP ranges
116
-	// FIXME: try bigger ranges! /24 is too small.
117
-	addrs := []string{"172.16.42.1/24", "10.0.42.1/24", "192.168.42.1/24"}
118
+	addrs := []string{
119
+		// Here we don't follow the convention of using the 1st IP of the range for the gateway.
120
+		// This is to use the same gateway IPs as the /24 ranges, which predate the /16 ranges.
121
+		// In theory this shouldn't matter - in practice there's bound to be a few scripts relying
122
+		// on the internal addressing or other stupid things like that.
123
+		// The shouldn't, but hey, let's not break them unless we really have to.
124
+		"172.16.42.1/16",
125
+		"10.0.42.1/16", // Don't even try using the entire /8, that's too intrusive
126
+		"10.1.42.1/16",
127
+		"10.42.42.1/16",
128
+		"172.16.42.1/24",
129
+		"172.16.43.1/24",
130
+		"172.16.44.1/24",
131
+		"10.0.42.1/24",
132
+		"10.0.43.1/24",
133
+		"192.168.42.1/24",
134
+		"192.168.43.1/24",
135
+		"192.168.44.1/24",
136
+	}
118 137
 
119 138
 	var ifaceAddr string
120 139
 	for _, addr := range addrs {
... ...
@@ -453,10 +473,16 @@ type NetworkInterface struct {
453 453
 
454 454
 	manager  *NetworkManager
455 455
 	extPorts []*Nat
456
+	disabled bool
456 457
 }
457 458
 
458 459
 // Allocate an external TCP port and map it to the interface
459 460
 func (iface *NetworkInterface) AllocatePort(spec string) (*Nat, error) {
461
+
462
+	if iface.disabled {
463
+		return nil, fmt.Errorf("Trying to allocate port for interface %v, which is disabled", iface) // FIXME
464
+	}
465
+
460 466
 	nat, err := parseNat(spec)
461 467
 	if err != nil {
462 468
 		return nil, err
... ...
@@ -552,6 +578,11 @@ func parseNat(spec string) (*Nat, error) {
552 552
 
553 553
 // Release: Network cleanup - release all resources
554 554
 func (iface *NetworkInterface) Release() {
555
+
556
+	if iface.disabled {
557
+		return
558
+	}
559
+
555 560
 	for _, nat := range iface.extPorts {
556 561
 		utils.Debugf("Unmaping %v/%v", nat.Proto, nat.Frontend)
557 562
 		if err := iface.manager.portMapper.Unmap(nat.Frontend, nat.Proto); err != nil {
... ...
@@ -579,10 +610,17 @@ type NetworkManager struct {
579 579
 	tcpPortAllocator *PortAllocator
580 580
 	udpPortAllocator *PortAllocator
581 581
 	portMapper       *PortMapper
582
+
583
+	disabled bool
582 584
 }
583 585
 
584 586
 // Allocate a network interface
585 587
 func (manager *NetworkManager) Allocate() (*NetworkInterface, error) {
588
+
589
+	if manager.disabled {
590
+		return &NetworkInterface{disabled: true}, nil
591
+	}
592
+
586 593
 	ip, err := manager.ipAllocator.Acquire()
587 594
 	if err != nil {
588 595
 		return nil, err
... ...
@@ -596,6 +634,14 @@ func (manager *NetworkManager) Allocate() (*NetworkInterface, error) {
596 596
 }
597 597
 
598 598
 func newNetworkManager(bridgeIface string) (*NetworkManager, error) {
599
+
600
+	if bridgeIface == DisableNetworkBridge {
601
+		manager := &NetworkManager{
602
+			disabled: true,
603
+		}
604
+		return manager, nil
605
+	}
606
+
599 607
 	addr, err := getIfaceAddr(bridgeIface)
600 608
 	if err != nil {
601 609
 		// If the iface is not found, try to create it
... ...
@@ -17,12 +17,12 @@ import (
17 17
 )
18 18
 
19 19
 const (
20
-	unitTestImageName	= "docker-test-image"
21
-	unitTestImageID		= "83599e29c455eb719f77d799bc7c51521b9551972f5a850d7ad265bc1b5292f6" // 1.0
22
-	unitTestNetworkBridge	= "testdockbr0"
23
-	unitTestStoreBase	= "/var/lib/docker/unit-tests"
24
-	testDaemonAddr		= "127.0.0.1:4270"
25
-	testDaemonProto		= "tcp"
20
+	unitTestImageName     = "docker-test-image"
21
+	unitTestImageID       = "83599e29c455eb719f77d799bc7c51521b9551972f5a850d7ad265bc1b5292f6" // 1.0
22
+	unitTestNetworkBridge = "testdockbr0"
23
+	unitTestStoreBase     = "/var/lib/docker/unit-tests"
24
+	testDaemonAddr        = "127.0.0.1:4270"
25
+	testDaemonProto       = "tcp"
26 26
 )
27 27
 
28 28
 var globalRuntime *Runtime
... ...
@@ -19,6 +19,7 @@ import (
19 19
 	"runtime"
20 20
 	"strings"
21 21
 	"sync"
22
+	"time"
22 23
 )
23 24
 
24 25
 func (srv *Server) DockerVersion() APIVersion {
... ...
@@ -32,8 +33,9 @@ func (srv *Server) DockerVersion() APIVersion {
32 32
 func (srv *Server) ContainerKill(name string) error {
33 33
 	if container := srv.runtime.Get(name); container != nil {
34 34
 		if err := container.Kill(); err != nil {
35
-			return fmt.Errorf("Error restarting container %s: %s", name, err)
35
+			return fmt.Errorf("Error killing container %s: %s", name, err)
36 36
 		}
37
+		srv.LogEvent("kill", name)
37 38
 	} else {
38 39
 		return fmt.Errorf("No such container: %s", name)
39 40
 	}
... ...
@@ -52,6 +54,7 @@ func (srv *Server) ContainerExport(name string, out io.Writer) error {
52 52
 		if _, err := io.Copy(out, data); err != nil {
53 53
 			return err
54 54
 		}
55
+		srv.LogEvent("export", name)
55 56
 		return nil
56 57
 	}
57 58
 	return fmt.Errorf("No such container: %s", name)
... ...
@@ -217,14 +220,15 @@ func (srv *Server) DockerInfo() *APIInfo {
217 217
 	}
218 218
 
219 219
 	return &APIInfo{
220
-		Containers:  len(srv.runtime.List()),
221
-		Images:      imgcount,
222
-		MemoryLimit: srv.runtime.capabilities.MemoryLimit,
223
-		SwapLimit:   srv.runtime.capabilities.SwapLimit,
224
-		Debug:       os.Getenv("DEBUG") != "",
225
-		NFd:         utils.GetTotalUsedFds(),
226
-		NGoroutines: runtime.NumGoroutine(),
227
-		LXCVersion:  lxcVersion,
220
+		Containers:      len(srv.runtime.List()),
221
+		Images:          imgcount,
222
+		MemoryLimit:     srv.runtime.capabilities.MemoryLimit,
223
+		SwapLimit:       srv.runtime.capabilities.SwapLimit,
224
+		Debug:           os.Getenv("DEBUG") != "",
225
+		NFd:             utils.GetTotalUsedFds(),
226
+		NGoroutines:     runtime.NumGoroutine(),
227
+		LXCVersion:      lxcVersion,
228
+		NEventsListener: len(srv.events),
228 229
 	}
229 230
 }
230 231
 
... ...
@@ -819,6 +823,7 @@ func (srv *Server) ContainerCreate(config *Config) (string, error) {
819 819
 		}
820 820
 		return "", err
821 821
 	}
822
+	srv.LogEvent("create", container.ShortID())
822 823
 	return container.ShortID(), nil
823 824
 }
824 825
 
... ...
@@ -827,6 +832,7 @@ func (srv *Server) ContainerRestart(name string, t int) error {
827 827
 		if err := container.Restart(t); err != nil {
828 828
 			return fmt.Errorf("Error restarting container %s: %s", name, err)
829 829
 		}
830
+		srv.LogEvent("restart", name)
830 831
 	} else {
831 832
 		return fmt.Errorf("No such container: %s", name)
832 833
 	}
... ...
@@ -846,6 +852,7 @@ func (srv *Server) ContainerDestroy(name string, removeVolume bool) error {
846 846
 		if err := srv.runtime.Destroy(container); err != nil {
847 847
 			return fmt.Errorf("Error destroying container %s: %s", name, err)
848 848
 		}
849
+		srv.LogEvent("destroy", name)
849 850
 
850 851
 		if removeVolume {
851 852
 			// Retrieve all volumes from all remaining containers
... ...
@@ -912,6 +919,7 @@ func (srv *Server) deleteImageAndChildren(id string, imgs *[]APIRmi) error {
912 912
 			return err
913 913
 		}
914 914
 		*imgs = append(*imgs, APIRmi{Deleted: utils.TruncateID(id)})
915
+		srv.LogEvent("delete", utils.TruncateID(id))
915 916
 		return nil
916 917
 	}
917 918
 	return nil
... ...
@@ -955,6 +963,7 @@ func (srv *Server) deleteImage(img *Image, repoName, tag string) ([]APIRmi, erro
955 955
 	}
956 956
 	if tagDeleted {
957 957
 		imgs = append(imgs, APIRmi{Untagged: img.ShortID()})
958
+		srv.LogEvent("untag", img.ShortID())
958 959
 	}
959 960
 	if len(srv.runtime.repositories.ByID()[img.ID]) == 0 {
960 961
 		if err := srv.deleteImageAndChildren(img.ID, &imgs); err != nil {
... ...
@@ -1027,6 +1036,7 @@ func (srv *Server) ContainerStart(name string, hostConfig *HostConfig) error {
1027 1027
 		if err := container.Start(hostConfig); err != nil {
1028 1028
 			return fmt.Errorf("Error starting container %s: %s", name, err)
1029 1029
 		}
1030
+		srv.LogEvent("start", name)
1030 1031
 	} else {
1031 1032
 		return fmt.Errorf("No such container: %s", name)
1032 1033
 	}
... ...
@@ -1038,6 +1048,7 @@ func (srv *Server) ContainerStop(name string, t int) error {
1038 1038
 		if err := container.Stop(t); err != nil {
1039 1039
 			return fmt.Errorf("Error stopping container %s: %s", name, err)
1040 1040
 		}
1041
+		srv.LogEvent("stop", name)
1041 1042
 	} else {
1042 1043
 		return fmt.Errorf("No such container: %s", name)
1043 1044
 	}
... ...
@@ -1171,15 +1182,31 @@ func NewServer(flGraphPath string, autoRestart, enableCors bool, dns ListOpts) (
1171 1171
 		enableCors:  enableCors,
1172 1172
 		pullingPool: make(map[string]struct{}),
1173 1173
 		pushingPool: make(map[string]struct{}),
1174
+		events:      make([]utils.JSONMessage, 0, 64), //only keeps the 64 last events
1175
+		listeners:   make(map[string]chan utils.JSONMessage),
1174 1176
 	}
1175 1177
 	runtime.srv = srv
1176 1178
 	return srv, nil
1177 1179
 }
1178 1180
 
1181
+func (srv *Server) LogEvent(action, id string) {
1182
+	now := time.Now().Unix()
1183
+	jm := utils.JSONMessage{Status: action, ID: id, Time: now}
1184
+	srv.events = append(srv.events, jm)
1185
+	for _, c := range srv.listeners {
1186
+		select { // non blocking channel
1187
+		case c <- jm:
1188
+		default:
1189
+		}
1190
+	}
1191
+}
1192
+
1179 1193
 type Server struct {
1180 1194
 	sync.Mutex
1181 1195
 	runtime     *Runtime
1182 1196
 	enableCors  bool
1183 1197
 	pullingPool map[string]struct{}
1184 1198
 	pushingPool map[string]struct{}
1199
+	events      []utils.JSONMessage
1200
+	listeners   map[string]chan utils.JSONMessage
1185 1201
 }
... ...
@@ -1,7 +1,9 @@
1 1
 package docker
2 2
 
3 3
 import (
4
+	"github.com/dotcloud/docker/utils"
4 5
 	"testing"
6
+	"time"
5 7
 )
6 8
 
7 9
 func TestContainerTagImageDelete(t *testing.T) {
... ...
@@ -163,3 +165,41 @@ func TestRunWithTooLowMemoryLimit(t *testing.T) {
163 163
 	}
164 164
 
165 165
 }
166
+
167
+func TestLogEvent(t *testing.T) {
168
+	runtime := mkRuntime(t)
169
+	srv := &Server{
170
+		runtime:   runtime,
171
+		events:    make([]utils.JSONMessage, 0, 64),
172
+		listeners: make(map[string]chan utils.JSONMessage),
173
+	}
174
+
175
+	srv.LogEvent("fakeaction", "fakeid")
176
+
177
+	listener := make(chan utils.JSONMessage)
178
+	srv.Lock()
179
+	srv.listeners["test"] = listener
180
+	srv.Unlock()
181
+
182
+	srv.LogEvent("fakeaction2", "fakeid")
183
+
184
+	if len(srv.events) != 2 {
185
+		t.Fatalf("Expected 2 events, found %d", len(srv.events))
186
+	}
187
+	go func() {
188
+		time.Sleep(200 * time.Millisecond)
189
+		srv.LogEvent("fakeaction3", "fakeid")
190
+		time.Sleep(200 * time.Millisecond)
191
+		srv.LogEvent("fakeaction4", "fakeid")
192
+	}()
193
+
194
+	setTimeout(t, "Listening for events timed out", 2*time.Second, func() {
195
+		for i := 2; i < 4; i++ {
196
+			event := <-listener
197
+			if event != srv.events[i] {
198
+				t.Fatalf("Event received it different than expected")
199
+			}
200
+		}
201
+	})
202
+
203
+}
... ...
@@ -78,7 +78,7 @@ func MergeConfig(userConf, imageConf *Config) {
78 78
 			imageNat, _ := parseNat(imagePortSpec)
79 79
 			for _, userPortSpec := range userConf.PortSpecs {
80 80
 				userNat, _ := parseNat(userPortSpec)
81
-				if imageNat.Proto == userNat.Proto && imageNat.Frontend == userNat.Frontend {
81
+				if imageNat.Proto == userNat.Proto && imageNat.Backend == userNat.Backend {
82 82
 					found = true
83 83
 				}
84 84
 			}
... ...
@@ -611,8 +611,27 @@ type JSONMessage struct {
611 611
 	Status   string `json:"status,omitempty"`
612 612
 	Progress string `json:"progress,omitempty"`
613 613
 	Error    string `json:"error,omitempty"`
614
+	ID	 string `json:"id,omitempty"`
615
+	Time	 int64 `json:"time,omitempty"`
614 616
 }
615 617
 
618
+func (jm *JSONMessage) Display(out io.Writer) (error) {
619
+	if jm.Time != 0 {
620
+		fmt.Fprintf(out, "[%s] ", time.Unix(jm.Time, 0))
621
+	}
622
+	if jm.Progress != "" {
623
+		fmt.Fprintf(out, "%s %s\r", jm.Status, jm.Progress)
624
+	} else if jm.Error != "" {
625
+		return fmt.Errorf(jm.Error)
626
+	} else if jm.ID != "" {
627
+		fmt.Fprintf(out, "%s: %s\n", jm.ID, jm.Status)
628
+	} else {
629
+		fmt.Fprintf(out, "%s\n", jm.Status)
630
+	}
631
+	return nil
632
+}
633
+
634
+
616 635
 type StreamFormatter struct {
617 636
 	json bool
618 637
 	used bool
... ...
@@ -155,7 +155,7 @@ func TestMergeConfig(t *testing.T) {
155 155
 	volumesUser["/test3"] = struct{}{}
156 156
 	configUser := &Config{
157 157
 		Dns:       []string{"3.3.3.3"},
158
-		PortSpecs: []string{"2222:3333", "3333:3333"},
158
+		PortSpecs: []string{"3333:2222", "3333:3333"},
159 159
 		Env:       []string{"VAR2=3", "VAR3=3"},
160 160
 		Volumes:   volumesUser,
161 161
 	}
... ...
@@ -172,11 +172,11 @@ func TestMergeConfig(t *testing.T) {
172 172
 	}
173 173
 
174 174
 	if len(configUser.PortSpecs) != 3 {
175
-		t.Fatalf("Expected 3 portSpecs, 1111:1111, 2222:3333 and 3333:3333, found %d", len(configUser.PortSpecs))
175
+		t.Fatalf("Expected 3 portSpecs, 1111:1111, 3333:2222 and 3333:3333, found %d", len(configUser.PortSpecs))
176 176
 	}
177 177
 	for _, portSpecs := range configUser.PortSpecs {
178
-		if portSpecs != "1111:1111" && portSpecs != "2222:3333" && portSpecs != "3333:3333" {
179
-			t.Fatalf("Expected 1111:1111 or 2222:3333 or 3333:3333, found %s", portSpecs)
178
+		if portSpecs != "1111:1111" && portSpecs != "3333:2222" && portSpecs != "3333:3333" {
179
+			t.Fatalf("Expected 1111:1111 or 3333:2222 or 3333:3333, found %s", portSpecs)
180 180
 		}
181 181
 	}
182 182
 	if len(configUser.Env) != 3 {
... ...
@@ -197,3 +197,45 @@ func TestMergeConfig(t *testing.T) {
197 197
 		}
198 198
 	}
199 199
 }
200
+
201
+func TestMergeConfigPublicPortNotHonored(t *testing.T) {
202
+	volumesImage := make(map[string]struct{})
203
+	volumesImage["/test1"] = struct{}{}
204
+	volumesImage["/test2"] = struct{}{}
205
+	configImage := &Config{
206
+		Dns:       []string{"1.1.1.1", "2.2.2.2"},
207
+		PortSpecs: []string{"1111", "2222"},
208
+		Env:       []string{"VAR1=1", "VAR2=2"},
209
+		Volumes:   volumesImage,
210
+	}
211
+
212
+	volumesUser := make(map[string]struct{})
213
+	volumesUser["/test3"] = struct{}{}
214
+	configUser := &Config{
215
+		Dns:       []string{"3.3.3.3"},
216
+		PortSpecs: []string{"1111:3333"},
217
+		Env:       []string{"VAR2=3", "VAR3=3"},
218
+		Volumes:   volumesUser,
219
+	}
220
+
221
+	MergeConfig(configUser, configImage)
222
+
223
+	contains := func(a []string, expect string) bool {
224
+		for _, p := range a {
225
+			if p == expect {
226
+				return true
227
+			}
228
+		}
229
+		return false
230
+	}
231
+
232
+	if !contains(configUser.PortSpecs, "2222") {
233
+		t.Logf("Expected '2222' Ports: %v", configUser.PortSpecs)
234
+		t.Fail()
235
+	}
236
+
237
+	if !contains(configUser.PortSpecs, "1111:3333") {
238
+		t.Logf("Expected '1111:3333' Ports: %v", configUser.PortSpecs)
239
+		t.Fail()
240
+	}
241
+}