From 0db53bd2ecba097c6ace1a1088e11458e139390a Mon Sep 17 00:00:00 2001 From: Darren Shepherd Date: Fri, 21 Feb 2014 03:02:06 -0700 Subject: [PATCH 01/25] Add MTU to lxc conf to make host and container MTU match If you are using jumbo frames the host side of the veth was being set to 1500. Docker would set the MTU of the container side of the veth to 9001. This would lead to a situation in which the two sides of the veth had different MTU sizes causing issues in network traffic. Docker-DCO-1.1-Signed-off-by: Darren Shepherd (github: ibuildthecloud) --- execdriver/lxc/lxc_template.go | 1 + 1 file changed, 1 insertion(+) 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 From de848a14caf1636cc24e621a35d7073cebe5444d Mon Sep 17 00:00:00 2001 From: Vincent Batts Date: Mon, 24 Feb 2014 13:42:09 -0500 Subject: [PATCH 02/25] seperate out the terminal functions from lxc to the pkg/term Since these functions are indepenent of lxc, and could be used by other drivers. Docker-DCO-1.1-Signed-off-by: Vincent Batts (github: vbatts) --- execdriver/lxc/driver.go | 3 ++- execdriver/lxc/term.go => pkg/term/driver.go | 5 ++--- 2 files changed, 4 insertions(+), 4 deletions(-) rename execdriver/lxc/term.go => pkg/term/driver.go (94%) diff --git a/execdriver/lxc/driver.go b/execdriver/lxc/driver.go index 5be7ad2219..145965a383 100644 --- a/execdriver/lxc/driver.go +++ b/execdriver/lxc/driver.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/dotcloud/docker/execdriver" "github.com/dotcloud/docker/pkg/cgroups" + "github.com/dotcloud/docker/pkg/term" "github.com/dotcloud/docker/utils" "io/ioutil" "log" @@ -77,7 +78,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 := term.SetTerminal(c, pipes); err != nil { return -1, err } configPath, err := d.generateLXCConfig(c) diff --git a/execdriver/lxc/term.go b/pkg/term/driver.go similarity index 94% rename from execdriver/lxc/term.go rename to pkg/term/driver.go index d772f60972..6e1fae41ba 100644 --- a/execdriver/lxc/term.go +++ b/pkg/term/driver.go @@ -1,8 +1,7 @@ -package lxc +package term import ( "github.com/dotcloud/docker/execdriver" - "github.com/dotcloud/docker/pkg/term" "github.com/kr/pty" "io" "os" @@ -51,7 +50,7 @@ func (t *TtyConsole) Master() *os.File { } func (t *TtyConsole) Resize(h, w int) error { - return term.SetWinsize(t.master.Fd(), &term.Winsize{Height: uint16(h), Width: uint16(w)}) + return SetWinsize(t.master.Fd(), &Winsize{Height: uint16(h), Width: uint16(w)}) } func (t *TtyConsole) attach(command *execdriver.Command, pipes *execdriver.Pipes) error { From fabc478e7e33fd3b3c00c3bd74bad0d87a2c23e3 Mon Sep 17 00:00:00 2001 From: Vincent Batts Date: Mon, 24 Feb 2014 15:28:45 -0500 Subject: [PATCH 03/25] Move the terminal setup to execdriver, instead of ./pkg/term It is independent of any particular driver, but likely used by multiple execdrivers. Also, pkg/... is not to have any links to docker, which this terminal setup does. Docker-DCO-1.1-Signed-off-by: Vincent Batts (github: vbatts) --- .../driver.go => execdriver/termconsole.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) rename pkg/term/driver.go => execdriver/termconsole.go (74%) diff --git a/pkg/term/driver.go b/execdriver/termconsole.go similarity index 74% rename from pkg/term/driver.go rename to execdriver/termconsole.go index 6e1fae41ba..2da48d1864 100644 --- a/pkg/term/driver.go +++ b/execdriver/termconsole.go @@ -1,15 +1,15 @@ -package term +package execdriver import ( - "github.com/dotcloud/docker/execdriver" + "github.com/dotcloud/docker/pkg/term" "github.com/kr/pty" "io" "os" ) -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 { @@ -29,7 +29,7 @@ type TtyConsole struct { slave *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 @@ -50,10 +50,10 @@ func (t *TtyConsole) Master() *os.File { } func (t *TtyConsole) Resize(h, w int) error { - return SetWinsize(t.master.Fd(), &Winsize{Height: uint16(h), Width: uint16(w)}) + return term.SetWinsize(t.master.Fd(), &term.Winsize{Height: uint16(h), Width: uint16(w)}) } -func (t *TtyConsole) attach(command *execdriver.Command, pipes *execdriver.Pipes) error { +func (t *TtyConsole) attach(command *Command, pipes *Pipes) error { command.Stdout = t.slave command.Stderr = t.slave command.Console = t.slave.Name() @@ -87,7 +87,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.attach(command, pipes); err != nil { @@ -96,7 +96,7 @@ func NewStdConsole(command *execdriver.Command, pipes *execdriver.Pipes) (*StdCo return std, nil } -func (s *StdConsole) attach(command *execdriver.Command, pipes *execdriver.Pipes) error { +func (s *StdConsole) attach(command *Command, pipes *Pipes) error { command.Stdout = pipes.Stdout command.Stderr = pipes.Stderr From 62b21daded2ea9b703a6e5fa0479e007016b1edb Mon Sep 17 00:00:00 2001 From: Solomon Hykes Date: Mon, 24 Feb 2014 12:26:56 -0800 Subject: [PATCH 04/25] Engine: cleanup side effects between tests Docker-DCO-1.1-Signed-off-by: Solomon Hykes (github: shykes) --- engine/engine.go | 4 ++++ engine/engine_test.go | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/engine/engine.go b/engine/engine.go index 5814955fdd..946e8a9ece 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -29,6 +29,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*. diff --git a/engine/engine_test.go b/engine/engine_test.go index da59610727..90b61cda9a 100644 --- a/engine/engine_test.go +++ b/engine/engine_test.go @@ -16,6 +16,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) @@ -32,6 +34,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) { @@ -48,6 +51,7 @@ func TestJob(t *testing.T) { } eng.Register("dummy2", h) + defer unregister("dummy2") job2 := eng.Job("dummy2", "--level=awesome") if job2.handler == nil { From cd846ecb60ad936d58d70762ed91cffa906cef93 Mon Sep 17 00:00:00 2001 From: Solomon Hykes Date: Sat, 15 Feb 2014 15:06:21 -0800 Subject: [PATCH 05/25] Engine: builtin command 'commands' returns a list of registered commands Docker-DCO-1.1-Signed-off-by: Solomon Hykes (github: shykes) --- engine/engine.go | 18 ++++++++++++++++++ engine/engine_test.go | 19 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/engine/engine.go b/engine/engine.go index 946e8a9ece..68e109e7f2 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -9,6 +9,7 @@ import ( "os" "path/filepath" "runtime" + "sort" "strings" ) @@ -110,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 @@ -121,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 90b61cda9a..e922d8a875 100644 --- a/engine/engine_test.go +++ b/engine/engine_test.go @@ -1,6 +1,7 @@ package engine import ( + "bytes" "io/ioutil" "os" "path" @@ -63,6 +64,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 { From 2ae8180de2d814e04834d41f20b2c96a06995678 Mon Sep 17 00:00:00 2001 From: Tianon Gravi Date: Mon, 24 Feb 2014 13:38:58 -0700 Subject: [PATCH 06/25] Adjust kernel version parsing to be more lenient of strange things like "3.12-1-amd64" Docker-DCO-1.1-Signed-off-by: Andrew Page (github: tianon) --- utils/utils.go | 12 +++++++++--- utils/utils_test.go | 1 + 2 files changed, 10 insertions(+), 3 deletions(-) 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) { From 9c4799b7e7627b21f2748458e46d697c3ee4981e Mon Sep 17 00:00:00 2001 From: Tim Ruffles Date: Mon, 24 Feb 2014 23:07:40 +0000 Subject: [PATCH 07/25] help contributors be more productive certainly saved me time after I found this in hack/make.sh :) Docker-DCO-1.1-Signed-off-by: Tim Ruffles (github: github_handle) --- docs/sources/contributing/devenvironment.rst | 3 +++ 1 file changed, 3 insertions(+) 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 From b7a0f62f0f9079d0d628a284a77af0c474eb9375 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Tue, 25 Feb 2014 08:01:46 +0000 Subject: [PATCH 08/25] fix divide by zero error Docker-DCO-1.1-Signed-off-by: Victor Vieux (github: vieux) --- utils/jsonmessage.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From c35853191ccd9ca8fe91b666d598eac3e4e12c67 Mon Sep 17 00:00:00 2001 From: Vincent Batts Date: Tue, 25 Feb 2014 10:07:06 -0500 Subject: [PATCH 09/25] correcting the package name for the terminal setup Docker-DCO-1.1-Signed-off-by: Vincent Batts (github: vbatts) --- execdriver/lxc/driver.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/execdriver/lxc/driver.go b/execdriver/lxc/driver.go index 145965a383..c18b2e6ab4 100644 --- a/execdriver/lxc/driver.go +++ b/execdriver/lxc/driver.go @@ -4,7 +4,6 @@ import ( "fmt" "github.com/dotcloud/docker/execdriver" "github.com/dotcloud/docker/pkg/cgroups" - "github.com/dotcloud/docker/pkg/term" "github.com/dotcloud/docker/utils" "io/ioutil" "log" @@ -78,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 := term.SetTerminal(c, pipes); err != nil { + if err := execdriver.SetTerminal(c, pipes); err != nil { return -1, err } configPath, err := d.generateLXCConfig(c) From 7bce5957a08ae31f4b83d1a13ad056e5b2af15e1 Mon Sep 17 00:00:00 2001 From: Tianon Gravi Date: Tue, 25 Feb 2014 13:07:19 -0700 Subject: [PATCH 10/25] Convert api/server.go to also use the handy user package Docker-DCO-1.1-Signed-off-by: Andrew Page (github: tianon) --- api/server.go | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/api/server.go b/api/server.go index 4b10a2dae1..0dd9d966e6 100644 --- a/api/server.go +++ b/api/server.go @@ -12,6 +12,7 @@ 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/utils" "github.com/gorilla/mux" "io" @@ -21,7 +22,6 @@ import ( "net/http" "net/http/pprof" "os" - "regexp" "strconv" "strings" "syscall" @@ -1142,18 +1142,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 } } From 795ed6b1e511fa6713fbc9ea4e8569aab15b98ff Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Tue, 11 Feb 2014 02:44:17 +0000 Subject: [PATCH 11/25] Rewrite docker rmi Docker-DCO-1.1-Signed-off-by: Victor Vieux (github: vieux) --- api/client.go | 8 +- api/server.go | 2 +- integration/api_test.go | 2 + integration/commands_test.go | 5 +- integration/server_test.go | 13 ++-- server.go | 138 ++++++++++------------------------- 6 files changed, 58 insertions(+), 110 deletions(-) diff --git a/api/client.go b/api/client.go index eb345ae40b..7acf63f9ca 100644 --- a/api/client.go +++ b/api/client.go @@ -781,6 +781,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 := 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 +790,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") diff --git a/api/server.go b/api/server.go index 0dd9d966e6..03f451fe8b 100644 --- a/api/server.go +++ b/api/server.go @@ -631,7 +631,7 @@ 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() } diff --git a/integration/api_test.go b/integration/api_test.go index 5779e6b226..abbc1a1c59 100644 --- a/integration/api_test.go +++ b/integration/api_test.go @@ -1175,6 +1175,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..0cf7fda660 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 { diff --git a/integration/server_test.go b/integration/server_test.go index 2234bcab08..8600e78573 100644 --- a/integration/server_test.go +++ b/integration/server_test.go @@ -35,7 +35,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 +47,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 +56,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 +447,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 +682,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) } diff --git a/server.go b/server.go index 190ccbcc4b..91266c23b9 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,27 @@ 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{} ) 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) - } - return nil, nil + return fmt.Errorf("No such image: %s", name) } if !strings.Contains(img.ID, name) { repoName, tag = utils.ParseRepositoryTag(name) + if tag == "" { + tag = DEFAULTTAG + } } - // 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,10 +1841,10 @@ 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 { @@ -1931,37 +1855,51 @@ func (srv *Server) DeleteImage(name string, autoPrune bool) (*engine.Table, erro 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 + } + err := srv.runtime.graph.Delete(img.ID) + if 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 { + var 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 { From e7cc88c39f302c020f70a00bca54bddeecb608a4 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Fri, 14 Feb 2014 22:53:53 +0000 Subject: [PATCH 12/25] bump API version Docker-DCO-1.1-Signed-off-by: Victor Vieux (github: vieux) --- api/client.go | 6 +- api/common.go | 2 +- api/server.go | 128 +- .../reference/api/docker_remote_api.rst | 20 +- .../reference/api/docker_remote_api_v1.10.rst | 1282 +++++++++++++++++ utils/utils.go | 22 + utils/utils_test.go | 20 + 7 files changed, 1405 insertions(+), 75 deletions(-) create mode 100644 docs/sources/reference/api/docker_remote_api_v1.10.rst diff --git a/api/client.go b/api/client.go index 7acf63f9ca..da2e7730f1 100644 --- a/api/client.go +++ b/api/client.go @@ -2038,7 +2038,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 } @@ -2115,7 +2115,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 } @@ -2179,7 +2179,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 03f451fe8b..007c07ddb8 100644 --- a/api/server.go +++ b/api/server.go @@ -32,7 +32,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 string, 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 +113,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 string, w http.ResponseWriter, r *http.Request, vars map[string]string) error { var ( authConfig, err = ioutil.ReadAll(r.Body) job = eng.Job("auth") @@ -136,13 +136,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 string, 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 string, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if vars == nil { return fmt.Errorf("Missing parameter") } @@ -160,7 +160,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 string, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if vars == nil { return fmt.Errorf("Missing parameter") } @@ -172,7 +172,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 string, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if err := parseForm(r); err != nil { return err } @@ -186,7 +186,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 utils.CompareVersion(version, "1.7") >= 0 { streamJSON(job, w, false) } else if outs, err = job.Stdout.AddListTable(); err != nil { return err @@ -196,7 +196,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 utils.CompareVersion(version, "1.7") < 0 && outs != nil { // Convert to legacy format outsLegacy := engine.NewTable("Created", 0) for _, out := range outs.Data { for _, repoTag := range out.GetList("RepoTags") { @@ -219,8 +219,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 string, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if utils.CompareVersion(version, "1.6") > 0 { w.WriteHeader(http.StatusNotFound) return fmt.Errorf("This is now implemented in the client.") } @@ -228,13 +228,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 string, 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 string, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if err := parseForm(r); err != nil { return err } @@ -245,7 +245,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 string, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if vars == nil { return fmt.Errorf("Missing parameter") } @@ -259,7 +259,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 string, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if vars == nil { return fmt.Errorf("Missing parameter") } @@ -269,8 +269,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 string, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if utils.CompareVersion(version, "1.4") < 0 { return fmt.Errorf("top was improved a lot since 1.3, Please upgrade your docker client.") } if vars == nil { @@ -285,7 +285,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 string, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if err := parseForm(r); err != nil { return err } @@ -301,7 +301,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 utils.CompareVersion(version, "1.5") >= 0 { streamJSON(job, w, false) } else if outs, err = job.Stdout.AddTable(); err != nil { return err @@ -309,7 +309,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 utils.CompareVersion(version, "1.5") < 0 { // Convert to legacy format for _, out := range outs.Data { ports := engine.NewTable("", 0) ports.ReadListFrom([]byte(out.Get("Ports"))) @@ -323,7 +323,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 string, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if err := parseForm(r); err != nil { return err } @@ -340,7 +340,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 string, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if err := parseForm(r); err != nil { return err } @@ -369,7 +369,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 string, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if err := parseForm(r); err != nil { return err } @@ -389,9 +389,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 +397,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", utils.CompareVersion(version, "1.3") > 0) job.SetenvJson("metaHeaders", metaHeaders) job.SetenvJson("authConfig", authConfig) } else { //import @@ -408,7 +405,7 @@ func postImagesCreate(eng *engine.Engine, version float64, w http.ResponseWriter job.Stdin.Add(r.Body) } - if version > 1.0 { + if utils.CompareVersion(version, "1.0") > 0 { job.SetenvBool("json", true) streamJSON(job, w, true) } else { @@ -418,14 +415,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(utils.CompareVersion(version, "1.0") > 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 string, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if err := parseForm(r); err != nil { return err } @@ -457,19 +454,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 string, 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 utils.CompareVersion(version, "1.0") > 0 { job.SetenvBool("json", true) streamJSON(job, w, false) } else { @@ -479,14 +472,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(utils.CompareVersion(version, "1.0") > 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 string, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if vars == nil { return fmt.Errorf("Missing parameter") } @@ -517,13 +510,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 utils.CompareVersion(version, "1.0") > 0 { job.SetenvBool("json", true) streamJSON(job, w, true) } else { @@ -534,17 +524,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(utils.CompareVersion(version, "1.0") > 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 string, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if vars == nil { return fmt.Errorf("Missing parameter") } - if version > 1.0 { + if utils.CompareVersion(version, "1.0") > 0 { w.Header().Set("Content-Type", "application/x-tar") } job := eng.Job("image_export", vars["name"]) @@ -552,13 +542,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 string, 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 string, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if err := parseForm(r); err != nil { return nil } @@ -589,7 +579,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 string, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if err := parseForm(r); err != nil { return err } @@ -605,7 +595,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 string, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if err := parseForm(r); err != nil { return err } @@ -622,7 +612,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 string, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if err := parseForm(r); err != nil { return err } @@ -636,7 +626,7 @@ func deleteImages(eng *engine.Engine, version float64, w http.ResponseWriter, r 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 string, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if vars == nil { return fmt.Errorf("Missing parameter") } @@ -657,7 +647,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 string, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if err := parseForm(r); err != nil { return err } @@ -673,7 +663,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 string, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if vars == nil { return fmt.Errorf("Missing parameter") } @@ -695,7 +685,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 string, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if err := parseForm(r); err != nil { return err } @@ -708,7 +698,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 string, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if err := parseForm(r); err != nil { return err } @@ -750,7 +740,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") && utils.CompareVersion(version, "1.6") >= 0 { errStream = utils.NewStdWriter(outStream, utils.Stderr) outStream = utils.NewStdWriter(outStream, utils.Stdout) } else { @@ -773,7 +763,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 string, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if err := parseForm(r); err != nil { return err } @@ -805,7 +795,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 string, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if vars == nil { return fmt.Errorf("Missing parameter") } @@ -815,7 +805,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 string, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if vars == nil { return fmt.Errorf("Missing parameter") } @@ -825,8 +815,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 string, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if utils.CompareVersion(version, "1.3") < 0 { return fmt.Errorf("Multipart upload for build is no longer supported. Please upgrade your docker client.") } var ( @@ -841,7 +831,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 utils.CompareVersion(version, "1.9") < 0 && 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 +849,7 @@ func postBuild(eng *engine.Engine, version float64, w http.ResponseWriter, r *ht } } - if version >= 1.8 { + if utils.CompareVersion(version, "1.8") >= 0 { job.SetenvBool("json", true) streamJSON(job, w, true) } else { @@ -878,13 +868,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(utils.CompareVersion(version, "1.8") >= 0) 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 string, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if vars == nil { return fmt.Errorf("Missing parameter") } @@ -917,7 +907,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 string, w http.ResponseWriter, r *http.Request, vars map[string]string) error { w.WriteHeader(http.StatusOK) return nil } @@ -942,16 +932,16 @@ func makeHttpHandler(eng *engine.Engine, logging bool, localMethod string, local 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 := 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 utils.CompareVersion(version, APIVERSION) == 1 { + http.Error(w, fmt.Errorf("client and server don't have same version (client : %s, server: %s)", version, APIVERSION).Error(), http.StatusNotFound) return } diff --git a/docs/sources/reference/api/docker_remote_api.rst b/docs/sources/reference/api/docker_remote_api.rst index 69bbf71ec9..51c4a06c91 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..6ee3febc27 --- /dev/null +++ b/docs/sources/reference/api/docker_remote_api_v1.10.rst @@ -0,0 +1,1282 @@ +:title: Remote API v1.9 +:description: API Documentation for Docker +:keywords: API, Docker, rcli, REST, documentation + +:orphan: + +====================== +Docker Remote API v1.9 +====================== + +.. 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/utils/utils.go b/utils/utils.go index 1aba80ff41..8d15f85262 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -972,3 +972,25 @@ func NewReadCloserWrapper(r io.Reader, closer func() error) io.ReadCloser { closer: closer, } } + +func CompareVersion(a, b string) int { + aa := strings.Split(a, ".") + bb := strings.Split(b, ".") + for i, s := range aa { + var ai, bi int + ai, _ = strconv.Atoi(s) + if len(bb) > i { + bi, _ = strconv.Atoi(bb[i]) + } + if ai > bi { + return 1 + } + if bi > ai { + return -1 + } + } + if len(bb) > len(aa) { + return -1 + } + return 0 +} diff --git a/utils/utils_test.go b/utils/utils_test.go index 7e63a45cf7..41dba72aa7 100644 --- a/utils/utils_test.go +++ b/utils/utils_test.go @@ -479,3 +479,23 @@ func StrSlicesEqual(a, b []string) bool { return true } + +func asserVersion(t *testing.T, a, b string, result int) { + if r := CompareVersion(a, b); r != result { + t.Fatalf("Unexpected version comparison result. Found %d, expected %d", r, result) + } +} + +func TestCompareVersion(t *testing.T) { + asserVersion(t, "1.12", "1.12", 0) + asserVersion(t, "1.05.00.0156", "1.0.221.9289", 1) + asserVersion(t, "1", "1.0.1", -1) + asserVersion(t, "1.0.1", "1", 1) + asserVersion(t, "1.0.1", "1.0.2", -1) + asserVersion(t, "1.0.2", "1.0.3", -1) + asserVersion(t, "1.0.3", "1.1", -1) + asserVersion(t, "1.1", "1.1.1", -1) + asserVersion(t, "1.1.1", "1.1.2", -1) + asserVersion(t, "1.1.2", "1.2", -1) + +} From c7f825c0f08e8b6e3cd9dcebc81c21002afcf133 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Sat, 15 Feb 2014 00:10:57 +0000 Subject: [PATCH 13/25] := / var Docker-DCO-1.1-Signed-off-by: Victor Vieux (github: vieux) --- api/client.go | 6 ++++-- server.go | 7 +++---- utils/utils.go | 6 ++++-- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/api/client.go b/api/client.go index da2e7730f1..a6ad41a714 100644 --- a/api/client.go +++ b/api/client.go @@ -780,8 +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") - force := cmd.Bool([]string{"f", "-force"}, false, "Force") + 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 } diff --git a/server.go b/server.go index 91266c23b9..7c3d482b86 100644 --- a/server.go +++ b/server.go @@ -1812,10 +1812,10 @@ func (srv *Server) ContainerDestroy(job *engine.Job) engine.Status { func (srv *Server) DeleteImage(name string, imgs *engine.Table, first, force bool) error { var ( repoName, tag string - img, err = srv.runtime.repositories.LookupImage(name) tags = []string{} ) + img, err := srv.runtime.repositories.LookupImage(name) if err != nil { return fmt.Errorf("No such image: %s", name) } @@ -1873,8 +1873,7 @@ func (srv *Server) DeleteImage(name string, imgs *engine.Table, first, force boo if err := srv.runtime.repositories.DeleteAll(img.ID); err != nil { return err } - err := srv.runtime.graph.Delete(img.ID) - if err != nil { + if err := srv.runtime.graph.Delete(img.ID); err != nil { return err } out := &engine.Env{} @@ -1898,7 +1897,7 @@ func (srv *Server) ImageDelete(job *engine.Job) engine.Status { if n := len(job.Args); n != 1 { return job.Errorf("Usage: %s IMAGE", job.Name) } - var imgs = engine.NewTable("", 0) + imgs := engine.NewTable("", 0) if err := srv.DeleteImage(job.Args[0], imgs, true, job.GetenvBool("force")); err != nil { return job.Error(err) } diff --git a/utils/utils.go b/utils/utils.go index 8d15f85262..88f43c8bd5 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -974,8 +974,10 @@ func NewReadCloserWrapper(r io.Reader, closer func() error) io.ReadCloser { } func CompareVersion(a, b string) int { - aa := strings.Split(a, ".") - bb := strings.Split(b, ".") + var ( + aa = strings.Split(a, ".") + bb = strings.Split(b, ".") + ) for i, s := range aa { var ai, bi int ai, _ = strconv.Atoi(s) From 964d82d00506c3005d102b9a1f94848182c5989d Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Tue, 18 Feb 2014 01:44:53 +0000 Subject: [PATCH 14/25] fix tests Docker-DCO-1.1-Signed-off-by: Victor Vieux (github: vieux) --- api/server.go | 4 ++-- integration/api_test.go | 1 + integration/server_test.go | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/api/server.go b/api/server.go index 007c07ddb8..1019521e03 100644 --- a/api/server.go +++ b/api/server.go @@ -1057,13 +1057,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 string, 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 } diff --git a/integration/api_test.go b/integration/api_test.go index abbc1a1c59..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" diff --git a/integration/server_test.go b/integration/server_test.go index 8600e78573..2fe16551dc 100644 --- a/integration/server_test.go +++ b/integration/server_test.go @@ -691,7 +691,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) } } From 626cee87f779dff2840b1a31cbe6912545758a9f Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Tue, 18 Feb 2014 03:03:19 +0000 Subject: [PATCH 15/25] fix message 'No such image: ubuntu' => 'No such image: ubuntu:latest' Docker-DCO-1.1-Signed-off-by: Victor Vieux (github: vieux) --- server.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/server.go b/server.go index 7c3d482b86..9feca36e5b 100644 --- a/server.go +++ b/server.go @@ -1815,16 +1815,22 @@ func (srv *Server) DeleteImage(name string, imgs *engine.Table, first, force boo tags = []string{} ) + repoName, tag = utils.ParseRepositoryTag(name) + if tag == "" { + tag = DEFAULTTAG + } + img, err := srv.runtime.repositories.LookupImage(name) if err != nil { + if r, _ := srv.runtime.repositories.Get(repoName); r != nil { + return fmt.Errorf("No such image: %s:%s", repoName, tag) + } return fmt.Errorf("No such image: %s", name) } - if !strings.Contains(img.ID, name) { - repoName, tag = utils.ParseRepositoryTag(name) - if tag == "" { - tag = DEFAULTTAG - } + if strings.Contains(img.ID, name) { + repoName = "" + tag = "" } byParents, err := srv.runtime.graph.ByParent() From 21f56c04e4f4df4e171985896b54b243c6ef0fcb Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Wed, 19 Feb 2014 01:21:32 +0000 Subject: [PATCH 16/25] fix doc Docker-DCO-1.1-Signed-off-by: Victor Vieux (github: vieux) --- docs/sources/reference/api/docker_remote_api.rst | 2 +- docs/sources/reference/api/docker_remote_api_v1.10.rst | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/sources/reference/api/docker_remote_api.rst b/docs/sources/reference/api/docker_remote_api.rst index 51c4a06c91..3733effaf8 100644 --- a/docs/sources/reference/api/docker_remote_api.rst +++ b/docs/sources/reference/api/docker_remote_api.rst @@ -36,7 +36,7 @@ You can still call an old version of the api using v1.10 -**** +***** Full Documentation ------------------ diff --git a/docs/sources/reference/api/docker_remote_api_v1.10.rst b/docs/sources/reference/api/docker_remote_api_v1.10.rst index 6ee3febc27..a6eec3551f 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.10.rst +++ b/docs/sources/reference/api/docker_remote_api_v1.10.rst @@ -1,12 +1,12 @@ -:title: Remote API v1.9 +:title: Remote API v1.10 :description: API Documentation for Docker :keywords: API, Docker, rcli, REST, documentation :orphan: -====================== -Docker Remote API v1.9 -====================== +======================= +Docker Remote API v1.10 +======================= .. contents:: Table of Contents From 3f6324f0e8e1b4d117a5d22f752e1d5980eea939 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Wed, 19 Feb 2014 01:34:37 +0000 Subject: [PATCH 17/25] fix bad rebase Docker-DCO-1.1-Signed-off-by: Victor Vieux (github: vieux) --- server.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server.go b/server.go index 9feca36e5b..759739cdd4 100644 --- a/server.go +++ b/server.go @@ -1857,6 +1857,10 @@ func (srv *Server) DeleteImage(name string, imgs *engine.Table, first, force boo 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) From 10faefac3b88069f22283f3f2e018d100a66f2b9 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Thu, 20 Feb 2014 00:13:37 +0000 Subject: [PATCH 18/25] add missing doc Docker-DCO-1.1-Signed-off-by: Victor Vieux (github: vieux) --- docs/sources/reference/commandline/cli.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/sources/reference/commandline/cli.rst b/docs/sources/reference/commandline/cli.rst index b221f0de6b..84cf2c1d67 100644 --- a/docs/sources/reference/commandline/cli.rst +++ b/docs/sources/reference/commandline/cli.rst @@ -1023,6 +1023,8 @@ containers will not be deleted. Usage: docker rmi IMAGE [IMAGE...] Remove one or more images + + -f, --force=false: Force Removing tagged images ~~~~~~~~~~~~~~~~~~~~~~ From 8dad771daa6572ca15949d3e53e825f4837c0af9 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Fri, 21 Feb 2014 23:15:28 +0000 Subject: [PATCH 19/25] add version pkg Docker-DCO-1.1-Signed-off-by: Victor Vieux (github: vieux) --- api/server.go | 123 ++++++++++++++++++------------------ integration/server_test.go | 1 + pkg/version/version.go | 52 +++++++++++++++ pkg/version/version_test.go | 25 ++++++++ utils/utils.go | 24 ------- utils/utils_test.go | 20 ------ 6 files changed, 140 insertions(+), 105 deletions(-) create mode 100644 pkg/version/version.go create mode 100644 pkg/version/version_test.go diff --git a/api/server.go b/api/server.go index 1019521e03..fc4a5c2f04 100644 --- a/api/server.go +++ b/api/server.go @@ -13,6 +13,7 @@ import ( "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" @@ -32,7 +33,7 @@ var ( activationLock chan struct{} ) -type HttpApiFunc func(eng *engine.Engine, version string, 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 string, 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 string, w http.ResponseWriter, r *http return nil } -func getVersion(eng *engine.Engine, version string, 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 string, 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 string, w http.ResponseWrite return nil } -func getContainersExport(eng *engine.Engine, version string, 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 string, w http.ResponseWrit return nil } -func getImagesJSON(eng *engine.Engine, version string, 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 string, w http.ResponseWriter, r job.Setenv("filter", r.Form.Get("filter")) job.Setenv("all", r.Form.Get("all")) - if utils.CompareVersion(version, "1.7") >= 0 { + 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 string, w http.ResponseWriter, r return err } - if utils.CompareVersion(version, "1.7") < 0 && 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 string, w http.ResponseWriter, r return nil } -func getImagesViz(eng *engine.Engine, version string, w http.ResponseWriter, r *http.Request, vars map[string]string) error { - if utils.CompareVersion(version, "1.6") > 0 { +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 string, w http.ResponseWriter, r * return nil } -func getInfo(eng *engine.Engine, version string, 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 string, 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 string, w http.ResponseWriter, r *htt return job.Run() } -func getImagesHistory(eng *engine.Engine, version string, 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 string, w http.ResponseWriter, return nil } -func getContainersChanges(eng *engine.Engine, version string, 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 string, w http.ResponseWri return job.Run() } -func getContainersTop(eng *engine.Engine, version string, w http.ResponseWriter, r *http.Request, vars map[string]string) error { - if utils.CompareVersion(version, "1.4") < 0 { +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 string, w http.ResponseWriter, return job.Run() } -func getContainersJSON(eng *engine.Engine, version string, 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 string, w http.ResponseWriter job.Setenv("before", r.Form.Get("before")) job.Setenv("limit", r.Form.Get("limit")) - if utils.CompareVersion(version, "1.5") >= 0 { + 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 string, w http.ResponseWriter if err = job.Run(); err != nil { return err } - if utils.CompareVersion(version, "1.5") < 0 { // 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 string, w http.ResponseWriter return nil } -func postImagesTag(eng *engine.Engine, version string, 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 string, w http.ResponseWriter, r return nil } -func postCommit(eng *engine.Engine, version string, 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 string, w http.ResponseWriter, r *ht } // Creates an image from Pull or from Import -func postImagesCreate(eng *engine.Engine, version string, 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 } @@ -397,7 +398,7 @@ func postImagesCreate(eng *engine.Engine, version string, w http.ResponseWriter, } } job = eng.Job("pull", r.Form.Get("fromImage"), tag) - job.SetenvBool("parallel", utils.CompareVersion(version, "1.3") > 0) + job.SetenvBool("parallel", version.GreaterThan("1.3")) job.SetenvJson("metaHeaders", metaHeaders) job.SetenvJson("authConfig", authConfig) } else { //import @@ -405,7 +406,7 @@ func postImagesCreate(eng *engine.Engine, version string, w http.ResponseWriter, job.Stdin.Add(r.Body) } - if utils.CompareVersion(version, "1.0") > 0 { + if version.GreaterThan("1.0") { job.SetenvBool("json", true) streamJSON(job, w, true) } else { @@ -415,14 +416,14 @@ func postImagesCreate(eng *engine.Engine, version string, w http.ResponseWriter, if !job.Stdout.Used() { return err } - sf := utils.NewStreamFormatter(utils.CompareVersion(version, "1.0") > 0) + sf := utils.NewStreamFormatter(version.GreaterThan("1.0")) w.Write(sf.FormatError(err)) } return nil } -func getImagesSearch(eng *engine.Engine, version string, 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 } @@ -454,7 +455,7 @@ func getImagesSearch(eng *engine.Engine, version string, w http.ResponseWriter, return job.Run() } -func postImagesInsert(eng *engine.Engine, version string, 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 } @@ -462,7 +463,7 @@ func postImagesInsert(eng *engine.Engine, version string, w http.ResponseWriter, return fmt.Errorf("Missing parameter") } job := eng.Job("insert", vars["name"], r.Form.Get("url"), r.Form.Get("path")) - if utils.CompareVersion(version, "1.0") > 0 { + if version.GreaterThan("1.0") { job.SetenvBool("json", true) streamJSON(job, w, false) } else { @@ -472,14 +473,14 @@ func postImagesInsert(eng *engine.Engine, version string, w http.ResponseWriter, if !job.Stdout.Used() { return err } - sf := utils.NewStreamFormatter(utils.CompareVersion(version, "1.0") > 0) + sf := utils.NewStreamFormatter(version.GreaterThan("1.0")) w.Write(sf.FormatError(err)) } return nil } -func postImagesPush(eng *engine.Engine, version string, 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") } @@ -513,7 +514,7 @@ func postImagesPush(eng *engine.Engine, version string, w http.ResponseWriter, r job := eng.Job("push", vars["name"]) job.SetenvJson("metaHeaders", metaHeaders) job.SetenvJson("authConfig", authConfig) - if utils.CompareVersion(version, "1.0") > 0 { + if version.GreaterThan("1.0") { job.SetenvBool("json", true) streamJSON(job, w, true) } else { @@ -524,17 +525,17 @@ func postImagesPush(eng *engine.Engine, version string, w http.ResponseWriter, r if !job.Stdout.Used() { return err } - sf := utils.NewStreamFormatter(utils.CompareVersion(version, "1.0") > 0) + sf := utils.NewStreamFormatter(version.GreaterThan("1.0")) w.Write(sf.FormatError(err)) } return nil } -func getImagesGet(eng *engine.Engine, version string, 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 utils.CompareVersion(version, "1.0") > 0 { + if version.GreaterThan("1.0") { w.Header().Set("Content-Type", "application/x-tar") } job := eng.Job("image_export", vars["name"]) @@ -542,13 +543,13 @@ func getImagesGet(eng *engine.Engine, version string, w http.ResponseWriter, r * return job.Run() } -func postImagesLoad(eng *engine.Engine, version string, 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 string, 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 } @@ -579,7 +580,7 @@ func postContainersCreate(eng *engine.Engine, version string, w http.ResponseWri return writeJSON(w, http.StatusCreated, out) } -func postContainersRestart(eng *engine.Engine, version string, 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 } @@ -595,7 +596,7 @@ func postContainersRestart(eng *engine.Engine, version string, w http.ResponseWr return nil } -func deleteContainers(eng *engine.Engine, version string, 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 } @@ -612,7 +613,7 @@ func deleteContainers(eng *engine.Engine, version string, w http.ResponseWriter, return nil } -func deleteImages(eng *engine.Engine, version string, 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 } @@ -626,7 +627,7 @@ func deleteImages(eng *engine.Engine, version string, w http.ResponseWriter, r * return job.Run() } -func postContainersStart(eng *engine.Engine, version string, 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") } @@ -647,7 +648,7 @@ func postContainersStart(eng *engine.Engine, version string, w http.ResponseWrit return nil } -func postContainersStop(eng *engine.Engine, version string, 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 } @@ -663,7 +664,7 @@ func postContainersStop(eng *engine.Engine, version string, w http.ResponseWrite return nil } -func postContainersWait(eng *engine.Engine, version string, 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") } @@ -685,7 +686,7 @@ func postContainersWait(eng *engine.Engine, version string, w http.ResponseWrite return writeJSON(w, http.StatusOK, env) } -func postContainersResize(eng *engine.Engine, version string, 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 } @@ -698,7 +699,7 @@ func postContainersResize(eng *engine.Engine, version string, w http.ResponseWri return nil } -func postContainersAttach(eng *engine.Engine, version string, 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 } @@ -740,7 +741,7 @@ func postContainersAttach(eng *engine.Engine, version string, w http.ResponseWri 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") && utils.CompareVersion(version, "1.6") >= 0 { + 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 { @@ -763,7 +764,7 @@ func postContainersAttach(eng *engine.Engine, version string, w http.ResponseWri return nil } -func wsContainersAttach(eng *engine.Engine, version string, 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 } @@ -795,7 +796,7 @@ func wsContainersAttach(eng *engine.Engine, version string, w http.ResponseWrite return nil } -func getContainersByName(eng *engine.Engine, version string, 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") } @@ -805,7 +806,7 @@ func getContainersByName(eng *engine.Engine, version string, w http.ResponseWrit return job.Run() } -func getImagesByName(eng *engine.Engine, version string, 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") } @@ -815,8 +816,8 @@ func getImagesByName(eng *engine.Engine, version string, w http.ResponseWriter, return job.Run() } -func postBuild(eng *engine.Engine, version string, w http.ResponseWriter, r *http.Request, vars map[string]string) error { - if utils.CompareVersion(version, "1.3") < 0 { +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 ( @@ -831,7 +832,7 @@ func postBuild(eng *engine.Engine, version string, w http.ResponseWriter, r *htt // 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 utils.CompareVersion(version, "1.9") < 0 && 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 @@ -849,7 +850,7 @@ func postBuild(eng *engine.Engine, version string, w http.ResponseWriter, r *htt } } - if utils.CompareVersion(version, "1.8") >= 0 { + if version.GreaterThanOrEqualTo("1.8") { job.SetenvBool("json", true) streamJSON(job, w, true) } else { @@ -868,13 +869,13 @@ func postBuild(eng *engine.Engine, version string, w http.ResponseWriter, r *htt if !job.Stdout.Used() { return err } - sf := utils.NewStreamFormatter(utils.CompareVersion(version, "1.8") >= 0) + sf := utils.NewStreamFormatter(version.GreaterThanOrEqualTo("1.8")) w.Write(sf.FormatError(err)) } return nil } -func postContainersCopy(eng *engine.Engine, version string, 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 +908,7 @@ func postContainersCopy(eng *engine.Engine, version string, w http.ResponseWrite return nil } -func optionsHandler(eng *engine.Engine, version string, 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 } @@ -917,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) @@ -928,11 +929,11 @@ 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 := mux.Vars(r)["version"] + version := version.Version(mux.Vars(r)["version"]) if version == "" { version = APIVERSION } @@ -940,7 +941,7 @@ func makeHttpHandler(eng *engine.Engine, logging bool, localMethod string, local writeCorsHeaders(w, r) } - if utils.CompareVersion(version, APIVERSION) == 1 { + 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 } @@ -1039,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 == "" { @@ -1057,7 +1058,7 @@ 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 string, 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 diff --git a/integration/server_test.go b/integration/server_test.go index 2fe16551dc..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" 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/utils/utils.go b/utils/utils.go index 88f43c8bd5..1aba80ff41 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -972,27 +972,3 @@ func NewReadCloserWrapper(r io.Reader, closer func() error) io.ReadCloser { closer: closer, } } - -func CompareVersion(a, b string) int { - var ( - aa = strings.Split(a, ".") - bb = strings.Split(b, ".") - ) - for i, s := range aa { - var ai, bi int - ai, _ = strconv.Atoi(s) - if len(bb) > i { - bi, _ = strconv.Atoi(bb[i]) - } - if ai > bi { - return 1 - } - if bi > ai { - return -1 - } - } - if len(bb) > len(aa) { - return -1 - } - return 0 -} diff --git a/utils/utils_test.go b/utils/utils_test.go index 41dba72aa7..7e63a45cf7 100644 --- a/utils/utils_test.go +++ b/utils/utils_test.go @@ -479,23 +479,3 @@ func StrSlicesEqual(a, b []string) bool { return true } - -func asserVersion(t *testing.T, a, b string, result int) { - if r := CompareVersion(a, b); r != result { - t.Fatalf("Unexpected version comparison result. Found %d, expected %d", r, result) - } -} - -func TestCompareVersion(t *testing.T) { - asserVersion(t, "1.12", "1.12", 0) - asserVersion(t, "1.05.00.0156", "1.0.221.9289", 1) - asserVersion(t, "1", "1.0.1", -1) - asserVersion(t, "1.0.1", "1", 1) - asserVersion(t, "1.0.1", "1.0.2", -1) - asserVersion(t, "1.0.2", "1.0.3", -1) - asserVersion(t, "1.0.3", "1.1", -1) - asserVersion(t, "1.1", "1.1.1", -1) - asserVersion(t, "1.1.1", "1.1.2", -1) - asserVersion(t, "1.1.2", "1.2", -1) - -} From 386e7c80c0dad778f873bcd6eadfa94ecb1ac840 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Tue, 25 Feb 2014 22:49:19 +0000 Subject: [PATCH 20/25] fix content-type docker copy Docker-DCO-1.1-Signed-off-by: Victor Vieux (github: vieux) --- api/server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/server.go b/api/server.go index 0dd9d966e6..68e4114f13 100644 --- a/api/server.go +++ b/api/server.go @@ -907,7 +907,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") { From 2f468a498ab9e8ebc5c7a9316e0242f8d7c54f1a Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Tue, 25 Feb 2014 23:13:43 +0000 Subject: [PATCH 21/25] update api Maintainers file Docker-DCO-1.1-Signed-off-by: Victor Vieux (github: vieux) --- MAINTAINERS | 3 +-- api/MAINTAINERS | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 api/MAINTAINERS 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/api/MAINTAINERS b/api/MAINTAINERS new file mode 100644 index 0000000000..e0f18f14f1 --- /dev/null +++ b/api/MAINTAINERS @@ -0,0 +1 @@ +Victor Vieux (@vieux) From 3806ff61d1580ca22b3b72257789285aa9bd9486 Mon Sep 17 00:00:00 2001 From: "Guillaume J. Charmes" Date: Tue, 25 Feb 2014 16:50:33 -0800 Subject: [PATCH 22/25] Fix exit code issue with TTY mode Docker-DCO-1.1-Signed-off-by: Guillaume J. Charmes (github: creack) --- container.go | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/container.go b/container.go index 3a277fb1f9..7e15761c37 100644 --- a/container.go +++ b/container.go @@ -789,22 +789,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. @@ -812,7 +798,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 } From 4039fbb1b01365273e99a3cba28db33b38197237 Mon Sep 17 00:00:00 2001 From: "Guillaume J. Charmes" Date: Tue, 25 Feb 2014 20:16:17 -0800 Subject: [PATCH 23/25] Fix TestCmdKill by closing the pipe before waiting on the container Docker-DCO-1.1-Signed-off-by: Guillaume J. Charmes (github: creack) --- integration/commands_test.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/integration/commands_test.go b/integration/commands_test.go index a3359ec631..33117d8794 100644 --- a/integration/commands_test.go +++ b/integration/commands_test.go @@ -1043,11 +1043,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 +1087,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") From d29ee876ac7900730e1f48f4f30ed33210872238 Mon Sep 17 00:00:00 2001 From: Tianon Gravi Date: Tue, 25 Feb 2014 23:35:59 -0700 Subject: [PATCH 24/25] Update mkimage-yum to not read /etc/redhat-release from the host Docker-DCO-1.1-Signed-off-by: Andrew Page (github: tianon) --- contrib/mkimage-yum.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 0b4aeb79d9e920f5a713a8ae57e3fdde011689e6 Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Sun, 23 Feb 2014 11:32:12 -0500 Subject: [PATCH 25/25] make docker build -rm=true default #4292 Docker-DCO-1.1-Signed-off-by: Brian Goff (github: cpuguy83) --- Makefile | 4 ++-- api/client.go | 2 +- docs/sources/reference/commandline/cli.rst | 13 +++++-------- 3 files changed, 8 insertions(+), 11 deletions(-) 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/client.go b/api/client.go index eb345ae40b..ae9ae56a34 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 } diff --git a/docs/sources/reference/commandline/cli.rst b/docs/sources/reference/commandline/cli.rst index 67794a41c5..f17e716e84 100644 --- a/docs/sources/reference/commandline/cli.rst +++ b/docs/sources/reference/commandline/cli.rst @@ -184,11 +184,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 @@ -197,8 +197,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`. @@ -208,7 +206,7 @@ Examples: .. code-block:: bash - $ sudo docker build --rm . + $ sudo docker build . Uploading context 10240 bytes Step 1 : FROM busybox Pulling repository busybox @@ -248,9 +246,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