... | ... |
@@ -15,6 +15,7 @@ Brian McCallister <brianm@skife.org> |
15 | 15 |
Bruno Bigras <bigras.bruno@gmail.com> |
16 | 16 |
Caleb Spare <cespare@gmail.com> |
17 | 17 |
Charles Hooper <charles.hooper@dotcloud.com> |
18 |
+Daniel Gasienica <daniel@gasienica.ch> |
|
18 | 19 |
Daniel Mizyrycki <daniel.mizyrycki@dotcloud.com> |
19 | 20 |
Daniel Robinson <gottagetmac@gmail.com> |
20 | 21 |
Daniel Von Fange <daniel@leancoder.com> |
... | ... |
@@ -1,5 +1,14 @@ |
1 | 1 |
# Changelog |
2 | 2 |
|
3 |
+## 0.3.4 (2013-05-30) |
|
4 |
+ + Builder: 'docker build' builds a container, layer by layer, from a source repository containing a Dockerfile |
|
5 |
+ + Builder: 'docker build -t FOO' applies the tag FOO to the newly built container. |
|
6 |
+ + Runtime: interactive TTYs correctly handle window resize |
|
7 |
+ * Runtime: fix how configuration is merged between layers |
|
8 |
+ + Remote API: split stdout and stderr on 'docker run' |
|
9 |
+ + Remote API: optionally listen on a different IP and port (use at your own risk) |
|
10 |
+ * Documentation: improved install instructions. |
|
11 |
+ |
|
3 | 12 |
## 0.3.3 (2013-05-23) |
4 | 13 |
- Registry: Fix push regression |
5 | 14 |
- Various bugfixes |
... | ... |
@@ -47,6 +47,8 @@ func httpError(w http.ResponseWriter, err error) { |
47 | 47 |
http.Error(w, err.Error(), http.StatusBadRequest) |
48 | 48 |
} else if strings.HasPrefix(err.Error(), "Conflict") { |
49 | 49 |
http.Error(w, err.Error(), http.StatusConflict) |
50 |
+ } else if strings.HasPrefix(err.Error(), "Impossible") { |
|
51 |
+ http.Error(w, err.Error(), http.StatusNotAcceptable) |
|
50 | 52 |
} else { |
51 | 53 |
http.Error(w, err.Error(), http.StatusInternalServerError) |
52 | 54 |
} |
... | ... |
@@ -69,7 +71,16 @@ func getBoolParam(value string) (bool, error) { |
69 | 69 |
} |
70 | 70 |
|
71 | 71 |
func getAuth(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { |
72 |
- b, err := json.Marshal(srv.registry.GetAuthConfig(false)) |
|
72 |
+ // FIXME: Handle multiple login at once |
|
73 |
+ // FIXME: return specific error code if config file missing? |
|
74 |
+ authConfig, err := auth.LoadConfig(srv.runtime.root) |
|
75 |
+ if err != nil { |
|
76 |
+ if err != auth.ErrConfigFileMissing { |
|
77 |
+ return err |
|
78 |
+ } |
|
79 |
+ authConfig = &auth.AuthConfig{} |
|
80 |
+ } |
|
81 |
+ b, err := json.Marshal(&auth.AuthConfig{Username: authConfig.Username, Email: authConfig.Email}) |
|
73 | 82 |
if err != nil { |
74 | 83 |
return err |
75 | 84 |
} |
... | ... |
@@ -78,11 +89,19 @@ func getAuth(srv *Server, version float64, w http.ResponseWriter, r *http.Reques |
78 | 78 |
} |
79 | 79 |
|
80 | 80 |
func postAuth(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { |
81 |
+ // FIXME: Handle multiple login at once |
|
81 | 82 |
config := &auth.AuthConfig{} |
82 | 83 |
if err := json.NewDecoder(r.Body).Decode(config); err != nil { |
83 | 84 |
return err |
84 | 85 |
} |
85 |
- authConfig := srv.registry.GetAuthConfig(true) |
|
86 |
+ |
|
87 |
+ authConfig, err := auth.LoadConfig(srv.runtime.root) |
|
88 |
+ if err != nil { |
|
89 |
+ if err != auth.ErrConfigFileMissing { |
|
90 |
+ return err |
|
91 |
+ } |
|
92 |
+ authConfig = &auth.AuthConfig{} |
|
93 |
+ } |
|
86 | 94 |
if config.Username == authConfig.Username { |
87 | 95 |
config.Password = authConfig.Password |
88 | 96 |
} |
... | ... |
@@ -92,7 +111,6 @@ func postAuth(srv *Server, version float64, w http.ResponseWriter, r *http.Reque |
92 | 92 |
if err != nil { |
93 | 93 |
return err |
94 | 94 |
} |
95 |
- srv.registry.ResetClient(newAuthConfig) |
|
96 | 95 |
|
97 | 96 |
if status != "" { |
98 | 97 |
b, err := json.Marshal(&ApiAuth{Status: status}) |
... | ... |
@@ -298,16 +316,25 @@ func postImagesCreate(srv *Server, version float64, w http.ResponseWriter, r *ht |
298 | 298 |
tag := r.Form.Get("tag") |
299 | 299 |
repo := r.Form.Get("repo") |
300 | 300 |
|
301 |
+ if version > 1.0 { |
|
302 |
+ w.Header().Set("Content-Type", "application/json") |
|
303 |
+ } |
|
304 |
+ sf := utils.NewStreamFormatter(version > 1.0) |
|
301 | 305 |
if image != "" { //pull |
302 | 306 |
registry := r.Form.Get("registry") |
303 |
- if version > 1.0 { |
|
304 |
- w.Header().Set("Content-Type", "application/json") |
|
305 |
- } |
|
306 |
- if err := srv.ImagePull(image, tag, registry, w, version > 1.0); err != nil { |
|
307 |
+ if err := srv.ImagePull(image, tag, registry, w, sf); err != nil { |
|
308 |
+ if sf.Used() { |
|
309 |
+ w.Write(sf.FormatError(err)) |
|
310 |
+ return nil |
|
311 |
+ } |
|
307 | 312 |
return err |
308 | 313 |
} |
309 | 314 |
} else { //import |
310 |
- if err := srv.ImageImport(src, repo, tag, r.Body, w); err != nil { |
|
315 |
+ if err := srv.ImageImport(src, repo, tag, r.Body, w, sf); err != nil { |
|
316 |
+ if sf.Used() { |
|
317 |
+ w.Write(sf.FormatError(err)) |
|
318 |
+ return nil |
|
319 |
+ } |
|
311 | 320 |
return err |
312 | 321 |
} |
313 | 322 |
} |
... | ... |
@@ -343,10 +370,16 @@ func postImagesInsert(srv *Server, version float64, w http.ResponseWriter, r *ht |
343 | 343 |
return fmt.Errorf("Missing parameter") |
344 | 344 |
} |
345 | 345 |
name := vars["name"] |
346 |
- |
|
347 |
- imgId, err := srv.ImageInsert(name, url, path, w) |
|
346 |
+ if version > 1.0 { |
|
347 |
+ w.Header().Set("Content-Type", "application/json") |
|
348 |
+ } |
|
349 |
+ sf := utils.NewStreamFormatter(version > 1.0) |
|
350 |
+ imgId, err := srv.ImageInsert(name, url, path, w, sf) |
|
348 | 351 |
if err != nil { |
349 |
- return err |
|
352 |
+ if sf.Used() { |
|
353 |
+ w.Write(sf.FormatError(err)) |
|
354 |
+ return nil |
|
355 |
+ } |
|
350 | 356 |
} |
351 | 357 |
b, err := json.Marshal(&ApiId{Id: imgId}) |
352 | 358 |
if err != nil { |
... | ... |
@@ -366,8 +399,15 @@ func postImagesPush(srv *Server, version float64, w http.ResponseWriter, r *http |
366 | 366 |
return fmt.Errorf("Missing parameter") |
367 | 367 |
} |
368 | 368 |
name := vars["name"] |
369 |
- |
|
370 |
- if err := srv.ImagePush(name, registry, w); err != nil { |
|
369 |
+ if version > 1.0 { |
|
370 |
+ w.Header().Set("Content-Type", "application/json") |
|
371 |
+ } |
|
372 |
+ sf := utils.NewStreamFormatter(version > 1.0) |
|
373 |
+ if err := srv.ImagePush(name, registry, w, sf); err != nil { |
|
374 |
+ if sf.Used() { |
|
375 |
+ w.Write(sf.FormatError(err)) |
|
376 |
+ return nil |
|
377 |
+ } |
|
371 | 378 |
return err |
372 | 379 |
} |
373 | 380 |
return nil |
... | ... |
@@ -652,6 +692,13 @@ func postBuild(srv *Server, version float64, w http.ResponseWriter, r *http.Requ |
652 | 652 |
if err := r.ParseMultipartForm(4096); err != nil { |
653 | 653 |
return err |
654 | 654 |
} |
655 |
+ remote := r.FormValue("t") |
|
656 |
+ tag := "" |
|
657 |
+ if strings.Contains(remote, ":") { |
|
658 |
+ remoteParts := strings.Split(remote, ":") |
|
659 |
+ tag = remoteParts[1] |
|
660 |
+ remote = remoteParts[0] |
|
661 |
+ } |
|
655 | 662 |
|
656 | 663 |
dockerfile, _, err := r.FormFile("Dockerfile") |
657 | 664 |
if err != nil { |
... | ... |
@@ -666,8 +713,10 @@ func postBuild(srv *Server, version float64, w http.ResponseWriter, r *http.Requ |
666 | 666 |
} |
667 | 667 |
|
668 | 668 |
b := NewBuildFile(srv, utils.NewWriteFlusher(w)) |
669 |
- if _, err := b.Build(dockerfile, context); err != nil { |
|
669 |
+ if id, err := b.Build(dockerfile, context); err != nil { |
|
670 | 670 |
fmt.Fprintf(w, "Error build: %s\n", err) |
671 |
+ } else if remote != "" { |
|
672 |
+ srv.runtime.repositories.Set(remote, tag, id, false) |
|
671 | 673 |
} |
672 | 674 |
return nil |
673 | 675 |
} |
... | ... |
@@ -26,8 +26,7 @@ func TestGetAuth(t *testing.T) { |
26 | 26 |
defer nuke(runtime) |
27 | 27 |
|
28 | 28 |
srv := &Server{ |
29 |
- runtime: runtime, |
|
30 |
- registry: registry.NewRegistry(runtime.root), |
|
29 |
+ runtime: runtime, |
|
31 | 30 |
} |
32 | 31 |
|
33 | 32 |
r := httptest.NewRecorder() |
... | ... |
@@ -56,7 +55,7 @@ func TestGetAuth(t *testing.T) { |
56 | 56 |
t.Fatalf("%d OK or 0 expected, received %d\n", http.StatusOK, r.Code) |
57 | 57 |
} |
58 | 58 |
|
59 |
- newAuthConfig := srv.registry.GetAuthConfig(false) |
|
59 |
+ newAuthConfig := registry.NewRegistry(runtime.root).GetAuthConfig(false) |
|
60 | 60 |
if newAuthConfig.Username != authConfig.Username || |
61 | 61 |
newAuthConfig.Email != authConfig.Email { |
62 | 62 |
t.Fatalf("The auth configuration hasn't been set correctly") |
... | ... |
@@ -247,8 +246,7 @@ func TestGetImagesSearch(t *testing.T) { |
247 | 247 |
defer nuke(runtime) |
248 | 248 |
|
249 | 249 |
srv := &Server{ |
250 |
- runtime: runtime, |
|
251 |
- registry: registry.NewRegistry(runtime.root), |
|
250 |
+ runtime: runtime, |
|
252 | 251 |
} |
253 | 252 |
|
254 | 253 |
r := httptest.NewRecorder() |
... | ... |
@@ -504,15 +502,16 @@ func TestPostAuth(t *testing.T) { |
504 | 504 |
defer nuke(runtime) |
505 | 505 |
|
506 | 506 |
srv := &Server{ |
507 |
- runtime: runtime, |
|
508 |
- registry: registry.NewRegistry(runtime.root), |
|
507 |
+ runtime: runtime, |
|
509 | 508 |
} |
510 | 509 |
|
511 |
- authConfigOrig := &auth.AuthConfig{ |
|
510 |
+ config := &auth.AuthConfig{ |
|
512 | 511 |
Username: "utest", |
513 | 512 |
Email: "utest@yopmail.com", |
514 | 513 |
} |
515 |
- srv.registry.ResetClient(authConfigOrig) |
|
514 |
+ |
|
515 |
+ authStr := auth.EncodeAuth(config) |
|
516 |
+ auth.SaveConfig(runtime.root, authStr, config.Email) |
|
516 | 517 |
|
517 | 518 |
r := httptest.NewRecorder() |
518 | 519 |
if err := getAuth(srv, API_VERSION, r, nil, nil); err != nil { |
... | ... |
@@ -524,7 +523,7 @@ func TestPostAuth(t *testing.T) { |
524 | 524 |
t.Fatal(err) |
525 | 525 |
} |
526 | 526 |
|
527 |
- if authConfig.Username != authConfigOrig.Username || authConfig.Email != authConfigOrig.Email { |
|
527 |
+ if authConfig.Username != config.Username || authConfig.Email != config.Email { |
|
528 | 528 |
t.Errorf("The retrieve auth mismatch with the one set.") |
529 | 529 |
} |
530 | 530 |
} |
... | ... |
@@ -54,6 +54,9 @@ func Tar(path string, compression Compression) (io.Reader, error) { |
54 | 54 |
func Untar(archive io.Reader, path string) error { |
55 | 55 |
cmd := exec.Command("bsdtar", "-f", "-", "-C", path, "-x") |
56 | 56 |
cmd.Stdin = archive |
57 |
+ // Hardcode locale environment for predictable outcome regardless of host configuration. |
|
58 |
+ // (see https://github.com/dotcloud/docker/issues/355) |
|
59 |
+ cmd.Env = []string{"LANG=en_US.utf-8", "LC_ALL=en_US.utf-8"} |
|
57 | 60 |
output, err := cmd.CombinedOutput() |
58 | 61 |
if err != nil { |
59 | 62 |
return fmt.Errorf("%s: %s", err, output) |
... | ... |
@@ -3,6 +3,7 @@ package auth |
3 | 3 |
import ( |
4 | 4 |
"encoding/base64" |
5 | 5 |
"encoding/json" |
6 |
+ "errors" |
|
6 | 7 |
"fmt" |
7 | 8 |
"io/ioutil" |
8 | 9 |
"net/http" |
... | ... |
@@ -17,6 +18,12 @@ const CONFIGFILE = ".dockercfg" |
17 | 17 |
// the registry server we want to login against |
18 | 18 |
const INDEX_SERVER = "https://index.docker.io/v1" |
19 | 19 |
|
20 |
+//const INDEX_SERVER = "http://indexstaging-docker.dotcloud.com/" |
|
21 |
+ |
|
22 |
+var ( |
|
23 |
+ ErrConfigFileMissing error = errors.New("The Auth config file is missing") |
|
24 |
+) |
|
25 |
+ |
|
20 | 26 |
type AuthConfig struct { |
21 | 27 |
Username string `json:"username"` |
22 | 28 |
Password string `json:"password"` |
... | ... |
@@ -75,7 +82,7 @@ func DecodeAuth(authStr string) (*AuthConfig, error) { |
75 | 75 |
func LoadConfig(rootPath string) (*AuthConfig, error) { |
76 | 76 |
confFile := path.Join(rootPath, CONFIGFILE) |
77 | 77 |
if _, err := os.Stat(confFile); err != nil { |
78 |
- return &AuthConfig{}, fmt.Errorf("The Auth config file is missing") |
|
78 |
+ return nil, ErrConfigFileMissing |
|
79 | 79 |
} |
80 | 80 |
b, err := ioutil.ReadFile(confFile) |
81 | 81 |
if err != nil { |
... | ... |
@@ -97,7 +104,7 @@ func LoadConfig(rootPath string) (*AuthConfig, error) { |
97 | 97 |
} |
98 | 98 |
|
99 | 99 |
// save the auth config |
100 |
-func saveConfig(rootPath, authStr string, email string) error { |
|
100 |
+func SaveConfig(rootPath, authStr string, email string) error { |
|
101 | 101 |
confFile := path.Join(rootPath, CONFIGFILE) |
102 | 102 |
if len(email) == 0 { |
103 | 103 |
os.Remove(confFile) |
... | ... |
@@ -161,7 +168,9 @@ func Login(authConfig *AuthConfig) (string, error) { |
161 | 161 |
status = "Login Succeeded\n" |
162 | 162 |
storeConfig = true |
163 | 163 |
} else if resp.StatusCode == 401 { |
164 |
- saveConfig(authConfig.rootPath, "", "") |
|
164 |
+ if err := SaveConfig(authConfig.rootPath, "", ""); err != nil { |
|
165 |
+ return "", err |
|
166 |
+ } |
|
165 | 167 |
return "", fmt.Errorf("Wrong login/password, please try again") |
166 | 168 |
} else { |
167 | 169 |
return "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body, |
... | ... |
@@ -175,7 +184,9 @@ func Login(authConfig *AuthConfig) (string, error) { |
175 | 175 |
} |
176 | 176 |
if storeConfig { |
177 | 177 |
authStr := EncodeAuth(authConfig) |
178 |
- saveConfig(authConfig.rootPath, authStr, authConfig.Email) |
|
178 |
+ if err := SaveConfig(authConfig.rootPath, authStr, authConfig.Email); err != nil { |
|
179 |
+ return "", err |
|
180 |
+ } |
|
179 | 181 |
} |
180 | 182 |
return status, nil |
181 | 183 |
} |
... | ... |
@@ -32,8 +32,6 @@ type buildFile struct { |
32 | 32 |
tmpContainers map[string]struct{} |
33 | 33 |
tmpImages map[string]struct{} |
34 | 34 |
|
35 |
- needCommit bool |
|
36 |
- |
|
37 | 35 |
out io.Writer |
38 | 36 |
} |
39 | 37 |
|
... | ... |
@@ -63,7 +61,7 @@ func (b *buildFile) CmdFrom(name string) error { |
63 | 63 |
remote = name |
64 | 64 |
} |
65 | 65 |
|
66 |
- if err := b.srv.ImagePull(remote, tag, "", b.out, false); err != nil { |
|
66 |
+ if err := b.srv.ImagePull(remote, tag, "", b.out, utils.NewStreamFormatter(false)); err != nil { |
|
67 | 67 |
return err |
68 | 68 |
} |
69 | 69 |
|
... | ... |
@@ -81,9 +79,8 @@ func (b *buildFile) CmdFrom(name string) error { |
81 | 81 |
} |
82 | 82 |
|
83 | 83 |
func (b *buildFile) CmdMaintainer(name string) error { |
84 |
- b.needCommit = true |
|
85 | 84 |
b.maintainer = name |
86 |
- return nil |
|
85 |
+ return b.commit("", b.config.Cmd, fmt.Sprintf("MAINTAINER %s", name)) |
|
87 | 86 |
} |
88 | 87 |
|
89 | 88 |
func (b *buildFile) CmdRun(args string) error { |
... | ... |
@@ -95,28 +92,34 @@ func (b *buildFile) CmdRun(args string) error { |
95 | 95 |
return err |
96 | 96 |
} |
97 | 97 |
|
98 |
- cmd, env := b.config.Cmd, b.config.Env |
|
98 |
+ cmd := b.config.Cmd |
|
99 | 99 |
b.config.Cmd = nil |
100 | 100 |
MergeConfig(b.config, config) |
101 | 101 |
|
102 |
- if cache, err := b.srv.ImageGetCached(b.image, config); err != nil { |
|
102 |
+ utils.Debugf("Command to be executed: %v", b.config.Cmd) |
|
103 |
+ |
|
104 |
+ if cache, err := b.srv.ImageGetCached(b.image, b.config); err != nil { |
|
103 | 105 |
return err |
104 | 106 |
} else if cache != nil { |
105 |
- utils.Debugf("Use cached version") |
|
107 |
+ utils.Debugf("[BUILDER] Use cached version") |
|
106 | 108 |
b.image = cache.Id |
107 | 109 |
return nil |
110 |
+ } else { |
|
111 |
+ utils.Debugf("[BUILDER] Cache miss") |
|
108 | 112 |
} |
109 | 113 |
|
110 | 114 |
cid, err := b.run() |
111 | 115 |
if err != nil { |
112 | 116 |
return err |
113 | 117 |
} |
114 |
- b.config.Cmd, b.config.Env = cmd, env |
|
115 |
- return b.commit(cid) |
|
118 |
+ if err := b.commit(cid, cmd, "run"); err != nil { |
|
119 |
+ return err |
|
120 |
+ } |
|
121 |
+ b.config.Cmd = cmd |
|
122 |
+ return nil |
|
116 | 123 |
} |
117 | 124 |
|
118 | 125 |
func (b *buildFile) CmdEnv(args string) error { |
119 |
- b.needCommit = true |
|
120 | 126 |
tmp := strings.SplitN(args, " ", 2) |
121 | 127 |
if len(tmp) != 2 { |
122 | 128 |
return fmt.Errorf("Invalid ENV format") |
... | ... |
@@ -131,60 +134,34 @@ func (b *buildFile) CmdEnv(args string) error { |
131 | 131 |
} |
132 | 132 |
} |
133 | 133 |
b.config.Env = append(b.config.Env, key+"="+value) |
134 |
- return nil |
|
134 |
+ return b.commit("", b.config.Cmd, fmt.Sprintf("ENV %s=%s", key, value)) |
|
135 | 135 |
} |
136 | 136 |
|
137 | 137 |
func (b *buildFile) CmdCmd(args string) error { |
138 |
- b.needCommit = true |
|
139 | 138 |
var cmd []string |
140 | 139 |
if err := json.Unmarshal([]byte(args), &cmd); err != nil { |
141 | 140 |
utils.Debugf("Error unmarshalling: %s, using /bin/sh -c", err) |
142 |
- b.config.Cmd = []string{"/bin/sh", "-c", args} |
|
143 |
- } else { |
|
144 |
- b.config.Cmd = cmd |
|
141 |
+ cmd = []string{"/bin/sh", "-c", args} |
|
142 |
+ } |
|
143 |
+ if err := b.commit("", cmd, fmt.Sprintf("CMD %v", cmd)); err != nil { |
|
144 |
+ return err |
|
145 | 145 |
} |
146 |
+ b.config.Cmd = cmd |
|
146 | 147 |
return nil |
147 | 148 |
} |
148 | 149 |
|
149 | 150 |
func (b *buildFile) CmdExpose(args string) error { |
150 | 151 |
ports := strings.Split(args, " ") |
151 | 152 |
b.config.PortSpecs = append(ports, b.config.PortSpecs...) |
152 |
- return nil |
|
153 |
+ return b.commit("", b.config.Cmd, fmt.Sprintf("EXPOSE %v", ports)) |
|
153 | 154 |
} |
154 | 155 |
|
155 | 156 |
func (b *buildFile) CmdInsert(args string) error { |
156 |
- if b.image == "" { |
|
157 |
- return fmt.Errorf("Please provide a source image with `from` prior to insert") |
|
158 |
- } |
|
159 |
- tmp := strings.SplitN(args, " ", 2) |
|
160 |
- if len(tmp) != 2 { |
|
161 |
- return fmt.Errorf("Invalid INSERT format") |
|
162 |
- } |
|
163 |
- sourceUrl := strings.Trim(tmp[0], " ") |
|
164 |
- destPath := strings.Trim(tmp[1], " ") |
|
165 |
- |
|
166 |
- file, err := utils.Download(sourceUrl, b.out) |
|
167 |
- if err != nil { |
|
168 |
- return err |
|
169 |
- } |
|
170 |
- defer file.Body.Close() |
|
171 |
- |
|
172 |
- b.config.Cmd = []string{"echo", "INSERT", sourceUrl, "in", destPath} |
|
173 |
- cid, err := b.run() |
|
174 |
- if err != nil { |
|
175 |
- return err |
|
176 |
- } |
|
177 |
- |
|
178 |
- container := b.runtime.Get(cid) |
|
179 |
- if container == nil { |
|
180 |
- return fmt.Errorf("An error occured while creating the container") |
|
181 |
- } |
|
182 |
- |
|
183 |
- if err := container.Inject(file.Body, destPath); err != nil { |
|
184 |
- return err |
|
185 |
- } |
|
157 |
+ return fmt.Errorf("INSERT has been deprecated. Please use ADD instead") |
|
158 |
+} |
|
186 | 159 |
|
187 |
- return b.commit(cid) |
|
160 |
+func (b *buildFile) CmdCopy(args string) error { |
|
161 |
+ return fmt.Errorf("COPY has been deprecated. Please use ADD instead") |
|
188 | 162 |
} |
189 | 163 |
|
190 | 164 |
func (b *buildFile) CmdAdd(args string) error { |
... | ... |
@@ -193,12 +170,13 @@ func (b *buildFile) CmdAdd(args string) error { |
193 | 193 |
} |
194 | 194 |
tmp := strings.SplitN(args, " ", 2) |
195 | 195 |
if len(tmp) != 2 { |
196 |
- return fmt.Errorf("Invalid INSERT format") |
|
196 |
+ return fmt.Errorf("Invalid ADD format") |
|
197 | 197 |
} |
198 | 198 |
orig := strings.Trim(tmp[0], " ") |
199 | 199 |
dest := strings.Trim(tmp[1], " ") |
200 | 200 |
|
201 |
- b.config.Cmd = []string{"echo", "PUSH", orig, "in", dest} |
|
201 |
+ cmd := b.config.Cmd |
|
202 |
+ b.config.Cmd = []string{"/bin/sh", "-c", fmt.Sprintf("#(nop) ADD %s in %s", orig, dest)} |
|
202 | 203 |
cid, err := b.run() |
203 | 204 |
if err != nil { |
204 | 205 |
return err |
... | ... |
@@ -208,19 +186,23 @@ func (b *buildFile) CmdAdd(args string) error { |
208 | 208 |
if container == nil { |
209 | 209 |
return fmt.Errorf("Error while creating the container (CmdAdd)") |
210 | 210 |
} |
211 |
- |
|
212 |
- if err := os.MkdirAll(path.Join(container.rwPath(), dest), 0700); err != nil { |
|
211 |
+ if err := container.EnsureMounted(); err != nil { |
|
213 | 212 |
return err |
214 | 213 |
} |
214 |
+ defer container.Unmount() |
|
215 | 215 |
|
216 | 216 |
origPath := path.Join(b.context, orig) |
217 |
- destPath := path.Join(container.rwPath(), dest) |
|
217 |
+ destPath := path.Join(container.RootfsPath(), dest) |
|
218 | 218 |
|
219 | 219 |
fi, err := os.Stat(origPath) |
220 | 220 |
if err != nil { |
221 | 221 |
return err |
222 | 222 |
} |
223 | 223 |
if fi.IsDir() { |
224 |
+ if err := os.MkdirAll(destPath, 0700); err != nil { |
|
225 |
+ return err |
|
226 |
+ } |
|
227 |
+ |
|
224 | 228 |
files, err := ioutil.ReadDir(path.Join(b.context, orig)) |
225 | 229 |
if err != nil { |
226 | 230 |
return err |
... | ... |
@@ -231,12 +213,18 @@ func (b *buildFile) CmdAdd(args string) error { |
231 | 231 |
} |
232 | 232 |
} |
233 | 233 |
} else { |
234 |
+ if err := os.MkdirAll(path.Dir(destPath), 0700); err != nil { |
|
235 |
+ return err |
|
236 |
+ } |
|
234 | 237 |
if err := utils.CopyDirectory(origPath, destPath); err != nil { |
235 | 238 |
return err |
236 | 239 |
} |
237 | 240 |
} |
238 |
- |
|
239 |
- return b.commit(cid) |
|
241 |
+ if err := b.commit(cid, cmd, fmt.Sprintf("ADD %s in %s", orig, dest)); err != nil { |
|
242 |
+ return err |
|
243 |
+ } |
|
244 |
+ b.config.Cmd = cmd |
|
245 |
+ return nil |
|
240 | 246 |
} |
241 | 247 |
|
242 | 248 |
func (b *buildFile) run() (string, error) { |
... | ... |
@@ -265,20 +253,30 @@ func (b *buildFile) run() (string, error) { |
265 | 265 |
return c.Id, nil |
266 | 266 |
} |
267 | 267 |
|
268 |
-func (b *buildFile) commit(id string) error { |
|
268 |
+// Commit the container <id> with the autorun command <autoCmd> |
|
269 |
+func (b *buildFile) commit(id string, autoCmd []string, comment string) error { |
|
269 | 270 |
if b.image == "" { |
270 | 271 |
return fmt.Errorf("Please provide a source image with `from` prior to commit") |
271 | 272 |
} |
272 | 273 |
b.config.Image = b.image |
273 | 274 |
if id == "" { |
274 |
- cmd := b.config.Cmd |
|
275 |
- b.config.Cmd = []string{"true"} |
|
275 |
+ b.config.Cmd = []string{"/bin/sh", "-c", "#(nop) " + comment} |
|
276 |
+ |
|
277 |
+ if cache, err := b.srv.ImageGetCached(b.image, b.config); err != nil { |
|
278 |
+ return err |
|
279 |
+ } else if cache != nil { |
|
280 |
+ utils.Debugf("[BUILDER] Use cached version") |
|
281 |
+ b.image = cache.Id |
|
282 |
+ return nil |
|
283 |
+ } else { |
|
284 |
+ utils.Debugf("[BUILDER] Cache miss") |
|
285 |
+ } |
|
286 |
+ |
|
276 | 287 |
if cid, err := b.run(); err != nil { |
277 | 288 |
return err |
278 | 289 |
} else { |
279 | 290 |
id = cid |
280 | 291 |
} |
281 |
- b.config.Cmd = cmd |
|
282 | 292 |
} |
283 | 293 |
|
284 | 294 |
container := b.runtime.Get(id) |
... | ... |
@@ -286,20 +284,20 @@ func (b *buildFile) commit(id string) error { |
286 | 286 |
return fmt.Errorf("An error occured while creating the container") |
287 | 287 |
} |
288 | 288 |
|
289 |
+ // Note: Actually copy the struct |
|
290 |
+ autoConfig := *b.config |
|
291 |
+ autoConfig.Cmd = autoCmd |
|
289 | 292 |
// Commit the container |
290 |
- image, err := b.builder.Commit(container, "", "", "", b.maintainer, nil) |
|
293 |
+ image, err := b.builder.Commit(container, "", "", "", b.maintainer, &autoConfig) |
|
291 | 294 |
if err != nil { |
292 | 295 |
return err |
293 | 296 |
} |
294 | 297 |
b.tmpImages[image.Id] = struct{}{} |
295 | 298 |
b.image = image.Id |
296 |
- b.needCommit = false |
|
297 | 299 |
return nil |
298 | 300 |
} |
299 | 301 |
|
300 | 302 |
func (b *buildFile) Build(dockerfile, context io.Reader) (string, error) { |
301 |
- defer b.clearTmp(b.tmpContainers, b.tmpImages) |
|
302 |
- |
|
303 | 303 |
if context != nil { |
304 | 304 |
name, err := ioutil.TempDir("/tmp", "docker-build") |
305 | 305 |
if err != nil { |
... | ... |
@@ -337,6 +335,7 @@ func (b *buildFile) Build(dockerfile, context io.Reader) (string, error) { |
337 | 337 |
method, exists := reflect.TypeOf(b).MethodByName("Cmd" + strings.ToUpper(instruction[:1]) + strings.ToLower(instruction[1:])) |
338 | 338 |
if !exists { |
339 | 339 |
fmt.Fprintf(b.out, "Skipping unknown instruction %s\n", strings.ToUpper(instruction)) |
340 |
+ continue |
|
340 | 341 |
} |
341 | 342 |
ret := method.Func.Call([]reflect.Value{reflect.ValueOf(b), reflect.ValueOf(arguments)})[0].Interface() |
342 | 343 |
if ret != nil { |
... | ... |
@@ -345,22 +344,10 @@ func (b *buildFile) Build(dockerfile, context io.Reader) (string, error) { |
345 | 345 |
|
346 | 346 |
fmt.Fprintf(b.out, "===> %v\n", b.image) |
347 | 347 |
} |
348 |
- if b.needCommit { |
|
349 |
- if err := b.commit(""); err != nil { |
|
350 |
- return "", err |
|
351 |
- } |
|
352 |
- } |
|
353 | 348 |
if b.image != "" { |
354 |
- // The build is successful, keep the temporary containers and images |
|
355 |
- for i := range b.tmpImages { |
|
356 |
- delete(b.tmpImages, i) |
|
357 |
- } |
|
358 |
- fmt.Fprintf(b.out, "Build success.\n Image id:\n%s\n", b.image) |
|
349 |
+ fmt.Fprintf(b.out, "Build successful.\n===> %s\n", b.image) |
|
359 | 350 |
return b.image, nil |
360 | 351 |
} |
361 |
- for i := range b.tmpContainers { |
|
362 |
- delete(b.tmpContainers, i) |
|
363 |
- } |
|
364 | 352 |
return "", fmt.Errorf("An error occured during the build\n") |
365 | 353 |
} |
366 | 354 |
|
... | ... |
@@ -17,6 +17,7 @@ import ( |
17 | 17 |
"net/url" |
18 | 18 |
"os" |
19 | 19 |
"os/signal" |
20 |
+ "path" |
|
20 | 21 |
"path/filepath" |
21 | 22 |
"reflect" |
22 | 23 |
"strconv" |
... | ... |
@@ -27,7 +28,7 @@ import ( |
27 | 27 |
"unicode" |
28 | 28 |
) |
29 | 29 |
|
30 |
-const VERSION = "0.3.3" |
|
30 |
+const VERSION = "0.3.4" |
|
31 | 31 |
|
32 | 32 |
var ( |
33 | 33 |
GIT_COMMIT string |
... | ... |
@@ -73,37 +74,37 @@ func (cli *DockerCli) CmdHelp(args ...string) error { |
73 | 73 |
} |
74 | 74 |
} |
75 | 75 |
help := fmt.Sprintf("Usage: docker [OPTIONS] COMMAND [arg...]\n -H=\"%s:%d\": Host:port to bind/connect to\n\nA self-sufficient runtime for linux containers.\n\nCommands:\n", cli.host, cli.port) |
76 |
- for cmd, description := range map[string]string{ |
|
77 |
- "attach": "Attach to a running container", |
|
78 |
- "build": "Build a container from a Dockerfile", |
|
79 |
- "commit": "Create a new image from a container's changes", |
|
80 |
- "diff": "Inspect changes on a container's filesystem", |
|
81 |
- "export": "Stream the contents of a container as a tar archive", |
|
82 |
- "history": "Show the history of an image", |
|
83 |
- "images": "List images", |
|
84 |
- "import": "Create a new filesystem image from the contents of a tarball", |
|
85 |
- "info": "Display system-wide information", |
|
86 |
- "insert": "Insert a file in an image", |
|
87 |
- "inspect": "Return low-level information on a container", |
|
88 |
- "kill": "Kill a running container", |
|
89 |
- "login": "Register or Login to the docker registry server", |
|
90 |
- "logs": "Fetch the logs of a container", |
|
91 |
- "port": "Lookup the public-facing port which is NAT-ed to PRIVATE_PORT", |
|
92 |
- "ps": "List containers", |
|
93 |
- "pull": "Pull an image or a repository from the docker registry server", |
|
94 |
- "push": "Push an image or a repository to the docker registry server", |
|
95 |
- "restart": "Restart a running container", |
|
96 |
- "rm": "Remove a container", |
|
97 |
- "rmi": "Remove an image", |
|
98 |
- "run": "Run a command in a new container", |
|
99 |
- "search": "Search for an image in the docker index", |
|
100 |
- "start": "Start a stopped container", |
|
101 |
- "stop": "Stop a running container", |
|
102 |
- "tag": "Tag an image into a repository", |
|
103 |
- "version": "Show the docker version information", |
|
104 |
- "wait": "Block until a container stops, then print its exit code", |
|
76 |
+ for _, command := range [][2]string{ |
|
77 |
+ {"attach", "Attach to a running container"}, |
|
78 |
+ {"build", "Build a container from a Dockerfile"}, |
|
79 |
+ {"commit", "Create a new image from a container's changes"}, |
|
80 |
+ {"diff", "Inspect changes on a container's filesystem"}, |
|
81 |
+ {"export", "Stream the contents of a container as a tar archive"}, |
|
82 |
+ {"history", "Show the history of an image"}, |
|
83 |
+ {"images", "List images"}, |
|
84 |
+ {"import", "Create a new filesystem image from the contents of a tarball"}, |
|
85 |
+ {"info", "Display system-wide information"}, |
|
86 |
+ {"insert", "Insert a file in an image"}, |
|
87 |
+ {"inspect", "Return low-level information on a container"}, |
|
88 |
+ {"kill", "Kill a running container"}, |
|
89 |
+ {"login", "Register or Login to the docker registry server"}, |
|
90 |
+ {"logs", "Fetch the logs of a container"}, |
|
91 |
+ {"port", "Lookup the public-facing port which is NAT-ed to PRIVATE_PORT"}, |
|
92 |
+ {"ps", "List containers"}, |
|
93 |
+ {"pull", "Pull an image or a repository from the docker registry server"}, |
|
94 |
+ {"push", "Push an image or a repository to the docker registry server"}, |
|
95 |
+ {"restart", "Restart a running container"}, |
|
96 |
+ {"rm", "Remove a container"}, |
|
97 |
+ {"rmi", "Remove an image"}, |
|
98 |
+ {"run", "Run a command in a new container"}, |
|
99 |
+ {"search", "Search for an image in the docker index"}, |
|
100 |
+ {"start", "Start a stopped container"}, |
|
101 |
+ {"stop", "Stop a running container"}, |
|
102 |
+ {"tag", "Tag an image into a repository"}, |
|
103 |
+ {"version", "Show the docker version information"}, |
|
104 |
+ {"wait", "Block until a container stops, then print its exit code"}, |
|
105 | 105 |
} { |
106 |
- help += fmt.Sprintf(" %-10.10s%s\n", cmd, description) |
|
106 |
+ help += fmt.Sprintf(" %-10.10s%s\n", command[0], command[1]) |
|
107 | 107 |
} |
108 | 108 |
fmt.Println(help) |
109 | 109 |
return nil |
... | ... |
@@ -130,16 +131,20 @@ func (cli *DockerCli) CmdInsert(args ...string) error { |
130 | 130 |
} |
131 | 131 |
|
132 | 132 |
func (cli *DockerCli) CmdBuild(args ...string) error { |
133 |
- cmd := Subcmd("build", "[OPTIONS] [CONTEXT]", "Build an image from a Dockerfile") |
|
134 |
- fileName := cmd.String("f", "Dockerfile", "Use `file` as Dockerfile. Can be '-' for stdin") |
|
133 |
+ cmd := Subcmd("build", "[OPTIONS] PATH | -", "Build a new container image from the source code at PATH") |
|
134 |
+ tag := cmd.String("t", "", "Tag to be applied to the resulting image in case of success") |
|
135 | 135 |
if err := cmd.Parse(args); err != nil { |
136 | 136 |
return nil |
137 | 137 |
} |
138 |
+ if cmd.NArg() != 1 { |
|
139 |
+ cmd.Usage() |
|
140 |
+ return nil |
|
141 |
+ } |
|
138 | 142 |
|
139 | 143 |
var ( |
140 |
- file io.ReadCloser |
|
141 | 144 |
multipartBody io.Reader |
142 |
- err error |
|
145 |
+ file io.ReadCloser |
|
146 |
+ contextPath string |
|
143 | 147 |
) |
144 | 148 |
|
145 | 149 |
// Init the needed component for the Multipart |
... | ... |
@@ -148,27 +153,19 @@ func (cli *DockerCli) CmdBuild(args ...string) error { |
148 | 148 |
w := multipart.NewWriter(buff) |
149 | 149 |
boundary := strings.NewReader("\r\n--" + w.Boundary() + "--\r\n") |
150 | 150 |
|
151 |
- // Create a FormFile multipart for the Dockerfile |
|
152 |
- if *fileName == "-" { |
|
151 |
+ compression := Bzip2 |
|
152 |
+ |
|
153 |
+ if cmd.Arg(0) == "-" { |
|
153 | 154 |
file = os.Stdin |
154 | 155 |
} else { |
155 |
- file, err = os.Open(*fileName) |
|
156 |
- if err != nil { |
|
156 |
+ // Send Dockerfile from arg/Dockerfile (deprecate later) |
|
157 |
+ if f, err := os.Open(path.Join(cmd.Arg(0), "Dockerfile")); err != nil { |
|
157 | 158 |
return err |
159 |
+ } else { |
|
160 |
+ file = f |
|
158 | 161 |
} |
159 |
- defer file.Close() |
|
160 |
- } |
|
161 |
- if wField, err := w.CreateFormFile("Dockerfile", *fileName); err != nil { |
|
162 |
- return err |
|
163 |
- } else { |
|
164 |
- io.Copy(wField, file) |
|
165 |
- } |
|
166 |
- multipartBody = io.MultiReader(multipartBody, boundary) |
|
167 |
- |
|
168 |
- compression := Bzip2 |
|
169 |
- |
|
170 |
- // Create a FormFile multipart for the context if needed |
|
171 |
- if cmd.Arg(0) != "" { |
|
162 |
+ // Send context from arg |
|
163 |
+ // Create a FormFile multipart for the context if needed |
|
172 | 164 |
// FIXME: Use NewTempArchive in order to have the size and avoid too much memory usage? |
173 | 165 |
context, err := Tar(cmd.Arg(0), compression) |
174 | 166 |
if err != nil { |
... | ... |
@@ -183,19 +180,28 @@ func (cli *DockerCli) CmdBuild(args ...string) error { |
183 | 183 |
return err |
184 | 184 |
} else { |
185 | 185 |
// FIXME: Find a way to have a progressbar for the upload too |
186 |
- io.Copy(wField, utils.ProgressReader(ioutil.NopCloser(context), -1, os.Stdout, "Caching Context %v/%v (%v)\r", false)) |
|
186 |
+ sf := utils.NewStreamFormatter(false) |
|
187 |
+ io.Copy(wField, utils.ProgressReader(ioutil.NopCloser(context), -1, os.Stdout, sf.FormatProgress("Caching Context", "%v/%v (%v)"), sf)) |
|
187 | 188 |
} |
188 |
- |
|
189 | 189 |
multipartBody = io.MultiReader(multipartBody, boundary) |
190 | 190 |
} |
191 |
+ // Create a FormFile multipart for the Dockerfile |
|
192 |
+ if wField, err := w.CreateFormFile("Dockerfile", "Dockerfile"); err != nil { |
|
193 |
+ return err |
|
194 |
+ } else { |
|
195 |
+ io.Copy(wField, file) |
|
196 |
+ } |
|
197 |
+ multipartBody = io.MultiReader(multipartBody, boundary) |
|
191 | 198 |
|
199 |
+ v := &url.Values{} |
|
200 |
+ v.Set("t", *tag) |
|
192 | 201 |
// Send the multipart request with correct content-type |
193 |
- req, err := http.NewRequest("POST", fmt.Sprintf("http://%s:%d%s", cli.host, cli.port, "/build"), multipartBody) |
|
202 |
+ req, err := http.NewRequest("POST", fmt.Sprintf("http://%s:%d%s?%s", cli.host, cli.port, "/build", v.Encode()), multipartBody) |
|
194 | 203 |
if err != nil { |
195 | 204 |
return err |
196 | 205 |
} |
197 | 206 |
req.Header.Set("Content-Type", w.FormDataContentType()) |
198 |
- if cmd.Arg(0) != "" { |
|
207 |
+ if contextPath != "" { |
|
199 | 208 |
req.Header.Set("X-Docker-Context-Compression", compression.Flag()) |
200 | 209 |
fmt.Println("Uploading Context...") |
201 | 210 |
} |
... | ... |
@@ -366,12 +372,10 @@ func (cli *DockerCli) CmdWait(args ...string) error { |
366 | 366 |
// 'docker version': show version information |
367 | 367 |
func (cli *DockerCli) CmdVersion(args ...string) error { |
368 | 368 |
cmd := Subcmd("version", "", "Show the docker version information.") |
369 |
- fmt.Println(len(args)) |
|
370 | 369 |
if err := cmd.Parse(args); err != nil { |
371 | 370 |
return nil |
372 | 371 |
} |
373 | 372 |
|
374 |
- fmt.Println(cmd.NArg()) |
|
375 | 373 |
if cmd.NArg() > 0 { |
376 | 374 |
cmd.Usage() |
377 | 375 |
return nil |
... | ... |
@@ -882,9 +886,9 @@ func (cli *DockerCli) CmdPs(args ...string) error { |
882 | 882 |
for _, out := range outs { |
883 | 883 |
if !*quiet { |
884 | 884 |
if *noTrunc { |
885 |
- fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s ago\t%s\n", out.Id, out.Image, out.Command, utils.HumanDuration(time.Now().Sub(time.Unix(out.Created, 0))), out.Status, out.Ports) |
|
885 |
+ fmt.Fprintf(w, "%s\t%s\t%s\t%s ago\t%s\t%s\n", out.Id, out.Image, out.Command, utils.HumanDuration(time.Now().Sub(time.Unix(out.Created, 0))), out.Status, out.Ports) |
|
886 | 886 |
} else { |
887 |
- fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s ago\t%s\n", utils.TruncateId(out.Id), out.Image, utils.Trunc(out.Command, 20), utils.HumanDuration(time.Now().Sub(time.Unix(out.Created, 0))), out.Status, out.Ports) |
|
887 |
+ fmt.Fprintf(w, "%s\t%s\t%s\t%s ago\t%s\t%s\n", utils.TruncateId(out.Id), out.Image, utils.Trunc(out.Command, 20), utils.HumanDuration(time.Now().Sub(time.Unix(out.Created, 0))), out.Status, out.Ports) |
|
888 | 888 |
} |
889 | 889 |
} else { |
890 | 890 |
if *noTrunc { |
... | ... |
@@ -996,12 +1000,10 @@ func (cli *DockerCli) CmdLogs(args ...string) error { |
996 | 996 |
return nil |
997 | 997 |
} |
998 | 998 |
|
999 |
- v := url.Values{} |
|
1000 |
- v.Set("logs", "1") |
|
1001 |
- v.Set("stdout", "1") |
|
1002 |
- v.Set("stderr", "1") |
|
1003 |
- |
|
1004 |
- if err := cli.hijack("POST", "/containers/"+cmd.Arg(0)+"/attach?"+v.Encode(), false); err != nil { |
|
999 |
+ if err := cli.stream("POST", "/containers/"+cmd.Arg(0)+"/attach?logs=1&stdout=1", nil, os.Stdout); err != nil { |
|
1000 |
+ return err |
|
1001 |
+ } |
|
1002 |
+ if err := cli.stream("POST", "/containers/"+cmd.Arg(0)+"/attach?logs=1&stderr=1", nil, os.Stderr); err != nil { |
|
1005 | 1003 |
return err |
1006 | 1004 |
} |
1007 | 1005 |
return nil |
... | ... |
@@ -1028,15 +1030,35 @@ func (cli *DockerCli) CmdAttach(args ...string) error { |
1028 | 1028 |
return err |
1029 | 1029 |
} |
1030 | 1030 |
|
1031 |
+ splitStderr := container.Config.Tty |
|
1032 |
+ |
|
1033 |
+ connections := 1 |
|
1034 |
+ if splitStderr { |
|
1035 |
+ connections += 1 |
|
1036 |
+ } |
|
1037 |
+ chErrors := make(chan error, connections) |
|
1038 |
+ cli.monitorTtySize(cmd.Arg(0)) |
|
1039 |
+ if splitStderr { |
|
1040 |
+ go func() { |
|
1041 |
+ chErrors <- cli.hijack("POST", "/containers/"+cmd.Arg(0)+"/attach?stream=1&stderr=1", false, nil, os.Stderr) |
|
1042 |
+ }() |
|
1043 |
+ } |
|
1031 | 1044 |
v := url.Values{} |
1032 | 1045 |
v.Set("stream", "1") |
1033 |
- v.Set("stdout", "1") |
|
1034 |
- v.Set("stderr", "1") |
|
1035 | 1046 |
v.Set("stdin", "1") |
1036 |
- |
|
1037 |
- cli.monitorTtySize(cmd.Arg(0)) |
|
1038 |
- if err := cli.hijack("POST", "/containers/"+cmd.Arg(0)+"/attach?"+v.Encode(), container.Config.Tty); err != nil { |
|
1039 |
- return err |
|
1047 |
+ v.Set("stdout", "1") |
|
1048 |
+ if !splitStderr { |
|
1049 |
+ v.Set("stderr", "1") |
|
1050 |
+ } |
|
1051 |
+ go func() { |
|
1052 |
+ chErrors <- cli.hijack("POST", "/containers/"+cmd.Arg(0)+"/attach?"+v.Encode(), container.Config.Tty, os.Stdin, os.Stdout) |
|
1053 |
+ }() |
|
1054 |
+ for connections > 0 { |
|
1055 |
+ err := <-chErrors |
|
1056 |
+ if err != nil { |
|
1057 |
+ return err |
|
1058 |
+ } |
|
1059 |
+ connections -= 1 |
|
1040 | 1060 |
} |
1041 | 1061 |
return nil |
1042 | 1062 |
} |
... | ... |
@@ -1200,19 +1222,14 @@ func (cli *DockerCli) CmdRun(args ...string) error { |
1200 | 1200 |
fmt.Fprintln(os.Stderr, "WARNING: ", warning) |
1201 | 1201 |
} |
1202 | 1202 |
|
1203 |
- v := url.Values{} |
|
1204 |
- v.Set("logs", "1") |
|
1205 |
- v.Set("stream", "1") |
|
1203 |
+ splitStderr := !config.Tty |
|
1206 | 1204 |
|
1207 |
- if config.AttachStdin { |
|
1208 |
- v.Set("stdin", "1") |
|
1205 |
+ connections := 0 |
|
1206 |
+ if config.AttachStdin || config.AttachStdout || (!splitStderr && config.AttachStderr) { |
|
1207 |
+ connections += 1 |
|
1209 | 1208 |
} |
1210 |
- if config.AttachStdout { |
|
1211 |
- v.Set("stdout", "1") |
|
1212 |
- } |
|
1213 |
- if config.AttachStderr { |
|
1214 |
- v.Set("stderr", "1") |
|
1215 |
- |
|
1209 |
+ if splitStderr && config.AttachStderr { |
|
1210 |
+ connections += 1 |
|
1216 | 1211 |
} |
1217 | 1212 |
|
1218 | 1213 |
//start the container |
... | ... |
@@ -1221,10 +1238,38 @@ func (cli *DockerCli) CmdRun(args ...string) error { |
1221 | 1221 |
return err |
1222 | 1222 |
} |
1223 | 1223 |
|
1224 |
- if config.AttachStdin || config.AttachStdout || config.AttachStderr { |
|
1224 |
+ if connections > 0 { |
|
1225 |
+ chErrors := make(chan error, connections) |
|
1225 | 1226 |
cli.monitorTtySize(out.Id) |
1226 |
- if err := cli.hijack("POST", "/containers/"+out.Id+"/attach?"+v.Encode(), config.Tty); err != nil { |
|
1227 |
- return err |
|
1227 |
+ |
|
1228 |
+ if splitStderr && config.AttachStderr { |
|
1229 |
+ go func() { |
|
1230 |
+ chErrors <- cli.hijack("POST", "/containers/"+out.Id+"/attach?logs=1&stream=1&stderr=1", config.Tty, nil, os.Stderr) |
|
1231 |
+ }() |
|
1232 |
+ } |
|
1233 |
+ |
|
1234 |
+ v := url.Values{} |
|
1235 |
+ v.Set("logs", "1") |
|
1236 |
+ v.Set("stream", "1") |
|
1237 |
+ |
|
1238 |
+ if config.AttachStdin { |
|
1239 |
+ v.Set("stdin", "1") |
|
1240 |
+ } |
|
1241 |
+ if config.AttachStdout { |
|
1242 |
+ v.Set("stdout", "1") |
|
1243 |
+ } |
|
1244 |
+ if !splitStderr && config.AttachStderr { |
|
1245 |
+ v.Set("stderr", "1") |
|
1246 |
+ } |
|
1247 |
+ go func() { |
|
1248 |
+ chErrors <- cli.hijack("POST", "/containers/"+out.Id+"/attach?"+v.Encode(), config.Tty, os.Stdin, os.Stdout) |
|
1249 |
+ }() |
|
1250 |
+ for connections > 0 { |
|
1251 |
+ err := <-chErrors |
|
1252 |
+ if err != nil { |
|
1253 |
+ return err |
|
1254 |
+ } |
|
1255 |
+ connections -= 1 |
|
1228 | 1256 |
} |
1229 | 1257 |
} |
1230 | 1258 |
if !config.AttachStdout && !config.AttachStderr { |
... | ... |
@@ -1334,13 +1379,9 @@ func (cli *DockerCli) stream(method, path string, in io.Reader, out io.Writer) e |
1334 | 1334 |
} |
1335 | 1335 |
|
1336 | 1336 |
if resp.Header.Get("Content-Type") == "application/json" { |
1337 |
- type Message struct { |
|
1338 |
- Status string `json:"status,omitempty"` |
|
1339 |
- Progress string `json:"progress,omitempty"` |
|
1340 |
- } |
|
1341 | 1337 |
dec := json.NewDecoder(resp.Body) |
1342 | 1338 |
for { |
1343 |
- var m Message |
|
1339 |
+ var m utils.JsonMessage |
|
1344 | 1340 |
if err := dec.Decode(&m); err == io.EOF { |
1345 | 1341 |
break |
1346 | 1342 |
} else if err != nil { |
... | ... |
@@ -1348,6 +1389,8 @@ func (cli *DockerCli) stream(method, path string, in io.Reader, out io.Writer) e |
1348 | 1348 |
} |
1349 | 1349 |
if m.Progress != "" { |
1350 | 1350 |
fmt.Fprintf(out, "Downloading %s\r", m.Progress) |
1351 |
+ } else if m.Error != "" { |
|
1352 |
+ return fmt.Errorf(m.Error) |
|
1351 | 1353 |
} else { |
1352 | 1354 |
fmt.Fprintf(out, "%s\n", m.Status) |
1353 | 1355 |
} |
... | ... |
@@ -1360,7 +1403,7 @@ func (cli *DockerCli) stream(method, path string, in io.Reader, out io.Writer) e |
1360 | 1360 |
return nil |
1361 | 1361 |
} |
1362 | 1362 |
|
1363 |
-func (cli *DockerCli) hijack(method, path string, setRawTerminal bool) error { |
|
1363 |
+func (cli *DockerCli) hijack(method, path string, setRawTerminal bool, in *os.File, out io.Writer) error { |
|
1364 | 1364 |
req, err := http.NewRequest(method, fmt.Sprintf("/v%g%s", API_VERSION, path), nil) |
1365 | 1365 |
if err != nil { |
1366 | 1366 |
return err |
... | ... |
@@ -1378,20 +1421,19 @@ func (cli *DockerCli) hijack(method, path string, setRawTerminal bool) error { |
1378 | 1378 |
defer rwc.Close() |
1379 | 1379 |
|
1380 | 1380 |
receiveStdout := utils.Go(func() error { |
1381 |
- _, err := io.Copy(os.Stdout, br) |
|
1381 |
+ _, err := io.Copy(out, br) |
|
1382 | 1382 |
return err |
1383 | 1383 |
}) |
1384 | 1384 |
|
1385 |
- if setRawTerminal && term.IsTerminal(int(os.Stdin.Fd())) && os.Getenv("NORAW") == "" { |
|
1385 |
+ if in != nil && setRawTerminal && term.IsTerminal(int(in.Fd())) && os.Getenv("NORAW") == "" { |
|
1386 | 1386 |
if oldState, err := term.SetRawTerminal(); err != nil { |
1387 | 1387 |
return err |
1388 | 1388 |
} else { |
1389 | 1389 |
defer term.RestoreTerminal(oldState) |
1390 | 1390 |
} |
1391 | 1391 |
} |
1392 |
- |
|
1393 | 1392 |
sendStdin := utils.Go(func() error { |
1394 |
- _, err := io.Copy(rwc, os.Stdin) |
|
1393 |
+ _, err := io.Copy(rwc, in) |
|
1395 | 1394 |
if err := rwc.(*net.TCPConn).CloseWrite(); err != nil { |
1396 | 1395 |
fmt.Fprintf(os.Stderr, "Couldn't send EOF: %s\n", err) |
1397 | 1396 |
} |
... | ... |
@@ -2,18 +2,15 @@ |
2 | 2 |
set -e |
3 | 3 |
|
4 | 4 |
# these should match the names found at http://www.debian.org/releases/ |
5 |
-stableSuite='squeeze' |
|
6 |
-testingSuite='wheezy' |
|
5 |
+stableSuite='wheezy' |
|
6 |
+testingSuite='jessie' |
|
7 | 7 |
unstableSuite='sid' |
8 | 8 |
|
9 |
-# if suite is equal to this, it gets the "latest" tag |
|
10 |
-latestSuite="$testingSuite" |
|
11 |
- |
|
12 | 9 |
variant='minbase' |
13 | 10 |
include='iproute,iputils-ping' |
14 | 11 |
|
15 | 12 |
repo="$1" |
16 |
-suite="${2:-$latestSuite}" |
|
13 |
+suite="${2:-$stableSuite}" |
|
17 | 14 |
mirror="${3:-}" # stick to the default debootstrap mirror if one is not provided |
18 | 15 |
|
19 | 16 |
if [ ! "$repo" ]; then |
... | ... |
@@ -41,17 +38,14 @@ img=$(sudo tar -c . | docker import -) |
41 | 41 |
# tag suite |
42 | 42 |
docker tag $img $repo $suite |
43 | 43 |
|
44 |
-if [ "$suite" = "$latestSuite" ]; then |
|
45 |
- # tag latest |
|
46 |
- docker tag $img $repo latest |
|
47 |
-fi |
|
48 |
- |
|
49 | 44 |
# test the image |
50 | 45 |
docker run -i -t $repo:$suite echo success |
51 | 46 |
|
52 |
-# unstable's version numbers match testing (since it's mostly just a sandbox for testing), so it doesn't get a version number tag |
|
53 |
-if [ "$suite" != "$unstableSuite" -a "$suite" != 'unstable' ]; then |
|
54 |
- # tag the specific version |
|
47 |
+if [ "$suite" = "$stableSuite" -o "$suite" = 'stable' ]; then |
|
48 |
+ # tag latest |
|
49 |
+ docker tag $img $repo latest |
|
50 |
+ |
|
51 |
+ # tag the specific debian release version |
|
55 | 52 |
ver=$(docker run $repo:$suite cat /etc/debian_version) |
56 | 53 |
docker tag $img $repo $ver |
57 | 54 |
fi |
... | ... |
@@ -6,6 +6,7 @@ SPHINXOPTS = |
6 | 6 |
SPHINXBUILD = sphinx-build |
7 | 7 |
PAPER = |
8 | 8 |
BUILDDIR = _build |
9 |
+PYTHON = python |
|
9 | 10 |
|
10 | 11 |
# Internal variables. |
11 | 12 |
PAPEROPT_a4 = -D latex_paper_size=a4 |
... | ... |
@@ -38,6 +39,7 @@ help: |
38 | 38 |
# @echo " linkcheck to check all external links for integrity" |
39 | 39 |
# @echo " doctest to run all doctests embedded in the documentation (if enabled)" |
40 | 40 |
@echo " docs to build the docs and copy the static files to the outputdir" |
41 |
+ @echo " server to serve the docs in your browser under \`http://localhost:8000\`" |
|
41 | 42 |
@echo " publish to publish the app to dotcloud" |
42 | 43 |
|
43 | 44 |
clean: |
... | ... |
@@ -49,6 +51,8 @@ docs: |
49 | 49 |
@echo |
50 | 50 |
@echo "Build finished. The documentation pages are now in $(BUILDDIR)/html." |
51 | 51 |
|
52 |
+server: |
|
53 |
+ @cd $(BUILDDIR)/html; $(PYTHON) -m SimpleHTTPServer 8000 |
|
52 | 54 |
|
53 | 55 |
site: |
54 | 56 |
cp -r website $(BUILDDIR)/ |
... | ... |
@@ -14,20 +14,22 @@ Installation |
14 | 14 |
------------ |
15 | 15 |
|
16 | 16 |
* Work in your own fork of the code, we accept pull requests. |
17 |
-* Install sphinx: ``pip install sphinx`` |
|
18 |
-* Install sphinx httpdomain contrib package ``sphinxcontrib-httpdomain`` |
|
17 |
+* Install sphinx: `pip install sphinx` |
|
18 |
+ * Mac OS X: `[sudo] pip-2.7 install sphinx`) |
|
19 |
+* Install sphinx httpdomain contrib package: `pip install sphinxcontrib-httpdomain` |
|
20 |
+ * Mac OS X: `[sudo] pip-2.7 install sphinxcontrib-httpdomain` |
|
19 | 21 |
* If pip is not available you can probably install it using your favorite package manager as **python-pip** |
20 | 22 |
|
21 | 23 |
Usage |
22 | 24 |
----- |
23 |
-* change the .rst files with your favorite editor to your liking |
|
24 |
-* run *make docs* to clean up old files and generate new ones |
|
25 |
-* your static website can now be found in the _build dir |
|
26 |
-* to preview what you have generated, cd into _build/html and then run 'python -m SimpleHTTPServer 8000' |
|
25 |
+* Change the `.rst` files with your favorite editor to your liking. |
|
26 |
+* Run `make docs` to clean up old files and generate new ones. |
|
27 |
+* Your static website can now be found in the `_build` directory. |
|
28 |
+* To preview what you have generated run `make server` and open <http://localhost:8000/> in your favorite browser. |
|
27 | 29 |
|
28 |
-Working using github's file editor |
|
30 |
+Working using GitHub's file editor |
|
29 | 31 |
---------------------------------- |
30 |
-Alternatively, for small changes and typo's you might want to use github's built in file editor. It allows |
|
32 |
+Alternatively, for small changes and typo's you might want to use GitHub's built in file editor. It allows |
|
31 | 33 |
you to preview your changes right online. Just be carefull not to create many commits. |
32 | 34 |
|
33 | 35 |
Images |
... | ... |
@@ -72,4 +74,4 @@ Guides on using sphinx |
72 | 72 |
|
73 | 73 |
* Code examples |
74 | 74 |
|
75 |
- Start without $, so it's easy to copy and paste. |
|
76 | 75 |
\ No newline at end of file |
76 |
+ Start without $, so it's easy to copy and paste. |
... | ... |
@@ -15,10 +15,17 @@ Docker Remote API |
15 | 15 |
- Default port in the docker deamon is 4243 |
16 | 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 | 17 |
|
18 |
-2. Endpoints |
|
18 |
+2. Version |
|
19 |
+========== |
|
20 |
+ |
|
21 |
+The current verson of the API is 1.1 |
|
22 |
+Calling /images/<name>/insert is the same as calling /v1.1/images/<name>/insert |
|
23 |
+You can still call an old version of the api using /v1.0/images/<name>/insert |
|
24 |
+ |
|
25 |
+3. Endpoints |
|
19 | 26 |
============ |
20 | 27 |
|
21 |
-2.1 Containers |
|
28 |
+3.1 Containers |
|
22 | 29 |
-------------- |
23 | 30 |
|
24 | 31 |
List containers |
... | ... |
@@ -132,6 +139,7 @@ Create a container |
132 | 132 |
:jsonparam config: the container's configuration |
133 | 133 |
:statuscode 201: no error |
134 | 134 |
:statuscode 404: no such container |
135 |
+ :statuscode 406: impossible to attach (container not running) |
|
135 | 136 |
:statuscode 500: server error |
136 | 137 |
|
137 | 138 |
|
... | ... |
@@ -459,7 +467,7 @@ Remove a container |
459 | 459 |
:statuscode 500: server error |
460 | 460 |
|
461 | 461 |
|
462 |
-2.2 Images |
|
462 |
+3.2 Images |
|
463 | 463 |
---------- |
464 | 464 |
|
465 | 465 |
List Images |
... | ... |
@@ -548,7 +556,19 @@ Create an image |
548 | 548 |
|
549 | 549 |
POST /images/create?fromImage=base HTTP/1.1 |
550 | 550 |
|
551 |
- **Example response**: |
|
551 |
+ **Example response v1.1**: |
|
552 |
+ |
|
553 |
+ .. sourcecode:: http |
|
554 |
+ |
|
555 |
+ HTTP/1.1 200 OK |
|
556 |
+ Content-Type: application/json |
|
557 |
+ |
|
558 |
+ {"status":"Pulling..."} |
|
559 |
+ {"progress":"1/? (n/a)"} |
|
560 |
+ {"error":"Invalid..."} |
|
561 |
+ ... |
|
562 |
+ |
|
563 |
+ **Example response v1.0**: |
|
552 | 564 |
|
553 | 565 |
.. sourcecode:: http |
554 | 566 |
|
... | ... |
@@ -579,7 +599,19 @@ Insert a file in a image |
579 | 579 |
|
580 | 580 |
POST /images/test/insert?path=/usr&url=myurl HTTP/1.1 |
581 | 581 |
|
582 |
- **Example response**: |
|
582 |
+ **Example response v1.1**: |
|
583 |
+ |
|
584 |
+ .. sourcecode:: http |
|
585 |
+ |
|
586 |
+ HTTP/1.1 200 OK |
|
587 |
+ Content-Type: application/json |
|
588 |
+ |
|
589 |
+ {"status":"Inserting..."} |
|
590 |
+ {"progress":"1/? (n/a)"} |
|
591 |
+ {"error":"Invalid..."} |
|
592 |
+ ... |
|
593 |
+ |
|
594 |
+ **Example response v1.0**: |
|
583 | 595 |
|
584 | 596 |
.. sourcecode:: http |
585 | 597 |
|
... | ... |
@@ -694,7 +726,19 @@ Push an image on the registry |
694 | 694 |
|
695 | 695 |
POST /images/test/push HTTP/1.1 |
696 | 696 |
|
697 |
- **Example response**: |
|
697 |
+ **Example response v1.1**: |
|
698 |
+ |
|
699 |
+ .. sourcecode:: http |
|
700 |
+ |
|
701 |
+ HTTP/1.1 200 OK |
|
702 |
+ Content-Type: application/json |
|
703 |
+ |
|
704 |
+ {"status":"Pushing..."} |
|
705 |
+ {"progress":"1/? (n/a)"} |
|
706 |
+ {"error":"Invalid..."} |
|
707 |
+ ... |
|
708 |
+ |
|
709 |
+ **Example response v1.0**: |
|
698 | 710 |
|
699 | 711 |
.. sourcecode:: http |
700 | 712 |
|
... | ... |
@@ -817,7 +861,7 @@ Search images |
817 | 817 |
:statuscode 500: server error |
818 | 818 |
|
819 | 819 |
|
820 |
-2.3 Misc |
|
820 |
+3.3 Misc |
|
821 | 821 |
-------- |
822 | 822 |
|
823 | 823 |
Build an image from Dockerfile via stdin |
... | ... |
@@ -843,6 +887,7 @@ Build an image from Dockerfile via stdin |
843 | 843 |
|
844 | 844 |
{{ STREAM }} |
845 | 845 |
|
846 |
+ :query t: tag to be applied to the resulting image in case of success |
|
846 | 847 |
:statuscode 200: no error |
847 | 848 |
:statuscode 500: server error |
848 | 849 |
|
... | ... |
@@ -246,7 +246,6 @@ The Index has two main purposes (along with its fancy social features): |
246 | 246 |
|
247 | 247 |
- Resolve short names (to avoid passing absolute URLs all the time) |
248 | 248 |
- username/projectname -> \https://registry.docker.io/users/<username>/repositories/<projectname>/ |
249 |
- - team/projectname -> \https://registry.docker.io/team/<team>/repositories/<projectname>/ |
|
250 | 249 |
- Authenticate a user as a repos owner (for a central referenced repository) |
251 | 250 |
|
252 | 251 |
3.1 Without an Index |
... | ... |
@@ -2,12 +2,27 @@ |
2 | 2 |
:description: Build a new image from the Dockerfile passed via stdin |
3 | 3 |
:keywords: build, docker, container, documentation |
4 | 4 |
|
5 |
-======================================================== |
|
6 |
-``build`` -- Build a container from Dockerfile via stdin |
|
7 |
-======================================================== |
|
5 |
+================================================ |
|
6 |
+``build`` -- Build a container from a Dockerfile |
|
7 |
+================================================ |
|
8 | 8 |
|
9 | 9 |
:: |
10 | 10 |
|
11 |
- Usage: docker build - |
|
12 |
- Example: cat Dockerfile | docker build - |
|
13 |
- Build a new image from the Dockerfile passed via stdin |
|
11 |
+ Usage: docker build [OPTIONS] PATH | - |
|
12 |
+ Build a new container image from the source code at PATH |
|
13 |
+ -t="": Tag to be applied to the resulting image in case of success. |
|
14 |
+ |
|
15 |
+Examples |
|
16 |
+-------- |
|
17 |
+ |
|
18 |
+.. code-block:: bash |
|
19 |
+ |
|
20 |
+ docker build . |
|
21 |
+ |
|
22 |
+This will take the local Dockerfile |
|
23 |
+ |
|
24 |
+.. code-block:: bash |
|
25 |
+ |
|
26 |
+ docker build - |
|
27 |
+ |
|
28 |
+This will read a Dockerfile form Stdin without context |
14 | 29 |
deleted file mode 100644 |
... | ... |
@@ -1,25 +0,0 @@ |
1 |
-:title: Building Blocks |
|
2 |
-:description: An introduction to docker and standard containers? |
|
3 |
-:keywords: containers, lxc, concepts, explanation |
|
4 |
- |
|
5 |
- |
|
6 |
-Building blocks |
|
7 |
-=============== |
|
8 |
- |
|
9 |
-.. _images: |
|
10 |
- |
|
11 |
-Images |
|
12 |
-An original container image. These are stored on disk and are comparable with what you normally expect from a stopped virtual machine image. Images are stored (and retrieved from) repository |
|
13 |
- |
|
14 |
-Images are stored on your local file system under /var/lib/docker/graph |
|
15 |
- |
|
16 |
- |
|
17 |
-.. _containers: |
|
18 |
- |
|
19 |
-Containers |
|
20 |
-A container is a local version of an image. It can be running or stopped, The equivalent would be a virtual machine instance. |
|
21 |
- |
|
22 |
-Containers are stored on your local file system under /var/lib/docker/containers |
|
23 |
- |
... | ... |
@@ -5,8 +5,8 @@ |
5 | 5 |
Introduction |
6 | 6 |
============ |
7 | 7 |
|
8 |
-Docker - The Linux container runtime |
|
8 |
+Docker -- The Linux container runtime |
|
9 |
+------------------------------------- |
|
9 | 10 |
|
10 | 11 |
Docker complements LXC with a high-level API which operates at the process level. It runs unix processes with strong guarantees of isolation and repeatability across servers. |
11 | 12 |
|
... | ... |
@@ -1,8 +1,8 @@ |
1 |
-:title: Setting up a dev environment |
|
1 |
+:title: Setting Up a Dev Environment |
|
2 | 2 |
:description: Guides on how to contribute to docker |
3 | 3 |
:keywords: Docker, documentation, developers, contributing, dev environment |
4 | 4 |
|
5 |
-Setting up a dev environment |
|
5 |
+Setting Up a Dev Environment |
|
6 | 6 |
============================ |
7 | 7 |
|
8 | 8 |
Instructions that have been verified to work on Ubuntu 12.10, |
... | ... |
@@ -1,6 +1,6 @@ |
1 | 1 |
:title: Docker Examples |
2 | 2 |
:description: Examples on how to use Docker |
3 |
-:keywords: docker, hello world, examples |
|
3 |
+:keywords: docker, hello world, node, nodejs, python, couch, couchdb, redis, ssh, sshd, examples |
|
4 | 4 |
|
5 | 5 |
|
6 | 6 |
|
... | ... |
@@ -16,6 +16,7 @@ Contents: |
16 | 16 |
hello_world |
17 | 17 |
hello_world_daemon |
18 | 18 |
python_web_app |
19 |
+ nodejs_web_app |
|
19 | 20 |
running_redis_service |
20 | 21 |
running_ssh_service |
21 | 22 |
couchdb_data_volumes |
22 | 23 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,236 @@ |
0 |
+:title: Running a Node.js app on CentOS |
|
1 |
+:description: Installing and running a Node.js app on CentOS |
|
2 |
+:keywords: docker, example, package installation, node, centos |
|
3 |
+ |
|
4 |
+.. _nodejs_web_app: |
|
5 |
+ |
|
6 |
+Node.js Web App |
|
7 |
+=============== |
|
8 |
+ |
|
9 |
+.. include:: example_header.inc |
|
10 |
+ |
|
11 |
+The goal of this example is to show you how you can build your own docker images |
|
12 |
+from a parent image using a ``Dockerfile`` . We will do that by making a simple |
|
13 |
+Node.js hello world web application running on CentOS. You can get the full |
|
14 |
+source code at https://github.com/gasi/docker-node-hello. |
|
15 |
+ |
|
16 |
+Create Node.js app |
|
17 |
+ |
|
18 |
+First, create a ``package.json`` file that describes your app and its |
|
19 |
+dependencies: |
|
20 |
+ |
|
21 |
+.. code-block:: json |
|
22 |
+ |
|
23 |
+ { |
|
24 |
+ "name": "docker-centos-hello", |
|
25 |
+ "private": true, |
|
26 |
+ "version": "0.0.1", |
|
27 |
+ "description": "Node.js Hello World app on CentOS using docker", |
|
28 |
+ "author": "Daniel Gasienica <daniel@gasienica.ch>", |
|
29 |
+ "dependencies": { |
|
30 |
+ "express": "3.2.4" |
|
31 |
+ } |
|
32 |
+ } |
|
33 |
+ |
|
34 |
+Then, create an ``index.js`` file that defines a web app using the |
|
35 |
+`Express.js <http://expressjs.com/>`_ framework: |
|
36 |
+ |
|
37 |
+.. code-block:: javascript |
|
38 |
+ |
|
39 |
+ var express = require('express'); |
|
40 |
+ |
|
41 |
+ // Constants |
|
42 |
+ var PORT = 8080; |
|
43 |
+ |
|
44 |
+ // App |
|
45 |
+ var app = express(); |
|
46 |
+ app.get('/', function (req, res) { |
|
47 |
+ res.send('Hello World\n'); |
|
48 |
+ }); |
|
49 |
+ |
|
50 |
+ app.listen(PORT) |
|
51 |
+ console.log('Running on http://localhost:' + PORT); |
|
52 |
+ |
|
53 |
+ |
|
54 |
+In the next steps, we’ll look at how you can run this app inside a CentOS |
|
55 |
+container using docker. First, you’ll need to build a docker image of your app. |
|
56 |
+ |
|
57 |
+Creating a ``Dockerfile`` |
|
58 |
+ |
|
59 |
+Create an empty file called ``Dockerfile``: |
|
60 |
+ |
|
61 |
+.. code-block:: bash |
|
62 |
+ |
|
63 |
+ touch Dockerfile |
|
64 |
+ |
|
65 |
+Open the ``Dockerfile`` in your favorite text editor and add the following line |
|
66 |
+that defines the version of docker the image requires to build |
|
67 |
+(this example uses docker 0.3.4): |
|
68 |
+ |
|
69 |
+.. code-block:: bash |
|
70 |
+ |
|
71 |
+ # DOCKER-VERSION 0.3.4 |
|
72 |
+ |
|
73 |
+Next, define the parent image you want to use to build your own image on top of. |
|
74 |
+Here, we’ll use `CentOS <https://index.docker.io/_/centos/>`_ (tag: ``6.4``) |
|
75 |
+available on the `docker index`_: |
|
76 |
+ |
|
77 |
+.. code-block:: bash |
|
78 |
+ |
|
79 |
+ FROM centos:6.4 |
|
80 |
+ |
|
81 |
+Since we’re building a Node.js app, you’ll have to install Node.js as well as |
|
82 |
+npm on your CentOS image. Node.js is required to run your app and npm to install |
|
83 |
+your app’s dependencies defined in ``package.json``. |
|
84 |
+To install the right package for CentOS, we’ll use the instructions from the |
|
85 |
+`Node.js wiki`_: |
|
86 |
+ |
|
87 |
+.. code-block:: bash |
|
88 |
+ |
|
89 |
+ # Enable EPEL for Node.js |
|
90 |
+ RUN rpm -Uvh http://download.fedoraproject.org/pub/epel/6/i386/epel-release-6-8.noarch.rpm |
|
91 |
+ # Install Node.js and npm |
|
92 |
+ RUN yum install -y npm-1.2.17-5.el6 |
|
93 |
+ |
|
94 |
+To bundle your app’s source code inside the docker image, use the ``ADD`` |
|
95 |
+command: |
|
96 |
+ |
|
97 |
+.. code-block:: bash |
|
98 |
+ |
|
99 |
+ # Bundle app source |
|
100 |
+ ADD . /src |
|
101 |
+ |
|
102 |
+Install your app dependencies using npm: |
|
103 |
+ |
|
104 |
+.. code-block:: bash |
|
105 |
+ |
|
106 |
+ # Install app dependencies |
|
107 |
+ RUN cd /src; npm install |
|
108 |
+ |
|
109 |
+Your app binds to port ``8080`` so you’ll use the ``EXPOSE`` command to have it |
|
110 |
+mapped by the docker daemon: |
|
111 |
+ |
|
112 |
+.. code-block:: bash |
|
113 |
+ |
|
114 |
+ EXPOSE 8080 |
|
115 |
+ |
|
116 |
+Last but not least, define the command to run your app using ``CMD`` which |
|
117 |
+defines your runtime, i.e. ``node``, and the path to our app, i.e. |
|
118 |
+``src/index.js`` (see the step where we added the source to the container): |
|
119 |
+ |
|
120 |
+.. code-block:: bash |
|
121 |
+ |
|
122 |
+ CMD ["node", "/src/index.js"] |
|
123 |
+ |
|
124 |
+Your ``Dockerfile`` should now look like this: |
|
125 |
+ |
|
126 |
+.. code-block:: bash |
|
127 |
+ |
|
128 |
+ |
|
129 |
+ # DOCKER-VERSION 0.3.4 |
|
130 |
+ FROM centos:6.4 |
|
131 |
+ |
|
132 |
+ # Enable EPEL for Node.js |
|
133 |
+ RUN rpm -Uvh http://download.fedoraproject.org/pub/epel/6/i386/epel-release-6-8.noarch.rpm |
|
134 |
+ # Install Node.js and npm |
|
135 |
+ RUN yum install -y npm-1.2.17-5.el6 |
|
136 |
+ |
|
137 |
+ # Bundle app source |
|
138 |
+ ADD . /src |
|
139 |
+ # Install app dependencies |
|
140 |
+ RUN cd /src; npm install |
|
141 |
+ |
|
142 |
+ EXPOSE 8080 |
|
143 |
+ CMD ["node", "/src/index.js"] |
|
144 |
+ |
|
145 |
+ |
|
146 |
+Building your image |
|
147 |
+ |
|
148 |
+Go to the directory that has your ``Dockerfile`` and run the following command |
|
149 |
+to build a docker image. The ``-t`` flag let’s you tag your image so it’s easier |
|
150 |
+to find later using the ``docker images`` command: |
|
151 |
+ |
|
152 |
+.. code-block:: bash |
|
153 |
+ |
|
154 |
+ docker build -t <your username>/centos-node-hello . |
|
155 |
+ |
|
156 |
+Your image will now be listed by docker: |
|
157 |
+ |
|
158 |
+.. code-block:: bash |
|
159 |
+ |
|
160 |
+ docker images |
|
161 |
+ |
|
162 |
+ > # Example |
|
163 |
+ > REPOSITORY TAG ID CREATED |
|
164 |
+ > centos 6.4 539c0211cd76 8 weeks ago |
|
165 |
+ > gasi/centos-node-hello latest d64d3505b0d2 2 hours ago |
|
166 |
+ |
|
167 |
+ |
|
168 |
+Run the image |
|
169 |
+ |
|
170 |
+Running your image with ``-d`` runs the container in detached mode, leaving the |
|
171 |
+container running in the background. Run the image you previously built: |
|
172 |
+ |
|
173 |
+.. code-block:: bash |
|
174 |
+ |
|
175 |
+ docker run -d <your username>/centos-node-hello |
|
176 |
+ |
|
177 |
+Print the output of your app: |
|
178 |
+ |
|
179 |
+.. code-block:: bash |
|
180 |
+ |
|
181 |
+ # Get container ID |
|
182 |
+ docker ps |
|
183 |
+ |
|
184 |
+ # Print app output |
|
185 |
+ docker logs <container id> |
|
186 |
+ |
|
187 |
+ > # Example |
|
188 |
+ > Running on http://localhost:8080 |
|
189 |
+ |
|
190 |
+ |
|
191 |
+Test |
|
192 |
+ |
|
193 |
+To test your app, get the the port of your app that docker mapped: |
|
194 |
+ |
|
195 |
+.. code-block:: bash |
|
196 |
+ |
|
197 |
+ docker ps |
|
198 |
+ |
|
199 |
+ > # Example |
|
200 |
+ > ID IMAGE COMMAND ... PORTS |
|
201 |
+ > ecce33b30ebf gasi/centos-node-hello:latest node /src/index.js 49160->8080 |
|
202 |
+ |
|
203 |
+In the example above, docker mapped the ``8080`` port of the container to |
|
204 |
+``49160``. |
|
205 |
+ |
|
206 |
+Now you can call your app using ``curl`` (install if needed via: |
|
207 |
+``sudo apt-get install curl``): |
|
208 |
+ |
|
209 |
+.. code-block:: bash |
|
210 |
+ |
|
211 |
+ curl -i localhost:49160 |
|
212 |
+ |
|
213 |
+ > HTTP/1.1 200 OK |
|
214 |
+ > X-Powered-By: Express |
|
215 |
+ > Content-Type: text/html; charset=utf-8 |
|
216 |
+ > Content-Length: 12 |
|
217 |
+ > Date: Sun, 02 Jun 2013 03:53:22 GMT |
|
218 |
+ > Connection: keep-alive |
|
219 |
+ > |
|
220 |
+ > Hello World |
|
221 |
+ |
|
222 |
+We hope this tutorial helped you get up and running with Node.js and CentOS on |
|
223 |
+docker. You can get the full source code at |
|
224 |
+https://github.com/gasi/docker-node-hello. |
|
225 |
+ |
|
226 |
+Continue to :ref:`running_redis_service`. |
|
227 |
+ |
|
228 |
+ |
|
229 |
+.. _Node.js wiki: https://github.com/joyent/node/wiki/Installing-Node.js-via-package-manager#rhelcentosscientific-linux-6 |
|
230 |
+.. _docker index: https://index.docker.io/ |
... | ... |
@@ -4,8 +4,8 @@ |
4 | 4 |
|
5 | 5 |
.. _running_redis_service: |
6 | 6 |
|
7 |
-Create a redis service |
|
8 |
-====================== |
|
7 |
+Redis Service |
|
8 |
+============= |
|
9 | 9 |
|
10 | 10 |
.. include:: example_header.inc |
11 | 11 |
|
... | ... |
@@ -34,7 +34,7 @@ Snapshot the installation |
34 | 34 |
|
35 | 35 |
.. code-block:: bash |
36 | 36 |
|
37 |
- docker ps -a # grab the container id (this will be the last one in the list) |
|
37 |
+ docker ps -a # grab the container id (this will be the first one in the list) |
|
38 | 38 |
docker commit <container_id> <your username>/redis |
39 | 39 |
|
40 | 40 |
Run the service |
... | ... |
@@ -4,8 +4,8 @@ |
4 | 4 |
|
5 | 5 |
.. _running_ssh_service: |
6 | 6 |
|
7 |
-Create an ssh daemon service |
|
8 |
-============================ |
|
7 |
+SSH Daemon Service |
|
8 |
+================== |
|
9 | 9 |
|
10 | 10 |
.. include:: example_header.inc |
11 | 11 |
|
... | ... |
@@ -20,8 +20,7 @@ minutes and not entirely smooth, but gives you a good idea. |
20 | 20 |
<div style="margin-top:10px;"> |
21 | 21 |
<iframe width="800" height="400" src="http://ascii.io/a/2637/raw" frameborder="0"></iframe> |
22 | 22 |
</div> |
23 |
- |
|
24 |
- |
|
23 |
+ |
|
25 | 24 |
You can also get this sshd container by using |
26 | 25 |
:: |
27 | 26 |
|
... | ... |
@@ -30,3 +29,49 @@ You can also get this sshd container by using |
30 | 30 |
|
31 | 31 |
The password is 'screencast' |
32 | 32 |
|
33 |
+**Video's Transcription:** |
|
34 |
+ |
|
35 |
+.. code-block:: bash |
|
36 |
+ |
|
37 |
+ # Hello! We are going to try and install openssh on a container and run it as a servic |
|
38 |
+ # let's pull base to get a base ubuntu image. |
|
39 |
+ $ docker pull base |
|
40 |
+ # I had it so it was quick |
|
41 |
+ # now let's connect using -i for interactive and with -t for terminal |
|
42 |
+ # we execute /bin/bash to get a prompt. |
|
43 |
+ $ docker run -i -t base /bin/bash |
|
44 |
+ # now let's commit it |
|
45 |
+ # which container was it? |
|
46 |
+ $ docker ps -a |more |
|
47 |
+ $ docker commit a30a3a2f2b130749995f5902f079dc6ad31ea0621fac595128ec59c6da07feea dhrp/sshd |
|
48 |
+ # I gave the name dhrp/sshd for the container |
|
49 |
+ # now we can run it again |
|
50 |
+ $ docker run -d dhrp/sshd /usr/sbin/sshd -D # D for daemon mode |
|
51 |
+ # is it running? |
|
52 |
+ $ docker ps |
|
53 |
+ # yes! |
|
54 |
+ # let's stop it |
|
55 |
+ $ docker stop 0ebf7cec294755399d063f4b1627980d4cbff7d999f0bc82b59c300f8536a562 |
|
56 |
+ $ docker ps |
|
57 |
+ # and reconnect, but now open a port to it |
|
58 |
+ $ docker run -d -p 22 dhrp/sshd /usr/sbin/sshd -D |
|
59 |
+ $ docker port b2b407cf22cf8e7fa3736fa8852713571074536b1d31def3fdfcd9fa4fd8c8c5 22 |
|
60 |
+ # it has now given us a port to connect to |
|
61 |
+ # we have to connect using a public ip of our host |
|
62 |
+ $ hostname |
|
63 |
+ $ ifconfig |
|
64 |
+ $ ssh root@192.168.33.10 -p 49153 |
|
65 |
+ # Ah! forgot to set root passwd |
|
66 |
+ $ docker commit b2b407cf22cf8e7fa3736fa8852713571074536b1d31def3fdfcd9fa4fd8c8c5 dhrp/sshd |
|
67 |
+ $ docker ps -a |
|
68 |
+ $ docker run -i -t dhrp/sshd /bin/bash |
|
69 |
+ $ passwd |
|
70 |
+ $ exit |
|
71 |
+ $ docker commit 9e863f0ca0af31c8b951048ba87641d67c382d08d655c2e4879c51410e0fedc1 dhrp/sshd |
|
72 |
+ $ docker run -d -p 22 dhrp/sshd /usr/sbin/sshd -D |
|
73 |
+ $ docker port a0aaa9558c90cf5c7782648df904a82365ebacce523e4acc085ac1213bfe2206 22 |
|
74 |
+ $ ifconfig |
|
75 |
+ $ ssh root@192.168.33.10 -p 49154 |
|
76 |
+ # Thanks for watching, Thatcher thatcher@dotcloud.com |
|
77 |
+ |
|
78 |
+ |
... | ... |
@@ -7,8 +7,8 @@ |
7 | 7 |
Introduction |
8 | 8 |
============ |
9 | 9 |
|
10 |
-Docker - The Linux container runtime |
|
10 |
+Docker -- The Linux container runtime |
|
11 |
+------------------------------------- |
|
11 | 12 |
|
12 | 13 |
Docker complements LXC with a high-level API which operates at the process level. It runs unix processes with strong guarantees of isolation and repeatability across servers. |
13 | 14 |
|
... | ... |
@@ -67,3 +67,21 @@ To start on system boot: |
67 | 67 |
:: |
68 | 68 |
|
69 | 69 |
sudo systemctl enable docker |
70 |
+ |
|
71 |
+Network Configuration |
|
72 |
+--------------------- |
|
73 |
+ |
|
74 |
+IPv4 packet forwarding is disabled by default on Arch, so internet access from inside |
|
75 |
+the container may not work. |
|
76 |
+ |
|
77 |
+To enable the forwarding, run as root on the host system: |
|
78 |
+ |
|
79 |
+:: |
|
80 |
+ |
|
81 |
+ sysctl net.ipv4.ip_forward=1 |
|
82 |
+ |
|
83 |
+And, to make it persistent across reboots, enable it on the host's **/etc/sysctl.conf**: |
|
84 |
+ |
|
85 |
+:: |
|
86 |
+ |
|
87 |
+ net.ipv4.ip_forward=1 |
... | ... |
@@ -125,8 +125,14 @@ curl was installed within the image. |
125 | 125 |
.. note:: |
126 | 126 |
The path must include the file name. |
127 | 127 |
|
128 |
-.. note:: |
|
129 |
- This instruction has temporarily disabled |
|
128 |
+2.8 ADD |
|
129 |
+------- |
|
130 |
+ |
|
131 |
+ ``ADD <src> <dest>`` |
|
132 |
+ |
|
133 |
+The `ADD` instruction will insert the files from the `<src>` path of the context into `<dest>` path |
|
134 |
+of the container. |
|
135 |
+The context must be set in order to use this instruction. (see examples) |
|
130 | 136 |
|
131 | 137 |
3. Dockerfile Examples |
132 | 138 |
====================== |
... | ... |
@@ -4,8 +4,8 @@ |
4 | 4 |
|
5 | 5 |
.. _working_with_the_repository: |
6 | 6 |
|
7 |
-Working with the repository |
|
8 |
-============================ |
|
7 |
+Working with the Repository |
|
8 |
+=========================== |
|
9 | 9 |
|
10 | 10 |
|
11 | 11 |
Top-level repositories and user repositories |
... | ... |
@@ -14,9 +14,9 @@ Top-level repositories and user repositories |
14 | 14 |
Generally, there are two types of repositories: Top-level repositories which are controlled by the people behind |
15 | 15 |
Docker, and user repositories. |
16 | 16 |
|
17 |
-* Top-level repositories can easily be recognized by not having a / (slash) in their name. These repositories can |
|
17 |
+* Top-level repositories can easily be recognized by not having a ``/`` (slash) in their name. These repositories can |
|
18 | 18 |
generally be trusted. |
19 |
-* User repositories always come in the form of <username>/<repo_name>. This is what your published images will look like. |
|
19 |
+* User repositories always come in the form of ``<username>/<repo_name>``. This is what your published images will look like. |
|
20 | 20 |
* User images are not checked, it is therefore up to you whether or not you trust the creator of this image. |
21 | 21 |
|
22 | 22 |
|
... | ... |
@@ -270,7 +270,7 @@ |
270 | 270 |
<li>Filesystem isolation: each process container runs in a completely separate root filesystem.</li> |
271 | 271 |
<li>Resource isolation: system resources like cpu and memory can be allocated differently to each process container, using cgroups.</li> |
272 | 272 |
<li>Network isolation: each process container runs in its own network namespace, with a virtual interface and IP address of its own.</li> |
273 |
- <li>Copy-on-write: root filesystems are created using copy-on-write, which makes deployment extremeley fast, memory-cheap and disk-cheap.</li> |
|
273 |
+ <li>Copy-on-write: root filesystems are created using copy-on-write, which makes deployment extremely fast, memory-cheap and disk-cheap.</li> |
|
274 | 274 |
<li>Logging: the standard streams (stdout/stderr/stdin) of each process container is collected and logged for real-time or batch retrieval.</li> |
275 | 275 |
<li>Change management: changes to a container's filesystem can be committed into a new image and re-used to create more containers. No templating or manual configuration required.</li> |
276 | 276 |
<li>Interactive shell: docker can allocate a pseudo-tty and attach to the standard input of any container, for example to run a throwaway interactive shell.</li> |
... | ... |
@@ -107,6 +107,7 @@ func (graph *Graph) Create(layerData Archive, container *Container, comment, aut |
107 | 107 |
DockerVersion: VERSION, |
108 | 108 |
Author: author, |
109 | 109 |
Config: config, |
110 |
+ Architecture: "x86_64", |
|
110 | 111 |
} |
111 | 112 |
if container != nil { |
112 | 113 |
img.Parent = container.Image |
... | ... |
@@ -165,7 +166,8 @@ func (graph *Graph) TempLayerArchive(id string, compression Compression, output |
165 | 165 |
if err != nil { |
166 | 166 |
return nil, err |
167 | 167 |
} |
168 |
- return NewTempArchive(utils.ProgressReader(ioutil.NopCloser(archive), 0, output, "Buffering to disk %v/%v (%v)", false), tmp.Root) |
|
168 |
+ sf := utils.NewStreamFormatter(false) |
|
169 |
+ return NewTempArchive(utils.ProgressReader(ioutil.NopCloser(archive), 0, output, sf.FormatProgress("Buffering to disk", "%v/%v (%v)"), sf), tmp.Root) |
|
169 | 170 |
} |
170 | 171 |
|
171 | 172 |
// Mktemp creates a temporary sub-directory inside the graph's filesystem. |
... | ... |
@@ -1,23 +1,31 @@ |
1 | 1 |
# This will build a container capable of producing an official binary build of docker and |
2 | 2 |
# uploading it to S3 |
3 |
+from ubuntu:12.04 |
|
3 | 4 |
maintainer Solomon Hykes <solomon@dotcloud.com> |
4 |
-from ubuntu:12.10 |
|
5 |
+# Workaround the upstart issue |
|
6 |
+run dpkg-divert --local --rename --add /sbin/initctl |
|
7 |
+run ln -s /bin/true /sbin/initctl |
|
8 |
+# Enable universe and gophers PPA |
|
9 |
+run DEBIAN_FRONTEND=noninteractive apt-get install -y -q python-software-properties |
|
10 |
+run add-apt-repository "deb http://archive.ubuntu.com/ubuntu $(lsb_release -sc) universe" |
|
11 |
+run add-apt-repository -y ppa:gophers/go/ubuntu |
|
5 | 12 |
run apt-get update |
13 |
+# Packages required to checkout, build and upload docker |
|
6 | 14 |
run DEBIAN_FRONTEND=noninteractive apt-get install -y -q s3cmd |
7 | 15 |
run DEBIAN_FRONTEND=noninteractive apt-get install -y -q curl |
8 |
-# Packages required to checkout and build docker |
|
9 | 16 |
run curl -s -o /go.tar.gz https://go.googlecode.com/files/go1.1.linux-amd64.tar.gz |
10 | 17 |
run tar -C /usr/local -xzf /go.tar.gz |
11 |
-run echo "export PATH=$PATH:/usr/local/go/bin" > /.bashrc |
|
12 |
-run echo "export PATH=$PATH:/usr/local/go/bin" > /.bash_profile |
|
18 |
+run echo "export PATH=/usr/local/go/bin:$PATH" > /.bashrc |
|
19 |
+run echo "export PATH=/usr/local/go/bin:$PATH" > /.bash_profile |
|
13 | 20 |
run DEBIAN_FRONTEND=noninteractive apt-get install -y -q git |
14 | 21 |
run DEBIAN_FRONTEND=noninteractive apt-get install -y -q build-essential |
15 | 22 |
# Packages required to build an ubuntu package |
23 |
+run DEBIAN_FRONTEND=noninteractive apt-get install -y -q golang-stable |
|
16 | 24 |
run DEBIAN_FRONTEND=noninteractive apt-get install -y -q debhelper |
17 | 25 |
run DEBIAN_FRONTEND=noninteractive apt-get install -y -q autotools-dev |
18 |
-copy fake_initctl /usr/local/bin/initctl |
|
19 | 26 |
run apt-get install -y -q devscripts |
20 |
-add . /src |
|
27 |
+# Copy dockerbuilder files into the container |
|
28 |
+add . /src |
|
21 | 29 |
run cp /src/dockerbuilder /usr/local/bin/ && chmod +x /usr/local/bin/dockerbuilder |
22 | 30 |
run cp /src/s3cfg /.s3cfg |
23 | 31 |
cmd ["dockerbuilder"] |
... | ... |
@@ -2,7 +2,7 @@ |
2 | 2 |
set -x |
3 | 3 |
set -e |
4 | 4 |
|
5 |
-export PATH=$PATH:/usr/local/go/bin |
|
5 |
+export PATH=/usr/local/go/bin:$PATH |
|
6 | 6 |
|
7 | 7 |
PACKAGE=github.com/dotcloud/docker |
8 | 8 |
|
... | ... |
@@ -36,5 +36,6 @@ else |
36 | 36 |
fi |
37 | 37 |
|
38 | 38 |
if [ -z "$NO_UBUNTU" ]; then |
39 |
+ export PATH=`echo $PATH | sed 's#/usr/local/go/bin:##g'` |
|
39 | 40 |
(cd packaging/ubuntu && make ubuntu) |
40 | 41 |
fi |
0 | 2 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,5 @@ |
0 |
+# Docker project infrastructure |
|
1 |
+ |
|
2 |
+This directory holds all information about the technical infrastructure of the docker project; servers, dns, email, and all the corresponding tools and configuration. |
|
3 |
+ |
|
4 |
+Obviously credentials should not be stored in this repo, but how to obtain and use them should be documented here. |
... | ... |
@@ -1,3 +1,14 @@ |
1 |
+lxc-docker (0.3.4-1) UNRELEASED; urgency=low |
|
2 |
+ - Builder: 'docker build' builds a container, layer by layer, from a source repository containing a Dockerfile |
|
3 |
+ - Builder: 'docker build -t FOO' applies the tag FOO to the newly built container. |
|
4 |
+ - Runtime: interactive TTYs correctly handle window resize |
|
5 |
+ - Runtime: fix how configuration is merged between layers |
|
6 |
+ - Remote API: split stdout and stderr on 'docker run' |
|
7 |
+ - Remote API: optionally listen on a different IP and port (use at your own risk) |
|
8 |
+ - Documentation: improved install instructions. |
|
9 |
+ |
|
10 |
+ -- dotCloud <ops@dotcloud.com> Thu, 30 May 2013 00:00:00 -0700 |
|
11 |
+ |
|
1 | 12 |
lxc-docker (0.3.2-1) UNRELEASED; urgency=low |
2 | 13 |
- Runtime: Store the actual archive on commit |
3 | 14 |
- Registry: Improve the checksum process |
... | ... |
@@ -1,3 +1,15 @@ |
1 |
+lxc-docker (0.3.4-1) precise; urgency=low |
|
2 |
+ - Builder: 'docker build' builds a container, layer by layer, from a source repository containing a Dockerfile |
|
3 |
+ - Builder: 'docker build -t FOO' applies the tag FOO to the newly built container. |
|
4 |
+ - Runtime: interactive TTYs correctly handle window resize |
|
5 |
+ - Runtime: fix how configuration is merged between layers |
|
6 |
+ - Remote API: split stdout and stderr on 'docker run' |
|
7 |
+ - Remote API: optionally listen on a different IP and port (use at your own risk) |
|
8 |
+ - Documentation: improved install instructions. |
|
9 |
+ |
|
10 |
+ -- dotCloud <ops@dotcloud.com> Thu, 30 May 2013 00:00:00 -0700 |
|
11 |
+ |
|
12 |
+ |
|
1 | 13 |
lxc-docker (0.3.3-1) precise; urgency=low |
2 | 14 |
- Registry: Fix push regression |
3 | 15 |
- Various bugfixes |
... | ... |
@@ -330,6 +330,9 @@ func (r *Registry) PushImageJsonIndex(remote string, imgList []*ImgData, validat |
330 | 330 |
if validate { |
331 | 331 |
suffix = "images" |
332 | 332 |
} |
333 |
+ |
|
334 |
+ utils.Debugf("Image list pushed to index:\n%s\n", imgListJson) |
|
335 |
+ |
|
333 | 336 |
req, err := http.NewRequest("PUT", auth.IndexServerAddress()+"/repositories/"+remote+"/"+suffix, bytes.NewReader(imgListJson)) |
334 | 337 |
if err != nil { |
335 | 338 |
return nil, err |
... | ... |
@@ -2,13 +2,15 @@ package docker |
2 | 2 |
|
3 | 3 |
import ( |
4 | 4 |
"fmt" |
5 |
- "github.com/dotcloud/docker/registry" |
|
6 | 5 |
"github.com/dotcloud/docker/utils" |
7 | 6 |
"io" |
8 | 7 |
"io/ioutil" |
8 |
+ "log" |
|
9 | 9 |
"net" |
10 | 10 |
"os" |
11 | 11 |
"os/user" |
12 |
+ "strconv" |
|
13 |
+ "strings" |
|
12 | 14 |
"sync" |
13 | 15 |
"testing" |
14 | 16 |
"time" |
... | ... |
@@ -63,11 +65,10 @@ func init() { |
63 | 63 |
|
64 | 64 |
// Create the "Server" |
65 | 65 |
srv := &Server{ |
66 |
- runtime: runtime, |
|
67 |
- registry: registry.NewRegistry(runtime.root), |
|
66 |
+ runtime: runtime, |
|
68 | 67 |
} |
69 | 68 |
// Retrieve the Image |
70 |
- if err := srv.ImagePull(unitTestImageName, "", "", os.Stdout, false); err != nil { |
|
69 |
+ if err := srv.ImagePull(unitTestImageName, "", "", os.Stdout, utils.NewStreamFormatter(false)); err != nil { |
|
71 | 70 |
panic(err) |
72 | 71 |
} |
73 | 72 |
} |
... | ... |
@@ -279,24 +280,50 @@ func TestGet(t *testing.T) { |
279 | 279 |
|
280 | 280 |
} |
281 | 281 |
|
282 |
-// Run a container with a TCP port allocated, and test that it can receive connections on localhost |
|
283 |
-func TestAllocatePortLocalhost(t *testing.T) { |
|
284 |
- runtime, err := newTestRuntime() |
|
285 |
- if err != nil { |
|
286 |
- t.Fatal(err) |
|
287 |
- } |
|
282 |
+func findAvailalblePort(runtime *Runtime, port int) (*Container, error) { |
|
283 |
+ strPort := strconv.Itoa(port) |
|
288 | 284 |
container, err := NewBuilder(runtime).Create(&Config{ |
289 | 285 |
Image: GetTestImage(runtime).Id, |
290 |
- Cmd: []string{"sh", "-c", "echo well hello there | nc -l -p 5555"}, |
|
291 |
- PortSpecs: []string{"5555"}, |
|
286 |
+ Cmd: []string{"sh", "-c", "echo well hello there | nc -l -p " + strPort}, |
|
287 |
+ PortSpecs: []string{strPort}, |
|
292 | 288 |
}, |
293 | 289 |
) |
294 | 290 |
if err != nil { |
295 |
- t.Fatal(err) |
|
291 |
+ return nil, err |
|
296 | 292 |
} |
297 | 293 |
if err := container.Start(); err != nil { |
294 |
+ if strings.Contains(err.Error(), "address already in use") { |
|
295 |
+ return nil, nil |
|
296 |
+ } |
|
297 |
+ return nil, err |
|
298 |
+ } |
|
299 |
+ return container, nil |
|
300 |
+} |
|
301 |
+ |
|
302 |
+// Run a container with a TCP port allocated, and test that it can receive connections on localhost |
|
303 |
+func TestAllocatePortLocalhost(t *testing.T) { |
|
304 |
+ runtime, err := newTestRuntime() |
|
305 |
+ if err != nil { |
|
298 | 306 |
t.Fatal(err) |
299 | 307 |
} |
308 |
+ port := 5554 |
|
309 |
+ |
|
310 |
+ var container *Container |
|
311 |
+ for { |
|
312 |
+ port += 1 |
|
313 |
+ log.Println("Trying port", port) |
|
314 |
+ t.Log("Trying port", port) |
|
315 |
+ container, err = findAvailalblePort(runtime, port) |
|
316 |
+ if container != nil { |
|
317 |
+ break |
|
318 |
+ } |
|
319 |
+ if err != nil { |
|
320 |
+ t.Fatal(err) |
|
321 |
+ } |
|
322 |
+ log.Println("Port", port, "already in use") |
|
323 |
+ t.Log("Port", port, "already in use") |
|
324 |
+ } |
|
325 |
+ |
|
300 | 326 |
defer container.Kill() |
301 | 327 |
|
302 | 328 |
setTimeout(t, "Waiting for the container to be started timed out", 2*time.Second, func() { |
... | ... |
@@ -310,7 +337,7 @@ func TestAllocatePortLocalhost(t *testing.T) { |
310 | 310 |
|
311 | 311 |
conn, err := net.Dial("tcp", |
312 | 312 |
fmt.Sprintf( |
313 |
- "localhost:%s", container.NetworkSettings.PortMapping["5555"], |
|
313 |
+ "localhost:%s", container.NetworkSettings.PortMapping[strconv.Itoa(port)], |
|
314 | 314 |
), |
315 | 315 |
) |
316 | 316 |
if err != nil { |
... | ... |
@@ -50,7 +50,8 @@ func (srv *Server) ContainerExport(name string, out io.Writer) error { |
50 | 50 |
} |
51 | 51 |
|
52 | 52 |
func (srv *Server) ImagesSearch(term string) ([]ApiSearch, error) { |
53 |
- results, err := srv.registry.SearchRepositories(term) |
|
53 |
+ |
|
54 |
+ results, err := registry.NewRegistry(srv.runtime.root).SearchRepositories(term) |
|
54 | 55 |
if err != nil { |
55 | 56 |
return nil, err |
56 | 57 |
} |
... | ... |
@@ -68,7 +69,7 @@ func (srv *Server) ImagesSearch(term string) ([]ApiSearch, error) { |
68 | 68 |
return outs, nil |
69 | 69 |
} |
70 | 70 |
|
71 |
-func (srv *Server) ImageInsert(name, url, path string, out io.Writer) (string, error) { |
|
71 |
+func (srv *Server) ImageInsert(name, url, path string, out io.Writer, sf *utils.StreamFormatter) (string, error) { |
|
72 | 72 |
out = utils.NewWriteFlusher(out) |
73 | 73 |
img, err := srv.runtime.repositories.LookupImage(name) |
74 | 74 |
if err != nil { |
... | ... |
@@ -92,7 +93,7 @@ func (srv *Server) ImageInsert(name, url, path string, out io.Writer) (string, e |
92 | 92 |
return "", err |
93 | 93 |
} |
94 | 94 |
|
95 |
- if err := c.Inject(utils.ProgressReader(file.Body, int(file.ContentLength), out, "Downloading %v/%v (%v)\r", false), path); err != nil { |
|
95 |
+ if err := c.Inject(utils.ProgressReader(file.Body, int(file.ContentLength), out, sf.FormatProgress("Downloading", "%v/%v (%v)"), sf), path); err != nil { |
|
96 | 96 |
return "", err |
97 | 97 |
} |
98 | 98 |
// FIXME: Handle custom repo, tag comment, author |
... | ... |
@@ -100,7 +101,7 @@ func (srv *Server) ImageInsert(name, url, path string, out io.Writer) (string, e |
100 | 100 |
if err != nil { |
101 | 101 |
return "", err |
102 | 102 |
} |
103 |
- fmt.Fprintf(out, "%s\n", img.Id) |
|
103 |
+ out.Write(sf.FormatStatus(img.Id)) |
|
104 | 104 |
return img.ShortId(), nil |
105 | 105 |
} |
106 | 106 |
|
... | ... |
@@ -292,8 +293,8 @@ func (srv *Server) ContainerTag(name, repo, tag string, force bool) error { |
292 | 292 |
return nil |
293 | 293 |
} |
294 | 294 |
|
295 |
-func (srv *Server) pullImage(out io.Writer, imgId, registry string, token []string, json bool) error { |
|
296 |
- history, err := srv.registry.GetRemoteHistory(imgId, registry, token) |
|
295 |
+func (srv *Server) pullImage(r *registry.Registry, out io.Writer, imgId, endpoint string, token []string, sf *utils.StreamFormatter) error { |
|
296 |
+ history, err := r.GetRemoteHistory(imgId, endpoint, token) |
|
297 | 297 |
if err != nil { |
298 | 298 |
return err |
299 | 299 |
} |
... | ... |
@@ -302,8 +303,8 @@ func (srv *Server) pullImage(out io.Writer, imgId, registry string, token []stri |
302 | 302 |
// FIXME: Launch the getRemoteImage() in goroutines |
303 | 303 |
for _, id := range history { |
304 | 304 |
if !srv.runtime.graph.Exists(id) { |
305 |
- fmt.Fprintf(out, utils.FormatStatus("Pulling %s metadata", json), id) |
|
306 |
- imgJson, err := srv.registry.GetRemoteImageJson(id, registry, token) |
|
305 |
+ out.Write(sf.FormatStatus("Pulling %s metadata", id)) |
|
306 |
+ imgJson, err := r.GetRemoteImageJson(id, endpoint, token) |
|
307 | 307 |
if err != nil { |
308 | 308 |
// FIXME: Keep goging in case of error? |
309 | 309 |
return err |
... | ... |
@@ -314,12 +315,12 @@ func (srv *Server) pullImage(out io.Writer, imgId, registry string, token []stri |
314 | 314 |
} |
315 | 315 |
|
316 | 316 |
// Get the layer |
317 |
- fmt.Fprintf(out, utils.FormatStatus("Pulling %s fs layer", json), id) |
|
318 |
- layer, contentLength, err := srv.registry.GetRemoteImageLayer(img.Id, registry, token) |
|
317 |
+ out.Write(sf.FormatStatus("Pulling %s fs layer", id)) |
|
318 |
+ layer, contentLength, err := r.GetRemoteImageLayer(img.Id, endpoint, token) |
|
319 | 319 |
if err != nil { |
320 | 320 |
return err |
321 | 321 |
} |
322 |
- if err := srv.runtime.graph.Register(utils.ProgressReader(layer, contentLength, out, utils.FormatProgress("%v/%v (%v)", json), json), false, img); err != nil { |
|
322 |
+ if err := srv.runtime.graph.Register(utils.ProgressReader(layer, contentLength, out, sf.FormatProgress("Downloading", "%v/%v (%v)"), sf), false, img); err != nil { |
|
323 | 323 |
return err |
324 | 324 |
} |
325 | 325 |
} |
... | ... |
@@ -327,9 +328,9 @@ func (srv *Server) pullImage(out io.Writer, imgId, registry string, token []stri |
327 | 327 |
return nil |
328 | 328 |
} |
329 | 329 |
|
330 |
-func (srv *Server) pullRepository(out io.Writer, remote, askedTag string, json bool) error { |
|
331 |
- fmt.Fprintf(out, utils.FormatStatus("Pulling repository %s from %s", json), remote, auth.IndexServerAddress()) |
|
332 |
- repoData, err := srv.registry.GetRepositoryData(remote) |
|
330 |
+func (srv *Server) pullRepository(r *registry.Registry, out io.Writer, remote, askedTag string, sf *utils.StreamFormatter) error { |
|
331 |
+ out.Write(sf.FormatStatus("Pulling repository %s from %s", remote, auth.IndexServerAddress())) |
|
332 |
+ repoData, err := r.GetRepositoryData(remote) |
|
333 | 333 |
if err != nil { |
334 | 334 |
return err |
335 | 335 |
} |
... | ... |
@@ -341,7 +342,7 @@ func (srv *Server) pullRepository(out io.Writer, remote, askedTag string, json b |
341 | 341 |
} |
342 | 342 |
|
343 | 343 |
utils.Debugf("Retrieving the tag list") |
344 |
- tagsList, err := srv.registry.GetRemoteTags(repoData.Endpoints, remote, repoData.Tokens) |
|
344 |
+ tagsList, err := r.GetRemoteTags(repoData.Endpoints, remote, repoData.Tokens) |
|
345 | 345 |
if err != nil { |
346 | 346 |
return err |
347 | 347 |
} |
... | ... |
@@ -365,11 +366,11 @@ func (srv *Server) pullRepository(out io.Writer, remote, askedTag string, json b |
365 | 365 |
utils.Debugf("(%s) does not match %s (id: %s), skipping", img.Tag, askedTag, img.Id) |
366 | 366 |
continue |
367 | 367 |
} |
368 |
- fmt.Fprintf(out, utils.FormatStatus("Pulling image %s (%s) from %s", json), img.Id, img.Tag, remote) |
|
368 |
+ out.Write(sf.FormatStatus("Pulling image %s (%s) from %s", img.Id, img.Tag, remote)) |
|
369 | 369 |
success := false |
370 | 370 |
for _, ep := range repoData.Endpoints { |
371 |
- if err := srv.pullImage(out, img.Id, "https://"+ep+"/v1", repoData.Tokens, json); err != nil { |
|
372 |
- fmt.Fprintf(out, utils.FormatStatus("Error while retrieving image for tag: %s (%s); checking next endpoint\n", json), askedTag, err) |
|
371 |
+ if err := srv.pullImage(r, out, img.Id, "https://"+ep+"/v1", repoData.Tokens, sf); err != nil { |
|
372 |
+ out.Write(sf.FormatStatus("Error while retrieving image for tag: %s (%s); checking next endpoint", askedTag, err)) |
|
373 | 373 |
continue |
374 | 374 |
} |
375 | 375 |
success = true |
... | ... |
@@ -394,16 +395,17 @@ func (srv *Server) pullRepository(out io.Writer, remote, askedTag string, json b |
394 | 394 |
return nil |
395 | 395 |
} |
396 | 396 |
|
397 |
-func (srv *Server) ImagePull(name, tag, registry string, out io.Writer, json bool) error { |
|
397 |
+func (srv *Server) ImagePull(name, tag, endpoint string, out io.Writer, sf *utils.StreamFormatter) error { |
|
398 |
+ r := registry.NewRegistry(srv.runtime.root) |
|
398 | 399 |
out = utils.NewWriteFlusher(out) |
399 |
- if registry != "" { |
|
400 |
- if err := srv.pullImage(out, name, registry, nil, json); err != nil { |
|
400 |
+ if endpoint != "" { |
|
401 |
+ if err := srv.pullImage(r, out, name, endpoint, nil, sf); err != nil { |
|
401 | 402 |
return err |
402 | 403 |
} |
403 | 404 |
return nil |
404 | 405 |
} |
405 | 406 |
|
406 |
- if err := srv.pullRepository(out, name, tag, json); err != nil { |
|
407 |
+ if err := srv.pullRepository(r, out, name, tag, sf); err != nil { |
|
407 | 408 |
return err |
408 | 409 |
} |
409 | 410 |
|
... | ... |
@@ -476,53 +478,52 @@ func (srv *Server) getImageList(localRepo map[string]string) ([]*registry.ImgDat |
476 | 476 |
return imgList, nil |
477 | 477 |
} |
478 | 478 |
|
479 |
-func (srv *Server) pushRepository(out io.Writer, name string, localRepo map[string]string) error { |
|
479 |
+func (srv *Server) pushRepository(r *registry.Registry, out io.Writer, name string, localRepo map[string]string, sf *utils.StreamFormatter) error { |
|
480 | 480 |
out = utils.NewWriteFlusher(out) |
481 |
- fmt.Fprintf(out, "Processing checksums\n") |
|
481 |
+ out.Write(sf.FormatStatus("Processing checksums")) |
|
482 | 482 |
imgList, err := srv.getImageList(localRepo) |
483 | 483 |
if err != nil { |
484 | 484 |
return err |
485 | 485 |
} |
486 |
- fmt.Fprintf(out, "Sending image list\n") |
|
486 |
+ out.Write(sf.FormatStatus("Sending image list")) |
|
487 | 487 |
|
488 |
- repoData, err := srv.registry.PushImageJsonIndex(name, imgList, false) |
|
488 |
+ repoData, err := r.PushImageJsonIndex(name, imgList, false) |
|
489 | 489 |
if err != nil { |
490 | 490 |
return err |
491 | 491 |
} |
492 | 492 |
|
493 |
- // FIXME: Send only needed images |
|
494 | 493 |
for _, ep := range repoData.Endpoints { |
495 |
- fmt.Fprintf(out, "Pushing repository %s to %s (%d tags)\r\n", name, ep, len(localRepo)) |
|
494 |
+ out.Write(sf.FormatStatus("Pushing repository %s to %s (%d tags)", name, ep, len(localRepo))) |
|
496 | 495 |
// For each image within the repo, push them |
497 | 496 |
for _, elem := range imgList { |
498 | 497 |
if _, exists := repoData.ImgList[elem.Id]; exists { |
499 |
- fmt.Fprintf(out, "Image %s already on registry, skipping\n", name) |
|
498 |
+ out.Write(sf.FormatStatus("Image %s already on registry, skipping", name)) |
|
500 | 499 |
continue |
501 | 500 |
} |
502 |
- if err := srv.pushImage(out, name, elem.Id, ep, repoData.Tokens); err != nil { |
|
501 |
+ if err := srv.pushImage(r, out, name, elem.Id, ep, repoData.Tokens, sf); err != nil { |
|
503 | 502 |
// FIXME: Continue on error? |
504 | 503 |
return err |
505 | 504 |
} |
506 |
- fmt.Fprintf(out, "Pushing tags for rev [%s] on {%s}\n", elem.Id, ep+"/users/"+name+"/"+elem.Tag) |
|
507 |
- if err := srv.registry.PushRegistryTag(name, elem.Id, elem.Tag, ep, repoData.Tokens); err != nil { |
|
505 |
+ out.Write(sf.FormatStatus("Pushing tags for rev [%s] on {%s}", elem.Id, ep+"/users/"+name+"/"+elem.Tag)) |
|
506 |
+ if err := r.PushRegistryTag(name, elem.Id, elem.Tag, ep, repoData.Tokens); err != nil { |
|
508 | 507 |
return err |
509 | 508 |
} |
510 | 509 |
} |
511 | 510 |
} |
512 | 511 |
|
513 |
- if _, err := srv.registry.PushImageJsonIndex(name, imgList, true); err != nil { |
|
512 |
+ if _, err := r.PushImageJsonIndex(name, imgList, true); err != nil { |
|
514 | 513 |
return err |
515 | 514 |
} |
516 | 515 |
return nil |
517 | 516 |
} |
518 | 517 |
|
519 |
-func (srv *Server) pushImage(out io.Writer, remote, imgId, ep string, token []string) error { |
|
518 |
+func (srv *Server) pushImage(r *registry.Registry, out io.Writer, remote, imgId, ep string, token []string, sf *utils.StreamFormatter) error { |
|
520 | 519 |
out = utils.NewWriteFlusher(out) |
521 | 520 |
jsonRaw, err := ioutil.ReadFile(path.Join(srv.runtime.graph.Root, imgId, "json")) |
522 | 521 |
if err != nil { |
523 | 522 |
return fmt.Errorf("Error while retreiving the path for {%s}: %s", imgId, err) |
524 | 523 |
} |
525 |
- fmt.Fprintf(out, "Pushing %s\r\n", imgId) |
|
524 |
+ out.Write(sf.FormatStatus("Pushing %s", imgId)) |
|
526 | 525 |
|
527 | 526 |
// Make sure we have the image's checksum |
528 | 527 |
checksum, err := srv.getChecksum(imgId) |
... | ... |
@@ -535,9 +536,9 @@ func (srv *Server) pushImage(out io.Writer, remote, imgId, ep string, token []st |
535 | 535 |
} |
536 | 536 |
|
537 | 537 |
// Send the json |
538 |
- if err := srv.registry.PushImageJsonRegistry(imgData, jsonRaw, ep, token); err != nil { |
|
538 |
+ if err := r.PushImageJsonRegistry(imgData, jsonRaw, ep, token); err != nil { |
|
539 | 539 |
if err == registry.ErrAlreadyExists { |
540 |
- fmt.Fprintf(out, "Image %s already uploaded ; skipping\n", imgData.Id) |
|
540 |
+ out.Write(sf.FormatStatus("Image %s already uploaded ; skipping", imgData.Id)) |
|
541 | 541 |
return nil |
542 | 542 |
} |
543 | 543 |
return err |
... | ... |
@@ -570,20 +571,22 @@ func (srv *Server) pushImage(out io.Writer, remote, imgId, ep string, token []st |
570 | 570 |
} |
571 | 571 |
|
572 | 572 |
// Send the layer |
573 |
- if err := srv.registry.PushImageLayerRegistry(imgData.Id, utils.ProgressReader(layerData, int(layerData.Size), out, "", false), ep, token); err != nil { |
|
573 |
+ if err := r.PushImageLayerRegistry(imgData.Id, utils.ProgressReader(layerData, int(layerData.Size), out, sf.FormatProgress("", "%v/%v (%v)"), sf), ep, token); err != nil { |
|
574 | 574 |
return err |
575 | 575 |
} |
576 | 576 |
return nil |
577 | 577 |
} |
578 | 578 |
|
579 |
-func (srv *Server) ImagePush(name, registry string, out io.Writer) error { |
|
579 |
+func (srv *Server) ImagePush(name, endpoint string, out io.Writer, sf *utils.StreamFormatter) error { |
|
580 | 580 |
out = utils.NewWriteFlusher(out) |
581 | 581 |
img, err := srv.runtime.graph.Get(name) |
582 |
+ r := registry.NewRegistry(srv.runtime.root) |
|
583 |
+ |
|
582 | 584 |
if err != nil { |
583 |
- fmt.Fprintf(out, "The push refers to a repository [%s] (len: %d)\n", name, len(srv.runtime.repositories.Repositories[name])) |
|
585 |
+ out.Write(sf.FormatStatus("The push refers to a repository [%s] (len: %d)", name, len(srv.runtime.repositories.Repositories[name]))) |
|
584 | 586 |
// If it fails, try to get the repository |
585 | 587 |
if localRepo, exists := srv.runtime.repositories.Repositories[name]; exists { |
586 |
- if err := srv.pushRepository(out, name, localRepo); err != nil { |
|
588 |
+ if err := srv.pushRepository(r, out, name, localRepo, sf); err != nil { |
|
587 | 589 |
return err |
588 | 590 |
} |
589 | 591 |
return nil |
... | ... |
@@ -591,14 +594,14 @@ func (srv *Server) ImagePush(name, registry string, out io.Writer) error { |
591 | 591 |
|
592 | 592 |
return err |
593 | 593 |
} |
594 |
- fmt.Fprintf(out, "The push refers to an image: [%s]\n", name) |
|
595 |
- if err := srv.pushImage(out, name, img.Id, registry, nil); err != nil { |
|
594 |
+ out.Write(sf.FormatStatus("The push refers to an image: [%s]", name)) |
|
595 |
+ if err := srv.pushImage(r, out, name, img.Id, endpoint, nil, sf); err != nil { |
|
596 | 596 |
return err |
597 | 597 |
} |
598 | 598 |
return nil |
599 | 599 |
} |
600 | 600 |
|
601 |
-func (srv *Server) ImageImport(src, repo, tag string, in io.Reader, out io.Writer) error { |
|
601 |
+func (srv *Server) ImageImport(src, repo, tag string, in io.Reader, out io.Writer, sf *utils.StreamFormatter) error { |
|
602 | 602 |
var archive io.Reader |
603 | 603 |
var resp *http.Response |
604 | 604 |
|
... | ... |
@@ -607,21 +610,21 @@ func (srv *Server) ImageImport(src, repo, tag string, in io.Reader, out io.Write |
607 | 607 |
} else { |
608 | 608 |
u, err := url.Parse(src) |
609 | 609 |
if err != nil { |
610 |
- fmt.Fprintf(out, "Error: %s\n", err) |
|
610 |
+ return err |
|
611 | 611 |
} |
612 | 612 |
if u.Scheme == "" { |
613 | 613 |
u.Scheme = "http" |
614 | 614 |
u.Host = src |
615 | 615 |
u.Path = "" |
616 | 616 |
} |
617 |
- fmt.Fprintf(out, "Downloading from %s\n", u) |
|
617 |
+ out.Write(sf.FormatStatus("Downloading from %s", u)) |
|
618 | 618 |
// Download with curl (pretty progress bar) |
619 | 619 |
// If curl is not available, fallback to http.Get() |
620 | 620 |
resp, err = utils.Download(u.String(), out) |
621 | 621 |
if err != nil { |
622 | 622 |
return err |
623 | 623 |
} |
624 |
- archive = utils.ProgressReader(resp.Body, int(resp.ContentLength), out, "Importing %v/%v (%v)\r", false) |
|
624 |
+ archive = utils.ProgressReader(resp.Body, int(resp.ContentLength), out, sf.FormatProgress("Importing", "%v/%v (%v)"), sf) |
|
625 | 625 |
} |
626 | 626 |
img, err := srv.runtime.graph.Create(archive, nil, "Imported from "+src, "", nil) |
627 | 627 |
if err != nil { |
... | ... |
@@ -633,7 +636,7 @@ func (srv *Server) ImageImport(src, repo, tag string, in io.Reader, out io.Write |
633 | 633 |
return err |
634 | 634 |
} |
635 | 635 |
} |
636 |
- fmt.Fprintf(out, "%s\n", img.ShortId()) |
|
636 |
+ out.Write(sf.FormatStatus(img.ShortId())) |
|
637 | 637 |
return nil |
638 | 638 |
} |
639 | 639 |
|
... | ... |
@@ -883,7 +886,6 @@ func (srv *Server) ContainerAttach(name string, logs, stream, stdin, stdout, std |
883 | 883 |
if container == nil { |
884 | 884 |
return fmt.Errorf("No such container: %s", name) |
885 | 885 |
} |
886 |
- |
|
887 | 886 |
//logs |
888 | 887 |
if logs { |
889 | 888 |
if stdout { |
... | ... |
@@ -909,6 +911,9 @@ func (srv *Server) ContainerAttach(name string, logs, stream, stdin, stdout, std |
909 | 909 |
if container.State.Ghost { |
910 | 910 |
return fmt.Errorf("Impossible to attach to a ghost container") |
911 | 911 |
} |
912 |
+ if !container.State.Running { |
|
913 |
+ return fmt.Errorf("Impossible to attach to a stopped container, start it first") |
|
914 |
+ } |
|
912 | 915 |
|
913 | 916 |
var ( |
914 | 917 |
cStdin io.ReadCloser |
... | ... |
@@ -967,14 +972,12 @@ func NewServer(autoRestart bool) (*Server, error) { |
967 | 967 |
return nil, err |
968 | 968 |
} |
969 | 969 |
srv := &Server{ |
970 |
- runtime: runtime, |
|
971 |
- registry: registry.NewRegistry(runtime.root), |
|
970 |
+ runtime: runtime, |
|
972 | 971 |
} |
973 | 972 |
runtime.srv = srv |
974 | 973 |
return srv, nil |
975 | 974 |
} |
976 | 975 |
|
977 | 976 |
type Server struct { |
978 |
- runtime *Runtime |
|
979 |
- registry *registry.Registry |
|
977 |
+ runtime *Runtime |
|
980 | 978 |
} |
... | ... |
@@ -4,6 +4,7 @@ import ( |
4 | 4 |
"bytes" |
5 | 5 |
"crypto/sha256" |
6 | 6 |
"encoding/hex" |
7 |
+ "encoding/json" |
|
7 | 8 |
"errors" |
8 | 9 |
"fmt" |
9 | 10 |
"index/suffixarray" |
... | ... |
@@ -69,7 +70,7 @@ type progressReader struct { |
69 | 69 |
readProgress int // How much has been read so far (bytes) |
70 | 70 |
lastUpdate int // How many bytes read at least update |
71 | 71 |
template string // Template to print. Default "%v/%v (%v)" |
72 |
- json bool |
|
72 |
+ sf *StreamFormatter |
|
73 | 73 |
} |
74 | 74 |
|
75 | 75 |
func (r *progressReader) Read(p []byte) (n int, err error) { |
... | ... |
@@ -93,7 +94,7 @@ func (r *progressReader) Read(p []byte) (n int, err error) { |
93 | 93 |
} |
94 | 94 |
// Send newline when complete |
95 | 95 |
if err != nil { |
96 |
- fmt.Fprintf(r.output, FormatStatus("", r.json)) |
|
96 |
+ r.output.Write(r.sf.FormatStatus("")) |
|
97 | 97 |
} |
98 | 98 |
|
99 | 99 |
return read, err |
... | ... |
@@ -101,11 +102,12 @@ func (r *progressReader) Read(p []byte) (n int, err error) { |
101 | 101 |
func (r *progressReader) Close() error { |
102 | 102 |
return io.ReadCloser(r.reader).Close() |
103 | 103 |
} |
104 |
-func ProgressReader(r io.ReadCloser, size int, output io.Writer, template string, json bool) *progressReader { |
|
105 |
- if template == "" { |
|
106 |
- template = "%v/%v (%v)\r" |
|
104 |
+func ProgressReader(r io.ReadCloser, size int, output io.Writer, template []byte, sf *StreamFormatter) *progressReader { |
|
105 |
+ tpl := string(template) |
|
106 |
+ if tpl == "" { |
|
107 |
+ tpl = string(sf.FormatProgress("", "%v/%v (%v)")) |
|
107 | 108 |
} |
108 |
- return &progressReader{r, NewWriteFlusher(output), size, 0, 0, template, json} |
|
109 |
+ return &progressReader{r, NewWriteFlusher(output), size, 0, 0, tpl, sf} |
|
109 | 110 |
} |
110 | 111 |
|
111 | 112 |
// HumanDuration returns a human-readable approximation of a duration |
... | ... |
@@ -564,16 +566,57 @@ func NewWriteFlusher(w io.Writer) *WriteFlusher { |
564 | 564 |
return &WriteFlusher{w: w, flusher: flusher} |
565 | 565 |
} |
566 | 566 |
|
567 |
-func FormatStatus(str string, json bool) string { |
|
568 |
- if json { |
|
569 |
- return "{\"status\" : \"" + str + "\"}" |
|
567 |
+type JsonMessage struct { |
|
568 |
+ Status string `json:"status,omitempty"` |
|
569 |
+ Progress string `json:"progress,omitempty"` |
|
570 |
+ Error string `json:"error,omitempty"` |
|
571 |
+} |
|
572 |
+ |
|
573 |
+type StreamFormatter struct { |
|
574 |
+ json bool |
|
575 |
+ used bool |
|
576 |
+} |
|
577 |
+ |
|
578 |
+func NewStreamFormatter(json bool) *StreamFormatter { |
|
579 |
+ return &StreamFormatter{json, false} |
|
580 |
+} |
|
581 |
+ |
|
582 |
+func (sf *StreamFormatter) FormatStatus(format string, a ...interface{}) []byte { |
|
583 |
+ sf.used = true |
|
584 |
+ str := fmt.Sprintf(format, a...) |
|
585 |
+ if sf.json { |
|
586 |
+ b, err := json.Marshal(&JsonMessage{Status:str}); |
|
587 |
+ if err != nil { |
|
588 |
+ return sf.FormatError(err) |
|
589 |
+ } |
|
590 |
+ return b |
|
570 | 591 |
} |
571 |
- return str + "\r\n" |
|
592 |
+ return []byte(str + "\r\n") |
|
572 | 593 |
} |
573 | 594 |
|
574 |
-func FormatProgress(str string, json bool) string { |
|
575 |
- if json { |
|
576 |
- return "{\"progress\" : \"" + str + "\"}" |
|
595 |
+func (sf *StreamFormatter) FormatError(err error) []byte { |
|
596 |
+ sf.used = true |
|
597 |
+ if sf.json { |
|
598 |
+ if b, err := json.Marshal(&JsonMessage{Error:err.Error()}); err == nil { |
|
599 |
+ return b |
|
600 |
+ } |
|
601 |
+ return []byte("{\"error\":\"format error\"}") |
|
577 | 602 |
} |
578 |
- return "Downloading " + str + "\r" |
|
603 |
+ return []byte("Error: " + err.Error() + "\r\n") |
|
604 |
+} |
|
605 |
+ |
|
606 |
+func (sf *StreamFormatter) FormatProgress(action, str string) []byte { |
|
607 |
+ sf.used = true |
|
608 |
+ if sf.json { |
|
609 |
+ b, err := json.Marshal(&JsonMessage{Progress:str}) |
|
610 |
+ if err != nil { |
|
611 |
+ return nil |
|
612 |
+ } |
|
613 |
+ return b |
|
614 |
+ } |
|
615 |
+ return []byte(action + " " + str + "\r") |
|
616 |
+} |
|
617 |
+ |
|
618 |
+func (sf *StreamFormatter) Used() bool { |
|
619 |
+ return sf.used |
|
579 | 620 |
} |