diff --git a/AUTHORS b/AUTHORS index 1599a1d0e1..eb90cd02d9 100644 --- a/AUTHORS +++ b/AUTHORS @@ -42,6 +42,7 @@ Ken Cochrane Kevin J. Lynagh Louis Opter Maxim Treskin +Michael Crosby Mikhail Sobolev Nate Jones Nelson Chen diff --git a/FIXME b/FIXME new file mode 100644 index 0000000000..e252fb2589 --- /dev/null +++ b/FIXME @@ -0,0 +1,18 @@ + +## FIXME + +This file is a loose collection of things to improve in the codebase, for the internal +use of the maintainers. + +They are not big enough to be in the roadmap, not user-facing enough to be github issues, +and not important enough to be discussed in the mailing list. + +They are just like FIXME comments in the source code, except we're not sure where in the source +to put them - so we put them here :) + + +* Merge Runtime, Server and Builder into Runtime +* Run linter on codebase +* Unify build commands and regular commands +* Move source code into src/ subdir for clarity +* Clean up the Makefile, it's a mess diff --git a/NOTICE b/NOTICE index f55cc6950a..a11ff94049 100644 --- a/NOTICE +++ b/NOTICE @@ -3,4 +3,9 @@ Copyright 2012-2013 dotCloud, inc. This product includes software developed at dotCloud, inc. (http://www.dotcloud.com). -This product contains software (https://github.com/kr/pty) developed by Keith Rarick, licensed under the MIT License. \ No newline at end of file +This product contains software (https://github.com/kr/pty) developed by Keith Rarick, licensed under the MIT License. + +Transfers of Docker shall be in accordance with applicable export controls of any country and all other applicable +legal requirements. Docker shall not be distributed or downloaded to or in Cuba, Iran, North Korea, Sudan or Syria +and shall not be distributed or downloaded to any person on the Denied Persons List administered by the U.S. +Department of Commerce. diff --git a/README.md b/README.md index b65f5eeb2e..1c909e5431 100644 --- a/README.md +++ b/README.md @@ -251,7 +251,7 @@ Note ---- We also keep the documentation in this repository. The website documentation is generated using sphinx using these sources. -Please find it under docs/sources/ and read more about it https://github.com/dotcloud/docker/master/docs/README.md +Please find it under docs/sources/ and read more about it https://github.com/dotcloud/docker/tree/master/docs/README.md Please feel free to fix / update the documentation and send us pull requests. More tutorials are also welcome. @@ -373,5 +373,8 @@ Standard Container Specification ### Legal -Transfers Docker shall be in accordance with any applicable export control or other legal requirements. +Transfers of Docker shall be in accordance with applicable export controls of any country and all other applicable +legal requirements. Docker shall not be distributed or downloaded to or in Cuba, Iran, North Korea, Sudan or Syria +and shall not be distributed or downloaded to any person on the Denied Persons List administered by the U.S. +Department of Commerce. diff --git a/api.go b/api.go index 5a9bdbbe23..5e1a6d7011 100644 --- a/api.go +++ b/api.go @@ -13,7 +13,7 @@ import ( "strings" ) -const APIVERSION = 1.1 +const APIVERSION = 1.2 func hijackServer(w http.ResponseWriter) (io.ReadCloser, io.Writer, error) { conn, _, err := w.(http.Hijacker).Hijack() @@ -45,6 +45,8 @@ func httpError(w http.ResponseWriter, err error) { http.Error(w, err.Error(), http.StatusNotFound) } else if strings.HasPrefix(err.Error(), "Bad parameter") { http.Error(w, err.Error(), http.StatusBadRequest) + } else if strings.HasPrefix(err.Error(), "Conflict") { + http.Error(w, err.Error(), http.StatusConflict) } else if strings.HasPrefix(err.Error(), "Impossible") { http.Error(w, err.Error(), http.StatusNotAcceptable) } else { @@ -69,8 +71,10 @@ func getBoolParam(value string) (bool, error) { } func getAuth(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { - // FIXME: Handle multiple login at once - // FIXME: return specific error code if config file missing? + if version > 1.1 { + w.WriteHeader(http.StatusNotFound) + return nil + } authConfig, err := auth.LoadConfig(srv.runtime.root) if err != nil { if err != auth.ErrConfigFileMissing { @@ -87,29 +91,34 @@ func getAuth(srv *Server, version float64, w http.ResponseWriter, r *http.Reques } func postAuth(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { - // FIXME: Handle multiple login at once - config := &auth.AuthConfig{} - if err := json.NewDecoder(r.Body).Decode(config); err != nil { + authConfig := &auth.AuthConfig{} + err := json.NewDecoder(r.Body).Decode(authConfig) + if err != nil { return err } - - authConfig, err := auth.LoadConfig(srv.runtime.root) - if err != nil { - if err != auth.ErrConfigFileMissing { + status := "" + if version > 1.1 { + status, err = auth.Login(authConfig, false) + if err != nil { return err } - authConfig = &auth.AuthConfig{} - } - if config.Username == authConfig.Username { - config.Password = authConfig.Password - } + } else { + localAuthConfig, err := auth.LoadConfig(srv.runtime.root) + if err != nil { + if err != auth.ErrConfigFileMissing { + return err + } + } + if authConfig.Username == localAuthConfig.Username { + authConfig.Password = localAuthConfig.Password + } - newAuthConfig := auth.NewAuthConfig(config.Username, config.Password, config.Email, srv.runtime.root) - status, err := auth.Login(newAuthConfig) - if err != nil { - return err + newAuthConfig := auth.NewAuthConfig(authConfig.Username, authConfig.Password, authConfig.Email, srv.runtime.root) + status, err = auth.Login(newAuthConfig, true) + if err != nil { + return err + } } - if status != "" { b, err := json.Marshal(&APIAuth{Status: status}) if err != nil { @@ -320,7 +329,7 @@ func postImagesCreate(srv *Server, version float64, w http.ResponseWriter, r *ht sf := utils.NewStreamFormatter(version > 1.0) if image != "" { //pull registry := r.Form.Get("registry") - if err := srv.ImagePull(image, tag, registry, w, sf); err != nil { + if err := srv.ImagePull(image, tag, registry, w, sf, &auth.AuthConfig{}); err != nil { if sf.Used() { w.Write(sf.FormatError(err)) return nil @@ -388,6 +397,18 @@ func postImagesInsert(srv *Server, version float64, w http.ResponseWriter, r *ht } func postImagesPush(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + authConfig := &auth.AuthConfig{} + if version > 1.1 { + if err := json.NewDecoder(r.Body).Decode(authConfig); err != nil { + return err + } + } else { + localAuthConfig, err := auth.LoadConfig(srv.runtime.root) + if err != nil && err != auth.ErrConfigFileMissing { + return err + } + authConfig = localAuthConfig + } if err := parseForm(r); err != nil { return err } @@ -401,7 +422,7 @@ func postImagesPush(srv *Server, version float64, w http.ResponseWriter, r *http w.Header().Set("Content-Type", "application/json") } sf := utils.NewStreamFormatter(version > 1.0) - if err := srv.ImagePush(name, registry, w, sf); err != nil { + if err := srv.ImagePush(name, registry, w, sf, authConfig); err != nil { if sf.Used() { w.Write(sf.FormatError(err)) return nil @@ -481,14 +502,30 @@ func deleteContainers(srv *Server, version float64, w http.ResponseWriter, r *ht } func deleteImages(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := parseForm(r); err != nil { + return err + } if vars == nil { return fmt.Errorf("Missing parameter") } name := vars["name"] - if err := srv.ImageDelete(name); err != nil { + imgs, err := srv.ImageDelete(name, version > 1.1) + if err != nil { return err } - w.WriteHeader(http.StatusNoContent) + if imgs != nil { + if len(*imgs) != 0 { + b, err := json.Marshal(imgs) + if err != nil { + return err + } + writeJSON(w, b) + } else { + return fmt.Errorf("Conflict, %s wasn't deleted", name) + } + } else { + w.WriteHeader(http.StatusNoContent) + } return nil } @@ -703,9 +740,18 @@ func postBuild(srv *Server, version float64, w http.ResponseWriter, r *http.Requ return nil } -func ListenAndServe(addr string, srv *Server, logging bool) error { +func optionsHandler(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + w.WriteHeader(http.StatusOK) + return nil +} +func writeCorsHeaders(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Access-Control-Allow-Origin", "*") + w.Header().Add("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept") + w.Header().Add("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT, OPTIONS") +} + +func createRouter(srv *Server, logging bool) (*mux.Router, error) { r := mux.NewRouter() - log.Printf("Listening for HTTP on %s\n", addr) m := map[string]map[string]func(*Server, float64, http.ResponseWriter, *http.Request, map[string]string) error{ "GET": { @@ -745,6 +791,9 @@ func ListenAndServe(addr string, srv *Server, logging bool) error { "/containers/{name:.*}": deleteContainers, "/images/{name:.*}": deleteImages, }, + "OPTIONS": { + "": optionsHandler, + }, } for method, routes := range m { @@ -769,6 +818,9 @@ func ListenAndServe(addr string, srv *Server, logging bool) error { if err != nil { version = APIVERSION } + if srv.enableCors { + writeCorsHeaders(w, r) + } if version == 0 || version > APIVERSION { w.WriteHeader(http.StatusNotFound) return @@ -777,9 +829,24 @@ func ListenAndServe(addr string, srv *Server, logging bool) error { httpError(w, err) } } - r.Path("/v{version:[0-9.]+}" + localRoute).Methods(localMethod).HandlerFunc(f) - r.Path(localRoute).Methods(localMethod).HandlerFunc(f) + + if localRoute == "" { + r.Methods(localMethod).HandlerFunc(f) + } else { + r.Path("/v{version:[0-9.]+}" + localRoute).Methods(localMethod).HandlerFunc(f) + r.Path(localRoute).Methods(localMethod).HandlerFunc(f) + } } } + return r, nil +} + +func ListenAndServe(addr string, srv *Server, logging bool) error { + log.Printf("Listening for HTTP on %s\n", addr) + + r, err := createRouter(srv, logging) + if err != nil { + return err + } return http.ListenAndServe(addr, r) } diff --git a/api_params.go b/api_params.go index b8596d854b..2a641f8ccd 100644 --- a/api_params.go +++ b/api_params.go @@ -7,10 +7,12 @@ type APIHistory struct { } type APIImages struct { - Repository string `json:",omitempty"` - Tag string `json:",omitempty"` - ID string `json:"Id"` - Created int64 + Repository string `json:",omitempty"` + Tag string `json:",omitempty"` + ID string `json:"Id"` + Created int64 + Size int64 + VirtualSize int64 } type APIInfo struct { @@ -23,13 +25,20 @@ type APIInfo struct { SwapLimit bool `json:",omitempty"` } +type APIRmi struct { + Deleted string `json:",omitempty"` + Untagged string `json:",omitempty"` +} + type APIContainers struct { - ID string `json:"Id"` - Image string - Command string - Created int64 - Status string - Ports string + ID string `json:"Id"` + Image string + Command string + Created int64 + Status string + Ports string + SizeRw int64 + SizeRootFs int64 } type APISearch struct { diff --git a/api_test.go b/api_test.go index e122786b86..40b31d4961 100644 --- a/api_test.go +++ b/api_test.go @@ -6,7 +6,6 @@ import ( "bytes" "encoding/json" "github.com/dotcloud/docker/auth" - "github.com/dotcloud/docker/registry" "github.com/dotcloud/docker/utils" "io" "net" @@ -18,7 +17,7 @@ import ( "time" ) -func TestGetAuth(t *testing.T) { +func TestPostAuth(t *testing.T) { runtime, err := newTestRuntime() if err != nil { t.Fatal(err) @@ -54,12 +53,6 @@ func TestGetAuth(t *testing.T) { if r.Code != http.StatusOK && r.Code != 0 { t.Fatalf("%d OK or 0 expected, received %d\n", http.StatusOK, r.Code) } - - newAuthConfig := registry.NewRegistry(runtime.root).GetAuthConfig(false) - if newAuthConfig.Username != authConfig.Username || - newAuthConfig.Email != authConfig.Email { - t.Fatalf("The auth configuration hasn't been set correctly") - } } func TestGetVersion(t *testing.T) { @@ -494,40 +487,6 @@ func TestGetContainersByName(t *testing.T) { } } -func TestPostAuth(t *testing.T) { - runtime, err := newTestRuntime() - if err != nil { - t.Fatal(err) - } - defer nuke(runtime) - - srv := &Server{ - runtime: runtime, - } - - config := &auth.AuthConfig{ - Username: "utest", - Email: "utest@yopmail.com", - } - - authStr := auth.EncodeAuth(config) - auth.SaveConfig(runtime.root, authStr, config.Email) - - r := httptest.NewRecorder() - if err := getAuth(srv, APIVERSION, r, nil, nil); err != nil { - t.Fatal(err) - } - - authConfig := &auth.AuthConfig{} - if err := json.Unmarshal(r.Body.Bytes(), authConfig); err != nil { - t.Fatal(err) - } - - if authConfig.Username != config.Username || authConfig.Email != config.Email { - t.Errorf("The retrieve auth mismatch with the one set.") - } -} - func TestPostCommit(t *testing.T) { runtime, err := newTestRuntime() if err != nil { @@ -1239,9 +1198,131 @@ func TestDeleteContainers(t *testing.T) { } } +func TestOptionsRoute(t *testing.T) { + runtime, err := newTestRuntime() + if err != nil { + t.Fatal(err) + } + defer nuke(runtime) + + srv := &Server{runtime: runtime, enableCors: true} + + r := httptest.NewRecorder() + router, err := createRouter(srv, false) + if err != nil { + t.Fatal(err) + } + + req, err := http.NewRequest("OPTIONS", "/", nil) + if err != nil { + t.Fatal(err) + } + + router.ServeHTTP(r, req) + if r.Code != http.StatusOK { + t.Errorf("Expected response for OPTIONS request to be \"200\", %v found.", r.Code) + } +} + +func TestGetEnabledCors(t *testing.T) { + runtime, err := newTestRuntime() + if err != nil { + t.Fatal(err) + } + defer nuke(runtime) + + srv := &Server{runtime: runtime, enableCors: true} + + r := httptest.NewRecorder() + + router, err := createRouter(srv, false) + if err != nil { + t.Fatal(err) + } + + req, err := http.NewRequest("GET", "/version", nil) + if err != nil { + t.Fatal(err) + } + + router.ServeHTTP(r, req) + if r.Code != http.StatusOK { + t.Errorf("Expected response for OPTIONS request to be \"200\", %v found.", r.Code) + } + + allowOrigin := r.Header().Get("Access-Control-Allow-Origin") + allowHeaders := r.Header().Get("Access-Control-Allow-Headers") + allowMethods := r.Header().Get("Access-Control-Allow-Methods") + + if allowOrigin != "*" { + t.Errorf("Expected header Access-Control-Allow-Origin to be \"*\", %s found.", allowOrigin) + } + if allowHeaders != "Origin, X-Requested-With, Content-Type, Accept" { + t.Errorf("Expected header Access-Control-Allow-Headers to be \"Origin, X-Requested-With, Content-Type, Accept\", %s found.", allowHeaders) + } + if allowMethods != "GET, POST, DELETE, PUT, OPTIONS" { + t.Errorf("Expected hearder Access-Control-Allow-Methods to be \"GET, POST, DELETE, PUT, OPTIONS\", %s found.", allowMethods) + } +} + func TestDeleteImages(t *testing.T) { - //FIXME: Implement this test - t.Log("Test not implemented") + runtime, err := newTestRuntime() + if err != nil { + t.Fatal(err) + } + defer nuke(runtime) + + srv := &Server{runtime: runtime} + + if err := srv.runtime.repositories.Set("test", "test", unitTestImageName, true); err != nil { + t.Fatal(err) + } + + images, err := srv.Images(false, "") + if err != nil { + t.Fatal(err) + } + + if len(images) != 2 { + t.Errorf("Excepted 2 images, %d found", len(images)) + } + + req, err := http.NewRequest("DELETE", "/images/test:test", nil) + if err != nil { + t.Fatal(err) + } + + r := httptest.NewRecorder() + if err := deleteImages(srv, APIVERSION, r, req, map[string]string{"name": "test:test"}); err != nil { + t.Fatal(err) + } + if r.Code != http.StatusOK { + t.Fatalf("%d OK expected, received %d\n", http.StatusOK, r.Code) + } + + var outs []APIRmi + if err := json.Unmarshal(r.Body.Bytes(), &outs); err != nil { + t.Fatal(err) + } + if len(outs) != 1 { + t.Fatalf("Expected %d event (untagged), got %d", 1, len(outs)) + } + images, err = srv.Images(false, "") + if err != nil { + t.Fatal(err) + } + + if len(images) != 1 { + t.Errorf("Excepted 1 image, %d found", len(images)) + } + + /* if c := runtime.Get(container.Id); c != nil { + t.Fatalf("The container as not been deleted") + } + + if _, err := os.Stat(path.Join(container.rwPath(), "test")); err == nil { + t.Fatalf("The test file has not been deleted") + } */ } // Mocked types for tests diff --git a/auth/auth.go b/auth/auth.go index 72cf36efa5..b7ad73b9ed 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -48,7 +48,7 @@ func IndexServerAddress() string { } // create a base64 encoded auth string to store in config -func EncodeAuth(authConfig *AuthConfig) string { +func encodeAuth(authConfig *AuthConfig) string { authStr := authConfig.Username + ":" + authConfig.Password msg := []byte(authStr) encoded := make([]byte, base64.StdEncoding.EncodedLen(len(msg))) @@ -57,7 +57,7 @@ func EncodeAuth(authConfig *AuthConfig) string { } // decode the auth string -func DecodeAuth(authStr string) (*AuthConfig, error) { +func decodeAuth(authStr string) (*AuthConfig, error) { decLen := base64.StdEncoding.DecodedLen(len(authStr)) decoded := make([]byte, decLen) authByte := []byte(authStr) @@ -82,7 +82,7 @@ func DecodeAuth(authStr string) (*AuthConfig, error) { func LoadConfig(rootPath string) (*AuthConfig, error) { confFile := path.Join(rootPath, CONFIGFILE) if _, err := os.Stat(confFile); err != nil { - return nil, ErrConfigFileMissing + return &AuthConfig{rootPath:rootPath}, ErrConfigFileMissing } b, err := ioutil.ReadFile(confFile) if err != nil { @@ -94,7 +94,7 @@ func LoadConfig(rootPath string) (*AuthConfig, error) { } origAuth := strings.Split(arr[0], " = ") origEmail := strings.Split(arr[1], " = ") - authConfig, err := DecodeAuth(origAuth[1]) + authConfig, err := decodeAuth(origAuth[1]) if err != nil { return nil, err } @@ -104,13 +104,13 @@ func LoadConfig(rootPath string) (*AuthConfig, error) { } // save the auth config -func SaveConfig(rootPath, authStr string, email string) error { - confFile := path.Join(rootPath, CONFIGFILE) - if len(email) == 0 { +func SaveConfig(authConfig *AuthConfig) error { + confFile := path.Join(authConfig.rootPath, CONFIGFILE) + if len(authConfig.Email) == 0 { os.Remove(confFile) return nil } - lines := "auth = " + authStr + "\n" + "email = " + email + "\n" + lines := "auth = " + encodeAuth(authConfig) + "\n" + "email = " + authConfig.Email + "\n" b := []byte(lines) err := ioutil.WriteFile(confFile, b, 0600) if err != nil { @@ -120,7 +120,7 @@ func SaveConfig(rootPath, authStr string, email string) error { } // try to register/login to the registry server -func Login(authConfig *AuthConfig) (string, error) { +func Login(authConfig *AuthConfig, store bool) (string, error) { storeConfig := false client := &http.Client{} reqStatusCode := 0 @@ -168,8 +168,10 @@ func Login(authConfig *AuthConfig) (string, error) { status = "Login Succeeded\n" storeConfig = true } else if resp.StatusCode == 401 { - if err := SaveConfig(authConfig.rootPath, "", ""); err != nil { - return "", err + if store { + if err := SaveConfig(authConfig); err != nil { + return "", err + } } return "", fmt.Errorf("Wrong login/password, please try again") } else { @@ -182,9 +184,8 @@ func Login(authConfig *AuthConfig) (string, error) { } else { return "", fmt.Errorf("Unexpected status code [%d] : %s", reqStatusCode, reqBody) } - if storeConfig { - authStr := EncodeAuth(authConfig) - if err := SaveConfig(authConfig.rootPath, authStr, authConfig.Email); err != nil { + if storeConfig && store { + if err := SaveConfig(authConfig); err != nil { return "", err } } diff --git a/buildfile.go b/buildfile.go index 5ea5607fc7..3c706b04ba 100644 --- a/buildfile.go +++ b/buildfile.go @@ -61,7 +61,7 @@ func (b *buildFile) CmdFrom(name string) error { remote = name } - if err := b.srv.ImagePull(remote, tag, "", b.out, utils.NewStreamFormatter(false)); err != nil { + if err := b.srv.ImagePull(remote, tag, "", b.out, utils.NewStreamFormatter(false), nil); err != nil { return err } @@ -176,16 +176,14 @@ func (b *buildFile) CmdAdd(args string) error { dest := strings.Trim(tmp[1], " ") cmd := b.config.Cmd - b.config.Cmd = []string{"/bin/sh", "-c", fmt.Sprintf("#(nop) ADD %s in %s", orig, dest)} - cid, err := b.run() + + // Create the container and start it + container, err := b.builder.Create(b.config) if err != nil { return err } + b.tmpContainers[container.ID] = struct{}{} - container := b.runtime.Get(cid) - if container == nil { - return fmt.Errorf("Error while creating the container (CmdAdd)") - } if err := container.EnsureMounted(); err != nil { return err } @@ -220,7 +218,7 @@ func (b *buildFile) CmdAdd(args string) error { return err } } - if err := b.commit(cid, cmd, fmt.Sprintf("ADD %s in %s", orig, dest)); err != nil { + if err := b.commit(container.ID, cmd, fmt.Sprintf("ADD %s in %s", orig, dest)); err != nil { return err } b.config.Cmd = cmd @@ -272,11 +270,19 @@ func (b *buildFile) commit(id string, autoCmd []string, comment string) error { utils.Debugf("[BUILDER] Cache miss") } - cid, err := b.run() + // Create the container and start it + container, err := b.builder.Create(b.config) if err != nil { return err } - id = cid + b.tmpContainers[container.ID] = struct{}{} + + if err := container.EnsureMounted(); err != nil { + return err + } + defer container.Unmount() + + id = container.ID } container := b.runtime.Get(id) @@ -313,10 +319,11 @@ func (b *buildFile) Build(dockerfile, context io.Reader) (string, error) { for { line, err := file.ReadString('\n') if err != nil { - if err == io.EOF { + if err == io.EOF && line == "" { break + } else if err != io.EOF { + return "", err } - return "", err } line = strings.Replace(strings.TrimSpace(line), " ", " ", 1) // Skip comments and empty line diff --git a/buildfile_test.go b/buildfile_test.go index ffbcdbbddc..d9c60a70d5 100644 --- a/buildfile_test.go +++ b/buildfile_test.go @@ -15,58 +15,69 @@ run sh -c 'echo root:testpass > /tmp/passwd' run mkdir -p /var/run/sshd ` +const DockerfileNoNewLine = ` +# VERSION 0.1 +# DOCKER-VERSION 0.2 + +from ` + unitTestImageName + ` +run sh -c 'echo root:testpass > /tmp/passwd' +run mkdir -p /var/run/sshd` + func TestBuild(t *testing.T) { - runtime, err := newTestRuntime() - if err != nil { - t.Fatal(err) - } - defer nuke(runtime) + dockerfiles := []string{Dockerfile, DockerfileNoNewLine} + for _, Dockerfile := range dockerfiles { + runtime, err := newTestRuntime() + if err != nil { + t.Fatal(err) + } + defer nuke(runtime) - srv := &Server{runtime: runtime} + srv := &Server{runtime: runtime} - buildfile := NewBuildFile(srv, &utils.NopWriter{}) + buildfile := NewBuildFile(srv, &utils.NopWriter{}) - imgID, err := buildfile.Build(strings.NewReader(Dockerfile), nil) - if err != nil { - t.Fatal(err) - } + imgID, err := buildfile.Build(strings.NewReader(Dockerfile), nil) + if err != nil { + t.Fatal(err) + } - builder := NewBuilder(runtime) - container, err := builder.Create( - &Config{ - Image: imgID, - Cmd: []string{"cat", "/tmp/passwd"}, - }, - ) - if err != nil { - t.Fatal(err) - } - defer runtime.Destroy(container) + builder := NewBuilder(runtime) + container, err := builder.Create( + &Config{ + Image: imgID, + Cmd: []string{"cat", "/tmp/passwd"}, + }, + ) + if err != nil { + t.Fatal(err) + } + defer runtime.Destroy(container) - output, err := container.Output() - if err != nil { - t.Fatal(err) - } - if string(output) != "root:testpass\n" { - t.Fatalf("Unexpected output. Read '%s', expected '%s'", output, "root:testpass\n") - } + output, err := container.Output() + if err != nil { + t.Fatal(err) + } + if string(output) != "root:testpass\n" { + t.Fatalf("Unexpected output. Read '%s', expected '%s'", output, "root:testpass\n") + } - container2, err := builder.Create( - &Config{ - Image: imgID, - Cmd: []string{"ls", "-d", "/var/run/sshd"}, - }, - ) - if err != nil { - t.Fatal(err) - } - defer runtime.Destroy(container2) + container2, err := builder.Create( + &Config{ + Image: imgID, + Cmd: []string{"ls", "-d", "/var/run/sshd"}, + }, + ) + if err != nil { + t.Fatal(err) + } + defer runtime.Destroy(container2) - output, err = container2.Output() - if err != nil { - t.Fatal(err) - } - if string(output) != "/var/run/sshd\n" { - t.Fatal("/var/run/sshd has not been created") + output, err = container2.Output() + if err != nil { + t.Fatal(err) + } + if string(output) != "/var/run/sshd\n" { + t.Fatal("/var/run/sshd has not been created") + } } } diff --git a/commands.go b/commands.go index 50aaac8fb1..e9c1197f3f 100644 --- a/commands.go +++ b/commands.go @@ -218,7 +218,10 @@ func (cli *DockerCli) CmdBuild(args ...string) error { if err != nil { return err } - return fmt.Errorf("error: %s", body) + if len(body) == 0 { + return fmt.Errorf("Error: %s", http.StatusText(resp.StatusCode)) + } + return fmt.Errorf("Error: %s", body) } // Output the result @@ -284,27 +287,16 @@ func (cli *DockerCli) CmdLogin(args ...string) error { return nil } - body, _, err := cli.call("GET", "/auth", nil) - if err != nil { - return err - } - - var out auth.AuthConfig - err = json.Unmarshal(body, &out) - if err != nil { - return err - } - var username string var password string var email string - fmt.Print("Username (", out.Username, "): ") + fmt.Print("Username (", cli.authConfig.Username, "): ") username = readAndEchoString(os.Stdin, os.Stdout) if username == "" { - username = out.Username + username = cli.authConfig.Username } - if username != out.Username { + if username != cli.authConfig.Username { fmt.Print("Password: ") password = readString(os.Stdin, os.Stdout) @@ -312,20 +304,21 @@ func (cli *DockerCli) CmdLogin(args ...string) error { return fmt.Errorf("Error : Password Required") } - fmt.Print("Email (", out.Email, "): ") + fmt.Print("Email (", cli.authConfig.Email, "): ") email = readAndEchoString(os.Stdin, os.Stdout) if email == "" { - email = out.Email + email = cli.authConfig.Email } } else { - email = out.Email + email = cli.authConfig.Email } + term.RestoreTerminal(oldState) - out.Username = username - out.Password = password - out.Email = email + cli.authConfig.Username = username + cli.authConfig.Password = password + cli.authConfig.Email = email - body, _, err = cli.call("POST", "/auth", out) + body, _, err := cli.call("POST", "/auth", cli.authConfig) if err != nil { return err } @@ -333,10 +326,11 @@ func (cli *DockerCli) CmdLogin(args ...string) error { var out2 APIAuth err = json.Unmarshal(body, &out2) if err != nil { + auth.LoadConfig(os.Getenv("HOME")) return err } + auth.SaveConfig(cli.authConfig) if out2.Status != "" { - term.RestoreTerminal(oldState) fmt.Print(out2.Status) } return nil @@ -457,7 +451,7 @@ func (cli *DockerCli) CmdStop(args ...string) error { for _, name := range cmd.Args() { _, _, err := cli.call("POST", "/containers/"+name+"/stop?"+v.Encode(), nil) if err != nil { - fmt.Printf("%s", err) + fmt.Fprintf(os.Stderr, "%s", err) } else { fmt.Println(name) } @@ -482,7 +476,7 @@ func (cli *DockerCli) CmdRestart(args ...string) error { for _, name := range cmd.Args() { _, _, err := cli.call("POST", "/containers/"+name+"/restart?"+v.Encode(), nil) if err != nil { - fmt.Printf("%s", err) + fmt.Fprintf(os.Stderr, "%s", err) } else { fmt.Println(name) } @@ -503,7 +497,7 @@ func (cli *DockerCli) CmdStart(args ...string) error { for _, name := range args { _, _, err := cli.call("POST", "/containers/"+name+"/start", nil) if err != nil { - fmt.Printf("%s", err) + fmt.Fprintf(os.Stderr, "%s", err) } else { fmt.Println(name) } @@ -512,29 +506,38 @@ func (cli *DockerCli) CmdStart(args ...string) error { } func (cli *DockerCli) CmdInspect(args ...string) error { - cmd := Subcmd("inspect", "CONTAINER|IMAGE", "Return low-level information on a container/image") + cmd := Subcmd("inspect", "CONTAINER|IMAGE [CONTAINER|IMAGE...]", "Return low-level information on a container/image") if err := cmd.Parse(args); err != nil { return nil } - if cmd.NArg() != 1 { + if cmd.NArg() < 1 { cmd.Usage() return nil } - obj, _, err := cli.call("GET", "/containers/"+cmd.Arg(0)+"/json", nil) - if err != nil { - obj, _, err = cli.call("GET", "/images/"+cmd.Arg(0)+"/json", nil) + fmt.Printf("[") + for i, name := range args { + if i > 0 { + fmt.Printf(",") + } + obj, _, err := cli.call("GET", "/containers/"+name+"/json", nil) if err != nil { - return err + obj, _, err = cli.call("GET", "/images/"+name+"/json", nil) + if err != nil { + fmt.Fprintf(os.Stderr, "%s", err) + continue + } + } + + indented := new(bytes.Buffer) + if err = json.Indent(indented, obj, "", " "); err != nil { + fmt.Fprintf(os.Stderr, "%s", err) + continue + } + if _, err := io.Copy(os.Stdout, indented); err != nil { + fmt.Fprintf(os.Stderr, "%s", err) } } - - indented := new(bytes.Buffer) - if err = json.Indent(indented, obj, "", " "); err != nil { - return err - } - if _, err := io.Copy(os.Stdout, indented); err != nil { - return err - } + fmt.Printf("]") return nil } @@ -561,7 +564,7 @@ func (cli *DockerCli) CmdPort(args ...string) error { if frontend, exists := out.NetworkSettings.PortMapping[cmd.Arg(1)]; exists { fmt.Println(frontend) } else { - return fmt.Errorf("error: No private port '%s' allocated on %s", cmd.Arg(1), cmd.Arg(0)) + return fmt.Errorf("Error: No private port '%s' allocated on %s", cmd.Arg(1), cmd.Arg(0)) } return nil } @@ -578,11 +581,22 @@ func (cli *DockerCli) CmdRmi(args ...string) error { } for _, name := range cmd.Args() { - _, _, err := cli.call("DELETE", "/images/"+name, nil) + body, _, err := cli.call("DELETE", "/images/"+name, nil) if err != nil { - fmt.Printf("%s", err) + fmt.Fprintf(os.Stderr, "%s", err) } else { - fmt.Println(name) + var outs []APIRmi + err = json.Unmarshal(body, &outs) + if err != nil { + return err + } + for _, out := range outs { + if out.Deleted != "" { + fmt.Println("Deleted:", out.Deleted) + } else { + fmt.Println("Untagged:", out.Untagged) + } + } } } return nil @@ -701,18 +715,22 @@ func (cli *DockerCli) CmdPush(args ...string) error { return nil } - username, err := cli.checkIfLogged(*registry == "", "push") - if err != nil { + if err := cli.checkIfLogged(*registry == "", "push"); err != nil { return err } if len(strings.SplitN(name, "/", 2)) == 1 { - return fmt.Errorf("Impossible to push a \"root\" repository. Please rename your repository in / (ex: %s/%s)", username, name) + return fmt.Errorf("Impossible to push a \"root\" repository. Please rename your repository in / (ex: %s/%s)", cli.authConfig.Username, name) + } + + buf, err := json.Marshal(cli.authConfig) + if err != nil { + return err } v := url.Values{} v.Set("registry", *registry) - if err := cli.stream("POST", "/images/"+name+"/push?"+v.Encode(), nil, os.Stdout); err != nil { + if err := cli.stream("POST", "/images/"+name+"/push?"+v.Encode(), bytes.NewBuffer(buf), os.Stdout); err != nil { return err } return nil @@ -793,7 +811,7 @@ func (cli *DockerCli) CmdImages(args ...string) error { w := tabwriter.NewWriter(os.Stdout, 20, 1, 3, ' ', 0) if !*quiet { - fmt.Fprintln(w, "REPOSITORY\tTAG\tID\tCREATED") + fmt.Fprintln(w, "REPOSITORY\tTAG\tID\tCREATED\tSIZE") } for _, out := range outs { @@ -811,7 +829,12 @@ func (cli *DockerCli) CmdImages(args ...string) error { } else { fmt.Fprintf(w, "%s\t", utils.TruncateID(out.ID)) } - fmt.Fprintf(w, "%s ago\n", utils.HumanDuration(time.Now().Sub(time.Unix(out.Created, 0)))) + fmt.Fprintf(w, "%s ago\t", utils.HumanDuration(time.Now().Sub(time.Unix(out.Created, 0)))) + if out.VirtualSize > 0 { + fmt.Fprintf(w, "%s (virtual %s)\n", utils.HumanSize(out.Size), utils.HumanSize(out.VirtualSize)) + } else { + fmt.Fprintf(w, "%s\n", utils.HumanSize(out.Size)) + } } else { if *noTrunc { fmt.Fprintln(w, out.ID) @@ -870,15 +893,20 @@ func (cli *DockerCli) CmdPs(args ...string) error { } w := tabwriter.NewWriter(os.Stdout, 20, 1, 3, ' ', 0) if !*quiet { - fmt.Fprintln(w, "ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS") + fmt.Fprintln(w, "ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tSIZE") } for _, out := range outs { if !*quiet { if *noTrunc { - 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) + fmt.Fprintf(w, "%s\t%s\t%s\t%s ago\t%s\t%s\t", out.ID, out.Image, out.Command, utils.HumanDuration(time.Now().Sub(time.Unix(out.Created, 0))), out.Status, out.Ports) } else { - 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) + fmt.Fprintf(w, "%s\t%s\t%s\t%s ago\t%s\t%s\t", 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) + } + if out.SizeRootFs > 0 { + fmt.Fprintf(w, "%s (virtual %s)\n", utils.HumanSize(out.SizeRw), utils.HumanSize(out.SizeRootFs)) + } else { + fmt.Fprintf(w, "%s\n", utils.HumanSize(out.SizeRw)) } } else { if *noTrunc { @@ -1027,7 +1055,9 @@ func (cli *DockerCli) CmdAttach(args ...string) error { connections += 1 } chErrors := make(chan error, connections) - cli.monitorTtySize(cmd.Arg(0)) + if container.Config.Tty { + cli.monitorTtySize(cmd.Arg(0)) + } if splitStderr { go func() { chErrors <- cli.hijack("POST", "/containers/"+cmd.Arg(0)+"/attach?stream=1&stderr=1", false, nil, os.Stderr) @@ -1079,7 +1109,12 @@ func (cli *DockerCli) CmdSearch(args ...string) error { w := tabwriter.NewWriter(os.Stdout, 20, 1, 3, ' ', 0) fmt.Fprintf(w, "NAME\tDESCRIPTION\n") for _, out := range outs { - fmt.Fprintf(w, "%s\t%s\n", out.Name, out.Description) + desc := strings.Replace(out.Description, "\n", " ", -1) + desc = strings.Replace(desc, "\r", " ", -1) + if len(desc) > 45 { + desc = utils.Trunc(desc, 42) + "..." + } + fmt.Fprintf(w, "%s\t%s\n", out.Name, desc) } w.Flush() return nil @@ -1233,7 +1268,9 @@ func (cli *DockerCli) CmdRun(args ...string) error { } if connections > 0 { chErrors := make(chan error, connections) - cli.monitorTtySize(out.ID) + if config.Tty { + cli.monitorTtySize(out.ID) + } if splitStderr && config.AttachStderr { go func() { @@ -1260,6 +1297,7 @@ func (cli *DockerCli) CmdRun(args ...string) error { for connections > 0 { err := <-chErrors if err != nil { + utils.Debugf("Error hijack: %s", err) return err } connections -= 1 @@ -1268,38 +1306,17 @@ func (cli *DockerCli) CmdRun(args ...string) error { return nil } -func (cli *DockerCli) checkIfLogged(condition bool, action string) (string, error) { - body, _, err := cli.call("GET", "/auth", nil) - if err != nil { - return "", err - } - - var out auth.AuthConfig - err = json.Unmarshal(body, &out) - if err != nil { - return "", err - } - +func (cli *DockerCli) checkIfLogged(condition bool, action string) error { // If condition AND the login failed - if condition && out.Username == "" { + if condition && cli.authConfig.Username == "" { if err := cli.CmdLogin(""); err != nil { - return "", err + return err } - - body, _, err = cli.call("GET", "/auth", nil) - if err != nil { - return "", err - } - err = json.Unmarshal(body, &out) - if err != nil { - return "", err - } - - if out.Username == "" { - return "", fmt.Errorf("Please login prior to %s. ('docker login')", action) + if cli.authConfig.Username == "" { + return fmt.Errorf("Please login prior to %s. ('docker login')", action) } } - return out.Username, nil + return nil } func (cli *DockerCli) call(method, path string, data interface{}) ([]byte, int, error) { @@ -1335,7 +1352,10 @@ func (cli *DockerCli) call(method, path string, data interface{}) ([]byte, int, return nil, -1, err } if resp.StatusCode < 200 || resp.StatusCode >= 400 { - return nil, resp.StatusCode, fmt.Errorf("error: %s", body) + if len(body) == 0 { + return nil, resp.StatusCode, fmt.Errorf("Error: %s", http.StatusText(resp.StatusCode)) + } + return nil, resp.StatusCode, fmt.Errorf("Error: %s", body) } return body, resp.StatusCode, nil } @@ -1365,7 +1385,10 @@ func (cli *DockerCli) stream(method, path string, in io.Reader, out io.Writer) e if err != nil { return err } - return fmt.Errorf("error: %s", body) + if len(body) == 0 { + return fmt.Errorf("Error :%s", http.StatusText(resp.StatusCode)) + } + return fmt.Errorf("Error: %s", body) } if resp.Header.Get("Content-Type") == "application/json" { @@ -1423,19 +1446,22 @@ func (cli *DockerCli) hijack(method, path string, setRawTerminal bool, in *os.Fi defer term.RestoreTerminal(oldState) } sendStdin := utils.Go(func() error { - _, err := io.Copy(rwc, in) + io.Copy(rwc, in) if err := rwc.(*net.TCPConn).CloseWrite(); err != nil { - fmt.Fprintf(os.Stderr, "Couldn't send EOF: %s\n", err) + utils.Debugf("Couldn't send EOF: %s\n", err) } - return err + // Discard errors due to pipe interruption + return nil }) if err := <-receiveStdout; err != nil { + utils.Debugf("Error receiveStdout: %s", err) return err } - if !term.IsTerminal(os.Stdin.Fd()) { + if !term.IsTerminal(in.Fd()) { if err := <-sendStdin; err != nil { + utils.Debugf("Error sendStdin: %s", err) return err } } @@ -1480,10 +1506,12 @@ func Subcmd(name, signature, description string) *flag.FlagSet { } func NewDockerCli(addr string, port int) *DockerCli { - return &DockerCli{addr, port} + authConfig, _ := auth.LoadConfig(os.Getenv("HOME")) + return &DockerCli{addr, port, authConfig} } type DockerCli struct { - host string - port int + host string + port int + authConfig *auth.AuthConfig } diff --git a/container.go b/container.go index 29c07eea0b..f60de21bdc 100644 --- a/container.go +++ b/container.go @@ -13,6 +13,7 @@ import ( "os" "os/exec" "path" + "path/filepath" "sort" "strconv" "strings" @@ -355,6 +356,18 @@ func (container *Container) Attach(stdin io.ReadCloser, stdinCloser io.Closer, s errors <- err }() } + } else { + go func() { + if stdinCloser != nil { + defer stdinCloser.Close() + } + + if cStdout, err := container.StdoutPipe(); err != nil { + utils.Debugf("Error stdout pipe") + } else { + io.Copy(&utils.NopWriter{}, cStdout) + } + }() } if stderr != nil { nJobs += 1 @@ -381,7 +394,20 @@ func (container *Container) Attach(stdin io.ReadCloser, stdinCloser io.Closer, s errors <- err }() } + } else { + go func() { + if stdinCloser != nil { + defer stdinCloser.Close() + } + + if cStderr, err := container.StderrPipe(); err != nil { + utils.Debugf("Error stdout pipe") + } else { + io.Copy(&utils.NopWriter{}, cStderr) + } + }() } + return utils.Go(func() error { if cStdout != nil { defer cStdout.Close() @@ -894,3 +920,26 @@ func validateID(id string) error { } return nil } + +// GetSize, return real size, virtual size +func (container *Container) GetSize() (int64, int64) { + var sizeRw, sizeRootfs int64 + + filepath.Walk(container.rwPath(), func(path string, fileInfo os.FileInfo, err error) error { + if fileInfo != nil { + sizeRw += fileInfo.Size() + } + return nil + }) + + _, err := os.Stat(container.RootfsPath()) + if err == nil { + filepath.Walk(container.RootfsPath(), func(path string, fileInfo os.FileInfo, err error) error { + if fileInfo != nil { + sizeRootfs += fileInfo.Size() + } + return nil + }) + } + return sizeRw, sizeRootfs +} diff --git a/docker/docker.go b/docker/docker.go index dada16e11e..74236613a7 100644 --- a/docker/docker.go +++ b/docker/docker.go @@ -33,6 +33,7 @@ func main() { bridgeName := flag.String("b", "", "Attach containers to a pre-existing network bridge") pidfile := flag.String("p", "/var/run/docker.pid", "File containing process PID") flHost := flag.String("H", fmt.Sprintf("%s:%d", host, port), "Host:port to bind/connect to") + flEnableCors := flag.Bool("api-enable-cors", false, "Enable CORS requests in the remote api.") flag.Parse() if *bridgeName != "" { docker.NetworkBridgeIface = *bridgeName @@ -65,7 +66,7 @@ func main() { flag.Usage() return } - if err := daemon(*pidfile, host, port, *flAutoRestart); err != nil { + if err := daemon(*pidfile, host, port, *flAutoRestart, *flEnableCors); err != nil { log.Fatal(err) os.Exit(-1) } @@ -104,7 +105,7 @@ func removePidFile(pidfile string) { } } -func daemon(pidfile, addr string, port int, autoRestart bool) error { +func daemon(pidfile, addr string, port int, autoRestart, enableCors bool) error { if addr != "127.0.0.1" { log.Println("/!\\ DON'T BIND ON ANOTHER IP ADDRESS THAN 127.0.0.1 IF YOU DON'T KNOW WHAT YOU'RE DOING /!\\") } @@ -122,7 +123,7 @@ func daemon(pidfile, addr string, port int, autoRestart bool) error { os.Exit(0) }() - server, err := docker.NewServer(autoRestart) + server, err := docker.NewServer(autoRestart, enableCors) if err != nil { return err } diff --git a/docs/Makefile b/docs/Makefile index 517d01658f..a97255f516 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -46,12 +46,11 @@ clean: -rm -rf $(BUILDDIR)/* docs: - #-rm -rf $(BUILDDIR)/* $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The documentation pages are now in $(BUILDDIR)/html." -server: +server: docs @cd $(BUILDDIR)/html; $(PYTHON) -m SimpleHTTPServer 8000 site: @@ -62,12 +61,13 @@ site: connect: @echo connecting dotcloud to www.docker.io website, make sure to use user 1 - @cd _build/website/ ; \ + @echo or create your own "dockerwebsite" app + @cd $(BUILDDIR)/website/ ; \ dotcloud connect dockerwebsite ; \ dotcloud list push: - @cd _build/website/ ; \ + @cd $(BUILDDIR)/website/ ; \ dotcloud push $(VERSIONS): diff --git a/docs/sources/api/README.md b/docs/sources/api/README.md new file mode 100644 index 0000000000..10dede382b --- /dev/null +++ b/docs/sources/api/README.md @@ -0,0 +1,5 @@ +This directory holds the authoritative specifications of APIs defined and implemented by Docker. Currently this includes: + +* The remote API by which a docker node can be queried over HTTP +* The registry API by which a docker node can download and upload container images for storage and sharing +* The index search API by which a docker node can search the public index for images to download diff --git a/docs/sources/api/docker_remote_api.rst b/docs/sources/api/docker_remote_api.rst index 1c46cf148a..824bfa4657 100644 --- a/docs/sources/api/docker_remote_api.rst +++ b/docs/sources/api/docker_remote_api.rst @@ -14,719 +14,44 @@ Docker Remote API - The Remote API is replacing rcli - Default port in the docker deamon is 4243 - The API tends to be REST, but for some complex commands, like attach or pull, the HTTP connection is hijacked to transport stdout stdin and stderr +- Since API version 1.2, the auth configuration is now handled client side, so the client has to send the authConfig as POST in /images/(name)/push -2. Version -========== +2. Versions +=========== -The current verson of the API is 1.1 -Calling /images//insert is the same as calling /v1.1/images//insert +The current verson of the API is 1.2 +Calling /images//insert is the same as calling /v1.2/images//insert You can still call an old version of the api using /v1.0/images//insert -3. Endpoints -============ - -3.1 Containers --------------- - -List containers -*************** - -.. http:get:: /containers/json - - List containers - - **Example request**: - - .. sourcecode:: http - - GET /containers/json?all=1&before=8dfafdbc3a40 HTTP/1.1 - - **Example response**: - - .. sourcecode:: http - - HTTP/1.1 200 OK - Content-Type: application/json - - [ - { - "Id": "8dfafdbc3a40", - "Image": "base:latest", - "Command": "echo 1", - "Created": 1367854155, - "Status": "Exit 0" - }, - { - "Id": "9cd87474be90", - "Image": "base:latest", - "Command": "echo 222222", - "Created": 1367854155, - "Status": "Exit 0" - }, - { - "Id": "3176a2479c92", - "Image": "base:latest", - "Command": "echo 3333333333333333", - "Created": 1367854154, - "Status": "Exit 0" - }, - { - "Id": "4cb07b47f9fb", - "Image": "base:latest", - "Command": "echo 444444444444444444444444444444444", - "Created": 1367854152, - "Status": "Exit 0" - } - ] - - :query all: 1/True/true or 0/False/false, Show all containers. Only running containers are shown by default - :query limit: Show ``limit`` last created containers, include non-running ones. - :query since: Show only containers created since Id, include non-running ones. - :query before: Show only containers created before Id, include non-running ones. - :statuscode 200: no error - :statuscode 400: bad parameter - :statuscode 500: server error - - -Create a container -****************** - -.. http:post:: /containers/create - - Create a container - - **Example request**: - - .. sourcecode:: http - - POST /containers/create HTTP/1.1 - Content-Type: application/json - - { - "Hostname":"", - "User":"", - "Memory":0, - "MemorySwap":0, - "AttachStdin":false, - "AttachStdout":true, - "AttachStderr":true, - "PortSpecs":null, - "Tty":false, - "OpenStdin":false, - "StdinOnce":false, - "Env":null, - "Cmd":[ - "date" - ], - "Dns":null, - "Image":"base", - "Volumes":{}, - "VolumesFrom":"" - } - - **Example response**: - - .. sourcecode:: http - - HTTP/1.1 201 OK - Content-Type: application/json - - { - "Id":"e90e34656806" - "Warnings":[] - } - - :jsonparam config: the container's configuration - :statuscode 201: no error - :statuscode 404: no such container - :statuscode 406: impossible to attach (container not running) - :statuscode 500: server error - - -Inspect a container -******************* - -.. http:get:: /containers/(id)/json - - Return low-level information on the container ``id`` - - **Example request**: - - .. sourcecode:: http - - GET /containers/4fa6e0f0c678/json HTTP/1.1 - - **Example response**: - - .. sourcecode:: http - - HTTP/1.1 200 OK - Content-Type: application/json - - { - "Id": "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2", - "Created": "2013-05-07T14:51:42.041847+02:00", - "Path": "date", - "Args": [], - "Config": { - "Hostname": "4fa6e0f0c678", - "User": "", - "Memory": 0, - "MemorySwap": 0, - "AttachStdin": false, - "AttachStdout": true, - "AttachStderr": true, - "PortSpecs": null, - "Tty": false, - "OpenStdin": false, - "StdinOnce": false, - "Env": null, - "Cmd": [ - "date" - ], - "Dns": null, - "Image": "base", - "Volumes": {}, - "VolumesFrom": "" - }, - "State": { - "Running": false, - "Pid": 0, - "ExitCode": 0, - "StartedAt": "2013-05-07T14:51:42.087658+02:01360", - "Ghost": false - }, - "Image": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", - "NetworkSettings": { - "IpAddress": "", - "IpPrefixLen": 0, - "Gateway": "", - "Bridge": "", - "PortMapping": null - }, - "SysInitPath": "/home/kitty/go/src/github.com/dotcloud/docker/bin/docker", - "ResolvConfPath": "/etc/resolv.conf", - "Volumes": {} - } - - :statuscode 200: no error - :statuscode 404: no such container - :statuscode 500: server error - - -Inspect changes on a container's filesystem -******************************************* - -.. http:get:: /containers/(id)/changes - - Inspect changes on container ``id`` 's filesystem - - **Example request**: - - .. sourcecode:: http - - GET /containers/4fa6e0f0c678/changes HTTP/1.1 - - - **Example response**: - - .. sourcecode:: http - - HTTP/1.1 200 OK - Content-Type: application/json - - [ - { - "Path":"/dev", - "Kind":0 - }, - { - "Path":"/dev/kmsg", - "Kind":1 - }, - { - "Path":"/test", - "Kind":1 - } - ] - - :statuscode 200: no error - :statuscode 404: no such container - :statuscode 500: server error - - -Export a container -****************** - -.. http:get:: /containers/(id)/export - - Export the contents of container ``id`` - - **Example request**: - - .. sourcecode:: http - - GET /containers/4fa6e0f0c678/export HTTP/1.1 - - - **Example response**: - - .. sourcecode:: http - - HTTP/1.1 200 OK - Content-Type: application/octet-stream - - {{ STREAM }} - - :statuscode 200: no error - :statuscode 404: no such container - :statuscode 500: server error - - -Start a container -***************** - -.. http:post:: /containers/(id)/start - - Start the container ``id`` - - **Example request**: - - .. sourcecode:: http - - POST /containers/e90e34656806/start HTTP/1.1 - - **Example response**: - - .. sourcecode:: http - - HTTP/1.1 200 OK - - :statuscode 200: no error - :statuscode 404: no such container - :statuscode 500: server error - - -Stop a contaier -*************** - -.. http:post:: /containers/(id)/stop - - Stop the container ``id`` - - **Example request**: - - .. sourcecode:: http - - POST /containers/e90e34656806/stop?t=5 HTTP/1.1 - - **Example response**: - - .. sourcecode:: http - - HTTP/1.1 204 OK - - :query t: number of seconds to wait before killing the container - :statuscode 204: no error - :statuscode 404: no such container - :statuscode 500: server error - - -Restart a container -******************* - -.. http:post:: /containers/(id)/restart - - Restart the container ``id`` - - **Example request**: - - .. sourcecode:: http - - POST /containers/e90e34656806/restart?t=5 HTTP/1.1 - - **Example response**: - - .. sourcecode:: http - - HTTP/1.1 204 OK - - :query t: number of seconds to wait before killing the container - :statuscode 204: no error - :statuscode 404: no such container - :statuscode 500: server error - - -Kill a container -**************** - -.. http:post:: /containers/(id)/kill - - Kill the container ``id`` - - **Example request**: - - .. sourcecode:: http - - POST /containers/e90e34656806/kill HTTP/1.1 - - **Example response**: - - .. sourcecode:: http - - HTTP/1.1 204 OK - - :statuscode 204: no error - :statuscode 404: no such container - :statuscode 500: server error - - -Attach to a container -********************* - -.. http:post:: /containers/(id)/attach - - Attach to the container ``id`` - - **Example request**: - - .. sourcecode:: http - - POST /containers/16253994b7c4/attach?logs=1&stream=0&stdout=1 HTTP/1.1 - - **Example response**: - - .. sourcecode:: http - - HTTP/1.1 200 OK - Content-Type: application/vnd.docker.raw-stream - - {{ STREAM }} - - :query logs: 1/True/true or 0/False/false, return logs. Default false - :query stream: 1/True/true or 0/False/false, return stream. Default false - :query stdin: 1/True/true or 0/False/false, if stream=true, attach to stdin. Default false - :query stdout: 1/True/true or 0/False/false, if logs=true, return stdout log, if stream=true, attach to stdout. Default false - :query stderr: 1/True/true or 0/False/false, if logs=true, return stderr log, if stream=true, attach to stderr. Default false - :statuscode 200: no error - :statuscode 400: bad parameter - :statuscode 404: no such container - :statuscode 500: server error - - -Wait a container -**************** - -.. http:post:: /containers/(id)/wait - - Block until container ``id`` stops, then returns the exit code - - **Example request**: - - .. sourcecode:: http - - POST /containers/16253994b7c4/wait HTTP/1.1 - - **Example response**: - - .. sourcecode:: http - - HTTP/1.1 200 OK - Content-Type: application/json - - {"StatusCode":0} - - :statuscode 200: no error - :statuscode 404: no such container - :statuscode 500: server error - - -Remove a container -******************* - -.. http:delete:: /containers/(id) - - Remove the container ``id`` from the filesystem - - **Example request**: - - .. sourcecode:: http - - DELETE /containers/16253994b7c4?v=1 HTTP/1.1 - - **Example response**: - - .. sourcecode:: http - - HTTP/1.1 204 OK - - :query v: 1/True/true or 0/False/false, Remove the volumes associated to the container. Default false - :statuscode 204: no error - :statuscode 400: bad parameter - :statuscode 404: no such container - :statuscode 500: server error - - -3.2 Images ----------- - -List Images -*********** - -.. http:get:: /images/(format) - - List images ``format`` could be json or viz (json default) - - **Example request**: - - .. sourcecode:: http - - GET /images/json?all=0 HTTP/1.1 - - **Example response**: - - .. sourcecode:: http - - HTTP/1.1 200 OK - Content-Type: application/json - - [ - { - "Repository":"base", - "Tag":"ubuntu-12.10", - "Id":"b750fe79269d", - "Created":1364102658 - }, - { - "Repository":"base", - "Tag":"ubuntu-quantal", - "Id":"b750fe79269d", - "Created":1364102658 - } - ] - - - **Example request**: - - .. sourcecode:: http - - GET /images/viz HTTP/1.1 - - **Example response**: - - .. sourcecode:: http - - HTTP/1.1 200 OK - Content-Type: text/plain - - digraph docker { - "d82cbacda43a" -> "074be284591f" - "1496068ca813" -> "08306dc45919" - "08306dc45919" -> "0e7893146ac2" - "b750fe79269d" -> "1496068ca813" - base -> "27cf78414709" [style=invis] - "f71189fff3de" -> "9a33b36209ed" - "27cf78414709" -> "b750fe79269d" - "0e7893146ac2" -> "d6434d954665" - "d6434d954665" -> "d82cbacda43a" - base -> "e9aa60c60128" [style=invis] - "074be284591f" -> "f71189fff3de" - "b750fe79269d" [label="b750fe79269d\nbase",shape=box,fillcolor="paleturquoise",style="filled,rounded"]; - "e9aa60c60128" [label="e9aa60c60128\nbase2",shape=box,fillcolor="paleturquoise",style="filled,rounded"]; - "9a33b36209ed" [label="9a33b36209ed\ntest",shape=box,fillcolor="paleturquoise",style="filled,rounded"]; - base [style=invisible] - } - - :query all: 1/True/true or 0/False/false, Show all containers. Only running containers are shown by default - :statuscode 200: no error - :statuscode 400: bad parameter - :statuscode 500: server error - - -Create an image -*************** - -.. http:post:: /images/create - - Create an image, either by pull it from the registry or by importing it - - **Example request**: - - .. sourcecode:: http - - POST /images/create?fromImage=base HTTP/1.1 - - **Example response v1.1**: - - .. sourcecode:: http - - HTTP/1.1 200 OK - Content-Type: application/json - - {"status":"Pulling..."} - {"status":"Pulling", "progress":"1/? (n/a)"} - {"error":"Invalid..."} - ... - - **Example response v1.0**: - - .. sourcecode:: http - - HTTP/1.1 200 OK - Content-Type: application/vnd.docker.raw-stream - - {{ STREAM }} - - :query fromImage: name of the image to pull - :query fromSrc: source to import, - means stdin - :query repo: repository - :query tag: tag - :query registry: the registry to pull from - :statuscode 200: no error - :statuscode 500: server error - - -Insert a file in a image -************************ - -.. http:post:: /images/(name)/insert - - Insert a file from ``url`` in the image ``name`` at ``path`` - - **Example request**: - - .. sourcecode:: http - - POST /images/test/insert?path=/usr&url=myurl HTTP/1.1 - - **Example response v1.1**: - - .. sourcecode:: http - - HTTP/1.1 200 OK - Content-Type: application/json - - {"status":"Inserting..."} - {"status":"Inserting", "progress":"1/? (n/a)"} - {"error":"Invalid..."} - ... - - **Example response v1.0**: - - .. sourcecode:: http - - HTTP/1.1 200 OK - - {{ STREAM }} - - :statuscode 200: no error - :statuscode 500: server error - - -Inspect an image -**************** - -.. http:get:: /images/(name)/json - - Return low-level information on the image ``name`` - - **Example request**: - - .. sourcecode:: http - - GET /images/base/json HTTP/1.1 - - **Example response**: - - .. sourcecode:: http - - HTTP/1.1 200 OK - Content-Type: application/json - - { - "id":"b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", - "parent":"27cf784147099545", - "created":"2013-03-23T22:24:18.818426-07:00", - "container":"3d67245a8d72ecf13f33dffac9f79dcdf70f75acb84d308770391510e0c23ad0", - "container_config": - { - "Hostname":"", - "User":"", - "Memory":0, - "MemorySwap":0, - "AttachStdin":false, - "AttachStdout":false, - "AttachStderr":false, - "PortSpecs":null, - "Tty":true, - "OpenStdin":true, - "StdinOnce":false, - "Env":null, - "Cmd": ["/bin/bash"] - ,"Dns":null, - "Image":"base", - "Volumes":null, - "VolumesFrom":"" - } - } - - :statuscode 200: no error - :statuscode 404: no such image - :statuscode 500: server error - - -Get the history of an image -*************************** - -.. http:get:: /images/(name)/history - - Return the history of the image ``name`` - - **Example request**: - - .. sourcecode:: http - - GET /images/base/history HTTP/1.1 - - **Example response**: - - .. sourcecode:: http - - HTTP/1.1 200 OK - Content-Type: application/json - - [ - { - "Id":"b750fe79269d", - "Created":1364102658, - "CreatedBy":"/bin/bash" - }, - { - "Id":"27cf78414709", - "Created":1364068391, - "CreatedBy":"" - } - ] - - :statuscode 200: no error - :statuscode 404: no such image - :statuscode 500: server error - - -Push an image on the registry +:doc:`docker_remote_api_v1.2` ***************************** +What's new +---------- + +The auth configuration is now handled by the client. +The client should send it's authConfig as POST on each call of /images/(name)/push + +.. http:get:: /auth is now deprecated +.. http:post:: /auth only checks the configuration but doesn't store it on the server + +Deleting an image is now improved, will only untag the image if it has chidrens and remove all the untagged parents if has any. +.. http:post:: /images//delete now returns a JSON with the list of images deleted/untagged + + +:doc:`docker_remote_api_v1.1` +***************************** + +docker v0.4.0 a8ae398_ + +What's new +---------- + +.. http:post:: /images/create +.. http:post:: /images/(name)/insert .. http:post:: /images/(name)/push - Push the image ``name`` on the registry - - **Example request**: - - .. sourcecode:: http - - POST /images/test/push HTTP/1.1 - - **Example response v1.1**: +Uses json stream instead of HTML hijack, it looks like this: .. sourcecode:: http @@ -738,321 +63,38 @@ Push an image on the registry {"error":"Invalid..."} ... - **Example response v1.0**: - .. sourcecode:: http +docker v0.3.4 8d73740_ - HTTP/1.1 200 OK - Content-Type: application/vnd.docker.raw-stream +What's new +---------- - {{ STREAM }} +Initial version - :query registry: the registry you wan to push, optional - :statuscode 200: no error - :statuscode 404: no such image - :statuscode 500: server error +.. _a8ae398: https://github.com/dotcloud/docker/commit/a8ae398bf52e97148ee7bd0d5868de2e15bd297f +.. _8d73740: https://github.com/dotcloud/docker/commit/8d73740343778651c09160cde9661f5f387b36f4 -Tag an image into a repository -****************************** +================================== +Docker Remote API Client Libraries +================================== -.. http:post:: /images/(name)/tag +These libraries have been not tested by the Docker Maintainers for +compatibility. Please file issues with the library owners. If you +find more library implementations, please list them in Docker doc bugs +and we will add the libraries here. - Tag the image ``name`` into a repository - - **Example request**: - - .. sourcecode:: http - - POST /images/test/tag?repo=myrepo&force=0 HTTP/1.1 - - **Example response**: - - .. sourcecode:: http - - HTTP/1.1 200 OK - - :query repo: The repository to tag in - :query force: 1/True/true or 0/False/false, default false - :statuscode 200: no error - :statuscode 400: bad parameter - :statuscode 404: no such image - :statuscode 500: server error - - -Remove an image -*************** - -.. http:delete:: /images/(name) - - Remove the image ``name`` from the filesystem - - **Example request**: - - .. sourcecode:: http - - DELETE /images/test HTTP/1.1 - - **Example response**: - - .. sourcecode:: http - - HTTP/1.1 204 OK - - :statuscode 204: no error - :statuscode 404: no such image - :statuscode 500: server error - - -Search images -************* - -.. http:get:: /images/search - - Search for an image in the docker index - - **Example request**: - - .. sourcecode:: http - - GET /images/search?term=sshd HTTP/1.1 - - **Example response**: - - .. sourcecode:: http - - HTTP/1.1 200 OK - Content-Type: application/json - - [ - { - "Name":"cespare/sshd", - "Description":"" - }, - { - "Name":"johnfuller/sshd", - "Description":"" - }, - { - "Name":"dhrp/mongodb-sshd", - "Description":"" - } - ] - - :query term: term to search - :statuscode 200: no error - :statuscode 500: server error - - -3.3 Misc --------- - -Build an image from Dockerfile via stdin -**************************************** - -.. http:post:: /build - - Build an image from Dockerfile via stdin - - **Example request**: - - .. sourcecode:: http - - POST /build HTTP/1.1 - - {{ STREAM }} - - **Example response**: - - .. sourcecode:: http - - HTTP/1.1 200 OK - - {{ STREAM }} - - :query t: tag to be applied to the resulting image in case of success - :statuscode 200: no error - :statuscode 500: server error - - -Get default username and email -****************************** - -.. http:get:: /auth - - Get the default username and email - - **Example request**: - - .. sourcecode:: http - - GET /auth HTTP/1.1 - - **Example response**: - - .. sourcecode:: http - - HTTP/1.1 200 OK - Content-Type: application/json - - { - "username":"hannibal", - "email":"hannibal@a-team.com" - } - - :statuscode 200: no error - :statuscode 500: server error - - -Set auth configuration -********************** - -.. http:post:: /auth - - Get the default username and email - - **Example request**: - - .. sourcecode:: http - - POST /auth HTTP/1.1 - Content-Type: application/json - - { - "username":"hannibal", - "password:"xxxx", - "email":"hannibal@a-team.com" - } - - **Example response**: - - .. sourcecode:: http - - HTTP/1.1 200 OK - - :statuscode 200: no error - :statuscode 204: no error - :statuscode 500: server error - - -Display system-wide information -******************************* - -.. http:get:: /info - - Display system-wide information - - **Example request**: - - .. sourcecode:: http - - GET /info HTTP/1.1 - - **Example response**: - - .. sourcecode:: http - - HTTP/1.1 200 OK - Content-Type: application/json - - { - "Containers":11, - "Images":16, - "Debug":false, - "NFd": 11, - "NGoroutines":21, - "MemoryLimit":true, - "SwapLimit":false - } - - :statuscode 200: no error - :statuscode 500: server error - - -Show the docker version information -*********************************** - -.. http:get:: /version - - Show the docker version information - - **Example request**: - - .. sourcecode:: http - - GET /version HTTP/1.1 - - **Example response**: - - .. sourcecode:: http - - HTTP/1.1 200 OK - Content-Type: application/json - - { - "Version":"0.2.2", - "GitCommit":"5a2a5cc+CHANGES", - "GoVersion":"go1.0.3" - } - - :statuscode 200: no error - :statuscode 500: server error - - -Create a new image from a container's changes -********************************************* - -.. http:post:: /commit - - Create a new image from a container's changes - - **Example request**: - - .. sourcecode:: http - - POST /commit?container=44c004db4b17&m=message&repo=myrepo HTTP/1.1 - - **Example response**: - - .. sourcecode:: http - - HTTP/1.1 201 OK - Content-Type: application/vnd.docker.raw-stream - - {"Id":"596069db4bf5"} - - :query container: source container - :query repo: repository - :query tag: tag - :query m: commit message - :query author: author (eg. "John Hannibal Smith ") - :query run: config automatically applied when the image is run. (ex: {"Cmd": ["cat", "/world"], "PortSpecs":["22"]}) - :statuscode 201: no error - :statuscode 404: no such container - :statuscode 500: server error - - -3. Going further -================ - -3.1 Inside 'docker run' ------------------------ - -Here are the steps of 'docker run' : - -* Create the container -* If the status code is 404, it means the image doesn't exists: - * Try to pull it - * Then retry to create the container -* Start the container -* If you are not in detached mode: - * Attach to the container, using logs=1 (to have stdout and stderr from the container's start) and stream=1 -* If in detached mode or only stdin is attached: - * Display the container's id - - -3.2 Hijacking -------------- - -In this first version of the API, some of the endpoints, like /attach, /pull or /push uses hijacking to transport stdin, -stdout and stderr on the same socket. This might change in the future. ++----------------------+----------------+--------------------------------------------+ +| Language/Framework | Name | Repository | ++======================+================+============================================+ +| Python | docker-py | https://github.com/dotcloud/docker-py | ++----------------------+----------------+--------------------------------------------+ +| Ruby | docker-ruby | https://github.com/ActiveState/docker-ruby | ++----------------------+----------------+--------------------------------------------+ +| Ruby | docker-client | https://github.com/geku/docker-client | ++----------------------+----------------+--------------------------------------------+ +| Javascript | docker-js | https://github.com/dgoujard/docker-js | ++----------------------+----------------+--------------------------------------------+ +| Javascript (Angular) | dockerui | https://github.com/crosbymichael/dockerui | +| **WebUI** | | | ++----------------------+----------------+--------------------------------------------+ diff --git a/docs/sources/api/docker_remote_api_v1.0.rst b/docs/sources/api/docker_remote_api_v1.0.rst new file mode 100644 index 0000000000..a789337093 --- /dev/null +++ b/docs/sources/api/docker_remote_api_v1.0.rst @@ -0,0 +1,1015 @@ +:title: Remote API v1.0 +:description: API Documentation for Docker +:keywords: API, Docker, rcli, REST, documentation + +====================== +Docker Remote API v1.0 +====================== + +.. contents:: Table of Contents + +1. Brief introduction +===================== + +- The Remote API is replacing rcli +- Default port in the docker deamon is 4243 +- The API tends to be REST, but for some complex commands, like attach or pull, the HTTP connection is hijacked to transport stdout stdin and stderr + +2. Endpoints +============ + +2.1 Containers +-------------- + +List containers +*************** + +.. http:get:: /containers/json + + List containers + + **Example request**: + + .. sourcecode:: http + + GET /containers/json?all=1&before=8dfafdbc3a40 HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Id": "8dfafdbc3a40", + "Image": "base:latest", + "Command": "echo 1", + "Created": 1367854155, + "Status": "Exit 0" + }, + { + "Id": "9cd87474be90", + "Image": "base:latest", + "Command": "echo 222222", + "Created": 1367854155, + "Status": "Exit 0" + }, + { + "Id": "3176a2479c92", + "Image": "base:latest", + "Command": "echo 3333333333333333", + "Created": 1367854154, + "Status": "Exit 0" + }, + { + "Id": "4cb07b47f9fb", + "Image": "base:latest", + "Command": "echo 444444444444444444444444444444444", + "Created": 1367854152, + "Status": "Exit 0" + } + ] + + :query all: 1/True/true or 0/False/false, Show all containers. Only running containers are shown by default + :query limit: Show ``limit`` last created containers, include non-running ones. + :query since: Show only containers created since Id, include non-running ones. + :query before: Show only containers created before Id, include non-running ones. + :statuscode 200: no error + :statuscode 400: bad parameter + :statuscode 500: server error + + +Create a container +****************** + +.. http:post:: /containers/create + + Create a container + + **Example request**: + + .. sourcecode:: http + + POST /containers/create HTTP/1.1 + Content-Type: application/json + + { + "Hostname":"", + "User":"", + "Memory":0, + "MemorySwap":0, + "AttachStdin":false, + "AttachStdout":true, + "AttachStderr":true, + "PortSpecs":null, + "Tty":false, + "OpenStdin":false, + "StdinOnce":false, + "Env":null, + "Cmd":[ + "date" + ], + "Dns":null, + "Image":"base", + "Volumes":{}, + "VolumesFrom":"" + } + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 201 OK + Content-Type: application/json + + { + "Id":"e90e34656806" + "Warnings":[] + } + + :jsonparam config: the container's configuration + :statuscode 201: no error + :statuscode 404: no such container + :statuscode 406: impossible to attach (container not running) + :statuscode 500: server error + + +Inspect a container +******************* + +.. http:get:: /containers/(id)/json + + Return low-level information on the container ``id`` + + **Example request**: + + .. sourcecode:: http + + GET /containers/4fa6e0f0c678/json HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Id": "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2", + "Created": "2013-05-07T14:51:42.041847+02:00", + "Path": "date", + "Args": [], + "Config": { + "Hostname": "4fa6e0f0c678", + "User": "", + "Memory": 0, + "MemorySwap": 0, + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "PortSpecs": null, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": null, + "Cmd": [ + "date" + ], + "Dns": null, + "Image": "base", + "Volumes": {}, + "VolumesFrom": "" + }, + "State": { + "Running": false, + "Pid": 0, + "ExitCode": 0, + "StartedAt": "2013-05-07T14:51:42.087658+02:01360", + "Ghost": false + }, + "Image": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", + "NetworkSettings": { + "IpAddress": "", + "IpPrefixLen": 0, + "Gateway": "", + "Bridge": "", + "PortMapping": null + }, + "SysInitPath": "/home/kitty/go/src/github.com/dotcloud/docker/bin/docker", + "ResolvConfPath": "/etc/resolv.conf", + "Volumes": {} + } + + :statuscode 200: no error + :statuscode 404: no such container + :statuscode 500: server error + + +Inspect changes on a container's filesystem +******************************************* + +.. http:get:: /containers/(id)/changes + + Inspect changes on container ``id`` 's filesystem + + **Example request**: + + .. sourcecode:: http + + GET /containers/4fa6e0f0c678/changes HTTP/1.1 + + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Path":"/dev", + "Kind":0 + }, + { + "Path":"/dev/kmsg", + "Kind":1 + }, + { + "Path":"/test", + "Kind":1 + } + ] + + :statuscode 200: no error + :statuscode 404: no such container + :statuscode 500: server error + + +Export a container +****************** + +.. http:get:: /containers/(id)/export + + Export the contents of container ``id`` + + **Example request**: + + .. sourcecode:: http + + GET /containers/4fa6e0f0c678/export HTTP/1.1 + + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/octet-stream + + {{ STREAM }} + + :statuscode 200: no error + :statuscode 404: no such container + :statuscode 500: server error + + +Start a container +***************** + +.. http:post:: /containers/(id)/start + + Start the container ``id`` + + **Example request**: + + .. sourcecode:: http + + POST /containers/e90e34656806/start HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + + :statuscode 200: no error + :statuscode 404: no such container + :statuscode 500: server error + + +Stop a contaier +*************** + +.. http:post:: /containers/(id)/stop + + Stop the container ``id`` + + **Example request**: + + .. sourcecode:: http + + POST /containers/e90e34656806/stop?t=5 HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 204 OK + + :query t: number of seconds to wait before killing the container + :statuscode 204: no error + :statuscode 404: no such container + :statuscode 500: server error + + +Restart a container +******************* + +.. http:post:: /containers/(id)/restart + + Restart the container ``id`` + + **Example request**: + + .. sourcecode:: http + + POST /containers/e90e34656806/restart?t=5 HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 204 OK + + :query t: number of seconds to wait before killing the container + :statuscode 204: no error + :statuscode 404: no such container + :statuscode 500: server error + + +Kill a container +**************** + +.. http:post:: /containers/(id)/kill + + Kill the container ``id`` + + **Example request**: + + .. sourcecode:: http + + POST /containers/e90e34656806/kill HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 204 OK + + :statuscode 204: no error + :statuscode 404: no such container + :statuscode 500: server error + + +Attach to a container +********************* + +.. http:post:: /containers/(id)/attach + + Attach to the container ``id`` + + **Example request**: + + .. sourcecode:: http + + POST /containers/16253994b7c4/attach?logs=1&stream=0&stdout=1 HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/vnd.docker.raw-stream + + {{ STREAM }} + + :query logs: 1/True/true or 0/False/false, return logs. Default false + :query stream: 1/True/true or 0/False/false, return stream. Default false + :query stdin: 1/True/true or 0/False/false, if stream=true, attach to stdin. Default false + :query stdout: 1/True/true or 0/False/false, if logs=true, return stdout log, if stream=true, attach to stdout. Default false + :query stderr: 1/True/true or 0/False/false, if logs=true, return stderr log, if stream=true, attach to stderr. Default false + :statuscode 200: no error + :statuscode 400: bad parameter + :statuscode 404: no such container + :statuscode 500: server error + + +Wait a container +**************** + +.. http:post:: /containers/(id)/wait + + Block until container ``id`` stops, then returns the exit code + + **Example request**: + + .. sourcecode:: http + + POST /containers/16253994b7c4/wait HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + {"StatusCode":0} + + :statuscode 200: no error + :statuscode 404: no such container + :statuscode 500: server error + + +Remove a container +******************* + +.. http:delete:: /containers/(id) + + Remove the container ``id`` from the filesystem + + **Example request**: + + .. sourcecode:: http + + DELETE /containers/16253994b7c4?v=1 HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 204 OK + + :query v: 1/True/true or 0/False/false, Remove the volumes associated to the container. Default false + :statuscode 204: no error + :statuscode 400: bad parameter + :statuscode 404: no such container + :statuscode 500: server error + + +2.2 Images +---------- + +List Images +*********** + +.. http:get:: /images/(format) + + List images ``format`` could be json or viz (json default) + + **Example request**: + + .. sourcecode:: http + + GET /images/json?all=0 HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Repository":"base", + "Tag":"ubuntu-12.10", + "Id":"b750fe79269d", + "Created":1364102658 + }, + { + "Repository":"base", + "Tag":"ubuntu-quantal", + "Id":"b750fe79269d", + "Created":1364102658 + } + ] + + + **Example request**: + + .. sourcecode:: http + + GET /images/viz HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: text/plain + + digraph docker { + "d82cbacda43a" -> "074be284591f" + "1496068ca813" -> "08306dc45919" + "08306dc45919" -> "0e7893146ac2" + "b750fe79269d" -> "1496068ca813" + base -> "27cf78414709" [style=invis] + "f71189fff3de" -> "9a33b36209ed" + "27cf78414709" -> "b750fe79269d" + "0e7893146ac2" -> "d6434d954665" + "d6434d954665" -> "d82cbacda43a" + base -> "e9aa60c60128" [style=invis] + "074be284591f" -> "f71189fff3de" + "b750fe79269d" [label="b750fe79269d\nbase",shape=box,fillcolor="paleturquoise",style="filled,rounded"]; + "e9aa60c60128" [label="e9aa60c60128\nbase2",shape=box,fillcolor="paleturquoise",style="filled,rounded"]; + "9a33b36209ed" [label="9a33b36209ed\ntest",shape=box,fillcolor="paleturquoise",style="filled,rounded"]; + base [style=invisible] + } + + :query all: 1/True/true or 0/False/false, Show all containers. Only running containers are shown by default + :statuscode 200: no error + :statuscode 400: bad parameter + :statuscode 500: server error + + +Create an image +*************** + +.. http:post:: /images/create + + Create an image, either by pull it from the registry or by importing it + + **Example request**: + + .. sourcecode:: http + + POST /images/create?fromImage=base HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/vnd.docker.raw-stream + + {{ STREAM }} + + :query fromImage: name of the image to pull + :query fromSrc: source to import, - means stdin + :query repo: repository + :query tag: tag + :query registry: the registry to pull from + :statuscode 200: no error + :statuscode 500: server error + + +Insert a file in a image +************************ + +.. http:post:: /images/(name)/insert + + Insert a file from ``url`` in the image ``name`` at ``path`` + + **Example request**: + + .. sourcecode:: http + + POST /images/test/insert?path=/usr&url=myurl HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + + {{ STREAM }} + + :statuscode 200: no error + :statuscode 500: server error + + +Inspect an image +**************** + +.. http:get:: /images/(name)/json + + Return low-level information on the image ``name`` + + **Example request**: + + .. sourcecode:: http + + GET /images/base/json HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "id":"b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", + "parent":"27cf784147099545", + "created":"2013-03-23T22:24:18.818426-07:00", + "container":"3d67245a8d72ecf13f33dffac9f79dcdf70f75acb84d308770391510e0c23ad0", + "container_config": + { + "Hostname":"", + "User":"", + "Memory":0, + "MemorySwap":0, + "AttachStdin":false, + "AttachStdout":false, + "AttachStderr":false, + "PortSpecs":null, + "Tty":true, + "OpenStdin":true, + "StdinOnce":false, + "Env":null, + "Cmd": ["/bin/bash"] + ,"Dns":null, + "Image":"base", + "Volumes":null, + "VolumesFrom":"" + } + } + + :statuscode 200: no error + :statuscode 404: no such image + :statuscode 500: server error + + +Get the history of an image +*************************** + +.. http:get:: /images/(name)/history + + Return the history of the image ``name`` + + **Example request**: + + .. sourcecode:: http + + GET /images/base/history HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Id":"b750fe79269d", + "Created":1364102658, + "CreatedBy":"/bin/bash" + }, + { + "Id":"27cf78414709", + "Created":1364068391, + "CreatedBy":"" + } + ] + + :statuscode 200: no error + :statuscode 404: no such image + :statuscode 500: server error + + +Push an image on the registry +***************************** + +.. http:post:: /images/(name)/push + + Push the image ``name`` on the registry + + **Example request**: + + .. sourcecode:: http + + POST /images/test/push HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/vnd.docker.raw-stream + + {{ STREAM }} + + :query registry: the registry you wan to push, optional + :statuscode 200: no error + :statuscode 404: no such image + :statuscode 500: server error + + +Tag an image into a repository +****************************** + +.. http:post:: /images/(name)/tag + + Tag the image ``name`` into a repository + + **Example request**: + + .. sourcecode:: http + + POST /images/test/tag?repo=myrepo&force=0 HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + + :query repo: The repository to tag in + :query force: 1/True/true or 0/False/false, default false + :statuscode 200: no error + :statuscode 400: bad parameter + :statuscode 404: no such image + :statuscode 500: server error + + +Remove an image +*************** + +.. http:delete:: /images/(name) + + Remove the image ``name`` from the filesystem + + **Example request**: + + .. sourcecode:: http + + DELETE /images/test HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 204 OK + + :statuscode 204: no error + :statuscode 404: no such image + :statuscode 500: server error + + +Search images +************* + +.. http:get:: /images/search + + Search for an image in the docker index + + **Example request**: + + .. sourcecode:: http + + GET /images/search?term=sshd HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Name":"cespare/sshd", + "Description":"" + }, + { + "Name":"johnfuller/sshd", + "Description":"" + }, + { + "Name":"dhrp/mongodb-sshd", + "Description":"" + } + ] + + :query term: term to search + :statuscode 200: no error + :statuscode 500: server error + + +2.3 Misc +-------- + +Build an image from Dockerfile via stdin +**************************************** + +.. http:post:: /build + + Build an image from Dockerfile via stdin + + **Example request**: + + .. sourcecode:: http + + POST /build HTTP/1.1 + + {{ STREAM }} + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + + {{ STREAM }} + + :query t: tag to be applied to the resulting image in case of success + :statuscode 200: no error + :statuscode 500: server error + + +Get default username and email +****************************** + +.. http:get:: /auth + + Get the default username and email + + **Example request**: + + .. sourcecode:: http + + GET /auth HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "username":"hannibal", + "email":"hannibal@a-team.com" + } + + :statuscode 200: no error + :statuscode 500: server error + + +Check auth configuration and store it +************************************* + +.. http:post:: /auth + + Get the default username and email + + **Example request**: + + .. sourcecode:: http + + POST /auth HTTP/1.1 + Content-Type: application/json + + { + "username":"hannibal", + "password:"xxxx", + "email":"hannibal@a-team.com" + } + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + + :statuscode 200: no error + :statuscode 204: no error + :statuscode 500: server error + + +Display system-wide information +******************************* + +.. http:get:: /info + + Display system-wide information + + **Example request**: + + .. sourcecode:: http + + GET /info HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Containers":11, + "Images":16, + "Debug":false, + "NFd": 11, + "NGoroutines":21, + "MemoryLimit":true, + "SwapLimit":false + } + + :statuscode 200: no error + :statuscode 500: server error + + +Show the docker version information +*********************************** + +.. http:get:: /version + + Show the docker version information + + **Example request**: + + .. sourcecode:: http + + GET /version HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Version":"0.2.2", + "GitCommit":"5a2a5cc+CHANGES", + "GoVersion":"go1.0.3" + } + + :statuscode 200: no error + :statuscode 500: server error + + +Create a new image from a container's changes +********************************************* + +.. http:post:: /commit + + Create a new image from a container's changes + + **Example request**: + + .. sourcecode:: http + + POST /commit?container=44c004db4b17&m=message&repo=myrepo HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 201 OK + Content-Type: application/vnd.docker.raw-stream + + {"Id":"596069db4bf5"} + + :query container: source container + :query repo: repository + :query tag: tag + :query m: commit message + :query author: author (eg. "John Hannibal Smith ") + :query run: config automatically applied when the image is run. (ex: {"Cmd": ["cat", "/world"], "PortSpecs":["22"]}) + :statuscode 201: no error + :statuscode 404: no such container + :statuscode 500: server error + + +3. Going further +================ + +3.1 Inside 'docker run' +----------------------- + +Here are the steps of 'docker run' : + +* Create the container +* If the status code is 404, it means the image doesn't exists: + * Try to pull it + * Then retry to create the container +* Start the container +* If you are not in detached mode: + * Attach to the container, using logs=1 (to have stdout and stderr from the container's start) and stream=1 +* If in detached mode or only stdin is attached: + * Display the container's id + + +3.2 Hijacking +------------- + +In this first version of the API, some of the endpoints, like /attach, /pull or /push uses hijacking to transport stdin, +stdout and stderr on the same socket. This might change in the future. diff --git a/docs/sources/api/docker_remote_api_v1.1.rst b/docs/sources/api/docker_remote_api_v1.1.rst new file mode 100644 index 0000000000..3e0ef34eba --- /dev/null +++ b/docs/sources/api/docker_remote_api_v1.1.rst @@ -0,0 +1,1026 @@ + +:title: Remote API v1.1 +:description: API Documentation for Docker +:keywords: API, Docker, rcli, REST, documentation + +====================== +Docker Remote API v1.1 +====================== + +.. contents:: Table of Contents + +1. Brief introduction +===================== + +- The Remote API is replacing rcli +- Default port in the docker deamon is 4243 +- The API tends to be REST, but for some complex commands, like attach or pull, the HTTP connection is hijacked to transport stdout stdin and stderr + +2. Endpoints +============ + +2.1 Containers +-------------- + +List containers +*************** + +.. http:get:: /containers/json + + List containers + + **Example request**: + + .. sourcecode:: http + + GET /containers/json?all=1&before=8dfafdbc3a40 HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Id": "8dfafdbc3a40", + "Image": "base:latest", + "Command": "echo 1", + "Created": 1367854155, + "Status": "Exit 0" + }, + { + "Id": "9cd87474be90", + "Image": "base:latest", + "Command": "echo 222222", + "Created": 1367854155, + "Status": "Exit 0" + }, + { + "Id": "3176a2479c92", + "Image": "base:latest", + "Command": "echo 3333333333333333", + "Created": 1367854154, + "Status": "Exit 0" + }, + { + "Id": "4cb07b47f9fb", + "Image": "base:latest", + "Command": "echo 444444444444444444444444444444444", + "Created": 1367854152, + "Status": "Exit 0" + } + ] + + :query all: 1/True/true or 0/False/false, Show all containers. Only running containers are shown by default + :query limit: Show ``limit`` last created containers, include non-running ones. + :query since: Show only containers created since Id, include non-running ones. + :query before: Show only containers created before Id, include non-running ones. + :statuscode 200: no error + :statuscode 400: bad parameter + :statuscode 500: server error + + +Create a container +****************** + +.. http:post:: /containers/create + + Create a container + + **Example request**: + + .. sourcecode:: http + + POST /containers/create HTTP/1.1 + Content-Type: application/json + + { + "Hostname":"", + "User":"", + "Memory":0, + "MemorySwap":0, + "AttachStdin":false, + "AttachStdout":true, + "AttachStderr":true, + "PortSpecs":null, + "Tty":false, + "OpenStdin":false, + "StdinOnce":false, + "Env":null, + "Cmd":[ + "date" + ], + "Dns":null, + "Image":"base", + "Volumes":{}, + "VolumesFrom":"" + } + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 201 OK + Content-Type: application/json + + { + "Id":"e90e34656806" + "Warnings":[] + } + + :jsonparam config: the container's configuration + :statuscode 201: no error + :statuscode 404: no such container + :statuscode 406: impossible to attach (container not running) + :statuscode 500: server error + + +Inspect a container +******************* + +.. http:get:: /containers/(id)/json + + Return low-level information on the container ``id`` + + **Example request**: + + .. sourcecode:: http + + GET /containers/4fa6e0f0c678/json HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Id": "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2", + "Created": "2013-05-07T14:51:42.041847+02:00", + "Path": "date", + "Args": [], + "Config": { + "Hostname": "4fa6e0f0c678", + "User": "", + "Memory": 0, + "MemorySwap": 0, + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "PortSpecs": null, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": null, + "Cmd": [ + "date" + ], + "Dns": null, + "Image": "base", + "Volumes": {}, + "VolumesFrom": "" + }, + "State": { + "Running": false, + "Pid": 0, + "ExitCode": 0, + "StartedAt": "2013-05-07T14:51:42.087658+02:01360", + "Ghost": false + }, + "Image": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", + "NetworkSettings": { + "IpAddress": "", + "IpPrefixLen": 0, + "Gateway": "", + "Bridge": "", + "PortMapping": null + }, + "SysInitPath": "/home/kitty/go/src/github.com/dotcloud/docker/bin/docker", + "ResolvConfPath": "/etc/resolv.conf", + "Volumes": {} + } + + :statuscode 200: no error + :statuscode 404: no such container + :statuscode 500: server error + + +Inspect changes on a container's filesystem +******************************************* + +.. http:get:: /containers/(id)/changes + + Inspect changes on container ``id`` 's filesystem + + **Example request**: + + .. sourcecode:: http + + GET /containers/4fa6e0f0c678/changes HTTP/1.1 + + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Path":"/dev", + "Kind":0 + }, + { + "Path":"/dev/kmsg", + "Kind":1 + }, + { + "Path":"/test", + "Kind":1 + } + ] + + :statuscode 200: no error + :statuscode 404: no such container + :statuscode 500: server error + + +Export a container +****************** + +.. http:get:: /containers/(id)/export + + Export the contents of container ``id`` + + **Example request**: + + .. sourcecode:: http + + GET /containers/4fa6e0f0c678/export HTTP/1.1 + + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/octet-stream + + {{ STREAM }} + + :statuscode 200: no error + :statuscode 404: no such container + :statuscode 500: server error + + +Start a container +***************** + +.. http:post:: /containers/(id)/start + + Start the container ``id`` + + **Example request**: + + .. sourcecode:: http + + POST /containers/e90e34656806/start HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + + :statuscode 200: no error + :statuscode 404: no such container + :statuscode 500: server error + + +Stop a contaier +*************** + +.. http:post:: /containers/(id)/stop + + Stop the container ``id`` + + **Example request**: + + .. sourcecode:: http + + POST /containers/e90e34656806/stop?t=5 HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 204 OK + + :query t: number of seconds to wait before killing the container + :statuscode 204: no error + :statuscode 404: no such container + :statuscode 500: server error + + +Restart a container +******************* + +.. http:post:: /containers/(id)/restart + + Restart the container ``id`` + + **Example request**: + + .. sourcecode:: http + + POST /containers/e90e34656806/restart?t=5 HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 204 OK + + :query t: number of seconds to wait before killing the container + :statuscode 204: no error + :statuscode 404: no such container + :statuscode 500: server error + + +Kill a container +**************** + +.. http:post:: /containers/(id)/kill + + Kill the container ``id`` + + **Example request**: + + .. sourcecode:: http + + POST /containers/e90e34656806/kill HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 204 OK + + :statuscode 204: no error + :statuscode 404: no such container + :statuscode 500: server error + + +Attach to a container +********************* + +.. http:post:: /containers/(id)/attach + + Attach to the container ``id`` + + **Example request**: + + .. sourcecode:: http + + POST /containers/16253994b7c4/attach?logs=1&stream=0&stdout=1 HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/vnd.docker.raw-stream + + {{ STREAM }} + + :query logs: 1/True/true or 0/False/false, return logs. Default false + :query stream: 1/True/true or 0/False/false, return stream. Default false + :query stdin: 1/True/true or 0/False/false, if stream=true, attach to stdin. Default false + :query stdout: 1/True/true or 0/False/false, if logs=true, return stdout log, if stream=true, attach to stdout. Default false + :query stderr: 1/True/true or 0/False/false, if logs=true, return stderr log, if stream=true, attach to stderr. Default false + :statuscode 200: no error + :statuscode 400: bad parameter + :statuscode 404: no such container + :statuscode 500: server error + + +Wait a container +**************** + +.. http:post:: /containers/(id)/wait + + Block until container ``id`` stops, then returns the exit code + + **Example request**: + + .. sourcecode:: http + + POST /containers/16253994b7c4/wait HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + {"StatusCode":0} + + :statuscode 200: no error + :statuscode 404: no such container + :statuscode 500: server error + + +Remove a container +******************* + +.. http:delete:: /containers/(id) + + Remove the container ``id`` from the filesystem + + **Example request**: + + .. sourcecode:: http + + DELETE /containers/16253994b7c4?v=1 HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 204 OK + + :query v: 1/True/true or 0/False/false, Remove the volumes associated to the container. Default false + :statuscode 204: no error + :statuscode 400: bad parameter + :statuscode 404: no such container + :statuscode 500: server error + + +2.2 Images +---------- + +List Images +*********** + +.. http:get:: /images/(format) + + List images ``format`` could be json or viz (json default) + + **Example request**: + + .. sourcecode:: http + + GET /images/json?all=0 HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Repository":"base", + "Tag":"ubuntu-12.10", + "Id":"b750fe79269d", + "Created":1364102658 + }, + { + "Repository":"base", + "Tag":"ubuntu-quantal", + "Id":"b750fe79269d", + "Created":1364102658 + } + ] + + + **Example request**: + + .. sourcecode:: http + + GET /images/viz HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: text/plain + + digraph docker { + "d82cbacda43a" -> "074be284591f" + "1496068ca813" -> "08306dc45919" + "08306dc45919" -> "0e7893146ac2" + "b750fe79269d" -> "1496068ca813" + base -> "27cf78414709" [style=invis] + "f71189fff3de" -> "9a33b36209ed" + "27cf78414709" -> "b750fe79269d" + "0e7893146ac2" -> "d6434d954665" + "d6434d954665" -> "d82cbacda43a" + base -> "e9aa60c60128" [style=invis] + "074be284591f" -> "f71189fff3de" + "b750fe79269d" [label="b750fe79269d\nbase",shape=box,fillcolor="paleturquoise",style="filled,rounded"]; + "e9aa60c60128" [label="e9aa60c60128\nbase2",shape=box,fillcolor="paleturquoise",style="filled,rounded"]; + "9a33b36209ed" [label="9a33b36209ed\ntest",shape=box,fillcolor="paleturquoise",style="filled,rounded"]; + base [style=invisible] + } + + :query all: 1/True/true or 0/False/false, Show all containers. Only running containers are shown by default + :statuscode 200: no error + :statuscode 400: bad parameter + :statuscode 500: server error + + +Create an image +*************** + +.. http:post:: /images/create + + Create an image, either by pull it from the registry or by importing it + + **Example request**: + + .. sourcecode:: http + + POST /images/create?fromImage=base HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + {"status":"Pulling..."} + {"status":"Pulling", "progress":"1/? (n/a)"} + {"error":"Invalid..."} + ... + + :query fromImage: name of the image to pull + :query fromSrc: source to import, - means stdin + :query repo: repository + :query tag: tag + :query registry: the registry to pull from + :statuscode 200: no error + :statuscode 500: server error + + +Insert a file in a image +************************ + +.. http:post:: /images/(name)/insert + + Insert a file from ``url`` in the image ``name`` at ``path`` + + **Example request**: + + .. sourcecode:: http + + POST /images/test/insert?path=/usr&url=myurl HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + {"status":"Inserting..."} + {"status":"Inserting", "progress":"1/? (n/a)"} + {"error":"Invalid..."} + ... + + :statuscode 200: no error + :statuscode 500: server error + + +Inspect an image +**************** + +.. http:get:: /images/(name)/json + + Return low-level information on the image ``name`` + + **Example request**: + + .. sourcecode:: http + + GET /images/base/json HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "id":"b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", + "parent":"27cf784147099545", + "created":"2013-03-23T22:24:18.818426-07:00", + "container":"3d67245a8d72ecf13f33dffac9f79dcdf70f75acb84d308770391510e0c23ad0", + "container_config": + { + "Hostname":"", + "User":"", + "Memory":0, + "MemorySwap":0, + "AttachStdin":false, + "AttachStdout":false, + "AttachStderr":false, + "PortSpecs":null, + "Tty":true, + "OpenStdin":true, + "StdinOnce":false, + "Env":null, + "Cmd": ["/bin/bash"] + ,"Dns":null, + "Image":"base", + "Volumes":null, + "VolumesFrom":"" + } + } + + :statuscode 200: no error + :statuscode 404: no such image + :statuscode 500: server error + + +Get the history of an image +*************************** + +.. http:get:: /images/(name)/history + + Return the history of the image ``name`` + + **Example request**: + + .. sourcecode:: http + + GET /images/base/history HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Id":"b750fe79269d", + "Created":1364102658, + "CreatedBy":"/bin/bash" + }, + { + "Id":"27cf78414709", + "Created":1364068391, + "CreatedBy":"" + } + ] + + :statuscode 200: no error + :statuscode 404: no such image + :statuscode 500: server error + + +Push an image on the registry +***************************** + +.. http:post:: /images/(name)/push + + Push the image ``name`` on the registry + + **Example request**: + + .. sourcecode:: http + + POST /images/test/push HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + {"status":"Pushing..."} + {"status":"Pushing", "progress":"1/? (n/a)"} + {"error":"Invalid..."} + ... + + :query registry: the registry you wan to push, optional + :statuscode 200: no error + :statuscode 404: no such image + :statuscode 500: server error + + +Tag an image into a repository +****************************** + +.. http:post:: /images/(name)/tag + + Tag the image ``name`` into a repository + + **Example request**: + + .. sourcecode:: http + + POST /images/test/tag?repo=myrepo&force=0 HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + + :query repo: The repository to tag in + :query force: 1/True/true or 0/False/false, default false + :statuscode 200: no error + :statuscode 400: bad parameter + :statuscode 404: no such image + :statuscode 409: conflict + :statuscode 500: server error + + +Remove an image +*************** + +.. http:delete:: /images/(name) + + Remove the image ``name`` from the filesystem + + **Example request**: + + .. sourcecode:: http + + DELETE /images/test HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 204 OK + + :statuscode 204: no error + :statuscode 404: no such image + :statuscode 500: server error + + +Search images +************* + +.. http:get:: /images/search + + Search for an image in the docker index + + **Example request**: + + .. sourcecode:: http + + GET /images/search?term=sshd HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Name":"cespare/sshd", + "Description":"" + }, + { + "Name":"johnfuller/sshd", + "Description":"" + }, + { + "Name":"dhrp/mongodb-sshd", + "Description":"" + } + ] + + :query term: term to search + :statuscode 200: no error + :statuscode 500: server error + + +2.3 Misc +-------- + +Build an image from Dockerfile via stdin +**************************************** + +.. http:post:: /build + + Build an image from Dockerfile via stdin + + **Example request**: + + .. sourcecode:: http + + POST /build HTTP/1.1 + + {{ STREAM }} + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + + {{ STREAM }} + + :query t: tag to be applied to the resulting image in case of success + :statuscode 200: no error + :statuscode 500: server error + + +Get default username and email +****************************** + +.. http:get:: /auth + + Get the default username and email + + **Example request**: + + .. sourcecode:: http + + GET /auth HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "username":"hannibal", + "email":"hannibal@a-team.com" + } + + :statuscode 200: no error + :statuscode 500: server error + + +Check auth configuration and store it +************************************* + +.. http:post:: /auth + + Get the default username and email + + **Example request**: + + .. sourcecode:: http + + POST /auth HTTP/1.1 + Content-Type: application/json + + { + "username":"hannibal", + "password:"xxxx", + "email":"hannibal@a-team.com" + } + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + + :statuscode 200: no error + :statuscode 204: no error + :statuscode 500: server error + + +Display system-wide information +******************************* + +.. http:get:: /info + + Display system-wide information + + **Example request**: + + .. sourcecode:: http + + GET /info HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Containers":11, + "Images":16, + "Debug":false, + "NFd": 11, + "NGoroutines":21, + "MemoryLimit":true, + "SwapLimit":false + } + + :statuscode 200: no error + :statuscode 500: server error + + +Show the docker version information +*********************************** + +.. http:get:: /version + + Show the docker version information + + **Example request**: + + .. sourcecode:: http + + GET /version HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Version":"0.2.2", + "GitCommit":"5a2a5cc+CHANGES", + "GoVersion":"go1.0.3" + } + + :statuscode 200: no error + :statuscode 500: server error + + +Create a new image from a container's changes +********************************************* + +.. http:post:: /commit + + Create a new image from a container's changes + + **Example request**: + + .. sourcecode:: http + + POST /commit?container=44c004db4b17&m=message&repo=myrepo HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 201 OK + Content-Type: application/vnd.docker.raw-stream + + {"Id":"596069db4bf5"} + + :query container: source container + :query repo: repository + :query tag: tag + :query m: commit message + :query author: author (eg. "John Hannibal Smith ") + :query run: config automatically applied when the image is run. (ex: {"Cmd": ["cat", "/world"], "PortSpecs":["22"]}) + :statuscode 201: no error + :statuscode 404: no such container + :statuscode 500: server error + + +3. Going further +================ + +3.1 Inside 'docker run' +----------------------- + +Here are the steps of 'docker run' : + +* Create the container +* If the status code is 404, it means the image doesn't exists: + * Try to pull it + * Then retry to create the container +* Start the container +* If you are not in detached mode: + * Attach to the container, using logs=1 (to have stdout and stderr from the container's start) and stream=1 +* If in detached mode or only stdin is attached: + * Display the container's id + + +3.2 Hijacking +------------- + +In this version of the API, /attach uses hijacking to transport stdin, stdout and stderr on the same socket. This might change in the future. diff --git a/docs/sources/api/docker_remote_api_v1.2.rst b/docs/sources/api/docker_remote_api_v1.2.rst new file mode 100644 index 0000000000..fb69168120 --- /dev/null +++ b/docs/sources/api/docker_remote_api_v1.2.rst @@ -0,0 +1,1030 @@ +:title: Remote API v1.2 +:description: API Documentation for Docker +:keywords: API, Docker, rcli, REST, documentation + +====================== +Docker Remote API v1.2 +====================== + +.. contents:: Table of Contents + +1. Brief introduction +===================== + +- The Remote API is replacing rcli +- Default port in the docker deamon is 4243 +- The API tends to be REST, but for some complex commands, like attach or pull, the HTTP connection is hijacked to transport stdout stdin and stderr + +2. Endpoints +============ + +2.1 Containers +-------------- + +List containers +*************** + +.. http:get:: /containers/json + + List containers + + **Example request**: + + .. sourcecode:: http + + GET /containers/json?all=1&before=8dfafdbc3a40 HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Id": "8dfafdbc3a40", + "Image": "base:latest", + "Command": "echo 1", + "Created": 1367854155, + "Status": "Exit 0", + "Ports":"", + "SizeRw":12288, + "SizeRootFs":0 + }, + { + "Id": "9cd87474be90", + "Image": "base:latest", + "Command": "echo 222222", + "Created": 1367854155, + "Status": "Exit 0", + "Ports":"", + "SizeRw":12288, + "SizeRootFs":0 + }, + { + "Id": "3176a2479c92", + "Image": "base:latest", + "Command": "echo 3333333333333333", + "Created": 1367854154, + "Status": "Exit 0", + "Ports":"", + "SizeRw":12288, + "SizeRootFs":0 + }, + { + "Id": "4cb07b47f9fb", + "Image": "base:latest", + "Command": "echo 444444444444444444444444444444444", + "Created": 1367854152, + "Status": "Exit 0", + "Ports":"", + "SizeRw":12288, + "SizeRootFs":0 + } + ] + + :query all: 1/True/true or 0/False/false, Show all containers. Only running containers are shown by default + :query limit: Show ``limit`` last created containers, include non-running ones. + :query since: Show only containers created since Id, include non-running ones. + :query before: Show only containers created before Id, include non-running ones. + :statuscode 200: no error + :statuscode 400: bad parameter + :statuscode 500: server error + + +Create a container +****************** + +.. http:post:: /containers/create + + Create a container + + **Example request**: + + .. sourcecode:: http + + POST /containers/create HTTP/1.1 + Content-Type: application/json + + { + "Hostname":"", + "User":"", + "Memory":0, + "MemorySwap":0, + "AttachStdin":false, + "AttachStdout":true, + "AttachStderr":true, + "PortSpecs":null, + "Tty":false, + "OpenStdin":false, + "StdinOnce":false, + "Env":null, + "Cmd":[ + "date" + ], + "Dns":null, + "Image":"base", + "Volumes":{}, + "VolumesFrom":"" + } + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 201 OK + Content-Type: application/json + + { + "Id":"e90e34656806" + "Warnings":[] + } + + :jsonparam config: the container's configuration + :statuscode 201: no error + :statuscode 404: no such container + :statuscode 406: impossible to attach (container not running) + :statuscode 500: server error + + +Inspect a container +******************* + +.. http:get:: /containers/(id)/json + + Return low-level information on the container ``id`` + + **Example request**: + + .. sourcecode:: http + + GET /containers/4fa6e0f0c678/json HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Id": "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2", + "Created": "2013-05-07T14:51:42.041847+02:00", + "Path": "date", + "Args": [], + "Config": { + "Hostname": "4fa6e0f0c678", + "User": "", + "Memory": 0, + "MemorySwap": 0, + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "PortSpecs": null, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": null, + "Cmd": [ + "date" + ], + "Dns": null, + "Image": "base", + "Volumes": {}, + "VolumesFrom": "" + }, + "State": { + "Running": false, + "Pid": 0, + "ExitCode": 0, + "StartedAt": "2013-05-07T14:51:42.087658+02:01360", + "Ghost": false + }, + "Image": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", + "NetworkSettings": { + "IpAddress": "", + "IpPrefixLen": 0, + "Gateway": "", + "Bridge": "", + "PortMapping": null + }, + "SysInitPath": "/home/kitty/go/src/github.com/dotcloud/docker/bin/docker", + "ResolvConfPath": "/etc/resolv.conf", + "Volumes": {} + } + + :statuscode 200: no error + :statuscode 404: no such container + :statuscode 500: server error + + +Inspect changes on a container's filesystem +******************************************* + +.. http:get:: /containers/(id)/changes + + Inspect changes on container ``id`` 's filesystem + + **Example request**: + + .. sourcecode:: http + + GET /containers/4fa6e0f0c678/changes HTTP/1.1 + + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Path":"/dev", + "Kind":0 + }, + { + "Path":"/dev/kmsg", + "Kind":1 + }, + { + "Path":"/test", + "Kind":1 + } + ] + + :statuscode 200: no error + :statuscode 404: no such container + :statuscode 500: server error + + +Export a container +****************** + +.. http:get:: /containers/(id)/export + + Export the contents of container ``id`` + + **Example request**: + + .. sourcecode:: http + + GET /containers/4fa6e0f0c678/export HTTP/1.1 + + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/octet-stream + + {{ STREAM }} + + :statuscode 200: no error + :statuscode 404: no such container + :statuscode 500: server error + + +Start a container +***************** + +.. http:post:: /containers/(id)/start + + Start the container ``id`` + + **Example request**: + + .. sourcecode:: http + + POST /containers/e90e34656806/start HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + + :statuscode 200: no error + :statuscode 404: no such container + :statuscode 500: server error + + +Stop a contaier +*************** + +.. http:post:: /containers/(id)/stop + + Stop the container ``id`` + + **Example request**: + + .. sourcecode:: http + + POST /containers/e90e34656806/stop?t=5 HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 204 OK + + :query t: number of seconds to wait before killing the container + :statuscode 204: no error + :statuscode 404: no such container + :statuscode 500: server error + + +Restart a container +******************* + +.. http:post:: /containers/(id)/restart + + Restart the container ``id`` + + **Example request**: + + .. sourcecode:: http + + POST /containers/e90e34656806/restart?t=5 HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 204 OK + + :query t: number of seconds to wait before killing the container + :statuscode 204: no error + :statuscode 404: no such container + :statuscode 500: server error + + +Kill a container +**************** + +.. http:post:: /containers/(id)/kill + + Kill the container ``id`` + + **Example request**: + + .. sourcecode:: http + + POST /containers/e90e34656806/kill HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 204 OK + + :statuscode 204: no error + :statuscode 404: no such container + :statuscode 500: server error + + +Attach to a container +********************* + +.. http:post:: /containers/(id)/attach + + Attach to the container ``id`` + + **Example request**: + + .. sourcecode:: http + + POST /containers/16253994b7c4/attach?logs=1&stream=0&stdout=1 HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/vnd.docker.raw-stream + + {{ STREAM }} + + :query logs: 1/True/true or 0/False/false, return logs. Default false + :query stream: 1/True/true or 0/False/false, return stream. Default false + :query stdin: 1/True/true or 0/False/false, if stream=true, attach to stdin. Default false + :query stdout: 1/True/true or 0/False/false, if logs=true, return stdout log, if stream=true, attach to stdout. Default false + :query stderr: 1/True/true or 0/False/false, if logs=true, return stderr log, if stream=true, attach to stderr. Default false + :statuscode 200: no error + :statuscode 400: bad parameter + :statuscode 404: no such container + :statuscode 500: server error + + +Wait a container +**************** + +.. http:post:: /containers/(id)/wait + + Block until container ``id`` stops, then returns the exit code + + **Example request**: + + .. sourcecode:: http + + POST /containers/16253994b7c4/wait HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + {"StatusCode":0} + + :statuscode 200: no error + :statuscode 404: no such container + :statuscode 500: server error + + +Remove a container +******************* + +.. http:delete:: /containers/(id) + + Remove the container ``id`` from the filesystem + + **Example request**: + + .. sourcecode:: http + + DELETE /containers/16253994b7c4?v=1 HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 204 OK + + :query v: 1/True/true or 0/False/false, Remove the volumes associated to the container. Default false + :statuscode 204: no error + :statuscode 400: bad parameter + :statuscode 404: no such container + :statuscode 500: server error + + +2.2 Images +---------- + +List Images +*********** + +.. http:get:: /images/(format) + + List images ``format`` could be json or viz (json default) + + **Example request**: + + .. sourcecode:: http + + GET /images/json?all=0 HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Repository":"base", + "Tag":"ubuntu-12.10", + "Id":"b750fe79269d", + "Created":1364102658, + "Size":24653, + "VirtualSize":180116135 + }, + { + "Repository":"base", + "Tag":"ubuntu-quantal", + "Id":"b750fe79269d", + "Created":1364102658, + "Size":24653, + "VirtualSize":180116135 + } + ] + + + **Example request**: + + .. sourcecode:: http + + GET /images/viz HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: text/plain + + digraph docker { + "d82cbacda43a" -> "074be284591f" + "1496068ca813" -> "08306dc45919" + "08306dc45919" -> "0e7893146ac2" + "b750fe79269d" -> "1496068ca813" + base -> "27cf78414709" [style=invis] + "f71189fff3de" -> "9a33b36209ed" + "27cf78414709" -> "b750fe79269d" + "0e7893146ac2" -> "d6434d954665" + "d6434d954665" -> "d82cbacda43a" + base -> "e9aa60c60128" [style=invis] + "074be284591f" -> "f71189fff3de" + "b750fe79269d" [label="b750fe79269d\nbase",shape=box,fillcolor="paleturquoise",style="filled,rounded"]; + "e9aa60c60128" [label="e9aa60c60128\nbase2",shape=box,fillcolor="paleturquoise",style="filled,rounded"]; + "9a33b36209ed" [label="9a33b36209ed\ntest",shape=box,fillcolor="paleturquoise",style="filled,rounded"]; + base [style=invisible] + } + + :query all: 1/True/true or 0/False/false, Show all containers. Only running containers are shown by default + :statuscode 200: no error + :statuscode 400: bad parameter + :statuscode 500: server error + + +Create an image +*************** + +.. http:post:: /images/create + + Create an image, either by pull it from the registry or by importing it + + **Example request**: + + .. sourcecode:: http + + POST /images/create?fromImage=base HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + {"status":"Pulling..."} + {"status":"Pulling", "progress":"1/? (n/a)"} + {"error":"Invalid..."} + ... + + :query fromImage: name of the image to pull + :query fromSrc: source to import, - means stdin + :query repo: repository + :query tag: tag + :query registry: the registry to pull from + :statuscode 200: no error + :statuscode 500: server error + + +Insert a file in a image +************************ + +.. http:post:: /images/(name)/insert + + Insert a file from ``url`` in the image ``name`` at ``path`` + + **Example request**: + + .. sourcecode:: http + + POST /images/test/insert?path=/usr&url=myurl HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + {"status":"Inserting..."} + {"status":"Inserting", "progress":"1/? (n/a)"} + {"error":"Invalid..."} + ... + + :statuscode 200: no error + :statuscode 500: server error + + +Inspect an image +**************** + +.. http:get:: /images/(name)/json + + Return low-level information on the image ``name`` + + **Example request**: + + .. sourcecode:: http + + GET /images/base/json HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "id":"b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", + "parent":"27cf784147099545", + "created":"2013-03-23T22:24:18.818426-07:00", + "container":"3d67245a8d72ecf13f33dffac9f79dcdf70f75acb84d308770391510e0c23ad0", + "container_config": + { + "Hostname":"", + "User":"", + "Memory":0, + "MemorySwap":0, + "AttachStdin":false, + "AttachStdout":false, + "AttachStderr":false, + "PortSpecs":null, + "Tty":true, + "OpenStdin":true, + "StdinOnce":false, + "Env":null, + "Cmd": ["/bin/bash"] + ,"Dns":null, + "Image":"base", + "Volumes":null, + "VolumesFrom":"" + }, + "Size": 6824592 + } + + :statuscode 200: no error + :statuscode 404: no such image + :statuscode 500: server error + + +Get the history of an image +*************************** + +.. http:get:: /images/(name)/history + + Return the history of the image ``name`` + + **Example request**: + + .. sourcecode:: http + + GET /images/base/history HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Id":"b750fe79269d", + "Created":1364102658, + "CreatedBy":"/bin/bash" + }, + { + "Id":"27cf78414709", + "Created":1364068391, + "CreatedBy":"" + } + ] + + :statuscode 200: no error + :statuscode 404: no such image + :statuscode 500: server error + + +Push an image on the registry +***************************** + +.. http:post:: /images/(name)/push + + Push the image ``name`` on the registry + + **Example request**: + + .. sourcecode:: http + + POST /images/test/push HTTP/1.1 + {{ authConfig }} + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + {"status":"Pushing..."} + {"status":"Pushing", "progress":"1/? (n/a)"} + {"error":"Invalid..."} + ... + + :query registry: the registry you wan to push, optional + :statuscode 200: no error + :statuscode 404: no such image + :statuscode 500: server error + + +Tag an image into a repository +****************************** + +.. http:post:: /images/(name)/tag + + Tag the image ``name`` into a repository + + **Example request**: + + .. sourcecode:: http + + POST /images/test/tag?repo=myrepo&force=0 HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + + :query repo: The repository to tag in + :query force: 1/True/true or 0/False/false, default false + :statuscode 200: no error + :statuscode 400: bad parameter + :statuscode 404: no such image + :statuscode 409: conflict + :statuscode 500: server error + + +Remove an image +*************** + +.. http:delete:: /images/(name) + + Remove the image ``name`` from the filesystem + + **Example request**: + + .. sourcecode:: http + + DELETE /images/test HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-type: application/json + + [ + {"Untagged":"3e2f21a89f"}, + {"Deleted":"3e2f21a89f"}, + {"Deleted":"53b4f83ac9"} + ] + + :statuscode 204: no error + :statuscode 404: no such image + :statuscode 409: conflict + :statuscode 500: server error + + +Search images +************* + +.. http:get:: /images/search + + Search for an image in the docker index + + **Example request**: + + .. sourcecode:: http + + GET /images/search?term=sshd HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Name":"cespare/sshd", + "Description":"" + }, + { + "Name":"johnfuller/sshd", + "Description":"" + }, + { + "Name":"dhrp/mongodb-sshd", + "Description":"" + } + ] + + :query term: term to search + :statuscode 200: no error + :statuscode 500: server error + + +2.3 Misc +-------- + +Build an image from Dockerfile via stdin +**************************************** + +.. http:post:: /build + + Build an image from Dockerfile via stdin + + **Example request**: + + .. sourcecode:: http + + POST /build HTTP/1.1 + + {{ STREAM }} + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + + {{ STREAM }} + + :query t: tag to be applied to the resulting image in case of success + :statuscode 200: no error + :statuscode 500: server error + + +Check auth configuration +************************ + +.. http:post:: /auth + + Get the default username and email + + **Example request**: + + .. sourcecode:: http + + POST /auth HTTP/1.1 + Content-Type: application/json + + { + "username":"hannibal", + "password:"xxxx", + "email":"hannibal@a-team.com" + } + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + + :statuscode 200: no error + :statuscode 204: no error + :statuscode 500: server error + + +Display system-wide information +******************************* + +.. http:get:: /info + + Display system-wide information + + **Example request**: + + .. sourcecode:: http + + GET /info HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Containers":11, + "Images":16, + "Debug":false, + "NFd": 11, + "NGoroutines":21, + "MemoryLimit":true, + "SwapLimit":false + } + + :statuscode 200: no error + :statuscode 500: server error + + +Show the docker version information +*********************************** + +.. http:get:: /version + + Show the docker version information + + **Example request**: + + .. sourcecode:: http + + GET /version HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Version":"0.2.2", + "GitCommit":"5a2a5cc+CHANGES", + "GoVersion":"go1.0.3" + } + + :statuscode 200: no error + :statuscode 500: server error + + +Create a new image from a container's changes +********************************************* + +.. http:post:: /commit + + Create a new image from a container's changes + + **Example request**: + + .. sourcecode:: http + + POST /commit?container=44c004db4b17&m=message&repo=myrepo HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 201 OK + Content-Type: application/vnd.docker.raw-stream + + {"Id":"596069db4bf5"} + + :query container: source container + :query repo: repository + :query tag: tag + :query m: commit message + :query author: author (eg. "John Hannibal Smith ") + :query run: config automatically applied when the image is run. (ex: {"Cmd": ["cat", "/world"], "PortSpecs":["22"]}) + :statuscode 201: no error + :statuscode 404: no such container + :statuscode 500: server error + + +3. Going further +================ + +3.1 Inside 'docker run' +----------------------- + +Here are the steps of 'docker run' : + +* Create the container +* If the status code is 404, it means the image doesn't exists: + * Try to pull it + * Then retry to create the container +* Start the container +* If you are not in detached mode: + * Attach to the container, using logs=1 (to have stdout and stderr from the container's start) and stream=1 +* If in detached mode or only stdin is attached: + * Display the container's id + + +3.2 Hijacking +------------- + +In this version of the API, /attach, uses hijacking to transport stdin, stdout and stderr on the same socket. This might change in the future. + +3.3 CORS Requests +----------------- + +To enable cross origin requests to the remote api add the flag "-api-enable-cors" when running docker in daemon mode. + + docker -d -H="192.168.1.9:4243" -api-enable-cors + diff --git a/docs/sources/api/index.rst b/docs/sources/api/index.rst index 85770f484e..d7345e8746 100644 --- a/docs/sources/api/index.rst +++ b/docs/sources/api/index.rst @@ -5,13 +5,14 @@ APIs ==== -This following : +Your programs and scripts can access Docker's functionality via these interfaces: .. toctree:: :maxdepth: 3 + registry_index_spec registry_api - index_search_api + index_api docker_remote_api diff --git a/docs/sources/api/index_api.rst b/docs/sources/api/index_api.rst new file mode 100644 index 0000000000..42dc49a5d7 --- /dev/null +++ b/docs/sources/api/index_api.rst @@ -0,0 +1,553 @@ +:title: Index API +:description: API Documentation for Docker Index +:keywords: API, Docker, index, REST, documentation + +================= +Docker Index API +================= + +.. contents:: Table of Contents + +1. Brief introduction +===================== + +- This is the REST API for the Docker index +- Authorization is done with basic auth over SSL +- Not all commands require authentication, only those noted as such. + +2. Endpoints +============ + +2.1 Repository +^^^^^^^^^^^^^^ + +Repositories +************* + +User Repo +~~~~~~~~~ + +.. http:put:: /v1/repositories/(namespace)/(repo_name)/ + + Create a user repository with the given ``namespace`` and ``repo_name``. + + **Example Request**: + + .. sourcecode:: http + + PUT /v1/repositories/foo/bar/ HTTP/1.1 + Host: index.docker.io + Accept: application/json + Content-Type: application/json + Authorization: Basic akmklmasadalkm== + X-Docker-Token: true + + [{“id”: “9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f”}] + + :parameter namespace: the namespace for the repo + :parameter repo_name: the name for the repo + + **Example Response**: + + .. sourcecode:: http + + HTTP/1.1 200 + Vary: Accept + Content-Type: application/json + WWW-Authenticate: Token signature=123abc,repository=”foo/bar”,access=write + X-Docker-Endpoints: registry-1.docker.io [, registry-2.docker.io] + + "" + + :statuscode 200: Created + :statuscode 400: Errors (invalid json, missing or invalid fields, etc) + :statuscode 401: Unauthorized + :statuscode 403: Account is not Active + + +.. http:delete:: /v1/repositories/(namespace)/(repo_name)/ + + Delete a user repository with the given ``namespace`` and ``repo_name``. + + **Example Request**: + + .. sourcecode:: http + + DELETE /v1/repositories/foo/bar/ HTTP/1.1 + Host: index.docker.io + Accept: application/json + Content-Type: application/json + Authorization: Basic akmklmasadalkm== + X-Docker-Token: true + + "" + + :parameter namespace: the namespace for the repo + :parameter repo_name: the name for the repo + + **Example Response**: + + .. sourcecode:: http + + HTTP/1.1 202 + Vary: Accept + Content-Type: application/json + WWW-Authenticate: Token signature=123abc,repository=”foo/bar”,access=delete + X-Docker-Endpoints: registry-1.docker.io [, registry-2.docker.io] + + "" + + :statuscode 200: Deleted + :statuscode 202: Accepted + :statuscode 400: Errors (invalid json, missing or invalid fields, etc) + :statuscode 401: Unauthorized + :statuscode 403: Account is not Active + +Library Repo +~~~~~~~~~~~~ + +.. http:put:: /v1/repositories/(repo_name)/ + + Create a library repository with the given ``repo_name``. + This is a restricted feature only available to docker admins. + + When namespace is missing, it is assumed to be ``library`` + + **Example Request**: + + .. sourcecode:: http + + PUT /v1/repositories/foobar/ HTTP/1.1 + Host: index.docker.io + Accept: application/json + Content-Type: application/json + Authorization: Basic akmklmasadalkm== + X-Docker-Token: true + + [{“id”: “9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f”}] + + :parameter repo_name: the library name for the repo + + **Example Response**: + + .. sourcecode:: http + + HTTP/1.1 200 + Vary: Accept + Content-Type: application/json + WWW-Authenticate: Token signature=123abc,repository=”library/foobar”,access=write + X-Docker-Endpoints: registry-1.docker.io [, registry-2.docker.io] + + "" + + :statuscode 200: Created + :statuscode 400: Errors (invalid json, missing or invalid fields, etc) + :statuscode 401: Unauthorized + :statuscode 403: Account is not Active + +.. http:delete:: /v1/repositories/(repo_name)/ + + Delete a library repository with the given ``repo_name``. + This is a restricted feature only available to docker admins. + + When namespace is missing, it is assumed to be ``library`` + + **Example Request**: + + .. sourcecode:: http + + DELETE /v1/repositories/foobar/ HTTP/1.1 + Host: index.docker.io + Accept: application/json + Content-Type: application/json + Authorization: Basic akmklmasadalkm== + X-Docker-Token: true + + "" + + :parameter repo_name: the library name for the repo + + **Example Response**: + + .. sourcecode:: http + + HTTP/1.1 202 + Vary: Accept + Content-Type: application/json + WWW-Authenticate: Token signature=123abc,repository=”library/foobar”,access=delete + X-Docker-Endpoints: registry-1.docker.io [, registry-2.docker.io] + + "" + + :statuscode 200: Deleted + :statuscode 202: Accepted + :statuscode 400: Errors (invalid json, missing or invalid fields, etc) + :statuscode 401: Unauthorized + :statuscode 403: Account is not Active + +Repository Images +***************** + +User Repo Images +~~~~~~~~~~~~~~~~ + +.. http:put:: /v1/repositories/(namespace)/(repo_name)/images + + Update the images for a user repo. + + **Example Request**: + + .. sourcecode:: http + + PUT /v1/repositories/foo/bar/images HTTP/1.1 + Host: index.docker.io + Accept: application/json + Content-Type: application/json + Authorization: Basic akmklmasadalkm== + + [{“id”: “9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f”, + “checksum”: “b486531f9a779a0c17e3ed29dae8f12c4f9e89cc6f0bc3c38722009fe6857087”}] + + :parameter namespace: the namespace for the repo + :parameter repo_name: the name for the repo + + **Example Response**: + + .. sourcecode:: http + + HTTP/1.1 204 + Vary: Accept + Content-Type: application/json + + "" + + :statuscode 204: Created + :statuscode 400: Errors (invalid json, missing or invalid fields, etc) + :statuscode 401: Unauthorized + :statuscode 403: Account is not Active or permission denied + + +.. http:get:: /v1/repositories/(namespace)/(repo_name)/images + + get the images for a user repo. + + **Example Request**: + + .. sourcecode:: http + + GET /v1/repositories/foo/bar/images HTTP/1.1 + Host: index.docker.io + Accept: application/json + + :parameter namespace: the namespace for the repo + :parameter repo_name: the name for the repo + + **Example Response**: + + .. sourcecode:: http + + HTTP/1.1 200 + Vary: Accept + Content-Type: application/json + + [{“id”: “9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f”, + “checksum”: “b486531f9a779a0c17e3ed29dae8f12c4f9e89cc6f0bc3c38722009fe6857087”}, + {“id”: “ertwetewtwe38722009fe6857087b486531f9a779a0c1dfddgfgsdgdsgds”, + “checksum”: “34t23f23fc17e3ed29dae8f12c4f9e89cc6f0bsdfgfsdgdsgdsgerwgew”}] + + :statuscode 200: OK + :statuscode 404: Not found + +Library Repo Images +~~~~~~~~~~~~~~~~~~~ + +.. http:put:: /v1/repositories/(repo_name)/images + + Update the images for a library repo. + + **Example Request**: + + .. sourcecode:: http + + PUT /v1/repositories/foobar/images HTTP/1.1 + Host: index.docker.io + Accept: application/json + Content-Type: application/json + Authorization: Basic akmklmasadalkm== + + [{“id”: “9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f”, + “checksum”: “b486531f9a779a0c17e3ed29dae8f12c4f9e89cc6f0bc3c38722009fe6857087”}] + + :parameter repo_name: the library name for the repo + + **Example Response**: + + .. sourcecode:: http + + HTTP/1.1 204 + Vary: Accept + Content-Type: application/json + + "" + + :statuscode 204: Created + :statuscode 400: Errors (invalid json, missing or invalid fields, etc) + :statuscode 401: Unauthorized + :statuscode 403: Account is not Active or permission denied + + +.. http:get:: /v1/repositories/(repo_name)/images + + get the images for a library repo. + + **Example Request**: + + .. sourcecode:: http + + GET /v1/repositories/foobar/images HTTP/1.1 + Host: index.docker.io + Accept: application/json + + :parameter repo_name: the library name for the repo + + **Example Response**: + + .. sourcecode:: http + + HTTP/1.1 200 + Vary: Accept + Content-Type: application/json + + [{“id”: “9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f”, + “checksum”: “b486531f9a779a0c17e3ed29dae8f12c4f9e89cc6f0bc3c38722009fe6857087”}, + {“id”: “ertwetewtwe38722009fe6857087b486531f9a779a0c1dfddgfgsdgdsgds”, + “checksum”: “34t23f23fc17e3ed29dae8f12c4f9e89cc6f0bsdfgfsdgdsgdsgerwgew”}] + + :statuscode 200: OK + :statuscode 404: Not found + + +Repository Authorization +************************ + +Library Repo +~~~~~~~~~~~~ + +.. http:put:: /v1/repositories/(repo_name)/auth + + authorize a token for a library repo + + **Example Request**: + + .. sourcecode:: http + + PUT /v1/repositories/foobar/auth HTTP/1.1 + Host: index.docker.io + Accept: application/json + Authorization: Token signature=123abc,repository="library/foobar",access=write + + :parameter repo_name: the library name for the repo + + **Example Response**: + + .. sourcecode:: http + + HTTP/1.1 200 + Vary: Accept + Content-Type: application/json + + "OK" + + :statuscode 200: OK + :statuscode 403: Permission denied + :statuscode 404: Not found + + +User Repo +~~~~~~~~~ + +.. http:put:: /v1/repositories/(namespace)/(repo_name)/auth + + authorize a token for a user repo + + **Example Request**: + + .. sourcecode:: http + + PUT /v1/repositories/foo/bar/auth HTTP/1.1 + Host: index.docker.io + Accept: application/json + Authorization: Token signature=123abc,repository="foo/bar",access=write + + :parameter namespace: the namespace for the repo + :parameter repo_name: the name for the repo + + **Example Response**: + + .. sourcecode:: http + + HTTP/1.1 200 + Vary: Accept + Content-Type: application/json + + "OK" + + :statuscode 200: OK + :statuscode 403: Permission denied + :statuscode 404: Not found + + +2.2 Users +^^^^^^^^^ + +User Login +********** + +.. http:get:: /v1/users + + If you want to check your login, you can try this endpoint + + **Example Request**: + + .. sourcecode:: http + + GET /v1/users HTTP/1.1 + Host: index.docker.io + Accept: application/json + Authorization: Basic akmklmasadalkm== + + **Example Response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: application/json + + OK + + :statuscode 200: no error + :statuscode 401: Unauthorized + :statuscode 403: Account is not Active + + +User Register +************* + +.. http:post:: /v1/users + + Registering a new account. + + **Example request**: + + .. sourcecode:: http + + POST /v1/users HTTP/1.1 + Host: index.docker.io + Accept: application/json + Content-Type: application/json + + {"email": "sam@dotcloud.com", + "password": "toto42", + "username": "foobar"'} + + :jsonparameter email: valid email address, that needs to be confirmed + :jsonparameter username: min 4 character, max 30 characters, must match the regular expression [a-z0-9_]. + :jsonparameter password: min 5 characters + + **Example Response**: + + .. sourcecode:: http + + HTTP/1.1 201 OK + Vary: Accept + Content-Type: application/json + + "User Created" + + :statuscode 201: User Created + :statuscode 400: Errors (invalid json, missing or invalid fields, etc) + +Update User +*********** + +.. http:put:: /v1/users/(username)/ + + Change a password or email address for given user. If you pass in an email, + it will add it to your account, it will not remove the old one. Passwords will + be updated. + + It is up to the client to verify that that password that is sent is the one that + they want. Common approach is to have them type it twice. + + **Example Request**: + + .. sourcecode:: http + + PUT /v1/users/fakeuser/ HTTP/1.1 + Host: index.docker.io + Accept: application/json + Content-Type: application/json + Authorization: Basic akmklmasadalkm== + + {"email": "sam@dotcloud.com", + "password": "toto42"} + + :parameter username: username for the person you want to update + + **Example Response**: + + .. sourcecode:: http + + HTTP/1.1 204 + Vary: Accept + Content-Type: application/json + + "" + + :statuscode 204: User Updated + :statuscode 400: Errors (invalid json, missing or invalid fields, etc) + :statuscode 401: Unauthorized + :statuscode 403: Account is not Active + :statuscode 404: User not found + + +2.3 Search +^^^^^^^^^^ +If you need to search the index, this is the endpoint you would use. + +Search +****** + +.. http:get:: /v1/search + + Search the Index given a search term. It accepts :http:method:`get` only. + + **Example request**: + + .. sourcecode:: http + + GET /v1/search?q=search_term HTTP/1.1 + Host: example.com + Accept: application/json + + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: application/json + + {"query":"search_term", + "num_results": 2, + "results" : [ + {"name": "dotcloud/base", "description": "A base ubuntu64 image..."}, + {"name": "base2", "description": "A base ubuntu64 image..."}, + ] + } + + :query q: what you want to search for + :statuscode 200: no error + :statuscode 500: server error diff --git a/docs/sources/api/index_search_api.rst b/docs/sources/api/index_search_api.rst deleted file mode 100644 index e2f8edc492..0000000000 --- a/docs/sources/api/index_search_api.rst +++ /dev/null @@ -1,43 +0,0 @@ -:title: Docker Index documentation -:description: Documentation for docker Index -:keywords: docker, index, api - - -======================= -Docker Index Search API -======================= - -Search ------- - -.. http:get:: /v1/search - - Search the Index given a search term. It accepts :http:method:`get` only. - - **Example request**: - - .. sourcecode:: http - - GET /v1/search?q=search_term HTTP/1.1 - Host: example.com - Accept: application/json - - **Example response**: - - .. sourcecode:: http - - HTTP/1.1 200 OK - Vary: Accept - Content-Type: application/json - - {"query":"search_term", - "num_results": 2, - "results" : [ - {"name": "dotcloud/base", "description": "A base ubuntu64 image..."}, - {"name": "base2", "description": "A base ubuntu64 image..."}, - ] - } - - :query q: what you want to search for - :statuscode 200: no error - :statuscode 500: server error \ No newline at end of file diff --git a/docs/sources/api/registry_api.rst b/docs/sources/api/registry_api.rst index bb8a16652d..58c8ff3bd4 100644 --- a/docs/sources/api/registry_api.rst +++ b/docs/sources/api/registry_api.rst @@ -1,7 +1,6 @@ -:title: Registry Documentation -:description: Documentation for docker Registry and Registry API -:keywords: docker, registry, api, index - +:title: Registry API +:description: API Documentation for Docker Registry +:keywords: API, Docker, index, registry, REST, documentation =================== Docker Registry API @@ -9,29 +8,10 @@ Docker Registry API .. contents:: Table of Contents -1. The 3 roles -=============== +1. Brief introduction +===================== -1.1 Index ---------- - -The Index is responsible for centralizing information about: -- User accounts -- Checksums of the images -- Public namespaces - -The Index has different components: -- Web UI -- Meta-data store (comments, stars, list public repositories) -- Authentication service -- Tokenization - -The index is authoritative for those information. - -We expect that there will be only one instance of the index, run and managed by dotCloud. - -1.2 Registry ------------- +- This is the REST API for the Docker Registry - It stores the images and the graph for a set of repositories - It does not have user accounts data - It has no notion of user accounts or authorization @@ -60,418 +40,424 @@ We expect that there will be multiple registries out there. To help to grasp the The latter would only require two new commands in docker, e.g. “registryget” and “registryput”, wrapping access to the local filesystem (and optionally doing consistency checks). Authentication and authorization are then delegated to SSH (e.g. with public keys). -1.3 Docker +2. Endpoints +============ + +2.1 Images ---------- -On top of being a runtime for LXC, Docker is the Registry client. It supports: -- Push / Pull on the registry -- Client authentication on the Index +Layer +***** -2. Workflow -=========== +.. http:get:: /v1/images/(image_id)/layer -2.1 Pull + get image layer for a given ``image_id`` + + **Example Request**: + + .. sourcecode:: http + + GET /v1/images/088b4505aa3adc3d35e79c031fa126b403200f02f51920fbd9b7c503e87c7a2c/layer HTTP/1.1 + Host: registry-1.docker.io + Accept: application/json + Content-Type: application/json + Authorization: Token akmklmasadalkmsdfgsdgdge33 + + :parameter image_id: the id for the layer you want to get + + **Example Response**: + + .. sourcecode:: http + + HTTP/1.1 200 + Vary: Accept + Content-Type: application/json + Cookie: (Cookie provided by the Registry) + + { + id: "088b4505aa3adc3d35e79c031fa126b403200f02f51920fbd9b7c503e87c7a2c", + parent: "aeee6396d62273d180a49c96c62e45438d87c7da4a5cf5d2be6bee4e21bc226f", + created: "2013-04-30T17:46:10.843673+03:00", + container: "8305672a76cc5e3d168f97221106ced35a76ec7ddbb03209b0f0d96bf74f6ef7", + container_config: { + Hostname: "host-test", + User: "", + Memory: 0, + MemorySwap: 0, + AttachStdin: false, + AttachStdout: false, + AttachStderr: false, + PortSpecs: null, + Tty: false, + OpenStdin: false, + StdinOnce: false, + Env: null, + Cmd: [ + "/bin/bash", + "-c", + "apt-get -q -yy -f install libevent-dev" + ], + Dns: null, + Image: "imagename/blah", + Volumes: { }, + VolumesFrom: "" + }, + docker_version: "0.1.7" + } + + :statuscode 200: OK + :statuscode 401: Requires authorization + :statuscode 404: Image not found + + +.. http:put:: /v1/images/(image_id)/layer + + put image layer for a given ``image_id`` + + **Example Request**: + + .. sourcecode:: http + + PUT /v1/images/088b4505aa3adc3d35e79c031fa126b403200f02f51920fbd9b7c503e87c7a2c/layer HTTP/1.1 + Host: registry-1.docker.io + Accept: application/json + Content-Type: application/json + Authorization: Token akmklmasadalkmsdfgsdgdge33 + + { + id: "088b4505aa3adc3d35e79c031fa126b403200f02f51920fbd9b7c503e87c7a2c", + parent: "aeee6396d62273d180a49c96c62e45438d87c7da4a5cf5d2be6bee4e21bc226f", + created: "2013-04-30T17:46:10.843673+03:00", + container: "8305672a76cc5e3d168f97221106ced35a76ec7ddbb03209b0f0d96bf74f6ef7", + container_config: { + Hostname: "host-test", + User: "", + Memory: 0, + MemorySwap: 0, + AttachStdin: false, + AttachStdout: false, + AttachStderr: false, + PortSpecs: null, + Tty: false, + OpenStdin: false, + StdinOnce: false, + Env: null, + Cmd: [ + "/bin/bash", + "-c", + "apt-get -q -yy -f install libevent-dev" + ], + Dns: null, + Image: "imagename/blah", + Volumes: { }, + VolumesFrom: "" + }, + docker_version: "0.1.7" + } + + :parameter image_id: the id for the layer you want to get + + + **Example Response**: + + .. sourcecode:: http + + HTTP/1.1 200 + Vary: Accept + Content-Type: application/json + + "" + + :statuscode 200: OK + :statuscode 401: Requires authorization + :statuscode 404: Image not found + + +Image +***** + +.. http:put:: /v1/images/(image_id)/json + + put image for a given ``image_id`` + + **Example Request**: + + .. sourcecode:: http + + PUT /v1/images/088b4505aa3adc3d35e79c031fa126b403200f02f51920fbd9b7c503e87c7a2c/json HTTP/1.1 + Host: registry-1.docker.io + Accept: application/json + Content-Type: application/json + Cookie: (Cookie provided by the Registry) + + { + “id”: “088b4505aa3adc3d35e79c031fa126b403200f02f51920fbd9b7c503e87c7a2c”, + “checksum”: “sha256:b486531f9a779a0c17e3ed29dae8f12c4f9e89cc6f0bc3c38722009fe6857087” + } + + :parameter image_id: the id for the layer you want to get + + + **Example Response**: + + .. sourcecode:: http + + HTTP/1.1 200 + Vary: Accept + Content-Type: application/json + + "" + + :statuscode 200: OK + :statuscode 401: Requires authorization + +.. http:get:: /v1/images/(image_id)/json + + get image for a given ``image_id`` + + **Example Request**: + + .. sourcecode:: http + + GET /v1/images/088b4505aa3adc3d35e79c031fa126b403200f02f51920fbd9b7c503e87c7a2c/json HTTP/1.1 + Host: registry-1.docker.io + Accept: application/json + Content-Type: application/json + Cookie: (Cookie provided by the Registry) + + :parameter image_id: the id for the layer you want to get + + **Example Response**: + + .. sourcecode:: http + + HTTP/1.1 200 + Vary: Accept + Content-Type: application/json + + { + “id”: “088b4505aa3adc3d35e79c031fa126b403200f02f51920fbd9b7c503e87c7a2c”, + “checksum”: “sha256:b486531f9a779a0c17e3ed29dae8f12c4f9e89cc6f0bc3c38722009fe6857087” + } + + :statuscode 200: OK + :statuscode 401: Requires authorization + :statuscode 404: Image not found + + +Ancestry +******** + +.. http:get:: /v1/images/(image_id)/ancestry + + get ancestry for an image given an ``image_id`` + + **Example Request**: + + .. sourcecode:: http + + GET /v1/images/088b4505aa3adc3d35e79c031fa126b403200f02f51920fbd9b7c503e87c7a2c/ancestry HTTP/1.1 + Host: registry-1.docker.io + Accept: application/json + Content-Type: application/json + Cookie: (Cookie provided by the Registry) + + :parameter image_id: the id for the layer you want to get + + **Example Response**: + + .. sourcecode:: http + + HTTP/1.1 200 + Vary: Accept + Content-Type: application/json + + ["088b4502f51920fbd9b7c503e87c7a2c05aa3adc3d35e79c031fa126b403200f", + "aeee63968d87c7da4a5cf5d2be6bee4e21bc226fd62273d180a49c96c62e4543", + "bfa4c5326bc764280b0863b46a4b20d940bc1897ef9c1dfec060604bdc383280", + "6ab5893c6927c15a15665191f2c6cf751f5056d8b95ceee32e43c5e8a3648544"] + + :statuscode 200: OK + :statuscode 401: Requires authorization + :statuscode 404: Image not found + + +2.2 Tags -------- -.. image:: /static_files/docker_pull_chart.png +.. http:get:: /v1/repositories/(namespace)/(repository)/tags -1. Contact the Index to know where I should download “samalba/busybox” -2. Index replies: - a. “samalba/busybox” is on Registry A - b. here are the checksums for “samalba/busybox” (for all layers) - c. token -3. Contact Registry A to receive the layers for “samalba/busybox” (all of them to the base image). Registry A is authoritative for “samalba/busybox” but keeps a copy of all inherited layers and serve them all from the same location. -4. registry contacts index to verify if token/user is allowed to download images -5. Index returns true/false lettings registry know if it should proceed or error out -6. Get the payload for all layers + get all of the tags for the given repo. -It’s possible to run docker pull \https:///repositories/samalba/busybox. In this case, docker bypasses the Index. However the security is not guaranteed (in case Registry A is corrupted) because there won’t be any checksum checks. + **Example Request**: -Currently registry redirects to s3 urls for downloads, going forward all downloads need to be streamed through the registry. The Registry will then abstract the calls to S3 by a top-level class which implements sub-classes for S3 and local storage. + .. sourcecode:: http -Token is only returned when the 'X-Docker-Token' header is sent with request. - -Basic Auth is required to pull private repos. Basic auth isn't required for pulling public repos, but if one is provided, it needs to be valid and for an active account. - -API (pulling repository foo/bar): -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -1. (Docker -> Index) GET /v1/repositories/foo/bar/images - **Headers**: - Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== - X-Docker-Token: true - **Action**: - (looking up the foo/bar in db and gets images and checksums for that repo (all if no tag is specified, if tag, only checksums for those tags) see part 4.4.1) - -2. (Index -> Docker) HTTP 200 OK - - **Headers**: - - Authorization: Token signature=123abc,repository=”foo/bar”,access=write - - X-Docker-Endpoints: registry.docker.io [, registry2.docker.io] - **Body**: - Jsonified checksums (see part 4.4.1) - -3. (Docker -> Registry) GET /v1/repositories/foo/bar/tags/latest - **Headers**: - Authorization: Token signature=123abc,repository=”foo/bar”,access=write - -4. (Registry -> Index) GET /v1/repositories/foo/bar/images - - **Headers**: - Authorization: Token signature=123abc,repository=”foo/bar”,access=read - - **Body**: - - - **Action**: - ( Lookup token see if they have access to pull.) - - If good: - HTTP 200 OK - Index will invalidate the token - If bad: - HTTP 401 Unauthorized - -5. (Docker -> Registry) GET /v1/images/928374982374/ancestry - **Action**: - (for each image id returned in the registry, fetch /json + /layer) - -.. note:: - - If someone makes a second request, then we will always give a new token, never reuse tokens. - -2.2 Push --------- - -.. image:: /static_files/docker_push_chart.png - -1. Contact the index to allocate the repository name “samalba/busybox” (authentication required with user credentials) -2. If authentication works and namespace available, “samalba/busybox” is allocated and a temporary token is returned (namespace is marked as initialized in index) -3. Push the image on the registry (along with the token) -4. Registry A contacts the Index to verify the token (token must corresponds to the repository name) -5. Index validates the token. Registry A starts reading the stream pushed by docker and store the repository (with its images) -6. docker contacts the index to give checksums for upload images - -.. note:: - - **It’s possible not to use the Index at all!** In this case, a deployed version of the Registry is deployed to store and serve images. Those images are not authentified and the security is not guaranteed. - -.. note:: - - **Index can be replaced!** For a private Registry deployed, a custom Index can be used to serve and validate token according to different policies. - -Docker computes the checksums and submit them to the Index at the end of the push. When a repository name does not have checksums on the Index, it means that the push is in progress (since checksums are submitted at the end). - -API (pushing repos foo/bar): -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -1. (Docker -> Index) PUT /v1/repositories/foo/bar/ - **Headers**: - Authorization: Basic sdkjfskdjfhsdkjfh== - X-Docker-Token: true - - **Action**:: - - in index, we allocated a new repository, and set to initialized - - **Body**:: - (The body contains the list of images that are going to be pushed, with empty checksums. The checksums will be set at the end of the push):: - - [{“id”: “9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f”}] - -2. (Index -> Docker) 200 Created - **Headers**: - - WWW-Authenticate: Token signature=123abc,repository=”foo/bar”,access=write - - X-Docker-Endpoints: registry.docker.io [, registry2.docker.io] - -3. (Docker -> Registry) PUT /v1/images/98765432_parent/json - **Headers**: - Authorization: Token signature=123abc,repository=”foo/bar”,access=write - -4. (Registry->Index) GET /v1/repositories/foo/bar/images - **Headers**: - Authorization: Token signature=123abc,repository=”foo/bar”,access=write - **Action**:: - - Index: - will invalidate the token. - - Registry: - grants a session (if token is approved) and fetches the images id - -5. (Docker -> Registry) PUT /v1/images/98765432_parent/json - **Headers**:: - - Authorization: Token signature=123abc,repository=”foo/bar”,access=write - - Cookie: (Cookie provided by the Registry) - -6. (Docker -> Registry) PUT /v1/images/98765432/json - **Headers**: + GET /v1/repositories/foo/bar/tags HTTP/1.1 + Host: registry-1.docker.io + Accept: application/json + Content-Type: application/json Cookie: (Cookie provided by the Registry) -7. (Docker -> Registry) PUT /v1/images/98765432_parent/layer - **Headers**: + :parameter namespace: namespace for the repo + :parameter repository: name for the repo + + **Example Response**: + + .. sourcecode:: http + + HTTP/1.1 200 + Vary: Accept + Content-Type: application/json + + { + "latest": "9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f", + “0.1.1”: “b486531f9a779a0c17e3ed29dae8f12c4f9e89cc6f0bc3c38722009fe6857087” + } + + :statuscode 200: OK + :statuscode 401: Requires authorization + :statuscode 404: Repository not found + + +.. http:get:: /v1/repositories/(namespace)/(repository)/tags/(tag) + + get a tag for the given repo. + + **Example Request**: + + .. sourcecode:: http + + GET /v1/repositories/foo/bar/tags/latest HTTP/1.1 + Host: registry-1.docker.io + Accept: application/json + Content-Type: application/json Cookie: (Cookie provided by the Registry) -8. (Docker -> Registry) PUT /v1/images/98765432/layer - **Headers**: - X-Docker-Checksum: sha256:436745873465fdjkhdfjkgh + :parameter namespace: namespace for the repo + :parameter repository: name for the repo + :parameter tag: name of tag you want to get -9. (Docker -> Registry) PUT /v1/repositories/foo/bar/tags/latest - **Headers**: + **Example Response**: + + .. sourcecode:: http + + HTTP/1.1 200 + Vary: Accept + Content-Type: application/json + + "9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f" + + :statuscode 200: OK + :statuscode 401: Requires authorization + :statuscode 404: Tag not found + +.. http:delete:: /v1/repositories/(namespace)/(repository)/tags/(tag) + + delete the tag for the repo + + **Example Request**: + + .. sourcecode:: http + + DELETE /v1/repositories/foo/bar/tags/latest HTTP/1.1 + Host: registry-1.docker.io + Accept: application/json + Content-Type: application/json Cookie: (Cookie provided by the Registry) - **Body**: - “98765432” -10. (Docker -> Index) PUT /v1/repositories/foo/bar/images + :parameter namespace: namespace for the repo + :parameter repository: name for the repo + :parameter tag: name of tag you want to delete - **Headers**: - Authorization: Basic 123oislifjsldfj== - X-Docker-Endpoints: registry1.docker.io (no validation on this right now) + **Example Response**: - **Body**: - (The image, id’s, tags and checksums) + .. sourcecode:: http - [{“id”: “9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f”, - “checksum”: “b486531f9a779a0c17e3ed29dae8f12c4f9e89cc6f0bc3c38722009fe6857087”}] + HTTP/1.1 200 + Vary: Accept + Content-Type: application/json - **Return** HTTP 204 + "" -.. note:: + :statuscode 200: OK + :statuscode 401: Requires authorization + :statuscode 404: Tag not found - If push fails and they need to start again, what happens in the index, there will already be a record for the namespace/name, but it will be initialized. Should we allow it, or mark as name already used? One edge case could be if someone pushes the same thing at the same time with two different shells. - If it's a retry on the Registry, Docker has a cookie (provided by the registry after token validation). So the Index won’t have to provide a new token. +.. http:put:: /v1/repositories/(namespace)/(repository)/tags/(tag) -3. How to use the Registry in standalone mode -============================================= + put a tag for the given repo. -The Index has two main purposes (along with its fancy social features): + **Example Request**: -- Resolve short names (to avoid passing absolute URLs all the time) - - username/projectname -> \https://registry.docker.io/users//repositories// -- Authenticate a user as a repos owner (for a central referenced repository) + .. sourcecode:: http -3.1 Without an Index --------------------- -Using the Registry without the Index can be useful to store the images on a private network without having to rely on an external entity controlled by dotCloud. + PUT /v1/repositories/foo/bar/tags/latest HTTP/1.1 + Host: registry-1.docker.io + Accept: application/json + Content-Type: application/json + Cookie: (Cookie provided by the Registry) -In this case, the registry will be launched in a special mode (--standalone? --no-index?). In this mode, the only thing which changes is that Registry will never contact the Index to verify a token. It will be the Registry owner responsibility to authenticate the user who pushes (or even pulls) an image using any mechanism (HTTP auth, IP based, etc...). + “9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f” -In this scenario, the Registry is responsible for the security in case of data corruption since the checksums are not delivered by a trusted entity. + :parameter namespace: namespace for the repo + :parameter repository: name for the repo + :parameter tag: name of tag you want to add -As hinted previously, a standalone registry can also be implemented by any HTTP server handling GET/PUT requests (or even only GET requests if no write access is necessary). + **Example Response**: -3.2 With an Index ------------------ + .. sourcecode:: http -The Index data needed by the Registry are simple: -- Serve the checksums -- Provide and authorize a Token + HTTP/1.1 200 + Vary: Accept + Content-Type: application/json -In the scenario of a Registry running on a private network with the need of centralizing and authorizing, it’s easy to use a custom Index. + "" -The only challenge will be to tell Docker to contact (and trust) this custom Index. Docker will be configurable at some point to use a specific Index, it’ll be the private entity responsibility (basically the organization who uses Docker in a private environment) to maintain the Index and the Docker’s configuration among its consumers. + :statuscode 200: OK + :statuscode 400: Invalid data + :statuscode 401: Requires authorization + :statuscode 404: Image not found -4. The API -========== +2.3 Repositories +---------------- -The first version of the api is available here: https://github.com/jpetazzo/docker/blob/acd51ecea8f5d3c02b00a08176171c59442df8b3/docs/images-repositories-push-pull.md +.. http:delete:: /v1/repositories/(namespace)/(repository)/ -4.1 Images ----------- + delete a repository -The format returned in the images is not defined here (for layer and json), basically because Registry stores exactly the same kind of information as Docker uses to manage them. + **Example Request**: -The format of ancestry is a line-separated list of image ids, in age order. I.e. the image’s parent is on the last line, the parent of the parent on the next-to-last line, etc.; if the image has no parent, the file is empty. + .. sourcecode:: http -GET /v1/images//layer -PUT /v1/images//layer -GET /v1/images//json -PUT /v1/images//json -GET /v1/images//ancestry -PUT /v1/images//ancestry + DELETE /v1/repositories/foo/bar/ HTTP/1.1 + Host: registry-1.docker.io + Accept: application/json + Content-Type: application/json + Cookie: (Cookie provided by the Registry) -4.2 Users ---------- + "" -4.2.1 Create a user (Index) -^^^^^^^^^^^^^^^^^^^^^^^^^^^ + :parameter namespace: namespace for the repo + :parameter repository: name for the repo -POST /v1/users + **Example Response**: -**Body**: - {"email": "sam@dotcloud.com", "password": "toto42", "username": "foobar"'} + .. sourcecode:: http -**Validation**: - - **username** : min 4 character, max 30 characters, must match the regular expression [a-z0-9_]. - - **password**: min 5 characters + HTTP/1.1 200 + Vary: Accept + Content-Type: application/json -**Valid**: return HTTP 200 + "" -Errors: HTTP 400 (we should create error codes for possible errors) -- invalid json -- missing field -- wrong format (username, password, email, etc) -- forbidden name -- name already exists + :statuscode 200: OK + :statuscode 401: Requires authorization + :statuscode 404: Repository not found -.. note:: +3.0 Authorization +================= +This is where we describe the authorization process, including the tokens and cookies. - A user account will be valid only if the email has been validated (a validation link is sent to the email address). - -4.2.2 Update a user (Index) -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -PUT /v1/users/ - -**Body**: - {"password": "toto"} - -.. note:: - - We can also update email address, if they do, they will need to reverify their new email address. - -4.2.3 Login (Index) -^^^^^^^^^^^^^^^^^^^ -Does nothing else but asking for a user authentication. Can be used to validate credentials. HTTP Basic Auth for now, maybe change in future. - -GET /v1/users - -**Return**: - - Valid: HTTP 200 - - Invalid login: HTTP 401 - - Account inactive: HTTP 403 Account is not Active - -4.3 Tags (Registry) -------------------- - -The Registry does not know anything about users. Even though repositories are under usernames, it’s just a namespace for the registry. Allowing us to implement organizations or different namespaces per user later, without modifying the Registry’s API. - -The following naming restrictions apply: - -- Namespaces must match the same regular expression as usernames (See 4.2.1.) -- Repository names must match the regular expression [a-zA-Z0-9-_.] - -4.3.1 Get all tags -^^^^^^^^^^^^^^^^^^ - -GET /v1/repositories///tags - -**Return**: HTTP 200 - { - "latest": "9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f", - “0.1.1”: “b486531f9a779a0c17e3ed29dae8f12c4f9e89cc6f0bc3c38722009fe6857087” - } - -4.3.2 Read the content of a tag (resolve the image id) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -GET /v1/repositories///tags/ - -**Return**: - "9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f" - -4.3.3 Delete a tag (registry) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -DELETE /v1/repositories///tags/ - -4.4 Images (Index) ------------------- - -For the Index to “resolve” the repository name to a Registry location, it uses the X-Docker-Endpoints header. In other terms, this requests always add a “X-Docker-Endpoints” to indicate the location of the registry which hosts this repository. - -4.4.1 Get the images -^^^^^^^^^^^^^^^^^^^^^ - -GET /v1/repositories///images - -**Return**: HTTP 200 - [{“id”: “9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f”, “checksum”: “md5:b486531f9a779a0c17e3ed29dae8f12c4f9e89cc6f0bc3c38722009fe6857087”}] - - -4.4.2 Add/update the images -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -You always add images, you never remove them. - -PUT /v1/repositories///images - -**Body**: - [ {“id”: “9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f”, “checksum”: “sha256:b486531f9a779a0c17e3ed29dae8f12c4f9e89cc6f0bc3c38722009fe6857087”} ] - -**Return** 204 - -5. Chaining Registries -====================== - -It’s possible to chain Registries server for several reasons: -- Load balancing -- Delegate the next request to another server - -When a Registry is a reference for a repository, it should host the entire images chain in order to avoid breaking the chain during the download. - -The Index and Registry use this mechanism to redirect on one or the other. - -Example with an image download: -On every request, a special header can be returned: - -X-Docker-Endpoints: server1,server2 - -On the next request, the client will always pick a server from this list. - -6. Authentication & Authorization -================================= - -6.1 On the Index ------------------ - -The Index supports both “Basic” and “Token” challenges. Usually when there is a “401 Unauthorized”, the Index replies this:: - - 401 Unauthorized - WWW-Authenticate: Basic realm="auth required",Token - -You have 3 options: - -1. Provide user credentials and ask for a token - - **Header**: - - Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== - - X-Docker-Token: true - - In this case, along with the 200 response, you’ll get a new token (if user auth is ok): - If authorization isn't correct you get a 401 response. - If account isn't active you will get a 403 response. - - **Response**: - - 200 OK - - X-Docker-Token: Token signature=123abc,repository=”foo/bar”,access=read - -2. Provide user credentials only - - **Header**: - Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== - -3. Provide Token - - **Header**: - Authorization: Token signature=123abc,repository=”foo/bar”,access=read - -6.2 On the Registry -------------------- - -The Registry only supports the Token challenge:: - - 401 Unauthorized - WWW-Authenticate: Token - -The only way is to provide a token on “401 Unauthorized” responses:: - - Authorization: Token signature=123abc,repository=”foo/bar”,access=read - -Usually, the Registry provides a Cookie when a Token verification succeeded. Every time the Registry passes a Cookie, you have to pass it back the same cookie.:: - - 200 OK - Set-Cookie: session="wD/J7LqL5ctqw8haL10vgfhrb2Q=?foo=UydiYXInCnAxCi4=×tamp=RjEzNjYzMTQ5NDcuNDc0NjQzCi4="; Path=/; HttpOnly - -Next request:: - - GET /(...) - Cookie: session="wD/J7LqL5ctqw8haL10vgfhrb2Q=?foo=UydiYXInCnAxCi4=×tamp=RjEzNjYzMTQ5NDcuNDc0NjQzCi4=" +TODO: add more info. diff --git a/docs/sources/api/registry_index_spec.rst b/docs/sources/api/registry_index_spec.rst new file mode 100644 index 0000000000..c1854194b2 --- /dev/null +++ b/docs/sources/api/registry_index_spec.rst @@ -0,0 +1,569 @@ +:title: Registry Documentation +:description: Documentation for docker Registry and Registry API +:keywords: docker, registry, api, index + + +===================== +Registry & index Spec +===================== + +.. contents:: Table of Contents + +1. The 3 roles +=============== + +1.1 Index +--------- + +The Index is responsible for centralizing information about: +- User accounts +- Checksums of the images +- Public namespaces + +The Index has different components: +- Web UI +- Meta-data store (comments, stars, list public repositories) +- Authentication service +- Tokenization + +The index is authoritative for those information. + +We expect that there will be only one instance of the index, run and managed by dotCloud. + +1.2 Registry +------------ +- It stores the images and the graph for a set of repositories +- It does not have user accounts data +- It has no notion of user accounts or authorization +- It delegates authentication and authorization to the Index Auth service using tokens +- It supports different storage backends (S3, cloud files, local FS) +- It doesn’t have a local database +- It will be open-sourced at some point + +We expect that there will be multiple registries out there. To help to grasp the context, here are some examples of registries: + +- **sponsor registry**: such a registry is provided by a third-party hosting infrastructure as a convenience for their customers and the docker community as a whole. Its costs are supported by the third party, but the management and operation of the registry are supported by dotCloud. It features read/write access, and delegates authentication and authorization to the Index. +- **mirror registry**: such a registry is provided by a third-party hosting infrastructure but is targeted at their customers only. Some mechanism (unspecified to date) ensures that public images are pulled from a sponsor registry to the mirror registry, to make sure that the customers of the third-party provider can “docker pull” those images locally. +- **vendor registry**: such a registry is provided by a software vendor, who wants to distribute docker images. It would be operated and managed by the vendor. Only users authorized by the vendor would be able to get write access. Some images would be public (accessible for anyone), others private (accessible only for authorized users). Authentication and authorization would be delegated to the Index. The goal of vendor registries is to let someone do “docker pull basho/riak1.3” and automatically push from the vendor registry (instead of a sponsor registry); i.e. get all the convenience of a sponsor registry, while retaining control on the asset distribution. +- **private registry**: such a registry is located behind a firewall, or protected by an additional security layer (HTTP authorization, SSL client-side certificates, IP address authorization...). The registry is operated by a private entity, outside of dotCloud’s control. It can optionally delegate additional authorization to the Index, but it is not mandatory. + +.. note:: + + Mirror registries and private registries which do not use the Index don’t even need to run the registry code. They can be implemented by any kind of transport implementing HTTP GET and PUT. Read-only registries can be powered by a simple static HTTP server. + +.. note:: + + The latter implies that while HTTP is the protocol of choice for a registry, multiple schemes are possible (and in some cases, trivial): + - HTTP with GET (and PUT for read-write registries); + - local mount point; + - remote docker addressed through SSH. + +The latter would only require two new commands in docker, e.g. “registryget” and “registryput”, wrapping access to the local filesystem (and optionally doing consistency checks). Authentication and authorization are then delegated to SSH (e.g. with public keys). + +1.3 Docker +---------- + +On top of being a runtime for LXC, Docker is the Registry client. It supports: +- Push / Pull on the registry +- Client authentication on the Index + +2. Workflow +=========== + +2.1 Pull +-------- + +.. image:: /static_files/docker_pull_chart.png + +1. Contact the Index to know where I should download “samalba/busybox” +2. Index replies: + a. “samalba/busybox” is on Registry A + b. here are the checksums for “samalba/busybox” (for all layers) + c. token +3. Contact Registry A to receive the layers for “samalba/busybox” (all of them to the base image). Registry A is authoritative for “samalba/busybox” but keeps a copy of all inherited layers and serve them all from the same location. +4. registry contacts index to verify if token/user is allowed to download images +5. Index returns true/false lettings registry know if it should proceed or error out +6. Get the payload for all layers + +It’s possible to run docker pull \https:///repositories/samalba/busybox. In this case, docker bypasses the Index. However the security is not guaranteed (in case Registry A is corrupted) because there won’t be any checksum checks. + +Currently registry redirects to s3 urls for downloads, going forward all downloads need to be streamed through the registry. The Registry will then abstract the calls to S3 by a top-level class which implements sub-classes for S3 and local storage. + +Token is only returned when the 'X-Docker-Token' header is sent with request. + +Basic Auth is required to pull private repos. Basic auth isn't required for pulling public repos, but if one is provided, it needs to be valid and for an active account. + +API (pulling repository foo/bar): +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +1. (Docker -> Index) GET /v1/repositories/foo/bar/images + **Headers**: + Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== + X-Docker-Token: true + **Action**: + (looking up the foo/bar in db and gets images and checksums for that repo (all if no tag is specified, if tag, only checksums for those tags) see part 4.4.1) + +2. (Index -> Docker) HTTP 200 OK + + **Headers**: + - Authorization: Token signature=123abc,repository=”foo/bar”,access=write + - X-Docker-Endpoints: registry.docker.io [, registry2.docker.io] + **Body**: + Jsonified checksums (see part 4.4.1) + +3. (Docker -> Registry) GET /v1/repositories/foo/bar/tags/latest + **Headers**: + Authorization: Token signature=123abc,repository=”foo/bar”,access=write + +4. (Registry -> Index) GET /v1/repositories/foo/bar/images + + **Headers**: + Authorization: Token signature=123abc,repository=”foo/bar”,access=read + + **Body**: + + + **Action**: + ( Lookup token see if they have access to pull.) + + If good: + HTTP 200 OK + Index will invalidate the token + If bad: + HTTP 401 Unauthorized + +5. (Docker -> Registry) GET /v1/images/928374982374/ancestry + **Action**: + (for each image id returned in the registry, fetch /json + /layer) + +.. note:: + + If someone makes a second request, then we will always give a new token, never reuse tokens. + +2.2 Push +-------- + +.. image:: /static_files/docker_push_chart.png + +1. Contact the index to allocate the repository name “samalba/busybox” (authentication required with user credentials) +2. If authentication works and namespace available, “samalba/busybox” is allocated and a temporary token is returned (namespace is marked as initialized in index) +3. Push the image on the registry (along with the token) +4. Registry A contacts the Index to verify the token (token must corresponds to the repository name) +5. Index validates the token. Registry A starts reading the stream pushed by docker and store the repository (with its images) +6. docker contacts the index to give checksums for upload images + +.. note:: + + **It’s possible not to use the Index at all!** In this case, a deployed version of the Registry is deployed to store and serve images. Those images are not authentified and the security is not guaranteed. + +.. note:: + + **Index can be replaced!** For a private Registry deployed, a custom Index can be used to serve and validate token according to different policies. + +Docker computes the checksums and submit them to the Index at the end of the push. When a repository name does not have checksums on the Index, it means that the push is in progress (since checksums are submitted at the end). + +API (pushing repos foo/bar): +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +1. (Docker -> Index) PUT /v1/repositories/foo/bar/ + **Headers**: + Authorization: Basic sdkjfskdjfhsdkjfh== + X-Docker-Token: true + + **Action**:: + - in index, we allocated a new repository, and set to initialized + + **Body**:: + (The body contains the list of images that are going to be pushed, with empty checksums. The checksums will be set at the end of the push):: + + [{“id”: “9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f”}] + +2. (Index -> Docker) 200 Created + **Headers**: + - WWW-Authenticate: Token signature=123abc,repository=”foo/bar”,access=write + - X-Docker-Endpoints: registry.docker.io [, registry2.docker.io] + +3. (Docker -> Registry) PUT /v1/images/98765432_parent/json + **Headers**: + Authorization: Token signature=123abc,repository=”foo/bar”,access=write + +4. (Registry->Index) GET /v1/repositories/foo/bar/images + **Headers**: + Authorization: Token signature=123abc,repository=”foo/bar”,access=write + **Action**:: + - Index: + will invalidate the token. + - Registry: + grants a session (if token is approved) and fetches the images id + +5. (Docker -> Registry) PUT /v1/images/98765432_parent/json + **Headers**:: + - Authorization: Token signature=123abc,repository=”foo/bar”,access=write + - Cookie: (Cookie provided by the Registry) + +6. (Docker -> Registry) PUT /v1/images/98765432/json + **Headers**: + Cookie: (Cookie provided by the Registry) + +7. (Docker -> Registry) PUT /v1/images/98765432_parent/layer + **Headers**: + Cookie: (Cookie provided by the Registry) + +8. (Docker -> Registry) PUT /v1/images/98765432/layer + **Headers**: + X-Docker-Checksum: sha256:436745873465fdjkhdfjkgh + +9. (Docker -> Registry) PUT /v1/repositories/foo/bar/tags/latest + **Headers**: + Cookie: (Cookie provided by the Registry) + **Body**: + “98765432” + +10. (Docker -> Index) PUT /v1/repositories/foo/bar/images + + **Headers**: + Authorization: Basic 123oislifjsldfj== + X-Docker-Endpoints: registry1.docker.io (no validation on this right now) + + **Body**: + (The image, id’s, tags and checksums) + + [{“id”: “9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f”, + “checksum”: “b486531f9a779a0c17e3ed29dae8f12c4f9e89cc6f0bc3c38722009fe6857087”}] + + **Return** HTTP 204 + +.. note:: + + If push fails and they need to start again, what happens in the index, there will already be a record for the namespace/name, but it will be initialized. Should we allow it, or mark as name already used? One edge case could be if someone pushes the same thing at the same time with two different shells. + + If it's a retry on the Registry, Docker has a cookie (provided by the registry after token validation). So the Index won’t have to provide a new token. + +2.3 Delete +---------- + +If you need to delete something from the index or registry, we need a nice clean way to do that. Here is the workflow. + +1. Docker contacts the index to request a delete of a repository “samalba/busybox” (authentication required with user credentials) +2. If authentication works and repository is valid, “samalba/busybox” is marked as deleted and a temporary token is returned +3. Send a delete request to the registry for the repository (along with the token) +4. Registry A contacts the Index to verify the token (token must corresponds to the repository name) +5. Index validates the token. Registry A deletes the repository and everything associated to it. +6. docker contacts the index to let it know it was removed from the registry, the index removes all records from the database. + +.. note:: + + The Docker client should present an "Are you sure?" prompt to confirm the deletion before starting the process. Once it starts it can't be undone. + +API (deleting repository foo/bar): +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +1. (Docker -> Index) DELETE /v1/repositories/foo/bar/ + **Headers**: + Authorization: Basic sdkjfskdjfhsdkjfh== + X-Docker-Token: true + + **Action**:: + - in index, we make sure it is a valid repository, and set to deleted (logically) + + **Body**:: + Empty + +2. (Index -> Docker) 202 Accepted + **Headers**: + - WWW-Authenticate: Token signature=123abc,repository=”foo/bar”,access=delete + - X-Docker-Endpoints: registry.docker.io [, registry2.docker.io] # list of endpoints where this repo lives. + +3. (Docker -> Registry) DELETE /v1/repositories/foo/bar/ + **Headers**: + Authorization: Token signature=123abc,repository=”foo/bar”,access=delete + +4. (Registry->Index) PUT /v1/repositories/foo/bar/auth + **Headers**: + Authorization: Token signature=123abc,repository=”foo/bar”,access=delete + **Action**:: + - Index: + will invalidate the token. + - Registry: + deletes the repository (if token is approved) + +5. (Registry -> Docker) 200 OK + 200 If success + 403 if forbidden + 400 if bad request + 404 if repository isn't found + +6. (Docker -> Index) DELETE /v1/repositories/foo/bar/ + + **Headers**: + Authorization: Basic 123oislifjsldfj== + X-Docker-Endpoints: registry-1.docker.io (no validation on this right now) + + **Body**: + Empty + + **Return** HTTP 200 + + +3. How to use the Registry in standalone mode +============================================= + +The Index has two main purposes (along with its fancy social features): + +- Resolve short names (to avoid passing absolute URLs all the time) + - username/projectname -> \https://registry.docker.io/users//repositories// + - team/projectname -> \https://registry.docker.io/team//repositories// +- Authenticate a user as a repos owner (for a central referenced repository) + +3.1 Without an Index +-------------------- +Using the Registry without the Index can be useful to store the images on a private network without having to rely on an external entity controlled by dotCloud. + +In this case, the registry will be launched in a special mode (--standalone? --no-index?). In this mode, the only thing which changes is that Registry will never contact the Index to verify a token. It will be the Registry owner responsibility to authenticate the user who pushes (or even pulls) an image using any mechanism (HTTP auth, IP based, etc...). + +In this scenario, the Registry is responsible for the security in case of data corruption since the checksums are not delivered by a trusted entity. + +As hinted previously, a standalone registry can also be implemented by any HTTP server handling GET/PUT requests (or even only GET requests if no write access is necessary). + +3.2 With an Index +----------------- + +The Index data needed by the Registry are simple: +- Serve the checksums +- Provide and authorize a Token + +In the scenario of a Registry running on a private network with the need of centralizing and authorizing, it’s easy to use a custom Index. + +The only challenge will be to tell Docker to contact (and trust) this custom Index. Docker will be configurable at some point to use a specific Index, it’ll be the private entity responsibility (basically the organization who uses Docker in a private environment) to maintain the Index and the Docker’s configuration among its consumers. + +4. The API +========== + +The first version of the api is available here: https://github.com/jpetazzo/docker/blob/acd51ecea8f5d3c02b00a08176171c59442df8b3/docs/images-repositories-push-pull.md + +4.1 Images +---------- + +The format returned in the images is not defined here (for layer and json), basically because Registry stores exactly the same kind of information as Docker uses to manage them. + +The format of ancestry is a line-separated list of image ids, in age order. I.e. the image’s parent is on the last line, the parent of the parent on the next-to-last line, etc.; if the image has no parent, the file is empty. + +GET /v1/images//layer +PUT /v1/images//layer +GET /v1/images//json +PUT /v1/images//json +GET /v1/images//ancestry +PUT /v1/images//ancestry + +4.2 Users +--------- + +4.2.1 Create a user (Index) +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +POST /v1/users + +**Body**: + {"email": "sam@dotcloud.com", "password": "toto42", "username": "foobar"'} + +**Validation**: + - **username** : min 4 character, max 30 characters, must match the regular expression [a-z0-9_]. + - **password**: min 5 characters + +**Valid**: return HTTP 200 + +Errors: HTTP 400 (we should create error codes for possible errors) +- invalid json +- missing field +- wrong format (username, password, email, etc) +- forbidden name +- name already exists + +.. note:: + + A user account will be valid only if the email has been validated (a validation link is sent to the email address). + +4.2.2 Update a user (Index) +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +PUT /v1/users/ + +**Body**: + {"password": "toto"} + +.. note:: + + We can also update email address, if they do, they will need to reverify their new email address. + +4.2.3 Login (Index) +^^^^^^^^^^^^^^^^^^^ +Does nothing else but asking for a user authentication. Can be used to validate credentials. HTTP Basic Auth for now, maybe change in future. + +GET /v1/users + +**Return**: + - Valid: HTTP 200 + - Invalid login: HTTP 401 + - Account inactive: HTTP 403 Account is not Active + +4.3 Tags (Registry) +------------------- + +The Registry does not know anything about users. Even though repositories are under usernames, it’s just a namespace for the registry. Allowing us to implement organizations or different namespaces per user later, without modifying the Registry’s API. + +The following naming restrictions apply: + +- Namespaces must match the same regular expression as usernames (See 4.2.1.) +- Repository names must match the regular expression [a-zA-Z0-9-_.] + +4.3.1 Get all tags +^^^^^^^^^^^^^^^^^^ + +GET /v1/repositories///tags + +**Return**: HTTP 200 + { + "latest": "9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f", + “0.1.1”: “b486531f9a779a0c17e3ed29dae8f12c4f9e89cc6f0bc3c38722009fe6857087” + } + +4.3.2 Read the content of a tag (resolve the image id) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +GET /v1/repositories///tags/ + +**Return**: + "9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f" + +4.3.3 Delete a tag (registry) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +DELETE /v1/repositories///tags/ + +4.4 Images (Index) +------------------ + +For the Index to “resolve” the repository name to a Registry location, it uses the X-Docker-Endpoints header. In other terms, this requests always add a “X-Docker-Endpoints” to indicate the location of the registry which hosts this repository. + +4.4.1 Get the images +^^^^^^^^^^^^^^^^^^^^^ + +GET /v1/repositories///images + +**Return**: HTTP 200 + [{“id”: “9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f”, “checksum”: “md5:b486531f9a779a0c17e3ed29dae8f12c4f9e89cc6f0bc3c38722009fe6857087”}] + + +4.4.2 Add/update the images +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +You always add images, you never remove them. + +PUT /v1/repositories///images + +**Body**: + [ {“id”: “9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f”, “checksum”: “sha256:b486531f9a779a0c17e3ed29dae8f12c4f9e89cc6f0bc3c38722009fe6857087”} ] + +**Return** 204 + +4.5 Repositories +---------------- + +4.5.1 Remove a Repository (Registry) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +DELETE /v1/repositories// + +Return 200 OK + +4.5.2 Remove a Repository (Index) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +This starts the delete process. see 2.3 for more details. + +DELETE /v1/repositories// + +Return 202 OK + +5. Chaining Registries +====================== + +It’s possible to chain Registries server for several reasons: +- Load balancing +- Delegate the next request to another server + +When a Registry is a reference for a repository, it should host the entire images chain in order to avoid breaking the chain during the download. + +The Index and Registry use this mechanism to redirect on one or the other. + +Example with an image download: +On every request, a special header can be returned: + +X-Docker-Endpoints: server1,server2 + +On the next request, the client will always pick a server from this list. + +6. Authentication & Authorization +================================= + +6.1 On the Index +----------------- + +The Index supports both “Basic” and “Token” challenges. Usually when there is a “401 Unauthorized”, the Index replies this:: + + 401 Unauthorized + WWW-Authenticate: Basic realm="auth required",Token + +You have 3 options: + +1. Provide user credentials and ask for a token + + **Header**: + - Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== + - X-Docker-Token: true + + In this case, along with the 200 response, you’ll get a new token (if user auth is ok): + If authorization isn't correct you get a 401 response. + If account isn't active you will get a 403 response. + + **Response**: + - 200 OK + - X-Docker-Token: Token signature=123abc,repository=”foo/bar”,access=read + +2. Provide user credentials only + + **Header**: + Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== + +3. Provide Token + + **Header**: + Authorization: Token signature=123abc,repository=”foo/bar”,access=read + +6.2 On the Registry +------------------- + +The Registry only supports the Token challenge:: + + 401 Unauthorized + WWW-Authenticate: Token + +The only way is to provide a token on “401 Unauthorized” responses:: + + Authorization: Token signature=123abc,repository=”foo/bar”,access=read + +Usually, the Registry provides a Cookie when a Token verification succeeded. Every time the Registry passes a Cookie, you have to pass it back the same cookie.:: + + 200 OK + Set-Cookie: session="wD/J7LqL5ctqw8haL10vgfhrb2Q=?foo=UydiYXInCnAxCi4=×tamp=RjEzNjYzMTQ5NDcuNDc0NjQzCi4="; Path=/; HttpOnly + +Next request:: + + GET /(...) + Cookie: session="wD/J7LqL5ctqw8haL10vgfhrb2Q=?foo=UydiYXInCnAxCi4=×tamp=RjEzNjYzMTQ5NDcuNDc0NjQzCi4=" + + +7.0 Document Version +--------------------- + +- 1.0 : May 6th 2013 : initial release +- 1.1 : June 1st 2013 : Added Delete Repository and way to handle new source namespace. \ No newline at end of file diff --git a/docs/sources/commandline/command/build.rst b/docs/sources/commandline/command/build.rst index 81120b22d2..254b0371a9 100644 --- a/docs/sources/commandline/command/build.rst +++ b/docs/sources/commandline/command/build.rst @@ -19,10 +19,15 @@ Examples docker build . -This will take the local Dockerfile +| This will read the Dockerfile from the current directory. It will also send any other files and directories found in the current directory to the docker daemon. +| The contents of this directory would be used by ADD commands found within the Dockerfile. +| This will send a lot of data to the docker daemon if the current directory contains a lot of data. +| If the absolute path is provided instead of '.', only the files and directories required by the ADD commands from the Dockerfile will be added to the context and transferred to the docker daemon. +| .. code-block:: bash docker build - -This will read a Dockerfile form Stdin without context +| This will read a Dockerfile from Stdin without context. Due to the lack of a context, no contents of any local directory will be sent to the docker daemon. +| ADD doesn't work when running in this mode due to the absence of the context, thus having no source files to copy to the container. diff --git a/docs/sources/contributing/contributing.rst b/docs/sources/contributing/contributing.rst index 25b4df763a..1913cec30d 100644 --- a/docs/sources/contributing/contributing.rst +++ b/docs/sources/contributing/contributing.rst @@ -5,5 +5,5 @@ Contributing to Docker ====================== -Want to hack on Docker? Awesome! The repository includes `all the instructions you need to get started `. +Want to hack on Docker? Awesome! The repository includes `all the instructions you need to get started `_. diff --git a/docs/sources/faq.rst b/docs/sources/faq.rst index dfffa012f4..12b9751338 100644 --- a/docs/sources/faq.rst +++ b/docs/sources/faq.rst @@ -35,13 +35,16 @@ Most frequently asked questions. You can find more answers on: - * `IRC: docker on freenode`_ + * `Docker club mailinglist`_ + * `IRC, docker on freenode`_ * `Github`_ * `Ask questions on Stackoverflow`_ * `Join the conversation on Twitter`_ + + .. _Docker club mailinglist: https://groups.google.com/d/forum/docker-club .. _the repo: http://www.github.com/dotcloud/docker - .. _IRC: docker on freenode: docker on freenode: irc://chat.freenode.net#docker + .. _IRC, docker on freenode: irc://chat.freenode.net#docker .. _Github: http://www.github.com/dotcloud/docker .. _Ask questions on Stackoverflow: http://stackoverflow.com/search?q=docker .. _Join the conversation on Twitter: http://twitter.com/getdocker diff --git a/docs/sources/use/builder.rst b/docs/sources/use/builder.rst index abd5b9ecb1..f2e9ce97ce 100644 --- a/docs/sources/use/builder.rst +++ b/docs/sources/use/builder.rst @@ -18,7 +18,7 @@ steps and commit them along the way, giving you a final image. To use Docker Builder, assemble the steps into a text file (commonly referred to as a Dockerfile) and supply this to `docker build` on STDIN, like so: - ``docker build < Dockerfile`` + ``docker build - < Dockerfile`` Docker will run your steps one-by-one, committing the result if necessary, before finally outputting the ID of your new image. diff --git a/graph.go b/graph.go index daa21f3053..f2b2ccec8e 100644 --- a/graph.go +++ b/graph.go @@ -90,6 +90,15 @@ func (graph *Graph) Get(name string) (*Image, error) { return nil, fmt.Errorf("Image stored at '%s' has wrong id '%s'", id, img.ID) } img.graph = graph + if img.Size == 0 { + root, err := img.root() + if err != nil { + return nil, err + } + if err := StoreSize(img, root); err != nil { + return nil, err + } + } graph.lockSumMap.Lock() defer graph.lockSumMap.Unlock() if _, exists := graph.checksumLock[img.ID]; !exists { diff --git a/hack/PRINCIPLES.md b/hack/PRINCIPLES.md index 77ce4f5e51..e59c167598 100644 --- a/hack/PRINCIPLES.md +++ b/hack/PRINCIPLES.md @@ -5,6 +5,10 @@ In the design and development of Docker we try to follow these principles: (Work in progress) * Don't try to replace every tool. Instead, be an ingredient to improve them. +* Less code is better. +* Less components is better. Do you really need to add one more class? +* 50 lines of straightforward, readable code is better than 10 lines of magic that nobody can understand. +* Don't do later what you can do now. "//FIXME: refactor" is not acceptable in new code. * When hesitating between 2 options, choose the one that is easier to reverse. * No is temporary, Yes is forever. If you're not sure about a new feature, say no. You can change your mind later. * Containers must be portable to the greatest possible number of machines. Be suspicious of any change which makes machines less interchangeable. diff --git a/hack/ROADMAP.md b/hack/ROADMAP.md index 61387e21cf..f335db6953 100644 --- a/hack/ROADMAP.md +++ b/hack/ROADMAP.md @@ -86,3 +86,20 @@ Production-ready Docker is still alpha software, and not suited for production. We are working hard to get there, and we are confident that it will be possible within a few months. + +Advanced port redirections +-------------------------- + +Docker currently supports 2 flavors of port redirection: STATIC->STATIC (eg. "redirect public port 80 to private port 80") +and RANDOM->STATIC (eg. "redirect any public port to private port 80"). + +With these 2 flavors, docker can support the majority of backend programs out there. But some applications have more exotic +requirements, generally to implement custom clustering techniques. These applications include Hadoop, MongoDB, Riak, RabbitMQ, +Disco, and all programs relying on Erlang's OTP. + +To support these applications, Docker needs to support more advanced redirection flavors, including: + +* RANDOM->RANDOM +* STATIC1->STATIC2 + +These flavors should be implemented without breaking existing semantics, if at all possible. diff --git a/hack/dockerbuilder/Dockerfile b/hack/dockerbuilder/Dockerfile index 5f9e9c35ab..5b2504d378 100644 --- a/hack/dockerbuilder/Dockerfile +++ b/hack/dockerbuilder/Dockerfile @@ -13,7 +13,7 @@ run apt-get update # Packages required to checkout, build and upload docker run DEBIAN_FRONTEND=noninteractive apt-get install -y -q s3cmd run DEBIAN_FRONTEND=noninteractive apt-get install -y -q curl -run curl -s -o /go.tar.gz https://go.googlecode.com/files/go1.1.linux-amd64.tar.gz +run curl -s -o /go.tar.gz https://go.googlecode.com/files/go1.1.1.linux-amd64.tar.gz run tar -C /usr/local -xzf /go.tar.gz run echo "export PATH=/usr/local/go/bin:$PATH" > /.bashrc run echo "export PATH=/usr/local/go/bin:$PATH" > /.bash_profile diff --git a/hack/dockerbuilder/MAITAINERS b/hack/dockerbuilder/MAINTAINERS similarity index 100% rename from hack/dockerbuilder/MAITAINERS rename to hack/dockerbuilder/MAINTAINERS diff --git a/image.go b/image.go index 7a98ef41a1..cd76b8c432 100644 --- a/image.go +++ b/image.go @@ -13,6 +13,7 @@ import ( "os" "os/exec" "path" + "path/filepath" "strings" "time" ) @@ -29,6 +30,7 @@ type Image struct { Config *Config `json:"config,omitempty"` Architecture string `json:"architecture,omitempty"` graph *Graph + Size int64 } func LoadImage(root string) (*Image, error) { @@ -94,6 +96,18 @@ func StoreImage(img *Image, layerData Archive, root string, store bool) error { if err := Untar(layerData, layer); err != nil { return err } + + return StoreSize(img, root) +} + +func StoreSize(img *Image, root string) error { + layer := layerPath(root) + + filepath.Walk(layer, func(path string, fileInfo os.FileInfo, err error) error { + img.Size += fileInfo.Size() + return nil + }) + // Store the json ball jsonData, err := json.Marshal(img) if err != nil { @@ -126,6 +140,8 @@ func MountAUFS(ro []string, rw string, target string) error { } branches := fmt.Sprintf("br:%v:%v", rwBranch, roBranches) + branches += ",xino=/dev/shm/aufs.xino" + //if error, try to load aufs kernel module if err := mount("none", target, "aufs", 0, branches); err != nil { log.Printf("Kernel does not support AUFS, trying to load the AUFS module with modprobe...") @@ -361,6 +377,15 @@ func (img *Image) Checksum() (string, error) { return hash, nil } +func (img *Image) getParentsSize(size int64) int64 { + parentImage, err := img.GetParent() + if err != nil || parentImage == nil { + return size + } + size += parentImage.Size + return parentImage.getParentsSize(size) +} + // Build an Image object from raw json data func NewImgJSON(src []byte) (*Image, error) { ret := &Image{} diff --git a/network.go b/network.go index 690d252ec2..ea5e5c8586 100644 --- a/network.go +++ b/network.go @@ -485,20 +485,38 @@ type Nat struct { func parseNat(spec string) (*Nat, error) { var nat Nat - // If spec starts with ':', external and internal ports must be the same. - // This might fail if the requested external port is not available. - var sameFrontend bool - if spec[0] == ':' { - sameFrontend = true - spec = spec[1:] - } - port, err := strconv.ParseUint(spec, 10, 16) - if err != nil { - return nil, err - } - nat.Backend = int(port) - if sameFrontend { - nat.Frontend = nat.Backend + + if strings.Contains(spec, ":") { + specParts := strings.Split(spec, ":") + if len(specParts) != 2 { + return nil, fmt.Errorf("Invalid port format.") + } + // If spec starts with ':', external and internal ports must be the same. + // This might fail if the requested external port is not available. + var sameFrontend bool + if len(specParts[0]) == 0 { + sameFrontend = true + } else { + front, err := strconv.ParseUint(specParts[0], 10, 16) + if err != nil { + return nil, err + } + nat.Frontend = int(front) + } + back, err := strconv.ParseUint(specParts[1], 10, 16) + if err != nil { + return nil, err + } + nat.Backend = int(back) + if sameFrontend { + nat.Frontend = nat.Backend + } + } else { + port, err := strconv.ParseUint(spec, 10, 16) + if err != nil { + return nil, err + } + nat.Backend = int(port) } nat.Proto = "tcp" return &nat, nil diff --git a/network_test.go b/network_test.go index df19a73d74..8e56c04ac2 100644 --- a/network_test.go +++ b/network_test.go @@ -18,6 +18,32 @@ func TestIptables(t *testing.T) { } } +func TestParseNat(t *testing.T) { + if nat, err := parseNat("4500"); err == nil { + if nat.Frontend != 0 || nat.Backend != 4500 { + t.Errorf("-p 4500 should produce 0->4500, got %d->%d", nat.Frontend, nat.Backend) + } + } else { + t.Fatal(err) + } + + if nat, err := parseNat(":4501"); err == nil { + if nat.Frontend != 4501 || nat.Backend != 4501 { + t.Errorf("-p :4501 should produce 4501->4501, got %d->%d", nat.Frontend, nat.Backend) + } + } else { + t.Fatal(err) + } + + if nat, err := parseNat("4502:4503"); err == nil { + if nat.Frontend != 4502 || nat.Backend != 4503 { + t.Errorf("-p 4502:4503 should produce 4502->4503, got %d->%d", nat.Frontend, nat.Backend) + } + } else { + t.Fatal(err) + } +} + func TestPortAllocation(t *testing.T) { allocator, err := newPortAllocator() if err != nil { diff --git a/registry/registry.go b/registry/registry.go index a2b43eeda1..18bdad26f2 100644 --- a/registry/registry.go +++ b/registry/registry.go @@ -334,7 +334,7 @@ func (r *Registry) PushRegistryTag(remote, revision, tag, registry string, token return nil } -func (r *Registry) PushImageJSONIndex(remote string, imgList []*ImgData, validate bool) (*RepositoryData, error) { +func (r *Registry) PushImageJSONIndex(remote string, imgList []*ImgData, validate bool, regs []string) (*RepositoryData, error) { imgListJSON, err := json.Marshal(imgList) if err != nil { return nil, err @@ -353,6 +353,9 @@ func (r *Registry) PushImageJSONIndex(remote string, imgList []*ImgData, validat req.SetBasicAuth(r.authConfig.Username, r.authConfig.Password) req.ContentLength = int64(len(imgListJSON)) req.Header.Set("X-Docker-Token", "true") + if validate { + req.Header["X-Docker-Endpoints"] = regs + } res, err := r.client.Do(req) if err != nil { @@ -370,7 +373,9 @@ func (r *Registry) PushImageJSONIndex(remote string, imgList []*ImgData, validat req.SetBasicAuth(r.authConfig.Username, r.authConfig.Password) req.ContentLength = int64(len(imgListJSON)) req.Header.Set("X-Docker-Token", "true") - + if validate { + req.Header["X-Docker-Endpoints"] = regs + } res, err = r.client.Do(req) if err != nil { return nil, err @@ -479,10 +484,7 @@ type Registry struct { authConfig *auth.AuthConfig } -func NewRegistry(root string) *Registry { - // If the auth file does not exist, keep going - authConfig, _ := auth.LoadConfig(root) - +func NewRegistry(root string, authConfig *auth.AuthConfig) *Registry { httpTransport := &http.Transport{ DisableKeepAlives: true, Proxy: http.ProxyFromEnvironment, diff --git a/runtime_test.go b/runtime_test.go index 642c8a0ce6..d7d9a5a315 100644 --- a/runtime_test.go +++ b/runtime_test.go @@ -17,7 +17,7 @@ import ( ) const unitTestImageName string = "docker-ut" - +const unitTestImageId string = "e9aa60c60128cad1" const unitTestStoreBase string = "/var/lib/docker/unit-tests" func nuke(runtime *Runtime) error { @@ -68,7 +68,7 @@ func init() { runtime: runtime, } // Retrieve the Image - if err := srv.ImagePull(unitTestImageName, "", "", os.Stdout, utils.NewStreamFormatter(false)); err != nil { + if err := srv.ImagePull(unitTestImageName, "", "", os.Stdout, utils.NewStreamFormatter(false), nil); err != nil { panic(err) } } diff --git a/server.go b/server.go index 37a053abbb..a3ffc55f68 100644 --- a/server.go +++ b/server.go @@ -1,6 +1,7 @@ package docker import ( + "errors" "fmt" "github.com/dotcloud/docker/auth" "github.com/dotcloud/docker/registry" @@ -54,7 +55,7 @@ func (srv *Server) ContainerExport(name string, out io.Writer) error { func (srv *Server) ImagesSearch(term string) ([]APISearch, error) { - results, err := registry.NewRegistry(srv.runtime.root).SearchRepositories(term) + results, err := registry.NewRegistry(srv.runtime.root, nil).SearchRepositories(term) if err != nil { return nil, err } @@ -63,9 +64,6 @@ func (srv *Server) ImagesSearch(term string) ([]APISearch, error) { for _, repo := range results.Results { var out APISearch out.Description = repo["description"] - if len(out.Description) > 45 { - out.Description = utils.Trunc(out.Description, 42) + "..." - } out.Name = repo["name"] outs = append(outs, out) } @@ -176,6 +174,8 @@ func (srv *Server) Images(all bool, filter string) ([]APIImages, error) { out.Tag = tag out.ID = image.ID out.Created = image.Created.Unix() + out.Size = image.Size + out.VirtualSize = image.getParentsSize(0) + image.Size outs = append(outs, out) } } @@ -185,6 +185,8 @@ func (srv *Server) Images(all bool, filter string) ([]APIImages, error) { var out APIImages out.ID = image.ID out.Created = image.Created.Unix() + out.Size = image.Size + out.VirtualSize = image.getParentsSize(0) + image.Size outs = append(outs, out) } } @@ -270,6 +272,8 @@ func (srv *Server) Containers(all bool, n int, since, before string) []APIContai c.Created = container.Created.Unix() c.Status = container.State.String() c.Ports = container.NetworkSettings.PortMappingHuman() + c.SizeRw, c.SizeRootFs = container.GetSize() + retContainers = append(retContainers, c) } return retContainers @@ -330,8 +334,8 @@ func (srv *Server) pullImage(r *registry.Registry, out io.Writer, imgId, endpoin return nil } -func (srv *Server) pullRepository(r *registry.Registry, out io.Writer, remote, askedTag string, sf *utils.StreamFormatter) error { - out.Write(sf.FormatStatus("Pulling repository %s from %s", remote, auth.IndexServerAddress())) +func (srv *Server) pullRepository(r *registry.Registry, out io.Writer, local, remote, askedTag string, sf *utils.StreamFormatter) error { + out.Write(sf.FormatStatus("Pulling repository %s from %s", local, auth.IndexServerAddress())) repoData, err := r.GetRepositoryData(remote) if err != nil { return err @@ -358,7 +362,7 @@ func (srv *Server) pullRepository(r *registry.Registry, out io.Writer, remote, a // Otherwise, check that the tag exists and use only that one id, exists := tagsList[askedTag] if !exists { - return fmt.Errorf("Tag %s not found in repositoy %s", askedTag, remote) + return fmt.Errorf("Tag %s not found in repositoy %s", askedTag, local) } repoData.ImgList[id].Tag = askedTag } @@ -386,7 +390,7 @@ func (srv *Server) pullRepository(r *registry.Registry, out io.Writer, remote, a if askedTag != "" && tag != askedTag { continue } - if err := srv.runtime.repositories.Set(remote, tag, id, true); err != nil { + if err := srv.runtime.repositories.Set(local, tag, id, true); err != nil { return err } } @@ -397,8 +401,8 @@ func (srv *Server) pullRepository(r *registry.Registry, out io.Writer, remote, a return nil } -func (srv *Server) ImagePull(name, tag, endpoint string, out io.Writer, sf *utils.StreamFormatter) error { - r := registry.NewRegistry(srv.runtime.root) +func (srv *Server) ImagePull(name, tag, endpoint string, out io.Writer, sf *utils.StreamFormatter, authConfig *auth.AuthConfig) error { + r := registry.NewRegistry(srv.runtime.root, authConfig) out = utils.NewWriteFlusher(out) if endpoint != "" { if err := srv.pullImage(r, out, name, endpoint, nil, sf); err != nil { @@ -406,8 +410,12 @@ func (srv *Server) ImagePull(name, tag, endpoint string, out io.Writer, sf *util } return nil } - - if err := srv.pullRepository(r, out, name, tag, sf); err != nil { + remote := name + parts := strings.Split(name, "/") + if len(parts) > 2 { + remote = fmt.Sprintf("src/%s", url.QueryEscape(strings.Join(parts, "/"))) + } + if err := srv.pullRepository(r, out, name, remote, tag, sf); err != nil { return err } @@ -489,7 +497,13 @@ func (srv *Server) pushRepository(r *registry.Registry, out io.Writer, name stri } out.Write(sf.FormatStatus("Sending image list")) - repoData, err := r.PushImageJSONIndex(name, imgList, false) + srvName := name + parts := strings.Split(name, "/") + if len(parts) > 2 { + srvName = fmt.Sprintf("src/%s", url.QueryEscape(strings.Join(parts, "/"))) + } + + repoData, err := r.PushImageJSONIndex(srvName, imgList, false, nil) if err != nil { return err } @@ -506,14 +520,14 @@ func (srv *Server) pushRepository(r *registry.Registry, out io.Writer, name stri // FIXME: Continue on error? return err } - out.Write(sf.FormatStatus("Pushing tags for rev [%s] on {%s}", elem.ID, ep+"/users/"+name+"/"+elem.Tag)) - if err := r.PushRegistryTag(name, elem.ID, elem.Tag, ep, repoData.Tokens); err != nil { + out.Write(sf.FormatStatus("Pushing tags for rev [%s] on {%s}", elem.ID, ep+"/users/"+srvName+"/"+elem.Tag)) + if err := r.PushRegistryTag(srvName, elem.ID, elem.Tag, ep, repoData.Tokens); err != nil { return err } } } - if _, err := r.PushImageJSONIndex(name, imgList, true); err != nil { + if _, err := r.PushImageJSONIndex(srvName, imgList, true, repoData.Endpoints); err != nil { return err } return nil @@ -579,10 +593,10 @@ func (srv *Server) pushImage(r *registry.Registry, out io.Writer, remote, imgId, return nil } -func (srv *Server) ImagePush(name, endpoint string, out io.Writer, sf *utils.StreamFormatter) error { +func (srv *Server) ImagePush(name, endpoint string, out io.Writer, sf *utils.StreamFormatter, authConfig *auth.AuthConfig) error { out = utils.NewWriteFlusher(out) img, err := srv.runtime.graph.Get(name) - r := registry.NewRegistry(srv.runtime.root) + r := registry.NewRegistry(srv.runtime.root, authConfig) if err != nil { out.Write(sf.FormatStatus("The push refers to a repository [%s] (len: %d)", name, len(srv.runtime.repositories.Repositories[name]))) @@ -710,17 +724,112 @@ func (srv *Server) ContainerDestroy(name string, removeVolume bool) error { return nil } -func (srv *Server) ImageDelete(name string) error { - img, err := srv.runtime.repositories.LookupImage(name) - if err != nil { - return fmt.Errorf("No such image: %s", name) +var ErrImageReferenced = errors.New("Image referenced by a repository") + +func (srv *Server) deleteImageAndChildren(id string, imgs *[]APIRmi) error { + // If the image is referenced by a repo, do not delete + if len(srv.runtime.repositories.ByID()[id]) != 0 { + return ErrImageReferenced } - if err := srv.runtime.graph.Delete(img.ID); err != nil { - return fmt.Errorf("Error deleting image %s: %s", name, err.Error()) + + // If the image is not referenced but has children, go recursive + referenced := false + byParents, err := srv.runtime.graph.ByParent() + if err != nil { + return err + } + for _, img := range byParents[id] { + if err := srv.deleteImageAndChildren(img.ID, imgs); err != nil { + if err != ErrImageReferenced { + return err + } + referenced = true + } + } + if referenced { + return ErrImageReferenced + } + + // If the image is not referenced and has no children, remove it + byParents, err = srv.runtime.graph.ByParent() + if err != nil { + return err + } + if len(byParents[id]) == 0 { + if err := srv.runtime.repositories.DeleteAll(id); err != nil { + return err + } + err := srv.runtime.graph.Delete(id) + if err != nil { + return err + } + *imgs = append(*imgs, APIRmi{Deleted: utils.TruncateID(id)}) + return nil } return nil } +func (srv *Server) deleteImageParents(img *Image, imgs *[]APIRmi) error { + if img.Parent != "" { + parent, err := srv.runtime.graph.Get(img.Parent) + if err != nil { + return err + } + // Remove all children images + if err := srv.deleteImageAndChildren(img.Parent, imgs); err != nil { + return err + } + return srv.deleteImageParents(parent, imgs) + } + return nil +} + +func (srv *Server) deleteImage(img *Image, repoName, tag string) (*[]APIRmi, error) { + //Untag the current image + var imgs []APIRmi + tagDeleted, err := srv.runtime.repositories.Delete(repoName, tag) + if err != nil { + return nil, err + } + if tagDeleted { + imgs = append(imgs, APIRmi{Untagged: img.ShortID()}) + } + if len(srv.runtime.repositories.ByID()[img.ID]) == 0 { + if err := srv.deleteImageAndChildren(img.ID, &imgs); err != nil { + if err != ErrImageReferenced { + return &imgs, err + } + } else if err := srv.deleteImageParents(img, &imgs); err != nil { + if err != ErrImageReferenced { + return &imgs, err + } + } + } + return &imgs, nil +} + +func (srv *Server) ImageDelete(name string, autoPrune bool) (*[]APIRmi, error) { + img, err := srv.runtime.repositories.LookupImage(name) + if err != nil { + return nil, fmt.Errorf("No such image: %s", name) + } + if !autoPrune { + if err := srv.runtime.graph.Delete(img.ID); err != nil { + return nil, fmt.Errorf("Error deleting image %s: %s", name, err.Error()) + } + return nil, nil + } + + var tag string + if strings.Contains(name, ":") { + nameParts := strings.Split(name, ":") + name = nameParts[0] + tag = nameParts[1] + } + + return srv.deleteImage(img, name, tag) +} + func (srv *Server) ImageGetCached(imgId string, config *Config) (*Image, error) { // Retrieve all images @@ -869,7 +978,7 @@ func (srv *Server) ImageInspect(name string) (*Image, error) { return nil, fmt.Errorf("No such image: %s", name) } -func NewServer(autoRestart bool) (*Server, error) { +func NewServer(autoRestart, enableCors bool) (*Server, error) { if runtime.GOARCH != "amd64" { log.Fatalf("The docker runtime currently only supports amd64 (not %s). This will change in the future. Aborting.", runtime.GOARCH) } @@ -878,12 +987,14 @@ func NewServer(autoRestart bool) (*Server, error) { return nil, err } srv := &Server{ - runtime: runtime, + runtime: runtime, + enableCors: enableCors, } runtime.srv = srv return srv, nil } type Server struct { - runtime *Runtime + runtime *Runtime + enableCors bool } diff --git a/server_test.go b/server_test.go index ab43dd77e8..532757c61e 100644 --- a/server_test.go +++ b/server_test.go @@ -4,6 +4,58 @@ import ( "testing" ) +func TestContainerTagImageDelete(t *testing.T) { + runtime, err := newTestRuntime() + if err != nil { + t.Fatal(err) + } + defer nuke(runtime) + + srv := &Server{runtime: runtime} + + if err := srv.runtime.repositories.Set("utest", "tag1", unitTestImageName, false); err != nil { + t.Fatal(err) + } + if err := srv.runtime.repositories.Set("utest/docker", "tag2", unitTestImageName, false); err != nil { + t.Fatal(err) + } + + images, err := srv.Images(false, "") + if err != nil { + t.Fatal(err) + } + + if len(images) != 3 { + t.Errorf("Excepted 3 images, %d found", len(images)) + } + + if _, err := srv.ImageDelete("utest/docker:tag2", true); err != nil { + t.Fatal(err) + } + + images, err = srv.Images(false, "") + if err != nil { + t.Fatal(err) + } + + if len(images) != 2 { + t.Errorf("Excepted 2 images, %d found", len(images)) + } + + if _, err := srv.ImageDelete("utest:tag1", true); err != nil { + t.Fatal(err) + } + + images, err = srv.Images(false, "") + if err != nil { + t.Fatal(err) + } + + if len(images) != 1 { + t.Errorf("Excepted 1 image, %d found", len(images)) + } +} + func TestCreateRm(t *testing.T) { runtime, err := newTestRuntime() if err != nil { diff --git a/tags.go b/tags.go index d862289ce6..33ec4e149d 100644 --- a/tags.go +++ b/tags.go @@ -110,6 +110,52 @@ func (store *TagStore) ImageName(id string) string { return utils.TruncateID(id) } +func (store *TagStore) DeleteAll(id string) error { + names, exists := store.ByID()[id] + if !exists || len(names) == 0 { + return nil + } + for _, name := range names { + if strings.Contains(name, ":") { + nameParts := strings.Split(name, ":") + if _, err := store.Delete(nameParts[0], nameParts[1]); err != nil { + return err + } + } else { + if _, err := store.Delete(name, ""); err != nil { + return err + } + } + } + return nil +} + +func (store *TagStore) Delete(repoName, tag string) (bool, error) { + deleted := false + if err := store.Reload(); err != nil { + return false, err + } + if r, exists := store.Repositories[repoName]; exists { + if tag != "" { + if _, exists2 := r[tag]; exists2 { + delete(r, tag) + if len(r) == 0 { + delete(store.Repositories, repoName) + } + deleted = true + } else { + return false, fmt.Errorf("No such tag: %s:%s", repoName, tag) + } + } else { + delete(store.Repositories, repoName) + deleted = true + } + } else { + fmt.Errorf("No such repository: %s", repoName) + } + return deleted, store.Save() +} + func (store *TagStore) Set(repoName, tag, imageName string, force bool) error { img, err := store.LookupImage(imageName) if err != nil { @@ -133,7 +179,7 @@ func (store *TagStore) Set(repoName, tag, imageName string, force bool) error { } else { repo = make(map[string]string) if old, exists := store.Repositories[repoName]; exists && !force { - return fmt.Errorf("Tag %s:%s is already set to %s", repoName, tag, old) + return fmt.Errorf("Conflict: Tag %s:%s is already set to %s", repoName, tag, old) } store.Repositories[repoName] = repo } @@ -151,14 +197,20 @@ func (store *TagStore) Get(repoName string) (Repository, error) { return nil, nil } -func (store *TagStore) GetImage(repoName, tag string) (*Image, error) { +func (store *TagStore) GetImage(repoName, tagOrId string) (*Image, error) { repo, err := store.Get(repoName) if err != nil { return nil, err } else if repo == nil { return nil, nil } - if revision, exists := repo[tag]; exists { + //go through all the tags, to see if tag is in fact an ID + for _, revision := range repo { + if strings.HasPrefix(revision, tagOrId) { + return store.graph.Get(revision) + } + } + if revision, exists := repo[tagOrId]; exists { return store.graph.Get(revision) } return nil, nil diff --git a/tags_test.go b/tags_test.go new file mode 100644 index 0000000000..90bc056406 --- /dev/null +++ b/tags_test.go @@ -0,0 +1,49 @@ +package docker + +import ( + "testing" +) + +func TestLookupImage(t *testing.T) { + runtime, err := newTestRuntime() + if err != nil { + t.Fatal(err) + } + defer nuke(runtime) + + if img, err := runtime.repositories.LookupImage(unitTestImageName); err != nil { + t.Fatal(err) + } else if img == nil { + t.Errorf("Expected 1 image, none found") + } + + if img, err := runtime.repositories.LookupImage(unitTestImageName + ":" + DEFAULTTAG); err != nil { + t.Fatal(err) + } else if img == nil { + t.Errorf("Expected 1 image, none found") + } + + if img, err := runtime.repositories.LookupImage(unitTestImageName + ":" + "fail"); err == nil { + t.Errorf("Expected error, none found") + } else if img != nil { + t.Errorf("Expected 0 image, 1 found") + } + + if img, err := runtime.repositories.LookupImage("fail:fail"); err == nil { + t.Errorf("Expected error, none found") + } else if img != nil { + t.Errorf("Expected 0 image, 1 found") + } + + if img, err := runtime.repositories.LookupImage(unitTestImageId); err != nil { + t.Fatal(err) + } else if img == nil { + t.Errorf("Expected 1 image, none found") + } + + if img, err := runtime.repositories.LookupImage(unitTestImageName + ":" + unitTestImageId); err != nil { + t.Fatal(err) + } else if img == nil { + t.Errorf("Expected 1 image, none found") + } +} diff --git a/utils/utils.go b/utils/utils.go index b92b30633c..4c9f9eeee1 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -147,6 +147,20 @@ func HumanDuration(d time.Duration) string { return fmt.Sprintf("%d years", d.Hours()/24/365) } +// HumanSize returns a human-readable approximation of a size +// using SI standard (eg. "44kB", "17MB") +func HumanSize(size int64) string { + i := 0 + var sizef float64 + sizef = float64(size) + units := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"} + for sizef >= 1000.0 { + sizef = sizef / 1000.0 + i++ + } + return fmt.Sprintf("%.4g %s", sizef, units[i]) +} + func Trunc(s string, maxlen int) string { if len(s) <= maxlen { return s diff --git a/utils/utils_test.go b/utils/utils_test.go index 4413f44efc..eec06d5134 100644 --- a/utils/utils_test.go +++ b/utils/utils_test.go @@ -261,3 +261,16 @@ func TestCompareKernelVersion(t *testing.T) { &KernelVersionInfo{Kernel: 3, Major: 8, Minor: 0, Flavor: "0"}, -1) } + +func TestHumanSize(t *testing.T) { + + size1000 := HumanSize(1000) + if size1000 != "1 kB" { + t.Errorf("1000 -> expected 1 kB, got %s", size1000) + } + + size1024 := HumanSize(1024) + if size1024 != "1.024 kB" { + t.Errorf("1024 -> expected 1.024 kB, got %s", size1024) + } +}