diff --git a/MAINTAINERS b/MAINTAINERS index a08f8f3140..49d14ba0bd 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -1,8 +1,7 @@ Solomon Hykes (@shykes) Guillaume Charmes (@creack) -Victor Vieux (@vieux) +Victor Vieux (@vieux) Michael Crosby (@crosbymichael) .travis.yml: Tianon Gravi (@tianon) -api.go: Victor Vieux (@vieux) Dockerfile: Tianon Gravi (@tianon) Makefile: Tianon Gravi (@tianon) diff --git a/Makefile b/Makefile index e124d1d7e6..e15ed35fda 100644 --- a/Makefile +++ b/Makefile @@ -32,10 +32,10 @@ shell: build $(DOCKER_RUN_DOCKER) bash build: bundles - docker build -rm -t "$(DOCKER_IMAGE)" . + docker build -t "$(DOCKER_IMAGE)" . docs-build: - docker build -rm -t "$(DOCKER_DOCS_IMAGE)" docs + docker build -t "$(DOCKER_DOCS_IMAGE)" docs bundles: mkdir bundles diff --git a/api/MAINTAINERS b/api/MAINTAINERS new file mode 100644 index 0000000000..e0f18f14f1 --- /dev/null +++ b/api/MAINTAINERS @@ -0,0 +1 @@ +Victor Vieux (@vieux) diff --git a/api/client.go b/api/client.go index eb345ae40b..2ea2dc44ea 100644 --- a/api/client.go +++ b/api/client.go @@ -138,7 +138,7 @@ func (cli *DockerCli) CmdBuild(args ...string) error { tag := cmd.String([]string{"t", "-tag"}, "", "Repository name (and optionally a tag) to be applied to the resulting image in case of success") suppressOutput := cmd.Bool([]string{"q", "-quiet"}, false, "Suppress the verbose output generated by the containers") noCache := cmd.Bool([]string{"#no-cache", "-no-cache"}, false, "Do not use cache when building the image") - rm := cmd.Bool([]string{"#rm", "-rm"}, false, "Remove intermediate containers after a successful build") + rm := cmd.Bool([]string{"#rm", "-rm"}, true, "Remove intermediate containers after a successful build") if err := cmd.Parse(args); err != nil { return nil } @@ -780,7 +780,10 @@ 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 := cli.Subcmd("rmi", "IMAGE [IMAGE...]", "Remove one or more images") + var ( + cmd = cli.Subcmd("rmi", "IMAGE [IMAGE...]", "Remove one or more images") + force = cmd.Bool([]string{"f", "-force"}, false, "Force") + ) if err := cmd.Parse(args); err != nil { return nil } @@ -789,9 +792,14 @@ func (cli *DockerCli) CmdRmi(args ...string) error { return nil } + v := url.Values{} + if *force { + v.Set("force", "1") + } + var encounteredError error for _, name := range cmd.Args() { - body, _, err := readBody(cli.call("DELETE", "/images/"+name, nil, false)) + body, _, err := readBody(cli.call("DELETE", "/images/"+name+"?"+v.Encode(), nil, false)) if err != nil { fmt.Fprintf(cli.err, "%s\n", err) encounteredError = fmt.Errorf("Error: failed to remove one or more images") @@ -2032,7 +2040,7 @@ func (cli *DockerCli) call(method, path string, data interface{}, passAuthInfo b re := regexp.MustCompile("/+") path = re.ReplaceAllString(path, "/") - req, err := http.NewRequest(method, fmt.Sprintf("/v%g%s", APIVERSION, path), params) + req, err := http.NewRequest(method, fmt.Sprintf("/v%s%s", APIVERSION, path), params) if err != nil { return nil, -1, err } @@ -2109,7 +2117,7 @@ func (cli *DockerCli) stream(method, path string, in io.Reader, out io.Writer, h re := regexp.MustCompile("/+") path = re.ReplaceAllString(path, "/") - req, err := http.NewRequest(method, fmt.Sprintf("/v%g%s", APIVERSION, path), in) + req, err := http.NewRequest(method, fmt.Sprintf("/v%s%s", APIVERSION, path), in) if err != nil { return err } @@ -2173,7 +2181,7 @@ func (cli *DockerCli) hijack(method, path string, setRawTerminal bool, in io.Rea re := regexp.MustCompile("/+") path = re.ReplaceAllString(path, "/") - req, err := http.NewRequest(method, fmt.Sprintf("/v%g%s", APIVERSION, path), nil) + req, err := http.NewRequest(method, fmt.Sprintf("/v%s%s", APIVERSION, path), nil) if err != nil { return err } diff --git a/api/common.go b/api/common.go index c79b16fda2..10e7ddb4ae 100644 --- a/api/common.go +++ b/api/common.go @@ -9,7 +9,7 @@ import ( ) const ( - APIVERSION = 1.9 + APIVERSION = "1.10" DEFAULTHTTPHOST = "127.0.0.1" DEFAULTUNIXSOCKET = "/var/run/docker.sock" ) diff --git a/api/server.go b/api/server.go index 4b10a2dae1..b6beb45403 100644 --- a/api/server.go +++ b/api/server.go @@ -12,6 +12,8 @@ import ( "github.com/dotcloud/docker/engine" "github.com/dotcloud/docker/pkg/listenbuffer" "github.com/dotcloud/docker/pkg/systemd" + "github.com/dotcloud/docker/pkg/user" + "github.com/dotcloud/docker/pkg/version" "github.com/dotcloud/docker/utils" "github.com/gorilla/mux" "io" @@ -21,7 +23,6 @@ import ( "net/http" "net/http/pprof" "os" - "regexp" "strconv" "strings" "syscall" @@ -32,7 +33,7 @@ var ( activationLock chan struct{} ) -type HttpApiFunc func(eng *engine.Engine, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error +type HttpApiFunc func(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error func hijackServer(w http.ResponseWriter) (io.ReadCloser, io.Writer, error) { conn, _, err := w.(http.Hijacker).Hijack() @@ -113,7 +114,7 @@ func getBoolParam(value string) (bool, error) { return ret, nil } -func postAuth(eng *engine.Engine, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { +func postAuth(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { var ( authConfig, err = ioutil.ReadAll(r.Body) job = eng.Job("auth") @@ -136,13 +137,13 @@ func postAuth(eng *engine.Engine, version float64, w http.ResponseWriter, r *htt return nil } -func getVersion(eng *engine.Engine, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { +func getVersion(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { w.Header().Set("Content-Type", "application/json") eng.ServeHTTP(w, r) return nil } -func postContainersKill(eng *engine.Engine, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { +func postContainersKill(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if vars == nil { return fmt.Errorf("Missing parameter") } @@ -160,7 +161,7 @@ func postContainersKill(eng *engine.Engine, version float64, w http.ResponseWrit return nil } -func getContainersExport(eng *engine.Engine, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { +func getContainersExport(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if vars == nil { return fmt.Errorf("Missing parameter") } @@ -172,7 +173,7 @@ func getContainersExport(eng *engine.Engine, version float64, w http.ResponseWri return nil } -func getImagesJSON(eng *engine.Engine, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { +func getImagesJSON(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if err := parseForm(r); err != nil { return err } @@ -186,7 +187,7 @@ func getImagesJSON(eng *engine.Engine, version float64, w http.ResponseWriter, r job.Setenv("filter", r.Form.Get("filter")) job.Setenv("all", r.Form.Get("all")) - if version >= 1.7 { + if version.GreaterThanOrEqualTo("1.7") { streamJSON(job, w, false) } else if outs, err = job.Stdout.AddListTable(); err != nil { return err @@ -196,7 +197,7 @@ func getImagesJSON(eng *engine.Engine, version float64, w http.ResponseWriter, r return err } - if version < 1.7 && outs != nil { // Convert to legacy format + if version.LessThan("1.7") && outs != nil { // Convert to legacy format outsLegacy := engine.NewTable("Created", 0) for _, out := range outs.Data { for _, repoTag := range out.GetList("RepoTags") { @@ -219,8 +220,8 @@ func getImagesJSON(eng *engine.Engine, version float64, w http.ResponseWriter, r return nil } -func getImagesViz(eng *engine.Engine, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { - if version > 1.6 { +func getImagesViz(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if version.GreaterThan("1.6") { w.WriteHeader(http.StatusNotFound) return fmt.Errorf("This is now implemented in the client.") } @@ -228,13 +229,13 @@ func getImagesViz(eng *engine.Engine, version float64, w http.ResponseWriter, r return nil } -func getInfo(eng *engine.Engine, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { +func getInfo(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { w.Header().Set("Content-Type", "application/json") eng.ServeHTTP(w, r) return nil } -func getEvents(eng *engine.Engine, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { +func getEvents(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if err := parseForm(r); err != nil { return err } @@ -245,7 +246,7 @@ func getEvents(eng *engine.Engine, version float64, w http.ResponseWriter, r *ht return job.Run() } -func getImagesHistory(eng *engine.Engine, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { +func getImagesHistory(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if vars == nil { return fmt.Errorf("Missing parameter") } @@ -259,7 +260,7 @@ func getImagesHistory(eng *engine.Engine, version float64, w http.ResponseWriter return nil } -func getContainersChanges(eng *engine.Engine, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { +func getContainersChanges(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if vars == nil { return fmt.Errorf("Missing parameter") } @@ -269,8 +270,8 @@ func getContainersChanges(eng *engine.Engine, version float64, w http.ResponseWr return job.Run() } -func getContainersTop(eng *engine.Engine, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { - if version < 1.4 { +func getContainersTop(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if version.LessThan("1.4") { return fmt.Errorf("top was improved a lot since 1.3, Please upgrade your docker client.") } if vars == nil { @@ -285,7 +286,7 @@ func getContainersTop(eng *engine.Engine, version float64, w http.ResponseWriter return job.Run() } -func getContainersJSON(eng *engine.Engine, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { +func getContainersJSON(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if err := parseForm(r); err != nil { return err } @@ -301,7 +302,7 @@ func getContainersJSON(eng *engine.Engine, version float64, w http.ResponseWrite job.Setenv("before", r.Form.Get("before")) job.Setenv("limit", r.Form.Get("limit")) - if version >= 1.5 { + if version.GreaterThanOrEqualTo("1.5") { streamJSON(job, w, false) } else if outs, err = job.Stdout.AddTable(); err != nil { return err @@ -309,7 +310,7 @@ func getContainersJSON(eng *engine.Engine, version float64, w http.ResponseWrite if err = job.Run(); err != nil { return err } - if version < 1.5 { // Convert to legacy format + if version.LessThan("1.5") { // Convert to legacy format for _, out := range outs.Data { ports := engine.NewTable("", 0) ports.ReadListFrom([]byte(out.Get("Ports"))) @@ -323,7 +324,7 @@ func getContainersJSON(eng *engine.Engine, version float64, w http.ResponseWrite return nil } -func postImagesTag(eng *engine.Engine, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { +func postImagesTag(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if err := parseForm(r); err != nil { return err } @@ -340,7 +341,7 @@ func postImagesTag(eng *engine.Engine, version float64, w http.ResponseWriter, r return nil } -func postCommit(eng *engine.Engine, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { +func postCommit(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if err := parseForm(r); err != nil { return err } @@ -369,7 +370,7 @@ func postCommit(eng *engine.Engine, version float64, w http.ResponseWriter, r *h } // Creates an image from Pull or from Import -func postImagesCreate(eng *engine.Engine, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { +func postImagesCreate(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if err := parseForm(r); err != nil { return err } @@ -389,9 +390,6 @@ func postImagesCreate(eng *engine.Engine, version float64, w http.ResponseWriter authConfig = &auth.AuthConfig{} } } - if version > 1.0 { - w.Header().Set("Content-Type", "application/json") - } if image != "" { //pull metaHeaders := map[string][]string{} for k, v := range r.Header { @@ -400,7 +398,7 @@ func postImagesCreate(eng *engine.Engine, version float64, w http.ResponseWriter } } job = eng.Job("pull", r.Form.Get("fromImage"), tag) - job.SetenvBool("parallel", version > 1.3) + job.SetenvBool("parallel", version.GreaterThan("1.3")) job.SetenvJson("metaHeaders", metaHeaders) job.SetenvJson("authConfig", authConfig) } else { //import @@ -408,7 +406,7 @@ func postImagesCreate(eng *engine.Engine, version float64, w http.ResponseWriter job.Stdin.Add(r.Body) } - if version > 1.0 { + if version.GreaterThan("1.0") { job.SetenvBool("json", true) streamJSON(job, w, true) } else { @@ -418,14 +416,14 @@ func postImagesCreate(eng *engine.Engine, version float64, w http.ResponseWriter if !job.Stdout.Used() { return err } - sf := utils.NewStreamFormatter(version > 1.0) + sf := utils.NewStreamFormatter(version.GreaterThan("1.0")) w.Write(sf.FormatError(err)) } return nil } -func getImagesSearch(eng *engine.Engine, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { +func getImagesSearch(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if err := parseForm(r); err != nil { return err } @@ -457,19 +455,15 @@ func getImagesSearch(eng *engine.Engine, version float64, w http.ResponseWriter, return job.Run() } -func postImagesInsert(eng *engine.Engine, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { +func postImagesInsert(eng *engine.Engine, version version.Version, 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") } - if version > 1.0 { - w.Header().Set("Content-Type", "application/json") - } - job := eng.Job("insert", vars["name"], r.Form.Get("url"), r.Form.Get("path")) - if version > 1.0 { + if version.GreaterThan("1.0") { job.SetenvBool("json", true) streamJSON(job, w, false) } else { @@ -479,14 +473,14 @@ func postImagesInsert(eng *engine.Engine, version float64, w http.ResponseWriter if !job.Stdout.Used() { return err } - sf := utils.NewStreamFormatter(version > 1.0) + sf := utils.NewStreamFormatter(version.GreaterThan("1.0")) w.Write(sf.FormatError(err)) } return nil } -func postImagesPush(eng *engine.Engine, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { +func postImagesPush(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if vars == nil { return fmt.Errorf("Missing parameter") } @@ -517,13 +511,10 @@ func postImagesPush(eng *engine.Engine, version float64, w http.ResponseWriter, } } - if version > 1.0 { - w.Header().Set("Content-Type", "application/json") - } job := eng.Job("push", vars["name"]) job.SetenvJson("metaHeaders", metaHeaders) job.SetenvJson("authConfig", authConfig) - if version > 1.0 { + if version.GreaterThan("1.0") { job.SetenvBool("json", true) streamJSON(job, w, true) } else { @@ -534,17 +525,17 @@ func postImagesPush(eng *engine.Engine, version float64, w http.ResponseWriter, if !job.Stdout.Used() { return err } - sf := utils.NewStreamFormatter(version > 1.0) + sf := utils.NewStreamFormatter(version.GreaterThan("1.0")) w.Write(sf.FormatError(err)) } return nil } -func getImagesGet(eng *engine.Engine, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { +func getImagesGet(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if vars == nil { return fmt.Errorf("Missing parameter") } - if version > 1.0 { + if version.GreaterThan("1.0") { w.Header().Set("Content-Type", "application/x-tar") } job := eng.Job("image_export", vars["name"]) @@ -552,13 +543,13 @@ func getImagesGet(eng *engine.Engine, version float64, w http.ResponseWriter, r return job.Run() } -func postImagesLoad(eng *engine.Engine, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { +func postImagesLoad(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { job := eng.Job("load") job.Stdin.Add(r.Body) return job.Run() } -func postContainersCreate(eng *engine.Engine, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { +func postContainersCreate(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if err := parseForm(r); err != nil { return nil } @@ -589,7 +580,7 @@ func postContainersCreate(eng *engine.Engine, version float64, w http.ResponseWr return writeJSON(w, http.StatusCreated, out) } -func postContainersRestart(eng *engine.Engine, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { +func postContainersRestart(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if err := parseForm(r); err != nil { return err } @@ -605,7 +596,7 @@ func postContainersRestart(eng *engine.Engine, version float64, w http.ResponseW return nil } -func deleteContainers(eng *engine.Engine, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { +func deleteContainers(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if err := parseForm(r); err != nil { return err } @@ -622,7 +613,7 @@ func deleteContainers(eng *engine.Engine, version float64, w http.ResponseWriter return nil } -func deleteImages(eng *engine.Engine, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { +func deleteImages(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if err := parseForm(r); err != nil { return err } @@ -631,12 +622,12 @@ func deleteImages(eng *engine.Engine, version float64, w http.ResponseWriter, r } var job = eng.Job("image_delete", vars["name"]) streamJSON(job, w, false) - job.SetenvBool("autoPrune", version > 1.1) + job.Setenv("force", r.Form.Get("force")) return job.Run() } -func postContainersStart(eng *engine.Engine, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { +func postContainersStart(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if vars == nil { return fmt.Errorf("Missing parameter") } @@ -657,7 +648,7 @@ func postContainersStart(eng *engine.Engine, version float64, w http.ResponseWri return nil } -func postContainersStop(eng *engine.Engine, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { +func postContainersStop(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if err := parseForm(r); err != nil { return err } @@ -673,7 +664,7 @@ func postContainersStop(eng *engine.Engine, version float64, w http.ResponseWrit return nil } -func postContainersWait(eng *engine.Engine, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { +func postContainersWait(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if vars == nil { return fmt.Errorf("Missing parameter") } @@ -695,7 +686,7 @@ func postContainersWait(eng *engine.Engine, version float64, w http.ResponseWrit return writeJSON(w, http.StatusOK, env) } -func postContainersResize(eng *engine.Engine, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { +func postContainersResize(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if err := parseForm(r); err != nil { return err } @@ -708,7 +699,7 @@ func postContainersResize(eng *engine.Engine, version float64, w http.ResponseWr return nil } -func postContainersAttach(eng *engine.Engine, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { +func postContainersAttach(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if err := parseForm(r); err != nil { return err } @@ -750,7 +741,7 @@ func postContainersAttach(eng *engine.Engine, version float64, w http.ResponseWr fmt.Fprintf(outStream, "HTTP/1.1 200 OK\r\nContent-Type: application/vnd.docker.raw-stream\r\n\r\n") - if c.GetSubEnv("Config") != nil && !c.GetSubEnv("Config").GetBool("Tty") && version >= 1.6 { + if c.GetSubEnv("Config") != nil && !c.GetSubEnv("Config").GetBool("Tty") && version.GreaterThanOrEqualTo("1.6") { errStream = utils.NewStdWriter(outStream, utils.Stderr) outStream = utils.NewStdWriter(outStream, utils.Stdout) } else { @@ -773,7 +764,7 @@ func postContainersAttach(eng *engine.Engine, version float64, w http.ResponseWr return nil } -func wsContainersAttach(eng *engine.Engine, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { +func wsContainersAttach(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if err := parseForm(r); err != nil { return err } @@ -805,7 +796,7 @@ func wsContainersAttach(eng *engine.Engine, version float64, w http.ResponseWrit return nil } -func getContainersByName(eng *engine.Engine, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { +func getContainersByName(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if vars == nil { return fmt.Errorf("Missing parameter") } @@ -815,7 +806,7 @@ func getContainersByName(eng *engine.Engine, version float64, w http.ResponseWri return job.Run() } -func getImagesByName(eng *engine.Engine, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { +func getImagesByName(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if vars == nil { return fmt.Errorf("Missing parameter") } @@ -825,8 +816,8 @@ func getImagesByName(eng *engine.Engine, version float64, w http.ResponseWriter, return job.Run() } -func postBuild(eng *engine.Engine, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { - if version < 1.3 { +func postBuild(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if version.LessThan("1.3") { return fmt.Errorf("Multipart upload for build is no longer supported. Please upgrade your docker client.") } var ( @@ -841,7 +832,7 @@ func postBuild(eng *engine.Engine, version float64, w http.ResponseWriter, r *ht // Both headers will be parsed and sent along to the daemon, but if a non-empty // ConfigFile is present, any value provided as an AuthConfig directly will // be overridden. See BuildFile::CmdFrom for details. - if version < 1.9 && authEncoded != "" { + if version.LessThan("1.9") && authEncoded != "" { authJson := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authEncoded)) if err := json.NewDecoder(authJson).Decode(authConfig); err != nil { // for a pull it is not an error if no auth was given @@ -859,7 +850,7 @@ func postBuild(eng *engine.Engine, version float64, w http.ResponseWriter, r *ht } } - if version >= 1.8 { + if version.GreaterThanOrEqualTo("1.8") { job.SetenvBool("json", true) streamJSON(job, w, true) } else { @@ -878,13 +869,13 @@ func postBuild(eng *engine.Engine, version float64, w http.ResponseWriter, r *ht if !job.Stdout.Used() { return err } - sf := utils.NewStreamFormatter(version >= 1.8) + sf := utils.NewStreamFormatter(version.GreaterThanOrEqualTo("1.8")) w.Write(sf.FormatError(err)) } return nil } -func postContainersCopy(eng *engine.Engine, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { +func postContainersCopy(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if vars == nil { return fmt.Errorf("Missing parameter") } @@ -907,7 +898,7 @@ func postContainersCopy(eng *engine.Engine, version float64, w http.ResponseWrit } job := eng.Job("container_copy", vars["name"], copyData.Get("Resource")) - streamJSON(job, w, false) + job.Stdout.Add(w) if err := job.Run(); err != nil { utils.Errorf("%s", err.Error()) if strings.Contains(err.Error(), "No such container") { @@ -917,7 +908,7 @@ func postContainersCopy(eng *engine.Engine, version float64, w http.ResponseWrit return nil } -func optionsHandler(eng *engine.Engine, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { +func optionsHandler(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { w.WriteHeader(http.StatusOK) return nil } @@ -927,7 +918,7 @@ func writeCorsHeaders(w http.ResponseWriter, r *http.Request) { w.Header().Add("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT, OPTIONS") } -func makeHttpHandler(eng *engine.Engine, logging bool, localMethod string, localRoute string, handlerFunc HttpApiFunc, enableCors bool, dockerVersion string) http.HandlerFunc { +func makeHttpHandler(eng *engine.Engine, logging bool, localMethod string, localRoute string, handlerFunc HttpApiFunc, enableCors bool, dockerVersion version.Version) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // log the request utils.Debugf("Calling %s %s", localMethod, localRoute) @@ -938,20 +929,20 @@ func makeHttpHandler(eng *engine.Engine, logging bool, localMethod string, local if strings.Contains(r.Header.Get("User-Agent"), "Docker-Client/") { userAgent := strings.Split(r.Header.Get("User-Agent"), "/") - if len(userAgent) == 2 && userAgent[1] != dockerVersion { + if len(userAgent) == 2 && !dockerVersion.Equal(userAgent[1]) { utils.Debugf("Warning: client and server don't have the same version (client: %s, server: %s)", userAgent[1], dockerVersion) } } - version, err := strconv.ParseFloat(mux.Vars(r)["version"], 64) - if err != nil { + version := version.Version(mux.Vars(r)["version"]) + if version == "" { version = APIVERSION } if enableCors { writeCorsHeaders(w, r) } - if version == 0 || version > APIVERSION { - http.Error(w, fmt.Errorf("client and server don't have same version (client : %g, server: %g)", version, APIVERSION).Error(), http.StatusNotFound) + if version.GreaterThan(APIVERSION) { + http.Error(w, fmt.Errorf("client and server don't have same version (client : %s, server: %s)", version, APIVERSION).Error(), http.StatusNotFound) return } @@ -1049,7 +1040,7 @@ func createRouter(eng *engine.Engine, logging, enableCors bool, dockerVersion st localMethod := method // build the handler function - f := makeHttpHandler(eng, logging, localMethod, localRoute, localFct, enableCors, dockerVersion) + f := makeHttpHandler(eng, logging, localMethod, localRoute, localFct, enableCors, version.Version(dockerVersion)) // add the new route if localRoute == "" { @@ -1067,13 +1058,13 @@ func createRouter(eng *engine.Engine, logging, enableCors bool, dockerVersion st // ServeRequest processes a single http request to the docker remote api. // FIXME: refactor this to be part of Server and not require re-creating a new // router each time. This requires first moving ListenAndServe into Server. -func ServeRequest(eng *engine.Engine, apiversion float64, w http.ResponseWriter, req *http.Request) error { +func ServeRequest(eng *engine.Engine, apiversion version.Version, w http.ResponseWriter, req *http.Request) error { router, err := createRouter(eng, false, true, "") if err != nil { return err } // Insert APIVERSION into the request as a convenience - req.URL.Path = fmt.Sprintf("/v%g%s", apiversion, req.URL.Path) + req.URL.Path = fmt.Sprintf("/v%s%s", apiversion, req.URL.Path) router.ServeHTTP(w, req) return nil } @@ -1142,18 +1133,15 @@ func ListenAndServe(proto, addr string, eng *engine.Engine, logging, enableCors return err } - groups, err := ioutil.ReadFile("/etc/group") + groups, err := user.ParseGroupFilter(func(g *user.Group) bool { + return g.Name == "docker" + }) if err != nil { return err } - re := regexp.MustCompile("(^|\n)docker:.*?:([0-9]+)") - if gidMatch := re.FindStringSubmatch(string(groups)); gidMatch != nil { - gid, err := strconv.Atoi(gidMatch[2]) - if err != nil { - return err - } - utils.Debugf("docker group found. gid: %d", gid) - if err := os.Chown(addr, 0, gid); err != nil { + if len(groups) > 0 { + utils.Debugf("docker group found. gid: %d", groups[0].Gid) + if err := os.Chown(addr, 0, groups[0].Gid); err != nil { return err } } diff --git a/container.go b/container.go index e9952fd4e4..0362dac33a 100644 --- a/container.go +++ b/container.go @@ -790,22 +790,8 @@ func (container *Container) monitor(callback execdriver.StartCallback) error { utils.Errorf("Error running container: %s", err) } - // Cleanup - container.cleanup() - - // Re-create a brand new stdin pipe once the container exited - if container.Config.OpenStdin { - container.stdin, container.stdinPipe = io.Pipe() - } - container.State.SetStopped(exitCode) - if container.runtime != nil && container.runtime.srv != nil { - container.runtime.srv.LogEvent("die", container.ID, container.runtime.repositories.ImageName(container.Image)) - } - - close(container.waitLock) - // FIXME: there is a race condition here which causes this to fail during the unit tests. // If another goroutine was waiting for Wait() to return before removing the container's root // from the filesystem... At this point it may already have done so. @@ -813,7 +799,23 @@ func (container *Container) monitor(callback execdriver.StartCallback) error { // to return. // FIXME: why are we serializing running state to disk in the first place? //log.Printf("%s: Failed to dump configuration to the disk: %s", container.ID, err) - container.ToDisk() + if err := container.ToDisk(); err != nil { + utils.Errorf("Error dumping container state to disk: %s\n", err) + } + + // Cleanup + container.cleanup() + + // Re-create a brand new stdin pipe once the container exited + if container.Config.OpenStdin { + container.stdin, container.stdinPipe = io.Pipe() + } + + if container.runtime != nil && container.runtime.srv != nil { + container.runtime.srv.LogEvent("die", container.ID, container.runtime.repositories.ImageName(container.Image)) + } + + close(container.waitLock) return err } diff --git a/contrib/mkimage-yum.sh b/contrib/mkimage-yum.sh index d7ffdc1902..5ff9d3f7d5 100755 --- a/contrib/mkimage-yum.sh +++ b/contrib/mkimage-yum.sh @@ -76,7 +76,7 @@ rm -rf "$target"/var/cache/ldconfig/* version= if [ -r "$target"/etc/redhat-release ]; then - version="$(sed 's/^[^0-9\]*\([0-9.]\+\).*$/\1/' /etc/redhat-release)" + version="$(sed 's/^[^0-9\]*\([0-9.]\+\).*$/\1/' "$target"/etc/redhat-release)" fi if [ -z "$version" ]; then diff --git a/docs/sources/contributing/devenvironment.rst b/docs/sources/contributing/devenvironment.rst index f0a1398d70..42e6f9be84 100644 --- a/docs/sources/contributing/devenvironment.rst +++ b/docs/sources/contributing/devenvironment.rst @@ -122,7 +122,10 @@ If the test are successful then the tail of the output should look something lik PASS ok github.com/dotcloud/docker/utils 0.017s +If $TESTFLAGS is set in the environment, it is passed as extra arguments to 'go test'. +You can use this to select certain tests to run, eg. + TESTFLAGS='-run ^TestBuild$' make test Step 6: Use Docker diff --git a/docs/sources/reference/api/docker_remote_api.rst b/docs/sources/reference/api/docker_remote_api.rst index 69bbf71ec9..3733effaf8 100644 --- a/docs/sources/reference/api/docker_remote_api.rst +++ b/docs/sources/reference/api/docker_remote_api.rst @@ -26,15 +26,31 @@ Docker Remote API 2. Versions =========== -The current version of the API is 1.9 +The current version of the API is 1.10 Calling /images//insert is the same as calling -/v1.9/images//insert +/v1.10/images//insert You can still call an old version of the api using /v1.0/images//insert +v1.10 +***** + +Full Documentation +------------------ + +:doc:`docker_remote_api_v1.10` + +What's new +---------- + +.. http:delete:: /images/(name) + + **New!** You can now use the force parameter to force delete of an image, even if it's + tagged in multiple repositories. + v1.9 **** diff --git a/docs/sources/reference/api/docker_remote_api_v1.10.rst b/docs/sources/reference/api/docker_remote_api_v1.10.rst new file mode 100644 index 0000000000..a6eec3551f --- /dev/null +++ b/docs/sources/reference/api/docker_remote_api_v1.10.rst @@ -0,0 +1,1282 @@ +:title: Remote API v1.10 +:description: API Documentation for Docker +:keywords: API, Docker, rcli, REST, documentation + +:orphan: + +======================= +Docker Remote API v1.10 +======================= + +.. contents:: Table of Contents + +1. Brief introduction +===================== + +- The Remote API has replaced rcli +- The daemon listens on ``unix:///var/run/docker.sock``, but you can + :ref:`bind_docker`. +- 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&size=1 HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Id": "8dfafdbc3a40", + "Image": "base:latest", + "Command": "echo 1", + "Created": 1367854155, + "Status": "Exit 0", + "Ports":[{"PrivatePort": 2222, "PublicPort": 3333, "Type": "tcp"}], + "SizeRw":12288, + "SizeRootFs":0 + }, + { + "Id": "9cd87474be90", + "Image": "base:latest", + "Command": "echo 222222", + "Created": 1367854155, + "Status": "Exit 0", + "Ports":[], + "SizeRw":12288, + "SizeRootFs":0 + }, + { + "Id": "3176a2479c92", + "Image": "base:latest", + "Command": "echo 3333333333333333", + "Created": 1367854154, + "Status": "Exit 0", + "Ports":[], + "SizeRw":12288, + "SizeRootFs":0 + }, + { + "Id": "4cb07b47f9fb", + "Image": "base:latest", + "Command": "echo 444444444444444444444444444444444", + "Created": 1367854152, + "Status": "Exit 0", + "Ports":[], + "SizeRw":12288, + "SizeRootFs":0 + } + ] + + :query all: 1/True/true or 0/False/false, Show all containers. Only running containers are shown by default + :query limit: Show ``limit`` last created containers, include non-running ones. + :query since: Show only containers created since Id, include non-running ones. + :query before: Show only containers created before Id, include non-running ones. + :query size: 1/True/true or 0/False/false, Show the containers sizes + :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":{ + "/tmp": {} + }, + "VolumesFrom":"", + "WorkingDir":"", + "ExposedPorts":{ + "22/tcp": {} + } + } + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 201 OK + Content-Type: application/json + + { + "Id":"e90e34656806" + "Warnings":[] + } + + :jsonparam config: the container's configuration + :query name: Assign the specified name to the container. Must match ``/?[a-zA-Z0-9_-]+``. + :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": "", + "WorkingDir":"" + + }, + "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": {}, + "HostConfig": { + "Binds": null, + "ContainerIDFile": "", + "LxcConf": [], + "Privileged": false, + "PortBindings": { + "80/tcp": [ + { + "HostIp": "0.0.0.0", + "HostPort": "49153" + } + ] + }, + "Links": null, + "PublishAllPorts": false + } + } + + :statuscode 200: no error + :statuscode 404: no such container + :statuscode 500: server error + + +List processes running inside a container +***************************************** + +.. http:get:: /containers/(id)/top + + List processes running inside the container ``id`` + + **Example request**: + + .. sourcecode:: http + + GET /containers/4fa6e0f0c678/top HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Titles":[ + "USER", + "PID", + "%CPU", + "%MEM", + "VSZ", + "RSS", + "TTY", + "STAT", + "START", + "TIME", + "COMMAND" + ], + "Processes":[ + ["root","20147","0.0","0.1","18060","1864","pts/4","S","10:06","0:00","bash"], + ["root","20271","0.0","0.0","4312","352","pts/4","S+","10:07","0:00","sleep","10"] + ] + } + + :query ps_args: ps arguments to use (eg. aux) + :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/(id)/start HTTP/1.1 + Content-Type: application/json + + { + "Binds":["/tmp:/tmp"], + "LxcConf":{"lxc.utsname":"docker"}, + "PortBindings":{ "22/tcp": [{ "HostPort": "11022" }] }, + "PublishAllPorts":false, + "Privileged":false + } + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 204 No Content + Content-Type: text/plain + + :jsonparam hostConfig: the container's host configuration (optional) + :statuscode 204: no error + :statuscode 404: no such container + :statuscode 500: server error + + +Stop a container +**************** + +.. 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 + + **Stream details**: + + When using the TTY setting is enabled in + :http:post:`/containers/create`, the stream is the raw data + from the process PTY and client's stdin. When the TTY is + disabled, then the stream is multiplexed to separate stdout + and stderr. + + The format is a **Header** and a **Payload** (frame). + + **HEADER** + + The header will contain the information on which stream write + the stream (stdout or stderr). It also contain the size of + the associated frame encoded on the last 4 bytes (uint32). + + It is encoded on the first 8 bytes like this:: + + header := [8]byte{STREAM_TYPE, 0, 0, 0, SIZE1, SIZE2, SIZE3, SIZE4} + + ``STREAM_TYPE`` can be: + + - 0: stdin (will be writen on stdout) + - 1: stdout + - 2: stderr + + ``SIZE1, SIZE2, SIZE3, SIZE4`` are the 4 bytes of the uint32 size encoded as big endian. + + **PAYLOAD** + + The payload is the raw stream. + + **IMPLEMENTATION** + + The simplest way to implement the Attach protocol is the following: + + 1) Read 8 bytes + 2) chose stdout or stderr depending on the first byte + 3) Extract the frame size from the last 4 byets + 4) Read the extracted size and output it on the correct output + 5) Goto 1) + + + +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 + + +Copy files or folders from a container +************************************** + +.. http:post:: /containers/(id)/copy + + Copy files or folders of container ``id`` + + **Example request**: + + .. sourcecode:: http + + POST /containers/4fa6e0f0c678/copy HTTP/1.1 + Content-Type: application/json + + { + "Resource":"test.txt" + } + + **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 + + +2.2 Images +---------- + +List Images +*********** + +.. http:get:: /images/json + + **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 + + [ + { + "RepoTags": [ + "ubuntu:12.04", + "ubuntu:precise", + "ubuntu:latest" + ], + "Id": "8dbd9e392a964056420e5d58ca5cc376ef18e2de93b5cc90e868a1bbc8318c1c", + "Created": 1365714795, + "Size": 131506275, + "VirtualSize": 131506275 + }, + { + "RepoTags": [ + "ubuntu:12.10", + "ubuntu:quantal" + ], + "ParentId": "27cf784147099545", + "Id": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", + "Created": 1364102658, + "Size": 24653, + "VirtualSize": 180116135 + } + ] + + +Create an image +*************** + +.. http:post:: /images/create + + Create an image, either by pull it from the registry or by importing it + + **Example request**: + + .. sourcecode:: http + + POST /images/create?fromImage=base HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + {"status":"Pulling..."} + {"status":"Pulling", "progress":"1 B/ 100 B", "progressDetail":{"current":1, "total":100}} + {"error":"Invalid..."} + ... + + When using this endpoint to pull an image from the registry, + the ``X-Registry-Auth`` header can be used to include a + base64-encoded AuthConfig object. + + :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 + :reqheader X-Registry-Auth: base64-encoded AuthConfig object + :statuscode 200: no error + :statuscode 500: server error + + + +Insert a file in an image +************************* + +.. http:post:: /images/(name)/insert + + Insert a file from ``url`` in the image ``name`` at ``path`` + + **Example request**: + + .. sourcecode:: http + + POST /images/test/insert?path=/usr&url=myurl HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + {"status":"Inserting..."} + {"status":"Inserting", "progress":"1/? (n/a)", "progressDetail":{"current":1}} + {"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":"", + "WorkingDir":"" + }, + "Size": 6824592 + } + + :statuscode 200: no error + :statuscode 404: no such image + :statuscode 500: server error + + +Get the history of an image +*************************** + +.. http:get:: /images/(name)/history + + Return the history of the image ``name`` + + **Example request**: + + .. sourcecode:: http + + GET /images/base/history HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Id":"b750fe79269d", + "Created":1364102658, + "CreatedBy":"/bin/bash" + }, + { + "Id":"27cf78414709", + "Created":1364068391, + "CreatedBy":"" + } + ] + + :statuscode 200: no error + :statuscode 404: no such image + :statuscode 500: server error + + +Push an image on the registry +***************************** + +.. http:post:: /images/(name)/push + + Push the image ``name`` on the registry + + **Example request**: + + .. sourcecode:: http + + POST /images/test/push HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + {"status":"Pushing..."} + {"status":"Pushing", "progress":"1/? (n/a)", "progressDetail":{"current":1}}} + {"error":"Invalid..."} + ... + + :query registry: the registry you wan to push, optional + :reqheader X-Registry-Auth: include a base64-encoded AuthConfig object. + :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 201 OK + + :query repo: The repository to tag in + :query force: 1/True/true or 0/False/false, default false + :statuscode 201: no error + :statuscode 400: bad parameter + :statuscode 404: no such image + :statuscode 409: conflict + :statuscode 500: server error + + +Remove an image +*************** + +.. http:delete:: /images/(name) + + Remove the image ``name`` from the filesystem + + **Example request**: + + .. sourcecode:: http + + DELETE /images/test HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-type: application/json + + [ + {"Untagged":"3e2f21a89f"}, + {"Deleted":"3e2f21a89f"}, + {"Deleted":"53b4f83ac9"} + ] + + :query force: 1/True/true or 0/False/false, default false + :statuscode 200: no error + :statuscode 404: no such image + :statuscode 409: conflict + :statuscode 500: server error + + +Search images +************* + +.. http:get:: /images/search + + Search for an image in the docker index. + + .. note:: + + The response keys have changed from API v1.6 to reflect the JSON + sent by the registry server to the docker daemon's request. + + **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 + + [ + { + "description": "", + "is_official": false, + "is_trusted": false, + "name": "wma55/u1210sshd", + "star_count": 0 + }, + { + "description": "", + "is_official": false, + "is_trusted": false, + "name": "jdswinbank/sshd", + "star_count": 0 + }, + { + "description": "", + "is_official": false, + "is_trusted": false, + "name": "vgauthier/sshd", + "star_count": 0 + } + ... + ] + + :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 + Content-Type: application/json + + {"stream":"Step 1..."} + {"stream":"..."} + {"error":"Error...", "errorDetail":{"code": 123, "message": "Error..."}} + + + The stream must be a tar archive compressed with one of the + following algorithms: identity (no compression), gzip, bzip2, + xz. + + The archive must include a file called ``Dockerfile`` at its + root. It may include any number of other files, which will be + accessible in the build context (See the :ref:`ADD build command + `). + + :query t: repository name (and optionally a tag) to be applied to the resulting image in case of success + :query q: suppress verbose build output + :query nocache: do not use the cache when building the image + :reqheader Content-type: should be set to ``"application/tar"``. + :reqheader X-Registry-Config: base64-encoded ConfigFile object + :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", + "serveraddress":"https://index.docker.io/v1/" + } + + **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, + "IPv4Forwarding":true + } + + :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 + + +Monitor Docker's events +*********************** + +.. http:get:: /events + + Get events from docker, either in real time via streaming, or via polling (using `since`) + + **Example request**: + + .. sourcecode:: http + + GET /events?since=1374067924 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + {"status":"create","id":"dfdf82bd3881","from":"base:latest","time":1374067924} + {"status":"start","id":"dfdf82bd3881","from":"base:latest","time":1374067924} + {"status":"stop","id":"dfdf82bd3881","from":"base:latest","time":1374067966} + {"status":"destroy","id":"dfdf82bd3881","from":"base:latest","time":1374067970} + + :query since: timestamp used for polling + :statuscode 200: no error + :statuscode 500: server error + +Get a tarball containing all images and tags in a repository +************************************************************ + +.. http:get:: /images/(name)/get + + Get a tarball containing all images and metadata for the repository specified by ``name``. + + **Example request** + + .. sourcecode:: http + + GET /images/ubuntu/get + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/x-tar + + Binary data stream + + :statuscode 200: no error + :statuscode 500: server error + +Load a tarball with a set of images and tags into docker +******************************************************** + +.. http:post:: /images/load + + Load a set of images and tags into the docker repository. + + **Example request** + + .. sourcecode:: http + + POST /images/load + + Tarball in body + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + + :statuscode 200: no error + :statuscode 500: server error + +3. Going further +================ + +3.1 Inside 'docker run' +----------------------- + +Here are the steps of 'docker run' : + +* Create the container +* If the status code is 404, it means the image doesn't exists: + * Try to pull it + * Then retry to create the container +* Start the container +* If you are not in detached mode: + * Attach to the container, using logs=1 (to have stdout and stderr from the container's start) and stream=1 +* If in detached mode or only stdin is attached: + * Display the container's id + + +3.2 Hijacking +------------- + +In this version of the API, /attach, uses hijacking to transport stdin, stdout and stderr on the same socket. This might change in the future. + +3.3 CORS Requests +----------------- + +To enable cross origin requests to the remote api add the flag "-api-enable-cors" when running docker in daemon mode. + +.. code-block:: bash + + docker -d -H="192.168.1.9:4243" -api-enable-cors diff --git a/docs/sources/reference/commandline/cli.rst b/docs/sources/reference/commandline/cli.rst index b221f0de6b..83161e3898 100644 --- a/docs/sources/reference/commandline/cli.rst +++ b/docs/sources/reference/commandline/cli.rst @@ -185,11 +185,11 @@ Examples: Usage: docker build [OPTIONS] PATH | URL | - Build a new container image from the source code at PATH - -t, --time="": Repository name (and optionally a tag) to be applied + -t, --time="": Repository name (and optionally a tag) to be applied to the resulting image in case of success. -q, --quiet=false: Suppress the verbose output generated by the containers. --no-cache: Do not use the cache when building the image. - --rm: Remove intermediate containers after a successful build + --rm=true: Remove intermediate containers after a successful build The files at ``PATH`` or ``URL`` are called the "context" of the build. The build process may refer to any of the files in the context, for example when @@ -198,8 +198,6 @@ is given as ``URL``, then no context is set. When a Git repository is set as ``URL``, then the repository is used as the context. Git repositories are cloned with their submodules (`git clone --recursive`). -.. note:: ``docker build --rm`` does not affect the image cache which is used to accelerate builds, it only removes the duplicate writeable container layers. - .. _cli_build_examples: .. seealso:: :ref:`dockerbuilder`. @@ -209,7 +207,7 @@ Examples: .. code-block:: bash - $ sudo docker build --rm . + $ sudo docker build . Uploading context 10240 bytes Step 1 : FROM busybox Pulling repository busybox @@ -249,9 +247,8 @@ The transfer of context from the local machine to the Docker daemon is what the ``docker`` client means when you see the "Uploading context" message. -The ``--rm`` option tells Docker to remove the intermediate containers and -layers that were used to create each image layer. Doing so has no impact on -the image build cache. +If you wish to keep the intermediate containers after the build is complete, +you must use ``--rm=false``. This does not affect the build cache. .. code-block:: bash @@ -1023,6 +1020,8 @@ containers will not be deleted. Usage: docker rmi IMAGE [IMAGE...] Remove one or more images + + -f, --force=false: Force Removing tagged images ~~~~~~~~~~~~~~~~~~~~~~ diff --git a/engine/engine.go b/engine/engine.go index 5814955fdd..68e109e7f2 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -9,6 +9,7 @@ import ( "os" "path/filepath" "runtime" + "sort" "strings" ) @@ -29,6 +30,10 @@ func Register(name string, handler Handler) error { return nil } +func unregister(name string) { + delete(globalHandlers, name) +} + // The Engine is the core of Docker. // It acts as a store for *containers*, and allows manipulation of these // containers by executing *jobs*. @@ -106,6 +111,12 @@ func New(root string) (*Engine, error) { Stderr: os.Stderr, Stdin: os.Stdin, } + eng.Register("commands", func(job *Job) Status { + for _, name := range eng.commands() { + job.Printf("%s\n", name) + } + return StatusOK + }) // Copy existing global handlers for k, v := range globalHandlers { eng.handlers[k] = v @@ -117,6 +128,17 @@ func (eng *Engine) String() string { return fmt.Sprintf("%s|%s", eng.Root(), eng.id[:8]) } +// Commands returns a list of all currently registered commands, +// sorted alphabetically. +func (eng *Engine) commands() []string { + names := make([]string, 0, len(eng.handlers)) + for name := range eng.handlers { + names = append(names, name) + } + sort.Strings(names) + return names +} + // Job creates a new job which can later be executed. // This function mimics `Command` from the standard os/exec package. func (eng *Engine) Job(name string, args ...string) *Job { diff --git a/engine/engine_test.go b/engine/engine_test.go index cb3b939cbc..a16c352678 100644 --- a/engine/engine_test.go +++ b/engine/engine_test.go @@ -1,6 +1,7 @@ package engine import ( + "bytes" "io/ioutil" "os" "path" @@ -17,6 +18,8 @@ func TestRegister(t *testing.T) { if err := Register("dummy1", nil); err == nil { t.Fatalf("Expecting error, got none") } + // Register is global so let's cleanup to avoid conflicts + defer unregister("dummy1") eng := newTestEngine(t) @@ -33,6 +36,7 @@ func TestRegister(t *testing.T) { if err := eng.Register("dummy2", nil); err == nil { t.Fatalf("Expecting error, got none") } + defer unregister("dummy2") } func TestJob(t *testing.T) { @@ -49,6 +53,7 @@ func TestJob(t *testing.T) { } eng.Register("dummy2", h) + defer unregister("dummy2") job2 := eng.Job("dummy2", "--level=awesome") if job2.handler == nil { @@ -60,6 +65,24 @@ func TestJob(t *testing.T) { } } +func TestEngineCommands(t *testing.T) { + eng := newTestEngine(t) + defer os.RemoveAll(eng.Root()) + handler := func(job *Job) Status { return StatusOK } + eng.Register("foo", handler) + eng.Register("bar", handler) + eng.Register("echo", handler) + eng.Register("die", handler) + var output bytes.Buffer + commands := eng.Job("commands") + commands.Stdout.Add(&output) + commands.Run() + expected := "bar\ncommands\ndie\necho\nfoo\n" + if result := output.String(); result != expected { + t.Fatalf("Unexpected output:\nExpected = %v\nResult = %v\n", expected, result) + } +} + func TestEngineRoot(t *testing.T) { tmp, err := ioutil.TempDir("", "docker-test-TestEngineCreateDir") if err != nil { diff --git a/execdriver/lxc/driver.go b/execdriver/lxc/driver.go index fce6d6717c..f1c93af789 100644 --- a/execdriver/lxc/driver.go +++ b/execdriver/lxc/driver.go @@ -77,7 +77,7 @@ func (d *driver) Name() string { } func (d *driver) Run(c *execdriver.Command, pipes *execdriver.Pipes, startCallback execdriver.StartCallback) (int, error) { - if err := SetTerminal(c, pipes); err != nil { + if err := execdriver.SetTerminal(c, pipes); err != nil { return -1, err } configPath, err := d.generateLXCConfig(c) diff --git a/execdriver/lxc/lxc_template.go b/execdriver/lxc/lxc_template.go index 639780f5d8..1181396a18 100644 --- a/execdriver/lxc/lxc_template.go +++ b/execdriver/lxc/lxc_template.go @@ -12,6 +12,7 @@ const LxcTemplate = ` lxc.network.type = veth lxc.network.link = {{.Network.Bridge}} lxc.network.name = eth0 +lxc.network.mtu = {{.Network.Mtu}} {{else}} # network is disabled (-n=false) lxc.network.type = empty diff --git a/execdriver/native/term.go b/execdriver/native/term.go index 9b8e813aab..ec69820f75 100644 --- a/execdriver/native/term.go +++ b/execdriver/native/term.go @@ -6,14 +6,13 @@ package native import ( "github.com/dotcloud/docker/execdriver" - "github.com/dotcloud/docker/execdriver/lxc" "io" "os" "os/exec" ) type dockerStdTerm struct { - lxc.StdConsole + execdriver.StdConsole pipes *execdriver.Pipes } @@ -26,7 +25,7 @@ func (d *dockerStdTerm) SetMaster(master *os.File) { } type dockerTtyTerm struct { - lxc.TtyConsole + execdriver.TtyConsole pipes *execdriver.Pipes } diff --git a/execdriver/lxc/term.go b/execdriver/termconsole.go similarity index 79% rename from execdriver/lxc/term.go rename to execdriver/termconsole.go index db58c3181a..af6b88d3d1 100644 --- a/execdriver/lxc/term.go +++ b/execdriver/termconsole.go @@ -1,7 +1,6 @@ -package lxc +package execdriver import ( - "github.com/dotcloud/docker/execdriver" "github.com/dotcloud/docker/pkg/term" "github.com/kr/pty" "io" @@ -9,9 +8,9 @@ import ( "os/exec" ) -func SetTerminal(command *execdriver.Command, pipes *execdriver.Pipes) error { +func SetTerminal(command *Command, pipes *Pipes) error { var ( - term execdriver.Terminal + term Terminal err error ) if command.Tty { @@ -31,7 +30,7 @@ type TtyConsole struct { SlavePty *os.File } -func NewTtyConsole(command *execdriver.Command, pipes *execdriver.Pipes) (*TtyConsole, error) { +func NewTtyConsole(command *Command, pipes *Pipes) (*TtyConsole, error) { ptyMaster, ptySlave, err := pty.Open() if err != nil { return nil, err @@ -56,7 +55,7 @@ func (t *TtyConsole) Resize(h, w int) error { return term.SetWinsize(t.MasterPty.Fd(), &term.Winsize{Height: uint16(h), Width: uint16(w)}) } -func (t *TtyConsole) AttachPipes(command *exec.Cmd, pipes *execdriver.Pipes) error { +func (t *TtyConsole) AttachPipes(command *exec.Cmd, pipes *Pipes) error { command.Stdout = t.SlavePty command.Stderr = t.SlavePty @@ -89,7 +88,7 @@ func (t *TtyConsole) Close() error { type StdConsole struct { } -func NewStdConsole(command *execdriver.Command, pipes *execdriver.Pipes) (*StdConsole, error) { +func NewStdConsole(command *Command, pipes *Pipes) (*StdConsole, error) { std := &StdConsole{} if err := std.AttachPipes(&command.Cmd, pipes); err != nil { @@ -98,7 +97,7 @@ func NewStdConsole(command *execdriver.Command, pipes *execdriver.Pipes) (*StdCo return std, nil } -func (s *StdConsole) AttachPipes(command *exec.Cmd, pipes *execdriver.Pipes) error { +func (s *StdConsole) AttachPipes(command *exec.Cmd, pipes *Pipes) error { command.Stdout = pipes.Stdout command.Stderr = pipes.Stderr diff --git a/integration/api_test.go b/integration/api_test.go index 5779e6b226..4c22d54b3d 100644 --- a/integration/api_test.go +++ b/integration/api_test.go @@ -13,6 +13,7 @@ import ( "github.com/dotcloud/docker/runconfig" "github.com/dotcloud/docker/utils" "io" + "io/ioutil" "net" "net/http" "net/http/httptest" @@ -1175,6 +1176,8 @@ func TestGetEnabledCors(t *testing.T) { func TestDeleteImages(t *testing.T) { eng := NewTestEngine(t) + //we expect errors, so we disable stderr + eng.Stderr = ioutil.Discard defer mkRuntimeFromEngine(eng, t).Nuke() initialImages := getImages(eng, t, true, "") diff --git a/integration/commands_test.go b/integration/commands_test.go index a3359ec631..37ac79fa46 100644 --- a/integration/commands_test.go +++ b/integration/commands_test.go @@ -1031,7 +1031,10 @@ func TestContainerOrphaning(t *testing.T) { buildSomething(template2, imageName) // remove the second image by name - resp, err := srv.DeleteImage(imageName, true) + resp := engine.NewTable("", 0) + if err := srv.DeleteImage(imageName, resp, true, false); err == nil { + t.Fatal("Expected error, got none") + } // see if we deleted the first image (and orphaned the container) for _, i := range resp.Data { @@ -1043,11 +1046,12 @@ func TestContainerOrphaning(t *testing.T) { } func TestCmdKill(t *testing.T) { - stdin, stdinPipe := io.Pipe() - stdout, stdoutPipe := io.Pipe() - - cli := api.NewDockerCli(stdin, stdoutPipe, ioutil.Discard, testDaemonProto, testDaemonAddr) - cli2 := api.NewDockerCli(nil, ioutil.Discard, ioutil.Discard, testDaemonProto, testDaemonAddr) + var ( + stdin, stdinPipe = io.Pipe() + stdout, stdoutPipe = io.Pipe() + cli = api.NewDockerCli(stdin, stdoutPipe, ioutil.Discard, testDaemonProto, testDaemonAddr) + cli2 = api.NewDockerCli(nil, ioutil.Discard, ioutil.Discard, testDaemonProto, testDaemonAddr) + ) defer cleanup(globalEngine, t) ch := make(chan struct{}) @@ -1086,6 +1090,7 @@ func TestCmdKill(t *testing.T) { } }) + stdout.Close() time.Sleep(500 * time.Millisecond) if !container.State.IsRunning() { t.Fatal("The container should be still running") diff --git a/integration/server_test.go b/integration/server_test.go index 2234bcab08..1247e8d2d8 100644 --- a/integration/server_test.go +++ b/integration/server_test.go @@ -2,6 +2,7 @@ package docker import ( "github.com/dotcloud/docker" + "github.com/dotcloud/docker/engine" "github.com/dotcloud/docker/runconfig" "strings" "testing" @@ -35,7 +36,7 @@ func TestImageTagImageDelete(t *testing.T) { t.Errorf("Expected %d images, %d found", nExpected, nActual) } - if _, err := srv.DeleteImage("utest/docker:tag2", true); err != nil { + if err := srv.DeleteImage("utest/docker:tag2", engine.NewTable("", 0), true, false); err != nil { t.Fatal(err) } @@ -47,7 +48,7 @@ func TestImageTagImageDelete(t *testing.T) { t.Errorf("Expected %d images, %d found", nExpected, nActual) } - if _, err := srv.DeleteImage("utest:5000/docker:tag3", true); err != nil { + if err := srv.DeleteImage("utest:5000/docker:tag3", engine.NewTable("", 0), true, false); err != nil { t.Fatal(err) } @@ -56,7 +57,7 @@ func TestImageTagImageDelete(t *testing.T) { nExpected = len(initialImages.Data[0].GetList("RepoTags")) + 1 nActual = len(images.Data[0].GetList("RepoTags")) - if _, err := srv.DeleteImage("utest:tag1", true); err != nil { + if err := srv.DeleteImage("utest:tag1", engine.NewTable("", 0), true, false); err != nil { t.Fatal(err) } @@ -447,8 +448,7 @@ func TestRmi(t *testing.T) { t.Fatalf("Expected 2 new images, found %d.", images.Len()-initialImages.Len()) } - _, err = srv.DeleteImage(imageID, true) - if err != nil { + if err = srv.DeleteImage(imageID, engine.NewTable("", 0), true, false); err != nil { t.Fatal(err) } @@ -683,8 +683,8 @@ func TestDeleteTagWithExistingContainers(t *testing.T) { } // Try to remove the tag - imgs, err := srv.DeleteImage("utest:tag1", true) - if err != nil { + imgs := engine.NewTable("", 0) + if err := srv.DeleteImage("utest:tag1", imgs, true, false); err != nil { t.Fatal(err) } @@ -692,7 +692,7 @@ func TestDeleteTagWithExistingContainers(t *testing.T) { t.Fatalf("Should only have deleted one untag %d", len(imgs.Data)) } - if untag := imgs.Data[0].Get("Untagged"); untag != unitTestImageID { + if untag := imgs.Data[0].Get("Untagged"); untag != "utest:tag1" { t.Fatalf("Expected %s got %s", unitTestImageID, untag) } } diff --git a/pkg/version/version.go b/pkg/version/version.go new file mode 100644 index 0000000000..3721d64aa8 --- /dev/null +++ b/pkg/version/version.go @@ -0,0 +1,52 @@ +package version + +import ( + "strconv" + "strings" +) + +type Version string + +func (me Version) compareTo(other string) int { + var ( + meTab = strings.Split(string(me), ".") + otherTab = strings.Split(other, ".") + ) + for i, s := range meTab { + var meInt, otherInt int + meInt, _ = strconv.Atoi(s) + if len(otherTab) > i { + otherInt, _ = strconv.Atoi(otherTab[i]) + } + if meInt > otherInt { + return 1 + } + if otherInt > meInt { + return -1 + } + } + if len(otherTab) > len(meTab) { + return -1 + } + return 0 +} + +func (me Version) LessThan(other string) bool { + return me.compareTo(other) == -1 +} + +func (me Version) LessThanOrEqualTo(other string) bool { + return me.compareTo(other) <= 0 +} + +func (me Version) GreaterThan(other string) bool { + return me.compareTo(other) == 1 +} + +func (me Version) GreaterThanOrEqualTo(other string) bool { + return me.compareTo(other) >= 0 +} + +func (me Version) Equal(other string) bool { + return me.compareTo(other) == 0 +} diff --git a/pkg/version/version_test.go b/pkg/version/version_test.go new file mode 100644 index 0000000000..4bebd0c434 --- /dev/null +++ b/pkg/version/version_test.go @@ -0,0 +1,25 @@ +package version + +import ( + "testing" +) + +func assertVersion(t *testing.T, a, b string, result int) { + if r := Version(a).compareTo(b); r != result { + t.Fatalf("Unexpected version comparison result. Found %d, expected %d", r, result) + } +} + +func TestCompareVersion(t *testing.T) { + assertVersion(t, "1.12", "1.12", 0) + assertVersion(t, "1.05.00.0156", "1.0.221.9289", 1) + assertVersion(t, "1", "1.0.1", -1) + assertVersion(t, "1.0.1", "1", 1) + assertVersion(t, "1.0.1", "1.0.2", -1) + assertVersion(t, "1.0.2", "1.0.3", -1) + assertVersion(t, "1.0.3", "1.1", -1) + assertVersion(t, "1.1", "1.1.1", -1) + assertVersion(t, "1.1.1", "1.1.2", -1) + assertVersion(t, "1.1.2", "1.2", -1) + +} diff --git a/server.go b/server.go index 190ccbcc4b..759739cdd4 100644 --- a/server.go +++ b/server.go @@ -2,7 +2,6 @@ package docker import ( "encoding/json" - "errors" "fmt" "github.com/dotcloud/docker/archive" "github.com/dotcloud/docker/auth" @@ -1810,102 +1809,33 @@ func (srv *Server) ContainerDestroy(job *engine.Job) engine.Status { return engine.StatusOK } -var ErrImageReferenced = errors.New("Image referenced by a repository") - -func (srv *Server) deleteImageAndChildren(id string, imgs *engine.Table, byParents map[string][]*Image) error { - // If the image is referenced by a repo, do not delete - if len(srv.runtime.repositories.ByID()[id]) != 0 { - return ErrImageReferenced - } - // If the image is not referenced but has children, go recursive - referenced := false - for _, img := range byParents[id] { - if err := srv.deleteImageAndChildren(img.ID, imgs, byParents); err != nil { - if err != ErrImageReferenced { - return err - } - referenced = true - } - } - if referenced { - return ErrImageReferenced - } - - // If the image is not referenced and has no children, remove it - byParents, err := srv.runtime.graph.ByParent() - if err != nil { - return err - } - if len(byParents[id]) == 0 && srv.canDeleteImage(id) == nil { - if err := srv.runtime.repositories.DeleteAll(id); err != nil { - return err - } - err := srv.runtime.graph.Delete(id) - if err != nil { - return err - } - out := &engine.Env{} - out.Set("Deleted", id) - imgs.Add(out) - srv.LogEvent("delete", id, "") - return nil - } - return nil -} - -func (srv *Server) deleteImageParents(img *Image, imgs *engine.Table) error { - if img.Parent != "" { - parent, err := srv.runtime.graph.Get(img.Parent) - if err != nil { - return err - } - byParents, err := srv.runtime.graph.ByParent() - if err != nil { - return err - } - // Remove all children images - if err := srv.deleteImageAndChildren(img.Parent, imgs, byParents); err != nil { - return err - } - return srv.deleteImageParents(parent, imgs) - } - return nil -} - -func (srv *Server) DeleteImage(name string, autoPrune bool) (*engine.Table, error) { +func (srv *Server) DeleteImage(name string, imgs *engine.Table, first, force bool) error { var ( repoName, tag string - img, err = srv.runtime.repositories.LookupImage(name) - imgs = engine.NewTable("", 0) tags = []string{} ) + repoName, tag = utils.ParseRepositoryTag(name) + if tag == "" { + tag = DEFAULTTAG + } + + img, err := srv.runtime.repositories.LookupImage(name) if err != nil { - return nil, fmt.Errorf("No such image: %s", name) - } - - // FIXME: What does autoPrune mean ? - if !autoPrune { - if err := srv.runtime.graph.Delete(img.ID); err != nil { - return nil, fmt.Errorf("Cannot delete image %s: %s", name, err) + if r, _ := srv.runtime.repositories.Get(repoName); r != nil { + return fmt.Errorf("No such image: %s:%s", repoName, tag) } - return nil, nil + return fmt.Errorf("No such image: %s", name) } - if !strings.Contains(img.ID, name) { - repoName, tag = utils.ParseRepositoryTag(name) + if strings.Contains(img.ID, name) { + repoName = "" + tag = "" } - // If we have a repo and the image is not referenced anywhere else - // then just perform an untag and do not validate. - // - // i.e. only validate if we are performing an actual delete and not - // an untag op - if repoName != "" && len(srv.runtime.repositories.ByID()[img.ID]) == 1 { - // Prevent deletion if image is used by a container - if err := srv.canDeleteImage(img.ID); err != nil { - return nil, err - } + byParents, err := srv.runtime.graph.ByParent() + if err != nil { + return err } //If delete by id, see if the id belong only to one repository @@ -1917,51 +1847,68 @@ func (srv *Server) DeleteImage(name string, autoPrune bool) (*engine.Table, erro if parsedTag != "" { tags = append(tags, parsedTag) } - } else if repoName != parsedRepo { + } else if repoName != parsedRepo && !force { // the id belongs to multiple repos, like base:latest and user:test, // in that case return conflict - return nil, fmt.Errorf("Conflict, cannot delete image %s because it is tagged in multiple repositories", utils.TruncateID(img.ID)) + return fmt.Errorf("Conflict, cannot delete image %s because it is tagged in multiple repositories, use -f to force", name) } } } else { tags = append(tags, tag) } + if !first && len(tags) > 0 { + return nil + } + //Untag the current image for _, tag := range tags { tagDeleted, err := srv.runtime.repositories.Delete(repoName, tag) if err != nil { - return nil, err + return err } if tagDeleted { out := &engine.Env{} - out.Set("Untagged", img.ID) + out.Set("Untagged", repoName+":"+tag) imgs.Add(out) srv.LogEvent("untag", img.ID, "") } } + tags = srv.runtime.repositories.ByID()[img.ID] + if (len(tags) <= 1 && repoName == "") || len(tags) == 0 { + if len(byParents[img.ID]) == 0 { + if err := srv.canDeleteImage(img.ID); err != nil { + return err + } + if err := srv.runtime.repositories.DeleteAll(img.ID); err != nil { + return err + } + if err := srv.runtime.graph.Delete(img.ID); err != nil { + return err + } + out := &engine.Env{} + out.Set("Deleted", img.ID) + imgs.Add(out) + srv.LogEvent("delete", img.ID, "") + if img.Parent != "" { + err := srv.DeleteImage(img.Parent, imgs, false, force) + if first { + return err + } - if len(srv.runtime.repositories.ByID()[img.ID]) == 0 { - if err := srv.deleteImageAndChildren(img.ID, imgs, nil); err != nil { - if err != ErrImageReferenced { - return imgs, err - } - } else if err := srv.deleteImageParents(img, imgs); err != nil { - if err != ErrImageReferenced { - return imgs, err } + } } - return imgs, nil + return nil } func (srv *Server) ImageDelete(job *engine.Job) engine.Status { if n := len(job.Args); n != 1 { return job.Errorf("Usage: %s IMAGE", job.Name) } - - imgs, err := srv.DeleteImage(job.Args[0], job.GetenvBool("autoPrune")) - if err != nil { + imgs := engine.NewTable("", 0) + if err := srv.DeleteImage(job.Args[0], imgs, true, job.GetenvBool("force")); err != nil { return job.Error(err) } if len(imgs.Data) == 0 { diff --git a/utils/jsonmessage.go b/utils/jsonmessage.go index 3e4e0f86ad..9050dda746 100644 --- a/utils/jsonmessage.go +++ b/utils/jsonmessage.go @@ -52,7 +52,7 @@ func (p *JSONProgress) String() string { } numbersBox = fmt.Sprintf("%8v/%v", current, total) - if p.Start > 0 && percentage < 50 { + if p.Current > 0 && p.Start > 0 && percentage < 50 { fromStart := time.Now().UTC().Sub(time.Unix(int64(p.Start), 0)) perEntry := fromStart / time.Duration(p.Current) left := time.Duration(p.Total-p.Current) * perEntry diff --git a/utils/utils.go b/utils/utils.go index 1aba80ff41..f24e17c38e 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -606,16 +606,22 @@ func GetKernelVersion() (*KernelVersionInfo, error) { func ParseRelease(release string) (*KernelVersionInfo, error) { var ( kernel, major, minor, parsed int - flavor string + flavor, partial string ) // Ignore error from Sscanf to allow an empty flavor. Instead, just // make sure we got all the version numbers. - parsed, _ = fmt.Sscanf(release, "%d.%d.%d%s", &kernel, &major, &minor, &flavor) - if parsed < 3 { + parsed, _ = fmt.Sscanf(release, "%d.%d%s", &kernel, &major, &partial) + if parsed < 2 { return nil, errors.New("Can't parse kernel version " + release) } + // sometimes we have 3.12.25-gentoo, but sometimes we just have 3.12-1-amd64 + parsed, _ = fmt.Sscanf(partial, ".%d%s", &minor, &flavor) + if parsed < 1 { + flavor = partial + } + return &KernelVersionInfo{ Kernel: kernel, Major: major, diff --git a/utils/utils_test.go b/utils/utils_test.go index 7e63a45cf7..78f4ca2bd2 100644 --- a/utils/utils_test.go +++ b/utils/utils_test.go @@ -420,6 +420,7 @@ func TestParseRelease(t *testing.T) { assertParseRelease(t, "3.4.54.longterm-1", &KernelVersionInfo{Kernel: 3, Major: 4, Minor: 54, Flavor: ".longterm-1"}, 0) assertParseRelease(t, "3.8.0-19-generic", &KernelVersionInfo{Kernel: 3, Major: 8, Minor: 0, Flavor: "-19-generic"}, 0) assertParseRelease(t, "3.12.8tag", &KernelVersionInfo{Kernel: 3, Major: 12, Minor: 8, Flavor: "tag"}, 0) + assertParseRelease(t, "3.12-1-amd64", &KernelVersionInfo{Kernel: 3, Major: 12, Minor: 0, Flavor: "-1-amd64"}, 0) } func TestParsePortMapping(t *testing.T) {