From c80448c4d1836dd0e23953fa923f7a270d32df05 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Wed, 15 May 2013 13:11:39 +0000 Subject: [PATCH 01/55] improve rmi --- api_test.go | 47 +++++++++++++++++++++++++++++++++++++++++++-- server.go | 36 ++++++++++++++++++++++++++++++++++ server_test.go | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++ tags.go | 23 ++++++++++++++++++++++ 4 files changed, 156 insertions(+), 2 deletions(-) diff --git a/api_test.go b/api_test.go index 5096b05212..17075dae7f 100644 --- a/api_test.go +++ b/api_test.go @@ -1189,8 +1189,51 @@ func TestDeleteContainers(t *testing.T) { } 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)) + } + + r := httptest.NewRecorder() + if err := deleteImages(srv, r, nil, map[string]string{"name": "test:test"}); err != nil { + t.Fatal(err) + } + if r.Code != http.StatusNoContent { + t.Fatalf("%d NO CONTENT expected, received %d\n", http.StatusNoContent, r.Code) + } + + 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/server.go b/server.go index 453574946d..d749ead73c 100644 --- a/server.go +++ b/server.go @@ -439,9 +439,45 @@ func (srv *Server) ImageDelete(name string) error { if err != nil { return fmt.Errorf("No such image: %s", name) } else { + tag := "" + if strings.Contains(name, ":") { + nameParts := strings.Split(name, ":") + name = nameParts[0] + tag = nameParts[1] + } + // if the images is referenced several times + Debugf("Image %s referenced %d times", img.Id, len(srv.runtime.repositories.ById()[img.Id])) + if len(srv.runtime.repositories.ById()[img.Id]) > 1 { + // if it's repo:tag, try to delete the tag (docker rmi base:latest) + if tag != "" { + if err := srv.runtime.repositories.Delete(name, tag, img.Id); err != nil { + return err + } + return nil + } else { + // check if the image is referenced in another repo (base and user/base are the same, docker rmi user/base) + var other bool + for _, repoTag := range srv.runtime.repositories.ById()[img.Id] { + if !strings.Contains(repoTag, name+":") { + other = true + break + } + } + // if found in another repo, delete the repo, other delete the whole image (docker rmi base) + if other { + if err := srv.runtime.repositories.Delete(name, "", img.Id); err != nil { + return err + } + return nil + } + } + } if err := srv.runtime.graph.Delete(img.Id); err != nil { return fmt.Errorf("Error deleting image %s: %s", name, err.Error()) } + if err := srv.runtime.repositories.Delete(name, tag, img.Id); err != nil { + return err + } } return nil } diff --git a/server_test.go b/server_test.go index 7b90252864..fe1cbecd9b 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"); 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"); 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 1b9cd19c83..b7aa692477 100644 --- a/tags.go +++ b/tags.go @@ -109,6 +109,29 @@ func (store *TagStore) ImageName(id string) string { return TruncateId(id) } +func (store *TagStore) Delete(repoName, tag, imageName string) error { + if err := store.Reload(); err != nil { + return 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) + } + } else { + return fmt.Errorf("No such tag: %s:%s", repoName, tag) + } + } else { + delete(store.Repositories, repoName) + } + } else { + fmt.Errorf("No such repository: %s", repoName) + } + return store.Save() +} + func (store *TagStore) Set(repoName, tag, imageName string, force bool) error { img, err := store.LookupImage(imageName) if err != nil { From db1e965b657e7e184b9f4e136e9c0860769d8f68 Mon Sep 17 00:00:00 2001 From: "Guillaume J. Charmes" Date: Thu, 16 May 2013 11:27:50 -0700 Subject: [PATCH 02/55] Merge fixes + cleanup --- server.go | 62 +++++++++++++++++++++++++------------------------------ 1 file changed, 28 insertions(+), 34 deletions(-) diff --git a/server.go b/server.go index 8b97555ba6..03030e120f 100644 --- a/server.go +++ b/server.go @@ -699,46 +699,40 @@ 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) - } else { - tag := "" - if strings.Contains(name, ":") { - nameParts := strings.Split(name, ":") - name = nameParts[0] - tag = nameParts[1] + } + var tag string + if strings.Contains(name, ":") { + nameParts := strings.Split(name, ":") + name = nameParts[0] + tag = nameParts[1] + } + + // if the images is referenced several times + utils.Debugf("Image %s referenced %d times", img.Id, len(srv.runtime.repositories.ById()[img.Id])) + if len(srv.runtime.repositories.ById()[img.Id]) > 1 { + // if it's repo:tag, try to delete the tag (docker rmi base:latest) + if tag != "" { + if err := srv.runtime.repositories.Delete(name, tag, img.Id); err != nil { + return err + } + return nil } - // if the images is referenced several times - Debugf("Image %s referenced %d times", img.Id, len(srv.runtime.repositories.ById()[img.Id])) - if len(srv.runtime.repositories.ById()[img.Id]) > 1 { - // if it's repo:tag, try to delete the tag (docker rmi base:latest) - if tag != "" { - if err := srv.runtime.repositories.Delete(name, tag, img.Id); err != nil { + // check if the image is referenced in another repo (base and user/base are the same, docker rmi user/base) + for _, repoTag := range srv.runtime.repositories.ById()[img.Id] { + if !strings.Contains(repoTag, name+":") { + // if found in another repo, delete the repo, otherwie delete the whole image (docker rmi base) + if err := srv.runtime.repositories.Delete(name, "", img.Id); err != nil { return err } return nil - } else { - // check if the image is referenced in another repo (base and user/base are the same, docker rmi user/base) - var other bool - for _, repoTag := range srv.runtime.repositories.ById()[img.Id] { - if !strings.Contains(repoTag, name+":") { - other = true - break - } - } - // if found in another repo, delete the repo, other delete the whole image (docker rmi base) - if other { - if err := srv.runtime.repositories.Delete(name, "", img.Id); err != nil { - return err - } - return nil - } } } - if err := srv.runtime.graph.Delete(img.Id); err != nil { - return fmt.Errorf("Error deleting image %s: %s", name, err.Error()) - } - if err := srv.runtime.repositories.Delete(name, tag, img.Id); err != nil { - return err - } + } + if err := srv.runtime.graph.Delete(img.Id); err != nil { + return fmt.Errorf("Error deleting image %s: %s", name, err.Error()) + } + if err := srv.runtime.repositories.Delete(name, tag, img.Id); err != nil { + return err } return nil } From f01990aad2200690618cd64d2378e7e610433a71 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Fri, 17 May 2013 17:57:44 +0000 Subject: [PATCH 03/55] fix --- server.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/server.go b/server.go index 03030e120f..1ad1d80a4f 100644 --- a/server.go +++ b/server.go @@ -717,10 +717,11 @@ func (srv *Server) ImageDelete(name string) error { } return nil } - // check if the image is referenced in another repo (base and user/base are the same, docker rmi user/base) + // Let's say you have the same image referenced by base and base2 + // check if the image is referenced in another repo (docker rmi base) for _, repoTag := range srv.runtime.repositories.ById()[img.Id] { - if !strings.Contains(repoTag, name+":") { - // if found in another repo, delete the repo, otherwie delete the whole image (docker rmi base) + if !strings.HasPrefix(repoTag, name+":") { + // if found in another repo (base2) delete the repo base, otherwise delete the whole image if err := srv.runtime.repositories.Delete(name, "", img.Id); err != nil { return err } From d7673274d22140dc6dfa288351f1d4d2e2fa4b63 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Sat, 18 May 2013 14:29:32 +0000 Subject: [PATCH 04/55] check if the image to delete isn't parent of another --- server.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/server.go b/server.go index 1ad1d80a4f..d709aba7e2 100644 --- a/server.go +++ b/server.go @@ -729,6 +729,15 @@ func (srv *Server) ImageDelete(name string) error { } } } + // check is the image to delete isn't parent of another image + images, _ := srv.runtime.graph.All() + for _, image := range images { + if imgParent, err := image.GetParent(); err == nil && imgParent != nil { + if imgParent.Id == img.Id { + return fmt.Errorf("Can't delete %s, otherwise %s will be broken", name, image.ShortId()) + } + } + } if err := srv.runtime.graph.Delete(img.Id); err != nil { return fmt.Errorf("Error deleting image %s: %s", name, err.Error()) } From 67b20f2c8cc8d85ab09de95970e4d28c80f0aeb6 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Mon, 20 May 2013 18:31:45 +0000 Subject: [PATCH 05/55] add check to see if the image isn't parent of another and add -f to force --- api.go | 11 ++++++++++- api_test.go | 7 ++++++- commands.go | 8 +++++++- docs/sources/api/docker_remote_api.rst | 3 +++ server.go | 14 ++++++++------ server_test.go | 4 ++-- tags.go | 2 +- 7 files changed, 37 insertions(+), 12 deletions(-) diff --git a/api.go b/api.go index 8984d00cd9..53a895918c 100644 --- a/api.go +++ b/api.go @@ -36,6 +36,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 { http.Error(w, err.Error(), http.StatusInternalServerError) } @@ -453,11 +455,18 @@ func deleteContainers(srv *Server, w http.ResponseWriter, r *http.Request, vars } func deleteImages(srv *Server, 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 { + force, err := getBoolParam(r.Form.Get("force")) + if err != nil { + return err + } + if err := srv.ImageDelete(name, force); err != nil { return err } w.WriteHeader(http.StatusNoContent) diff --git a/api_test.go b/api_test.go index 921aebe4ce..283ebb130d 100644 --- a/api_test.go +++ b/api_test.go @@ -1312,8 +1312,13 @@ func TestDeleteImages(t *testing.T) { 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, r, nil, map[string]string{"name": "test:test"}); err != nil { + if err := deleteImages(srv, r, req, map[string]string{"name": "test:test"}); err != nil { t.Fatal(err) } if r.Code != http.StatusNoContent { diff --git a/commands.go b/commands.go index 33ba8125de..26cea0189e 100644 --- a/commands.go +++ b/commands.go @@ -459,6 +459,7 @@ func (cli *DockerCli) CmdPort(args ...string) error { // 'docker rmi IMAGE' removes all images with the name IMAGE func (cli *DockerCli) CmdRmi(args ...string) error { cmd := Subcmd("rmi", "IMAGE [IMAGE...]", "Remove an image") + force := cmd.Bool("f", false, "Force") if err := cmd.Parse(args); err != nil { return nil } @@ -467,8 +468,13 @@ func (cli *DockerCli) CmdRmi(args ...string) error { return nil } + v := url.Values{} + if *force { + v.Set("force", "1") + } + for _, name := range cmd.Args() { - _, _, err := cli.call("DELETE", "/images/"+name, nil) + _, _, err := cli.call("DELETE", "/images/"+name+"?"+v.Encode(), nil) if err != nil { fmt.Printf("%s", err) } else { diff --git a/docs/sources/api/docker_remote_api.rst b/docs/sources/api/docker_remote_api.rst index 03f1d4b9cf..9541e27de7 100644 --- a/docs/sources/api/docker_remote_api.rst +++ b/docs/sources/api/docker_remote_api.rst @@ -728,6 +728,7 @@ Tag an image into a repository :statuscode 200: no error :statuscode 400: bad parameter :statuscode 404: no such image + :statuscode 409: conflict :statuscode 500: server error @@ -750,8 +751,10 @@ Remove an image HTTP/1.1 204 OK + :query force: 1/True/true or 0/False/false, default false :statuscode 204: no error :statuscode 404: no such image + :statuscode 409: conflict :statuscode 500: server error diff --git a/server.go b/server.go index d90e7fda08..9a2ab4edf5 100644 --- a/server.go +++ b/server.go @@ -710,7 +710,7 @@ func (srv *Server) ContainerDestroy(name string, removeVolume bool) error { return nil } -func (srv *Server) ImageDelete(name string) error { +func (srv *Server) ImageDelete(name string, force bool) error { img, err := srv.runtime.repositories.LookupImage(name) if err != nil { return fmt.Errorf("No such image: %s", name) @@ -745,11 +745,13 @@ func (srv *Server) ImageDelete(name string) error { } } // check is the image to delete isn't parent of another image - images, _ := srv.runtime.graph.All() - for _, image := range images { - if imgParent, err := image.GetParent(); err == nil && imgParent != nil { - if imgParent.Id == img.Id { - return fmt.Errorf("Can't delete %s, otherwise %s will be broken", name, image.ShortId()) + if !force { + images, _ := srv.runtime.graph.All() + for _, image := range images { + if imgParent, err := image.GetParent(); err == nil && imgParent != nil { + if imgParent.Id == img.Id { + return fmt.Errorf("Conflict: Can't delete %s otherwise %s will be broken", name, image.ShortId()) + } } } } diff --git a/server_test.go b/server_test.go index fe1cbecd9b..f223fbe53d 100644 --- a/server_test.go +++ b/server_test.go @@ -29,7 +29,7 @@ func TestContainerTagImageDelete(t *testing.T) { t.Errorf("Excepted 3 images, %d found", len(images)) } - if err := srv.ImageDelete("utest/docker:tag2"); err != nil { + if err := srv.ImageDelete("utest/docker:tag2", true); err != nil { t.Fatal(err) } @@ -42,7 +42,7 @@ func TestContainerTagImageDelete(t *testing.T) { t.Errorf("Excepted 2 images, %d found", len(images)) } - if err := srv.ImageDelete("utest:tag1"); err != nil { + if err := srv.ImageDelete("utest:tag1", true); err != nil { t.Fatal(err) } diff --git a/tags.go b/tags.go index f6b3a93a95..4a54398c5f 100644 --- a/tags.go +++ b/tags.go @@ -156,7 +156,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 } From 2eb4e2a0b86f8d244f0b6cbeb0422f8499d834f8 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Wed, 29 May 2013 16:31:47 +0000 Subject: [PATCH 06/55] removed the -f --- api.go | 6 +----- api_test.go | 2 +- commands.go | 8 +------- server.go | 18 +++++++++++------- server_test.go | 4 ++-- 5 files changed, 16 insertions(+), 22 deletions(-) diff --git a/api.go b/api.go index 8f2befb237..1759257e6e 100644 --- a/api.go +++ b/api.go @@ -450,11 +450,7 @@ func deleteImages(srv *Server, version float64, w http.ResponseWriter, r *http.R return fmt.Errorf("Missing parameter") } name := vars["name"] - force, err := getBoolParam(r.Form.Get("force")) - if err != nil { - return err - } - if err := srv.ImageDelete(name, force); err != nil { + if err := srv.ImageDelete(name); err != nil { return err } w.WriteHeader(http.StatusNoContent) diff --git a/api_test.go b/api_test.go index fb1cd211ad..844d15cc13 100644 --- a/api_test.go +++ b/api_test.go @@ -1268,7 +1268,7 @@ func TestDeleteImages(t *testing.T) { } r := httptest.NewRecorder() - if err := deleteImages(srv, r, req, map[string]string{"name": "test:test"}); err != nil { + if err := deleteImages(srv, API_VERSION, r, req, map[string]string{"name": "test:test"}); err != nil { t.Fatal(err) } if r.Code != http.StatusNoContent { diff --git a/commands.go b/commands.go index 704f5ac48d..2af7e548fc 100644 --- a/commands.go +++ b/commands.go @@ -558,7 +558,6 @@ func (cli *DockerCli) CmdPort(args ...string) error { // 'docker rmi IMAGE' removes all images with the name IMAGE func (cli *DockerCli) CmdRmi(args ...string) error { cmd := Subcmd("rmi", "IMAGE [IMAGE...]", "Remove an image") - force := cmd.Bool("f", false, "Force") if err := cmd.Parse(args); err != nil { return nil } @@ -567,13 +566,8 @@ func (cli *DockerCli) CmdRmi(args ...string) error { return nil } - v := url.Values{} - if *force { - v.Set("force", "1") - } - for _, name := range cmd.Args() { - _, _, err := cli.call("DELETE", "/images/"+name+"?"+v.Encode(), nil) + _, _, err := cli.call("DELETE", "/images/"+name, nil) if err != nil { fmt.Printf("%s", err) } else { diff --git a/server.go b/server.go index d1a089c3a9..2e819728b4 100644 --- a/server.go +++ b/server.go @@ -704,7 +704,7 @@ func (srv *Server) ContainerDestroy(name string, removeVolume bool) error { return nil } -func (srv *Server) ImageDelete(name string, force bool) error { +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) @@ -739,13 +739,17 @@ func (srv *Server) ImageDelete(name string, force bool) error { } } // check is the image to delete isn't parent of another image - if !force { - images, _ := srv.runtime.graph.All() - for _, image := range images { - if imgParent, err := image.GetParent(); err == nil && imgParent != nil { - if imgParent.Id == img.Id { - return fmt.Errorf("Conflict: Can't delete %s otherwise %s will be broken", name, image.ShortId()) + images, _ := srv.runtime.graph.All() + for _, image := range images { + if imgParent, err := image.GetParent(); err == nil && imgParent != nil { + if imgParent.Id == img.Id { + if strings.Contains(img.Id, name) { + return fmt.Errorf("Conflict with %s, %s was not removed", image.ShortId(), name) } + if err := srv.runtime.repositories.Delete(name, tag, img.Id); err != nil { + return err + } + return nil } } } diff --git a/server_test.go b/server_test.go index 743b33d545..541c927b83 100644 --- a/server_test.go +++ b/server_test.go @@ -29,7 +29,7 @@ func TestContainerTagImageDelete(t *testing.T) { t.Errorf("Excepted 3 images, %d found", len(images)) } - if err := srv.ImageDelete("utest/docker:tag2", true); err != nil { + if err := srv.ImageDelete("utest/docker:tag2"); err != nil { t.Fatal(err) } @@ -42,7 +42,7 @@ func TestContainerTagImageDelete(t *testing.T) { t.Errorf("Excepted 2 images, %d found", len(images)) } - if err := srv.ImageDelete("utest:tag1", true); err != nil { + if err := srv.ImageDelete("utest:tag1"); err != nil { t.Fatal(err) } From 94f0d478de6e50e8b26e9779da22dbd74cc5952f Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Wed, 29 May 2013 17:01:54 +0000 Subject: [PATCH 07/55] refacto --- server.go | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/server.go b/server.go index 2e819728b4..64bde12c4d 100644 --- a/server.go +++ b/server.go @@ -739,19 +739,15 @@ func (srv *Server) ImageDelete(name string) error { } } // check is the image to delete isn't parent of another image - images, _ := srv.runtime.graph.All() - for _, image := range images { - if imgParent, err := image.GetParent(); err == nil && imgParent != nil { - if imgParent.Id == img.Id { - if strings.Contains(img.Id, name) { - return fmt.Errorf("Conflict with %s, %s was not removed", image.ShortId(), name) - } - if err := srv.runtime.repositories.Delete(name, tag, img.Id); err != nil { - return err - } - return nil - } + byParent, _ := srv.runtime.graph.ByParent() + if childs, exists := byParent[img.Id]; exists { + if strings.Contains(img.Id, name) { + return fmt.Errorf("Conflict with %s, %s was not removed", childs[0].ShortId(), name) } + if err := srv.runtime.repositories.Delete(name, tag, img.Id); err != nil { + return err + } + return nil } if err := srv.runtime.graph.Delete(img.Id); err != nil { return fmt.Errorf("Error deleting image %s: %s", name, err.Error()) From 7e92302c4fba259ac1128db548cf01dad9ad087c Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Wed, 29 May 2013 17:27:32 +0000 Subject: [PATCH 08/55] auto prune WIP --- server.go | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/server.go b/server.go index 64bde12c4d..69ead3aff6 100644 --- a/server.go +++ b/server.go @@ -749,11 +749,23 @@ func (srv *Server) ImageDelete(name string) error { } return nil } - if err := srv.runtime.graph.Delete(img.Id); err != nil { - return fmt.Errorf("Error deleting image %s: %s", name, err.Error()) - } - if err := srv.runtime.repositories.Delete(name, tag, img.Id); err != nil { - return err + parents, _ := img.History() + for _, parent := range parents { + byParent, _ = srv.runtime.graph.ByParent() + //stop if image has children + if _, exists := byParent[parent.Id]; exists { + break + } + //stop if image is tagged and it is not the first image we delete + if _, hasTags := srv.runtime.repositories.ById()[parent.Id]; hasTags && img.Id != parent.Id { + break + } + if err := srv.runtime.graph.Delete(parent.Id); err != nil { + return fmt.Errorf("Error deleting image %s: %s", name, err.Error()) + } + if err := srv.runtime.repositories.Delete(name, tag, img.Id); err != nil { + return err + } } return nil } From c05e9f856d5f12a1244924b02bad66ba5bacb550 Mon Sep 17 00:00:00 2001 From: "Guillaume J. Charmes" Date: Wed, 29 May 2013 11:11:19 -0700 Subject: [PATCH 09/55] Error output fix for docker rmi --- commands.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/commands.go b/commands.go index 2af7e548fc..0289048b89 100644 --- a/commands.go +++ b/commands.go @@ -567,9 +567,8 @@ func (cli *DockerCli) CmdRmi(args ...string) error { } for _, name := range cmd.Args() { - _, _, err := cli.call("DELETE", "/images/"+name, nil) - if err != nil { - fmt.Printf("%s", err) + if _, _, err := cli.call("DELETE", "/images/"+name, nil); err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) } else { fmt.Println(name) } From 49e656839fb3d846dc65fec544f2984a9c72a2cf Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Thu, 30 May 2013 15:39:43 +0000 Subject: [PATCH 10/55] move auth to the client WIP --- api.go | 50 ++++++-------------------- api_test.go | 43 +---------------------- auth/auth.go | 28 +++++---------- buildfile.go | 2 +- commands.go | 83 ++++++++++++++------------------------------ registry/registry.go | 5 +-- runtime_test.go | 2 +- server.go | 10 +++--- 8 files changed, 55 insertions(+), 168 deletions(-) diff --git a/api.go b/api.go index 3c440d7331..60d3b632cb 100644 --- a/api.go +++ b/api.go @@ -66,48 +66,15 @@ func getBoolParam(value string) (bool, error) { return false, fmt.Errorf("Bad parameter") } -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? - authConfig, err := auth.LoadConfig(srv.runtime.root) - if err != nil { - if err != auth.ErrConfigFileMissing { - return err - } - authConfig = &auth.AuthConfig{} - } - b, err := json.Marshal(&auth.AuthConfig{Username: authConfig.Username, Email: authConfig.Email}) - if err != nil { - return err - } - writeJson(w, b) - return nil -} - func postAuth(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { - // FIXME: Handle multiple login at once - config := &auth.AuthConfig{} - if err := json.NewDecoder(r.Body).Decode(config); err != nil { + authConfig := &auth.AuthConfig{} + if err := json.NewDecoder(r.Body).Decode(authConfig); err != nil { return err } - - authConfig, err := auth.LoadConfig(srv.runtime.root) - if err != nil { - if err != auth.ErrConfigFileMissing { - return err - } - authConfig = &auth.AuthConfig{} - } - if config.Username == authConfig.Username { - config.Password = authConfig.Password - } - - newAuthConfig := auth.NewAuthConfig(config.Username, config.Password, config.Email, srv.runtime.root) - status, err := auth.Login(newAuthConfig) + status, err := auth.Login(authConfig) if err != nil { return err } - if status != "" { b, err := json.Marshal(&ApiAuth{Status: status}) if err != nil { @@ -317,7 +284,9 @@ func postImagesCreate(srv *Server, version float64, w http.ResponseWriter, r *ht if version > 1.0 { w.Header().Set("Content-Type", "application/json") } - if err := srv.ImagePull(image, tag, registry, w, version > 1.0); err != nil { + authConfig := &auth.AuthConfig{} + json.NewDecoder(r.Body).Decode(authConfig) + if err := srv.ImagePull(image, tag, registry, w, version > 1.0, authConfig); err != nil { return err } } else { //import @@ -371,6 +340,10 @@ 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 err := json.NewDecoder(r.Body).Decode(authConfig); err != nil { + return err + } if err := parseForm(r); err != nil { return err } @@ -381,7 +354,7 @@ func postImagesPush(srv *Server, version float64, w http.ResponseWriter, r *http } name := vars["name"] - if err := srv.ImagePush(name, registry, w); err != nil { + if err := srv.ImagePush(name, registry, w, authConfig); err != nil { return err } return nil @@ -676,7 +649,6 @@ func ListenAndServe(addr string, srv *Server, logging bool) error { m := map[string]map[string]func(*Server, float64, http.ResponseWriter, *http.Request, map[string]string) error{ "GET": { - "/auth": getAuth, "/version": getVersion, "/info": getInfo, "/images/json": getImagesJson, diff --git a/api_test.go b/api_test.go index f364c6c895..b0f1444367 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, API_VERSION, 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 { diff --git a/auth/auth.go b/auth/auth.go index 9c34604419..5da0b30415 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 { @@ -121,7 +121,6 @@ func SaveConfig(rootPath, authStr string, email string) error { // try to register/login to the registry server func Login(authConfig *AuthConfig) (string, error) { - storeConfig := false client := &http.Client{} reqStatusCode := 0 var status string @@ -147,7 +146,6 @@ func Login(authConfig *AuthConfig) (string, error) { if reqStatusCode == 201 { status = "Account created. Please use the confirmation link we sent" + " to your e-mail to activate it.\n" - storeConfig = true } else if reqStatusCode == 403 { return "", fmt.Errorf("Login: Your account hasn't been activated. " + "Please check your e-mail for a confirmation link.") @@ -166,11 +164,7 @@ func Login(authConfig *AuthConfig) (string, error) { } if resp.StatusCode == 200 { status = "Login Succeeded\n" - storeConfig = true } else if resp.StatusCode == 401 { - if err := SaveConfig(authConfig.rootPath, "", ""); err != nil { - return "", err - } return "", fmt.Errorf("Wrong login/password, please try again") } else { return "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body, @@ -182,11 +176,5 @@ 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 { - return "", err - } - } return status, nil } diff --git a/buildfile.go b/buildfile.go index 23f2f47172..8a6d6305b0 100644 --- a/buildfile.go +++ b/buildfile.go @@ -63,7 +63,7 @@ func (b *buildFile) CmdFrom(name string) error { remote = name } - if err := b.srv.ImagePull(remote, tag, "", b.out, false); err != nil { + if err := b.srv.ImagePull(remote, tag, "", b.out, false, nil); err != nil { return err } diff --git a/commands.go b/commands.go index 97a249e54a..cbd276b455 100644 --- a/commands.go +++ b/commands.go @@ -279,27 +279,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) @@ -307,20 +296,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 } @@ -328,10 +318,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 @@ -688,13 +679,12 @@ 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) } v := url.Values{} @@ -726,7 +716,7 @@ func (cli *DockerCli) CmdPull(args ...string) error { } if strings.Contains(remote, "/") { - if _, err := cli.checkIfLogged(true, "pull"); err != nil { + if err := cli.checkIfLogged(true, "pull"); err != nil { return err } } @@ -1220,38 +1210,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) { @@ -1435,10 +1404,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/registry/registry.go b/registry/registry.go index 36b01d643a..cc5e7496b9 100644 --- a/registry/registry.go +++ b/registry/registry.go @@ -466,10 +466,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 { r := &Registry{ authConfig: authConfig, client: &http.Client{}, diff --git a/runtime_test.go b/runtime_test.go index 384cbd1eb8..40f6425e78 100644 --- a/runtime_test.go +++ b/runtime_test.go @@ -65,7 +65,7 @@ func init() { runtime: runtime, } // Retrieve the Image - if err := srv.ImagePull(unitTestImageName, "", "", os.Stdout, false); err != nil { + if err := srv.ImagePull(unitTestImageName, "", "", os.Stdout, false, nil); err != nil { panic(err) } } diff --git a/server.go b/server.go index 0440b0a8a4..42726fb59c 100644 --- a/server.go +++ b/server.go @@ -50,7 +50,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 } @@ -394,8 +394,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, json bool) error { - r := registry.NewRegistry(srv.runtime.root) +func (srv *Server) ImagePull(name, tag, endpoint string, out io.Writer, json bool, 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, json); err != nil { @@ -576,10 +576,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) error { +func (srv *Server) ImagePush(name, endpoint string, out io.Writer, 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 { fmt.Fprintf(out, "The push refers to a repository [%s] (len: %d)\n", name, len(srv.runtime.repositories.Repositories[name])) From 054451fd190f195e06d8a8aa65e83882f60b1f14 Mon Sep 17 00:00:00 2001 From: "Guillaume J. Charmes" Date: Thu, 30 May 2013 12:30:21 -0700 Subject: [PATCH 11/55] NON-WORKING: Beginning of rmi refactor --- server.go | 108 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 83 insertions(+), 25 deletions(-) diff --git a/server.go b/server.go index 69ead3aff6..126ac7d6e9 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" @@ -704,11 +705,86 @@ 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) +func (srv *Server) pruneImage(img *Image, repo, tag string) error { + return nil +} + +var ErrImageReferenced = errors.New("Image referenced by a repository") + +func (srv *Server) deleteImageChildren(id string, byId map[string][]string, byParents map[string][]*Image) error { + // If the image is referenced by a repo, do not delete + if len(byId[id]) != 0 { + return ErrImageReferenced } + // If the image is not referenced and has no children, remove it + if len(byParents[id]) == 0 { + return srv.runtime.graph.Delete(id) + } + + // If the image is not referenced but has children, go recursive + referenced := false + for _, img := range byParents[id] { + if err := srv.deleteImageChildren(img.Id, byId, byParents); err != nil { + if err != ErrImageReferenced { + return err + } else { + referenced = true + } + } + } + if referenced { + return ErrImageReferenced + } + return nil +} + +func (srv *Server) deleteImageParents(img *Image, byId map[string][]string, byParents map[string][]*Image) error { + if img.Parent != "" { + parent, err := srv.runtime.graph.Get(img.Parent) + if err != nil { + return err + } + // Remove all children images + if err := srv.deleteImageChildren(img.Id, byId, byParents); err != nil { + return err + } + // If no error (no referenced children), then remove the parent + if err := srv.runtime.graph.Delete(img.Parent); err != nil { + return err + } + return srv.deleteImageParents(parent, byId, byParents) + } + return nil +} + +func (srv *Server) deleteImage(repoName, tag string) error { + img, err := srv.runtime.repositories.LookupImage(repoName + ":" + tag) + if err != nil { + return fmt.Errorf("No such image: %s:%s", repoName, tag) + } + utils.Debugf("Image %s referenced %d times", img.Id, len(srv.runtime.repositories.ById()[img.Id])) + if err := srv.runtime.repositories.Delete(repoName, tag, img.Id); err != nil { + return err + } + byId := srv.runtime.repositories.ById() + if len(byId[img.Id]) == 0 { + byParents, err := srv.runtime.graph.ByParent() + if err != nil { + return err + } + if err := srv.deleteImageChildren(img.Id, byId, byParents); err != nil { + if err != ErrImageReferenced { + return err + } + } else { + srv.deleteImageParents(img) + } + + } + return nil +} + +func (srv *Server) ImageDelete(name string) error { var tag string if strings.Contains(name, ":") { nameParts := strings.Split(name, ":") @@ -716,28 +792,10 @@ func (srv *Server) ImageDelete(name string) error { tag = nameParts[1] } + srv.deleteImage(name, tag) + // if the images is referenced several times - utils.Debugf("Image %s referenced %d times", img.Id, len(srv.runtime.repositories.ById()[img.Id])) - if len(srv.runtime.repositories.ById()[img.Id]) > 1 { - // if it's repo:tag, try to delete the tag (docker rmi base:latest) - if tag != "" { - if err := srv.runtime.repositories.Delete(name, tag, img.Id); err != nil { - return err - } - return nil - } - // Let's say you have the same image referenced by base and base2 - // check if the image is referenced in another repo (docker rmi base) - for _, repoTag := range srv.runtime.repositories.ById()[img.Id] { - if !strings.HasPrefix(repoTag, name+":") { - // if found in another repo (base2) delete the repo base, otherwise delete the whole image - if err := srv.runtime.repositories.Delete(name, "", img.Id); err != nil { - return err - } - return nil - } - } - } + // check is the image to delete isn't parent of another image byParent, _ := srv.runtime.graph.ByParent() if childs, exists := byParent[img.Id]; exists { From 5aa95b667c5986e3cea45b93bb2370cd46deeea3 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Thu, 30 May 2013 22:53:45 +0000 Subject: [PATCH 12/55] WIP needs to fix HTTP error codes --- server.go | 106 +++++++++++++++++++----------------------------------- tags.go | 22 +++++++++++- 2 files changed, 58 insertions(+), 70 deletions(-) diff --git a/server.go b/server.go index 126ac7d6e9..2af1e3cd8f 100644 --- a/server.go +++ b/server.go @@ -705,26 +705,22 @@ func (srv *Server) ContainerDestroy(name string, removeVolume bool) error { return nil } -func (srv *Server) pruneImage(img *Image, repo, tag string) error { - return nil -} - var ErrImageReferenced = errors.New("Image referenced by a repository") -func (srv *Server) deleteImageChildren(id string, byId map[string][]string, byParents map[string][]*Image) error { +func (srv *Server) deleteImageAndChildren(id string) error { // If the image is referenced by a repo, do not delete - if len(byId[id]) != 0 { + if len(srv.runtime.repositories.ById()[id]) != 0 { return ErrImageReferenced } - // If the image is not referenced and has no children, remove it - if len(byParents[id]) == 0 { - return srv.runtime.graph.Delete(id) - } // 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.deleteImageChildren(img.Id, byId, byParents); err != nil { + if err := srv.deleteImageAndChildren(img.Id); err != nil { if err != ErrImageReferenced { return err } else { @@ -735,56 +731,61 @@ func (srv *Server) deleteImageChildren(id string, byId map[string][]string, byPa 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 + } + return srv.runtime.graph.Delete(id) + } return nil } -func (srv *Server) deleteImageParents(img *Image, byId map[string][]string, byParents map[string][]*Image) error { +func (srv *Server) deleteImageParents(img *Image) error { if img.Parent != "" { parent, err := srv.runtime.graph.Get(img.Parent) if err != nil { return err } // Remove all children images - if err := srv.deleteImageChildren(img.Id, byId, byParents); err != nil { + if err := srv.deleteImageAndChildren(img.Parent); err != nil { return err } - // If no error (no referenced children), then remove the parent - if err := srv.runtime.graph.Delete(img.Parent); err != nil { - return err - } - return srv.deleteImageParents(parent, byId, byParents) + return srv.deleteImageParents(parent) } return nil } -func (srv *Server) deleteImage(repoName, tag string) error { - img, err := srv.runtime.repositories.LookupImage(repoName + ":" + tag) - if err != nil { - return fmt.Errorf("No such image: %s:%s", repoName, tag) - } - utils.Debugf("Image %s referenced %d times", img.Id, len(srv.runtime.repositories.ById()[img.Id])) - if err := srv.runtime.repositories.Delete(repoName, tag, img.Id); err != nil { +func (srv *Server) deleteImage(img *Image, repoName, tag string) error { + //Untag the current image + if err := srv.runtime.repositories.Delete(repoName, tag); err != nil { return err } - byId := srv.runtime.repositories.ById() - if len(byId[img.Id]) == 0 { - byParents, err := srv.runtime.graph.ByParent() - if err != nil { - return err - } - if err := srv.deleteImageChildren(img.Id, byId, byParents); err != nil { + if len(srv.runtime.repositories.ById()[img.Id]) == 0 { + if err := srv.deleteImageAndChildren(img.Id); err != nil { + if err != ErrImageReferenced { + return err + } + } else if err := srv.deleteImageParents(img); err != nil { if err != ErrImageReferenced { return err } - } else { - srv.deleteImageParents(img) } - } 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 tag string if strings.Contains(name, ":") { nameParts := strings.Split(name, ":") @@ -792,40 +793,7 @@ func (srv *Server) ImageDelete(name string) error { tag = nameParts[1] } - srv.deleteImage(name, tag) - - // if the images is referenced several times - - // check is the image to delete isn't parent of another image - byParent, _ := srv.runtime.graph.ByParent() - if childs, exists := byParent[img.Id]; exists { - if strings.Contains(img.Id, name) { - return fmt.Errorf("Conflict with %s, %s was not removed", childs[0].ShortId(), name) - } - if err := srv.runtime.repositories.Delete(name, tag, img.Id); err != nil { - return err - } - return nil - } - parents, _ := img.History() - for _, parent := range parents { - byParent, _ = srv.runtime.graph.ByParent() - //stop if image has children - if _, exists := byParent[parent.Id]; exists { - break - } - //stop if image is tagged and it is not the first image we delete - if _, hasTags := srv.runtime.repositories.ById()[parent.Id]; hasTags && img.Id != parent.Id { - break - } - if err := srv.runtime.graph.Delete(parent.Id); err != nil { - return fmt.Errorf("Error deleting image %s: %s", name, err.Error()) - } - if err := srv.runtime.repositories.Delete(name, tag, img.Id); err != nil { - return err - } - } - return nil + return srv.deleteImage(img, name, tag) } func (srv *Server) ImageGetCached(imgId string, config *Config) (*Image, error) { diff --git a/tags.go b/tags.go index 4a54398c5f..1148203d3d 100644 --- a/tags.go +++ b/tags.go @@ -110,7 +110,27 @@ func (store *TagStore) ImageName(id string) string { return utils.TruncateId(id) } -func (store *TagStore) Delete(repoName, tag, imageName string) error { +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) error { if err := store.Reload(); err != nil { return err } From 9060b5c2f52d29dec1920eb89ee0d48341540e3b Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Fri, 31 May 2013 14:37:02 +0000 Subject: [PATCH 13/55] added proper returns type, move the auto-prune in v1.1 api --- api.go | 17 +++++++-- api_params.go | 5 +++ api_test.go | 11 ++++-- commands.go | 18 ++++++++-- docs/sources/api/docker_remote_api.rst | 16 ++++++++- server.go | 48 +++++++++++++++++--------- server_test.go | 4 +-- tags.go | 15 ++++---- 8 files changed, 102 insertions(+), 32 deletions(-) diff --git a/api.go b/api.go index 1759257e6e..586fcf7a64 100644 --- a/api.go +++ b/api.go @@ -450,10 +450,23 @@ func deleteImages(srv *Server, version float64, w http.ResponseWriter, r *http.R return fmt.Errorf("Missing parameter") } name := vars["name"] - if err := srv.ImageDelete(name); err != nil { + imgs, err := srv.ImageDelete(name, version > 1.0) + 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 } diff --git a/api_params.go b/api_params.go index 1a24ab2875..6f759f2d80 100644 --- a/api_params.go +++ b/api_params.go @@ -23,6 +23,11 @@ type ApiInfo struct { NGoroutines int `json:",omitempty"` } +type ApiRmi struct { + Deleted string `json:",omitempty"` + Untagged string `json:",omitempty"` +} + type ApiContainers struct { Id string Image string diff --git a/api_test.go b/api_test.go index 844d15cc13..61d0bcb5ff 100644 --- a/api_test.go +++ b/api_test.go @@ -1271,10 +1271,17 @@ func TestDeleteImages(t *testing.T) { if err := deleteImages(srv, API_VERSION, r, req, map[string]string{"name": "test:test"}); err != nil { t.Fatal(err) } - if r.Code != http.StatusNoContent { - t.Fatalf("%d NO CONTENT expected, received %d\n", http.StatusNoContent, r.Code) + 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) diff --git a/commands.go b/commands.go index 0289048b89..42dc1b06af 100644 --- a/commands.go +++ b/commands.go @@ -567,10 +567,22 @@ func (cli *DockerCli) CmdRmi(args ...string) error { } for _, name := range cmd.Args() { - if _, _, err := cli.call("DELETE", "/images/"+name, nil); err != nil { - fmt.Fprintf(os.Stderr, "%s\n", err) + body, _, err := cli.call("DELETE", "/images/"+name, nil) + if err != nil { + 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 diff --git a/docs/sources/api/docker_remote_api.rst b/docs/sources/api/docker_remote_api.rst index b941367632..60285fc792 100644 --- a/docs/sources/api/docker_remote_api.rst +++ b/docs/sources/api/docker_remote_api.rst @@ -750,13 +750,27 @@ Remove an image DELETE /images/test HTTP/1.1 - **Example response**: + **Example response v1.0**: .. sourcecode:: http HTTP/1.1 204 OK + **Example response v1.1**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-type: application/json + + [ + {"Untagged":"3e2f21a89f"}, + {"Deleted":"3e2f21a89f"}, + {"Deleted":"53b4f83ac9"} + ] + :query force: 1/True/true or 0/False/false, default false + :statuscode 200: no error :statuscode 204: no error :statuscode 404: no such image :statuscode 409: conflict diff --git a/server.go b/server.go index 2af1e3cd8f..3d2cfaa500 100644 --- a/server.go +++ b/server.go @@ -707,7 +707,7 @@ func (srv *Server) ContainerDestroy(name string, removeVolume bool) error { var ErrImageReferenced = errors.New("Image referenced by a repository") -func (srv *Server) deleteImageAndChildren(id string) error { +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 @@ -720,7 +720,7 @@ func (srv *Server) deleteImageAndChildren(id string) error { return err } for _, img := range byParents[id] { - if err := srv.deleteImageAndChildren(img.Id); err != nil { + if err := srv.deleteImageAndChildren(img.Id, imgs); err != nil { if err != ErrImageReferenced { return err } else { @@ -741,49 +741,65 @@ func (srv *Server) deleteImageAndChildren(id string) error { if err := srv.runtime.repositories.DeleteAll(id); err != nil { return err } - return srv.runtime.graph.Delete(id) + 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) error { +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); err != nil { + if err := srv.deleteImageAndChildren(img.Parent, imgs); err != nil { return err } - return srv.deleteImageParents(parent) + return srv.deleteImageParents(parent, imgs) } return nil } -func (srv *Server) deleteImage(img *Image, repoName, tag string) error { +func (srv *Server) deleteImage(img *Image, repoName, tag string) (*[]ApiRmi, error) { //Untag the current image - if err := srv.runtime.repositories.Delete(repoName, tag); err != nil { - return err + 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); err != nil { + if err := srv.deleteImageAndChildren(img.Id, &imgs); err != nil { if err != ErrImageReferenced { - return err + return &imgs, err } - } else if err := srv.deleteImageParents(img); err != nil { + } else if err := srv.deleteImageParents(img, &imgs); err != nil { if err != ErrImageReferenced { - return err + return &imgs, err } } } - return nil + return &imgs, nil } -func (srv *Server) ImageDelete(name string) error { +func (srv *Server) ImageDelete(name string, autoPrune bool) (*[]ApiRmi, error) { img, err := srv.runtime.repositories.LookupImage(name) if err != nil { - return fmt.Errorf("No such image: %s", name) + 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 diff --git a/server_test.go b/server_test.go index 541c927b83..52b3479263 100644 --- a/server_test.go +++ b/server_test.go @@ -29,7 +29,7 @@ func TestContainerTagImageDelete(t *testing.T) { t.Errorf("Excepted 3 images, %d found", len(images)) } - if err := srv.ImageDelete("utest/docker:tag2"); err != nil { + if _, err := srv.ImageDelete("utest/docker:tag2", true); err != nil { t.Fatal(err) } @@ -42,7 +42,7 @@ func TestContainerTagImageDelete(t *testing.T) { t.Errorf("Excepted 2 images, %d found", len(images)) } - if err := srv.ImageDelete("utest:tag1"); err != nil { + if _, err := srv.ImageDelete("utest:tag1", true); err != nil { t.Fatal(err) } diff --git a/tags.go b/tags.go index 1148203d3d..630727aa60 100644 --- a/tags.go +++ b/tags.go @@ -118,11 +118,11 @@ func (store *TagStore) DeleteAll(id string) error { for _, name := range names { if strings.Contains(name, ":") { nameParts := strings.Split(name, ":") - if err := store.Delete(nameParts[0], nameParts[1]); err != nil { + if _, err := store.Delete(nameParts[0], nameParts[1]); err != nil { return err } } else { - if err := store.Delete(name, ""); err != nil { + if _, err := store.Delete(name, ""); err != nil { return err } } @@ -130,9 +130,10 @@ func (store *TagStore) DeleteAll(id string) error { return nil } -func (store *TagStore) Delete(repoName, tag string) error { +func (store *TagStore) Delete(repoName, tag string) (bool, error) { + deleted := false if err := store.Reload(); err != nil { - return err + return false, err } if r, exists := store.Repositories[repoName]; exists { if tag != "" { @@ -141,16 +142,18 @@ func (store *TagStore) Delete(repoName, tag string) error { if len(r) == 0 { delete(store.Repositories, repoName) } + deleted = true } else { - return fmt.Errorf("No such tag: %s:%s", repoName, tag) + 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 store.Save() + return deleted, store.Save() } func (store *TagStore) Set(repoName, tag, imageName string, force bool) error { From 3dd1e4d58c9d676d49f41db450c3d42901526edc Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Mon, 3 Jun 2013 12:09:16 +0000 Subject: [PATCH 14/55] added docs and moved to api version 1.2 --- api.go | 75 ++++++++++++++++++++++---- auth/auth.go | 15 +++++- docs/sources/api/docker_remote_api.rst | 29 +++++++--- 3 files changed, 102 insertions(+), 17 deletions(-) diff --git a/api.go b/api.go index 0817dcc9cd..63698c81bb 100644 --- a/api.go +++ b/api.go @@ -13,7 +13,7 @@ import ( "strings" ) -const API_VERSION = 1.1 +const API_VERSION = 1.2 func hijackServer(w http.ResponseWriter) (io.ReadCloser, io.Writer, error) { conn, _, err := w.(http.Hijacker).Hijack() @@ -68,15 +68,55 @@ func getBoolParam(value string) (bool, error) { return false, fmt.Errorf("Bad parameter") } -func postAuth(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { - authConfig := &auth.AuthConfig{} - if err := json.NewDecoder(r.Body).Decode(authConfig); err != nil { - return err +func getAuth(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if version > 1.1 { + w.WriteHeader(http.StatusNotFound) + return nil } - status, err := auth.Login(authConfig) + authConfig, err := auth.LoadConfig(srv.runtime.root) + if err != nil { + if err != auth.ErrConfigFileMissing { + return err + } + authConfig = &auth.AuthConfig{} + } + b, err := json.Marshal(&auth.AuthConfig{Username: authConfig.Username, Email: authConfig.Email}) if err != nil { return err } + writeJson(w, b) + return nil +} + +func postAuth(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + authConfig := &auth.AuthConfig{} + err := json.NewDecoder(r.Body).Decode(authConfig) + if err != nil { + return err + } + status := "" + if version > 1.1 { + status, err = auth.Login(authConfig, false) + if err != nil { + return err + } + } else { + localAuthConfig, err := auth.LoadConfig(srv.runtime.root) + if err != nil { + if err != auth.ErrConfigFileMissing { + return err + } + } + if authConfig.Username == localAuthConfig.Username { + authConfig.Password = localAuthConfig.Password + } + + newAuthConfig := auth.NewAuthConfig(authConfig.Username, authConfig.Password, authConfig.Email, srv.runtime.root) + status, err = auth.Login(newAuthConfig, true) + if err != nil { + return err + } + } if status != "" { b, err := json.Marshal(&ApiAuth{Status: status}) if err != nil { @@ -288,7 +328,15 @@ func postImagesCreate(srv *Server, version float64, w http.ResponseWriter, r *ht if image != "" { //pull registry := r.Form.Get("registry") authConfig := &auth.AuthConfig{} - json.NewDecoder(r.Body).Decode(authConfig) + if version > 1.1 { + json.NewDecoder(r.Body).Decode(authConfig) + } else { + localAuthConfig, err := auth.LoadConfig(srv.runtime.root) + if err != nil && err != auth.ErrConfigFileMissing { + return err + } + authConfig = localAuthConfig + } if err := srv.ImagePull(image, tag, registry, w, sf, authConfig); err != nil { if sf.Used() { w.Write(sf.FormatError(err)) @@ -358,8 +406,16 @@ 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 err := json.NewDecoder(r.Body).Decode(authConfig); err != nil { - return err + 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 @@ -682,6 +738,7 @@ func ListenAndServe(addr string, srv *Server, logging bool) error { m := map[string]map[string]func(*Server, float64, http.ResponseWriter, *http.Request, map[string]string) error{ "GET": { + "/auth": getAuth, "/version": getVersion, "/info": getInfo, "/images/json": getImagesJson, diff --git a/auth/auth.go b/auth/auth.go index 5da0b30415..35bf024617 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -120,7 +120,8 @@ func SaveConfig(authConfig *AuthConfig) 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 var status string @@ -146,6 +147,7 @@ func Login(authConfig *AuthConfig) (string, error) { if reqStatusCode == 201 { status = "Account created. Please use the confirmation link we sent" + " to your e-mail to activate it.\n" + storeConfig = true } else if reqStatusCode == 403 { return "", fmt.Errorf("Login: Your account hasn't been activated. " + "Please check your e-mail for a confirmation link.") @@ -164,7 +166,13 @@ func Login(authConfig *AuthConfig) (string, error) { } if resp.StatusCode == 200 { status = "Login Succeeded\n" + storeConfig = true } else if resp.StatusCode == 401 { + if store { + if err := SaveConfig(authConfig); err != nil { + return "", err + } + } return "", fmt.Errorf("Wrong login/password, please try again") } else { return "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body, @@ -176,5 +184,10 @@ func Login(authConfig *AuthConfig) (string, error) { } else { return "", fmt.Errorf("Unexpected status code [%d] : %s", reqStatusCode, reqBody) } + if storeConfig && store { + if err := SaveConfig(authConfig); err != nil { + return "", err + } + } return status, nil } diff --git a/docs/sources/api/docker_remote_api.rst b/docs/sources/api/docker_remote_api.rst index 9087c21e1e..f6e2795857 100644 --- a/docs/sources/api/docker_remote_api.rst +++ b/docs/sources/api/docker_remote_api.rst @@ -14,12 +14,13 @@ 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/create and /images//pull 2. Version ========== -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 @@ -550,12 +551,19 @@ Create an image Create an image, either by pull it from the registry or by importing it - **Example request**: + **Example request v1.0**: .. sourcecode:: http POST /images/create?fromImage=base HTTP/1.1 + **Example request v1.2**: + + .. sourcecode:: http + + POST /images/create?fromImage=base HTTP/1.1 + {{ authConfig }} + **Example response v1.1**: .. sourcecode:: http @@ -720,12 +728,19 @@ Push an image on the registry Push the image ``name`` on the registry - **Example request**: + **Example request v1.0**: .. sourcecode:: http POST /images/test/push HTTP/1.1 + **Example request v1.2**: + + .. sourcecode:: http + + POST /images/test/push HTTP/1.1 + {{ authConfig }} + **Example response v1.1**: .. sourcecode:: http @@ -875,7 +890,7 @@ Build an image from Dockerfile via stdin :statuscode 500: server error -Get default username and email +Get default username and email ****************************** .. http:get:: /auth @@ -904,8 +919,8 @@ Get default username and email :statuscode 500: server error -Set auth configuration -********************** +Check auth configuration (and store if if api < 1.2) +**************************************************** .. http:post:: /auth From 844a8db6c68fbd964d316c69964b29b2dc6f06b3 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Mon, 3 Jun 2013 12:21:22 +0000 Subject: [PATCH 15/55] add debug --- server.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server.go b/server.go index 6165dc5061..c90f64bb15 100644 --- a/server.go +++ b/server.go @@ -395,6 +395,7 @@ func (srv *Server) pullRepository(r *registry.Registry, out io.Writer, remote, a } func (srv *Server) ImagePull(name, tag, endpoint string, out io.Writer, sf *utils.StreamFormatter, authConfig *auth.AuthConfig) error { + utils.Debugf("ImagePull from <%s>", authConfig.Username) r := registry.NewRegistry(srv.runtime.root, authConfig) out = utils.NewWriteFlusher(out) if endpoint != "" { @@ -577,6 +578,7 @@ func (srv *Server) pushImage(r *registry.Registry, out io.Writer, remote, imgId, } func (srv *Server) ImagePush(name, endpoint string, out io.Writer, sf *utils.StreamFormatter, authConfig *auth.AuthConfig) error { + utils.Debugf("ImagePush from <%s>", authConfig.Username) out = utils.NewWriteFlusher(out) img, err := srv.runtime.graph.Get(name) r := registry.NewRegistry(srv.runtime.root, authConfig) From d26a3b37a6a8d42b9e7cb7486b928170c43e052e Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Mon, 3 Jun 2013 20:00:15 +0000 Subject: [PATCH 16/55] allow docker run : --- tags.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tags.go b/tags.go index 5bc2978e09..140182890d 100644 --- a/tags.go +++ b/tags.go @@ -151,14 +151,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 utils.TruncateId(revision) == tagOrId { + return store.graph.Get(revision) + } + } + if revision, exists := repo[tagOrId]; exists { return store.graph.Get(revision) } return nil, nil From 0ca88443985e7a944106ed4ceaf877a97f1ca2ec Mon Sep 17 00:00:00 2001 From: "Guillaume J. Charmes" Date: Mon, 3 Jun 2013 17:39:29 -0700 Subject: [PATCH 17/55] Fix stale command with stdout is not allocated --- container.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/container.go b/container.go index c6b7c8a51c..ef449653c4 100644 --- a/container.go +++ b/container.go @@ -355,6 +355,17 @@ func (container *Container) Attach(stdin io.ReadCloser, stdinCloser io.Closer, s errors <- err }() } + } else { + go func() { + defer stdinCloser.Close() + + cStdout, err := container.StdoutPipe() + if err != nil { + utils.Debugf("Error stdout pipe") + return + } + io.Copy(&utils.NopWriter{}, cStdout) + }() } if stderr != nil { nJobs += 1 @@ -381,7 +392,19 @@ func (container *Container) Attach(stdin io.ReadCloser, stdinCloser io.Closer, s errors <- err }() } + } else { + go func() { + defer stdinCloser.Close() + + cStderr, err := container.StdoutPipe() + if err != nil { + utils.Debugf("Error stdout pipe") + return + } + io.Copy(&utils.NopWriter{}, cStderr) + }() } + return utils.Go(func() error { if cStdout != nil { defer cStdout.Close() From 6d5bdff3942ce5e030b2cbd1510f418de25a1a53 Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Mon, 3 Jun 2013 21:39:00 -0400 Subject: [PATCH 18/55] Add flag to enable cross domain requests in Api Add the -api-enable-cors flag when running docker in daemon mode to allow CORS requests to be made to the Remote Api. The default value is false for this flag to not allow cross origin request to be made. Also added a handler for OPTIONS requests the standard for cross domain requests is to initially make an OPTIONS request to the api. --- api.go | 13 +++++++++++++ api_test.go | 26 ++++++++++++++++++++++++++ docker/docker.go | 7 ++++--- docs/sources/api/docker_remote_api.rst | 8 ++++++++ server.go | 8 +++++--- 5 files changed, 56 insertions(+), 6 deletions(-) diff --git a/api.go b/api.go index 7666b79a5f..978cd296a5 100644 --- a/api.go +++ b/api.go @@ -703,6 +703,11 @@ func postBuild(srv *Server, version float64, w http.ResponseWriter, r *http.Requ 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") +} + func ListenAndServe(addr string, srv *Server, logging bool) error { r := mux.NewRouter() log.Printf("Listening for HTTP on %s\n", addr) @@ -773,12 +778,20 @@ func ListenAndServe(addr string, srv *Server, logging bool) error { w.WriteHeader(http.StatusNotFound) return } + if srv.enableCors { + writeCorsHeaders(w, r) + } if err := localFct(srv, version, w, r, mux.Vars(r)); err != nil { httpError(w, err) } } r.Path("/v{version:[0-9.]+}" + localRoute).Methods(localMethod).HandlerFunc(f) r.Path(localRoute).Methods(localMethod).HandlerFunc(f) + r.Methods("OPTIONS").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if srv.enableCors { + writeCorsHeaders(w, r) + } + }) } } return http.ListenAndServe(addr, r) diff --git a/api_test.go b/api_test.go index 9121167e10..e464e51108 100644 --- a/api_test.go +++ b/api_test.go @@ -1239,6 +1239,32 @@ func TestDeleteContainers(t *testing.T) { } } +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() + + if err := getVersion(srv, API_VERSION, r, nil, nil); err != nil { + t.Fatal(err) + } + + allowOrigin := r.Header().Get("Access-Control-Allow-Origin") + allowHeaders := r.Header().Get("Access-Control-Allow-Headers") + + 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) + } +} + func TestDeleteImages(t *testing.T) { //FIXME: Implement this test t.Log("Test not implemented") diff --git a/docker/docker.go b/docker/docker.go index 7b8aa7f858..dd804f81c0 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/sources/api/docker_remote_api.rst b/docs/sources/api/docker_remote_api.rst index dca4599c55..e59b93d621 100644 --- a/docs/sources/api/docker_remote_api.rst +++ b/docs/sources/api/docker_remote_api.rst @@ -1056,3 +1056,11 @@ Here are the steps of 'docker run' : 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. + + +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/server.go b/server.go index 08cb37a72e..d86ffe0f44 100644 --- a/server.go +++ b/server.go @@ -870,7 +870,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) } @@ -879,12 +879,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 } From 63e80384ea753c74046c2a4c3f64229c359f466f Mon Sep 17 00:00:00 2001 From: "Guillaume J. Charmes" Date: Tue, 4 Jun 2013 14:35:32 -0700 Subject: [PATCH 19/55] Fix nil pointer on some situatuion --- container.go | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/container.go b/container.go index ef449653c4..0b54419122 100644 --- a/container.go +++ b/container.go @@ -357,14 +357,15 @@ func (container *Container) Attach(stdin io.ReadCloser, stdinCloser io.Closer, s } } else { go func() { - defer stdinCloser.Close() - - cStdout, err := container.StdoutPipe() - if err != nil { - utils.Debugf("Error stdout pipe") - return + if stdinCloser != nil { + defer stdinCloser.Close() + } + + if cStdout, err := container.StdoutPipe(); err != nil { + utils.Debugf("Error stdout pipe") + } else { + io.Copy(&utils.NopWriter{}, cStdout) } - io.Copy(&utils.NopWriter{}, cStdout) }() } if stderr != nil { @@ -394,14 +395,15 @@ func (container *Container) Attach(stdin io.ReadCloser, stdinCloser io.Closer, s } } else { go func() { - defer stdinCloser.Close() - - cStderr, err := container.StdoutPipe() - if err != nil { - utils.Debugf("Error stdout pipe") - return + if stdinCloser != nil { + defer stdinCloser.Close() + } + + if cStderr, err := container.StdoutPipe(); err != nil { + utils.Debugf("Error stdout pipe") + } else { + io.Copy(&utils.NopWriter{}, cStderr) } - io.Copy(&utils.NopWriter{}, cStderr) }() } From 62551129263e339d7ca9c21afbd4eda16452c6da Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Wed, 5 Jun 2013 13:19:57 +0200 Subject: [PATCH 20/55] updated doc --- docs/sources/api/docker_remote_api.rst | 1057 +------------------ docs/sources/api/docker_remote_api_v1.0.rst | 1015 ++++++++++++++++++ docs/sources/api/docker_remote_api_v1.1.rst | 1024 ++++++++++++++++++ docs/sources/api/docker_remote_api_v1.2.rst | 996 +++++++++++++++++ 4 files changed, 3066 insertions(+), 1026 deletions(-) create mode 100644 docs/sources/api/docker_remote_api_v1.0.rst create mode 100644 docs/sources/api/docker_remote_api_v1.1.rst create mode 100644 docs/sources/api/docker_remote_api_v1.2.rst diff --git a/docs/sources/api/docker_remote_api.rst b/docs/sources/api/docker_remote_api.rst index a42d623391..14edf44293 100644 --- a/docs/sources/api/docker_remote_api.rst +++ b/docs/sources/api/docker_remote_api.rst @@ -14,727 +14,41 @@ 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/create +- 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.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..."} - {"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..."} - {"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 + + +: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 v1.0**: - - .. sourcecode:: http - - POST /images/test/push HTTP/1.1 - - **Example request v1.2**: - - .. sourcecode:: http - - POST /images/test/push HTTP/1.1 - {{ authConfig }} - - **Example response v1.1**: +Uses json stream instead of HTML hijack, it looks like this: .. sourcecode:: http @@ -746,321 +60,12 @@ Push an image on the registry {"error":"Invalid..."} ... - **Example response v1.0**: +:doc:`docker_remote_api_v1.0` +***************************** - .. sourcecode:: http +docker v0.3.4 8d73740_ - HTTP/1.1 200 OK - Content-Type: application/vnd.docker.raw-stream +Initial version - {{ 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 - - -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 - - -Check auth configuration (and store if if api < 1.2) -**************************************************** - -.. 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. +.. _a8ae398: https://github.com/dotcloud/docker/commit/a8ae398bf52e97148ee7bd0d5868de2e15bd297f +.. _8d73740: https://github.com/dotcloud/docker/commit/8d73740343778651c09160cde9661f5f387b36f4 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..40dd17a7c1 --- /dev/null +++ b/docs/sources/api/docker_remote_api_v1.1.rst @@ -0,0 +1,1024 @@ +: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..."} + {"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..."} + {"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..."} + {"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 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..ede8ad7505 --- /dev/null +++ b/docs/sources/api/docker_remote_api_v1.2.rst @@ -0,0 +1,996 @@ +: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" + }, + { + "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..."} + {"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..."} + {"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 + {{ authConfig }} + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + {"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 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 + + +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. From f67ea78cce83114998390c16305a6869c72f5100 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Wed, 5 Jun 2013 12:59:05 +0000 Subject: [PATCH 21/55] move xino stuff to /dev/shm --- image.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/image.go b/image.go index 7a98ef41a1..4bd8f2df31 100644 --- a/image.go +++ b/image.go @@ -126,6 +126,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...") From 0f23fb949dd894ff70641cc01ac116aa55083157 Mon Sep 17 00:00:00 2001 From: Thatcher Peskens Date: Wed, 5 Jun 2013 18:06:51 -0700 Subject: [PATCH 22/55] Fixed some links * Added Google group to FAQ on docs * Changed IRC link * Fixed link to contributing broken by 326faec --- docs/sources/contributing/contributing.rst | 2 +- docs/sources/faq.rst | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) 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 From a799cdad3e25136ff313d692b9972cb725754055 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Thu, 6 Jun 2013 15:22:54 +0000 Subject: [PATCH 23/55] allow multiple params in inspect --- commands.go | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/commands.go b/commands.go index 50aaac8fb1..1e0abdc0b4 100644 --- a/commands.go +++ b/commands.go @@ -512,28 +512,33 @@ 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) - if err != nil { - return 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 + for _, name := range args { + obj, _, err := cli.call("GET", "/containers/"+name+"/json", nil) + if err != nil { + obj, _, err = cli.call("GET", "/images/"+name+"/json", nil) + if err != nil { + fmt.Printf("%s", err) + continue + } + } + + indented := new(bytes.Buffer) + if err = json.Indent(indented, obj, "", " "); err != nil { + fmt.Printf("%s", err) + continue + } + if _, err := io.Copy(os.Stdout, indented); err != nil { + fmt.Printf("%s", err) + } } return nil } From 4107701062bc729d06e1729e2b4c8c92b3b8b4f2 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Thu, 6 Jun 2013 15:45:08 +0000 Subject: [PATCH 24/55] add [] and move errors to stderr --- commands.go | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/commands.go b/commands.go index 1e0abdc0b4..78b4418ea5 100644 --- a/commands.go +++ b/commands.go @@ -457,7 +457,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 +482,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 +503,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) } @@ -520,26 +520,30 @@ func (cli *DockerCli) CmdInspect(args ...string) error { cmd.Usage() return nil } - - for _, name := range args { + fmt.Printf("[") + for i, name := range args { + if i > 0 { + fmt.Printf(",") + } obj, _, err := cli.call("GET", "/containers/"+name+"/json", nil) if err != nil { obj, _, err = cli.call("GET", "/images/"+name+"/json", nil) if err != nil { - fmt.Printf("%s", err) + fmt.Fprintf(os.Stderr, "%s", err) continue } } indented := new(bytes.Buffer) if err = json.Indent(indented, obj, "", " "); err != nil { - fmt.Printf("%s", err) + fmt.Fprintf(os.Stderr, "%s", err) continue } if _, err := io.Copy(os.Stdout, indented); err != nil { - fmt.Printf("%s", err) + fmt.Fprintf(os.Stderr, "%s", err) } } + fmt.Printf("]") return nil } From efa7ea592c1819f006acf9480c10e50ec63b3001 Mon Sep 17 00:00:00 2001 From: unclejack Date: Thu, 6 Jun 2013 22:06:12 +0300 Subject: [PATCH 25/55] docs: warn about build data tx & ADD w/o context This updates the documentation to mention that: 1. a lot of data may get sent to the docker daemon if there is a lot of data in the directory passed to docker build 2. ADD doesn't work in the absence of the context 3. running without a context doesn't send file data to the docker daemon 4. explain that the data sent to the docker daemon will be used by ADD commands --- docs/sources/commandline/command/build.rst | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/sources/commandline/command/build.rst b/docs/sources/commandline/command/build.rst index 81120b22d2..3e30cbdd58 100644 --- a/docs/sources/commandline/command/build.rst +++ b/docs/sources/commandline/command/build.rst @@ -19,10 +19,14 @@ 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. +| .. 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. From 56473d4cce79b0f06e46d88d14c716c1858188e2 Mon Sep 17 00:00:00 2001 From: Solomon Hykes Date: Thu, 6 Jun 2013 22:52:55 +0200 Subject: [PATCH 26/55] Typo in MAINTAINERS file --- hack/dockerbuilder/{MAITAINERS => MAINTAINERS} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename hack/dockerbuilder/{MAITAINERS => MAINTAINERS} (100%) diff --git a/hack/dockerbuilder/MAITAINERS b/hack/dockerbuilder/MAINTAINERS similarity index 100% rename from hack/dockerbuilder/MAITAINERS rename to hack/dockerbuilder/MAINTAINERS From 4b3a381f39bc90b36877f4832fe5c53de65a1ec3 Mon Sep 17 00:00:00 2001 From: unclejack Date: Fri, 7 Jun 2013 00:39:43 +0300 Subject: [PATCH 27/55] docs: build: ADD copies just needed data w/ full path --- docs/sources/commandline/command/build.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/sources/commandline/command/build.rst b/docs/sources/commandline/command/build.rst index 3e30cbdd58..254b0371a9 100644 --- a/docs/sources/commandline/command/build.rst +++ b/docs/sources/commandline/command/build.rst @@ -22,6 +22,7 @@ Examples | 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 From 8d4282cd36e3516c08dfa624da352b445bedf204 Mon Sep 17 00:00:00 2001 From: shin- Date: Fri, 7 Jun 2013 06:09:24 -0700 Subject: [PATCH 28/55] Remove CR/NL from description in docker CLI. Also moved description shortening to the client --- commands.go | 7 ++++++- server.go | 3 --- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/commands.go b/commands.go index 50aaac8fb1..701ade29a7 100644 --- a/commands.go +++ b/commands.go @@ -1079,7 +1079,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 diff --git a/server.go b/server.go index 6666123658..0d1387f93f 100644 --- a/server.go +++ b/server.go @@ -63,9 +63,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) } From 9e1cd37bbc2a1017612ea03e24a8002e6e81e1ea Mon Sep 17 00:00:00 2001 From: Ken Cochrane Date: Fri, 7 Jun 2013 13:42:52 -0400 Subject: [PATCH 29/55] seperated the registry and index API's into their own docs seperated the registry and index API's into their own docs and merged in the index search api into the index api. Also renamed the original registry api to registry_index_spec. --- docs/sources/api/index.rst | 3 +- docs/sources/api/index_api.rst | 553 ++++++++++++++++ docs/sources/api/index_search_api.rst | 43 -- docs/sources/api/registry_api.rst | 768 +++++++++++------------ docs/sources/api/registry_index_spec.rst | 569 +++++++++++++++++ 5 files changed, 1501 insertions(+), 435 deletions(-) create mode 100644 docs/sources/api/index_api.rst delete mode 100644 docs/sources/api/index_search_api.rst create mode 100644 docs/sources/api/registry_index_spec.rst diff --git a/docs/sources/api/index.rst b/docs/sources/api/index.rst index 85770f484e..68975262a6 100644 --- a/docs/sources/api/index.rst +++ b/docs/sources/api/index.rst @@ -10,8 +10,9 @@ This following : .. 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 From 393e873d25093f579d1a293bc473007b04f3c239 Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Sun, 9 Jun 2013 17:17:35 -0900 Subject: [PATCH 30/55] Add Access-Control-Allow-Methods header Add the Access-Control-Allow-Methods header so that DELETE operations are allowed. Also move the write CORS headers method before docker writes a 404 not found so that the client receives the correct response and not an invalid CORS request. --- api.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/api.go b/api.go index 978cd296a5..3831f326f2 100644 --- a/api.go +++ b/api.go @@ -706,6 +706,7 @@ func postBuild(srv *Server, version float64, w http.ResponseWriter, r *http.Requ 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 ListenAndServe(addr string, srv *Server, logging bool) error { @@ -774,13 +775,13 @@ func ListenAndServe(addr string, srv *Server, logging bool) error { if err != nil { version = API_VERSION } + if srv.enableCors { + writeCorsHeaders(w, r) + } if version == 0 || version > API_VERSION { w.WriteHeader(http.StatusNotFound) return } - if srv.enableCors { - writeCorsHeaders(w, r) - } if err := localFct(srv, version, w, r, mux.Vars(r)); err != nil { httpError(w, err) } From 95d66ebc6b068f2762d1a8bf3f78cd78d6ab4900 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Mon, 10 Jun 2013 13:56:43 +0000 Subject: [PATCH 31/55] specify public port --- network.go | 46 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 14 deletions(-) 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 From 0de3f1ca9a2135033ca482a8916123e51d82de09 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Mon, 10 Jun 2013 14:14:54 +0000 Subject: [PATCH 32/55] add tests --- network_test.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) 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 { From ab0d0a28a8085255558b5ee7be978d667f44def9 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Mon, 10 Jun 2013 15:06:52 +0000 Subject: [PATCH 33/55] fix errors when no body --- commands.go | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/commands.go b/commands.go index 50aaac8fb1..81fa90d8da 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 @@ -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 } @@ -1335,7 +1338,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 +1371,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" { From 7169212683ba02e2da4c80792702c5210f1c16ea Mon Sep 17 00:00:00 2001 From: "Guillaume J. Charmes" Date: Mon, 10 Jun 2013 11:08:40 -0700 Subject: [PATCH 34/55] Fix typo --- container.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/container.go b/container.go index 0b54419122..5db98ac382 100644 --- a/container.go +++ b/container.go @@ -399,7 +399,7 @@ func (container *Container) Attach(stdin io.ReadCloser, stdinCloser io.Closer, s defer stdinCloser.Close() } - if cStderr, err := container.StdoutPipe(); err != nil { + if cStderr, err := container.StderrPipe(); err != nil { utils.Debugf("Error stdout pipe") } else { io.Copy(&utils.NopWriter{}, cStderr) From 4e180107314a0a7768120ed66e615c8f668677bd Mon Sep 17 00:00:00 2001 From: shin- Date: Tue, 4 Jun 2013 09:29:47 -0700 Subject: [PATCH 35/55] Support for special namespace 'src' (highland support) --- server.go | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/server.go b/server.go index 6666123658..9815b510f2 100644 --- a/server.go +++ b/server.go @@ -330,8 +330,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 +358,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 +386,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 } } @@ -406,8 +406,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", strings.Join(parts, "%2F")) + } + if err := srv.pullRepository(r, out, name, remote, tag, sf); err != nil { return err } @@ -489,7 +493,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", strings.Join(parts, "%2F")) + } + + repoData, err := r.PushImageJSONIndex(srvName, imgList, false) if err != nil { return err } @@ -506,14 +516,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); err != nil { return err } return nil From d227af1edd5366df91f96193ddff3ff43d54b4f4 Mon Sep 17 00:00:00 2001 From: shin- Date: Wed, 5 Jun 2013 08:54:33 -0700 Subject: [PATCH 36/55] Escape remote names on repo push/pull --- server.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server.go b/server.go index 9815b510f2..f34235b057 100644 --- a/server.go +++ b/server.go @@ -409,7 +409,7 @@ func (srv *Server) ImagePull(name, tag, endpoint string, out io.Writer, sf *util remote := name parts := strings.Split(name, "/") if len(parts) > 2 { - remote = fmt.Sprintf("src/%s", strings.Join(parts, "%2F")) + remote = fmt.Sprintf("src/%s", url.QueryEscape(strings.Join(parts, "/"))) } if err := srv.pullRepository(r, out, name, remote, tag, sf); err != nil { return err @@ -496,7 +496,7 @@ func (srv *Server) pushRepository(r *registry.Registry, out io.Writer, name stri srvName := name parts := strings.Split(name, "/") if len(parts) > 2 { - srvName = fmt.Sprintf("src/%s", strings.Join(parts, "%2F")) + srvName = fmt.Sprintf("src/%s", url.QueryEscape(strings.Join(parts, "/"))) } repoData, err := r.PushImageJSONIndex(srvName, imgList, false) From b1ed75078ea04de8725ce5c39ae3b463f86faf4e Mon Sep 17 00:00:00 2001 From: Francisco Souza Date: Mon, 10 Jun 2013 16:07:57 -0300 Subject: [PATCH 37/55] docs/api/remote: fix rst syntax in the "Search images" section --- docs/sources/api/docker_remote_api.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/sources/api/docker_remote_api.rst b/docs/sources/api/docker_remote_api.rst index 1c46cf148a..2e9453ef60 100644 --- a/docs/sources/api/docker_remote_api.rst +++ b/docs/sources/api/docker_remote_api.rst @@ -839,9 +839,9 @@ Search images } ] - :query term: term to search - :statuscode 200: no error - :statuscode 500: server error + :query term: term to search + :statuscode 200: no error + :statuscode 500: server error 3.3 Misc From 0a28628c02d486512dc7e62eb54ccfd27eaa0a27 Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Mon, 10 Jun 2013 13:02:40 -0900 Subject: [PATCH 38/55] Add Cors and OPTIONS route unit tests Move creating the router and populating the routes to a separate function outside of ListenAndServe to allow unit tests to make assertions on the configured routes and handler funcs. --- api.go | 23 ++++++++++++++++------- api_test.go | 43 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 58 insertions(+), 8 deletions(-) diff --git a/api.go b/api.go index 3831f326f2..cc00849a60 100644 --- a/api.go +++ b/api.go @@ -709,9 +709,8 @@ func writeCorsHeaders(w http.ResponseWriter, r *http.Request) { w.Header().Add("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT, OPTIONS") } -func ListenAndServe(addr string, srv *Server, logging bool) error { +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": { @@ -788,12 +787,22 @@ func ListenAndServe(addr string, srv *Server, logging bool) error { } r.Path("/v{version:[0-9.]+}" + localRoute).Methods(localMethod).HandlerFunc(f) r.Path(localRoute).Methods(localMethod).HandlerFunc(f) - r.Methods("OPTIONS").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if srv.enableCors { - writeCorsHeaders(w, r) - } - }) } } + r.Methods("OPTIONS").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if srv.enableCors { + writeCorsHeaders(w, r) + } + }) + 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_test.go b/api_test.go index e464e51108..748bcf812c 100644 --- a/api_test.go +++ b/api_test.go @@ -1239,6 +1239,32 @@ 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 != 200 { + 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 { @@ -1250,12 +1276,24 @@ func TestGetEnabledCors(t *testing.T) { r := httptest.NewRecorder() - if err := getVersion(srv, API_VERSION, r, nil, nil); err != nil { + 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 != 200 { + 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) @@ -1263,6 +1301,9 @@ func TestGetEnabledCors(t *testing.T) { 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) { From eeea9ac9468a73bb9514a2d729060889b48384ee Mon Sep 17 00:00:00 2001 From: Andy Rothfusz Date: Mon, 10 Jun 2013 15:17:27 -0700 Subject: [PATCH 39/55] Add list of Docker Remote API Client Libraries. Fixes #800. --- docs/Makefile | 8 ++++---- docs/sources/api/docker_remote_api.rst | 24 ++++++++++++++++++++++++ docs/sources/api/index.rst | 2 +- 3 files changed, 29 insertions(+), 5 deletions(-) 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/docker_remote_api.rst b/docs/sources/api/docker_remote_api.rst index 1c46cf148a..625544cc73 100644 --- a/docs/sources/api/docker_remote_api.rst +++ b/docs/sources/api/docker_remote_api.rst @@ -1056,3 +1056,27 @@ Here are the steps of 'docker run' : 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. + +================================== +Docker Remote API Client Libraries +================================== + +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. + ++----------------------+----------------+--------------------------------------------+ +| 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/index.rst b/docs/sources/api/index.rst index 85770f484e..29de81d192 100644 --- a/docs/sources/api/index.rst +++ b/docs/sources/api/index.rst @@ -5,7 +5,7 @@ APIs ==== -This following : +Your programs and scripts can access Docker's functionality via these interfaces: .. toctree:: :maxdepth: 3 From ac599d652846f6456366b8028b2c38da0565d8b1 Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Mon, 10 Jun 2013 14:44:10 -0900 Subject: [PATCH 40/55] Add explicit status response to OPTIONS handler Write the http.StatusOK header in the OPTIONS handler and update the unit tests to refer to the response code using the const from the http package. --- api.go | 1 + api_test.go | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/api.go b/api.go index cc00849a60..681b719c1a 100644 --- a/api.go +++ b/api.go @@ -793,6 +793,7 @@ func createRouter(srv *Server, logging bool) (*mux.Router, error) { if srv.enableCors { writeCorsHeaders(w, r) } + w.WriteHeader(http.StatusOK) }) return r, nil } diff --git a/api_test.go b/api_test.go index 748bcf812c..62da94113a 100644 --- a/api_test.go +++ b/api_test.go @@ -1260,7 +1260,7 @@ func TestOptionsRoute(t *testing.T) { } router.ServeHTTP(r, req) - if r.Code != 200 { + if r.Code != http.StatusOK { t.Errorf("Expected response for OPTIONS request to be \"200\", %v found.", r.Code) } } @@ -1287,7 +1287,7 @@ func TestGetEnabledCors(t *testing.T) { } router.ServeHTTP(r, req) - if r.Code != 200 { + if r.Code != http.StatusOK { t.Errorf("Expected response for OPTIONS request to be \"200\", %v found.", r.Code) } From dd53c457d75a49e6e140c6d71642b237f3ee9056 Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Mon, 10 Jun 2013 16:10:40 -0900 Subject: [PATCH 41/55] Add OPTIONS to route map Move the OPTIONS method registration into the existing route map. Also add support for empty paths in the map. --- api.go | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/api.go b/api.go index 681b719c1a..29ff76171b 100644 --- a/api.go +++ b/api.go @@ -703,6 +703,10 @@ func postBuild(srv *Server, version float64, w http.ResponseWriter, r *http.Requ return nil } +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") @@ -750,6 +754,9 @@ func createRouter(srv *Server, logging bool) (*mux.Router, error) { "/containers/{name:.*}": deleteContainers, "/images/{name:.*}": deleteImages, }, + "OPTIONS": { + "": optionsHandler, + }, } for method, routes := range m { @@ -785,16 +792,15 @@ func createRouter(srv *Server, logging bool) (*mux.Router, 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) + } } } - r.Methods("OPTIONS").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if srv.enableCors { - writeCorsHeaders(w, r) - } - w.WriteHeader(http.StatusOK) - }) return r, nil } From 3ea6a2c7c39695fbfe3dfe228db00335c9937c14 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Tue, 11 Jun 2013 10:17:39 +0000 Subject: [PATCH 42/55] add Michael Crosby to AUTHORS --- AUTHORS | 1 + 1 file changed, 1 insertion(+) 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 From 2e9403b047d926d39d5b299abc071f3c29627fc6 Mon Sep 17 00:00:00 2001 From: Francisco Souza Date: Tue, 11 Jun 2013 11:39:06 -0300 Subject: [PATCH 43/55] build: don't ignore last line in Dockerfile when it doesn't end with \n --- buildfile.go | 5 ++- buildfile_test.go | 101 +++++++++++++++++++++++++--------------------- 2 files changed, 59 insertions(+), 47 deletions(-) diff --git a/buildfile.go b/buildfile.go index 5ea5607fc7..e0c5f794a8 100644 --- a/buildfile.go +++ b/buildfile.go @@ -313,10 +313,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") + } } } From 5918a5a3228442cdf55bed6591090c00ebb1357c Mon Sep 17 00:00:00 2001 From: Solomon Hykes Date: Tue, 11 Jun 2013 09:27:36 -0700 Subject: [PATCH 44/55] More principles. Raw and unstructured to spawn discussion. --- hack/PRINCIPLES.md | 4 ++++ 1 file changed, 4 insertions(+) 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. From d64f105b44d202aeb83a5f4edbdffdf2375992fa Mon Sep 17 00:00:00 2001 From: Solomon Hykes Date: Tue, 11 Jun 2013 10:39:02 -0700 Subject: [PATCH 45/55] Added a readme explaining the role of each API --- docs/sources/api/README.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docs/sources/api/README.md 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 From da54abaf2e86e530d0096e0f85b64b6cc17fcbe0 Mon Sep 17 00:00:00 2001 From: Francisco Souza Date: Wed, 12 Jun 2013 09:54:37 -0300 Subject: [PATCH 46/55] commands: use in instead of os.Stdin in hijack --- commands.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands.go b/commands.go index 81fa90d8da..d342807701 100644 --- a/commands.go +++ b/commands.go @@ -1443,7 +1443,7 @@ func (cli *DockerCli) hijack(method, path string, setRawTerminal bool, in *os.Fi return err } - if !term.IsTerminal(os.Stdin.Fd()) { + if !term.IsTerminal(in.Fd()) { if err := <-sendStdin; err != nil { return err } From ecae342434aeccb05b3cffe30c34051d84afdb74 Mon Sep 17 00:00:00 2001 From: Solomon Hykes Date: Wed, 12 Jun 2013 10:50:47 -0700 Subject: [PATCH 47/55] New roadmap item: advanced port redirections --- hack/ROADMAP.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) 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. From 04cca097ae9f7a8f64dbea1e12f3b871f6a67d65 Mon Sep 17 00:00:00 2001 From: Sam Alba Date: Wed, 12 Jun 2013 15:50:09 -0600 Subject: [PATCH 48/55] Update README.md --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bcad502abc..1c909e5431 100644 --- a/README.md +++ b/README.md @@ -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. From 81a11a3c30d01040451898f0e88a19363568edc3 Mon Sep 17 00:00:00 2001 From: Sam Alba Date: Wed, 12 Jun 2013 15:50:30 -0600 Subject: [PATCH 49/55] Update NOTICE --- NOTICE | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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. From f57175cbadbe7d8f48bfd5e3594116913de6e3ab Mon Sep 17 00:00:00 2001 From: Solomon Hykes Date: Wed, 12 Jun 2013 15:21:28 -0700 Subject: [PATCH 50/55] FIXME: a loose collection of FIXMEs for internal use by the maintainers --- FIXME | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 FIXME 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 From 78a76ad50eaf9d5a99336a47b2b78f547fa8a972 Mon Sep 17 00:00:00 2001 From: unclejack Date: Thu, 13 Jun 2013 08:59:41 +0300 Subject: [PATCH 51/55] use Go 1.1.1 to build docker --- hack/dockerbuilder/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 45a8945746a205ea513efc17bb242c63362c79a9 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Thu, 13 Jun 2013 13:17:56 +0000 Subject: [PATCH 52/55] added test --- runtime_test.go | 2 +- tags.go | 2 +- tags_test.go | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 tags_test.go diff --git a/runtime_test.go b/runtime_test.go index 1190510279..eea69aa5dc 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 { diff --git a/tags.go b/tags.go index 140182890d..7133649f6e 100644 --- a/tags.go +++ b/tags.go @@ -160,7 +160,7 @@ func (store *TagStore) GetImage(repoName, tagOrId string) (*Image, error) { } //go through all the tags, to see if tag is in fact an ID for _, revision := range repo { - if utils.TruncateId(revision) == tagOrId { + if strings.HasPrefix(revision, tagOrId) { return store.graph.Get(revision) } } diff --git a/tags_test.go b/tags_test.go new file mode 100644 index 0000000000..5d03275909 --- /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 + ":" + DEFAULT_TAG); 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") + } +} From 42d1c36a5c775a37a0173cf315086fde1c1b7dd8 Mon Sep 17 00:00:00 2001 From: "Guillaume J. Charmes" Date: Thu, 13 Jun 2013 10:25:43 -0700 Subject: [PATCH 53/55] Fix merge issue --- tags_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tags_test.go b/tags_test.go index 5d03275909..90bc056406 100644 --- a/tags_test.go +++ b/tags_test.go @@ -17,7 +17,7 @@ func TestLookupImage(t *testing.T) { t.Errorf("Expected 1 image, none found") } - if img, err := runtime.repositories.LookupImage(unitTestImageName + ":" + DEFAULT_TAG); err != nil { + if img, err := runtime.repositories.LookupImage(unitTestImageName + ":" + DEFAULTTAG); err != nil { t.Fatal(err) } else if img == nil { t.Errorf("Expected 1 image, none found") From 2eaa0a1dd7354e429c056d68515f903351e3eeb4 Mon Sep 17 00:00:00 2001 From: "Guillaume J. Charmes" Date: Thu, 13 Jun 2013 12:57:35 -0700 Subject: [PATCH 54/55] Fix non-tty run issue --- commands.go | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/commands.go b/commands.go index 6748fb0138..63c787c40a 100644 --- a/commands.go +++ b/commands.go @@ -1045,7 +1045,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) @@ -1256,7 +1258,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() { @@ -1283,6 +1287,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 @@ -1431,19 +1436,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(in.Fd()) { if err := <-sendStdin; err != nil { + utils.Debugf("Error sendStdin: %s", err) return err } } From 452128f0daf8117502fad2fe2f72b6f022223ddb Mon Sep 17 00:00:00 2001 From: "Guillaume J. Charmes" Date: Thu, 13 Jun 2013 15:18:15 -0700 Subject: [PATCH 55/55] Remove run() where it is not needed within the builder --- buildfile.go | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/buildfile.go b/buildfile.go index e9c6b6cfaf..3c706b04ba 100644 --- a/buildfile.go +++ b/buildfile.go @@ -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)