From 3aae63f4528ab1d7e1e12cc4e2e7bacd97991d43 Mon Sep 17 00:00:00 2001 From: "Daniel, Dao Quang Minh" Date: Tue, 11 Nov 2014 14:37:47 -0500 Subject: [PATCH 001/513] speed up creation of args and msg for huge cmds Whenever a command arguments is formed by a large linked list, repeatedly appending to arguments and displayed messages took a long time because go will have to allocate/copy a lot of times. This speeds up the allocation by preallocate arrays of correct size for args and msg Docker-DCO-1.1-Signed-off-by: Daniel, Dao Quang Minh (github: dqminh) --- builder/evaluator.go | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/builder/evaluator.go b/builder/evaluator.go index 645038bb1..3f2600e29 100644 --- a/builder/evaluator.go +++ b/builder/evaluator.go @@ -211,6 +211,21 @@ func (b *Builder) dispatch(stepN int, ast *parser.Node) error { msg += " " + ast.Value } + // count the number of nodes that we are going to traverse first + // so we can pre-create the argument and message array. This speeds up the + // allocation of those list a lot when they have a lot of arguments + cursor := ast + var n int + for cursor.Next != nil { + cursor = cursor.Next + n++ + } + l := len(strs) + strList := make([]string, n+l) + copy(strList, strs) + msgList := make([]string, n) + + var i int for ast.Next != nil { ast = ast.Next var str string @@ -218,16 +233,18 @@ func (b *Builder) dispatch(stepN int, ast *parser.Node) error { if _, ok := replaceEnvAllowed[cmd]; ok { str = b.replaceEnv(ast.Value) } - strs = append(strs, str) - msg += " " + ast.Value + strList[i+l] = str + msgList[i] = ast.Value + i++ } + msg += " " + strings.Join(msgList, " ") fmt.Fprintln(b.OutStream, msg) // XXX yes, we skip any cmds that are not valid; the parser should have // picked these out already. if f, ok := evaluateTable[cmd]; ok { - return f(b, strs, attrs, original) + return f(b, strList, attrs, original) } fmt.Fprintf(b.ErrStream, "# Skipping unknown instruction %s\n", strings.ToUpper(cmd)) From a2a50aa35ab58f52b68a65acfe668c26b765608f Mon Sep 17 00:00:00 2001 From: "Daniel, Dao Quang Minh" Date: Tue, 11 Nov 2014 15:15:00 -0500 Subject: [PATCH 002/513] use cached images instead of fetching the same image again Docker-DCO-1.1-Signed-off-by: Daniel, Dao Quang Minh (github: dqminh) --- daemon/daemon.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/daemon/daemon.go b/daemon/daemon.go index b0feae917..39e816c52 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -1090,9 +1090,9 @@ func (daemon *Daemon) ImageGetCached(imgID string, config *runconfig.Config) (*i // Loop on the children of the given image and check the config var match *image.Image for elem := range imageMap[imgID] { - img, err := daemon.Graph().Get(elem) - if err != nil { - return nil, err + img, ok := images[elem] + if !ok { + return nil, fmt.Errorf("unable to find image %q", elem) } if runconfig.Compare(&img.ContainerConfig, config) { if match == nil || match.Created.Before(img.Created) { From 1e7ba09b60aa0bc527dc7e0159071a6d47796de4 Mon Sep 17 00:00:00 2001 From: "Daniel, Dao Quang Minh" Date: Wed, 12 Nov 2014 09:13:47 -0500 Subject: [PATCH 003/513] add a test case for EXPOSE ports order changing order of EXPOSE ports should not invalidate the cache as the content doesnt change Docker-DCO-1.1-Signed-off-by: Daniel, Dao Quang Minh (github: dqminh) --- integration-cli/docker_cli_build_test.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/integration-cli/docker_cli_build_test.go b/integration-cli/docker_cli_build_test.go index de60a8017..1f23c8516 100644 --- a/integration-cli/docker_cli_build_test.go +++ b/integration-cli/docker_cli_build_test.go @@ -1732,6 +1732,29 @@ func TestBuildExpose(t *testing.T) { logDone("build - expose") } +func TestBuildExposeOrder(t *testing.T) { + buildID := func(name, exposed string) string { + _, err := buildImage(name, fmt.Sprintf(`FROM scratch + EXPOSE %s`, exposed), true) + if err != nil { + t.Fatal(err) + } + id, err := inspectField(name, "Id") + if err != nil { + t.Fatal(err) + } + return id + } + + id1 := buildID("testbuildexpose1", "80 2375") + id2 := buildID("testbuildexpose2", "2375 80") + defer deleteImages("testbuildexpose1", "testbuildexpose2") + if id1 != id2 { + t.Errorf("EXPOSE should invalidate the cache only when ports actually changed") + } + logDone("build - expose order") +} + func TestBuildEmptyEntrypointInheritance(t *testing.T) { name := "testbuildentrypointinheritance" name2 := "testbuildentrypointinheritance2" From 87d0562c61b80aba05f4c3b4f49f5527afb55989 Mon Sep 17 00:00:00 2001 From: "Daniel, Dao Quang Minh" Date: Wed, 12 Nov 2014 03:22:08 -0500 Subject: [PATCH 004/513] expose sorts its ports before saving as comment Saving ports as `map[nat.Port]struct{}` directly has ordering issue which is more replicatable where we expose a huge number of ports at the same time. As a result, the cache will be burst whenever the map order is different from the previous build. This sorts the ports first and save them as a whitespace-separated list instead of the map representation, so the order will always be consistent if the port list isnt changed. NOTICE: this will burst the old expose caches Docker-DCO-1.1-Signed-off-by: Daniel, Dao Quang Minh (github: dqminh) --- builder/dispatchers.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/builder/dispatchers.go b/builder/dispatchers.go index f2fdd3595..b138fe303 100644 --- a/builder/dispatchers.go +++ b/builder/dispatchers.go @@ -12,6 +12,7 @@ import ( "io/ioutil" "path/filepath" "regexp" + "sort" "strings" log "github.com/Sirupsen/logrus" @@ -302,14 +303,21 @@ func expose(b *Builder, args []string, attributes map[string]bool, original stri return err } + // instead of using ports directly, we build a list of ports and sort it so + // the order is consistent. This prevents cache burst where map ordering + // changes between builds + portList := make([]string, len(ports)) + var i int for port := range ports { if _, exists := b.Config.ExposedPorts[port]; !exists { b.Config.ExposedPorts[port] = struct{}{} } + portList[i] = string(port) + i++ } + sort.Strings(portList) b.Config.PortSpecs = nil - - return b.commit("", b.Config.Cmd, fmt.Sprintf("EXPOSE %v", ports)) + return b.commit("", b.Config.Cmd, fmt.Sprintf("EXPOSE %s", strings.Join(portList, " "))) } // USER foo From 29be7b439ec4d0c8a54852ccbbe7b6bcf040e13f Mon Sep 17 00:00:00 2001 From: "Daniel, Dao Quang Minh" Date: Wed, 12 Nov 2014 22:14:15 -0500 Subject: [PATCH 005/513] add test for exposing large number of ports this test checks if exposing a large number of ports in Dockerfile properly saves the port in configs. We dont actually expose a VERY large number of ports here because the result is the same and it increases the test time by a few seconds Docker-DCO-1.1-Signed-off-by: Daniel, Dao Quang Minh (github: dqminh) --- integration-cli/docker_cli_build_test.go | 59 ++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/integration-cli/docker_cli_build_test.go b/integration-cli/docker_cli_build_test.go index 1f23c8516..033169194 100644 --- a/integration-cli/docker_cli_build_test.go +++ b/integration-cli/docker_cli_build_test.go @@ -2,6 +2,7 @@ package main import ( "archive/tar" + "bytes" "encoding/json" "fmt" "io/ioutil" @@ -10,9 +11,11 @@ import ( "path" "path/filepath" "regexp" + "strconv" "strings" "syscall" "testing" + "text/template" "time" "github.com/docker/docker/pkg/archive" @@ -1732,6 +1735,62 @@ func TestBuildExpose(t *testing.T) { logDone("build - expose") } +func TestBuildExposeMorePorts(t *testing.T) { + // start building docker file with a large number of ports + portList := make([]string, 50) + line := make([]string, 100) + expectedPorts := make([]int, len(portList)*len(line)) + for i := 0; i < len(portList); i++ { + for j := 0; j < len(line); j++ { + p := i*len(line) + j + 1 + line[j] = strconv.Itoa(p) + expectedPorts[p-1] = p + } + if i == len(portList)-1 { + portList[i] = strings.Join(line, " ") + } else { + portList[i] = strings.Join(line, " ") + ` \` + } + } + + dockerfile := `FROM scratch + EXPOSE {{range .}} {{.}} + {{end}}` + tmpl := template.Must(template.New("dockerfile").Parse(dockerfile)) + buf := bytes.NewBuffer(nil) + tmpl.Execute(buf, portList) + + name := "testbuildexpose" + defer deleteImages(name) + _, err := buildImage(name, buf.String(), true) + if err != nil { + t.Fatal(err) + } + + // check if all the ports are saved inside Config.ExposedPorts + res, err := inspectFieldJSON(name, "Config.ExposedPorts") + if err != nil { + t.Fatal(err) + } + var exposedPorts map[string]interface{} + if err := json.Unmarshal([]byte(res), &exposedPorts); err != nil { + t.Fatal(err) + } + + for _, p := range expectedPorts { + ep := fmt.Sprintf("%d/tcp", p) + if _, ok := exposedPorts[ep]; !ok { + t.Errorf("Port(%s) is not exposed", ep) + } else { + delete(exposedPorts, ep) + } + } + if len(exposedPorts) != 0 { + t.Errorf("Unexpected extra exposed ports %v", exposedPorts) + } + logDone("build - expose large number of ports") +} + func TestBuildExposeOrder(t *testing.T) { buildID := func(name, exposed string) string { _, err := buildImage(name, fmt.Sprintf(`FROM scratch From 6c11d07759bcb6e697d2aea37ce7e88b5e0c0260 Mon Sep 17 00:00:00 2001 From: Vincent Batts Date: Wed, 29 Oct 2014 11:52:11 -0400 Subject: [PATCH 006/513] CONTRIBUTING: provide a template for new issues _maybe_ we could even automate this to populate and complain in some situations. I see https://github.com/isaacs/github/issues/99 that is old and needs some love from @github ... Signed-off-by: Vincent Batts --- CONTRIBUTING.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 29a3ce140..d2943b8c1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -64,6 +64,45 @@ Please also include the steps required to reproduce the problem if possible and applicable. This information will help us review and fix your issue faster. +### Template + +``` +Description of problem: + + +`docker version`: + + +`docker info`: + + +`uname -a`: + + +Environment details (AWS, VirtualBox, physical, etc.): + + +How reproducible: + + +Steps to Reproduce: +1. +2. +3. + + +Actual Results: + + +Expected Results: + + +Additional info: + + + +``` + ## Build Environment For instructions on setting up your development environment, please From 967a42f116d23051c862a3b4983925de2016f83c Mon Sep 17 00:00:00 2001 From: Tatsushi Inagaki Date: Wed, 19 Nov 2014 15:06:49 +0900 Subject: [PATCH 007/513] Fix to avoid a compile error due to float to int truncation with GCCGO Signed-off-by: Tatsushi Inagaki --- pkg/units/size_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/units/size_test.go b/pkg/units/size_test.go index 5b329fcf6..3e410b0db 100644 --- a/pkg/units/size_test.go +++ b/pkg/units/size_test.go @@ -23,9 +23,9 @@ func TestHumanSize(t *testing.T) { assertEquals(t, "1 MB", HumanSize(1000000)) assertEquals(t, "1.049 MB", HumanSize(1048576)) assertEquals(t, "2 MB", HumanSize(2*MB)) - assertEquals(t, "3.42 GB", HumanSize(3.42*GB)) - assertEquals(t, "5.372 TB", HumanSize(5.372*TB)) - assertEquals(t, "2.22 PB", HumanSize(2.22*PB)) + assertEquals(t, "3.42 GB", HumanSize(int64(float64(3.42*GB)))) + assertEquals(t, "5.372 TB", HumanSize(int64(float64(5.372*TB)))) + assertEquals(t, "2.22 PB", HumanSize(int64(float64(2.22*PB)))) } func TestFromHumanSize(t *testing.T) { From ea5a2c1fd74af1a806c8f4ef7858afe6435d90fa Mon Sep 17 00:00:00 2001 From: Vincent Batts Date: Thu, 13 Nov 2014 11:25:17 -0500 Subject: [PATCH 008/513] issue report script Add a script that will facilitate standard issue reports. Signed-off-by: Vincent Batts --- project/report-issue.sh | 105 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 project/report-issue.sh diff --git a/project/report-issue.sh b/project/report-issue.sh new file mode 100644 index 000000000..5ef2ecee2 --- /dev/null +++ b/project/report-issue.sh @@ -0,0 +1,105 @@ +#!/bin/sh + +# This is a convenience script for reporting issues that include a base +# template of information. See https://github.com/docker/docker/pull/8845 + +set -e + +DOCKER_ISSUE_URL=${DOCKER_ISSUE_URL:-"https://github.com/docker/docker/issues/new"} +DOCKER_ISSUE_NAME_PREFIX=${DOCKER_ISSUE_NAME_PREFIX:-"Report: "} +DOCKER=${DOCKER:-"docker"} +DOCKER_COMMAND="${DOCKER}" +export DOCKER_COMMAND + +# pulled from https://gist.github.com/cdown/1163649 +function urlencode() { + # urlencode + + local length="${#1}" + for (( i = 0; i < length; i++ )); do + local c="${1:i:1}" + case $c in + [a-zA-Z0-9.~_-]) printf "$c" ;; + *) printf '%%%02X' "'$c" + esac + done +} + +function template() { +# this should always match the template from CONTRIBUTING.md + cat <<- EOM + Description of problem: + + + \`docker version\`: + `${DOCKER_COMMAND} -D version` + + + \`docker info\`: + `${DOCKER_COMMAND} -D info` + + + \`uname -a\`: + `uname -a` + + + Environment details (AWS, VirtualBox, physical, etc.): + + + How reproducible: + + + Steps to Reproduce: + 1. + 2. + 3. + + + Actual Results: + + + Expected Results: + + + Additional info: + + + EOM +} + +function format_issue_url() { + if [ ${#@} -ne 2 ] ; then + return 1 + fi + local issue_name=$(urlencode "${DOCKER_ISSUE_NAME_PREFIX}${1}") + local issue_body=$(urlencode "${2}") + echo "${DOCKER_ISSUE_URL}?title=${issue_name}&body=${issue_body}" +} + + +echo -ne "Do you use \`sudo\` to call docker? [y|N]: " +read -r -n 1 use_sudo +echo "" + +if [ "x${use_sudo}" = "xy" -o "x${use_sudo}" = "xY" ]; then + export DOCKER_COMMAND="sudo ${DOCKER}" +fi + +echo -ne "Title of new issue?: " +read -r issue_title +echo "" + +issue_url=$(format_issue_url "${issue_title}" "$(template)") + +if which xdg-open 2>/dev/null >/dev/null ; then + echo -ne "Would like to launch this report in your browser? [Y|n]: " + read -r -n 1 launch_now + echo "" + + if [ "${launch_now}" != "n" -a "${launch_now}" != "N" ]; then + xdg-open "${issue_url}" + fi +fi + +echo "If you would like to manually open the url, you can open this link if your browser: ${issue_url}" + From 7c225333f22378e380309bd0c3afc1b3311b1373 Mon Sep 17 00:00:00 2001 From: Arnaud Porterie Date: Fri, 14 Nov 2014 20:38:02 -0800 Subject: [PATCH 009/513] Allocate daemon listening ports Mark the daemon listening ports as allocated in the portallocator in order to prevent containers from exposing this port themselves. Signed-off-by: Arnaud Porterie --- api/server/server.go | 31 +++++++++++++++++++++++ integration-cli/docker_cli_daemon_test.go | 31 +++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/api/server/server.go b/api/server/server.go index 97349959a..ddc9958b3 100644 --- a/api/server/server.go +++ b/api/server/server.go @@ -27,6 +27,7 @@ import ( log "github.com/Sirupsen/logrus" "github.com/docker/docker/api" + "github.com/docker/docker/daemon/networkdriver/portallocator" "github.com/docker/docker/engine" "github.com/docker/docker/pkg/listenbuffer" "github.com/docker/docker/pkg/parsers" @@ -1493,6 +1494,32 @@ func setupUnixHttp(addr string, job *engine.Job) (*HttpServer, error) { return &HttpServer{&http.Server{Addr: addr, Handler: r}, l}, nil } +func allocateDaemonPort(addr string) error { + host, port, err := net.SplitHostPort(addr) + if err != nil { + return err + } + + intPort, err := strconv.Atoi(port) + if err != nil { + return err + } + + var hostIPs []net.IP + if parsedIP := net.ParseIP(host); parsedIP != nil { + hostIPs = append(hostIPs, parsedIP) + } else if hostIPs, err = net.LookupIP(host); err != nil { + return fmt.Errorf("failed to lookup %s address in host specification", host) + } + + for _, hostIP := range hostIPs { + if _, err := portallocator.RequestPort(hostIP, "tcp", intPort); err != nil { + return fmt.Errorf("failed to allocate daemon listening port %d (err: %v)", intPort, err) + } + } + return nil +} + func setupTcpHttp(addr string, job *engine.Job) (*HttpServer, error) { if !strings.HasPrefix(addr, "127.0.0.1") && !job.GetenvBool("TlsVerify") { log.Infof("/!\\ DON'T BIND ON ANOTHER IP ADDRESS THAN 127.0.0.1 IF YOU DON'T KNOW WHAT YOU'RE DOING /!\\") @@ -1508,6 +1535,10 @@ func setupTcpHttp(addr string, job *engine.Job) (*HttpServer, error) { return nil, err } + if err := allocateDaemonPort(addr); err != nil { + return nil, err + } + if job.GetenvBool("Tls") || job.GetenvBool("TlsVerify") { var tlsCa string if job.GetenvBool("TlsVerify") { diff --git a/integration-cli/docker_cli_daemon_test.go b/integration-cli/docker_cli_daemon_test.go index 31bfac3f6..13b0a22a9 100644 --- a/integration-cli/docker_cli_daemon_test.go +++ b/integration-cli/docker_cli_daemon_test.go @@ -2,6 +2,7 @@ package main import ( "encoding/json" + "fmt" "io/ioutil" "os" "os/exec" @@ -284,3 +285,33 @@ func TestDaemonLoggingLevel(t *testing.T) { logDone("daemon - Logging Level") } + +func TestDaemonAllocatesListeningPort(t *testing.T) { + listeningPorts := [][]string{ + {"0.0.0.0", "0.0.0.0", "5678"}, + {"127.0.0.1", "127.0.0.1", "1234"}, + {"localhost", "127.0.0.1", "1235"}, + } + + cmdArgs := []string{} + for _, hostDirective := range listeningPorts { + cmdArgs = append(cmdArgs, "--host", fmt.Sprintf("tcp://%s:%s", hostDirective[0], hostDirective[2])) + } + + d := NewDaemon(t) + if err := d.StartWithBusybox(cmdArgs...); err != nil { + t.Fatalf("Could not start daemon with busybox: %v", err) + } + defer d.Stop() + + for _, hostDirective := range listeningPorts { + output, err := d.Cmd("run", "-p", fmt.Sprintf("%s:%s:80", hostDirective[1], hostDirective[2]), "busybox", "true") + if err == nil { + t.Fatalf("Container should not start, expected port already allocated error: %q", output) + } else if !strings.Contains(output, "port is already allocated") { + t.Fatalf("Expected port is already allocated error: %q", output) + } + } + + logDone("daemon - daemon listening port is allocated") +} From 36560a76d71f1222122c9e6c82f76a609da564e9 Mon Sep 17 00:00:00 2001 From: Tatsushi Inagaki Date: Tue, 25 Nov 2014 16:48:09 +0900 Subject: [PATCH 010/513] Revert "Fix to avoid a compile error due to float to int truncation with GCCGO" This reverts commit 967a42f116d23051c862a3b4983925de2016f83c. Signed-off-by: Tatsushi Inagaki Roll back the change to fix the parameter of HumanSize from int64 to float64 --- pkg/units/size_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/units/size_test.go b/pkg/units/size_test.go index 3e410b0db..5b329fcf6 100644 --- a/pkg/units/size_test.go +++ b/pkg/units/size_test.go @@ -23,9 +23,9 @@ func TestHumanSize(t *testing.T) { assertEquals(t, "1 MB", HumanSize(1000000)) assertEquals(t, "1.049 MB", HumanSize(1048576)) assertEquals(t, "2 MB", HumanSize(2*MB)) - assertEquals(t, "3.42 GB", HumanSize(int64(float64(3.42*GB)))) - assertEquals(t, "5.372 TB", HumanSize(int64(float64(5.372*TB)))) - assertEquals(t, "2.22 PB", HumanSize(int64(float64(2.22*PB)))) + assertEquals(t, "3.42 GB", HumanSize(3.42*GB)) + assertEquals(t, "5.372 TB", HumanSize(5.372*TB)) + assertEquals(t, "2.22 PB", HumanSize(2.22*PB)) } func TestFromHumanSize(t *testing.T) { From 82a5cd0d3701fc559fb92290ed6bc2974a9d8b6d Mon Sep 17 00:00:00 2001 From: Tatsushi Inagaki Date: Tue, 25 Nov 2014 17:02:47 +0900 Subject: [PATCH 011/513] Fix to avoid a compilation error of size_test.go with GCCGO due to float to int truncation Signed-off-by: Tatsushi Inagaki --- api/client/commands.go | 10 +++++----- daemon/graphdriver/devmapper/driver.go | 10 +++++----- pkg/units/size.go | 2 +- utils/jsonmessage.go | 4 ++-- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/api/client/commands.go b/api/client/commands.go index 63b52ada6..43c5baa6f 100644 --- a/api/client/commands.go +++ b/api/client/commands.go @@ -1052,7 +1052,7 @@ func (cli *DockerCli) CmdHistory(args ...string) error { } else { fmt.Fprintf(w, "%s\t", utils.Trunc(out.Get("CreatedBy"), 45)) } - fmt.Fprintf(w, "%s\n", units.HumanSize(out.GetInt64("Size"))) + fmt.Fprintf(w, "%s\n", units.HumanSize(float64(out.GetInt64("Size")))) } else { if *noTrunc { fmt.Fprintln(w, outID) @@ -1451,7 +1451,7 @@ func (cli *DockerCli) CmdImages(args ...string) error { } if !*quiet { - fmt.Fprintf(w, "%s\t%s\t%s\t%s ago\t%s\n", repo, tag, outID, units.HumanDuration(time.Now().UTC().Sub(time.Unix(out.GetInt64("Created"), 0))), units.HumanSize(out.GetInt64("VirtualSize"))) + fmt.Fprintf(w, "%s\t%s\t%s\t%s ago\t%s\n", repo, tag, outID, units.HumanDuration(time.Now().UTC().Sub(time.Unix(out.GetInt64("Created"), 0))), units.HumanSize(float64(out.GetInt64("VirtualSize")))) } else { fmt.Fprintln(w, outID) } @@ -1525,7 +1525,7 @@ func (cli *DockerCli) printTreeNode(noTrunc bool, image *engine.Env, prefix stri imageID = utils.TruncateID(image.Get("Id")) } - fmt.Fprintf(cli.out, "%s%s Virtual Size: %s", prefix, imageID, units.HumanSize(image.GetInt64("VirtualSize"))) + fmt.Fprintf(cli.out, "%s%s Virtual Size: %s", prefix, imageID, units.HumanSize(float64(image.GetInt64("VirtualSize")))) if image.GetList("RepoTags")[0] != ":" { fmt.Fprintf(cli.out, " Tags: %s\n", strings.Join(image.GetList("RepoTags"), ", ")) } else { @@ -1668,9 +1668,9 @@ func (cli *DockerCli) CmdPs(args ...string) error { if *size { if out.GetInt("SizeRootFs") > 0 { - fmt.Fprintf(w, "%s (virtual %s)\n", units.HumanSize(out.GetInt64("SizeRw")), units.HumanSize(out.GetInt64("SizeRootFs"))) + fmt.Fprintf(w, "%s (virtual %s)\n", units.HumanSize(float64(out.GetInt64("SizeRw"))), units.HumanSize(float64(out.GetInt64("SizeRootFs")))) } else { - fmt.Fprintf(w, "%s\n", units.HumanSize(out.GetInt64("SizeRw"))) + fmt.Fprintf(w, "%s\n", units.HumanSize(float64(out.GetInt64("SizeRw")))) } continue diff --git a/daemon/graphdriver/devmapper/driver.go b/daemon/graphdriver/devmapper/driver.go index b20f3e545..91e9491e3 100644 --- a/daemon/graphdriver/devmapper/driver.go +++ b/daemon/graphdriver/devmapper/driver.go @@ -56,13 +56,13 @@ func (d *Driver) Status() [][2]string { status := [][2]string{ {"Pool Name", s.PoolName}, - {"Pool Blocksize", fmt.Sprintf("%s", units.HumanSize(int64(s.SectorSize)))}, + {"Pool Blocksize", fmt.Sprintf("%s", units.HumanSize(float64(s.SectorSize)))}, {"Data file", s.DataLoopback}, {"Metadata file", s.MetadataLoopback}, - {"Data Space Used", fmt.Sprintf("%s", units.HumanSize(int64(s.Data.Used)))}, - {"Data Space Total", fmt.Sprintf("%s", units.HumanSize(int64(s.Data.Total)))}, - {"Metadata Space Used", fmt.Sprintf("%s", units.HumanSize(int64(s.Metadata.Used)))}, - {"Metadata Space Total", fmt.Sprintf("%s", units.HumanSize(int64(s.Metadata.Total)))}, + {"Data Space Used", fmt.Sprintf("%s", units.HumanSize(float64(s.Data.Used)))}, + {"Data Space Total", fmt.Sprintf("%s", units.HumanSize(float64(s.Data.Total)))}, + {"Metadata Space Used", fmt.Sprintf("%s", units.HumanSize(float64(s.Metadata.Used)))}, + {"Metadata Space Total", fmt.Sprintf("%s", units.HumanSize(float64(s.Metadata.Total)))}, } if vStr, err := devicemapper.GetLibraryVersion(); err == nil { status = append(status, [2]string{"Library Version", vStr}) diff --git a/pkg/units/size.go b/pkg/units/size.go index 264f38822..7cfb57ba5 100644 --- a/pkg/units/size.go +++ b/pkg/units/size.go @@ -39,7 +39,7 @@ var binaryAbbrs = []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", // HumanSize returns a human-readable approximation of a size // using SI standard (eg. "44kB", "17MB") -func HumanSize(size int64) string { +func HumanSize(size float64) string { return intToString(float64(size), 1000.0, decimapAbbrs) } diff --git a/utils/jsonmessage.go b/utils/jsonmessage.go index bdc47f0e1..a2bbbcf4d 100644 --- a/utils/jsonmessage.go +++ b/utils/jsonmessage.go @@ -44,11 +44,11 @@ func (p *JSONProgress) String() string { if p.Current <= 0 && p.Total <= 0 { return "" } - current := units.HumanSize(int64(p.Current)) + current := units.HumanSize(float64(p.Current)) if p.Total <= 0 { return fmt.Sprintf("%8v", current) } - total := units.HumanSize(int64(p.Total)) + total := units.HumanSize(float64(p.Total)) percentage := int(float64(p.Current)/float64(p.Total)*100) / 2 if width > 110 { // this number can't be negetive gh#7136 From 4f6cdb12ab64cf76f5a57cb1d24081882c61def3 Mon Sep 17 00:00:00 2001 From: Barnaby Gray Date: Sun, 7 Dec 2014 12:40:29 +0000 Subject: [PATCH 012/513] Update fish shell completions. Fixes #9550 Signed-off-by: Barnaby Gray --- contrib/completion/fish/docker.fish | 205 +++++++++++++++++----------- 1 file changed, 128 insertions(+), 77 deletions(-) diff --git a/contrib/completion/fish/docker.fish b/contrib/completion/fish/docker.fish index a082adc02..5eef6f30b 100644 --- a/contrib/completion/fish/docker.fish +++ b/contrib/completion/fish/docker.fish @@ -16,7 +16,7 @@ function __fish_docker_no_subcommand --description 'Test if docker has yet to be given the subcommand' for i in (commandline -opc) - if contains -- $i attach build commit cp create diff events export history images import info insert inspect kill load login logs port ps pull push restart rm rmi run save search start stop tag top version wait + if contains -- $i attach build commit cp create diff events exec export history images import info insert inspect kill load login logout logs pause port ps pull push restart rm rmi run save search start stop tag top unpause version wait return 1 end end @@ -43,31 +43,42 @@ function __fish_print_docker_repositories --description 'Print a list of docker end # common options -complete -c docker -f -n '__fish_docker_no_subcommand' -s D -l debug -d 'Enable debug mode' -complete -c docker -f -n '__fish_docker_no_subcommand' -s G -l group -d "Group to assign the unix socket specified by -H when running in daemon mode; use '' (the empty string) to disable setting of a group" -complete -c docker -f -n '__fish_docker_no_subcommand' -s H -l host -d 'tcp://host:port, unix://path/to/socket, fd://* or fd://socketfd to use in daemon mode. Multiple sockets can be specified' complete -c docker -f -n '__fish_docker_no_subcommand' -l api-enable-cors -d 'Enable CORS headers in the remote API' -complete -c docker -f -n '__fish_docker_no_subcommand' -s b -l bridge -d "Attach containers to a pre-existing network bridge; use 'none' to disable container networking" +complete -c docker -f -n '__fish_docker_no_subcommand' -s b -l bridge -d 'Attach containers to a pre-existing network bridge' complete -c docker -f -n '__fish_docker_no_subcommand' -l bip -d "Use this CIDR notation address for the network bridge's IP, not compatible with -b" +complete -c docker -f -n '__fish_docker_no_subcommand' -s D -l debug -d 'Enable debug mode' complete -c docker -f -n '__fish_docker_no_subcommand' -s d -l daemon -d 'Enable daemon mode' -complete -c docker -f -n '__fish_docker_no_subcommand' -l dns -d 'Force docker to use specific DNS servers' -complete -c docker -f -n '__fish_docker_no_subcommand' -s e -l exec-driver -d 'Force the docker runtime to use a specific exec driver' -complete -c docker -f -n '__fish_docker_no_subcommand' -s g -l graph -d 'Path to use as the root of the docker runtime' -complete -c docker -f -n '__fish_docker_no_subcommand' -l icc -d 'Allow unrestricted inter-container and Docker daemon host communication' +complete -c docker -f -n '__fish_docker_no_subcommand' -l dns -d 'Force Docker to use specific DNS servers' +complete -c docker -f -n '__fish_docker_no_subcommand' -l dns-search -d 'Force Docker to use specific DNS search domains' +complete -c docker -f -n '__fish_docker_no_subcommand' -s e -l exec-driver -d 'Force the Docker runtime to use a specific exec driver' +complete -c docker -f -n '__fish_docker_no_subcommand' -l fixed-cidr -d 'IPv4 subnet for fixed IPs (ex: 10.20.0.0/16)' +complete -c docker -f -n '__fish_docker_no_subcommand' -s G -l group -d 'Group to assign the unix socket specified by -H when running in daemon mode' +complete -c docker -f -n '__fish_docker_no_subcommand' -s g -l graph -d 'Path to use as the root of the Docker runtime' +complete -c docker -f -n '__fish_docker_no_subcommand' -s H -l host -d 'The socket(s) to bind to in daemon mode or connect to in client mode, specified using one or more tcp://host:port, unix:///path/to/socket, fd://* or fd://socketfd.' +complete -c docker -f -n '__fish_docker_no_subcommand' -l icc -d 'Enable inter-container communication' +complete -c docker -f -n '__fish_docker_no_subcommand' -l insecure-registry -d 'Enable insecure communication with specified registries (no certificate verification for HTTPS and enable HTTP fallback) (e.g., localhost:5000 or 10.20.0.0/16)' complete -c docker -f -n '__fish_docker_no_subcommand' -l ip -d 'Default IP address to use when binding container ports' -complete -c docker -f -n '__fish_docker_no_subcommand' -l ip-forward -d 'Disable enabling of net.ipv4.ip_forward' -complete -c docker -f -n '__fish_docker_no_subcommand' -l iptables -d "Disable docker's addition of iptables rules" -complete -c docker -f -n '__fish_docker_no_subcommand' -l mtu -d 'Set the containers network MTU; if no value is provided: default to the default route MTU or 1500 if no default route is available' +complete -c docker -f -n '__fish_docker_no_subcommand' -l ip-forward -d 'Enable net.ipv4.ip_forward' +complete -c docker -f -n '__fish_docker_no_subcommand' -l ip-masq -d "Enable IP masquerading for bridge's IP range" +complete -c docker -f -n '__fish_docker_no_subcommand' -l iptables -d "Enable Docker's addition of iptables rules" +complete -c docker -f -n '__fish_docker_no_subcommand' -l mtu -d 'Set the containers network MTU' complete -c docker -f -n '__fish_docker_no_subcommand' -s p -l pidfile -d 'Path to use for daemon PID file' -complete -c docker -f -n '__fish_docker_no_subcommand' -s r -l restart -d 'Restart previously running containers' -complete -c docker -f -n '__fish_docker_no_subcommand' -s s -l storage-driver -d 'Force the docker runtime to use a specific storage driver' +complete -c docker -f -n '__fish_docker_no_subcommand' -l registry-mirror -d 'Specify a preferred Docker registry mirror' +complete -c docker -f -n '__fish_docker_no_subcommand' -s s -l storage-driver -d 'Force the Docker runtime to use a specific storage driver' +complete -c docker -f -n '__fish_docker_no_subcommand' -l selinux-enabled -d 'Enable selinux support. SELinux does not presently support the BTRFS storage driver' +complete -c docker -f -n '__fish_docker_no_subcommand' -l storage-opt -d 'Set storage driver options' +complete -c docker -f -n '__fish_docker_no_subcommand' -l tls -d 'Use TLS; implied by tls-verify flags' +complete -c docker -f -n '__fish_docker_no_subcommand' -l tlscacert -d 'Trust only remotes providing a certificate signed by the CA given here' +complete -c docker -f -n '__fish_docker_no_subcommand' -l tlscert -d 'Path to TLS certificate file' +complete -c docker -f -n '__fish_docker_no_subcommand' -l tlskey -d 'Path to TLS key file' +complete -c docker -f -n '__fish_docker_no_subcommand' -l tlsverify -d 'Use TLS and verify the remote (daemon: verify client, client: verify daemon)' complete -c docker -f -n '__fish_docker_no_subcommand' -s v -l version -d 'Print version information and quit' # subcommands # attach complete -c docker -f -n '__fish_docker_no_subcommand' -a attach -d 'Attach to a running container' -complete -c docker -A -f -n '__fish_seen_subcommand_from attach' -l no-stdin -d 'Do not attach stdin' -complete -c docker -A -f -n '__fish_seen_subcommand_from attach' -l sig-proxy -d 'Proxify all received signal to the process (non-TTY mode only)' +complete -c docker -A -f -n '__fish_seen_subcommand_from attach' -l no-stdin -d 'Do not attach STDIN' +complete -c docker -A -f -n '__fish_seen_subcommand_from attach' -l sig-proxy -d 'Proxy all received signals to the process (even in non-TTY mode). SIGCHLD, SIGKILL, and SIGSTOP are not proxied.' complete -c docker -A -f -n '__fish_seen_subcommand_from attach' -a '(__fish_print_docker_containers running)' -d "Container" # build @@ -80,40 +91,48 @@ complete -c docker -A -f -n '__fish_seen_subcommand_from build' -s t -l tag -d ' # commit complete -c docker -f -n '__fish_docker_no_subcommand' -a commit -d "Create a new image from a container's changes" -complete -c docker -A -f -n '__fish_seen_subcommand_from commit' -s a -l author -d 'Author (e.g., "John Hannibal Smith "' +complete -c docker -A -f -n '__fish_seen_subcommand_from commit' -s a -l author -d 'Author (e.g., "John Hannibal Smith ")' complete -c docker -A -f -n '__fish_seen_subcommand_from commit' -s m -l message -d 'Commit message' -complete -c docker -A -f -n '__fish_seen_subcommand_from commit' -l run -d 'Config automatically applied when the image is run. (ex: -run=\'{"Cmd": ["cat", "/world"], "PortSpecs": ["22"]}\')' +complete -c docker -A -f -n '__fish_seen_subcommand_from commit' -s p -l pause -d 'Pause container during commit' complete -c docker -A -f -n '__fish_seen_subcommand_from commit' -a '(__fish_print_docker_containers all)' -d "Container" # cp complete -c docker -f -n '__fish_docker_no_subcommand' -a cp -d "Copy files/folders from a container's filesystem to the host path" # create -complete -c docker -f -n '__fish_docker_no_subcommand' -a run -d 'Run a command in a new container' -complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s P -l publish-all -d 'Publish all exposed ports to the host interfaces' -complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s a -l attach -d 'Attach to stdin, stdout or stderr.' -complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s c -l cpu-shares -d 'CPU shares (relative weight)' -complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l cidfile -d 'Write the container ID to the file' -complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l dns -d 'Set custom dns servers' -complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s e -l env -d 'Set environment variables' -complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l entrypoint -d 'Overwrite the default entrypoint of the image' -complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l expose -d 'Expose a port from the container without publishing it to your host' -complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s h -l hostname -d 'Container host name' -complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s i -l interactive -d 'Keep stdin open even if not attached' -complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l link -d 'Add link to another container (name:alias)' -complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l lxc-conf -d 'Add custom lxc options -lxc-conf="lxc.cgroup.cpuset.cpus = 0,1"' -complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s m -l memory -d 'Memory limit (format: , where unit = b, k, m or g)' -complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s n -l networking -d 'Enable networking for this container' -complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l name -d 'Assign a name to the container' -complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s p -l publish -d "Publish a container's port to the host (format: ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort) (use 'docker port' to see the actual mapping)" -complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l privileged -d 'Give extended privileges to this container' -complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s t -l tty -d 'Allocate a pseudo-tty' -complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s u -l user -d 'Username or UID' -complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s v -l volume -d 'Bind mount a volume (e.g. from the host: -v /host:/container, from docker: -v /container)' -complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l volumes-from -d 'Mount volumes from the specified container(s)' -complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s w -l workdir -d 'Working directory inside the container' -complete -c docker -A -f -n '__fish_seen_subcommand_from run' -a '(__fish_print_docker_images)' -d "Image" - +complete -c docker -f -n '__fish_docker_no_subcommand' -a create -d 'Create a new container' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -s a -l attach -d 'Attach to STDIN, STDOUT or STDERR.' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l add-host -d 'Add a custom host-to-IP mapping (host:ip)' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -s c -l cpu-shares -d 'CPU shares (relative weight)' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l cap-add -d 'Add Linux capabilities' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l cap-drop -d 'Drop Linux capabilities' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l cidfile -d 'Write the container ID to the file' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l cpuset -d 'CPUs in which to allow execution (0-3, 0,1)' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l device -d 'Add a host device to the container (e.g. --device=/dev/sdc:/dev/xvdc)' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l dns -d 'Set custom DNS servers' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l dns-search -d 'Set custom DNS search domains' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -s e -l env -d 'Set environment variables' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l entrypoint -d 'Overwrite the default ENTRYPOINT of the image' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l env-file -d 'Read in a line delimited file of environment variables' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l expose -d 'Expose a port from the container without publishing it to your host' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -s h -l hostname -d 'Container host name' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -s i -l interactive -d 'Keep STDIN open even if not attached' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l link -d 'Add link to another container in the form of name:alias' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l lxc-conf -d '(lxc exec-driver only) Add custom lxc options --lxc-conf="lxc.cgroup.cpuset.cpus = 0,1"' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -s m -l memory -d 'Memory limit (format: , where unit = b, k, m or g)' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l name -d 'Assign a name to the container' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l net -d 'Set the Network mode for the container' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -s P -l publish-all -d 'Publish all exposed ports to the host interfaces' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -s p -l publish -d "Publish a container's port to the host" +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l privileged -d 'Give extended privileges to this container' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l restart -d 'Restart policy to apply when a container exits (no, on-failure[:max-retry], always)' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l security-opt -d 'Security Options' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -s t -l tty -d 'Allocate a pseudo-TTY' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -s u -l user -d 'Username or UID' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -s v -l volume -d 'Bind mount a volume (e.g., from the host: -v /host:/container, from Docker: -v /container)' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l volumes-from -d 'Mount volumes from the specified container(s)' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -s w -l workdir -d 'Working directory inside the container' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -a '(__fish_print_docker_images)' -d "Image" # diff complete -c docker -f -n '__fish_docker_no_subcommand' -a diff -d "Inspect changes on a container's filesystem" @@ -121,7 +140,15 @@ complete -c docker -A -f -n '__fish_seen_subcommand_from diff' -a '(__fish_print # events complete -c docker -f -n '__fish_docker_no_subcommand' -a events -d 'Get real time events from the server' -complete -c docker -A -f -n '__fish_seen_subcommand_from events' -l since -d 'Show previously created events and then stream.' +complete -c docker -A -f -n '__fish_seen_subcommand_from events' -l since -d 'Show all events created since timestamp' +complete -c docker -A -f -n '__fish_seen_subcommand_from events' -l until -d 'Stream events until this timestamp' + +# exec +complete -c docker -f -n '__fish_docker_no_subcommand' -a exec -d 'Run a command in an existing container' +complete -c docker -A -f -n '__fish_seen_subcommand_from exec' -s d -l detach -d 'Detached mode: run command in the background' +complete -c docker -A -f -n '__fish_seen_subcommand_from exec' -s i -l interactive -d 'Keep STDIN open even if not attached' +complete -c docker -A -f -n '__fish_seen_subcommand_from exec' -s t -l tty -d 'Allocate a pseudo-TTY' +complete -c docker -A -f -n '__fish_seen_subcommand_from exec' -a '(__fish_print_docker_containers running)' -d "Container" # export complete -c docker -f -n '__fish_docker_no_subcommand' -a export -d 'Stream the contents of a container as a tar archive' @@ -136,10 +163,9 @@ complete -c docker -A -f -n '__fish_seen_subcommand_from history' -a '(__fish_pr # images complete -c docker -f -n '__fish_docker_no_subcommand' -a images -d 'List images' complete -c docker -A -f -n '__fish_seen_subcommand_from images' -s a -l all -d 'Show all images (by default filter out the intermediate image layers)' +complete -c docker -A -f -n '__fish_seen_subcommand_from images' -s f -l filter -d "Provide filter values (i.e. 'dangling=true')" complete -c docker -A -f -n '__fish_seen_subcommand_from images' -l no-trunc -d "Don't truncate output" complete -c docker -A -f -n '__fish_seen_subcommand_from images' -s q -l quiet -d 'Only show numeric IDs' -complete -c docker -A -f -n '__fish_seen_subcommand_from images' -s t -l tree -d 'Output graph in tree format' -complete -c docker -A -f -n '__fish_seen_subcommand_from images' -s v -l viz -d 'Output graph in graphviz format' complete -c docker -A -f -n '__fish_seen_subcommand_from images' -a '(__fish_print_docker_repositories)' -d "Repository" # import @@ -161,122 +187,147 @@ complete -c docker -A -f -n '__fish_seen_subcommand_from kill' -a '(__fish_print # load complete -c docker -f -n '__fish_docker_no_subcommand' -a load -d 'Load an image from a tar archive' +complete -c docker -A -f -n '__fish_seen_subcommand_from load' -s i -l input -d 'Read from a tar archive file, instead of STDIN' # login -complete -c docker -f -n '__fish_docker_no_subcommand' -a login -d 'Register or Login to the docker registry server' +complete -c docker -f -n '__fish_docker_no_subcommand' -a login -d 'Register or log in to a Docker registry server' complete -c docker -A -f -n '__fish_seen_subcommand_from login' -s e -l email -d 'Email' complete -c docker -A -f -n '__fish_seen_subcommand_from login' -s p -l password -d 'Password' complete -c docker -A -f -n '__fish_seen_subcommand_from login' -s u -l username -d 'Username' +# logout +complete -c docker -f -n '__fish_docker_no_subcommand' -a logout -d 'Log out from a Docker registry server' + # logs complete -c docker -f -n '__fish_docker_no_subcommand' -a logs -d 'Fetch the logs of a container' complete -c docker -A -f -n '__fish_seen_subcommand_from logs' -s f -l follow -d 'Follow log output' +complete -c docker -A -f -n '__fish_seen_subcommand_from logs' -s t -l timestamps -d 'Show timestamps' +complete -c docker -A -f -n '__fish_seen_subcommand_from logs' -l tail -d 'Output the specified number of lines at the end of logs (defaults to all logs)' complete -c docker -A -f -n '__fish_seen_subcommand_from logs' -a '(__fish_print_docker_containers running)' -d "Container" # port -complete -c docker -f -n '__fish_docker_no_subcommand' -a port -d 'Lookup the public-facing port which is NAT-ed to PRIVATE_PORT' +complete -c docker -f -n '__fish_docker_no_subcommand' -a port -d 'Lookup the public-facing port that is NAT-ed to PRIVATE_PORT' complete -c docker -A -f -n '__fish_seen_subcommand_from port' -a '(__fish_print_docker_containers running)' -d "Container" +# pause +complete -c docker -f -n '__fish_docker_no_subcommand' -a pause -d 'Pause all processes within a container' +complete -c docker -A -f -n '__fish_seen_subcommand_from pause' -a '(__fish_print_docker_containers running)' -d "Container" + # ps complete -c docker -f -n '__fish_docker_no_subcommand' -a ps -d 'List containers' complete -c docker -A -f -n '__fish_seen_subcommand_from ps' -s a -l all -d 'Show all containers. Only running containers are shown by default.' complete -c docker -A -f -n '__fish_seen_subcommand_from ps' -l before -d 'Show only container created before Id or Name, include non-running ones.' +complete -c docker -A -f -n '__fish_seen_subcommand_from ps' -s f -l filter -d 'Provide filter values. Valid filters:' complete -c docker -A -f -n '__fish_seen_subcommand_from ps' -s l -l latest -d 'Show only the latest created container, include non-running ones.' complete -c docker -A -f -n '__fish_seen_subcommand_from ps' -s n -d 'Show n last created containers, include non-running ones.' complete -c docker -A -f -n '__fish_seen_subcommand_from ps' -l no-trunc -d "Don't truncate output" complete -c docker -A -f -n '__fish_seen_subcommand_from ps' -s q -l quiet -d 'Only display numeric IDs' -complete -c docker -A -f -n '__fish_seen_subcommand_from ps' -s s -l size -d 'Display total file sizes' +complete -c docker -A -f -n '__fish_seen_subcommand_from ps' -s s -l size -d 'Display sizes' complete -c docker -A -f -n '__fish_seen_subcommand_from ps' -l since -d 'Show only containers created since Id or Name, include non-running ones.' # pull -complete -c docker -f -n '__fish_docker_no_subcommand' -a pull -d 'Pull an image or a repository from the docker registry server' -complete -c docker -A -f -n '__fish_seen_subcommand_from pull' -s t -l tag -d 'Download tagged image in repository' +complete -c docker -f -n '__fish_docker_no_subcommand' -a pull -d 'Pull an image or a repository from a Docker registry server' +complete -c docker -A -f -n '__fish_seen_subcommand_from pull' -s a -l all-tags -d 'Download all tagged images in the repository' complete -c docker -A -f -n '__fish_seen_subcommand_from pull' -a '(__fish_print_docker_images)' -d "Image" complete -c docker -A -f -n '__fish_seen_subcommand_from pull' -a '(__fish_print_docker_repositories)' -d "Repository" # push -complete -c docker -f -n '__fish_docker_no_subcommand' -a push -d 'Push an image or a repository to the docker registry server' +complete -c docker -f -n '__fish_docker_no_subcommand' -a push -d 'Push an image or a repository to a Docker registry server' complete -c docker -A -f -n '__fish_seen_subcommand_from push' -a '(__fish_print_docker_images)' -d "Image" complete -c docker -A -f -n '__fish_seen_subcommand_from push' -a '(__fish_print_docker_repositories)' -d "Repository" # restart complete -c docker -f -n '__fish_docker_no_subcommand' -a restart -d 'Restart a running container' -complete -c docker -A -f -n '__fish_seen_subcommand_from restart' -s t -l time -d 'Number of seconds to try to stop for before killing the container. Once killed it will then be restarted. Default=10' +complete -c docker -A -f -n '__fish_seen_subcommand_from restart' -s t -l time -d 'Number of seconds to try to stop for before killing the container. Once killed it will then be restarted. Default is 10 seconds.' complete -c docker -A -f -n '__fish_seen_subcommand_from restart' -a '(__fish_print_docker_containers running)' -d "Container" # rm complete -c docker -f -n '__fish_docker_no_subcommand' -a rm -d 'Remove one or more containers' -complete -c docker -A -f -n '__fish_seen_subcommand_from rm' -s f -l force -d 'Force removal of running container' +complete -c docker -A -f -n '__fish_seen_subcommand_from rm' -s f -l force -d 'Force the removal of a running container (uses SIGKILL)' complete -c docker -A -f -n '__fish_seen_subcommand_from rm' -s l -l link -d 'Remove the specified link and not the underlying container' -complete -c docker -A -f -n '__fish_seen_subcommand_from rm' -s v -l volumes -d 'Remove the volumes associated to the container' +complete -c docker -A -f -n '__fish_seen_subcommand_from rm' -s v -l volumes -d 'Remove the volumes associated with the container' complete -c docker -A -f -n '__fish_seen_subcommand_from rm' -a '(__fish_print_docker_containers stopped)' -d "Container" # rmi complete -c docker -f -n '__fish_docker_no_subcommand' -a rmi -d 'Remove one or more images' -complete -c docker -A -f -n '__fish_seen_subcommand_from rmi' -s f -l force -d 'Force' +complete -c docker -A -f -n '__fish_seen_subcommand_from rmi' -s f -l force -d 'Force removal of the image' +complete -c docker -A -f -n '__fish_seen_subcommand_from rmi' -l no-prune -d 'Do not delete untagged parents' complete -c docker -A -f -n '__fish_seen_subcommand_from rmi' -a '(__fish_print_docker_images)' -d "Image" # run complete -c docker -f -n '__fish_docker_no_subcommand' -a run -d 'Run a command in a new container' -complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s P -l publish-all -d 'Publish all exposed ports to the host interfaces' -complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s a -l attach -d 'Attach to stdin, stdout or stderr.' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s a -l attach -d 'Attach to STDIN, STDOUT or STDERR.' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l add-host -d 'Add a custom host-to-IP mapping (host:ip)' complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s c -l cpu-shares -d 'CPU shares (relative weight)' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l cap-add -d 'Add Linux capabilities' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l cap-drop -d 'Drop Linux capabilities' complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l cidfile -d 'Write the container ID to the file' -complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s d -l detach -d 'Detached mode: Run container in the background, print new container id' -complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l dns -d 'Set custom dns servers' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l cpuset -d 'CPUs in which to allow execution (0-3, 0,1)' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s d -l detach -d 'Detached mode: run the container in the background and print the new container ID' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l device -d 'Add a host device to the container (e.g. --device=/dev/sdc:/dev/xvdc)' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l dns -d 'Set custom DNS servers' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l dns-search -d 'Set custom DNS search domains' complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s e -l env -d 'Set environment variables' -complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l entrypoint -d 'Overwrite the default entrypoint of the image' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l entrypoint -d 'Overwrite the default ENTRYPOINT of the image' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l env-file -d 'Read in a line delimited file of environment variables' complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l expose -d 'Expose a port from the container without publishing it to your host' complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s h -l hostname -d 'Container host name' -complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s i -l interactive -d 'Keep stdin open even if not attached' -complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l link -d 'Add link to another container (name:alias)' -complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l lxc-conf -d 'Add custom lxc options -lxc-conf="lxc.cgroup.cpuset.cpus = 0,1"' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s i -l interactive -d 'Keep STDIN open even if not attached' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l link -d 'Add link to another container in the form of name:alias' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l lxc-conf -d '(lxc exec-driver only) Add custom lxc options --lxc-conf="lxc.cgroup.cpuset.cpus = 0,1"' complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s m -l memory -d 'Memory limit (format: , where unit = b, k, m or g)' -complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s n -l networking -d 'Enable networking for this container' complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l name -d 'Assign a name to the container' -complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s p -l publish -d "Publish a container's port to the host (format: ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort) (use 'docker port' to see the actual mapping)" +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l net -d 'Set the Network mode for the container' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s P -l publish-all -d 'Publish all exposed ports to the host interfaces' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s p -l publish -d "Publish a container's port to the host" complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l privileged -d 'Give extended privileges to this container' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l restart -d 'Restart policy to apply when a container exits (no, on-failure[:max-retry], always)' complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l rm -d 'Automatically remove the container when it exits (incompatible with -d)' -complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l sig-proxy -d 'Proxify all received signal to the process (non-TTY mode only)' -complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s t -l tty -d 'Allocate a pseudo-tty' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l security-opt -d 'Security Options' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l sig-proxy -d 'Proxy received signals to the process (even in non-TTY mode). SIGCHLD, SIGSTOP, and SIGKILL are not proxied.' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s t -l tty -d 'Allocate a pseudo-TTY' complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s u -l user -d 'Username or UID' -complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s v -l volume -d 'Bind mount a volume (e.g. from the host: -v /host:/container, from docker: -v /container)' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s v -l volume -d 'Bind mount a volume (e.g., from the host: -v /host:/container, from Docker: -v /container)' complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l volumes-from -d 'Mount volumes from the specified container(s)' complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s w -l workdir -d 'Working directory inside the container' complete -c docker -A -f -n '__fish_seen_subcommand_from run' -a '(__fish_print_docker_images)' -d "Image" # save complete -c docker -f -n '__fish_docker_no_subcommand' -a save -d 'Save an image to a tar archive' +complete -c docker -A -f -n '__fish_seen_subcommand_from save' -s o -l output -d 'Write to a file, instead of STDOUT' complete -c docker -A -f -n '__fish_seen_subcommand_from save' -a '(__fish_print_docker_images)' -d "Image" # search -complete -c docker -f -n '__fish_docker_no_subcommand' -a search -d 'Search for an image in the docker index' -complete -c docker -A -f -n '__fish_seen_subcommand_from search' -l no-trunc -d "Don't truncate output" -complete -c docker -A -f -n '__fish_seen_subcommand_from search' -s s -l stars -d 'Only displays with at least xxx stars' +complete -c docker -f -n '__fish_docker_no_subcommand' -a search -d 'Search for an image on the Docker Hub' complete -c docker -A -f -n '__fish_seen_subcommand_from search' -l automated -d 'Only show automated builds' +complete -c docker -A -f -n '__fish_seen_subcommand_from search' -l no-trunc -d "Don't truncate output" +complete -c docker -A -f -n '__fish_seen_subcommand_from search' -s s -l stars -d 'Only displays with at least x stars' # start complete -c docker -f -n '__fish_docker_no_subcommand' -a start -d 'Start a stopped container' -complete -c docker -A -f -n '__fish_seen_subcommand_from start' -s a -l attach -d "Attach container's stdout/stderr and forward all signals to the process" -complete -c docker -A -f -n '__fish_seen_subcommand_from start' -s i -l interactive -d "Attach container's stdin" +complete -c docker -A -f -n '__fish_seen_subcommand_from start' -s a -l attach -d "Attach container's STDOUT and STDERR and forward all signals to the process" +complete -c docker -A -f -n '__fish_seen_subcommand_from start' -s i -l interactive -d "Attach container's STDIN" complete -c docker -A -f -n '__fish_seen_subcommand_from start' -a '(__fish_print_docker_containers stopped)' -d "Container" # stop complete -c docker -f -n '__fish_docker_no_subcommand' -a stop -d 'Stop a running container' -complete -c docker -A -f -n '__fish_seen_subcommand_from stop' -s t -l time -d 'Number of seconds to wait for the container to stop before killing it.' +complete -c docker -A -f -n '__fish_seen_subcommand_from stop' -s t -l time -d 'Number of seconds to wait for the container to stop before killing it. Default is 10 seconds.' complete -c docker -A -f -n '__fish_seen_subcommand_from stop' -a '(__fish_print_docker_containers running)' -d "Container" # tag complete -c docker -f -n '__fish_docker_no_subcommand' -a tag -d 'Tag an image into a repository' complete -c docker -A -f -n '__fish_seen_subcommand_from tag' -s f -l force -d 'Force' -complete -c docker -A -f -n '__fish_seen_subcommand_from tag' -a '(__fish_print_docker_images)' -d "Image" # top complete -c docker -f -n '__fish_docker_no_subcommand' -a top -d 'Lookup the running processes of a container' complete -c docker -A -f -n '__fish_seen_subcommand_from top' -a '(__fish_print_docker_containers running)' -d "Container" +# unpause +complete -c docker -f -n '__fish_docker_no_subcommand' -a unpause -d 'Unpause a paused container' +complete -c docker -A -f -n '__fish_seen_subcommand_from unpause' -a '(__fish_print_docker_containers running)' -d "Container" + # version -complete -c docker -f -n '__fish_docker_no_subcommand' -a version -d 'Show the docker version information' +complete -c docker -f -n '__fish_docker_no_subcommand' -a version -d 'Show the Docker version information' # wait complete -c docker -f -n '__fish_docker_no_subcommand' -a wait -d 'Block until a container stops, then print its exit code' From ee1ba252187a7e1a80e3773fe9748410d01a39b8 Mon Sep 17 00:00:00 2001 From: Neal McBurnett Date: Sun, 7 Dec 2014 13:43:20 -0700 Subject: [PATCH 013/513] Fixes #9555: sudo not needed with cert authn Signed-off-by: Neal McBurnett --- docs/sources/articles/https.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/sources/articles/https.md b/docs/sources/articles/https.md index c8873bcbe..2fe5162d6 100644 --- a/docs/sources/articles/https.md +++ b/docs/sources/articles/https.md @@ -116,13 +116,13 @@ Finally, you need to remove the passphrase from the client and server key: Now you can make the Docker daemon only accept connections from clients providing a certificate trusted by our CA: - $ sudo docker -d --tlsverify --tlscacert=ca.pem --tlscert=server-cert.pem --tlskey=server-key.pem \ + $ docker -d --tlsverify --tlscacert=ca.pem --tlscert=server-cert.pem --tlskey=server-key.pem \ -H=0.0.0.0:2376 To be able to connect to Docker and validate its certificate, you now need to provide your client keys, certificates and trusted CA: - $ sudo docker --tlsverify --tlscacert=ca.pem --tlscert=cert.pem --tlskey=key.pem \ + $ docker --tlsverify --tlscacert=ca.pem --tlscert=cert.pem --tlskey=key.pem \ -H=dns-name-of-docker-host:2376 version > **Note**: @@ -150,7 +150,7 @@ the files to the `.docker` directory in your home directory - and set the Docker will now connect securely by default: - $ sudo docker ps + $ docker ps ## Other modes @@ -177,7 +177,7 @@ if you want to store your keys in another location, you can specify that location using the environment variable `DOCKER_CERT_PATH`. $ export DOCKER_CERT_PATH=${HOME}/.docker/zone1/ - $ sudo docker --tlsverify ps + $ docker --tlsverify ps ### Connecting to the Secure Docker port using `curl` From b3ade99a7822f4edb21400a1003ff0e3893caa38 Mon Sep 17 00:00:00 2001 From: Alexandr Morozov Date: Wed, 10 Dec 2014 11:59:18 -0800 Subject: [PATCH 014/513] Don't try release network in non-private modes Fixes #9594 Signed-off-by: Alexandr Morozov --- daemon/container.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/container.go b/daemon/container.go index 45658c583..3c05c645a 100644 --- a/daemon/container.go +++ b/daemon/container.go @@ -547,7 +547,7 @@ func (container *Container) AllocateNetwork() error { } func (container *Container) ReleaseNetwork() { - if container.Config.NetworkDisabled { + if container.Config.NetworkDisabled || !container.hostConfig.NetworkMode.IsPrivate() { return } eng := container.daemon.eng From 1a9b640e0d3e6916bff9cd7dd8ab435a70c6a0e8 Mon Sep 17 00:00:00 2001 From: Qiang Huang Date: Wed, 10 Dec 2014 16:53:43 -0800 Subject: [PATCH 015/513] add support to set MemorySwap Signed-off-by: Qiang Huang --- daemon/create.go | 3 +++ runconfig/parse.go | 11 +++++++++++ 2 files changed, 14 insertions(+) diff --git a/daemon/create.go b/daemon/create.go index f9d986491..5095e1347 100644 --- a/daemon/create.go +++ b/daemon/create.go @@ -29,6 +29,9 @@ func (daemon *Daemon) ContainerCreate(job *engine.Job) engine.Status { job.Errorf("Your kernel does not support swap limit capabilities. Limitation discarded.\n") config.MemorySwap = -1 } + if config.Memory > 0 && config.MemorySwap > 0 && config.MemorySwap < config.Memory { + return job.Errorf("Minimum memoryswap limit should larger than memory limit, see usage.\n") + } var hostConfig *runconfig.HostConfig if job.EnvExists("HostConfig") { diff --git a/runconfig/parse.go b/runconfig/parse.go index 0d682f35d..91dd895d7 100644 --- a/runconfig/parse.go +++ b/runconfig/parse.go @@ -53,6 +53,7 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe flEntrypoint = cmd.String([]string{"#entrypoint", "-entrypoint"}, "", "Overwrite the default ENTRYPOINT of the image") flHostname = cmd.String([]string{"h", "-hostname"}, "", "Container host name") flMemoryString = cmd.String([]string{"m", "-memory"}, "", "Memory limit (format: , where unit = b, k, m or g)") + flMemorySwap = cmd.String([]string{"-memory-swap"}, "", "Total memory usage (memory + swap), set '-1' to disable swap (format: , where unit = b, k, m or g)") flUser = cmd.String([]string{"u", "-user"}, "", "Username or UID") flWorkingDir = cmd.String([]string{"w", "-workdir"}, "", "Working directory inside the container") flCpuShares = cmd.Int64([]string{"c", "-cpu-shares"}, 0, "CPU shares (relative weight)") @@ -136,6 +137,15 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe flMemory = parsedMemory } + var MemorySwap int64 + if *flMemorySwap != "" { + parsedMemorySwap, err := units.RAMInBytes(*flMemorySwap) + if err != nil { + return nil, nil, cmd, err + } + MemorySwap = parsedMemorySwap + } + var binds []string // add any bind targets to the list of container volumes for bind := range flVolumes.GetMap() { @@ -261,6 +271,7 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe NetworkDisabled: !*flNetwork, OpenStdin: *flStdin, Memory: flMemory, + MemorySwap: MemorySwap, CpuShares: *flCpuShares, Cpuset: *flCpuset, AttachStdin: attachStdin, From 0d70ad1c7205ba9e82dd1f244d26df93dc79de9e Mon Sep 17 00:00:00 2001 From: Alexandr Morozov Date: Wed, 10 Dec 2014 20:55:51 -0800 Subject: [PATCH 016/513] Fix race in TestDaemonRestartWithVolumesRefs Sometimes rm begins before process death, but Kill called already after it, so we get error - no such process. Signed-off-by: Alexandr Morozov --- integration-cli/docker_cli_daemon_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration-cli/docker_cli_daemon_test.go b/integration-cli/docker_cli_daemon_test.go index 31bfac3f6..b64f71464 100644 --- a/integration-cli/docker_cli_daemon_test.go +++ b/integration-cli/docker_cli_daemon_test.go @@ -66,7 +66,7 @@ func TestDaemonRestartWithVolumesRefs(t *testing.T) { if err := d.Restart(); err != nil { t.Fatal(err) } - if _, err := d.Cmd("run", "-d", "--volumes-from", "volrestarttest1", "--name", "volrestarttest2", "busybox"); err != nil { + if _, err := d.Cmd("run", "-d", "--volumes-from", "volrestarttest1", "--name", "volrestarttest2", "busybox", "top"); err != nil { t.Fatal(err) } if out, err := d.Cmd("rm", "-fv", "volrestarttest2"); err != nil { From 3a08bbc4d29835fe5b8f3d2c5bde442f6a7014a0 Mon Sep 17 00:00:00 2001 From: Peter Waller Date: Thu, 11 Dec 2014 17:57:23 +0000 Subject: [PATCH 017/513] Improve message when pushing a non-existent image I was confused earlier when I did: ``` docker push localhost.localdomain:1234/foo ``` Because docker told me: ``` No such id: localhost.localdomain:1234/foo ``` I actually had buried in my mind the solution to this, but the error message confused me because I had recently had some fun trying to get the registry working and therefore thought it was telling me that I didn't have an account on the registry. This pull request makes it unambiguous that the error is that the specified image is unknown. /cc @cpuguy83 Docker-DCO-1.1-Signed-off-by: Peter Waller (github: pwaller) --- graph/graph.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graph/graph.go b/graph/graph.go index 720f6e696..b6ea22bdc 100644 --- a/graph/graph.go +++ b/graph/graph.go @@ -88,7 +88,7 @@ func (graph *Graph) Exists(id string) bool { func (graph *Graph) Get(name string) (*image.Image, error) { id, err := graph.idIndex.Get(name) if err != nil { - return nil, err + return nil, fmt.Errorf("could not find image: %v", err) } img, err := image.LoadImage(graph.ImageRoot(id)) if err != nil { From c509835475f4f222400e6a4d2211c59e41329817 Mon Sep 17 00:00:00 2001 From: Jessica Frazelle Date: Thu, 11 Dec 2014 17:00:43 -0800 Subject: [PATCH 018/513] Change version to 1.4.0-dev Docker-DCO-1.1-Signed-off-by: Jessica Frazelle (github: jfrazelle) --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 88c5fb891..c3f0d2bbd 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.4.0 +1.4.0-dev From fd831b4c5e50a3f1de8eb506016ffe89d5cac8d4 Mon Sep 17 00:00:00 2001 From: Jessica Frazelle Date: Thu, 11 Dec 2014 17:14:53 -0800 Subject: [PATCH 019/513] Fix conflicts. Docker-DCO-1.1-Signed-off-by: Jessica Frazelle (github: jfrazelle) --- integration-cli/docker_cli_save_load_test.go | 67 ++++++++++++++++++++ registry/registry.go | 4 -- 2 files changed, 67 insertions(+), 4 deletions(-) diff --git a/integration-cli/docker_cli_save_load_test.go b/integration-cli/docker_cli_save_load_test.go index 6de5a7963..94bfe3d6a 100644 --- a/integration-cli/docker_cli_save_load_test.go +++ b/integration-cli/docker_cli_save_load_test.go @@ -171,6 +171,73 @@ func TestSaveXzAndLoadRepoStdout(t *testing.T) { logDone("load - save a repo with xz compression & load it using stdout") } +// save a repo using xz+gz compression and try to load it using stdout +func TestSaveXzGzAndLoadRepoStdout(t *testing.T) { + tempDir, err := ioutil.TempDir("", "test-save-xz-gz-load-repo-stdout") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tempDir) + + tarballPath := filepath.Join(tempDir, "foobar-save-load-test.tar.xz.gz") + + runCmd := exec.Command(dockerBinary, "run", "-d", "busybox", "true") + out, _, err := runCommandWithOutput(runCmd) + if err != nil { + t.Fatalf("failed to create a container: %v %v", out, err) + } + + cleanedContainerID := stripTrailingCharacters(out) + + repoName := "foobar-save-load-test-xz-gz" + + inspectCmd := exec.Command(dockerBinary, "inspect", cleanedContainerID) + out, _, err = runCommandWithOutput(inspectCmd) + if err != nil { + t.Fatalf("output should've been a container id: %v %v", cleanedContainerID, err) + } + + commitCmd := exec.Command(dockerBinary, "commit", cleanedContainerID, repoName) + out, _, err = runCommandWithOutput(commitCmd) + if err != nil { + t.Fatalf("failed to commit container: %v %v", out, err) + } + + inspectCmd = exec.Command(dockerBinary, "inspect", repoName) + before, _, err := runCommandWithOutput(inspectCmd) + if err != nil { + t.Fatalf("the repo should exist before saving it: %v %v", before, err) + } + + saveCmdTemplate := `%v save %v | xz -c | gzip -c > %s` + saveCmdFinal := fmt.Sprintf(saveCmdTemplate, dockerBinary, repoName, tarballPath) + saveCmd := exec.Command("bash", "-c", saveCmdFinal) + out, _, err = runCommandWithOutput(saveCmd) + if err != nil { + t.Fatalf("failed to save repo: %v %v", out, err) + } + + deleteImages(repoName) + + loadCmdFinal := fmt.Sprintf(`cat %s | docker load`, tarballPath) + loadCmd := exec.Command("bash", "-c", loadCmdFinal) + out, _, err = runCommandWithOutput(loadCmd) + if err == nil { + t.Fatalf("expected error, but succeeded with no error and output: %v", out) + } + + inspectCmd = exec.Command(dockerBinary, "inspect", repoName) + after, _, err := runCommandWithOutput(inspectCmd) + if err == nil { + t.Fatalf("the repo should not exist: %v", after) + } + + deleteContainer(cleanedContainerID) + deleteImages(repoName) + + logDone("load - save a repo with xz+gz compression & load it using stdout") +} + func TestSaveSingleTag(t *testing.T) { repoName := "foobar-save-single-tag-test" diff --git a/registry/registry.go b/registry/registry.go index f3a4a340b..d503a63d6 100644 --- a/registry/registry.go +++ b/registry/registry.go @@ -47,10 +47,6 @@ func newClient(jar http.CookieJar, roots *x509.CertPool, certs []tls.Certificate tlsConfig.InsecureSkipVerify = true } - if !secure { - tlsConfig.InsecureSkipVerify = true - } - httpTransport := &http.Transport{ DisableKeepAlives: true, Proxy: http.ProxyFromEnvironment, From 862952c8d4d3df69f35535af5ce5a079b5fe739e Mon Sep 17 00:00:00 2001 From: Alexandr Morozov Date: Thu, 11 Dec 2014 17:22:59 -0800 Subject: [PATCH 020/513] Fix race condition between parseSecurityOpt and container.Mount Signed-off-by: Alexandr Morozov --- daemon/start.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/daemon/start.go b/daemon/start.go index f72407e3f..286ee58a3 100644 --- a/daemon/start.go +++ b/daemon/start.go @@ -44,6 +44,8 @@ func (daemon *Daemon) ContainerStart(job *engine.Job) engine.Status { } func (daemon *Daemon) setHostConfig(container *Container, hostConfig *runconfig.HostConfig) error { + container.Lock() + defer container.Unlock() if err := parseSecurityOpt(container, hostConfig); err != nil { return err } @@ -66,8 +68,8 @@ func (daemon *Daemon) setHostConfig(container *Container, hostConfig *runconfig. if err := daemon.RegisterLinks(container, hostConfig); err != nil { return err } - container.SetHostConfig(hostConfig) - container.ToDisk() + container.hostConfig = hostConfig + container.toDisk() return nil } From 1cab340c10be71382a8cfde8402f475d503c94be Mon Sep 17 00:00:00 2001 From: Arnaud Porterie Date: Thu, 11 Dec 2014 18:05:07 -0800 Subject: [PATCH 021/513] Suppress output of TestLoginWithoutTTY Signed-off-by: Arnaud Porterie --- integration-cli/docker_cli_login_test.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/integration-cli/docker_cli_login_test.go b/integration-cli/docker_cli_login_test.go index cf134e4c9..d2b927b11 100644 --- a/integration-cli/docker_cli_login_test.go +++ b/integration-cli/docker_cli_login_test.go @@ -3,16 +3,12 @@ package main import ( "bytes" "io" - "os" "os/exec" "testing" ) func TestLoginWithoutTTY(t *testing.T) { cmd := exec.Command(dockerBinary, "login") - // setup STDOUT and STDERR so that we see any output and errors in our console - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr // create a buffer with text then a new line as a return buf := bytes.NewBuffer([]byte("buffer test string \n")) From d04debddd983b3aa48ad38659d2c3debe794d374 Mon Sep 17 00:00:00 2001 From: Zoltan Tombol Date: Fri, 12 Dec 2014 03:25:14 +0100 Subject: [PATCH 022/513] Fix #9462 Docker-DCO-1.1-Signed-off-by: Zoltan Tombol (github: ztombol) --- contrib/mkimage-arch.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/mkimage-arch.sh b/contrib/mkimage-arch.sh index 35cb1617d..bf00e60e7 100755 --- a/contrib/mkimage-arch.sh +++ b/contrib/mkimage-arch.sh @@ -36,7 +36,7 @@ expect < $ROOTFS/etc/locale.gen arch-chroot $ROOTFS locale-gen From 473a443d84084e0393be2a1606d3fa49a26f4dbd Mon Sep 17 00:00:00 2001 From: Jessica Frazelle Date: Thu, 11 Dec 2014 18:47:47 -0800 Subject: [PATCH 023/513] Updates to release checklist. Docker-DCO-1.1-Signed-off-by: Jessica Frazelle (github: jfrazelle) --- project/RELEASE-CHECKLIST.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/project/RELEASE-CHECKLIST.md b/project/RELEASE-CHECKLIST.md index 250a25217..61b0c2b07 100644 --- a/project/RELEASE-CHECKLIST.md +++ b/project/RELEASE-CHECKLIST.md @@ -253,7 +253,7 @@ If this is a MAJOR.MINOR.0 release, you need to make an branch for the previous documentation: ```bash -git checkout -b docs-$PREVIOUS_MAJOR_MINOR docs +git checkout -b docs-$PREVIOUS_MAJOR_MINOR git fetch git reset --hard origin/docs git push -f origin docs-$PREVIOUS_MAJOR_MINOR @@ -282,8 +282,8 @@ Ask Sven, or JohnC to invalidate the cloudfront cache using the CND Planet chrom git checkout master git fetch git reset --hard origin/master -git merge origin/release git checkout -b merge_release_$VERSION +git merge origin/release echo ${VERSION#v}-dev > VERSION git add VERSION git commit -m "Change version to $(cat VERSION)" From a65396b079c1b05255553af8cde3a94d7796e752 Mon Sep 17 00:00:00 2001 From: Jessica Frazelle Date: Thu, 11 Dec 2014 19:00:34 -0800 Subject: [PATCH 024/513] Include fred's release notes for 1.4.0. He is unfortunately out of power and internet because of storms :( Docker-DCO-1.1-Signed-off-by: Fred Lifton (github: fredlf) Docker-DCO-1.1-Signed-off-by: Jessica Frazelle (github: jfrazelle) --- docs/sources/release-notes.md | 70 +++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/docs/sources/release-notes.md b/docs/sources/release-notes.md index 7ec08b1a8..c6e4dff6e 100644 --- a/docs/sources/release-notes.md +++ b/docs/sources/release-notes.md @@ -4,6 +4,76 @@ page_keywords: docker, documentation, about, technology, understanding, release #Release Notes +You can view release notes for earlier version of Docker by selecting the +desired version from the drop-down list at the top right of this page. + +##Version 1.4.0 +(2014-12-11) + +This release provides a number of new features, but is mainly focused on bug +fixes and improvements to platform stability and security. + +For a complete list of patches, fixes, and other improvements, see + +*New Features* + +* You can now add labels to the Docker daemon using key=value pairs defined with +the new `--label` flag. The labels are displayed by running `docker info`. In +addition, `docker info` also now returns an ID and hostname field. For more +information, see the +[command line reference](http://docs.docker.com/reference/commandline/cli/#daemon). +* The `ENV` instruction in the `Dockerfile` now supports arguments in the form +of `ENV name=value name2=value2..`. For more information, see the +[command line reference](http://docs.docker.com/reference/builder/#env) +* Introducing a new, still +[experimental, overlayfs storage driver](https://github.com/docker/docker/pull/7619/). +* You can now add filters to `docker events` to filter events by event name, +container, or image. For more information, see the +[command line reference](http://docs.docker.com/reference/commandline/cli/#events). +* The `docker cp` command now supports copying files from the filesystem of a +container's volumes. For more information, see the +[remote API reference](http://docs.docker.com/reference/api/docker_remote_api/). +* The `docker tag` command has been fixed so that it correctly honors `--force` +when overriding a tag for existing image. For more information, see +the [command line reference](http://docs.docker.com/reference/commandline/cli/#tag). + +* Container volumes are now initialized during `docker create`. For more information, see +the [command line reference](http://docs.docker.com/reference/commandline/cli/#create). + +*Security Fixes* + +Patches and changes were made to address the following vulnerabilities: + +* CVE-2014-9356: Path traversal during processing of absolute symlinks. +Absolute symlinks were not adequately checked for traversal which created a +vulnerability via image extraction and/or volume mounts. +* CVE-2014-9357: Escalation of privileges during decompression of LZMA (.xz) +archives. Docker 1.3.2 added `chroot` for archive extraction. This created a +vulnerability that could allow malicious images or builds to write files to the +host system and escape containerization, leading to privilege escalation. +* CVE-2014-9358: Path traversal and spoofing opportunities via image +identifiers. Image IDs passed either via `docker load` or registry communications +were not sufficiently validated. This created a vulnerability to path traversal +attacks wherein malicious images or repository spoofing could lead to graph +corruption and manipulation. + +Note that the above CVE's are also in Docker 1.3.3, which was released +concurrently with 1.4.0. + +*Runtime fixes* + +* Fixed an issue that caused image archives to be read slowly. + +*Client fixes* + +* Fixed a regression related to STDIN redirection. +* Fixed a regression involving `docker cp` when the current directory is the +destination. + +> **Note** +> Development history prior to version 1.0 can be found by +> searching in [GitHub](https://github.com/docker/docker). + ##Version 1.3.3 (2014-12-11) From bb25f54a9953ce4d87cddad880befb016aa276d7 Mon Sep 17 00:00:00 2001 From: Phil Estes Date: Fri, 12 Dec 2014 11:57:23 -0500 Subject: [PATCH 025/513] Update Ubuntu install instructions regarding dated Ubuntu docker.io Docker-DCO-1.1-Signed-off-by: Phil Estes (github: estesp) --- docs/sources/installation/ubuntulinux.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/sources/installation/ubuntulinux.md b/docs/sources/installation/ubuntulinux.md index 09b776f08..d4df599d0 100644 --- a/docs/sources/installation/ubuntulinux.md +++ b/docs/sources/installation/ubuntulinux.md @@ -17,15 +17,15 @@ Please read [*Docker and UFW*](#docker-and-ufw), if you plan to use [UFW ## Ubuntu Trusty 14.04 (LTS) (64-bit) Ubuntu Trusty comes with a 3.13.0 Linux kernel, and a `docker.io` package which -installs Docker 0.9.1 and all its prerequisites from Ubuntu's repository. +installs Docker 1.0.1 and all its prerequisites from Ubuntu's repository. > **Note**: > Ubuntu (and Debian) contain a much older KDE3/GNOME2 package called ``docker``, so the -> package and the executable are called ``docker.io``. +> Ubuntu-maintained package and executable are named ``docker.io``. -### Installation +### Ubuntu-maintained Package Installation -To install the latest Ubuntu package (may not be the latest Docker release): +To install the latest Ubuntu package (this is **not** the most recent Docker release): $ sudo apt-get update $ sudo apt-get install docker.io @@ -34,6 +34,13 @@ Then, to enable tab-completion of Docker commands in BASH, either restart BASH o $ source /etc/bash_completion.d/docker.io +> **Note**: +> Since the Ubuntu package is quite dated at this point, you may want to use +> the following section to install the most recent release of Docker. +> If you install the Docker version, you do not need to install ``docker.io`` from Ubuntu. + +### Docker-maintained Package Installation + If you'd like to try the latest version of Docker: First, check that your APT system can deal with `https` From 8f8d24cb1c762e74833bbb9b6e2b58438b42d36b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Greinhofer?= Date: Thu, 11 Dec 2014 15:09:48 -0800 Subject: [PATCH 026/513] Report number of times that docker has auto-restarted a container MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #9469. Signed-off-by: Rémy Greinhofer --- daemon/inspect.go | 1 + 1 file changed, 1 insertion(+) diff --git a/daemon/inspect.go b/daemon/inspect.go index a6ff2de69..d8397127c 100644 --- a/daemon/inspect.go +++ b/daemon/inspect.go @@ -41,6 +41,7 @@ func (daemon *Daemon) ContainerInspect(job *engine.Job) engine.Status { out.Set("HostnamePath", container.HostnamePath) out.Set("HostsPath", container.HostsPath) out.Set("Name", container.Name) + out.SetInt("RestartCount", container.RestartCount) out.Set("Driver", container.Driver) out.Set("ExecDriver", container.ExecDriver) out.Set("MountLabel", container.MountLabel) From bf05f78d700532bb7504a2e1fd388f81c48aa2e6 Mon Sep 17 00:00:00 2001 From: Fred Lifton Date: Fri, 12 Dec 2014 10:34:15 -0800 Subject: [PATCH 027/513] Adds User Invites to Hub Docs Docker-DCO-1.1-Signed-off-by: Fred Lifton (github: fredlf) --- docs/sources/docker-hub/accounts.md | 9 +++++++-- docs/sources/docker-hub/hub-images/invite.png | Bin 0 -> 115369 bytes docs/sources/docker-hub/invite.png | Bin 0 -> 116723 bytes docs/sources/docker-hub/orgs.png | Bin 21063 -> 65489 bytes 4 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 docs/sources/docker-hub/hub-images/invite.png create mode 100644 docs/sources/docker-hub/invite.png diff --git a/docs/sources/docker-hub/accounts.md b/docs/sources/docker-hub/accounts.md index 304010fb5..be3212005 100644 --- a/docs/sources/docker-hub/accounts.md +++ b/docs/sources/docker-hub/accounts.md @@ -37,8 +37,8 @@ page. Also available on the Docker Hub are organizations and groups that allow you to collaborate across your organization or team. You can see what organizations [you belong to and add new organizations]( -https://hub.docker.com/account/organizations/) from the Account -tab. +https://hub.docker.com/account/organizations/) from the Account Settings +tab. They are also listed below your user name on your repositories page and in your account profile. ![organizations](/docker-hub/orgs.png) @@ -47,3 +47,8 @@ further manage who can interact with your repositories. ![groups](/docker-hub/groups.png) +You can add or invite users to join groups by clicking on the organization and then clicking the edit button for the group to which you want to add members. Enter a user-name (for current Hub users) or email address (if they are not yet Hub users) for the person you want to invite. They will receive an email invitation to join the group. + +![invite members](/docker-hub/invite.png) + + diff --git a/docs/sources/docker-hub/hub-images/invite.png b/docs/sources/docker-hub/hub-images/invite.png new file mode 100644 index 0000000000000000000000000000000000000000..e6b74d0617948ba4897d9f7737e10a1817efe721 GIT binary patch literal 115369 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

`?gOD^@eMLoWJlV+_J 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 literal 0 HcmV?d00001 diff --git a/docs/sources/docker-hub/invite.png b/docs/sources/docker-hub/invite.png new file mode 100644 index 0000000000000000000000000000000000000000..5534d2b888c04fbe3c8455d5cd72a9da7f267c1c GIT binary patch literal 116723 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 literal 0 HcmV?d00001 diff --git a/docs/sources/docker-hub/orgs.png b/docs/sources/docker-hub/orgs.png index f205d7f8fd80124772e3471b4ad389da61665b56..6a49cc65dd7ac6da99f2d9ab95cfd27090243769 100644 GIT binary patch literal 65489 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 literal 21063 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

J@7UbM4WPiW4yQvPIp9CcICogyA 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ℜFrvB-i}_M0q?FNWDAdS> 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 HdL;fg)fxQJ From e7d086c2be41dfedfa5f2fb0c437eb5bbf6f2f5d Mon Sep 17 00:00:00 2001 From: Alexander Morozov Date: Fri, 12 Dec 2014 10:38:48 -0800 Subject: [PATCH 028/513] Fix vet errors about unkeyed fields Signed-off-by: Alexander Morozov --- daemon/daemon.go | 4 ++-- daemon/execdriver/lxc/driver.go | 12 ++++++------ daemon/execdriver/native/driver.go | 12 ++++++------ daemon/state_test.go | 2 +- integration/runtime_test.go | 2 +- pkg/chrootarchive/archive.go | 2 +- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/daemon/daemon.go b/daemon/daemon.go index a2e6a79bd..2dcd3c312 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -234,7 +234,7 @@ func (daemon *Daemon) register(container *Container, updateSuffixarray bool) err log.Debugf("killing old running container %s", container.ID) existingPid := container.Pid - container.SetStopped(&execdriver.ExitStatus{0, false}) + container.SetStopped(&execdriver.ExitStatus{ExitCode: 0}) // We only have to handle this for lxc because the other drivers will ensure that // no processes are left when docker dies @@ -266,7 +266,7 @@ func (daemon *Daemon) register(container *Container, updateSuffixarray bool) err log.Debugf("Marking as stopped") - container.SetStopped(&execdriver.ExitStatus{-127, false}) + container.SetStopped(&execdriver.ExitStatus{ExitCode: -127}) if err := container.ToDisk(); err != nil { return err } diff --git a/daemon/execdriver/lxc/driver.go b/daemon/execdriver/lxc/driver.go index 642247c85..c02ceae97 100644 --- a/daemon/execdriver/lxc/driver.go +++ b/daemon/execdriver/lxc/driver.go @@ -76,11 +76,11 @@ func (d *driver) Run(c *execdriver.Command, pipes *execdriver.Pipes, startCallba }) if err := d.generateEnvConfig(c); err != nil { - return execdriver.ExitStatus{-1, false}, err + return execdriver.ExitStatus{ExitCode: -1}, err } configPath, err := d.generateLXCConfig(c) if err != nil { - return execdriver.ExitStatus{-1, false}, err + return execdriver.ExitStatus{ExitCode: -1}, err } params := []string{ "lxc-start", @@ -154,11 +154,11 @@ func (d *driver) Run(c *execdriver.Command, pipes *execdriver.Pipes, startCallba c.ProcessConfig.Args = append([]string{name}, arg...) if err := nodes.CreateDeviceNodes(c.Rootfs, c.AutoCreatedDevices); err != nil { - return execdriver.ExitStatus{-1, false}, err + return execdriver.ExitStatus{ExitCode: -1}, err } if err := c.ProcessConfig.Start(); err != nil { - return execdriver.ExitStatus{-1, false}, err + return execdriver.ExitStatus{ExitCode: -1}, err } var ( @@ -182,7 +182,7 @@ func (d *driver) Run(c *execdriver.Command, pipes *execdriver.Pipes, startCallba c.ProcessConfig.Process.Kill() c.ProcessConfig.Wait() } - return execdriver.ExitStatus{-1, false}, err + return execdriver.ExitStatus{ExitCode: -1}, err } c.ContainerPid = pid @@ -193,7 +193,7 @@ func (d *driver) Run(c *execdriver.Command, pipes *execdriver.Pipes, startCallba <-waitLock - return execdriver.ExitStatus{getExitCode(c), false}, waitErr + return execdriver.ExitStatus{ExitCode: getExitCode(c)}, waitErr } /// Return the exit code of the process diff --git a/daemon/execdriver/native/driver.go b/daemon/execdriver/native/driver.go index 01455a810..a036abc21 100644 --- a/daemon/execdriver/native/driver.go +++ b/daemon/execdriver/native/driver.go @@ -74,7 +74,7 @@ func (d *driver) Run(c *execdriver.Command, pipes *execdriver.Pipes, startCallba // take the Command and populate the libcontainer.Config from it container, err := d.createContainer(c) if err != nil { - return execdriver.ExitStatus{-1, false}, err + return execdriver.ExitStatus{ExitCode: -1}, err } var term execdriver.Terminal @@ -85,7 +85,7 @@ func (d *driver) Run(c *execdriver.Command, pipes *execdriver.Pipes, startCallba term, err = execdriver.NewStdConsole(&c.ProcessConfig, pipes) } if err != nil { - return execdriver.ExitStatus{-1, false}, err + return execdriver.ExitStatus{ExitCode: -1}, err } c.ProcessConfig.Terminal = term @@ -102,12 +102,12 @@ func (d *driver) Run(c *execdriver.Command, pipes *execdriver.Pipes, startCallba ) if err := d.createContainerRoot(c.ID); err != nil { - return execdriver.ExitStatus{-1, false}, err + return execdriver.ExitStatus{ExitCode: -1}, err } defer d.cleanContainer(c.ID) if err := d.writeContainerFile(container, c.ID); err != nil { - return execdriver.ExitStatus{-1, false}, err + return execdriver.ExitStatus{ExitCode: -1}, err } execOutputChan := make(chan execOutput, 1) @@ -146,7 +146,7 @@ func (d *driver) Run(c *execdriver.Command, pipes *execdriver.Pipes, startCallba select { case execOutput := <-execOutputChan: - return execdriver.ExitStatus{execOutput.exitCode, false}, execOutput.err + return execdriver.ExitStatus{ExitCode: execOutput.exitCode}, execOutput.err case <-waitForStart: break } @@ -161,7 +161,7 @@ func (d *driver) Run(c *execdriver.Command, pipes *execdriver.Pipes, startCallba // wait for the container to exit. execOutput := <-execOutputChan - return execdriver.ExitStatus{execOutput.exitCode, oomKill}, execOutput.err + return execdriver.ExitStatus{ExitCode: execOutput.exitCode, OOMKilled: oomKill}, execOutput.err } func (d *driver) Kill(p *execdriver.Command, sig int) error { diff --git a/daemon/state_test.go b/daemon/state_test.go index 32c005cf2..861076aeb 100644 --- a/daemon/state_test.go +++ b/daemon/state_test.go @@ -49,7 +49,7 @@ func TestStateRunStop(t *testing.T) { atomic.StoreInt64(&exit, int64(exitCode)) close(stopped) }() - s.SetStopped(&execdriver.ExitStatus{i, false}) + s.SetStopped(&execdriver.ExitStatus{ExitCode: i}) if s.IsRunning() { t.Fatal("State is running") } diff --git a/integration/runtime_test.go b/integration/runtime_test.go index d173af1f7..93a32e9f7 100644 --- a/integration/runtime_test.go +++ b/integration/runtime_test.go @@ -653,7 +653,7 @@ func TestRestore(t *testing.T) { if err := container3.Run(); err != nil { t.Fatal(err) } - container2.SetStopped(&execdriver.ExitStatus{0, false}) + container2.SetStopped(&execdriver.ExitStatus{ExitCode: 0}) } func TestDefaultContainerName(t *testing.T) { diff --git a/pkg/chrootarchive/archive.go b/pkg/chrootarchive/archive.go index 0077f930d..66f837314 100644 --- a/pkg/chrootarchive/archive.go +++ b/pkg/chrootarchive/archive.go @@ -16,7 +16,7 @@ import ( "github.com/docker/docker/pkg/reexec" ) -var chrootArchiver = &archive.Archiver{Untar} +var chrootArchiver = &archive.Archiver{Untar: Untar} func chroot(path string) error { if err := syscall.Chroot(path); err != nil { From 2540765ddc8889e2757f9ccfbfe852074b61601c Mon Sep 17 00:00:00 2001 From: Alexander Morozov Date: Fri, 12 Dec 2014 10:46:09 -0800 Subject: [PATCH 029/513] Fix vet errors in aufs.go about Lock by value Signed-off-by: Alexander Morozov --- daemon/graphdriver/aufs/aufs.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/daemon/graphdriver/aufs/aufs.go b/daemon/graphdriver/aufs/aufs.go index 55cfd00c1..210f623e3 100644 --- a/daemon/graphdriver/aufs/aufs.go +++ b/daemon/graphdriver/aufs/aufs.go @@ -134,15 +134,15 @@ func supportsAufs() error { return ErrAufsNotSupported } -func (a Driver) rootPath() string { +func (a *Driver) rootPath() string { return a.root } -func (Driver) String() string { +func (*Driver) String() string { return "aufs" } -func (a Driver) Status() [][2]string { +func (a *Driver) Status() [][2]string { ids, _ := loadIds(path.Join(a.rootPath(), "layers")) return [][2]string{ {"Root Dir", a.rootPath()}, @@ -152,7 +152,7 @@ func (a Driver) Status() [][2]string { // Exists returns true if the given id is registered with // this driver -func (a Driver) Exists(id string) bool { +func (a *Driver) Exists(id string) bool { if _, err := os.Lstat(path.Join(a.rootPath(), "layers", id)); err != nil { return false } From 6b340e391cff68e6fc2c816cf15ffd67feddf029 Mon Sep 17 00:00:00 2001 From: Tianon Gravi Date: Fri, 12 Dec 2014 11:51:12 -0700 Subject: [PATCH 030/513] Fix a bashism and some minor bugs in nuke-graph-directory.sh Signed-off-by: Andrew Page --- contrib/nuke-graph-directory.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/contrib/nuke-graph-directory.sh b/contrib/nuke-graph-directory.sh index f44c45a17..8d12a9d64 100755 --- a/contrib/nuke-graph-directory.sh +++ b/contrib/nuke-graph-directory.sh @@ -50,9 +50,10 @@ for mount in $(awk '{ print $5 }' /proc/self/mountinfo); do done # now, let's go destroy individual btrfs subvolumes, if any exist -if command -v btrfs &> /dev/null; then +if command -v btrfs > /dev/null 2>&1; then root="$(df "$dir" | awk 'NR>1 { print $NF }')" - for subvol in $(btrfs subvolume list -o "$root" 2>/dev/null | awk -F' path ' '{ print $2 }'); do + root="${root#/}" # if root is "/", we want it to become "" + for subvol in $(btrfs subvolume list -o "$root/" 2>/dev/null | awk -F' path ' '{ print $2 }' | sort -r); do subvolDir="$root/$subvol" if dir_in_dir "$subvolDir" "$dir"; then ( set -x; btrfs subvolume delete "$subvolDir" ) From a7ae7fed7311551975d2bccb7417c328be3ea478 Mon Sep 17 00:00:00 2001 From: Alexander Morozov Date: Fri, 12 Dec 2014 10:58:56 -0800 Subject: [PATCH 031/513] Fix vet errors about formatting directives Signed-off-by: Alexander Morozov --- daemon/utils_test.go | 2 +- integration-cli/docker_cli_attach_test.go | 2 +- integration-cli/docker_cli_build_test.go | 2 +- integration-cli/docker_cli_exec_test.go | 2 +- integration-cli/docker_cli_run_test.go | 2 +- pkg/symlink/fs_test.go | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/daemon/utils_test.go b/daemon/utils_test.go index 8a2fa719e..28a15c64e 100644 --- a/daemon/utils_test.go +++ b/daemon/utils_test.go @@ -16,7 +16,7 @@ func TestMergeLxcConfig(t *testing.T) { out, err := mergeLxcConfIntoOptions(hostConfig) if err != nil { - t.Fatalf("Failed to merge Lxc Config ", err) + t.Fatalf("Failed to merge Lxc Config: %s", err) } cpuset := out[0] diff --git a/integration-cli/docker_cli_attach_test.go b/integration-cli/docker_cli_attach_test.go index 0530d3896..cf21cda58 100644 --- a/integration-cli/docker_cli_attach_test.go +++ b/integration-cli/docker_cli_attach_test.go @@ -122,7 +122,7 @@ func TestAttachTtyWithoutStdin(t *testing.T) { if out, _, err := runCommandWithOutput(cmd); err == nil { t.Fatal("attach should have failed") } else if !strings.Contains(out, expected) { - t.Fatal("attach failed with error %q: expected %q", out, expected) + t.Fatalf("attach failed with error %q: expected %q", out, expected) } }() diff --git a/integration-cli/docker_cli_build_test.go b/integration-cli/docker_cli_build_test.go index 0fd5b1363..bf64bf1ee 100644 --- a/integration-cli/docker_cli_build_test.go +++ b/integration-cli/docker_cli_build_test.go @@ -3799,7 +3799,7 @@ func TestBuildStderr(t *testing.T) { t.Fatal(err) } if stderr != "" { - t.Fatal("Stderr should have been empty, instead its: %q", stderr) + t.Fatalf("Stderr should have been empty, instead its: %q", stderr) } logDone("build - testing stderr") } diff --git a/integration-cli/docker_cli_exec_test.go b/integration-cli/docker_cli_exec_test.go index b07f215a3..747ad4ff8 100644 --- a/integration-cli/docker_cli_exec_test.go +++ b/integration-cli/docker_cli_exec_test.go @@ -339,7 +339,7 @@ func TestExecTtyWithoutStdin(t *testing.T) { if out, _, err := runCommandWithOutput(cmd); err == nil { t.Fatal("exec should have failed") } else if !strings.Contains(out, expected) { - t.Fatal("exec failed with error %q: expected %q", out, expected) + t.Fatalf("exec failed with error %q: expected %q", out, expected) } }() diff --git a/integration-cli/docker_cli_run_test.go b/integration-cli/docker_cli_run_test.go index 0b56f235f..5aa7b228f 100644 --- a/integration-cli/docker_cli_run_test.go +++ b/integration-cli/docker_cli_run_test.go @@ -2759,7 +2759,7 @@ func TestRunTtyWithPipe(t *testing.T) { if out, _, err := runCommandWithOutput(cmd); err == nil { t.Fatal("run should have failed") } else if !strings.Contains(out, expected) { - t.Fatal("run failed with error %q: expected %q", out, expected) + t.Fatalf("run failed with error %q: expected %q", out, expected) } }() diff --git a/pkg/symlink/fs_test.go b/pkg/symlink/fs_test.go index 6b2496c4e..89209484a 100644 --- a/pkg/symlink/fs_test.go +++ b/pkg/symlink/fs_test.go @@ -311,7 +311,7 @@ func TestFollowSymlinkEmpty(t *testing.T) { t.Fatal(err) } if res != wd { - t.Fatal("expected %q got %q", wd, res) + t.Fatalf("expected %q got %q", wd, res) } } From c7ff6bf69149bc5892633d95ebfacaf3ad36a008 Mon Sep 17 00:00:00 2001 From: Alexander Morozov Date: Fri, 12 Dec 2014 11:01:46 -0800 Subject: [PATCH 032/513] Fix vet errors about json tags for unexported fields Signed-off-by: Alexander Morozov --- daemon/graphdriver/devmapper/deviceset.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/daemon/graphdriver/devmapper/deviceset.go b/daemon/graphdriver/devmapper/deviceset.go index 71502a483..658000d75 100644 --- a/daemon/graphdriver/devmapper/deviceset.go +++ b/daemon/graphdriver/devmapper/deviceset.go @@ -45,15 +45,15 @@ type Transaction struct { } type DevInfo struct { - Hash string `json:"-"` - DeviceId int `json:"device_id"` - Size uint64 `json:"size"` - TransactionId uint64 `json:"transaction_id"` - Initialized bool `json:"initialized"` - devices *DeviceSet `json:"-"` + Hash string `json:"-"` + DeviceId int `json:"device_id"` + Size uint64 `json:"size"` + TransactionId uint64 `json:"transaction_id"` + Initialized bool `json:"initialized"` + devices *DeviceSet - mountCount int `json:"-"` - mountPath string `json:"-"` + mountCount int + mountPath string // The global DeviceSet lock guarantees that we serialize all // the calls to libdevmapper (which is not threadsafe), but we @@ -65,12 +65,12 @@ type DevInfo struct { // the global lock while holding the per-device locks all // device locks must be aquired *before* the device lock, and // multiple device locks should be aquired parent before child. - lock sync.Mutex `json:"-"` + lock sync.Mutex } type MetaData struct { Devices map[string]*DevInfo `json:"Devices"` - devicesLock sync.Mutex `json:"-"` // Protects all read/writes to Devices map + devicesLock sync.Mutex // Protects all read/writes to Devices map } type DeviceSet struct { From 4856ec075422a7926b62762749a7fbcc869efa99 Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Fri, 12 Dec 2014 11:15:31 -0800 Subject: [PATCH 033/513] Add test to enforce volume build content This tests ensures that the content from a dir within a build is carried over even if VOLUME for that dir is specified in the Dockerfile. This test ensures this long standing functionality. Signed-off-by: Michael Crosby --- integration-cli/docker_cli_build_test.go | 34 ++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/integration-cli/docker_cli_build_test.go b/integration-cli/docker_cli_build_test.go index 0fd5b1363..49905d204 100644 --- a/integration-cli/docker_cli_build_test.go +++ b/integration-cli/docker_cli_build_test.go @@ -3914,3 +3914,37 @@ RUN [ ! -e /injected ]`, logDone("build - xz host is being used") } + +func TestBuildVolumesRetainContents(t *testing.T) { + var ( + name = "testbuildvolumescontent" + expected = "some text" + ) + defer deleteImages(name) + ctx, err := fakeContext(` +FROM busybox +COPY content /foo/file +VOLUME /foo +CMD cat /foo/file`, + map[string]string{ + "content": expected, + }) + if err != nil { + t.Fatal(err) + } + defer ctx.Close() + + if _, err := buildImageFromContext(name, ctx, false); err != nil { + t.Fatal(err) + } + + out, _, err := runCommandWithOutput(exec.Command(dockerBinary, "run", "--rm", name)) + if err != nil { + t.Fatal(err) + } + if out != expected { + t.Fatalf("expected file contents for /foo/file to be %q but received %q", expected, out) + } + + logDone("build - volumes retain contents in build") +} From 0b3caf1f78c167239d7879d3bfd0d0d95a48a3d7 Mon Sep 17 00:00:00 2001 From: Dawn Chen Date: Wed, 10 Dec 2014 13:57:02 -0800 Subject: [PATCH 034/513] Using container-vm image alias instead of specific name with version. Signed-off-by: Dawn Chen --- docs/sources/installation/google.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/sources/installation/google.md b/docs/sources/installation/google.md index cbd1f8b63..1cee5290d 100644 --- a/docs/sources/installation/google.md +++ b/docs/sources/installation/google.md @@ -20,8 +20,7 @@ page_keywords: Docker, Docker documentation, installation, google, Google Comput (select a zone close to you and the desired instance size) $ gcloud compute instances create docker-playground \ - --image container-vm-v20140925 \ - --image-project google-containers \ + --image container-vm \ --zone us-central1-a \ --machine-type f1-micro From 54229fad933c91f5d890e14be389651e98f62e02 Mon Sep 17 00:00:00 2001 From: Jessica Frazelle Date: Fri, 12 Dec 2014 13:44:41 -0800 Subject: [PATCH 035/513] Add windows back. Docker-DCO-1.1-Signed-off-by: Jessica Frazelle (github: jfrazelle) --- Dockerfile | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index af559759b..cbddccac2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -68,9 +68,8 @@ RUN cd /usr/local/go/src && ./make.bash --no-clean 2>&1 ENV DOCKER_CROSSPLATFORMS \ linux/386 linux/arm \ darwin/amd64 darwin/386 \ - freebsd/amd64 freebsd/386 freebsd/arm -# windows is experimental for now -# windows/amd64 windows/386 + freebsd/amd64 freebsd/386 freebsd/arm \ + windows/amd64 windows/386 # (set an explicit GOARM of 5 for maximum compatibility) ENV GOARM 5 From 1b104ce7c3b18ad4bd38a819525a0d5ad78d3c85 Mon Sep 17 00:00:00 2001 From: Jessica Frazelle Date: Fri, 12 Dec 2014 12:14:26 -0800 Subject: [PATCH 036/513] Add master builds url to readme. Docker-DCO-1.1-Signed-off-by: Jessica Frazelle (github: jfrazelle) --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index c2273eb65..9b198f41c 100644 --- a/README.md +++ b/README.md @@ -187,6 +187,10 @@ documentation, please take a look at this [README.md](https://github.com/docker/ These instructions are probably not perfect, please let us know if anything feels wrong or incomplete. +Want to run Docker from a master build? You can can download +master builds at [master.dockerproject.com](https://master.dockerproject.com). +They are updated with each commit merged into the master branch. + ### Legal *Brought to you courtesy of our legal counsel. For more context, From afc262cc3ab68129104e3d799f8a2694f61aa68f Mon Sep 17 00:00:00 2001 From: Fred Lifton Date: Fri, 12 Dec 2014 17:18:21 -0800 Subject: [PATCH 037/513] Fixed errors in release notes Fixed a missing link and a few small formatting issues. Also deleted 1.3 notes as originally intended. Docker-DCO-1.1-Signed-off-by: Fred Lifton (github: fredlf) --- docs/sources/release-notes.md | 367 +--------------------------------- 1 file changed, 8 insertions(+), 359 deletions(-) diff --git a/docs/sources/release-notes.md b/docs/sources/release-notes.md index c6e4dff6e..395096150 100644 --- a/docs/sources/release-notes.md +++ b/docs/sources/release-notes.md @@ -13,7 +13,7 @@ desired version from the drop-down list at the top right of this page. This release provides a number of new features, but is mainly focused on bug fixes and improvements to platform stability and security. -For a complete list of patches, fixes, and other improvements, see +For a complete list of patches, fixes, and other improvements, see the [merge PR on GitHub](https://github.com/docker/docker/pull/9345). *New Features* @@ -57,371 +57,20 @@ were not sufficiently validated. This created a vulnerability to path traversal attacks wherein malicious images or repository spoofing could lead to graph corruption and manipulation. -Note that the above CVE's are also in Docker 1.3.3, which was released -concurrently with 1.4.0. - +> **Note:** the above CVEs are also patched in Docker 1.3.3, which was released +> concurrently with 1.4.0. + *Runtime fixes* - + * Fixed an issue that caused image archives to be read slowly. - + *Client fixes* * Fixed a regression related to STDIN redirection. * Fixed a regression involving `docker cp` when the current directory is the destination. -> **Note** +> **Note:** > Development history prior to version 1.0 can be found by -> searching in [GitHub](https://github.com/docker/docker). +> searching in the [Docker GitHub repo](https://github.com/docker/docker). -##Version 1.3.3 -(2014-12-11) - -This release fixes several security issues. In order to encourage immediate -upgrading, this release also patches some critical bugs. All users are highly -encouraged to upgrade as soon as possible. - -*Security fixes* - -Patches and changes were made to address the following vulnerabilities: - -* CVE-2014-9356: Path traversal during processing of absolute symlinks. -Absolute symlinks were not adequately checked for traversal which created a -vulnerability via image extraction and/or volume mounts. -* CVE-2014-9357: Escalation of privileges during decompression of LZMA (.xz) -archives. Docker 1.3.2 added `chroot` for archive extraction. This created a -vulnerability that could allow malicious images or builds to write files to the -host system and escape containerization, leading to privilege escalation. -* CVE-2014-9358: Path traversal and spoofing opportunities via image -identifiers. Image IDs passed either via `docker load` or registry communications -were not sufficiently validated. This created a vulnerability to path traversal -attacks wherein malicious images or repository spoofing could lead to graph -corruption and manipulation. - -*Runtime fixes* - -* Fixed an issue that cause image archives to be read slowly. - -*Client fixes* - -* Fixed a regression related to STDIN redirection. -* Fixed a regression involving `docker cp` when the current directory is the -destination. - -##Version 1.3.2 -(2014-11-24) - -This release fixes some bugs and addresses some security issues. We have also -made improvements to aspects of `docker run`. - -*Security fixes* - -Patches and changes were made to address CVE-2014-6407 and CVE-2014-6408. -Specifically, changes were made in order to: - -* Prevent host privilege escalation from an image extraction vulnerability (CVE-2014-6407). - -* Prevent container escalation from malicious security options applied to images (CVE-2014-6408). - -*Daemon fixes* - -The `--insecure-registry` flag of the `docker run` command has undergone -several refinements and additions. For details, please see the -[command-line reference](http://docs.docker.com/reference/commandline/cli/#run). - -* You can now specify a sub-net in order to set a range of registries which the Docker daemon will consider insecure. - -* By default, Docker now defines `localhost` as an insecure registry. - -* Registries can now be referenced using the Classless Inter-Domain Routing (CIDR) format. - -* When mirroring is enabled, the experimental registry v2 API is skipped. - -##Version 1.3.1 -(2014-10-28) - -This release fixes some bugs and addresses some security issues. - -*Security fixes* - -Patches and changes were made to address [CVE-2014-5277 and CVE-2014-3566](https://groups.google.com/forum/#!topic/docker-user/oYm0i3xShJU). -Specifically, changes were made to: - -* Prevent fallback to SSL protocols < TLS 1.0 for client, daemon and registry -* Secure HTTPS connection to registries with certificate verification and without HTTP fallback unless [`--insecure-registry`](/reference/commandline/cli/#run) is specified. - -*Runtime fixes* - -* Fixed issue where volumes would not be shared. - -*Client fixes* - -* Fixed issue with `--iptables=false` not automatically setting -`--ip-masq=false`. -* Fixed docker run output to non-TTY stdout. - -*Builder fixes* - -* Fixed escaping `$` for environment variables. -* Fixed issue with lowercase `onbuild` instruction in a `Dockerfile`. -* Restricted environment variable expansion to `ENV`, `ADD`, `COPY`, `WORKDIR`, -`EXPOSE`, `VOLUME`, and `USER` - -##Version 1.3.0 - -This version fixes a number of bugs and issues and adds new functions and other -improvements. The [GitHub 1.3milestone](https://github.com/docker/docker/issues?q=milestone%3A1.3.0+) has -more detailed information. Major additions and changes include: - -###New Features - -*New command: `docker exec`* - -The new `docker exec` command lets you run a process in an existing, active -container. The command has APIs for both the daemon and the client. With `docker -exec`, you'll be able to do things like add or remove devices from running -containers, debug running containers, and run commands that are not part of the -container's static specification. Details in the [command line reference](/reference/commandline/cli/#exec). - -*New command: `docker create`* - -Traditionally, the `docker run` command has been used to both create a container -and spawn a process to run it. The new `docker create` command breaks this -apart, letting you set up a container without actually starting it. This -provides more control over management of the container lifecycle, giving you the -ability to configure things like volumes or port mappings before the container -is started. For example, in a rapid-response scaling situation, you could use -`create` to prepare and stage ten containers in anticipation of heavy loads. -Details in the [command line reference](/reference/commandline/cli/#create). - -*Tech preview of new provenance features* - -This release offers a sneak peek at new image signing capabilities that are -currently under development. Soon, these capabilities will allow any image -author to sign their images to certify they have not been tampered with. For -this release, Official images are now signed by Docker, Inc. Not only does this -demonstrate the new functionality, we hope it will improve your confidence in -the security of Official images. Look for the blue ribbons denoting signed -images on the [Docker Hub](https://hub.docker.com/). The Docker Engine has been -updated to automatically verify that a given Official Repo has a current, valid -signature. When pulling a signed image, you'll see a message stating `the image -you are pulling has been verified`. If no valid signature is detected, Docker -Engine will fall back to pulling a regular, unsigned image. - -###Other improvements & changes* - -* We've added a new security options flag to the `docker run` command, -`--security-opt`, that lets you set SELinux and AppArmor labels and profiles. -This means you'll no longer have to use `docker run --privileged` on kernels -that support SE Linux or AppArmor. For more information, see the [command line -reference](/reference/commandline/cli/#run). - -* A new flag, `--add-host`, has been added to `docker run` that lets you add -lines to `/etc/hosts`. This allows you to specify different name resolution for -the container than it would get via DNS. For more information, see the [command -line reference](/reference/commandline/cli/#run). - -* You can now set a `DOCKER_TLS_VERIFY` environment variable to secure -connections by default (rather than having to pass the `--tlsverify` flag on -every call). For more information, see the [https guide](/articles/https). - -* Three security issues have been addressed in this release: [CVE-2014-5280, -CVE-2014-5270, and -CVE-2014-5282](https://groups.google.com/forum/#!msg/docker-announce/aQoVmQlcE0A/smPuBNYf8VwJ). - -##Version 1.2.0 - -This version fixes a number of bugs and issues and adds new functions and other -improvements. These include: - -###New Features - -*New restart policies* - -We added a `--restart flag` to `docker run` to specify a restart policy for your -container. Currently, there are three policies available: - -* `no` – Do not restart the container if it dies. (default) * `on-failure` – -Restart the container if it exits with a non-zero exit code. This can also -accept an optional maximum restart count (e.g. `on-failure:5`). * `always` – -Always restart the container no matter what exit code is returned. This -deprecates the `--restart` flag on the Docker daemon. - -*New flags for `docker run`: `--cap-add` and `--cap-drop`* - -In previous releases, Docker containers could either be given complete -capabilities or they could all follow a whitelist of allowed capabilities while -dropping all others. Further, using `--privileged` would grant all capabilities -inside a container, rather than applying a whitelist. This was not recommended -for production use because it’s really unsafe; it’s as if you were directly in -the host. - -This release introduces two new flags for `docker run`, `--cap-add` and -`--cap-drop`, that give you fine-grain control over the specific capabilities -you want grant to a particular container. - -*New `--device` flag for `docker run`* - -Previously, you could only use devices inside your containers by bind mounting -them (with `-v`) in a `--privileged` container. With this release, we introduce -the `--device flag` to `docker run` which lets you use a device without -requiring a privileged container. - -*Writable `/etc/hosts`, `/etc/hostname` and `/etc/resolv.conf`* - -You can now edit `/etc/hosts`, `/etc/hostname` and `/etc/resolve.conf` in a -running container. This is useful if you need to install BIND or other services -that might override one of those files. - -Note, however, that changes to these files are not saved when running `docker -build` and so will not be preserved in the resulting image. The changes will -only “stick” in a running container. - -*Docker proxy in a separate process* - -The Docker userland proxy that routes outbound traffic to your containers now -has its own separate process (one process per connection). This greatly reduces -the load on the daemon, which increases stability and efficiency. - -###Other improvements & changes - -* When using `docker rm -f`, Docker now kills the container (instead of stopping -it) before removing it . If you intend to stop the container cleanly, you can -use `docker stop`. - -* Added support for IPv6 addresses in `--dns` - -* Added search capability in private registries - -##Version 1.1.0 - -###New Features - -*`.dockerignore` support* - -You can now add a `.dockerignore` file next to your `Dockerfile` and Docker will -ignore files and directories specified in that file when sending the build -context to the daemon. Example: -https://github.com/docker/docker/blob/master/.dockerignore - -*Pause containers during commit* - -Doing a commit on a running container was not recommended because you could end -up with files in an inconsistent state (for example, if they were being written -during the commit). Containers are now paused when a commit is made to them. You -can disable this feature by doing a `docker commit --pause=false ` - -*Tailing logs* - -You can now tail the logs of a container. For example, you can get the last ten -lines of a log by using `docker logs --tail 10 `. You can also -follow the logs of a container without having to read the whole log file with -`docker logs --tail 0 -f `. - -*Allow a tar file as context for docker build* - -You can now pass a tar archive to `docker build` as context. This can be used to -automate docker builds, for example: `cat context.tar | docker build -` or -`docker run builder_image | docker build -` - -*Bind mounting your whole filesystem in a container* - -`/` is now allowed as source of `--volumes`. This means you can bind-mount your -whole system in a container if you need to. For example: `docker run -v -/:/my_host ubuntu:ro ls /my_host`. However, it is now forbidden to mount to /. - - -###Other Improvements & Changes - -* Port allocation has been improved. In the previous release, Docker could -prevent you from starting a container with previously allocated ports which -seemed to be in use when in fact they were not. This has been fixed. - -* A bug in `docker save` was introduced in the last release. The `docker save` -command could produce images with invalid metadata. The command now produces -images with correct metadata. - -* Running `docker inspect` in a container now returns which containers it is -linked to. - -* Parsing of the `docker commit` flag has improved validation, to better prevent -you from committing an image with a name such as `-m`. Image names with dashes -in them potentially conflict with command line flags. - -* The API now has Improved status codes for `start` and `stop`. Trying to start -a running container will now return a 304 error. - -* Performance has been improved overall. Starting the daemon is faster than in -previous releases. The daemon’s performance has also been improved when it is -working with large numbers of images and containers. - -* Fixed an issue with white-spaces and multi-lines in Dockerfiles. - -##Version 1.1.0 - -###New Features - -*`.dockerignore` support* - -You can now add a `.dockerignore` file next to your `Dockerfile` and Docker will -ignore files and directories specified in that file when sending the build -context to the daemon. Example: -https://github.com/dotcloud/docker/blob/master/.dockerignore - -*Pause containers during commit* - -Doing a commit on a running container was not recommended because you could end -up with files in an inconsistent state (for example, if they were being written -during the commit). Containers are now paused when a commit is made to them. You -can disable this feature by doing a `docker commit --pause=false ` - -*Tailing logs* - -You can now tail the logs of a container. For example, you can get the last ten -lines of a log by using `docker logs --tail 10 `. You can also -follow the logs of a container without having to read the whole log file with -`docker logs --tail 0 -f `. - -*Allow a tar file as context for docker build* - -You can now pass a tar archive to `docker build` as context. This can be used to -automate docker builds, for example: `cat context.tar | docker build -` or -`docker run builder_image | docker build -` - -*Bind mounting your whole filesystem in a container* - -`/` is now allowed as source of `--volumes`. This means you can bind-mount your -whole system in a container if you need to. For example: `docker run -v -/:/my_host ubuntu:ro ls /my_host`. However, it is now forbidden to mount to /. - - -###Other Improvements & Changes - -* Port allocation has been improved. In the previous release, Docker could -prevent you from starting a container with previously allocated ports which -seemed to be in use when in fact they were not. This has been fixed. - -* A bug in `docker save` was introduced in the last release. The `docker save` -command could produce images with invalid metadata. The command now produces -images with correct metadata. - -* Running `docker inspect` in a container now returns which containers it is -linked to. - -* Parsing of the `docker commit` flag has improved validation, to better prevent -you from committing an image with a name such as `-m`. Image names with dashes -in them potentially conflict with command line flags. - -* The API now has Improved status codes for `start` and `stop`. Trying to start -a running container will now return a 304 error. - -* Performance has been improved overall. Starting the daemon is faster than in -previous releases. The daemon’s performance has also been improved when it is -working with large numbers of images and containers. - -* Fixed an issue with white-spaces and multi-lines in Dockerfiles. - -##Version 1.0.0 - -First production-ready release. Prior development history can be found by -searching in [GitHub](https://github.com/docker/docker). From 9256578f62ffe885befa3ef2dbdd9ec0bea1d7e8 Mon Sep 17 00:00:00 2001 From: Jean-Tiare Le Bigot Date: Sat, 13 Dec 2014 13:00:59 +0100 Subject: [PATCH 038/513] Set HTTP upgrade headers when hijacking connection Signed-off-by: Jean-Tiare Le Bigot --- api/client/hijack.go | 2 ++ api/server/server.go | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/api/client/hijack.go b/api/client/hijack.go index 617a0b3f6..bd302a764 100644 --- a/api/client/hijack.go +++ b/api/client/hijack.go @@ -135,6 +135,8 @@ func (cli *DockerCli) hijack(method, path string, setRawTerminal bool, in io.Rea } req.Header.Set("User-Agent", "Docker-Client/"+dockerversion.VERSION) req.Header.Set("Content-Type", "plain/text") + req.Header.Set("Connection", "Upgrade") + req.Header.Set("Upgrade", "tcp") req.Host = cli.addr dial, err := cli.dial() diff --git a/api/server/server.go b/api/server/server.go index 629ad0ba0..d6e3d9098 100644 --- a/api/server/server.go +++ b/api/server/server.go @@ -887,7 +887,7 @@ func postContainersAttach(eng *engine.Engine, version version.Version, w http.Re var errStream io.Writer - fmt.Fprintf(outStream, "HTTP/1.1 200 OK\r\nContent-Type: application/vnd.docker.raw-stream\r\n\r\n") + fmt.Fprintf(outStream, "HTTP/1.1 101 UPGRADED\r\nContent-Type: application/vnd.docker.raw-stream\r\nConnection: Upgrade\r\nUpgrade: tcp\r\n\r\n") if c.GetSubEnv("Config") != nil && !c.GetSubEnv("Config").GetBool("Tty") && version.GreaterThanOrEqualTo("1.6") { errStream = stdcopy.NewStdWriter(outStream, stdcopy.Stderr) @@ -1137,7 +1137,7 @@ func postContainerExecStart(eng *engine.Engine, version version.Version, w http. var errStream io.Writer - fmt.Fprintf(outStream, "HTTP/1.1 200 OK\r\nContent-Type: application/vnd.docker.raw-stream\r\n\r\n") + fmt.Fprintf(outStream, "HTTP/1.1 101 UPGRADED\r\nContent-Type: application/vnd.docker.raw-stream\r\nConnection: Upgrade\r\nUpgrade: tcp\r\n\r\n") if !job.GetenvBool("Tty") && version.GreaterThanOrEqualTo("1.6") { errStream = stdcopy.NewStdWriter(outStream, stdcopy.Stderr) outStream = stdcopy.NewStdWriter(outStream, stdcopy.Stdout) From be27d97118764db994fbaf3632225a691c7418fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Gonz=C3=A1lez?= Date: Mon, 3 Nov 2014 00:25:44 +0000 Subject: [PATCH 039/513] Log when truncindex.Get returns >1 container When the user is not using the full has to retrieve a container it's possible that we find conflicts with the ids of other containers. At the moment it's just failing saying that it can not find a container, but it doesn't say why. Adding a small log saying that duplicates where found is going to help the user. Closes #8098 Signed-off-by: Alex Gonzalez --- daemon/daemon.go | 12 +++++++++++- pkg/truncindex/truncindex.go | 7 ++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/daemon/daemon.go b/daemon/daemon.go index a2e6a79bd..1b7f0e5bb 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -151,12 +151,22 @@ func (daemon *Daemon) Install(eng *engine.Engine) error { // Get looks for a container by the specified ID or name, and returns it. // If the container is not found, or if an error occurs, nil is returned. func (daemon *Daemon) Get(name string) *Container { - if id, err := daemon.idIndex.Get(name); err == nil { + var ( + id string + err error + ) + + if id, err = daemon.idIndex.Get(name); err == nil { return daemon.containers.Get(id) } + if c, _ := daemon.GetByName(name); c != nil { return c } + + if err == truncindex.ErrDuplicateID { + log.Errorf("Short ID %s is ambiguous: please retry with more characters or use the full ID.\n", name) + } return nil } diff --git a/pkg/truncindex/truncindex.go b/pkg/truncindex/truncindex.go index c5b71752b..eec559730 100644 --- a/pkg/truncindex/truncindex.go +++ b/pkg/truncindex/truncindex.go @@ -11,8 +11,9 @@ import ( var ( // ErrNoID is thrown when attempting to use empty prefixes - ErrNoID = errors.New("prefix can't be empty") - errDuplicateID = errors.New("multiple IDs were found") + ErrNoID = errors.New("prefix can't be empty") + // ErrDuplicateID is thrown when a duplicated id was found + ErrDuplicateID = errors.New("multiple IDs were found") ) func init() { @@ -98,7 +99,7 @@ func (idx *TruncIndex) Get(s string) (string, error) { if id != "" { // we haven't found the ID if there are two or more IDs id = "" - return errDuplicateID + return ErrDuplicateID } id = string(prefix) return nil From 228404860ca12ea5fb09098bc2a1a16cc96c7e10 Mon Sep 17 00:00:00 2001 From: Mary Anthony Date: Sat, 13 Dec 2014 14:18:46 -0800 Subject: [PATCH 040/513] Clarify Mac OS X experience. Signed-off-by: Mary Anthony --- docs/sources/contributing/devenvironment.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/docs/sources/contributing/devenvironment.md b/docs/sources/contributing/devenvironment.md index f39dec670..42a73827b 100644 --- a/docs/sources/contributing/devenvironment.md +++ b/docs/sources/contributing/devenvironment.md @@ -40,22 +40,24 @@ with the name of branch or revision number. ## Build the Environment -This following command will build a development environment using the -Dockerfile in the current directory. Essentially, it will install all +This following command builds a development environment using the +Dockerfile in the current directory. Essentially, it installs all the build and runtime dependencies necessary to build and test Docker. -This command will take some time to complete when you first execute it. +Your first build will take some time to complete. On Linux systems: $ sudo make build + +On Mac OS X, from within the `boot2docker` shell: + + $ make build + +> **Note**: +> On Mac OS X, **do not** build Docker make targets such as `build`, `binary`, and `test` +> under root (sudo). If the build is successful, congratulations! You have produced a clean build of docker, neatly encapsulated in a standard build environment. -> **Note**: -> On Mac OS X, make targets such as `build`, `binary`, and `test` -> must **not** be built under root. So, for example, instead of the above -> command, issue: -> -> $ make build ## Build the Docker Binary From 70a2b64ef2e31aef84c39b979686e9194aee22a6 Mon Sep 17 00:00:00 2001 From: Alexandr Morozov Date: Sat, 13 Dec 2014 21:45:02 -0800 Subject: [PATCH 041/513] Remove TestRunErrorBindNonExistingSource This test tests nothing because of error in cmd, where "echo 'should fail'" passed as binary. Also this test directly contradicts documentation and current daemon behavior. Fixes #7826 Signed-off-by: Alexandr Morozov --- integration/commands_test.go | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/integration/commands_test.go b/integration/commands_test.go index aa21791b5..09a16c0f7 100644 --- a/integration/commands_test.go +++ b/integration/commands_test.go @@ -507,27 +507,3 @@ func TestRunAutoRemove(t *testing.T) { t.Fatalf("failed to remove container automatically: container %s still exists", temporaryContainerID) } } - -// Expected behaviour: error out when attempting to bind mount non-existing source paths -func TestRunErrorBindNonExistingSource(t *testing.T) { - key, err := libtrust.GenerateECP256PrivateKey() - if err != nil { - t.Fatal(err) - } - - cli := client.NewDockerCli(nil, nil, ioutil.Discard, key, testDaemonProto, testDaemonAddr, nil) - defer cleanup(globalEngine, t) - - c := make(chan struct{}) - go func() { - defer close(c) - // This check is made at runtime, can't be "unit tested" - if err := cli.CmdRun("-v", "/i/dont/exist:/tmp", unitTestImageID, "echo 'should fail'"); err == nil { - t.Fatal("should have failed to run when using /i/dont/exist as a source for the bind mount") - } - }() - - setTimeout(t, "CmdRun timed out", 5*time.Second, func() { - <-c - }) -} From 4d7359f63db200298f9d07f6434a7a4b16c25c88 Mon Sep 17 00:00:00 2001 From: Alexandr Morozov Date: Sun, 14 Dec 2014 18:09:41 -0800 Subject: [PATCH 042/513] Remove TestRunExitOnStdinClose Because this is already tested by TestRunExitOnStdinClose in integration-cli/docker_cli_run_test.go Signed-off-by: Alexandr Morozov --- integration/commands_test.go | 49 ------------------------------------ 1 file changed, 49 deletions(-) diff --git a/integration/commands_test.go b/integration/commands_test.go index aa21791b5..53aa3ced8 100644 --- a/integration/commands_test.go +++ b/integration/commands_test.go @@ -114,55 +114,6 @@ func assertPipe(input, output string, r io.Reader, w io.Writer, count int) error return nil } -// Expected behaviour: the process dies when the client disconnects -func TestRunDisconnect(t *testing.T) { - - stdin, stdinPipe := io.Pipe() - stdout, stdoutPipe := io.Pipe() - key, err := libtrust.GenerateECP256PrivateKey() - if err != nil { - t.Fatal(err) - } - - cli := client.NewDockerCli(stdin, stdoutPipe, ioutil.Discard, key, testDaemonProto, testDaemonAddr, nil) - defer cleanup(globalEngine, t) - - c1 := make(chan struct{}) - go func() { - // We're simulating a disconnect so the return value doesn't matter. What matters is the - // fact that CmdRun returns. - cli.CmdRun("-i", unitTestImageID, "/bin/cat") - close(c1) - }() - - setTimeout(t, "Read/Write assertion timed out", 2*time.Second, func() { - if err := assertPipe("hello\n", "hello", stdout, stdinPipe, 150); err != nil { - t.Fatal(err) - } - }) - - // Close pipes (simulate disconnect) - if err := closeWrap(stdin, stdinPipe, stdout, stdoutPipe); err != nil { - t.Fatal(err) - } - - // as the pipes are close, we expect the process to die, - // therefore CmdRun to unblock. Wait for CmdRun - setTimeout(t, "Waiting for CmdRun timed out", 2*time.Second, func() { - <-c1 - }) - - // Client disconnect after run -i should cause stdin to be closed, which should - // cause /bin/cat to exit. - setTimeout(t, "Waiting for /bin/cat to exit timed out", 2*time.Second, func() { - container := globalDaemon.List()[0] - container.WaitStop(-1 * time.Second) - if container.IsRunning() { - t.Fatalf("/bin/cat is still running after closing stdin") - } - }) -} - // TestRunDetach checks attaching and detaching with the escape sequence. func TestRunDetach(t *testing.T) { stdout, stdoutPipe := io.Pipe() From bc4edbbe712a2d0fb839747814933916177daa74 Mon Sep 17 00:00:00 2001 From: Liang-Chi Hsieh Date: Mon, 15 Dec 2014 17:26:00 +0800 Subject: [PATCH 043/513] Fix incorrect error type. Signed-off-by: Liang-Chi Hsieh --- integration/https_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration/https_test.go b/integration/https_test.go index 0705dc812..fa61398c0 100644 --- a/integration/https_test.go +++ b/integration/https_test.go @@ -90,7 +90,7 @@ func TestHttpsInfoRogueServerCert(t *testing.T) { } if !strings.Contains(err.Error(), errCaUnknown) { - t.Fatalf("Expected error: %s, got instead: %s", errBadCertificate, err) + t.Fatalf("Expected error: %s, got instead: %s", errCaUnknown, err) } }) From d942c59b696d16def85f6b65ae65c176f66a5562 Mon Sep 17 00:00:00 2001 From: Doug Davis Date: Thu, 11 Dec 2014 04:56:21 -0800 Subject: [PATCH 044/513] Wrap strings that could look like ints in quotes When we use the engine/env object we can run into a situation where a string is passed in as the value but later on when we json serialize the name/value pairs, because the string is made up of just numbers it appears as an integer and not a string - meaning no quotes. This can cause parsing issues for clients. I tried to find all spots where we call env.Set() and the type of the name being set might end up having a value that could look like an int (like author). In those cases I switched it to use env.SetJson() instead because that will wrap it in quotes. One interesting thing to note about the testcase that I modified is that the escaped quotes should have been there all along and we were incorrectly letting it thru. If you look at the metadata stored for that resource you can see the quotes were escaped and we lost them during the serialization steps because of the env.Set() stuff. The use of env is probably not the best way to do all of this. Closes: #9602 Signed-off-by: Doug Davis --- daemon/image_delete.go | 2 +- daemon/info.go | 4 ++-- daemon/inspect.go | 6 +++--- daemon/list.go | 4 ++-- graph/history.go | 2 +- graph/list.go | 8 ++++---- graph/service.go | 16 +++++++-------- integration-cli/docker_cli_build_test.go | 26 ++++++++++++++++++++++-- 8 files changed, 45 insertions(+), 23 deletions(-) diff --git a/daemon/image_delete.go b/daemon/image_delete.go index b0b0c3a02..f39b0dd61 100644 --- a/daemon/image_delete.go +++ b/daemon/image_delete.go @@ -113,7 +113,7 @@ func (daemon *Daemon) DeleteImage(eng *engine.Engine, name string, imgs *engine. return err } out := &engine.Env{} - out.Set("Deleted", img.ID) + out.SetJson("Deleted", img.ID) imgs.Add(out) eng.Job("log", "delete", img.ID, "").Run() if img.Parent != "" && !noprune { diff --git a/daemon/info.go b/daemon/info.go index 518722b6c..bf7ec9968 100644 --- a/daemon/info.go +++ b/daemon/info.go @@ -56,7 +56,7 @@ func (daemon *Daemon) CmdInfo(job *engine.Job) engine.Status { return job.Error(err) } v := &engine.Env{} - v.Set("ID", daemon.ID) + v.SetJson("ID", daemon.ID) v.SetInt("Containers", len(daemon.List())) v.SetInt("Images", imgcount) v.Set("Driver", daemon.GraphDriver().String()) @@ -78,7 +78,7 @@ func (daemon *Daemon) CmdInfo(job *engine.Job) engine.Status { v.SetInt64("MemTotal", meminfo.MemTotal) v.Set("DockerRootDir", daemon.Config().Root) if hostname, err := os.Hostname(); err == nil { - v.Set("Name", hostname) + v.SetJson("Name", hostname) } v.SetList("Labels", daemon.Config().Labels) if _, err := v.WriteTo(job.Stdout); err != nil { diff --git a/daemon/inspect.go b/daemon/inspect.go index d8397127c..c930cdd7f 100644 --- a/daemon/inspect.go +++ b/daemon/inspect.go @@ -29,18 +29,18 @@ func (daemon *Daemon) ContainerInspect(job *engine.Job) engine.Status { } out := &engine.Env{} - out.Set("Id", container.ID) + out.SetJson("Id", container.ID) out.SetAuto("Created", container.Created) out.SetJson("Path", container.Path) out.SetList("Args", container.Args) out.SetJson("Config", container.Config) out.SetJson("State", container.State) - out.Set("Image", container.Image) + out.SetJson("Image", container.Image) out.SetJson("NetworkSettings", container.NetworkSettings) out.Set("ResolvConfPath", container.ResolvConfPath) out.Set("HostnamePath", container.HostnamePath) out.Set("HostsPath", container.HostsPath) - out.Set("Name", container.Name) + out.SetJson("Name", container.Name) out.SetInt("RestartCount", container.RestartCount) out.Set("Driver", container.Driver) out.Set("ExecDriver", container.ExecDriver) diff --git a/daemon/list.go b/daemon/list.go index 29d7298fc..188a9861e 100644 --- a/daemon/list.go +++ b/daemon/list.go @@ -114,9 +114,9 @@ func (daemon *Daemon) Containers(job *engine.Job) engine.Status { } displayed++ out := &engine.Env{} - out.Set("Id", container.ID) + out.SetJson("Id", container.ID) out.SetList("Names", names[container.ID]) - out.Set("Image", daemon.Repositories().ImageName(container.Image)) + out.SetJson("Image", daemon.Repositories().ImageName(container.Image)) if len(container.Args) > 0 { args := []string{} for _, arg := range container.Args { diff --git a/graph/history.go b/graph/history.go index 2030c4c78..356340673 100644 --- a/graph/history.go +++ b/graph/history.go @@ -31,7 +31,7 @@ func (s *TagStore) CmdHistory(job *engine.Job) engine.Status { outs := engine.NewTable("Created", 0) err = foundImage.WalkHistory(func(img *image.Image) error { out := &engine.Env{} - out.Set("Id", img.ID) + out.SetJson("Id", img.ID) out.SetInt64("Created", img.Created.Unix()) out.Set("CreatedBy", strings.Join(img.ContainerConfig.Cmd, " ")) out.SetList("Tags", lookupMap[img.ID]) diff --git a/graph/list.go b/graph/list.go index 0e0e97e44..63a906b9a 100644 --- a/graph/list.go +++ b/graph/list.go @@ -62,9 +62,9 @@ func (s *TagStore) CmdImages(job *engine.Job) engine.Status { delete(allImages, id) if filt_tagged { out := &engine.Env{} - out.Set("ParentId", image.Parent) + out.SetJson("ParentId", image.Parent) out.SetList("RepoTags", []string{fmt.Sprintf("%s:%s", name, tag)}) - out.Set("Id", image.ID) + out.SetJson("Id", image.ID) out.SetInt64("Created", image.Created.Unix()) out.SetInt64("Size", image.Size) out.SetInt64("VirtualSize", image.GetParentsSize(0)+image.Size) @@ -85,9 +85,9 @@ func (s *TagStore) CmdImages(job *engine.Job) engine.Status { if job.Getenv("filter") == "" { for _, image := range allImages { out := &engine.Env{} - out.Set("ParentId", image.Parent) + out.SetJson("ParentId", image.Parent) out.SetList("RepoTags", []string{":"}) - out.Set("Id", image.ID) + out.SetJson("Id", image.ID) out.SetInt64("Created", image.Created.Unix()) out.SetInt64("Size", image.Size) out.SetInt64("VirtualSize", image.GetParentsSize(0)+image.Size) diff --git a/graph/service.go b/graph/service.go index a27c9a8e3..2858d9b3e 100644 --- a/graph/service.go +++ b/graph/service.go @@ -109,12 +109,12 @@ func (s *TagStore) CmdGet(job *engine.Job) engine.Status { // metaphor, in practice people either ignore it or use it as a // generic description field which it isn't. On deprecation shortlist. res.SetAuto("Created", img.Created) - res.Set("Author", img.Author) + res.SetJson("Author", img.Author) res.Set("Os", img.OS) res.Set("Architecture", img.Architecture) res.Set("DockerVersion", img.DockerVersion) - res.Set("Id", img.ID) - res.Set("Parent", img.Parent) + res.SetJson("Id", img.ID) + res.SetJson("Parent", img.Parent) } res.WriteTo(job.Stdout) return engine.StatusOK @@ -137,14 +137,14 @@ func (s *TagStore) CmdLookup(job *engine.Job) engine.Status { } out := &engine.Env{} - out.Set("Id", image.ID) - out.Set("Parent", image.Parent) - out.Set("Comment", image.Comment) + out.SetJson("Id", image.ID) + out.SetJson("Parent", image.Parent) + out.SetJson("Comment", image.Comment) out.SetAuto("Created", image.Created) - out.Set("Container", image.Container) + out.SetJson("Container", image.Container) out.SetJson("ContainerConfig", image.ContainerConfig) out.Set("DockerVersion", image.DockerVersion) - out.Set("Author", image.Author) + out.SetJson("Author", image.Author) out.SetJson("Config", image.Config) out.Set("Architecture", image.Architecture) out.Set("Os", image.OS) diff --git a/integration-cli/docker_cli_build_test.go b/integration-cli/docker_cli_build_test.go index 0fd5b1363..768fb8a15 100644 --- a/integration-cli/docker_cli_build_test.go +++ b/integration-cli/docker_cli_build_test.go @@ -2922,13 +2922,35 @@ docker.com>" t.Fatal(err) } - if res != "Docker IO " { - t.Fatal("Parsed string did not match the escaped string") + if res != "\"Docker IO \"" { + t.Fatalf("Parsed string did not match the escaped string. Got: %q", res) } logDone("build - validate escaping whitespace") } +func TestBuildVerifyIntString(t *testing.T) { + // Verify that strings that look like ints are still passed as strings + name := "testbuildstringing" + defer deleteImages(name) + + _, err := buildImage(name, ` + FROM busybox + MAINTAINER 123 + `, true) + + out, rc, err := runCommandWithOutput(exec.Command(dockerBinary, "inspect", name)) + if rc != 0 || err != nil { + t.Fatalf("Unexcepted error from inspect: rc: %v err: %v", rc, err) + } + + if !strings.Contains(out, "\"123\"") { + t.Fatalf("Output does not contain the int as a string:\n%s", out) + } + + logDone("build - verify int/strings as strings") +} + func TestBuildDockerignore(t *testing.T) { name := "testbuilddockerignore" defer deleteImages(name) From 37bdb05615763f94f7877cce3426752d43b48ff7 Mon Sep 17 00:00:00 2001 From: Kamil Domanski Date: Sat, 13 Dec 2014 13:58:21 +0100 Subject: [PATCH 045/513] Add container list filtering to API docs. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kamil Domański (github: kdomanski) --- docs/sources/reference/api/docker_remote_api_v1.14.md | 1 + docs/sources/reference/api/docker_remote_api_v1.15.md | 1 + docs/sources/reference/api/docker_remote_api_v1.16.md | 1 + 3 files changed, 3 insertions(+) diff --git a/docs/sources/reference/api/docker_remote_api_v1.14.md b/docs/sources/reference/api/docker_remote_api_v1.14.md index a5392f3bc..df5b63672 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.14.md +++ b/docs/sources/reference/api/docker_remote_api_v1.14.md @@ -84,6 +84,7 @@ Query Parameters: - **since** – Show only containers created since Id, include non-running ones. - **before** – Show only containers created before Id, include non-running ones. - **size** – 1/True/true or 0/False/false, Show the containers sizes +- **filters** - a json encoded value of the filters (a map[string][]string) to process on the containers list. Status Codes: diff --git a/docs/sources/reference/api/docker_remote_api_v1.15.md b/docs/sources/reference/api/docker_remote_api_v1.15.md index ae265653a..c36e3ed21 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.15.md +++ b/docs/sources/reference/api/docker_remote_api_v1.15.md @@ -88,6 +88,7 @@ Query Parameters: non-running ones. - **size** – 1/True/true or 0/False/false, Show the containers sizes +- **filters** - a json encoded value of the filters (a map[string][]string) to process on the containers list. Status Codes: diff --git a/docs/sources/reference/api/docker_remote_api_v1.16.md b/docs/sources/reference/api/docker_remote_api_v1.16.md index 72f5519e1..959b95640 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.16.md +++ b/docs/sources/reference/api/docker_remote_api_v1.16.md @@ -88,6 +88,7 @@ Query Parameters: non-running ones. - **size** – 1/True/true or 0/False/false, Show the containers sizes +- **filters** - a json encoded value of the filters (a map[string][]string) to process on the containers list. Status Codes: From 51da97628a0ffb2a1c4be2c4d1bbcb09537d6c8d Mon Sep 17 00:00:00 2001 From: Kamil Domanski Date: Mon, 15 Dec 2014 16:28:42 +0100 Subject: [PATCH 046/513] Add available filters for containers, images and events to API docs. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kamil Domański (github: kdomanski) --- .../sources/reference/api/docker_remote_api_v1.12.md | 3 ++- .../sources/reference/api/docker_remote_api_v1.13.md | 3 ++- .../sources/reference/api/docker_remote_api_v1.14.md | 7 +++++-- .../sources/reference/api/docker_remote_api_v1.15.md | 7 +++++-- .../sources/reference/api/docker_remote_api_v1.16.md | 12 +++++++++--- 5 files changed, 23 insertions(+), 9 deletions(-) diff --git a/docs/sources/reference/api/docker_remote_api_v1.12.md b/docs/sources/reference/api/docker_remote_api_v1.12.md index f38b018ef..92736a188 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.12.md +++ b/docs/sources/reference/api/docker_remote_api_v1.12.md @@ -748,7 +748,8 @@ Query Parameters:   - **all** – 1/True/true or 0/False/false, default false -- **filters** – a JSON encoded value of the filters (a map[string][]string) to process on the images list. +- **filters** – a json encoded value of the filters (a map[string][]string) to process on the images list. Available filters: + - dangling=true diff --git a/docs/sources/reference/api/docker_remote_api_v1.13.md b/docs/sources/reference/api/docker_remote_api_v1.13.md index f5ca931fe..367ead583 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.13.md +++ b/docs/sources/reference/api/docker_remote_api_v1.13.md @@ -739,7 +739,8 @@ Status Codes: Query Parameters: - **all** – 1/True/true or 0/False/false, default false -- **filters** – a json encoded value of the filters (a map[string][string]) to process on the images list. +- **filters** – a json encoded value of the filters (a map[string][]string) to process on the images list. Available filters: + - dangling=true ### Create an image diff --git a/docs/sources/reference/api/docker_remote_api_v1.14.md b/docs/sources/reference/api/docker_remote_api_v1.14.md index df5b63672..270d10b63 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.14.md +++ b/docs/sources/reference/api/docker_remote_api_v1.14.md @@ -84,7 +84,9 @@ Query Parameters: - **since** – Show only containers created since Id, include non-running ones. - **before** – Show only containers created before Id, include non-running ones. - **size** – 1/True/true or 0/False/false, Show the containers sizes -- **filters** - a json encoded value of the filters (a map[string][]string) to process on the containers list. +- **filters** - a json encoded value of the filters (a map[string][]string) to process on the containers list. Available filters: + - exited=<int> -- containers with exit code of <int> + - status=(restarting|running|paused|exited) Status Codes: @@ -745,7 +747,8 @@ Status Codes: Query Parameters: - **all** – 1/True/true or 0/False/false, default false -- **filters** – a json encoded value of the filters (a map[string][string]) to process on the images list. +- **filters** – a json encoded value of the filters (a map[string][]string) to process on the images list. Available filters: + - dangling=true ### Create an image diff --git a/docs/sources/reference/api/docker_remote_api_v1.15.md b/docs/sources/reference/api/docker_remote_api_v1.15.md index c36e3ed21..6bd867e11 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.15.md +++ b/docs/sources/reference/api/docker_remote_api_v1.15.md @@ -88,7 +88,9 @@ Query Parameters: non-running ones. - **size** – 1/True/true or 0/False/false, Show the containers sizes -- **filters** - a json encoded value of the filters (a map[string][]string) to process on the containers list. +- **filters** - a json encoded value of the filters (a map[string][]string) to process on the containers list. Available filters: + - exited=<int> -- containers with exit code of <int> + - status=(restarting|running|paused|exited) Status Codes: @@ -885,7 +887,8 @@ Status Codes: Query Parameters: - **all** – 1/True/true or 0/False/false, default false -- **filters** – a json encoded value of the filters (a map[string][]string) to process on the images list. +- **filters** – a json encoded value of the filters (a map[string][]string) to process on the images list. Available filters: + - dangling=true ### Create an image diff --git a/docs/sources/reference/api/docker_remote_api_v1.16.md b/docs/sources/reference/api/docker_remote_api_v1.16.md index 959b95640..ade7b09b2 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.16.md +++ b/docs/sources/reference/api/docker_remote_api_v1.16.md @@ -88,7 +88,9 @@ Query Parameters: non-running ones. - **size** – 1/True/true or 0/False/false, Show the containers sizes -- **filters** - a json encoded value of the filters (a map[string][]string) to process on the containers list. +- **filters** - a json encoded value of the filters (a map[string][]string) to process on the containers list. Available filters: + - exited=<int> -- containers with exit code of <int> + - status=(restarting|running|paused|exited) Status Codes: @@ -833,7 +835,8 @@ Status Codes: Query Parameters: - **all** – 1/True/true or 0/False/false, default false -- **filters** – a json encoded value of the filters (a map[string][]string) to process on the images list. +- **filters** – a json encoded value of the filters (a map[string][]string) to process on the images list. Available filters: + - dangling=true ### Create an image @@ -1390,7 +1393,10 @@ Query Parameters: - **since** – timestamp used for polling - **until** – timestamp used for polling -- **filters** – a json encoded value of the filters (a map[string][]string) to process on the event list. +- **filters** – a json encoded value of the filters (a map[string][]string) to process on the event list. Available filters: + - event=<string> -- event to filter + - image=<string> -- image to filter + - container=<string> -- container to filter Status Codes: From c230f7041302351aed6d5c4ac82b98262d9562ef Mon Sep 17 00:00:00 2001 From: Jean-Tiare Le Bigot Date: Mon, 15 Dec 2014 19:57:39 +0100 Subject: [PATCH 047/513] Set HTTP upgrade header only when requested by client Signed-off-by: Jean-Tiare Le Bigot --- api/server/server.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/api/server/server.go b/api/server/server.go index d6e3d9098..9fddc558d 100644 --- a/api/server/server.go +++ b/api/server/server.go @@ -887,7 +887,11 @@ func postContainersAttach(eng *engine.Engine, version version.Version, w http.Re var errStream io.Writer - fmt.Fprintf(outStream, "HTTP/1.1 101 UPGRADED\r\nContent-Type: application/vnd.docker.raw-stream\r\nConnection: Upgrade\r\nUpgrade: tcp\r\n\r\n") + if _, ok := r.Header["Upgrade"]; ok { + fmt.Fprintf(outStream, "HTTP/1.1 101 UPGRADED\r\nContent-Type: application/vnd.docker.raw-stream\r\nConnection: Upgrade\r\nUpgrade: tcp\r\n\r\n") + } else { + 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.GreaterThanOrEqualTo("1.6") { errStream = stdcopy.NewStdWriter(outStream, stdcopy.Stderr) @@ -1137,7 +1141,12 @@ func postContainerExecStart(eng *engine.Engine, version version.Version, w http. var errStream io.Writer - fmt.Fprintf(outStream, "HTTP/1.1 101 UPGRADED\r\nContent-Type: application/vnd.docker.raw-stream\r\nConnection: Upgrade\r\nUpgrade: tcp\r\n\r\n") + if _, ok := r.Header["Upgrade"]; ok { + fmt.Fprintf(outStream, "HTTP/1.1 101 UPGRADED\r\nContent-Type: application/vnd.docker.raw-stream\r\nConnection: Upgrade\r\nUpgrade: tcp\r\n\r\n") + } else { + fmt.Fprintf(outStream, "HTTP/1.1 200 OK\r\nContent-Type: application/vnd.docker.raw-stream\r\n\r\n") + } + if !job.GetenvBool("Tty") && version.GreaterThanOrEqualTo("1.6") { errStream = stdcopy.NewStdWriter(outStream, stdcopy.Stderr) outStream = stdcopy.NewStdWriter(outStream, stdcopy.Stdout) From 46b104bd3977c4777b0d99ba6c190f37ae780245 Mon Sep 17 00:00:00 2001 From: Harald Albers Date: Mon, 15 Dec 2014 21:07:41 +0100 Subject: [PATCH 048/513] Refactor completion for docker run and docker create _docker_run and _docker_create had only one differing line. This refactoring features: - direct completion for both commands to the same function - factor out the common arguments, sort & format them nicely - compute the argument for _docker_pos_first_nonflag. Signed-off-by: Harald Albers --- contrib/completion/bash/docker | 166 +++++++++------------------------ 1 file changed, 46 insertions(+), 120 deletions(-) diff --git a/contrib/completion/bash/docker b/contrib/completion/bash/docker index 5364944fa..7fcbfca54 100755 --- a/contrib/completion/bash/docker +++ b/contrib/completion/bash/docker @@ -252,124 +252,7 @@ _docker_cp() { } _docker_create() { - case "$prev" in - -a|--attach) - COMPREPLY=( $( compgen -W 'stdin stdout stderr' -- "$cur" ) ) - return - ;; - --cidfile|--env-file) - _filedir - return - ;; - --volumes-from) - __docker_containers_all - return - ;; - -v|--volume|--device) - case "$cur" in - *:*) - # TODO somehow do _filedir for stuff inside the image, if it's already specified (which is also somewhat difficult to determine) - ;; - '') - COMPREPLY=( $( compgen -W '/' -- "$cur" ) ) - compopt -o nospace - ;; - /*) - _filedir - compopt -o nospace - ;; - esac - return - ;; - -e|--env) - COMPREPLY=( $( compgen -e -- "$cur" ) ) - compopt -o nospace - return - ;; - --link) - case "$cur" in - *:*) - ;; - *) - __docker_containers_running - COMPREPLY=( $( compgen -W "${COMPREPLY[*]}" -S ':' ) ) - compopt -o nospace - ;; - esac - return - ;; - --add-host) - case "$cur" in - *:) - __docker_resolve_hostname - return - ;; - esac - ;; - --cap-add|--cap-drop) - __docker_capabilities - return - ;; - --net) - case "$cur" in - container:*) - local cur=${cur#*:} - __docker_containers_all - ;; - *) - COMPREPLY=( $( compgen -W "bridge none container: host" -- "$cur") ) - if [ "${COMPREPLY[*]}" = "container:" ] ; then - compopt -o nospace - fi - ;; - esac - return - ;; - --restart) - case "$cur" in - on-failure:*) - ;; - *) - COMPREPLY=( $( compgen -W "no on-failure on-failure: always" -- "$cur") ) - ;; - esac - return - ;; - --security-opt) - case "$cur" in - label:*:*) - ;; - label:*) - local cur=${cur##*:} - COMPREPLY=( $( compgen -W "user: role: type: level: disable" -- "$cur") ) - if [ "${COMPREPLY[*]}" != "disable" ] ; then - compopt -o nospace - fi - ;; - *) - COMPREPLY=( $( compgen -W "label apparmor" -S ":" -- "$cur") ) - compopt -o nospace - ;; - esac - return - ;; - --entrypoint|-h|--hostname|-m|--memory|-u|--user|-w|--workdir|--cpuset|-c|--cpu-shares|-n|--name|-p|--publish|--expose|--dns|--lxc-conf|--dns-search) - return - ;; - esac - - case "$cur" in - -*) - COMPREPLY=( $( compgen -W "--privileged -P --publish-all -i --interactive -t --tty --cidfile --entrypoint -h --hostname -m --memory -u --user -w --workdir --cpuset -c --cpu-shares --name -a --attach -v --volume --link -e --env --env-file -p --publish --expose --dns --volumes-from --lxc-conf --security-opt --add-host --cap-add --cap-drop --device --dns-search --net --restart" -- "$cur" ) ) - ;; - *) - local counter=$(__docker_pos_first_nonflag '--cidfile|--volumes-from|-v|--volume|-e|--env|--env-file|--entrypoint|-h|--hostname|-m|--memory|-u|--user|-w|--workdir|--cpuset|-c|--cpu-shares|-n|--name|-a|--attach|--link|-p|--publish|--expose|--dns|--lxc-conf|--security-opt|--add-host|--cap-add|--cap-drop|--device|--dns-search|--net|--restart') - - if [ $cword -eq $counter ]; then - __docker_image_repos_and_tags_and_ids - fi - ;; - esac + _docker_run } _docker_diff() { @@ -617,6 +500,49 @@ _docker_rmi() { } _docker_run() { + local options_with_args=" + -a --attach + --add-host + --cap-add + --cap-drop + -c --cpu-shares + --cidfile + --cpuset + --device + --dns + --dns-search + -e --env + --entrypoint + --env-file + --expose + -h --hostname + --link + --lxc-conf + -m --memory + --name + --net + -p --publish + --restart + --security-opt + -u --user + --volumes-from + -v --volume + -w --workdir + " + + local all_options="$options_with_args + -i --interactive + -P --publish-all + --privileged + -t --tty + " + + [ "$command" = "run" ] && all_options="$all_options + -d --detach + --rm + --sig-proxy + " + case "$prev" in -a|--attach) COMPREPLY=( $( compgen -W 'stdin stdout stderr' -- "$cur" ) ) @@ -725,10 +651,10 @@ _docker_run() { case "$cur" in -*) - COMPREPLY=( $( compgen -W "--rm -d --detach --privileged -P --publish-all -i --interactive -t --tty --cidfile --entrypoint -h --hostname -m --memory -u --user -w --workdir --cpuset -c --cpu-shares --sig-proxy --name -a --attach -v --volume --link -e --env --env-file -p --publish --expose --dns --volumes-from --lxc-conf --security-opt --add-host --cap-add --cap-drop --device --dns-search --net --restart" -- "$cur" ) ) + COMPREPLY=( $( compgen -W "$all_options" -- "$cur" ) ) ;; *) - local counter=$(__docker_pos_first_nonflag '--cidfile|--volumes-from|-v|--volume|-e|--env|--env-file|--entrypoint|-h|--hostname|-m|--memory|-u|--user|-w|--workdir|--cpuset|-c|--cpu-shares|-n|--name|-a|--attach|--link|-p|--publish|--expose|--dns|--lxc-conf|--security-opt|--add-host|--cap-add|--cap-drop|--device|--dns-search|--net|--restart') + local counter=$( __docker_pos_first_nonflag $( echo $options_with_args | tr -d "\n" | tr " " "|" ) ) if [ $cword -eq $counter ]; then __docker_image_repos_and_tags_and_ids From 03bdacbb4e4bf2fabf67bfdb8b5118bc7ed1edd2 Mon Sep 17 00:00:00 2001 From: Alexander Morozov Date: Mon, 15 Dec 2014 13:44:22 -0800 Subject: [PATCH 049/513] Fix missing logDone for TestRunMutableNetworkFiles Signed-off-by: Alexander Morozov --- integration-cli/docker_cli_run_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/integration-cli/docker_cli_run_test.go b/integration-cli/docker_cli_run_test.go index 0b56f235f..4a508d6c3 100644 --- a/integration-cli/docker_cli_run_test.go +++ b/integration-cli/docker_cli_run_test.go @@ -1949,6 +1949,7 @@ func TestRunMutableNetworkFiles(t *testing.T) { t.Fatalf("Did not find the correct output in /etc/%s: %s %#v", fn, out, lines) } } + logDone("run - mutable network files") } // Ensure that CIDFile gets deleted if it's empty From d44c9f91472eb3df4c38c669134df04b2ccf9953 Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Fri, 12 Dec 2014 11:01:05 -0500 Subject: [PATCH 050/513] Fix volumes-from/bind-mounts passed in on start Fixes #9628 Slightly reverts #8683, HostConfig on start is _not_ deprecated. Signed-off-by: Brian Goff --- daemon/volumes.go | 25 +++- .../reference/api/docker_remote_api.md | 6 - integration-cli/docker_api_containers_test.go | 131 ++++++++++++++++++ 3 files changed, 150 insertions(+), 12 deletions(-) diff --git a/daemon/volumes.go b/daemon/volumes.go index 46ae5588a..ad2dd3a6a 100644 --- a/daemon/volumes.go +++ b/daemon/volumes.go @@ -24,6 +24,7 @@ type Mount struct { volume *volumes.Volume Writable bool copyData bool + from *Container } func (mnt *Mount) Export(resource string) (io.ReadCloser, error) { @@ -42,9 +43,6 @@ func (container *Container) prepareVolumes() error { if container.Volumes == nil || len(container.Volumes) == 0 { container.Volumes = make(map[string]string) container.VolumesRW = make(map[string]bool) - if err := container.applyVolumesFrom(); err != nil { - return err - } } return container.createVolumes() @@ -73,13 +71,27 @@ func (container *Container) createVolumes() error { } } - return nil + // On every start, this will apply any new `VolumesFrom` entries passed in via HostConfig, which may override volumes set in `create` + return container.applyVolumesFrom() } func (m *Mount) initialize() error { // No need to initialize anything since it's already been initialized - if _, exists := m.container.Volumes[m.MountToPath]; exists { - return nil + if hostPath, exists := m.container.Volumes[m.MountToPath]; exists { + // If this is a bind-mount/volumes-from, maybe it was passed in at start instead of create + // We need to make sure bind-mounts/volumes-from passed on start can override existing ones. + if !m.volume.IsBindMount && m.from == nil { + return nil + } + if m.volume.Path == hostPath { + return nil + } + + // Make sure we remove these old volumes we don't actually want now. + // Ignore any errors here since this is just cleanup, maybe someone volumes-from'd this volume + v := m.container.daemon.volumes.Get(hostPath) + v.RemoveContainer(m.container.ID) + m.container.daemon.volumes.Delete(v.Path) } // This is the full path to container fs + mntToPath @@ -217,6 +229,7 @@ func (container *Container) applyVolumesFrom() error { for _, mounts := range mountGroups { for _, mnt := range mounts { + mnt.from = mnt.container mnt.container = container if err := mnt.initialize(); err != nil { return err diff --git a/docs/sources/reference/api/docker_remote_api.md b/docs/sources/reference/api/docker_remote_api.md index 530b15d41..03613d938 100644 --- a/docs/sources/reference/api/docker_remote_api.md +++ b/docs/sources/reference/api/docker_remote_api.md @@ -61,12 +61,6 @@ You can set the new container's MAC address explicitly. **New!** Volumes are now initialized when the container is created. -`POST /containers/(id)/start` - -**New!** -Passing the container's `HostConfig` on start is now deprecated. You should -set this when creating the container. - `POST /containers/(id)/copy` **New!** diff --git a/integration-cli/docker_api_containers_test.go b/integration-cli/docker_api_containers_test.go index f02f619c4..8b0b8fd69 100644 --- a/integration-cli/docker_api_containers_test.go +++ b/integration-cli/docker_api_containers_test.go @@ -4,7 +4,10 @@ import ( "bytes" "encoding/json" "io" + "io/ioutil" + "os" "os/exec" + "strings" "testing" "github.com/docker/docker/vendor/src/code.google.com/p/go/src/pkg/archive/tar" @@ -120,3 +123,131 @@ func TestContainerApiGetChanges(t *testing.T) { logDone("container REST API - check GET containers/changes") } + +func TestContainerApiStartVolumeBinds(t *testing.T) { + defer deleteAllContainers() + name := "testing" + config := map[string]interface{}{ + "Image": "busybox", + "Volumes": map[string]struct{}{"/tmp": {}}, + } + + if _, err := sockRequest("POST", "/containers/create?name="+name, config); err != nil && !strings.Contains(err.Error(), "201 Created") { + t.Fatal(err) + } + + bindPath, err := ioutil.TempDir(os.TempDir(), "test") + if err != nil { + t.Fatal(err) + } + + config = map[string]interface{}{ + "Binds": []string{bindPath + ":/tmp"}, + } + if _, err := sockRequest("POST", "/containers/"+name+"/start", config); err != nil && !strings.Contains(err.Error(), "204 No Content") { + t.Fatal(err) + } + + pth, err := inspectFieldMap(name, "Volumes", "/tmp") + if err != nil { + t.Fatal(err) + } + + if pth != bindPath { + t.Fatalf("expected volume host path to be %s, got %s", bindPath, pth) + } + + logDone("container REST API - check volume binds on start") +} + +func TestContainerApiStartVolumesFrom(t *testing.T) { + defer deleteAllContainers() + volName := "voltst" + volPath := "/tmp" + + if out, _, err := runCommandWithOutput(exec.Command(dockerBinary, "run", "-d", "--name", volName, "-v", volPath, "busybox")); err != nil { + t.Fatal(out, err) + } + + name := "testing" + config := map[string]interface{}{ + "Image": "busybox", + "Volumes": map[string]struct{}{volPath: {}}, + } + + if _, err := sockRequest("POST", "/containers/create?name="+name, config); err != nil && !strings.Contains(err.Error(), "201 Created") { + t.Fatal(err) + } + + config = map[string]interface{}{ + "VolumesFrom": []string{volName}, + } + if _, err := sockRequest("POST", "/containers/"+name+"/start", config); err != nil && !strings.Contains(err.Error(), "204 No Content") { + t.Fatal(err) + } + + pth, err := inspectFieldMap(name, "Volumes", volPath) + if err != nil { + t.Fatal(err) + } + pth2, err := inspectFieldMap(volName, "Volumes", volPath) + if err != nil { + t.Fatal(err) + } + + if pth != pth2 { + t.Fatalf("expected volume host path to be %s, got %s", pth, pth2) + } + + logDone("container REST API - check VolumesFrom on start") +} + +// Ensure that volumes-from has priority over binds/anything else +// This is pretty much the same as TestRunApplyVolumesFromBeforeVolumes, except with passing the VolumesFrom and the bind on start +func TestVolumesFromHasPriority(t *testing.T) { + defer deleteAllContainers() + volName := "voltst" + volPath := "/tmp" + + if out, _, err := runCommandWithOutput(exec.Command(dockerBinary, "run", "-d", "--name", volName, "-v", volPath, "busybox")); err != nil { + t.Fatal(out, err) + } + + name := "testing" + config := map[string]interface{}{ + "Image": "busybox", + "Volumes": map[string]struct{}{volPath: {}}, + } + + if _, err := sockRequest("POST", "/containers/create?name="+name, config); err != nil && !strings.Contains(err.Error(), "201 Created") { + t.Fatal(err) + } + + bindPath, err := ioutil.TempDir(os.TempDir(), "test") + if err != nil { + t.Fatal(err) + } + + config = map[string]interface{}{ + "VolumesFrom": []string{volName}, + "Binds": []string{bindPath + ":/tmp"}, + } + if _, err := sockRequest("POST", "/containers/"+name+"/start", config); err != nil && !strings.Contains(err.Error(), "204 No Content") { + t.Fatal(err) + } + + pth, err := inspectFieldMap(name, "Volumes", volPath) + if err != nil { + t.Fatal(err) + } + pth2, err := inspectFieldMap(volName, "Volumes", volPath) + if err != nil { + t.Fatal(err) + } + + if pth != pth2 { + t.Fatalf("expected volume host path to be %s, got %s", pth, pth2) + } + + logDone("container REST API - check VolumesFrom has priority") +} From 97f07bb61ccf6e5a3a0fcc72894d5f37310d0109 Mon Sep 17 00:00:00 2001 From: Jacob Edelman Date: Mon, 15 Dec 2014 17:30:52 -0500 Subject: [PATCH 051/513] Fixed two misspellings. Fixed two misspellings of the word "beginning". Signed-off-by: JacobEdelman --- docs/docs-update.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs-update.py b/docs/docs-update.py index 586bde482..c40da773e 100755 --- a/docs/docs-update.py +++ b/docs/docs-update.py @@ -56,14 +56,14 @@ def update_cli_reference(): # Prose match = re.match("( \s*)Usage: docker ([a-z]+)", line) if match: - # the begining of a Docker command usage block + # the beginning of a Docker command usage block space = match.group(1) command = match.group(2) mode = 'c' else: match = re.match("( \s*)Usage of .*docker.*:", line) if match: - # the begining of the Docker --help usage block + # the beginning of the Docker --help usage block space = match.group(1) command = "" mode = 'c' From 4e7fb6b1f03173b0fbc41c18c9e9857b5708974a Mon Sep 17 00:00:00 2001 From: Scott Stamp Date: Fri, 12 Dec 2014 21:15:22 -0330 Subject: [PATCH 052/513] Update debian.md Fixed anchor links for Jessie/Wheezy (broken on docs.docker.com) Update debian.md Signed-off-by: Scott Stamp --- docs/sources/installation/debian.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/sources/installation/debian.md b/docs/sources/installation/debian.md index 1db160969..85ac82b8d 100644 --- a/docs/sources/installation/debian.md +++ b/docs/sources/installation/debian.md @@ -6,8 +6,8 @@ page_keywords: Docker, Docker documentation, installation, debian Docker is supported on the following versions of Debian: - - [*Debian 8.0 Jessie (64-bit)*](#debian-jessie-8-64-bit) - - [*Debian 7.5 Wheezy (64-bit)*](#debian-wheezy-7-64-bit) + - [*Debian 8.0 Jessie (64-bit)*](#debian-jessie-80-64-bit) + - [*Debian 7.5 Wheezy (64-bit)*](#debian-wheezystable-7x-64-bit) ## Debian Jessie 8.0 (64-bit) @@ -81,7 +81,7 @@ use the `-G` flag to specify an alternative group. > **Warning**: > The `docker` group (or the group specified with the `-G` flag) is > `root`-equivalent; see [*Docker Daemon Attack Surface*]( -> /articles/security/#dockersecurity-daemon) details. +> /articles/security/#docker-daemon-attack-surface) details. **Example:** From fbb9223b1adc16834768acaa7a5776697825deb2 Mon Sep 17 00:00:00 2001 From: Sven Dowideit Date: Tue, 16 Dec 2014 14:25:37 +1000 Subject: [PATCH 053/513] add Scott's link checker script, and fix what it finds Signed-off-by: Sven Dowideit --- Makefile | 3 + docs/README.md | 5 ++ docs/docvalidate.py | 79 +++++++++++++++++++ docs/sources/articles/baseimages.md | 2 +- docs/sources/articles/basics.md | 8 +- docs/sources/articles/chef.md | 2 +- docs/sources/articles/puppet.md | 2 +- docs/sources/articles/security.md | 2 +- docs/sources/articles/using_supervisord.md | 2 +- docs/sources/examples/apt-cacher-ng.md | 2 +- docs/sources/examples/couchdb_data_volumes.md | 2 +- docs/sources/examples/nodejs_web_app.md | 2 +- docs/sources/examples/postgresql_service.md | 2 +- docs/sources/http-routingtable.md | 2 +- docs/sources/installation/amazon.md | 8 +- docs/sources/installation/binaries.md | 2 +- docs/sources/installation/ubuntulinux.md | 2 +- .../reference/api/docker_remote_api.md | 2 +- .../reference/api/docker_remote_api_v1.10.md | 4 +- .../reference/api/docker_remote_api_v1.11.md | 2 +- .../reference/api/docker_remote_api_v1.12.md | 2 +- .../reference/api/docker_remote_api_v1.13.md | 2 +- .../reference/api/docker_remote_api_v1.14.md | 2 +- .../reference/api/docker_remote_api_v1.15.md | 2 +- .../reference/api/docker_remote_api_v1.16.md | 2 +- .../reference/api/docker_remote_api_v1.6.md | 2 +- .../reference/api/docker_remote_api_v1.7.md | 2 +- .../reference/api/docker_remote_api_v1.8.md | 2 +- .../reference/api/docker_remote_api_v1.9.md | 4 +- docs/sources/reference/builder.md | 10 +-- docs/sources/reference/commandline/cli.md | 8 +- docs/sources/reference/run.md | 8 +- docs/sources/terms/image.md | 4 +- docs/sources/terms/layer.md | 2 +- docs/sources/userguide/dockerrepos.md | 4 +- docs/sources/userguide/dockervolumes.md | 2 +- docs/sources/userguide/level1.md | 4 +- docs/sources/userguide/level2.md | 4 +- docs/test.sh | 6 ++ 39 files changed, 150 insertions(+), 57 deletions(-) create mode 100755 docs/docvalidate.py create mode 100755 docs/test.sh diff --git a/Makefile b/Makefile index 6f76fa4d2..ae1d29523 100644 --- a/Makefile +++ b/Makefile @@ -55,6 +55,9 @@ docs-shell: docs-build docs-release: docs-build $(DOCKER_RUN_DOCS) -e OPTIONS -e BUILD_ROOT "$(DOCKER_DOCS_IMAGE)" ./release.sh +docs-test: docs-build + $(DOCKER_RUN_DOCS) "$(DOCKER_DOCS_IMAGE)" ./test.sh + test: build $(DOCKER_RUN_DOCKER) hack/make.sh binary cross test-unit test-integration test-integration-cli diff --git a/docs/README.md b/docs/README.md index de3999ba7..b3e9b3230 100755 --- a/docs/README.md +++ b/docs/README.md @@ -33,6 +33,11 @@ In the root of the `docker` source directory: If you have any issues you need to debug, you can use `make docs-shell` and then run `mkdocs serve` +## Testing the links + +You can use `make docs-test` to generate a report of missing links that are referenced in +the documentation - there should be none. + ## Adding a new document New document (`.md`) files are added to the documentation builds by adding them diff --git a/docs/docvalidate.py b/docs/docvalidate.py new file mode 100755 index 000000000..582b8521d --- /dev/null +++ b/docs/docvalidate.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python + +""" I honestly don't even know how the hell this works, just use it. """ +__author__ = "Scott Stamp " + +from HTMLParser import HTMLParser +from urlparse import urljoin +from sys import setrecursionlimit +import re +import requests + +setrecursionlimit(10000) +root = 'http://localhost:8000' + + +class DataHolder: + + def __init__(self, value=None, attr_name='value'): + self._attr_name = attr_name + self.set(value) + + def __call__(self, value): + return self.set(value) + + def set(self, value): + setattr(self, self._attr_name, value) + return value + + def get(self): + return getattr(self, self._attr_name) + + +class Parser(HTMLParser): + global root + + ids = set() + crawled = set() + anchors = {} + pages = set() + save_match = DataHolder(attr_name='match') + + def __init__(self, origin): + self.origin = origin + HTMLParser.__init__(self) + + def handle_starttag(self, tag, attrs): + attrs = dict(attrs) + if 'href' in attrs: + href = attrs['href'] + + if re.match('^{0}|\/|\#[\S]{{1,}}'.format(root), href): + if self.save_match(re.search('.*\#(.*?)$', href)): + if self.origin not in self.anchors: + self.anchors[self.origin] = set() + self.anchors[self.origin].add( + self.save_match.match.groups(1)[0]) + + url = urljoin(root, href) + + if url not in self.crawled and not re.match('^\#', href): + self.crawled.add(url) + Parser(url).feed(requests.get(url).content) + + if 'id' in attrs: + self.ids.add(attrs['id']) + # explicit references + if 'name' in attrs: + self.ids.add(attrs['name']) + + +r = requests.get(root) +parser = Parser(root) +parser.feed(r.content) +for anchor in sorted(parser.anchors): + if not re.match('.*/\#.*', anchor): + for anchor_name in parser.anchors[anchor]: + if anchor_name not in parser.ids: + print 'Missing - ({0}): #{1}'.format( + anchor.replace(root, ''), anchor_name) diff --git a/docs/sources/articles/baseimages.md b/docs/sources/articles/baseimages.md index 3f53c8a84..5a5addd1a 100644 --- a/docs/sources/articles/baseimages.md +++ b/docs/sources/articles/baseimages.md @@ -5,7 +5,7 @@ page_keywords: Examples, Usage, base image, docker, documentation, examples # Create a Base Image So you want to create your own [*Base Image*]( -/terms/image/#base-image-def)? Great! +/terms/image/#base-image)? Great! The specific process will depend heavily on the Linux distribution you want to package. We have some examples below, and you are encouraged to diff --git a/docs/sources/articles/basics.md b/docs/sources/articles/basics.md index 8f3e1dc1a..29b9a2f19 100644 --- a/docs/sources/articles/basics.md +++ b/docs/sources/articles/basics.md @@ -17,7 +17,7 @@ If you get `docker: command not found` or something like incomplete Docker installation or insufficient privileges to access Docker on your machine. -Please refer to [*Installation*](/installation/#installation-list) +Please refer to [*Installation*](/installation) for installation instructions. ## Download a pre-built image @@ -26,7 +26,7 @@ for installation instructions. $ sudo docker pull ubuntu This will find the `ubuntu` image by name on -[*Docker Hub*](/userguide/dockerrepos/#find-public-images-on-docker-hub) +[*Docker Hub*](/userguide/dockerrepos/#searching-for-images) and download it from [Docker Hub](https://hub.docker.com) to a local image cache. @@ -174,6 +174,6 @@ will be stored (as a diff). See which images you already have using the You now have an image state from which you can create new instances. Read more about [*Share Images via -Repositories*](/userguide/dockerrepos/#working-with-the-repository) or +Repositories*](/userguide/dockerrepos) or continue to the complete [*Command -Line*](/reference/commandline/cli/#cli) +Line*](/reference/commandline/cli) diff --git a/docs/sources/articles/chef.md b/docs/sources/articles/chef.md index 6ca0eba73..cb70215c5 100644 --- a/docs/sources/articles/chef.md +++ b/docs/sources/articles/chef.md @@ -7,7 +7,7 @@ page_keywords: chef, installation, usage, docker, documentation > **Note**: > Please note this is a community contributed installation path. The only > `official` installation is using the -> [*Ubuntu*](/installation/ubuntulinux/#ubuntu-linux) installation +> [*Ubuntu*](/installation/ubuntulinux) installation > path. This version may sometimes be out of date. ## Requirements diff --git a/docs/sources/articles/puppet.md b/docs/sources/articles/puppet.md index e664d35c9..d9a7ceb70 100644 --- a/docs/sources/articles/puppet.md +++ b/docs/sources/articles/puppet.md @@ -6,7 +6,7 @@ page_keywords: puppet, installation, usage, docker, documentation > *Note:* Please note this is a community contributed installation path. The > only `official` installation is using the -> [*Ubuntu*](/installation/ubuntulinux/#ubuntu-linux) installation +> [*Ubuntu*](/installation/ubuntulinux) installation > path. This version may sometimes be out of date. ## Requirements diff --git a/docs/sources/articles/security.md b/docs/sources/articles/security.md index 12f7b350e..731638025 100644 --- a/docs/sources/articles/security.md +++ b/docs/sources/articles/security.md @@ -33,7 +33,7 @@ of another container. Of course, if the host system is setup accordingly, containers can interact with each other through their respective network interfaces — just like they can interact with external hosts. When you specify public ports for your containers or use -[*links*](/userguide/dockerlinks/#working-with-links-names) +[*links*](/userguide/dockerlinks) then IP traffic is allowed between containers. They can ping each other, send/receive UDP packets, and establish TCP connections, but that can be restricted if necessary. From a network architecture point of view, all diff --git a/docs/sources/articles/using_supervisord.md b/docs/sources/articles/using_supervisord.md index 10f32c7d1..01e60b659 100644 --- a/docs/sources/articles/using_supervisord.md +++ b/docs/sources/articles/using_supervisord.md @@ -6,7 +6,7 @@ page_keywords: docker, supervisor, process management > **Note**: > - **If you don't like sudo** then see [*Giving non-root -> access*](/installation/binaries/#dockergroup) +> access*](/installation/binaries/#giving-non-root-access) Traditionally a Docker container runs a single process when it is launched, for example an Apache daemon or a SSH server daemon. Often diff --git a/docs/sources/examples/apt-cacher-ng.md b/docs/sources/examples/apt-cacher-ng.md index 7dafec159..cd92cb59a 100644 --- a/docs/sources/examples/apt-cacher-ng.md +++ b/docs/sources/examples/apt-cacher-ng.md @@ -6,7 +6,7 @@ page_keywords: docker, example, package installation, networking, debian, ubuntu > **Note**: > - **If you don't like sudo** then see [*Giving non-root -> access*](/installation/binaries/#dockergroup). +> access*](/installation/binaries/#giving-non-root-access). > - **If you're using OS X or docker via TCP** then you shouldn't use > sudo. diff --git a/docs/sources/examples/couchdb_data_volumes.md b/docs/sources/examples/couchdb_data_volumes.md index 44043d641..8cd2408e4 100644 --- a/docs/sources/examples/couchdb_data_volumes.md +++ b/docs/sources/examples/couchdb_data_volumes.md @@ -6,7 +6,7 @@ page_keywords: docker, example, package installation, networking, couchdb, data > **Note**: > - **If you don't like sudo** then see [*Giving non-root -> access*](/installation/binaries/#dockergroup) +> access*](/installation/binaries/#giving-non-root-access) Here's an example of using data volumes to share the same data between two CouchDB containers. This could be used for hot upgrades, testing diff --git a/docs/sources/examples/nodejs_web_app.md b/docs/sources/examples/nodejs_web_app.md index 3a9183e32..39af59afc 100644 --- a/docs/sources/examples/nodejs_web_app.md +++ b/docs/sources/examples/nodejs_web_app.md @@ -6,7 +6,7 @@ page_keywords: docker, example, package installation, node, centos > **Note**: > - **If you don't like sudo** then see [*Giving non-root -> access*](/installation/binaries/#dockergroup) +> access*](/installation/binaries/#giving-non-root-access) The goal of this example is to show you how you can build your own Docker images from a parent image using a `Dockerfile` diff --git a/docs/sources/examples/postgresql_service.md b/docs/sources/examples/postgresql_service.md index 9a4c1816d..21044d369 100644 --- a/docs/sources/examples/postgresql_service.md +++ b/docs/sources/examples/postgresql_service.md @@ -6,7 +6,7 @@ page_keywords: docker, example, package installation, postgresql > **Note**: > - **If you don't like sudo** then see [*Giving non-root -> access*](/installation/binaries/#dockergroup) +> access*](/installation/binaries/#giving-non-root-access) ## Installing PostgreSQL on Docker diff --git a/docs/sources/http-routingtable.md b/docs/sources/http-routingtable.md index ff66c7a19..07029d2ca 100644 --- a/docs/sources/http-routingtable.md +++ b/docs/sources/http-routingtable.md @@ -42,7 +42,7 @@ [`POST /containers/(id)/stop`](../reference/api/docker_remote_api_v1.9/#post--containers-(id)-stop) ** [`GET /containers/(id)/top`](../reference/api/docker_remote_api_v1.9/#get--containers-(id)-top) ** [`POST /containers/(id)/wait`](../reference/api/docker_remote_api_v1.9/#post--containers-(id)-wait) ** - [`POST /containers/create`](../reference/api/docker_remote_api_v1.9/#post--containers-create) ** + [`POST /containers/create`](/reference/api/docker_remote_api_v1.9/#create-a-container) ** [`GET /containers/json`](../reference/api/docker_remote_api_v1.9/#get--containers-json) ** [`POST /containers/(id)/resize`](../reference/api/docker_remote_api_v1.9/#get--containers-resize) **   diff --git a/docs/sources/installation/amazon.md b/docs/sources/installation/amazon.md index 58d269ad7..6a28685dc 100644 --- a/docs/sources/installation/amazon.md +++ b/docs/sources/installation/amazon.md @@ -40,10 +40,10 @@ over to the [User Guide](/userguide). ## Standard Ubuntu Installation If you want a more hands-on installation, then you can follow the -[*Ubuntu*](../ubuntulinux/#ubuntu-linux) instructions installing Docker -on any EC2 instance running Ubuntu. Just follow Step 1 from [*Amazon -QuickStart*](#amazon-quickstart) to pick an image (or use one of your +[*Ubuntu*](/installation/ubuntulinux) instructions installing Docker +on any EC2 instance running Ubuntu. Just follow Step 1 from the Amazon +QuickStart above to pick an image (or use one of your own) and skip the step with the *User Data*. Then continue with the -[*Ubuntu*](../ubuntulinux/#ubuntu-linux) instructions. +[*Ubuntu*](/installation/ubuntulinux) instructions. Continue with the [User Guide](/userguide/). diff --git a/docs/sources/installation/binaries.md b/docs/sources/installation/binaries.md index da2b195c0..a2e40397f 100644 --- a/docs/sources/installation/binaries.md +++ b/docs/sources/installation/binaries.md @@ -77,7 +77,7 @@ need to add `sudo` to all the client commands. > **Warning**: > The *docker* group (or the group specified with `-G`) is root-equivalent; > see [*Docker Daemon Attack Surface*]( -> /articles/security/#dockersecurity-daemon) details. +> /articles/security/#docker-daemon-attack-surface) details. ## Upgrades diff --git a/docs/sources/installation/ubuntulinux.md b/docs/sources/installation/ubuntulinux.md index d4df599d0..2c1192701 100644 --- a/docs/sources/installation/ubuntulinux.md +++ b/docs/sources/installation/ubuntulinux.md @@ -240,7 +240,7 @@ alternative group. > **Warning**: > The `docker` group (or the group specified with the `-G` flag) is > `root`-equivalent; see [*Docker Daemon Attack Surface*]( -> /articles/security/#dockersecurity-daemon) for details. +> /articles/security/#docker-daemon-attack-surface) for details. **Example:** diff --git a/docs/sources/reference/api/docker_remote_api.md b/docs/sources/reference/api/docker_remote_api.md index 03613d938..6e571d3fd 100644 --- a/docs/sources/reference/api/docker_remote_api.md +++ b/docs/sources/reference/api/docker_remote_api.md @@ -366,7 +366,7 @@ output is now generated in the client, using the You can now split stderr from stdout. This is done by prefixing a header to each transmission. See [`POST /containers/(id)/attach`]( -/reference/api/docker_remote_api_v1.9/#post--containers-(id)-attach "POST /containers/(id)/attach"). +/reference/api/docker_remote_api_v1.9/#attach-to-a-container "POST /containers/(id)/attach"). The WebSocket attach is unchanged. Note that attach calls on the previous API version didn't change. Stdout and stderr are merged. diff --git a/docs/sources/reference/api/docker_remote_api_v1.10.md b/docs/sources/reference/api/docker_remote_api_v1.10.md index eb3f5cc1e..1855ccad6 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.10.md +++ b/docs/sources/reference/api/docker_remote_api_v1.10.md @@ -499,7 +499,7 @@ Status Codes: When using the TTY setting is enabled in [`POST /containers/create` -](../docker_remote_api_v1.9/#post--containers-create "POST /containers/create"), +](/reference/api/docker_remote_api_v1.9/#create-a-container "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. @@ -998,7 +998,7 @@ Build an image from Dockerfile via stdin 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 [*ADD build - command*](/reference/builder/#dockerbuilder)). + command*](/reference/builder/#add)). Query Parameters: diff --git a/docs/sources/reference/api/docker_remote_api_v1.11.md b/docs/sources/reference/api/docker_remote_api_v1.11.md index 838d199ea..dcf566ffb 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.11.md +++ b/docs/sources/reference/api/docker_remote_api_v1.11.md @@ -535,7 +535,7 @@ Status Codes: When using the TTY setting is enabled in [`POST /containers/create` - ](../docker_remote_api_v1.9/#post--containers-create "POST /containers/create"), + ](/reference/api/docker_remote_api_v1.9/#create-a-container "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. diff --git a/docs/sources/reference/api/docker_remote_api_v1.12.md b/docs/sources/reference/api/docker_remote_api_v1.12.md index f38b018ef..de15e660f 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.12.md +++ b/docs/sources/reference/api/docker_remote_api_v1.12.md @@ -583,7 +583,7 @@ Status Codes: When using the TTY setting is enabled in [`POST /containers/create` - ](../docker_remote_api_v1.9/#post--containers-create "POST /containers/create"), + ](/reference/api/docker_remote_api_v1.9/#create-a-container "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. diff --git a/docs/sources/reference/api/docker_remote_api_v1.13.md b/docs/sources/reference/api/docker_remote_api_v1.13.md index f5ca931fe..79e1af685 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.13.md +++ b/docs/sources/reference/api/docker_remote_api_v1.13.md @@ -576,7 +576,7 @@ Status Codes: When using the TTY setting is enabled in [`POST /containers/create` - ](../docker_remote_api_v1.9/#post--containers-create "POST /containers/create"), + ](/reference/api/docker_remote_api_v1.9/#create-a-container "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. diff --git a/docs/sources/reference/api/docker_remote_api_v1.14.md b/docs/sources/reference/api/docker_remote_api_v1.14.md index a5392f3bc..4eb316a12 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.14.md +++ b/docs/sources/reference/api/docker_remote_api_v1.14.md @@ -581,7 +581,7 @@ Status Codes: When using the TTY setting is enabled in [`POST /containers/create` - ](../docker_remote_api_v1.9/#post--containers-create "POST /containers/create"), + ](/reference/api/docker_remote_api_v1.9/#create-a-container "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. diff --git a/docs/sources/reference/api/docker_remote_api_v1.15.md b/docs/sources/reference/api/docker_remote_api_v1.15.md index ae265653a..f7f861f5e 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.15.md +++ b/docs/sources/reference/api/docker_remote_api_v1.15.md @@ -721,7 +721,7 @@ Status Codes: When using the TTY setting is enabled in [`POST /containers/create` - ](../docker_remote_api_v1.9/#post--containers-create "POST /containers/create"), + ](/reference/api/docker_remote_api_v1.9/#create-a-container "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. diff --git a/docs/sources/reference/api/docker_remote_api_v1.16.md b/docs/sources/reference/api/docker_remote_api_v1.16.md index 72f5519e1..5abf26205 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.16.md +++ b/docs/sources/reference/api/docker_remote_api_v1.16.md @@ -669,7 +669,7 @@ Status Codes: When using the TTY setting is enabled in [`POST /containers/create` - ](../docker_remote_api_v1.9/#post--containers-create "POST /containers/create"), + ](/reference/api/docker_remote_api_v1.9/#create-a-container "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. diff --git a/docs/sources/reference/api/docker_remote_api_v1.6.md b/docs/sources/reference/api/docker_remote_api_v1.6.md index 9055b2471..d92983a54 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.6.md +++ b/docs/sources/reference/api/docker_remote_api_v1.6.md @@ -525,7 +525,7 @@ Status Codes: When using the TTY setting is enabled in [`POST /containers/create` - ](/api/docker_remote_api_v1.9/#post--containers-create "POST /containers/create"), + ](/reference/api/docker_remote_api_v1.9/#create-a-container "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. diff --git a/docs/sources/reference/api/docker_remote_api_v1.7.md b/docs/sources/reference/api/docker_remote_api_v1.7.md index 2f07b2b69..7660d744f 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.7.md +++ b/docs/sources/reference/api/docker_remote_api_v1.7.md @@ -470,7 +470,7 @@ Status Codes: When using the TTY setting is enabled in [`POST /containers/create` - ](/api/docker_remote_api_v1.9/#post--containers-create "POST /containers/create"), + ](/reference/api/docker_remote_api_v1.7/#create-a-container), 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. diff --git a/docs/sources/reference/api/docker_remote_api_v1.8.md b/docs/sources/reference/api/docker_remote_api_v1.8.md index faaa71397..e0bdaa661 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.8.md +++ b/docs/sources/reference/api/docker_remote_api_v1.8.md @@ -518,7 +518,7 @@ Status Codes: When using the TTY setting is enabled in [`POST /containers/create` - ](/api/docker_remote_api_v1.9/#post--containers-create "POST /containers/create"), + ](/reference/api/docker_remote_api_v1.9/#create-a-container "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. diff --git a/docs/sources/reference/api/docker_remote_api_v1.9.md b/docs/sources/reference/api/docker_remote_api_v1.9.md index 4c7301ee1..7ddefc4f2 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.9.md +++ b/docs/sources/reference/api/docker_remote_api_v1.9.md @@ -522,7 +522,7 @@ Status Codes: **Stream details**: When using the TTY setting is enabled in - [`POST /containers/create`](#post--containers-create), the + [`POST /containers/create`](#create-a-container), 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. @@ -1004,7 +1004,7 @@ Build an image from Dockerfile using a POST body. 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 [*ADD build - command*](/reference/builder/#dockerbuilder)). + command*](/reference/builder/#add)). Query Parameters: diff --git a/docs/sources/reference/builder.md b/docs/sources/reference/builder.md index adc308c9d..10689bb1f 100644 --- a/docs/sources/reference/builder.md +++ b/docs/sources/reference/builder.md @@ -79,7 +79,7 @@ guide](/articles/dockerfile_best-practices/#build-cache) for more information): Successfully built 1a5ffc17324d When you're done with your build, you're ready to look into [*Pushing a -repository to its registry*]( /userguide/dockerrepos/#image-push). +repository to its registry*]( /userguide/dockerrepos/#contributing-to-docker-hub). ## Format @@ -93,7 +93,7 @@ be UPPERCASE in order to distinguish them from arguments more easily. Docker runs the instructions in a `Dockerfile` in order. **The first instruction must be \`FROM\`** in order to specify the [*Base -Image*](/terms/image/#base-image-def) from which you are building. +Image*](/terms/image/#base-image) from which you are building. Docker will treat lines that *begin* with `#` as a comment. A `#` marker anywhere else in the line will @@ -186,11 +186,11 @@ Or FROM : -The `FROM` instruction sets the [*Base Image*](/terms/image/#base-image-def) +The `FROM` instruction sets the [*Base Image*](/terms/image/#base-image) for subsequent instructions. As such, a valid `Dockerfile` must have `FROM` as its first instruction. The image can be any valid image – it is especially easy to start by **pulling an image** from the [*Public Repositories*]( -/userguide/dockerrepos/#using-public-repositories). +/userguide/dockerrepos). `FROM` must be the first non-comment instruction in the `Dockerfile`. @@ -763,7 +763,7 @@ and mark it as holding externally mounted volumes from native host or other containers. The value can be a JSON array, `VOLUME ["/var/log/"]`, or a plain string with multiple arguments, such as `VOLUME /var/log` or `VOLUME /var/log /var/db`. For more information/examples and mounting instructions via the -Docker client, refer to [*Share Directories via Volumes*](/userguide/dockervolumes/#volume-def) +Docker client, refer to [*Share Directories via Volumes*](/userguide/dockervolumes/#volume) documentation. > **Note**: diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index 577a4c68c..b73f7273b 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -459,7 +459,7 @@ Use this command to build Docker images from a Dockerfile and a 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 using an [*ADD*](/reference/builder/#dockerfile-add) instruction. +when using an [*ADD*](/reference/builder/#add) instruction. When a single Dockerfile is given as `URL` or is piped through `STDIN` (`docker build - < Dockerfile`), then no context is set. @@ -539,7 +539,7 @@ machine and that no parsing of the Dockerfile happens at the client side (where you're running `docker build`). That means that *all* the files at `PATH` get sent, not just the ones listed to -[*ADD*](/reference/builder/#dockerfile-add) in the Dockerfile. +[*ADD*](/reference/builder/#add) in the Dockerfile. The transfer of context from the local machine to the Docker daemon is what the `docker` client means when you see the @@ -1814,7 +1814,7 @@ Search [Docker Hub](https://hub.docker.com) for images -s, --stars=0 Only displays with at least x stars See [*Find Public Images on Docker Hub*]( -/userguide/dockerrepos/#find-public-images-on-docker-hub) for +/userguide/dockerrepos/#searching-for-images) for more details on finding shared images from the command line. ## start @@ -1850,7 +1850,7 @@ grace period, `SIGKILL`. You can group your images together using names and tags, and then upload them to [*Share Images via Repositories*]( -/userguide/dockerrepos/#working-with-the-repository). +/userguide/dockerrepos/#contributing-to-docker-hub). ## top diff --git a/docs/sources/reference/run.md b/docs/sources/reference/run.md index e9ecfff44..d13284b5d 100644 --- a/docs/sources/reference/run.md +++ b/docs/sources/reference/run.md @@ -7,7 +7,7 @@ page_keywords: docker, run, configure, runtime **Docker runs processes in isolated containers**. When an operator executes `docker run`, she starts a process with its own file system, its own networking, and its own isolated process tree. The -[*Image*](/terms/image/#image-def) which starts the process may define +[*Image*](/terms/image/#image) which starts the process may define defaults related to the binary to run, the networking to expose, and more, but `docker run` gives final control to the operator who starts the container from the image. That's the main reason @@ -114,7 +114,7 @@ The UUID identifiers come from the Docker daemon, and if you do not assign a name to the container with `--name` then the daemon will also generate a random string name too. The name can become a handy way to add meaning to a container since you can use this name when defining -[*links*](/userguide/dockerlinks/#working-with-links-names) (or any +[*links*](/userguide/dockerlinks) (or any other place you need to identify a container). This works for both background and foreground Docker containers. @@ -420,7 +420,7 @@ familiar with using LXC directly. ## Overriding Dockerfile image defaults -When a developer builds an image from a [*Dockerfile*](/reference/builder/#dockerbuilder) +When a developer builds an image from a [*Dockerfile*](/reference/builder) or when she commits it, the developer can set a number of default parameters that take effect when the image starts up as a container. @@ -634,7 +634,7 @@ container's `/etc/hosts` entry will be automatically updated. The volumes commands are complex enough to have their own documentation in section [*Managing data in -containers*](/userguide/dockervolumes/#volume-def). A developer can define +containers*](/userguide/dockervolumes). A developer can define one or more `VOLUME`'s associated with an image, but only the operator can give access from one container to another (or from a container to a volume mounted on the host). diff --git a/docs/sources/terms/image.md b/docs/sources/terms/image.md index 40438be63..e42a6cfa1 100644 --- a/docs/sources/terms/image.md +++ b/docs/sources/terms/image.md @@ -8,10 +8,10 @@ page_keywords: containers, lxc, concepts, explanation, image, container ![](/terms/images/docker-filesystems-debian.png) -In Docker terminology, a read-only [*Layer*](/terms/layer/#layer-def) is +In Docker terminology, a read-only [*Layer*](/terms/layer/#layer) is called an **image**. An image never changes. -Since Docker uses a [*Union File System*](/terms/layer/#ufs-def), the +Since Docker uses a [*Union File System*](/terms/layer/#union-file-system), the processes think the whole file system is mounted read-write. But all the changes go to the top-most writeable layer, and underneath, the original file in the read-only image is unchanged. Since images don't change, diff --git a/docs/sources/terms/layer.md b/docs/sources/terms/layer.md index 561807fc4..3e8704cd0 100644 --- a/docs/sources/terms/layer.md +++ b/docs/sources/terms/layer.md @@ -7,7 +7,7 @@ page_keywords: containers, lxc, concepts, explanation, image, container ## Introduction In a traditional Linux boot, the kernel first mounts the root [*File -System*](/terms/filesystem/#filesystem-def) as read-only, checks its +System*](/terms/filesystem) as read-only, checks its integrity, and then switches the whole rootfs volume to read-write mode. ## Layer diff --git a/docs/sources/userguide/dockerrepos.md b/docs/sources/userguide/dockerrepos.md index 9b5f9783e..d8dc44e69 100644 --- a/docs/sources/userguide/dockerrepos.md +++ b/docs/sources/userguide/dockerrepos.md @@ -36,8 +36,8 @@ e-mail address. It will then automatically log you in. You can now commit and push your own images up to your repos on Docker Hub. > **Note:** -> Your authentication credentials will be stored in the [`.dockercfg` -> authentication file](#authentication-file) in your home directory. +> Your authentication credentials will be stored in the `.dockercfg` +> authentication file in your home directory. ## Searching for images diff --git a/docs/sources/userguide/dockervolumes.md b/docs/sources/userguide/dockervolumes.md index 6f94b6dbd..4663683c5 100644 --- a/docs/sources/userguide/dockervolumes.md +++ b/docs/sources/userguide/dockervolumes.md @@ -21,7 +21,7 @@ Docker. A *data volume* is a specially-designated directory within one or more containers that bypasses the [*Union File -System*](/terms/layer/#ufs-def) to provide several useful features for +System*](/terms/layer/#union-file-system) to provide several useful features for persistent or shared data: - Data volumes can be shared and reused between containers diff --git a/docs/sources/userguide/level1.md b/docs/sources/userguide/level1.md index 56048bfcc..cca77dc36 100644 --- a/docs/sources/userguide/level1.md +++ b/docs/sources/userguide/level1.md @@ -29,7 +29,7 @@ page_keywords: documentation, docs, the docker guide, docker guide, docker, dock

@@ -69,4 +69,4 @@ Tell the world! Back -Go to the next level \ No newline at end of file +Go to the next level diff --git a/docs/sources/userguide/level2.md b/docs/sources/userguide/level2.md index 4ff76be07..fe6654e71 100644 --- a/docs/sources/userguide/level2.md +++ b/docs/sources/userguide/level2.md @@ -39,7 +39,7 @@ What is the Dockerfile instruction to specify the base image?
@@ -93,4 +93,4 @@ Thanks for going through our tutorial! We will be posting Level 3 in the future. To improve your Dockerfile writing skills even further, visit the Dockerfile best practices page. -Back to the Docs! \ No newline at end of file +Back to the Docs! diff --git a/docs/test.sh b/docs/test.sh new file mode 100755 index 000000000..351236844 --- /dev/null +++ b/docs/test.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +mkdocs serve & +echo "Waiting for 5 seconds to allow mkdocs server to be ready" +sleep 5 +./docvalidate.py From c9ca5c4dd3a90332cc40d25175e3b54aca1212a8 Mon Sep 17 00:00:00 2001 From: Sven Dowideit Date: Tue, 16 Dec 2014 15:25:09 +1000 Subject: [PATCH 054/513] Update the documentation for #9356 fix Signed-off-by: Sven Dowideit --- docs/sources/reference/commandline/cli.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index 577a4c68c..415f5adf1 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -864,8 +864,17 @@ The `docker exec` command runs a new command in a running container. The command started using `docker exec` will only run while the container's primary process (`PID 1`) is running, and will not be restarted if the container is restarted. -If the container is paused, then the `docker exec` command will wait until the -container is unpaused, and then run. +If the container is paused, then the `docker exec` command will fail with an error: + + $ docker pause test + test + $ docker ps + CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES + 1ae3b36715d2 ubuntu:latest "bash" 17 seconds ago Up 16 seconds (Paused) test + $ docker exec test ls + FATA[0000] Error response from daemon: Container test is paused, unpause the container before exec + $ echo $? + 1 #### Examples From 17f8e3dee2d0df54b848cbb13af1986abe07db9f Mon Sep 17 00:00:00 2001 From: "Daniel, Dao Quang Minh" Date: Tue, 16 Dec 2014 05:05:12 -0500 Subject: [PATCH 055/513] make.sh tgz should handle windows binary Docker-DCO-1.1-Signed-off-by: Daniel, Dao Quang Minh (github: dqminh) --- project/make/tgz | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/project/make/tgz b/project/make/tgz index 120339976..9307200cc 100644 --- a/project/make/tgz +++ b/project/make/tgz @@ -13,13 +13,19 @@ fi for d in "$CROSS/"*/*; do GOARCH="$(basename "$d")" GOOS="$(basename "$(dirname "$d")")" + BINARY_NAME="docker-$VERSION" + BINARY_EXTENSION= + if [ "$GOOS" = 'windows' ]; then + BINARY_EXTENSION='.exe' + fi + BINARY_FULLNAME="$BINARY_NAME$BINARY_EXTENSION" mkdir -p "$DEST/$GOOS/$GOARCH" - TGZ="$DEST/$GOOS/$GOARCH/docker-$VERSION.tgz" + TGZ="$DEST/$GOOS/$GOARCH/$BINARY_NAME.tgz" mkdir -p "$DEST/build" mkdir -p "$DEST/build/usr/local/bin" - cp -L "$d/docker-$VERSION" "$DEST/build/usr/local/bin/docker" + cp -L "$d/$BINARY_FULLNAME" "$DEST/build/usr/local/bin/docker" tar --numeric-owner --owner 0 -C "$DEST/build" -czf "$TGZ" usr From 46748f1302c236457f0b646a746a9c945200ed12 Mon Sep 17 00:00:00 2001 From: Mary Anthony Date: Tue, 16 Dec 2014 10:14:03 -0800 Subject: [PATCH 056/513] Updating per PR comments Signed-off-by: Mary Anthony --- docs/sources/contributing/devenvironment.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/contributing/devenvironment.md b/docs/sources/contributing/devenvironment.md index 42a73827b..194c16433 100644 --- a/docs/sources/contributing/devenvironment.md +++ b/docs/sources/contributing/devenvironment.md @@ -53,7 +53,7 @@ On Mac OS X, from within the `boot2docker` shell: > **Note**: > On Mac OS X, **do not** build Docker make targets such as `build`, `binary`, and `test` -> under root (sudo). +> under root using the `sudo` command. If the build is successful, congratulations! You have produced a clean build of docker, neatly encapsulated in a standard build environment. From 078ac9ca456af1ecfc283a43aa60328ac800b205 Mon Sep 17 00:00:00 2001 From: Mary Anthony Date: Tue, 16 Dec 2014 10:16:20 -0800 Subject: [PATCH 057/513] Updating per PR comment Signed-off-by: Mary Anthony --- docs/sources/contributing/devenvironment.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/contributing/devenvironment.md b/docs/sources/contributing/devenvironment.md index 194c16433..5030a2d75 100644 --- a/docs/sources/contributing/devenvironment.md +++ b/docs/sources/contributing/devenvironment.md @@ -41,7 +41,7 @@ with the name of branch or revision number. ## Build the Environment This following command builds a development environment using the -Dockerfile in the current directory. Essentially, it installs all +`Dockerfile` in the current directory. Essentially, it installs all the build and runtime dependencies necessary to build and test Docker. Your first build will take some time to complete. On Linux systems: From f42c0a53a38a2a141bec8768d0836a3726de4a83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20R=C3=B6thlisberger?= Date: Sat, 22 Nov 2014 08:25:57 +0000 Subject: [PATCH 058/513] upstart: Don't emit "started" event until docker.sock is available MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #6647: Other upstart jobs that depend on docker by specifying "start on started docker" would often start before the docker daemon was ready, so they'd fail with "Cannot connect to the Docker daemon" or "dial unix /var/run/docker.sock: no such file or directory". This is because "docker -d" doesn't daemonize, it runs in the foreground, so upstart can't know when the daemon is ready to receive incoming connections. (Traditionally, a daemon will create all necessary sockets and then fork to signal that it's ready; according to @tianon this "isn't possible in Go"[1]. See also [2].) Presumably this isn't a problem with systemd init with its socket activation. The SysV init scripts may or may not suffer from this problem but I have no motivation to fix them. This commit adds a "post-start" stanza to the upstart configuration that waits for the socket to be available. Upstart won't emit the "started" event until the "post-start" script completes.[3] Note that the system administrator might have specified a different path for the socket, or a tcp socket instead, by customising /etc/default/docker. In that case we don't try to figure out what the new socket is, but at least we don't wait in vain for /var/run/docker.sock to appear. If the main script (`docker -d`) fails to start, the `initctl status $UPSTART_JOB | grep -q "stop/"` line ensures that we don't loop forever. I stole this idea from Steve Langasek.[4] If for some reason we *still* end up in an infinite loop --I guess `docker -d` must have hung-- then at least we'll be able to see the "Waiting for /var/run/docker.sock" debug output in /var/log/upstart/docker.log. I considered using inotifywait instead of sleep, but it isn't worth the complexity & the extra dependency. [1] https://github.com/docker/docker/issues/6647#issuecomment-47001613 [2] https://code.google.com/p/go/issues/detail?id=227 [3] http://upstart.ubuntu.com/cookbook/#post-start [4] https://lists.ubuntu.com/archives/upstart-devel/2013-April/002492.html Signed-off-by: David Röthlisberger --- contrib/init/upstart/docker.conf | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/contrib/init/upstart/docker.conf b/contrib/init/upstart/docker.conf index 5a3f88887..f9930bd39 100644 --- a/contrib/init/upstart/docker.conf +++ b/contrib/init/upstart/docker.conf @@ -39,3 +39,20 @@ script fi exec "$DOCKER" -d $DOCKER_OPTS end script + +# Don't emit "started" event until docker.sock is ready. +# See https://github.com/docker/docker/issues/6647 +post-start script + DOCKER_OPTS= + if [ -f /etc/default/$UPSTART_JOB ]; then + . /etc/default/$UPSTART_JOB + fi + if ! printf "%s" "$DOCKER_OPTS" | grep -qE -e '-H|--host'; then + while ! [ -e /var/run/docker.sock ]; do + initctl status $UPSTART_JOB | grep -q "stop/" && exit 1 + echo "Waiting for /var/run/docker.sock" + sleep 0.1 + done + echo "/var/run/docker.sock is up" + fi +end script From afb06a3ce2e769951a89a889f11b9ab5eea54dff Mon Sep 17 00:00:00 2001 From: Jean-Tiare Le Bigot Date: Wed, 17 Dec 2014 00:22:06 +0100 Subject: [PATCH 059/513] Update connection hijacking documentation with HTTP Upgrade Headers Signed-off-by: Jean-Tiare Le Bigot --- .../reference/api/docker_remote_api_v1.16.md | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/docs/sources/reference/api/docker_remote_api_v1.16.md b/docs/sources/reference/api/docker_remote_api_v1.16.md index 72f5519e1..6eb8e7e01 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.16.md +++ b/docs/sources/reference/api/docker_remote_api_v1.16.md @@ -390,8 +390,10 @@ Get stdout and stderr logs from the container ``id`` **Example response**: - HTTP/1.1 200 OK + HTTP/1.1 101 UPGRADED Content-Type: application/vnd.docker.raw-stream + Connection: Upgrade + Upgrade: tcp {{ STREAM }} @@ -406,7 +408,8 @@ Query Parameters: Status Codes: -- **200** – no error +- **101** – no error, hints proxy about hijacking +- **200** – no error, no upgrade header found - **404** – no such container - **500** – server error @@ -641,8 +644,10 @@ Attach to the container `id` **Example response**: - HTTP/1.1 200 OK + HTTP/1.1 101 UPGRADED Content-Type: application/vnd.docker.raw-stream + Connection: Upgrade + Upgrade: tcp {{ STREAM }} @@ -660,7 +665,8 @@ Query Parameters: Status Codes: -- **200** – no error +- **101** – no error, hints proxy about hijacking +- **200** – no error, no upgrade header found - **400** – bad parameter - **404** – no such container - **500** – server error @@ -1739,7 +1745,18 @@ As an example, the `docker run` command line makes the following API calls: ## 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. +stdout and stderr on the same socket. + +To hint potential proxies about connection hijacking, Docker client sends +connection upgrade headers similarly to websocket. + + Upgrade: tcp + Connection: Upgrade + +When Docker daemon detects the `Upgrade` header, it will switch its status code +from **200 OK** to **101 UPGRADED** and resend the same headers. + +This might change in the future. ## 3.3 CORS Requests From af72f2128e7c4ebd12a9fef7e7381ef42a52ed0c Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Thu, 6 Nov 2014 12:03:13 -0800 Subject: [PATCH 060/513] Remove DCO small patch exception As we move forward on automating our pull request review process and tooling these exceptions hurt more than they help. For consistency we should not allow small patch exceptions for anything. The source of truth going forward for DCO and builds are the official drone status on each pull request. Signed-off-by: Michael Crosby --- CONTRIBUTING.md | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bd81695de..e4941cfa5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -272,18 +272,6 @@ Note that the old-style `Docker-DCO-1.1-Signed-off-by: ...` format is still accepted, so there is no need to update outstanding pull requests to the new format right away, but please do adjust your processes for future contributions. -#### Small patch exception - -There are several exceptions to the signing requirement. Currently these are: - -* Your patch fixes spelling or grammar errors. -* Your patch is a single line change to documentation contained in the - `docs` directory. -* Your patch fixes Markdown formatting or syntax errors in the - documentation contained in the `docs` directory. - -If you have any questions, please refer to the FAQ in the [docs](http://docs.docker.com) - ### How can I become a maintainer? * Step 1: Learn the component inside out From 3d8f40b35b7f035bbcd7b7f99064838b24afa1f6 Mon Sep 17 00:00:00 2001 From: Tianon Gravi Date: Thu, 6 Nov 2014 13:26:16 -0700 Subject: [PATCH 061/513] Remove small patch exception checking in hack/make/validate-dco Signed-off-by: Andrew Page --- project/make/validate-dco | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/project/make/validate-dco b/project/make/validate-dco index 1c75d91bf..84c47f526 100644 --- a/project/make/validate-dco +++ b/project/make/validate-dco @@ -4,7 +4,7 @@ source "$(dirname "$BASH_SOURCE")/.validate" adds=$(validate_diff --numstat | awk '{ s += $1 } END { print s }') dels=$(validate_diff --numstat | awk '{ s += $2 } END { print s }') -notDocs="$(validate_diff --numstat | awk '$3 !~ /^docs\// { print $3 }')" +#notDocs="$(validate_diff --numstat | awk '$3 !~ /^docs\// { print $3 }')" : ${adds:=0} : ${dels:=0} @@ -22,8 +22,6 @@ check_dco() { if [ $adds -eq 0 -a $dels -eq 0 ]; then echo '0 adds, 0 deletions; nothing to validate! :)' -elif [ -z "$notDocs" -a $adds -le 1 -a $dels -le 1 ]; then - echo 'Congratulations! DCO small-patch-exception material!' else commits=( $(validate_log --format='format:%H%n') ) badCommits=() From 56e3f49d1900f86b429a5a69839cc4f95af4dedc Mon Sep 17 00:00:00 2001 From: Sven Dowideit Date: Tue, 11 Nov 2014 16:15:17 +1000 Subject: [PATCH 062/513] Give maintainers the power to add contributor's DCO to a commit message, or to use their own if its a trvial change Signed-off-by: Sven Dowideit --- project/MAINTAINERS.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/project/MAINTAINERS.md b/project/MAINTAINERS.md index 0a4cd1440..1a27c9224 100644 --- a/project/MAINTAINERS.md +++ b/project/MAINTAINERS.md @@ -121,6 +121,23 @@ request that comments out your `MAINTAINERS` file entry using a `#`. Yes. Nobody should ever push to master directly. All changes should be made through a pull request. +### Helping contributors with the DCO + +The [DCO or `Sign your work`]( +https://github.com/docker/docker/blob/master/CONTRIBUTING.md#sign-your-work) +requirement is not intended as a roadblock or speed bump. + +Some Docker contributors are not as familiar with `git`, or have used a web based +editor, and thus asking them to `git commit --amend -s` is not the best way forward. + +In this case, maintainers can update the commits based on clause (c) of the DCO. The +most trivial way for a contributor to allow the maintainer to do this, is to add +a DCO signature in a Pull Requests's comment, or a maintainer can simply note that +the change is sufficiently trivial that it does not substantivly change the existing +contribution - i.e., a spelling change. + +When you add someone's DCO, please also add your own to keep a log. + ### Who assigns maintainers? Solomon has final `LGTM` approval for all pull requests to `MAINTAINERS` files. From 8123c1e9fef0eb0d6b4e89dce4089276b751906c Mon Sep 17 00:00:00 2001 From: Daehyeok Mun Date: Sun, 16 Nov 2014 22:25:10 +0900 Subject: [PATCH 063/513] Chnage LookupRemoteImage to return error This commit is patch for following comment // TODO: This method should return the errors instead of masking them and returning false Signed-off-by: Daehyeok Mun Signed-off-by: Michael Crosby --- graph/push.go | 9 ++++----- registry/registry_test.go | 9 +++++---- registry/session.go | 15 +++++++-------- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/graph/push.go b/graph/push.go index 29fc4a066..77db24381 100644 --- a/graph/push.go +++ b/graph/push.go @@ -115,17 +115,16 @@ func (s *TagStore) pushRepository(r *registry.Session, out io.Writer, localName, } for _, ep := range repoData.Endpoints { out.Write(sf.FormatStatus("", "Pushing repository %s (%d tags)", localName, nTag)) - for _, imgId := range imgList { - if r.LookupRemoteImage(imgId, ep, repoData.Tokens) { - out.Write(sf.FormatStatus("", "Image %s already pushed, skipping", utils.TruncateID(imgId))) - } else { + if err := r.LookupRemoteImage(imgId, ep, repoData.Tokens); err != nil { + log.Errorf("Error in LookupRemoteImage: %s", err) if _, err := s.pushImage(r, out, remoteName, imgId, ep, repoData.Tokens, sf); err != nil { // FIXME: Continue on error? return err } + } else { + out.Write(sf.FormatStatus("", "Image %s already pushed, skipping", utils.TruncateID(imgId))) } - for _, tag := range tagsByImage[imgId] { out.Write(sf.FormatStatus("", "Pushing tag for rev [%s] on {%s}", utils.TruncateID(imgId), ep+"repositories/"+remoteName+"/tags/"+tag)) diff --git a/registry/registry_test.go b/registry/registry_test.go index d24a5f575..5fd80da10 100644 --- a/registry/registry_test.go +++ b/registry/registry_test.go @@ -58,10 +58,11 @@ func TestGetRemoteHistory(t *testing.T) { func TestLookupRemoteImage(t *testing.T) { r := spawnTestRegistrySession(t) - found := r.LookupRemoteImage(imageID, makeURL("/v1/"), token) - assertEqual(t, found, true, "Expected remote lookup to succeed") - found = r.LookupRemoteImage("abcdef", makeURL("/v1/"), token) - assertEqual(t, found, false, "Expected remote lookup to fail") + err := r.LookupRemoteImage(imageID, makeURL("/v1/"), token) + assertEqual(t, err, nil, "Expected error of remote lookup to nil") + if err := r.LookupRemoteImage("abcdef", makeURL("/v1/"), token); err == nil { + t.Fatal("Expected error of remote lookup to not nil") + } } func TestGetRemoteImageJSON(t *testing.T) { diff --git a/registry/session.go b/registry/session.go index 4b2f55225..28cf18fbe 100644 --- a/registry/session.go +++ b/registry/session.go @@ -102,22 +102,21 @@ func (r *Session) GetRemoteHistory(imgID, registry string, token []string) ([]st } // Check if an image exists in the Registry -// TODO: This method should return the errors instead of masking them and returning false -func (r *Session) LookupRemoteImage(imgID, registry string, token []string) bool { - +func (r *Session) LookupRemoteImage(imgID, registry string, token []string) error { req, err := r.reqFactory.NewRequest("GET", registry+"images/"+imgID+"/json", nil) if err != nil { - log.Errorf("Error in LookupRemoteImage %s", err) - return false + return err } setTokenAuth(req, token) res, _, err := r.doRequest(req) if err != nil { - log.Errorf("Error in LookupRemoteImage %s", err) - return false + return err } res.Body.Close() - return res.StatusCode == 200 + if res.StatusCode != 200 { + return utils.NewHTTPRequestError(fmt.Sprintf("HTTP code %d", res.StatusCode), res) + } + return nil } // Retrieve an image from the Registry. From 6afe5bf9ede46b81fb63283c55488dbbc50d5a07 Mon Sep 17 00:00:00 2001 From: Fred Lifton Date: Tue, 16 Dec 2014 17:40:02 -0800 Subject: [PATCH 064/513] Additions for 1.4.1 release Docker-DCO-1.1-Signed-off-by: Fred Lifton (github: fredlf) --- docs/sources/reference/commandline/cli.md | 3 +++ docs/sources/release-notes.md | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index 577a4c68c..b7175561f 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -708,6 +708,9 @@ container at any point. This is useful when you want to set up a container configuration ahead of time so that it is ready to start when you need it. +Note that volumes set by `create` may be over-ridden by options set with +`start`. + Please see the [run command](#run) section for more details. #### Example diff --git a/docs/sources/release-notes.md b/docs/sources/release-notes.md index 395096150..67b4f2ec1 100644 --- a/docs/sources/release-notes.md +++ b/docs/sources/release-notes.md @@ -7,6 +7,11 @@ page_keywords: docker, documentation, about, technology, understanding, release You can view release notes for earlier version of Docker by selecting the desired version from the drop-down list at the top right of this page. +##Version 1.4.1 +(2014-12-17) + +This release fixes an issue related to mounting volumes on `create`. Details available in the [Github milestone](https://github.com/docker/docker/issues?q=milestone%3A1.4.1+is%3Aclosed). + ##Version 1.4.0 (2014-12-11) From ed7aa47dea9553a89e39c16397752ef0e791aab3 Mon Sep 17 00:00:00 2001 From: "Daniel, Dao Quang Minh" Date: Tue, 16 Dec 2014 22:55:20 -0500 Subject: [PATCH 065/513] add binary extension to docker binary in tgz Docker-DCO-1.1-Signed-off-by: Daniel, Dao Quang Minh (github: dqminh) --- project/make/tgz | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/make/tgz b/project/make/tgz index 9307200cc..8fc3cfb43 100644 --- a/project/make/tgz +++ b/project/make/tgz @@ -25,7 +25,7 @@ for d in "$CROSS/"*/*; do mkdir -p "$DEST/build" mkdir -p "$DEST/build/usr/local/bin" - cp -L "$d/$BINARY_FULLNAME" "$DEST/build/usr/local/bin/docker" + cp -L "$d/$BINARY_FULLNAME" "$DEST/build/usr/local/bin/docker$BINARY_EXTENSION" tar --numeric-owner --owner 0 -C "$DEST/build" -czf "$TGZ" usr From bc1507dfce956846ba1515ccbfcd202c06aa995b Mon Sep 17 00:00:00 2001 From: Dan Walsh Date: Wed, 17 Dec 2014 13:04:30 -0500 Subject: [PATCH 066/513] docker-run man page has screwed up indenting on --net option This patch fixes the indenting. Docker-DCO-1.1-Signed-off-by: Dan Walsh (github: rhatdan) --- docs/man/docker-run.1.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/man/docker-run.1.md b/docs/man/docker-run.1.md index 44c554508..659d3d321 100644 --- a/docs/man/docker-run.1.md +++ b/docs/man/docker-run.1.md @@ -196,7 +196,6 @@ according to RFC4862. Assign a name to the container The operator can identify a container in three ways: - UUID long identifier (“f78375b1c487e03c9438c729345e54db9d20cfa2ac1fc3494b6eb60872e74778”) UUID short identifier (“f78375b1c487”) Name (“jonah”) From 2524c0c5555f9127b4cd3d326430617f72979bab Mon Sep 17 00:00:00 2001 From: unclejack Date: Wed, 17 Dec 2014 23:23:24 +0200 Subject: [PATCH 067/513] kr/pty: vendor upstream 05017fcccf Signed-off-by: Cristian Staretu --- project/vendor.sh | 2 +- vendor/src/github.com/kr/pty/ioctl_linux.go | 42 ------------------- vendor/src/github.com/kr/pty/pty_linux.go | 9 +--- vendor/src/github.com/kr/pty/ztypes_ppc64.go | 11 +++++ .../src/github.com/kr/pty/ztypes_ppc64le.go | 11 +++++ vendor/src/github.com/kr/pty/ztypes_s390x.go | 11 +++++ 6 files changed, 36 insertions(+), 50 deletions(-) delete mode 100644 vendor/src/github.com/kr/pty/ioctl_linux.go create mode 100644 vendor/src/github.com/kr/pty/ztypes_ppc64.go create mode 100644 vendor/src/github.com/kr/pty/ztypes_ppc64le.go create mode 100644 vendor/src/github.com/kr/pty/ztypes_s390x.go diff --git a/project/vendor.sh b/project/vendor.sh index 6ebce73ca..0b56cb1b6 100755 --- a/project/vendor.sh +++ b/project/vendor.sh @@ -39,7 +39,7 @@ clone() { echo done } -clone git github.com/kr/pty 67e2db24c8 +clone git github.com/kr/pty 05017fcccf clone git github.com/gorilla/context 14f550f51a diff --git a/vendor/src/github.com/kr/pty/ioctl_linux.go b/vendor/src/github.com/kr/pty/ioctl_linux.go deleted file mode 100644 index 9fe7b0b0f..000000000 --- a/vendor/src/github.com/kr/pty/ioctl_linux.go +++ /dev/null @@ -1,42 +0,0 @@ -package pty - -// from -const ( - _IOC_NRBITS = 8 - _IOC_TYPEBITS = 8 - - _IOC_SIZEBITS = 14 - _IOC_DIRBITS = 2 - - _IOC_NRSHIFT = 0 - _IOC_TYPESHIFT = _IOC_NRSHIFT + _IOC_NRBITS - _IOC_SIZESHIFT = _IOC_TYPESHIFT + _IOC_TYPEBITS - _IOC_DIRSHIFT = _IOC_SIZESHIFT + _IOC_SIZEBITS - - _IOC_NONE uint = 0 - _IOC_WRITE uint = 1 - _IOC_READ uint = 2 -) - -func _IOC(dir uint, ioctl_type byte, nr byte, size uintptr) uintptr { - return (uintptr(dir)<<_IOC_DIRSHIFT | - uintptr(ioctl_type)<<_IOC_TYPESHIFT | - uintptr(nr)<<_IOC_NRSHIFT | - size<<_IOC_SIZESHIFT) -} - -func _IO(ioctl_type byte, nr byte) uintptr { - return _IOC(_IOC_NONE, ioctl_type, nr, 0) -} - -func _IOR(ioctl_type byte, nr byte, size uintptr) uintptr { - return _IOC(_IOC_READ, ioctl_type, nr, size) -} - -func _IOW(ioctl_type byte, nr byte, size uintptr) uintptr { - return _IOC(_IOC_WRITE, ioctl_type, nr, size) -} - -func _IOWR(ioctl_type byte, nr byte, size uintptr) uintptr { - return _IOC(_IOC_READ|_IOC_WRITE, ioctl_type, nr, size) -} diff --git a/vendor/src/github.com/kr/pty/pty_linux.go b/vendor/src/github.com/kr/pty/pty_linux.go index 6e5a04241..cb901a21e 100644 --- a/vendor/src/github.com/kr/pty/pty_linux.go +++ b/vendor/src/github.com/kr/pty/pty_linux.go @@ -7,11 +7,6 @@ import ( "unsafe" ) -var ( - ioctl_TIOCGPTN = _IOR('T', 0x30, unsafe.Sizeof(_C_uint(0))) /* Get Pty Number (of pty-mux device) */ - ioctl_TIOCSPTLCK = _IOW('T', 0x31, unsafe.Sizeof(_C_int(0))) /* Lock/unlock Pty */ -) - func open() (pty, tty *os.File, err error) { p, err := os.OpenFile("/dev/ptmx", os.O_RDWR, 0) if err != nil { @@ -37,7 +32,7 @@ func open() (pty, tty *os.File, err error) { func ptsname(f *os.File) (string, error) { var n _C_uint - err := ioctl(f.Fd(), ioctl_TIOCGPTN, uintptr(unsafe.Pointer(&n))) + err := ioctl(f.Fd(), syscall.TIOCGPTN, uintptr(unsafe.Pointer(&n))) if err != nil { return "", err } @@ -47,5 +42,5 @@ func ptsname(f *os.File) (string, error) { func unlockpt(f *os.File) error { var u _C_int // use TIOCSPTLCK with a zero valued arg to clear the slave pty lock - return ioctl(f.Fd(), ioctl_TIOCSPTLCK, uintptr(unsafe.Pointer(&u))) + return ioctl(f.Fd(), syscall.TIOCSPTLCK, uintptr(unsafe.Pointer(&u))) } diff --git a/vendor/src/github.com/kr/pty/ztypes_ppc64.go b/vendor/src/github.com/kr/pty/ztypes_ppc64.go new file mode 100644 index 000000000..4e1af8431 --- /dev/null +++ b/vendor/src/github.com/kr/pty/ztypes_ppc64.go @@ -0,0 +1,11 @@ +// +build ppc64 + +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types.go + +package pty + +type ( + _C_int int32 + _C_uint uint32 +) diff --git a/vendor/src/github.com/kr/pty/ztypes_ppc64le.go b/vendor/src/github.com/kr/pty/ztypes_ppc64le.go new file mode 100644 index 000000000..e6780f4e2 --- /dev/null +++ b/vendor/src/github.com/kr/pty/ztypes_ppc64le.go @@ -0,0 +1,11 @@ +// +build ppc64le + +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types.go + +package pty + +type ( + _C_int int32 + _C_uint uint32 +) diff --git a/vendor/src/github.com/kr/pty/ztypes_s390x.go b/vendor/src/github.com/kr/pty/ztypes_s390x.go new file mode 100644 index 000000000..a7452b61c --- /dev/null +++ b/vendor/src/github.com/kr/pty/ztypes_s390x.go @@ -0,0 +1,11 @@ +// +build s390x + +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types.go + +package pty + +type ( + _C_int int32 + _C_uint uint32 +) From 5d6eca6642c5749099513f1f66bb44e004aa0938 Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Mon, 15 Dec 2014 11:44:15 -0800 Subject: [PATCH 068/513] Add docker-py integration tests aginst the docker daemon This clones and run the integration tests for docker-py master as part of the integration tests created on master. docker-py hits the api directly and should be a good way to identify regressions in the api. Signed-off-by: Michael Crosby --- Dockerfile | 2 ++ Makefile | 2 +- project/make/test-docker-py | 43 +++++++++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 project/make/test-docker-py diff --git a/Dockerfile b/Dockerfile index cbddccac2..c276eb3e7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -42,6 +42,8 @@ RUN apt-get update && apt-get install -y \ lxc=1.0* \ mercurial \ parallel \ + python-mock \ + python-pip \ reprepro \ ruby1.9.1 \ ruby1.9.1-dev \ diff --git a/Makefile b/Makefile index 6f76fa4d2..70799d3c2 100644 --- a/Makefile +++ b/Makefile @@ -56,7 +56,7 @@ docs-release: docs-build $(DOCKER_RUN_DOCS) -e OPTIONS -e BUILD_ROOT "$(DOCKER_DOCS_IMAGE)" ./release.sh test: build - $(DOCKER_RUN_DOCKER) hack/make.sh binary cross test-unit test-integration test-integration-cli + $(DOCKER_RUN_DOCKER) hack/make.sh binary cross test-unit test-integration test-integration-cli test-docker-py test-unit: build $(DOCKER_RUN_DOCKER) hack/make.sh test-unit diff --git a/project/make/test-docker-py b/project/make/test-docker-py new file mode 100644 index 000000000..2a39c6fa5 --- /dev/null +++ b/project/make/test-docker-py @@ -0,0 +1,43 @@ +#!/bin/bash +set -e + +DEST=$1 + +DOCKER_GRAPHDRIVER=${DOCKER_GRAPHDRIVER:-vfs} +DOCKER_EXECDRIVER=${DOCKER_EXECDRIVER:-native} + +# subshell so that we can export PATH without breaking other things +exec > >(tee -a $DEST/test.log) 2>&1 +( + export PATH="$DEST/../binary:$DEST/../dynbinary:$PATH" + + if ! command -v docker &> /dev/null; then + echo >&2 'error: binary or dynbinary must be run before test-docker-py' + false + fi + + # intentionally open a couple bogus file descriptors to help test that they get scrubbed in containers + exec 41>&1 42>&2 + + ( set -x; exec \ + docker --daemon --debug \ + --storage-driver "$DOCKER_GRAPHDRIVER" \ + --exec-driver "$DOCKER_EXECDRIVER" \ + --pidfile "$DEST/docker.pid" \ + &> "$DEST/docker.log" + ) & + + mkdir -p /tmp/dockerpy-tests && cd /tmp/dockerpy-tests + git clone https://github.com/docker/docker-py.git + cd docker-py + git checkout 0.6.0-integration + python setup.py install + python tests/integration_test.py + + for pid in $(find "$DEST" -name docker.pid); do + DOCKER_PID=$(set -x; cat "$pid") + ( set -x; kill $DOCKER_PID ) + wait $DOCKERD_PID || true + done +) + From c16e41245ebaf426a5ad27ebe82597647c7743a3 Mon Sep 17 00:00:00 2001 From: Srini Brahmaroutu Date: Wed, 17 Dec 2014 22:33:50 +0000 Subject: [PATCH 069/513] Fix to avoid a compile error due to float to int truncation with GCCGO Signed-off-by: Srini Brahmaroutu --- pkg/units/size_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/units/size_test.go b/pkg/units/size_test.go index 5b329fcf6..3e410b0db 100644 --- a/pkg/units/size_test.go +++ b/pkg/units/size_test.go @@ -23,9 +23,9 @@ func TestHumanSize(t *testing.T) { assertEquals(t, "1 MB", HumanSize(1000000)) assertEquals(t, "1.049 MB", HumanSize(1048576)) assertEquals(t, "2 MB", HumanSize(2*MB)) - assertEquals(t, "3.42 GB", HumanSize(3.42*GB)) - assertEquals(t, "5.372 TB", HumanSize(5.372*TB)) - assertEquals(t, "2.22 PB", HumanSize(2.22*PB)) + assertEquals(t, "3.42 GB", HumanSize(int64(float64(3.42*GB)))) + assertEquals(t, "5.372 TB", HumanSize(int64(float64(5.372*TB)))) + assertEquals(t, "2.22 PB", HumanSize(int64(float64(2.22*PB)))) } func TestFromHumanSize(t *testing.T) { From de2a91970dfd1f6078179ffffd63ec1842c95bd9 Mon Sep 17 00:00:00 2001 From: Sven Dowideit Date: Thu, 18 Dec 2014 09:05:27 +1000 Subject: [PATCH 070/513] redirect openSUSE docs to SUSE doc Docker-DCO-1.1-Signed-off-by: Sven Dowideit (github: SvenDowideit) --- docs/mkdocs.yml | 2 +- docs/s3_website.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 06f9064d9..96c89c231 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -45,7 +45,7 @@ pages: - ['installation/archlinux.md', 'Installation', 'Arch Linux'] - ['installation/frugalware.md', 'Installation', 'FrugalWare'] - ['installation/fedora.md', 'Installation', 'Fedora'] -- ['installation/openSUSE.md', 'Installation', 'openSUSE'] +- ['installation/SUSE.md', 'Installation', 'SUSE'] - ['installation/cruxlinux.md', 'Installation', 'CRUX Linux'] - ['installation/windows.md', 'Installation', 'Microsoft Windows'] - ['installation/binaries.md', 'Installation', 'Binaries'] diff --git a/docs/s3_website.json b/docs/s3_website.json index 224ba816e..e468b678a 100644 --- a/docs/s3_website.json +++ b/docs/s3_website.json @@ -30,7 +30,8 @@ { "Condition": { "KeyPrefixEquals": "examples/ambassador_pattern_linking/" }, "Redirect": { "HostName": "$BUCKET", "ReplaceKeyPrefixWith": "articles/ambassador_pattern_linking/" } }, { "Condition": { "KeyPrefixEquals": "examples/using_supervisord/" }, "Redirect": { "HostName": "$BUCKET", "ReplaceKeyPrefixWith": "articles/using_supervisord/" } }, { "Condition": { "KeyPrefixEquals": "reference/api/registry_index_spec/" }, "Redirect": { "HostName": "$BUCKET", "ReplaceKeyPrefixWith": "reference/api/hub_registry_spec/" } }, - { "Condition": { "KeyPrefixEquals": "use/" }, "Redirect": { "HostName": "$BUCKET", "ReplaceKeyPrefixWith": "examples/" } } + { "Condition": { "KeyPrefixEquals": "use/" }, "Redirect": { "HostName": "$BUCKET", "ReplaceKeyPrefixWith": "examples/" } }, + { "Condition": { "KeyPrefixEquals": "installation/openSUSE/" }, "Redirect": { "HostName": "$BUCKET", "ReplaceKeyPrefixWith": "installation/SUSE/" } } ] } From 35a22c9e12c05e2a0a205964702ced78ea39d7a1 Mon Sep 17 00:00:00 2001 From: Josh Hawn Date: Wed, 17 Dec 2014 18:26:03 -0800 Subject: [PATCH 071/513] Refactor to optimize storage driver ApplyDiff() To avoid an expensive call to archive.ChangesDirs() which walks two directory trees and compares every entry, archive.ApplyLayer() has been extended to also return the size of the layer changes. Docker-DCO-1.1-Signed-off-by: Josh Hawn (github: jlhawn) --- daemon/graphdriver/aufs/aufs.go | 4 +-- daemon/graphdriver/driver.go | 4 +-- daemon/graphdriver/fsdiff.go | 30 ++++--------------- daemon/graphdriver/overlay/overlay.go | 13 +++----- pkg/archive/changes_test.go | 2 +- pkg/archive/diff.go | 40 ++++++++++++++----------- pkg/archive/utils_test.go | 3 +- pkg/chrootarchive/archive_test.go | 2 +- pkg/chrootarchive/diff.go | 43 ++++++++++++++++++++++----- 9 files changed, 75 insertions(+), 66 deletions(-) diff --git a/daemon/graphdriver/aufs/aufs.go b/daemon/graphdriver/aufs/aufs.go index 210f623e3..82a5c8905 100644 --- a/daemon/graphdriver/aufs/aufs.go +++ b/daemon/graphdriver/aufs/aufs.go @@ -312,7 +312,7 @@ func (a *Driver) applyDiff(id string, diff archive.ArchiveReader) error { // DiffSize calculates the changes between the specified id // and its parent and returns the size in bytes of the changes // relative to its base filesystem directory. -func (a *Driver) DiffSize(id, parent string) (bytes int64, err error) { +func (a *Driver) DiffSize(id, parent string) (size int64, err error) { // AUFS doesn't need the parent layer to calculate the diff size. return utils.TreeSize(path.Join(a.rootPath(), "diff", id)) } @@ -320,7 +320,7 @@ func (a *Driver) DiffSize(id, parent string) (bytes int64, err error) { // ApplyDiff extracts the changeset from the given diff into the // layer with the specified id and parent, returning the size of the // new layer in bytes. -func (a *Driver) ApplyDiff(id, parent string, diff archive.ArchiveReader) (bytes int64, err error) { +func (a *Driver) ApplyDiff(id, parent string, diff archive.ArchiveReader) (size int64, err error) { // AUFS doesn't need the parent id to apply the diff. if err = a.applyDiff(id, diff); err != nil { return diff --git a/daemon/graphdriver/driver.go b/daemon/graphdriver/driver.go index 95479bf64..d96961472 100644 --- a/daemon/graphdriver/driver.go +++ b/daemon/graphdriver/driver.go @@ -63,11 +63,11 @@ type Driver interface { // ApplyDiff extracts the changeset from the given diff into the // layer with the specified id and parent, returning the size of the // new layer in bytes. - ApplyDiff(id, parent string, diff archive.ArchiveReader) (bytes int64, err error) + ApplyDiff(id, parent string, diff archive.ArchiveReader) (size int64, err error) // DiffSize calculates the changes between the specified id // and its parent and returns the size in bytes of the changes // relative to its base filesystem directory. - DiffSize(id, parent string) (bytes int64, err error) + DiffSize(id, parent string) (size int64, err error) } var ( diff --git a/daemon/graphdriver/fsdiff.go b/daemon/graphdriver/fsdiff.go index 48852a563..ab1b08f62 100644 --- a/daemon/graphdriver/fsdiff.go +++ b/daemon/graphdriver/fsdiff.go @@ -3,14 +3,12 @@ package graphdriver import ( - "fmt" "time" log "github.com/Sirupsen/logrus" "github.com/docker/docker/pkg/archive" "github.com/docker/docker/pkg/chrootarchive" "github.com/docker/docker/pkg/ioutils" - "github.com/docker/docker/utils" ) // naiveDiffDriver takes a ProtoDriver and adds the @@ -27,8 +25,8 @@ type naiveDiffDriver struct { // it may or may not support on its own: // Diff(id, parent string) (archive.Archive, error) // Changes(id, parent string) ([]archive.Change, error) -// ApplyDiff(id, parent string, diff archive.ArchiveReader) (bytes int64, err error) -// DiffSize(id, parent string) (bytes int64, err error) +// ApplyDiff(id, parent string, diff archive.ArchiveReader) (size int64, err error) +// DiffSize(id, parent string) (size int64, err error) func NaiveDiffDriver(driver ProtoDriver) Driver { return &naiveDiffDriver{ProtoDriver: driver} } @@ -111,7 +109,7 @@ func (gdw *naiveDiffDriver) Changes(id, parent string) ([]archive.Change, error) // ApplyDiff extracts the changeset from the given diff into the // layer with the specified id and parent, returning the size of the // new layer in bytes. -func (gdw *naiveDiffDriver) ApplyDiff(id, parent string, diff archive.ArchiveReader) (bytes int64, err error) { +func (gdw *naiveDiffDriver) ApplyDiff(id, parent string, diff archive.ArchiveReader) (size int64, err error) { driver := gdw.ProtoDriver // Mount the root filesystem so we can apply the diff/layer. @@ -123,34 +121,18 @@ func (gdw *naiveDiffDriver) ApplyDiff(id, parent string, diff archive.ArchiveRea start := time.Now().UTC() log.Debugf("Start untar layer") - if err = chrootarchive.ApplyLayer(layerFs, diff); err != nil { + if size, err = chrootarchive.ApplyLayer(layerFs, diff); err != nil { return } log.Debugf("Untar time: %vs", time.Now().UTC().Sub(start).Seconds()) - if parent == "" { - return utils.TreeSize(layerFs) - } - - parentFs, err := driver.Get(parent, "") - if err != nil { - err = fmt.Errorf("Driver %s failed to get image parent %s: %s", driver, parent, err) - return - } - defer driver.Put(parent) - - changes, err := archive.ChangesDirs(layerFs, parentFs) - if err != nil { - return - } - - return archive.ChangesSize(layerFs, changes), nil + return } // DiffSize calculates the changes between the specified layer // and its parent and returns the size in bytes of the changes // relative to its base filesystem directory. -func (gdw *naiveDiffDriver) DiffSize(id, parent string) (bytes int64, err error) { +func (gdw *naiveDiffDriver) DiffSize(id, parent string) (size int64, err error) { driver := gdw.ProtoDriver changes, err := gdw.Changes(id, parent) diff --git a/daemon/graphdriver/overlay/overlay.go b/daemon/graphdriver/overlay/overlay.go index 2569ccb6d..68b6b0ed3 100644 --- a/daemon/graphdriver/overlay/overlay.go +++ b/daemon/graphdriver/overlay/overlay.go @@ -28,7 +28,7 @@ var ( type ApplyDiffProtoDriver interface { graphdriver.ProtoDriver - ApplyDiff(id, parent string, diff archive.ArchiveReader) (bytes int64, err error) + ApplyDiff(id, parent string, diff archive.ArchiveReader) (size int64, err error) } type naiveDiffDriverWithApply struct { @@ -309,7 +309,7 @@ func (d *Driver) Put(id string) { delete(d.active, id) } -func (d *Driver) ApplyDiff(id string, parent string, diff archive.ArchiveReader) (bytes int64, err error) { +func (d *Driver) ApplyDiff(id string, parent string, diff archive.ArchiveReader) (size int64, err error) { dir := d.dir(id) if parent == "" { @@ -347,7 +347,7 @@ func (d *Driver) ApplyDiff(id string, parent string, diff archive.ArchiveReader) return 0, err } - if err := chrootarchive.ApplyLayer(tmpRootDir, diff); err != nil { + if size, err = chrootarchive.ApplyLayer(tmpRootDir, diff); err != nil { return 0, err } @@ -356,12 +356,7 @@ func (d *Driver) ApplyDiff(id string, parent string, diff archive.ArchiveReader) return 0, err } - changes, err := archive.ChangesDirs(rootDir, parentRootDir) - if err != nil { - return 0, err - } - - return archive.ChangesSize(rootDir, changes), nil + return } func (d *Driver) Exists(id string) bool { diff --git a/pkg/archive/changes_test.go b/pkg/archive/changes_test.go index 34c0f0da6..6b8f2354b 100644 --- a/pkg/archive/changes_test.go +++ b/pkg/archive/changes_test.go @@ -286,7 +286,7 @@ func TestApplyLayer(t *testing.T) { t.Fatal(err) } - if err := ApplyLayer(src, layerCopy); err != nil { + if _, err := ApplyLayer(src, layerCopy); err != nil { t.Fatal(err) } diff --git a/pkg/archive/diff.go b/pkg/archive/diff.go index ba22c41f3..ca282071f 100644 --- a/pkg/archive/diff.go +++ b/pkg/archive/diff.go @@ -15,7 +15,7 @@ import ( "github.com/docker/docker/pkg/system" ) -func UnpackLayer(dest string, layer ArchiveReader) error { +func UnpackLayer(dest string, layer ArchiveReader) (size int64, err error) { tr := tar.NewReader(layer) trBuf := pools.BufioReader32KPool.Get(tr) defer pools.BufioReader32KPool.Put(trBuf) @@ -33,9 +33,11 @@ func UnpackLayer(dest string, layer ArchiveReader) error { break } if err != nil { - return err + return 0, err } + size += hdr.Size + // Normalize name, for safety and for a simple is-root check hdr.Name = filepath.Clean(hdr.Name) @@ -48,7 +50,7 @@ func UnpackLayer(dest string, layer ArchiveReader) error { if _, err := os.Lstat(parentPath); err != nil && os.IsNotExist(err) { err = os.MkdirAll(parentPath, 0600) if err != nil { - return err + return 0, err } } } @@ -63,12 +65,12 @@ func UnpackLayer(dest string, layer ArchiveReader) error { aufsHardlinks[basename] = hdr if aufsTempdir == "" { if aufsTempdir, err = ioutil.TempDir("", "dockerplnk"); err != nil { - return err + return 0, err } defer os.RemoveAll(aufsTempdir) } if err := createTarFile(filepath.Join(aufsTempdir, basename), dest, hdr, tr, true); err != nil { - return err + return 0, err } } continue @@ -77,10 +79,10 @@ func UnpackLayer(dest string, layer ArchiveReader) error { path := filepath.Join(dest, hdr.Name) rel, err := filepath.Rel(dest, path) if err != nil { - return err + return 0, err } if strings.HasPrefix(rel, "..") { - return breakoutError(fmt.Errorf("%q is outside of %q", hdr.Name, dest)) + return 0, breakoutError(fmt.Errorf("%q is outside of %q", hdr.Name, dest)) } base := filepath.Base(path) @@ -88,7 +90,7 @@ func UnpackLayer(dest string, layer ArchiveReader) error { originalBase := base[len(".wh."):] originalPath := filepath.Join(filepath.Dir(path), originalBase) if err := os.RemoveAll(originalPath); err != nil { - return err + return 0, err } } else { // If path exits we almost always just want to remove and replace it. @@ -98,7 +100,7 @@ func UnpackLayer(dest string, layer ArchiveReader) error { if fi, err := os.Lstat(path); err == nil { if !(fi.IsDir() && hdr.Typeflag == tar.TypeDir) { if err := os.RemoveAll(path); err != nil { - return err + return 0, err } } } @@ -113,18 +115,18 @@ func UnpackLayer(dest string, layer ArchiveReader) error { linkBasename := filepath.Base(hdr.Linkname) srcHdr = aufsHardlinks[linkBasename] if srcHdr == nil { - return fmt.Errorf("Invalid aufs hardlink") + return 0, fmt.Errorf("Invalid aufs hardlink") } tmpFile, err := os.Open(filepath.Join(aufsTempdir, linkBasename)) if err != nil { - return err + return 0, err } defer tmpFile.Close() srcData = tmpFile } if err := createTarFile(path, dest, srcHdr, srcData, true); err != nil { - return err + return 0, err } // Directory mtimes must be handled at the end to avoid further @@ -139,27 +141,29 @@ func UnpackLayer(dest string, layer ArchiveReader) error { path := filepath.Join(dest, hdr.Name) ts := []syscall.Timespec{timeToTimespec(hdr.AccessTime), timeToTimespec(hdr.ModTime)} if err := syscall.UtimesNano(path, ts); err != nil { - return err + return 0, err } } - return nil + + return size, nil } // ApplyLayer parses a diff in the standard layer format from `layer`, and -// applies it to the directory `dest`. -func ApplyLayer(dest string, layer ArchiveReader) error { +// applies it to the directory `dest`. Returns the size in bytes of the +// contents of the layer. +func ApplyLayer(dest string, layer ArchiveReader) (int64, error) { dest = filepath.Clean(dest) // We need to be able to set any perms oldmask, err := system.Umask(0) if err != nil { - return err + return 0, err } defer system.Umask(oldmask) // ignore err, ErrNotSupportedPlatform layer, err = DecompressStream(layer) if err != nil { - return err + return 0, err } return UnpackLayer(dest, layer) } diff --git a/pkg/archive/utils_test.go b/pkg/archive/utils_test.go index 3624fe5af..904802720 100644 --- a/pkg/archive/utils_test.go +++ b/pkg/archive/utils_test.go @@ -17,7 +17,8 @@ var testUntarFns = map[string]func(string, io.Reader) error{ return Untar(r, dest, nil) }, "applylayer": func(dest string, r io.Reader) error { - return ApplyLayer(dest, ArchiveReader(r)) + _, err := ApplyLayer(dest, ArchiveReader(r)) + return err }, } diff --git a/pkg/chrootarchive/archive_test.go b/pkg/chrootarchive/archive_test.go index 0fe3d64f9..bb8a22dc7 100644 --- a/pkg/chrootarchive/archive_test.go +++ b/pkg/chrootarchive/archive_test.go @@ -95,7 +95,7 @@ func TestChrootApplyEmptyArchiveFromSlowReader(t *testing.T) { t.Fatal(err) } stream := &slowEmptyTarReader{size: 10240, chunkSize: 1024} - if err := ApplyLayer(dest, stream); err != nil { + if _, err := ApplyLayer(dest, stream); err != nil { t.Fatal(err) } } diff --git a/pkg/chrootarchive/diff.go b/pkg/chrootarchive/diff.go index d4e9529b6..ac1cbf9be 100644 --- a/pkg/chrootarchive/diff.go +++ b/pkg/chrootarchive/diff.go @@ -1,6 +1,8 @@ package chrootarchive import ( + "bytes" + "encoding/json" "flag" "fmt" "io" @@ -14,6 +16,10 @@ import ( "github.com/docker/docker/pkg/reexec" ) +type applyLayerResponse struct { + LayerSize int64 `json:"layerSize"` +} + func applyLayer() { runtime.LockOSThread() flag.Parse() @@ -21,6 +27,7 @@ func applyLayer() { if err := chroot(flag.Arg(0)); err != nil { fatal(err) } + // We need to be able to set any perms oldmask := syscall.Umask(0) defer syscall.Umask(oldmask) @@ -28,33 +35,53 @@ func applyLayer() { if err != nil { fatal(err) } + os.Setenv("TMPDIR", tmpDir) - err = archive.UnpackLayer("/", os.Stdin) + size, err := archive.UnpackLayer("/", os.Stdin) os.RemoveAll(tmpDir) if err != nil { fatal(err) } - os.RemoveAll(tmpDir) + + encoder := json.NewEncoder(os.Stdout) + if err := encoder.Encode(applyLayerResponse{size}); err != nil { + fatal(fmt.Errorf("unable to encode layerSize JSON: %s", err)) + } + + flush(os.Stdout) flush(os.Stdin) os.Exit(0) } -func ApplyLayer(dest string, layer archive.ArchiveReader) error { +func ApplyLayer(dest string, layer archive.ArchiveReader) (size int64, err error) { dest = filepath.Clean(dest) decompressed, err := archive.DecompressStream(layer) if err != nil { - return err + return 0, err } + defer func() { if c, ok := decompressed.(io.Closer); ok { c.Close() } }() + cmd := reexec.Command("docker-applyLayer", dest) cmd.Stdin = decompressed - out, err := cmd.CombinedOutput() - if err != nil { - return fmt.Errorf("ApplyLayer %s %s", err, out) + + outBuf, errBuf := new(bytes.Buffer), new(bytes.Buffer) + cmd.Stdout, cmd.Stderr = outBuf, errBuf + + if err = cmd.Run(); err != nil { + return 0, fmt.Errorf("ApplyLayer %s stdout: %s stderr: %s", err, outBuf, errBuf) } - return nil + + // Stdout should be a valid JSON struct representing an applyLayerResponse. + response := applyLayerResponse{} + decoder := json.NewDecoder(outBuf) + if err = decoder.Decode(&response); err != nil { + return 0, fmt.Errorf("unable to decode ApplyLayer JSON response: %s", err) + } + + return response.LayerSize, nil } From f21f9f856e9d5af23521f131799028c2e67c04ed Mon Sep 17 00:00:00 2001 From: Doug Davis Date: Fri, 12 Dec 2014 10:32:11 -0800 Subject: [PATCH 072/513] Allow for relative paths on ADD/COPY Moved Tianon's PR from: https://github.com/docker/docker/pull/7870 on top of the latest code Closes: #3936 Signed-off-by: Andrew Page Signed-off-by: Doug Davis --- builder/dispatchers.go | 11 +++---- builder/internals.go | 12 +++++++ docs/man/Dockerfile.5.md | 20 +++++++++--- docs/man/docker-build.1.md | 10 +++--- docs/sources/reference/builder.md | 12 ++++--- integration-cli/docker_cli_build_test.go | 40 ++++++++++++++++++++++++ 6 files changed, 85 insertions(+), 20 deletions(-) diff --git a/builder/dispatchers.go b/builder/dispatchers.go index eb9485fa7..9fb44fc45 100644 --- a/builder/dispatchers.go +++ b/builder/dispatchers.go @@ -172,15 +172,12 @@ func workdir(b *Builder, args []string, attributes map[string]bool, original str workdir := args[0] - if workdir[0] == '/' { - b.Config.WorkingDir = workdir - } else { - if b.Config.WorkingDir == "" { - b.Config.WorkingDir = "/" - } - b.Config.WorkingDir = filepath.Join(b.Config.WorkingDir, workdir) + if !filepath.IsAbs(workdir) { + workdir = filepath.Join("/", b.Config.WorkingDir, workdir) } + b.Config.WorkingDir = workdir + return b.commit("", b.Config.Cmd, fmt.Sprintf("WORKDIR %v", workdir)) } diff --git a/builder/internals.go b/builder/internals.go index c1fd617a5..d30a7da81 100644 --- a/builder/internals.go +++ b/builder/internals.go @@ -217,6 +217,18 @@ func calcCopyInfo(b *Builder, cmdName string, cInfos *[]*copyInfo, origPath stri } origPath = strings.TrimPrefix(origPath, "./") + // Twiddle the destPath when its a relative path - meaning, make it + // relative to the WORKINGDIR + if !filepath.IsAbs(destPath) { + hasSlash := strings.HasSuffix(destPath, "/") + destPath = filepath.Join("/", b.Config.WorkingDir, destPath) + + // Make sure we preserve any trailing slash + if hasSlash { + destPath += "/" + } + } + // In the remote/URL case, download it and gen its hashcode if urlutil.IsURL(origPath) { if !allowRemote { diff --git a/docs/man/Dockerfile.5.md b/docs/man/Dockerfile.5.md index 4104dc232..0114f30ba 100644 --- a/docs/man/Dockerfile.5.md +++ b/docs/man/Dockerfile.5.md @@ -132,12 +132,22 @@ or **ADD** --**ADD ... ** The ADD instruction copies new files, directories - or remote file URLs to the filesystem of the container at path . + or remote file URLs to the filesystem of the container at path . Mutliple resources may be specified but if they are files or directories - then they must be relative to the source directory that is being built - (the context of the build). is the absolute path to - which the source is copied inside the target container. All new files and - directories are created with mode 0755, with uid and gid 0. + then they must be relative to the source directory that is being built + (the context of the build). The is the absolute path, or path relative + to `WORKDIR`, into which the source is copied inside the target container. + All new files and directories are created with mode 0755 and with the uid + and gid of 0. + +**COPY** + --**COPY ** The COPY instruction copies new files from and + adds them to the filesystem of the container at path . The must be + the path to a file or directory relative to the source directory that is + being built (the context of the build) or a remote file URL. The `` is an + absolute path, or a path relative to `WORKDIR`, into which the source will + be copied inside the target container. All new files and directories are + created with mode 0755 and with the uid and gid of 0. **ENTRYPOINT** --**ENTRYPOINT** has two forms: ENTRYPOINT ["executable", "param1", "param2"] diff --git a/docs/man/docker-build.1.md b/docs/man/docker-build.1.md index 67d7343af..3fed99640 100644 --- a/docs/man/docker-build.1.md +++ b/docs/man/docker-build.1.md @@ -65,10 +65,12 @@ directory called httpd may be used to store Dockerfiles for Apache web server images. It is also a good practice to add the files required for the image to the -sub-directory. These files will then be specified with the `ADD` instruction -in the Dockerfile. Note: If you include a tar file (a good practice!), then -Docker will automatically extract the contents of the tar file -specified within the `ADD` instruction into the specified target. +sub-directory. These files will then be specified with the `COPY` or `ADD` +instructions in the `Dockerfile`. + +Note: If you include a tar file (a good practice), then Docker will +automatically extract the contents of the tar file specified within the `ADD` +instruction into the specified target. ## Building an image and naming that image diff --git a/docs/sources/reference/builder.md b/docs/sources/reference/builder.md index adc308c9d..e8b2b0696 100644 --- a/docs/sources/reference/builder.md +++ b/docs/sources/reference/builder.md @@ -397,8 +397,10 @@ For most command line uses this should act as expected, for example: ADD hom* /mydir/ # adds all files starting with "hom" ADD hom?.txt /mydir/ # ? is replaced with any single character -The `` is the absolute path to which the source will be copied inside the -destination container. +The `` is an absolute path, or a path relative to `WORKDIR`, into which +the source will be copied inside the destination container. + + ADD test aDir/ # adds "test" to `WORKDIR`/aDir/ All new files and directories are created with a UID and GID of 0. @@ -494,8 +496,10 @@ For most command line uses this should act as expected, for example: COPY hom* /mydir/ # adds all files starting with "hom" COPY hom?.txt /mydir/ # ? is replaced with any single character -The `` is the absolute path to which the source will be copied inside the -destination container. +The `` is an absolute path, or a path relative to `WORKDIR`, into which +the source will be copied inside the destination container. + + COPY test aDir/ # adds "test" to `WORKDIR`/aDir/ All new files and directories are created with a UID and GID of 0. diff --git a/integration-cli/docker_cli_build_test.go b/integration-cli/docker_cli_build_test.go index ccd0b6d3e..0d527119c 100644 --- a/integration-cli/docker_cli_build_test.go +++ b/integration-cli/docker_cli_build_test.go @@ -1829,6 +1829,46 @@ func TestBuildWorkdirWithEnvVariables(t *testing.T) { logDone("build - workdir with env variables") } +func TestBuildRelativeCopy(t *testing.T) { + name := "testbuildrelativecopy" + defer deleteImages(name) + dockerfile := ` + FROM busybox + WORKDIR /test1 + WORKDIR test2 + RUN [ "$PWD" = '/test1/test2' ] + COPY foo ./ + RUN [ "$(cat /test1/test2/foo)" = 'hello' ] + ADD foo ./bar/baz + RUN [ "$(cat /test1/test2/bar/baz)" = 'hello' ] + COPY foo ./bar/baz2 + RUN [ "$(cat /test1/test2/bar/baz2)" = 'hello' ] + WORKDIR .. + COPY foo ./ + RUN [ "$(cat /test1/foo)" = 'hello' ] + COPY foo /test3/ + RUN [ "$(cat /test3/foo)" = 'hello' ] + WORKDIR /test4 + COPY . . + RUN [ "$(cat /test4/foo)" = 'hello' ] + WORKDIR /test5/test6 + COPY foo ../ + RUN [ "$(cat /test5/foo)" = 'hello' ] + ` + ctx, err := fakeContext(dockerfile, map[string]string{ + "foo": "hello", + }) + defer ctx.Close() + if err != nil { + t.Fatal(err) + } + _, err = buildImageFromContext(name, ctx, false) + if err != nil { + t.Fatal(err) + } + logDone("build - relative copy/add") +} + func TestBuildEnv(t *testing.T) { name := "testbuildenv" expected := "[PATH=/test:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin PORT=2375]" From 801fd1af256134b749ee3dd29ea5ed41d2594482 Mon Sep 17 00:00:00 2001 From: Mary Anthony Date: Thu, 18 Dec 2014 09:10:08 -0800 Subject: [PATCH 073/513] Argghhh...I need to automate that signoff. Fixing lack of signoff Signed-off-by: Mary Anthony --- docs/sources/contributing/devenvironment.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/sources/contributing/devenvironment.md b/docs/sources/contributing/devenvironment.md index 5030a2d75..c9efd3bf9 100644 --- a/docs/sources/contributing/devenvironment.md +++ b/docs/sources/contributing/devenvironment.md @@ -52,8 +52,9 @@ On Mac OS X, from within the `boot2docker` shell: $ make build > **Note**: -> On Mac OS X, **do not** build Docker make targets such as `build`, `binary`, and `test` -> under root using the `sudo` command. +> On Mac OS X, the Docker make targets such as `build`, `binary`, and `test` +> should **not** be built by the 'root' user. Therefore, you shouldn't use `sudo` when +> running these commands on OS X. If the build is successful, congratulations! You have produced a clean build of docker, neatly encapsulated in a standard build environment. From e7c0587d541cd9f224df622b2f97c918e68cfb05 Mon Sep 17 00:00:00 2001 From: unclejack Date: Thu, 18 Dec 2014 22:58:14 +0200 Subject: [PATCH 074/513] pkg/tarsum: delete the logging code Signed-off-by: Cristian Staretu --- pkg/tarsum/tarsum.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pkg/tarsum/tarsum.go b/pkg/tarsum/tarsum.go index ba09d4a12..c9f1315cf 100644 --- a/pkg/tarsum/tarsum.go +++ b/pkg/tarsum/tarsum.go @@ -10,8 +10,6 @@ import ( "strings" "github.com/docker/docker/vendor/src/code.google.com/p/go/src/pkg/archive/tar" - - log "github.com/Sirupsen/logrus" ) const ( @@ -228,11 +226,9 @@ func (ts *tarSum) Sum(extra []byte) string { h.Write(extra) } for _, fis := range ts.sums { - log.Debugf("-->%s<--", fis.Sum()) h.Write([]byte(fis.Sum())) } checksum := ts.Version().String() + "+" + ts.tHash.Name() + ":" + hex.EncodeToString(h.Sum(nil)) - log.Debugf("checksum processed: %s", checksum) return checksum } From 8936789919c5c8004f346f44a3452d1521818b60 Mon Sep 17 00:00:00 2001 From: Josh Hawn Date: Tue, 28 Oct 2014 14:06:23 -0700 Subject: [PATCH 075/513] Make `FROM scratch` a special cased 'no-base' spec There has been a lot of discussion (issues 4242 and 5262) about making `FROM scratch` either a special case or making `FROM` optional, implying starting from an empty file system. This patch makes the build command `FROM scratch` special cased from now on and if used does not pull/set the the initial layer of the build to the ancient image ID (511136ea..) but instead marks the build as having no base image. The next command in the dockerfile will create an image with a parent image ID of "". This means every image ever can now use one fewer layer! This also makes the image name `scratch` a reserved name by the TagStore. You will not be able to tag an image with this name from now on. If any users currently have an image tagged as `scratch`, they will still be able to use that image, but will not be able to tag a new image with that name. Goodbye '511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158', it was nice knowing you. Fixes #4242 Docker-DCO-1.1-Signed-off-by: Josh Hawn (github: jlhawn) --- api/client/commands.go | 7 +++++- builder/dispatchers.go | 14 +++++++++++- builder/evaluator.go | 2 +- builder/internals.go | 4 ++-- daemon/commit.go | 8 +++---- daemon/container.go | 8 +++---- daemon/create.go | 24 +++++++++++++------- daemon/daemon.go | 12 +++++----- daemon/image_delete.go | 2 +- daemon/inspect.go | 2 +- daemon/list.go | 2 +- graph/tags.go | 3 +++ integration-cli/docker_cli_events_test.go | 6 ++--- integration-cli/docker_cli_inspect_test.go | 2 +- integration-cli/docker_cli_pull_test.go | 6 ++--- integration-cli/docker_cli_save_load_test.go | 6 ++--- project/make/.ensure-scratch | 4 ++-- 17 files changed, 70 insertions(+), 42 deletions(-) diff --git a/api/client/commands.go b/api/client/commands.go index 89e5796bb..6290268b4 100644 --- a/api/client/commands.go +++ b/api/client/commands.go @@ -1696,7 +1696,12 @@ func (cli *DockerCli) CmdPs(args ...string) error { ports.ReadListFrom([]byte(out.Get("Ports"))) - fmt.Fprintf(w, "%s\t%s\t%s\t%s ago\t%s\t%s\t%s\t", outID, out.Get("Image"), outCommand, + image := out.Get("Image") + if image == "" { + image = "" + } + + fmt.Fprintf(w, "%s\t%s\t%s\t%s ago\t%s\t%s\t%s\t", outID, image, outCommand, units.HumanDuration(time.Now().UTC().Sub(time.Unix(out.GetInt64("Created"), 0))), out.Get("Status"), api.DisplayablePorts(ports), strings.Join(outNames, ",")) diff --git a/builder/dispatchers.go b/builder/dispatchers.go index 9fb44fc45..6108967c3 100644 --- a/builder/dispatchers.go +++ b/builder/dispatchers.go @@ -21,6 +21,12 @@ import ( "github.com/docker/docker/runconfig" ) +const ( + // NoBaseImageSpecifier is the symbol used by the FROM + // command to specify that no base image is to be used. + NoBaseImageSpecifier string = "scratch" +) + // dispatch with no layer / parsing. This is effectively not a command. func nullDispatch(b *Builder, args []string, attributes map[string]bool, original string) error { return nil @@ -115,6 +121,12 @@ func from(b *Builder, args []string, attributes map[string]bool, original string name := args[0] + if name == NoBaseImageSpecifier { + b.image = "" + b.noBaseImage = true + return nil + } + image, err := b.Daemon.Repositories().LookupImage(name) if b.Pull { image, err = b.pullImage(name) @@ -191,7 +203,7 @@ func workdir(b *Builder, args []string, attributes map[string]bool, original str // RUN [ "echo", "hi" ] # echo hi // func run(b *Builder, args []string, attributes map[string]bool, original string) error { - if b.image == "" { + if b.image == "" && !b.noBaseImage { return fmt.Errorf("Please provide a source image with `from` prior to run") } diff --git a/builder/evaluator.go b/builder/evaluator.go index 4ed66c005..eef222b94 100644 --- a/builder/evaluator.go +++ b/builder/evaluator.go @@ -110,7 +110,7 @@ type Builder struct { cmdSet bool // indicates is CMD was set in current Dockerfile context tarsum.TarSum // the context is a tarball that is uploaded by the client contextPath string // the path of the temporary directory the local context is unpacked to (server side) - + noBaseImage bool // indicates that this build does not start from any base image, but is being built from an empty file system. } // Run the builder with the context. This is the lynchpin of this package. This diff --git a/builder/internals.go b/builder/internals.go index d30a7da81..1caa33141 100644 --- a/builder/internals.go +++ b/builder/internals.go @@ -58,7 +58,7 @@ func (b *Builder) readContext(context io.Reader) error { } func (b *Builder) commit(id string, autoCmd []string, comment string) error { - if b.image == "" { + if b.image == "" && !b.noBaseImage { return fmt.Errorf("Please provide a source image with `from` prior to commit") } b.Config.Image = b.image @@ -513,7 +513,7 @@ func (b *Builder) probeCache() (bool, error) { } func (b *Builder) create() (*daemon.Container, error) { - if b.image == "" { + if b.image == "" && !b.noBaseImage { return nil, fmt.Errorf("Please provide a source image with `from` prior to run") } b.Config.Image = b.image diff --git a/daemon/commit.go b/daemon/commit.go index 950925ade..06d0465ad 100644 --- a/daemon/commit.go +++ b/daemon/commit.go @@ -59,17 +59,17 @@ func (daemon *Daemon) Commit(container *Container, repository, tag, comment, aut // Create a new image from the container's base layers + a new layer from container changes var ( - containerID, containerImage string - containerConfig *runconfig.Config + containerID, parentImageID string + containerConfig *runconfig.Config ) if container != nil { containerID = container.ID - containerImage = container.Image + parentImageID = container.ImageID containerConfig = container.Config } - img, err := daemon.graph.Create(rwTar, containerID, containerImage, comment, author, containerConfig, config) + img, err := daemon.graph.Create(rwTar, containerID, parentImageID, comment, author, containerConfig, config) if err != nil { return nil, err } diff --git a/daemon/container.go b/daemon/container.go index 3c05c645a..75cd133fe 100644 --- a/daemon/container.go +++ b/daemon/container.go @@ -62,8 +62,8 @@ type Container struct { Path string Args []string - Config *runconfig.Config - Image string + Config *runconfig.Config + ImageID string `json:"Image"` NetworkSettings *NetworkSettings @@ -186,7 +186,7 @@ func (container *Container) WriteHostConfig() error { func (container *Container) LogEvent(action string) { d := container.daemon - if err := d.eng.Job("log", action, container.ID, d.Repositories().ImageName(container.Image)).Run(); err != nil { + if err := d.eng.Job("log", action, container.ID, d.Repositories().ImageName(container.ImageID)).Run(); err != nil { log.Errorf("Error logging event %s for %s: %s", action, container.ID, err) } } @@ -786,7 +786,7 @@ func (container *Container) GetImage() (*image.Image, error) { if container.daemon == nil { return nil, fmt.Errorf("Can't get image of unregistered container") } - return container.daemon.graph.Get(container.Image) + return container.daemon.graph.Get(container.ImageID) } func (container *Container) Unmount() error { diff --git a/daemon/create.go b/daemon/create.go index f9d986491..958a42ea7 100644 --- a/daemon/create.go +++ b/daemon/create.go @@ -5,6 +5,7 @@ import ( "github.com/docker/docker/engine" "github.com/docker/docker/graph" + "github.com/docker/docker/image" "github.com/docker/docker/pkg/parsers" "github.com/docker/docker/runconfig" "github.com/docker/libcontainer/label" @@ -68,15 +69,22 @@ func (daemon *Daemon) Create(config *runconfig.Config, hostConfig *runconfig.Hos var ( container *Container warnings []string + img *image.Image + imgID string + err error ) - img, err := daemon.repositories.LookupImage(config.Image) - if err != nil { - return nil, nil, err - } - if err := img.CheckDepth(); err != nil { - return nil, nil, err + if config.Image != "" { + img, err = daemon.repositories.LookupImage(config.Image) + if err != nil { + return nil, nil, err + } + if err = img.CheckDepth(); err != nil { + return nil, nil, err + } + imgID = img.ID } + if warnings, err = daemon.mergeAndVerifyConfig(config, img); err != nil { return nil, nil, err } @@ -86,13 +94,13 @@ func (daemon *Daemon) Create(config *runconfig.Config, hostConfig *runconfig.Hos return nil, nil, err } } - if container, err = daemon.newContainer(name, config, img); err != nil { + if container, err = daemon.newContainer(name, config, imgID); err != nil { return nil, nil, err } if err := daemon.Register(container); err != nil { return nil, nil, err } - if err := daemon.createRootfs(container, img); err != nil { + if err := daemon.createRootfs(container); err != nil { return nil, nil, err } if hostConfig != nil { diff --git a/daemon/daemon.go b/daemon/daemon.go index 40b56ea1c..1553f198d 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -417,10 +417,10 @@ func (daemon *Daemon) checkDeprecatedExpose(config *runconfig.Config) bool { func (daemon *Daemon) mergeAndVerifyConfig(config *runconfig.Config, img *image.Image) ([]string, error) { warnings := []string{} - if daemon.checkDeprecatedExpose(img.Config) || daemon.checkDeprecatedExpose(config) { + if (img != nil && daemon.checkDeprecatedExpose(img.Config)) || daemon.checkDeprecatedExpose(config) { warnings = append(warnings, "The mapping to public ports on your host via Dockerfile EXPOSE (host:port:port) has been deprecated. Use -p to publish the ports.") } - if img.Config != nil { + if img != nil && img.Config != nil { if err := runconfig.Merge(config, img.Config); err != nil { return nil, err } @@ -557,7 +557,7 @@ func parseSecurityOpt(container *Container, config *runconfig.HostConfig) error return err } -func (daemon *Daemon) newContainer(name string, config *runconfig.Config, img *image.Image) (*Container, error) { +func (daemon *Daemon) newContainer(name string, config *runconfig.Config, imgID string) (*Container, error) { var ( id string err error @@ -578,7 +578,7 @@ func (daemon *Daemon) newContainer(name string, config *runconfig.Config, img *i Args: args, //FIXME: de-duplicate from config Config: config, hostConfig: &runconfig.HostConfig{}, - Image: img.ID, // Always use the resolved image id + ImageID: imgID, NetworkSettings: &NetworkSettings{}, Name: name, Driver: daemon.driver.String(), @@ -590,14 +590,14 @@ func (daemon *Daemon) newContainer(name string, config *runconfig.Config, img *i return container, err } -func (daemon *Daemon) createRootfs(container *Container, img *image.Image) error { +func (daemon *Daemon) createRootfs(container *Container) error { // Step 1: create the container directory. // This doubles as a barrier to avoid race conditions. if err := os.Mkdir(container.root, 0700); err != nil { return err } initID := fmt.Sprintf("%s-init", container.ID) - if err := daemon.driver.Create(initID, img.ID); err != nil { + if err := daemon.driver.Create(initID, container.ImageID); err != nil { return err } initPath, err := daemon.driver.Get(initID, "") diff --git a/daemon/image_delete.go b/daemon/image_delete.go index f39b0dd61..19f81f11a 100644 --- a/daemon/image_delete.go +++ b/daemon/image_delete.go @@ -131,7 +131,7 @@ func (daemon *Daemon) DeleteImage(eng *engine.Engine, name string, imgs *engine. func (daemon *Daemon) canDeleteImage(imgID string, force bool) error { for _, container := range daemon.List() { - parent, err := daemon.Repositories().LookupImage(container.Image) + parent, err := daemon.Repositories().LookupImage(container.ImageID) if err != nil { if daemon.Graph().IsNotExist(err) { return nil diff --git a/daemon/inspect.go b/daemon/inspect.go index c930cdd7f..2bf1773d3 100644 --- a/daemon/inspect.go +++ b/daemon/inspect.go @@ -35,7 +35,7 @@ func (daemon *Daemon) ContainerInspect(job *engine.Job) engine.Status { out.SetList("Args", container.Args) out.SetJson("Config", container.Config) out.SetJson("State", container.State) - out.SetJson("Image", container.Image) + out.Set("Image", container.ImageID) out.SetJson("NetworkSettings", container.NetworkSettings) out.Set("ResolvConfPath", container.ResolvConfPath) out.Set("HostnamePath", container.HostnamePath) diff --git a/daemon/list.go b/daemon/list.go index 188a9861e..937cdd212 100644 --- a/daemon/list.go +++ b/daemon/list.go @@ -116,7 +116,7 @@ func (daemon *Daemon) Containers(job *engine.Job) engine.Status { out := &engine.Env{} out.SetJson("Id", container.ID) out.SetList("Names", names[container.ID]) - out.SetJson("Image", daemon.Repositories().ImageName(container.Image)) + out.SetJson("Image", daemon.Repositories().ImageName(container.ImageID)) if len(container.Args) > 0 { args := []string{} for _, arg := range container.Args { diff --git a/graph/tags.go b/graph/tags.go index 5c3e533b2..826ab0bf7 100644 --- a/graph/tags.go +++ b/graph/tags.go @@ -298,6 +298,9 @@ func validateRepoName(name string) error { if name == "" { return fmt.Errorf("Repository name can't be empty") } + if name == "scratch" { + return fmt.Errorf("'scratch' is a reserved name") + } return nil } diff --git a/integration-cli/docker_cli_events_test.go b/integration-cli/docker_cli_events_test.go index a56788e21..8f7263c0b 100644 --- a/integration-cli/docker_cli_events_test.go +++ b/integration-cli/docker_cli_events_test.go @@ -230,9 +230,9 @@ func TestEventsRedirectStdout(t *testing.T) { func TestEventsImagePull(t *testing.T) { since := time.Now().Unix() - pullCmd := exec.Command(dockerBinary, "pull", "scratch") + pullCmd := exec.Command(dockerBinary, "pull", "hello-world") if out, _, err := runCommandWithOutput(pullCmd); err != nil { - t.Fatalf("pulling the scratch image from has failed: %s, %v", out, err) + t.Fatalf("pulling the hello-world image from has failed: %s, %v", out, err) } eventsCmd := exec.Command(dockerBinary, "events", @@ -243,7 +243,7 @@ func TestEventsImagePull(t *testing.T) { events := strings.Split(strings.TrimSpace(out), "\n") event := strings.TrimSpace(events[len(events)-1]) - if !strings.HasSuffix(event, "scratch:latest: pull") { + if !strings.HasSuffix(event, "hello-world:latest: pull") { t.Fatalf("Missing pull event - got:%q", event) } diff --git a/integration-cli/docker_cli_inspect_test.go b/integration-cli/docker_cli_inspect_test.go index bb99818bf..cf42217ac 100644 --- a/integration-cli/docker_cli_inspect_test.go +++ b/integration-cli/docker_cli_inspect_test.go @@ -7,7 +7,7 @@ import ( ) func TestInspectImage(t *testing.T) { - imageTest := "scratch" + imageTest := "emptyfs" imageTestID := "511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158" imagesCmd := exec.Command(dockerBinary, "inspect", "--format='{{.Id}}'", imageTest) out, exitCode, err := runCommandWithOutput(imagesCmd) diff --git a/integration-cli/docker_cli_pull_test.go b/integration-cli/docker_cli_pull_test.go index b67b1caca..5b3324c77 100644 --- a/integration-cli/docker_cli_pull_test.go +++ b/integration-cli/docker_cli_pull_test.go @@ -9,11 +9,11 @@ import ( // pulling an image from the central registry should work func TestPullImageFromCentralRegistry(t *testing.T) { - pullCmd := exec.Command(dockerBinary, "pull", "scratch") + pullCmd := exec.Command(dockerBinary, "pull", "hello-world") if out, _, err := runCommandWithOutput(pullCmd); err != nil { - t.Fatalf("pulling the scratch image from the registry has failed: %s, %v", out, err) + t.Fatalf("pulling the hello-world image from the registry has failed: %s, %v", out, err) } - logDone("pull - pull scratch") + logDone("pull - pull hello-world") } // pulling a non-existing image from the central registry should return a non-zero exit code diff --git a/integration-cli/docker_cli_save_load_test.go b/integration-cli/docker_cli_save_load_test.go index 94bfe3d6a..c745df69f 100644 --- a/integration-cli/docker_cli_save_load_test.go +++ b/integration-cli/docker_cli_save_load_test.go @@ -270,7 +270,7 @@ func TestSaveSingleTag(t *testing.T) { func TestSaveImageId(t *testing.T) { repoName := "foobar-save-image-id-test" - tagCmdFinal := fmt.Sprintf("%v tag scratch:latest %v:latest", dockerBinary, repoName) + tagCmdFinal := fmt.Sprintf("%v tag emptyfs:latest %v:latest", dockerBinary, repoName) tagCmd := exec.Command("bash", "-c", tagCmdFinal) if out, _, err := runCommandWithOutput(tagCmd); err != nil { t.Fatalf("failed to tag repo: %s, %v", out, err) @@ -370,7 +370,7 @@ func TestSaveMultipleNames(t *testing.T) { repoName := "foobar-save-multi-name-test" // Make one image - tagCmdFinal := fmt.Sprintf("%v tag scratch:latest %v-one:latest", dockerBinary, repoName) + tagCmdFinal := fmt.Sprintf("%v tag emptyfs:latest %v-one:latest", dockerBinary, repoName) tagCmd := exec.Command("bash", "-c", tagCmdFinal) if out, _, err := runCommandWithOutput(tagCmd); err != nil { t.Fatalf("failed to tag repo: %s, %v", out, err) @@ -378,7 +378,7 @@ func TestSaveMultipleNames(t *testing.T) { defer deleteImages(repoName + "-one") // Make two images - tagCmdFinal = fmt.Sprintf("%v tag scratch:latest %v-two:latest", dockerBinary, repoName) + tagCmdFinal = fmt.Sprintf("%v tag emptyfs:latest %v-two:latest", dockerBinary, repoName) tagCmd = exec.Command("bash", "-c", tagCmdFinal) if out, _, err := runCommandWithOutput(tagCmd); err != nil { t.Fatalf("failed to tag repo: %s, %v", out, err) diff --git a/project/make/.ensure-scratch b/project/make/.ensure-scratch index 9a9a43a0f..8c421ed29 100644 --- a/project/make/.ensure-scratch +++ b/project/make/.ensure-scratch @@ -1,13 +1,13 @@ #!/bin/bash if ! docker inspect scratch &> /dev/null; then - # let's build a "docker save" tarball for "scratch" + # let's build a "docker save" tarball for "emptyfs" # see https://github.com/docker/docker/pull/5262 # and also https://github.com/docker/docker/issues/4242 mkdir -p /docker-scratch ( cd /docker-scratch - echo '{"scratch":{"latest":"511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158"}}' > repositories + echo '{"emptyfs":{"latest":"511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158"}}' > repositories mkdir -p 511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158 ( cd 511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158 From a70d7aaf282948c6873c03031cee0704cbe86476 Mon Sep 17 00:00:00 2001 From: Tibor Vass Date: Thu, 18 Dec 2014 19:13:02 -0500 Subject: [PATCH 076/513] registry: add tests for unresolvable domain names in isSecure Signed-off-by: Tibor Vass --- registry/registry_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/registry/registry_test.go b/registry/registry_test.go index 5fd80da10..06619aef4 100644 --- a/registry/registry_test.go +++ b/registry/registry_test.go @@ -348,6 +348,9 @@ func TestIsSecure(t *testing.T) { {"example.com:5000", []string{"42.42.42.42/8"}, false}, {"127.0.0.1:5000", []string{"127.0.0.0/8"}, false}, {"42.42.42.42:5000", []string{"42.1.1.1/8"}, false}, + {"invalid.domain.com", []string{"42.42.0.0/16"}, true}, + {"invalid.domain.com", []string{"invalid.domain.com"}, false}, + {"invalid.domain.com:5000", []string{"invalid.domain.com"}, false}, } for _, tt := range tests { // TODO: remove this once we remove localhost insecure by default From ff4bfcc0e9f171a95dac5cc2650faacf73943057 Mon Sep 17 00:00:00 2001 From: Tibor Vass Date: Thu, 18 Dec 2014 19:13:56 -0500 Subject: [PATCH 077/513] registry: handle unresolvable domain names in isSecure to allow HTTP proxies to work as expected. Fixes #9708 Signed-off-by: Tibor Vass --- registry/endpoint.go | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/registry/endpoint.go b/registry/endpoint.go index c485a13d8..8609486a2 100644 --- a/registry/endpoint.go +++ b/registry/endpoint.go @@ -163,7 +163,10 @@ func (e Endpoint) Ping() (RegistryInfo, error) { // If the subnet contains one of the IPs of the registry specified by hostname, the latter is considered // insecure. // -// hostname should be a URL.Host (`host:port` or `host`) +// hostname should be a URL.Host (`host:port` or `host`) where the `host` part can be either a domain name +// or an IP address. If it is a domain name, then it will be resolved in order to check if the IP is contained +// in a subnet. If the resolving is not successful, isSecure will only try to match hostname to any element +// of insecureRegistries. func isSecure(hostname string, insecureRegistries []string) (bool, error) { if hostname == IndexServerURL.Host { return true, nil @@ -177,29 +180,30 @@ func isSecure(hostname string, insecureRegistries []string) (bool, error) { addrs, err := lookupIP(host) if err != nil { ip := net.ParseIP(host) - if ip == nil { - // if resolving `host` fails, error out, since host is to be net.Dial-ed anyway - return true, fmt.Errorf("issecure: could not resolve %q: %v", host, err) + if ip != nil { + addrs = []net.IP{ip} } - addrs = []net.IP{ip} - } - if len(addrs) == 0 { - return true, fmt.Errorf("issecure: could not resolve %q", host) + + // if ip == nil, then `host` is neither an IP nor it could be looked up, + // either because the index is unreachable, or because the index is behind an HTTP proxy. + // So, len(addrs) == 0 and we're not aborting. } - for _, addr := range addrs { - for _, r := range insecureRegistries { + for _, r := range insecureRegistries { + if hostname == r || host == r { // hostname matches insecure registry - if hostname == r { - return false, nil - } + return false, nil + } + + // Try CIDR notation only if addrs has any elements, i.e. if `host`'s IP could be determined. + for _, addr := range addrs { // now assume a CIDR was passed to --insecure-registry _, ipnet, err := net.ParseCIDR(r) if err != nil { - // if could not parse it as a CIDR, even after removing + // if we could not parse it as a CIDR, even after removing // assume it's not a CIDR and go on with the next candidate - continue + break } // check if the addr falls in the subnet From 29ed8a2289c932217c47b99cef3d91cbfc0d965e Mon Sep 17 00:00:00 2001 From: Jessica Frazelle Date: Thu, 18 Dec 2014 18:09:41 -0800 Subject: [PATCH 078/513] I don't know how this happened, repeated word typo. Docker-DCO-1.1-Signed-off-by: Jessica Frazelle (github: jfrazelle) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9b198f41c..4cda72070 100644 --- a/README.md +++ b/README.md @@ -187,7 +187,7 @@ documentation, please take a look at this [README.md](https://github.com/docker/ These instructions are probably not perfect, please let us know if anything feels wrong or incomplete. -Want to run Docker from a master build? You can can download +Want to run Docker from a master build? You can download master builds at [master.dockerproject.com](https://master.dockerproject.com). They are updated with each commit merged into the master branch. From e9f37a011872fa89669dc105df27e1c6e16124b6 Mon Sep 17 00:00:00 2001 From: unclejack Date: Fri, 19 Dec 2014 13:57:21 +0200 Subject: [PATCH 079/513] pkg/graphdb: use transactions for transactions Signed-off-by: Cristian Staretu --- pkg/graphdb/graphdb.go | 88 ++++++++++++++++++++---------------------- 1 file changed, 41 insertions(+), 47 deletions(-) diff --git a/pkg/graphdb/graphdb.go b/pkg/graphdb/graphdb.go index 62342033a..c6f13eda2 100644 --- a/pkg/graphdb/graphdb.go +++ b/pkg/graphdb/graphdb.go @@ -79,46 +79,43 @@ func NewDatabase(conn *sql.DB) (*Database, error) { } db := &Database{conn: conn} - if _, err := conn.Exec(createEntityTable); err != nil { - return nil, err - } - if _, err := conn.Exec(createEdgeTable); err != nil { - return nil, err - } - if _, err := conn.Exec(createEdgeIndices); err != nil { - return nil, err - } - - rollback := func() { - conn.Exec("ROLLBACK") - } - // Create root entities - if _, err := conn.Exec("BEGIN"); err != nil { + tx, err := conn.Begin() + if err != nil { return nil, err } - if _, err := conn.Exec("DELETE FROM entity where id = ?", "0"); err != nil { - rollback() + if _, err := tx.Exec(createEntityTable); err != nil { + return nil, err + } + if _, err := tx.Exec(createEdgeTable); err != nil { + return nil, err + } + if _, err := tx.Exec(createEdgeIndices); err != nil { return nil, err } - if _, err := conn.Exec("INSERT INTO entity (id) VALUES (?);", "0"); err != nil { - rollback() + if _, err := tx.Exec("DELETE FROM entity where id = ?", "0"); err != nil { + tx.Rollback() return nil, err } - if _, err := conn.Exec("DELETE FROM edge where entity_id=? and name=?", "0", "/"); err != nil { - rollback() + if _, err := tx.Exec("INSERT INTO entity (id) VALUES (?);", "0"); err != nil { + tx.Rollback() return nil, err } - if _, err := conn.Exec("INSERT INTO edge (entity_id, name) VALUES(?,?);", "0", "/"); err != nil { - rollback() + if _, err := tx.Exec("DELETE FROM edge where entity_id=? and name=?", "0", "/"); err != nil { + tx.Rollback() return nil, err } - if _, err := conn.Exec("COMMIT"); err != nil { + if _, err := tx.Exec("INSERT INTO edge (entity_id, name) VALUES(?,?);", "0", "/"); err != nil { + tx.Rollback() + return nil, err + } + + if err := tx.Commit(); err != nil { return nil, err } @@ -135,33 +132,32 @@ func (db *Database) Set(fullPath, id string) (*Entity, error) { db.mux.Lock() defer db.mux.Unlock() - rollback := func() { - db.conn.Exec("ROLLBACK") - } - if _, err := db.conn.Exec("BEGIN EXCLUSIVE"); err != nil { + tx, err := db.conn.Begin() + if err != nil { return nil, err } + var entityID string - if err := db.conn.QueryRow("SELECT id FROM entity WHERE id = ?;", id).Scan(&entityID); err != nil { + if err := tx.QueryRow("SELECT id FROM entity WHERE id = ?;", id).Scan(&entityID); err != nil { if err == sql.ErrNoRows { - if _, err := db.conn.Exec("INSERT INTO entity (id) VALUES(?);", id); err != nil { - rollback() + if _, err := tx.Exec("INSERT INTO entity (id) VALUES(?);", id); err != nil { + tx.Rollback() return nil, err } } else { - rollback() + tx.Rollback() return nil, err } } e := &Entity{id} parentPath, name := splitPath(fullPath) - if err := db.setEdge(parentPath, name, e); err != nil { - rollback() + if err := db.setEdge(parentPath, name, e, tx); err != nil { + tx.Rollback() return nil, err } - if _, err := db.conn.Exec("COMMIT"); err != nil { + if err := tx.Commit(); err != nil { return nil, err } return e, nil @@ -179,7 +175,7 @@ func (db *Database) Exists(name string) bool { return e != nil } -func (db *Database) setEdge(parentPath, name string, e *Entity) error { +func (db *Database) setEdge(parentPath, name string, e *Entity, tx *sql.Tx) error { parent, err := db.get(parentPath) if err != nil { return err @@ -188,7 +184,7 @@ func (db *Database) setEdge(parentPath, name string, e *Entity) error { return fmt.Errorf("Cannot set self as child") } - if _, err := db.conn.Exec("INSERT INTO edge (parent_id, name, entity_id) VALUES (?,?,?);", parent.id, name, e.id); err != nil { + if _, err := tx.Exec("INSERT INTO edge (parent_id, name, entity_id) VALUES (?,?,?);", parent.id, name, e.id); err != nil { return err } return nil @@ -371,18 +367,15 @@ func (db *Database) Purge(id string) (int, error) { db.mux.Lock() defer db.mux.Unlock() - rollback := func() { - db.conn.Exec("ROLLBACK") - } - - if _, err := db.conn.Exec("BEGIN"); err != nil { + tx, err := db.conn.Begin() + if err != nil { return -1, err } // Delete all edges - rows, err := db.conn.Exec("DELETE FROM edge WHERE entity_id = ?;", id) + rows, err := tx.Exec("DELETE FROM edge WHERE entity_id = ?;", id) if err != nil { - rollback() + tx.Rollback() return -1, err } @@ -392,14 +385,15 @@ func (db *Database) Purge(id string) (int, error) { } // Delete entity - if _, err := db.conn.Exec("DELETE FROM entity where id = ?;", id); err != nil { - rollback() + if _, err := tx.Exec("DELETE FROM entity where id = ?;", id); err != nil { + tx.Rollback() return -1, err } - if _, err := db.conn.Exec("COMMIT"); err != nil { + if err := tx.Commit(); err != nil { return -1, err } + return int(changes), nil } From edb75b486e5459c8ffce6d6b9d26645c1187c5ff Mon Sep 17 00:00:00 2001 From: Arnaud Porterie Date: Fri, 19 Dec 2014 08:11:10 -0800 Subject: [PATCH 080/513] Revert doc changes from #9652 Because the patch is not in 1.4, the v1.16 docs shouldn't have been updated. Docs were promoted to v1.17 by #9742. Signed-off-by: Arnaud Porterie --- .../reference/api/docker_remote_api_v1.16.md | 27 ++++--------------- 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/docs/sources/reference/api/docker_remote_api_v1.16.md b/docs/sources/reference/api/docker_remote_api_v1.16.md index f9105f6da..e186a73e6 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.16.md +++ b/docs/sources/reference/api/docker_remote_api_v1.16.md @@ -393,10 +393,8 @@ Get stdout and stderr logs from the container ``id`` **Example response**: - HTTP/1.1 101 UPGRADED + HTTP/1.1 200 OK Content-Type: application/vnd.docker.raw-stream - Connection: Upgrade - Upgrade: tcp {{ STREAM }} @@ -411,8 +409,7 @@ Query Parameters: Status Codes: -- **101** – no error, hints proxy about hijacking -- **200** – no error, no upgrade header found +- **200** – no error - **404** – no such container - **500** – server error @@ -647,10 +644,8 @@ Attach to the container `id` **Example response**: - HTTP/1.1 101 UPGRADED + HTTP/1.1 200 OK Content-Type: application/vnd.docker.raw-stream - Connection: Upgrade - Upgrade: tcp {{ STREAM }} @@ -668,8 +663,7 @@ Query Parameters: Status Codes: -- **101** – no error, hints proxy about hijacking -- **200** – no error, no upgrade header found +- **200** – no error - **400** – bad parameter - **404** – no such container - **500** – server error @@ -1752,18 +1746,7 @@ As an example, the `docker run` command line makes the following API calls: ## 3.2 Hijacking In this version of the API, /attach, uses hijacking to transport stdin, -stdout and stderr on the same socket. - -To hint potential proxies about connection hijacking, Docker client sends -connection upgrade headers similarly to websocket. - - Upgrade: tcp - Connection: Upgrade - -When Docker daemon detects the `Upgrade` header, it will switch its status code -from **200 OK** to **101 UPGRADED** and resend the same headers. - -This might change in the future. +stdout and stderr on the same socket. This might change in the future. ## 3.3 CORS Requests From e59aad9cd387bf20dc0f4664afdd5c55b394cb5b Mon Sep 17 00:00:00 2001 From: unclejack Date: Fri, 19 Dec 2014 18:38:12 +0200 Subject: [PATCH 081/513] pkg/units: fix size_test.go compilation Signed-off-by: Cristian Staretu --- pkg/units/size_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/units/size_test.go b/pkg/units/size_test.go index 3e410b0db..67c3b81e6 100644 --- a/pkg/units/size_test.go +++ b/pkg/units/size_test.go @@ -23,9 +23,9 @@ func TestHumanSize(t *testing.T) { assertEquals(t, "1 MB", HumanSize(1000000)) assertEquals(t, "1.049 MB", HumanSize(1048576)) assertEquals(t, "2 MB", HumanSize(2*MB)) - assertEquals(t, "3.42 GB", HumanSize(int64(float64(3.42*GB)))) - assertEquals(t, "5.372 TB", HumanSize(int64(float64(5.372*TB)))) - assertEquals(t, "2.22 PB", HumanSize(int64(float64(2.22*PB)))) + assertEquals(t, "3.42 GB", HumanSize(float64(3.42*GB))) + assertEquals(t, "5.372 TB", HumanSize(float64(5.372*TB))) + assertEquals(t, "2.22 PB", HumanSize(float64(2.22*PB))) } func TestFromHumanSize(t *testing.T) { From 100267de81985bbf3b976bfde850def89487dc11 Mon Sep 17 00:00:00 2001 From: Tianon Gravi Date: Fri, 19 Dec 2014 00:20:59 -0700 Subject: [PATCH 082/513] Tweak test-docker-py feature - move docker/docker-py clone to the Dockerfile - put "integration test daemon startup" code in a separate file for both scripts to source - add new test-docker-py Makefile target - include "python-websocket" package in Dockerfile for running the tests Signed-off-by: Andrew "Tianon" Page --- Dockerfile | 4 +++ Makefile | 5 +++- project/make.sh | 1 + project/make/.integration-daemon-start | 24 ++++++++++++++++ project/make/.integration-daemon-stop | 7 +++++ project/make/test-docker-py | 39 ++++++-------------------- project/make/test-integration-cli | 27 ++---------------- 7 files changed, 51 insertions(+), 56 deletions(-) create mode 100644 project/make/.integration-daemon-start create mode 100644 project/make/.integration-daemon-stop diff --git a/Dockerfile b/Dockerfile index c276eb3e7..a7b6bbc42 100644 --- a/Dockerfile +++ b/Dockerfile @@ -44,6 +44,7 @@ RUN apt-get update && apt-get install -y \ parallel \ python-mock \ python-pip \ + python-websocket \ reprepro \ ruby1.9.1 \ ruby1.9.1-dev \ @@ -95,6 +96,9 @@ RUN git clone -b buildroot-2014.02 https://github.com/jpetazzo/docker-busybox.gi # Get the "cirros" image source so we can import it instead of fetching it during tests RUN curl -sSL -o /cirros.tar.gz https://github.com/ewindisch/docker-cirros/raw/1cded459668e8b9dbf4ef976c94c05add9bbd8e9/cirros-0.3.0-x86_64-lxc.tar.gz +# Get the "docker-py" source so we can run their integration tests +RUN git clone -b 0.7.0 https://github.com/docker/docker-py.git /docker-py + # Setup s3cmd config RUN /bin/echo -e '[default]\naccess_key=$AWS_ACCESS_KEY\nsecret_key=$AWS_SECRET_KEY' > $HOME/.s3cfg diff --git a/Makefile b/Makefile index 70799d3c2..f1ae554a0 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all binary build cross default docs docs-build docs-shell shell test test-unit test-integration test-integration-cli validate +.PHONY: all binary build cross default docs docs-build docs-shell shell test test-unit test-integration test-integration-cli test-docker-py validate # env vars passed through directly to Docker's build scripts # to allow things like `make DOCKER_CLIENTONLY=1 binary` easily @@ -67,6 +67,9 @@ test-integration: build test-integration-cli: build $(DOCKER_RUN_DOCKER) hack/make.sh binary test-integration-cli +test-docker-py: build + $(DOCKER_RUN_DOCKER) hack/make.sh binary test-docker-py + validate: build $(DOCKER_RUN_DOCKER) hack/make.sh validate-gofmt validate-dco diff --git a/project/make.sh b/project/make.sh index 2b3a530ea..cf1201649 100755 --- a/project/make.sh +++ b/project/make.sh @@ -50,6 +50,7 @@ DEFAULT_BUNDLES=( test-unit test-integration test-integration-cli + test-docker-py dynbinary dyntest-unit diff --git a/project/make/.integration-daemon-start b/project/make/.integration-daemon-start new file mode 100644 index 000000000..b974422cd --- /dev/null +++ b/project/make/.integration-daemon-start @@ -0,0 +1,24 @@ +#!/bin/bash + +# see test-integration-cli for example usage of this script + +export PATH="$DEST/../binary:$DEST/../dynbinary:$PATH" + +if ! command -v docker &> /dev/null; then + echo >&2 'error: binary or dynbinary must be run before .integration-daemon-start' + false +fi + +# intentionally open a couple bogus file descriptors to help test that they get scrubbed in containers +exec 41>&1 42>&2 + +DOCKER_GRAPHDRIVER=${DOCKER_GRAPHDRIVER:-vfs} +DOCKER_EXECDRIVER=${DOCKER_EXECDRIVER:-native} + +( set -x; exec \ + docker --daemon --debug \ + --storage-driver "$DOCKER_GRAPHDRIVER" \ + --exec-driver "$DOCKER_EXECDRIVER" \ + --pidfile "$DEST/docker.pid" \ + &> "$DEST/docker.log" +) & diff --git a/project/make/.integration-daemon-stop b/project/make/.integration-daemon-stop new file mode 100644 index 000000000..57dc651d4 --- /dev/null +++ b/project/make/.integration-daemon-stop @@ -0,0 +1,7 @@ +#!/bin/bash + +for pid in $(find "$DEST" -name docker.pid); do + DOCKER_PID=$(set -x; cat "$pid") + ( set -x; kill $DOCKER_PID ) + wait $DOCKERD_PID || true +done diff --git a/project/make/test-docker-py b/project/make/test-docker-py index 2a39c6fa5..1096c9cbf 100644 --- a/project/make/test-docker-py +++ b/project/make/test-docker-py @@ -3,41 +3,20 @@ set -e DEST=$1 -DOCKER_GRAPHDRIVER=${DOCKER_GRAPHDRIVER:-vfs} -DOCKER_EXECDRIVER=${DOCKER_EXECDRIVER:-native} - # subshell so that we can export PATH without breaking other things exec > >(tee -a $DEST/test.log) 2>&1 ( - export PATH="$DEST/../binary:$DEST/../dynbinary:$PATH" + source "$(dirname "$BASH_SOURCE")/.integration-daemon-start" - if ! command -v docker &> /dev/null; then - echo >&2 'error: binary or dynbinary must be run before test-docker-py' - false - fi + dockerPy='/docker-py' + [ -d "$dockerPy" ] || { + dockerPy="$DEST/docker-py" + git clone https://github.com/docker/docker-py.git "$dockerPy" + } - # intentionally open a couple bogus file descriptors to help test that they get scrubbed in containers - exec 41>&1 42>&2 - - ( set -x; exec \ - docker --daemon --debug \ - --storage-driver "$DOCKER_GRAPHDRIVER" \ - --exec-driver "$DOCKER_EXECDRIVER" \ - --pidfile "$DEST/docker.pid" \ - &> "$DEST/docker.log" - ) & - - mkdir -p /tmp/dockerpy-tests && cd /tmp/dockerpy-tests - git clone https://github.com/docker/docker-py.git - cd docker-py - git checkout 0.6.0-integration - python setup.py install + cd "$dockerPy" + export PYTHONPATH=. # import "docker" from "." python tests/integration_test.py - for pid in $(find "$DEST" -name docker.pid); do - DOCKER_PID=$(set -x; cat "$pid") - ( set -x; kill $DOCKER_PID ) - wait $DOCKERD_PID || true - done + source "$(dirname "$BASH_SOURCE")/.integration-daemon-stop" ) - diff --git a/project/make/test-integration-cli b/project/make/test-integration-cli index e371fac07..b8647ef76 100644 --- a/project/make/test-integration-cli +++ b/project/make/test-integration-cli @@ -3,9 +3,6 @@ set -e DEST=$1 -DOCKER_GRAPHDRIVER=${DOCKER_GRAPHDRIVER:-vfs} -DOCKER_EXECDRIVER=${DOCKER_EXECDRIVER:-native} - bundle_test_integration_cli() { go_test_dir ./integration-cli } @@ -13,23 +10,7 @@ bundle_test_integration_cli() { # subshell so that we can export PATH without breaking other things exec > >(tee -a $DEST/test.log) 2>&1 ( - export PATH="$DEST/../binary:$DEST/../dynbinary:$PATH" - - if ! command -v docker &> /dev/null; then - echo >&2 'error: binary or dynbinary must be run before test-integration-cli' - false - fi - - # intentionally open a couple bogus file descriptors to help test that they get scrubbed in containers - exec 41>&1 42>&2 - - ( set -x; exec \ - docker --daemon --debug \ - --storage-driver "$DOCKER_GRAPHDRIVER" \ - --exec-driver "$DOCKER_EXECDRIVER" \ - --pidfile "$DEST/docker.pid" \ - &> "$DEST/docker.log" - ) & + source "$(dirname "$BASH_SOURCE")/.integration-daemon-start" # pull the busybox image before running the tests sleep 2 @@ -38,9 +19,5 @@ exec > >(tee -a $DEST/test.log) 2>&1 bundle_test_integration_cli - for pid in $(find "$DEST" -name docker.pid); do - DOCKER_PID=$(set -x; cat "$pid") - ( set -x; kill $DOCKER_PID ) - wait $DOCKERD_PID || true - done + source "$(dirname "$BASH_SOURCE")/.integration-daemon-stop" ) From 6599aa3368f369e6b8632cc2c12f214b0450a5a6 Mon Sep 17 00:00:00 2001 From: Srini Brahmaroutu Date: Tue, 9 Dec 2014 20:04:33 +0000 Subject: [PATCH 083/513] Error should be 409 as the container is different state to remove Closes #9569 Signed-off-by: Srini Brahmaroutu --- daemon/delete.go | 2 +- integration-cli/docker_cli_rm_test.go | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/daemon/delete.go b/daemon/delete.go index 55678f90a..990e4b448 100644 --- a/daemon/delete.go +++ b/daemon/delete.go @@ -55,7 +55,7 @@ func (daemon *Daemon) ContainerRm(job *engine.Job) engine.Status { return job.Errorf("Could not kill running container, cannot remove - %v", err) } } else { - return job.Errorf("You cannot remove a running container. Stop the container before attempting removal or use -f") + return job.Errorf("Conflict, You cannot remove a running container. Stop the container before attempting removal or use -f") } } if err := daemon.Destroy(container); err != nil { diff --git a/integration-cli/docker_cli_rm_test.go b/integration-cli/docker_cli_rm_test.go index 6681840ec..c3069a3ee 100644 --- a/integration-cli/docker_cli_rm_test.go +++ b/integration-cli/docker_cli_rm_test.go @@ -57,6 +57,24 @@ func TestRmRunningContainer(t *testing.T) { logDone("rm - running container") } +func TestRmRunningContainerCheckError409(t *testing.T) { + createRunningContainer(t, "foo") + + endpoint := "/containers/foo" + _, err := sockRequest("DELETE", endpoint, nil) + + if err == nil { + t.Fatalf("Expected error, can't rm a running container") + } + if !strings.Contains(err.Error(), "409 Conflict") { + t.Fatalf("Expected error to contain '409 Conflict' but found", err) + } + + deleteAllContainers() + + logDone("rm - running container") +} + func TestRmForceRemoveRunningContainer(t *testing.T) { createRunningContainer(t, "foo") From 9a50dd5f37d001d7c453ea8749454b4c8bf728f1 Mon Sep 17 00:00:00 2001 From: Tibor Vass Date: Fri, 19 Dec 2014 16:40:28 -0500 Subject: [PATCH 084/513] registry: remove accidentally added --insecure-registry feature If `--insecure-registry mydomain.com` was specified, it would match a registry at mydomain.com on any port. This was accidentally added in #9735 and is now being reverted. Signed-off-by: Tibor Vass --- registry/endpoint.go | 2 +- registry/registry_test.go | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/registry/endpoint.go b/registry/endpoint.go index 8609486a2..019bccfc6 100644 --- a/registry/endpoint.go +++ b/registry/endpoint.go @@ -190,7 +190,7 @@ func isSecure(hostname string, insecureRegistries []string) (bool, error) { } for _, r := range insecureRegistries { - if hostname == r || host == r { + if hostname == r { // hostname matches insecure registry return false, nil } diff --git a/registry/registry_test.go b/registry/registry_test.go index 06619aef4..52b8b32c5 100644 --- a/registry/registry_test.go +++ b/registry/registry_test.go @@ -350,7 +350,8 @@ func TestIsSecure(t *testing.T) { {"42.42.42.42:5000", []string{"42.1.1.1/8"}, false}, {"invalid.domain.com", []string{"42.42.0.0/16"}, true}, {"invalid.domain.com", []string{"invalid.domain.com"}, false}, - {"invalid.domain.com:5000", []string{"invalid.domain.com"}, false}, + {"invalid.domain.com:5000", []string{"invalid.domain.com"}, true}, + {"invalid.domain.com:5000", []string{"invalid.domain.com:5000"}, false}, } for _, tt := range tests { // TODO: remove this once we remove localhost insecure by default From cd9ad8d584374224cd253f48703954d48cf06401 Mon Sep 17 00:00:00 2001 From: Doug Davis Date: Mon, 8 Dec 2014 08:01:42 -0800 Subject: [PATCH 085/513] Make README point to other critical docker++ projects People might find it hard to find the newer/related Docker projects, especially when they're just github issues. So, while I would have preferred to have this at http://github.com/docker, I couldn't find a doc to edit to make that happen, so this is the next best spot. Signed-off-by: Doug Davis --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index c2273eb65..d2991dbe9 100644 --- a/README.md +++ b/README.md @@ -206,3 +206,19 @@ Docker is licensed under the Apache License, Version 2.0. See [LICENSE](https://github.com/docker/docker/blob/master/LICENSE) for the full license text. +Other Docker Related Projects +============================= + +Leveraging Docker as the core technology for managing Linux containers on a +single host, the following projects are also under development to provide a +more comprehensive set of tooling to help round out the Docker platform: + +* [Docker Registry](https://github.com/docker/docker-registry): Registry +server for Docker (hosting/delivering of repositories and images) +* [Docker Machine](https://github.com/docker/machine): Machine management +for a container-centric world +* [Docker Swarm](https://github.com/docker/swarm): A Docker-native clustering +system +* [Docker Compose](https://github.com/docker/docker/issues/9694): +Multi-container application management + From 585202650cdf0307fcc77ba3c165c5dc8338432c Mon Sep 17 00:00:00 2001 From: Tianon Gravi Date: Fri, 19 Dec 2014 15:28:12 -0700 Subject: [PATCH 086/513] Clarify preferred btrfs-progs dependency and fix some minor inconsistencies Signed-off-by: Andrew "Tianon" Page --- project/PACKAGERS.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/project/PACKAGERS.md b/project/PACKAGERS.md index ae3d7dfdd..7aba36c89 100644 --- a/project/PACKAGERS.md +++ b/project/PACKAGERS.md @@ -44,12 +44,12 @@ need to package Docker your way, without denaturing it in the process. To build Docker, you will need the following: -* A recent version of git and mercurial +* A recent version of Git and Mercurial * Go version 1.3 or later * A clean checkout of the source added to a valid [Go workspace](http://golang.org/doc/code.html#Workspaces) under the path *src/github.com/docker/docker* (unless you plan to use `AUTO_GOPATH`, - explained in more detail below). + explained in more detail below) To build the Docker daemon, you will additionally need: @@ -57,8 +57,9 @@ To build the Docker daemon, you will additionally need: * SQLite version 3.7.9 or later * libdevmapper version 1.02.68-cvs (2012-01-26) or later from lvm2 version 2.02.89 or later -* btrfs-progs version 3.8 or later (including commit e5cb128 from 2013-01-07) - for the necessary btrfs headers +* btrfs-progs version 3.16.1 or later (unless using an older version is + absolutely necessary, in which case 3.8 is the minimum and the note below + regarding `btrfs_noversion` applies) Be sure to also check out Docker's Dockerfile for the most up-to-date list of these build-time dependencies. @@ -162,7 +163,7 @@ SELinux, you will need to use the `selinux` build tag: export DOCKER_BUILDTAGS='selinux' ``` -If your version of btrfs-progs is < 3.16.1 (also called btrfs-tools), then you +If your version of btrfs-progs (also called btrfs-tools) is < 3.16.1, then you will need the following tag to not check for btrfs version headers: ```bash export DOCKER_BUILDTAGS='btrfs_noversion' From b74ccf2a1b9d146fef0bca89a2f6a40eb4c3e486 Mon Sep 17 00:00:00 2001 From: Don Kjer Date: Fri, 19 Dec 2014 22:48:31 +0000 Subject: [PATCH 087/513] Fixing ValidateTagName error message Signed-off-by: Don Kjer --- graph/tags.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graph/tags.go b/graph/tags.go index 826ab0bf7..d584ac2a0 100644 --- a/graph/tags.go +++ b/graph/tags.go @@ -310,7 +310,7 @@ func ValidateTagName(name string) error { return fmt.Errorf("Tag name can't be empty") } if !validTagName.MatchString(name) { - return fmt.Errorf("Illegal tag name (%s): only [A-Za-z0-9_.-] are allowed, minimum 2, maximum 30 in length", name) + return fmt.Errorf("Illegal tag name (%s): only [A-Za-z0-9_.-] are allowed, minimum 1, maximum 128 in length", name) } return nil } From f3ed42286e7df3a35ca41a4d19fab2a53a08b382 Mon Sep 17 00:00:00 2001 From: Arnaud Porterie Date: Wed, 19 Nov 2014 21:19:16 -0800 Subject: [PATCH 088/513] Enable test-integration-cli for Windows platform Signed-off-by: Arnaud Porterie --- integration-cli/docker_cli_daemon_test.go | 2 + integration-cli/docker_cli_events_test.go | 49 --------- .../docker_cli_events_test_unix.go | 59 ++++++++++ integration-cli/docker_cli_run_test.go | 79 -------------- integration-cli/docker_cli_run_test_unix.go | 93 ++++++++++++++++ integration-cli/docker_cli_save_load_test.go | 92 ---------------- .../docker_cli_save_load_test_unix.go | 102 ++++++++++++++++++ integration-cli/docker_test_vars.go | 15 ++- integration-cli/utils.go | 4 +- project/make/.integration-daemon-start | 18 ++-- 10 files changed, 281 insertions(+), 232 deletions(-) create mode 100644 integration-cli/docker_cli_events_test_unix.go create mode 100644 integration-cli/docker_cli_run_test_unix.go create mode 100644 integration-cli/docker_cli_save_load_test_unix.go diff --git a/integration-cli/docker_cli_daemon_test.go b/integration-cli/docker_cli_daemon_test.go index 3e4b2edbf..dbc4d232b 100644 --- a/integration-cli/docker_cli_daemon_test.go +++ b/integration-cli/docker_cli_daemon_test.go @@ -1,3 +1,5 @@ +// +build daemon + package main import ( diff --git a/integration-cli/docker_cli_events_test.go b/integration-cli/docker_cli_events_test.go index 8f7263c0b..be6f1202c 100644 --- a/integration-cli/docker_cli_events_test.go +++ b/integration-cli/docker_cli_events_test.go @@ -1,18 +1,12 @@ package main import ( - "bufio" "fmt" - "io/ioutil" - "os" "os/exec" "strconv" "strings" "testing" "time" - "unicode" - - "github.com/kr/pty" ) func TestEventsUntag(t *testing.T) { @@ -185,49 +179,6 @@ func TestEventsImageUntagDelete(t *testing.T) { logDone("events - image untag, delete is logged") } -// #5979 -func TestEventsRedirectStdout(t *testing.T) { - - since := time.Now().Unix() - - dockerCmd(t, "run", "busybox", "true") - - defer deleteAllContainers() - - file, err := ioutil.TempFile("", "") - if err != nil { - t.Fatalf("could not create temp file: %v", err) - } - defer os.Remove(file.Name()) - - command := fmt.Sprintf("%s events --since=%d --until=%d > %s", dockerBinary, since, time.Now().Unix(), file.Name()) - _, tty, err := pty.Open() - if err != nil { - t.Fatalf("Could not open pty: %v", err) - } - cmd := exec.Command("sh", "-c", command) - cmd.Stdin = tty - cmd.Stdout = tty - cmd.Stderr = tty - if err := cmd.Run(); err != nil { - t.Fatalf("run err for command %q: %v", command, err) - } - - scanner := bufio.NewScanner(file) - for scanner.Scan() { - for _, c := range scanner.Text() { - if unicode.IsControl(c) { - t.Fatalf("found control character %v", []byte(string(c))) - } - } - } - if err := scanner.Err(); err != nil { - t.Fatalf("Scan err for command %q: %v", command, err) - } - - logDone("events - redirect stdout") -} - func TestEventsImagePull(t *testing.T) { since := time.Now().Unix() pullCmd := exec.Command(dockerBinary, "pull", "hello-world") diff --git a/integration-cli/docker_cli_events_test_unix.go b/integration-cli/docker_cli_events_test_unix.go new file mode 100644 index 000000000..fd6c43475 --- /dev/null +++ b/integration-cli/docker_cli_events_test_unix.go @@ -0,0 +1,59 @@ +// +build !windows + +package main + +import ( + "bufio" + "fmt" + "io/ioutil" + "os" + "os/exec" + "testing" + "time" + "unicode" + + "github.com/kr/pty" +) + +// #5979 +func TestEventsRedirectStdout(t *testing.T) { + + since := time.Now().Unix() + + dockerCmd(t, "run", "busybox", "true") + + defer deleteAllContainers() + + file, err := ioutil.TempFile("", "") + if err != nil { + t.Fatalf("could not create temp file: %v", err) + } + defer os.Remove(file.Name()) + + command := fmt.Sprintf("%s events --since=%d --until=%d > %s", dockerBinary, since, time.Now().Unix(), file.Name()) + _, tty, err := pty.Open() + if err != nil { + t.Fatalf("Could not open pty: %v", err) + } + cmd := exec.Command("sh", "-c", command) + cmd.Stdin = tty + cmd.Stdout = tty + cmd.Stderr = tty + if err := cmd.Run(); err != nil { + t.Fatalf("run err for command %q: %v", command, err) + } + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + for _, c := range scanner.Text() { + if unicode.IsControl(c) { + t.Fatalf("found control character %v", []byte(string(c))) + } + } + } + if err := scanner.Err(); err != nil { + t.Fatalf("Scan err for command %q: %v", command, err) + } + + logDone("events - redirect stdout") +} diff --git a/integration-cli/docker_cli_run_test.go b/integration-cli/docker_cli_run_test.go index fafb31d89..876b7e645 100644 --- a/integration-cli/docker_cli_run_test.go +++ b/integration-cli/docker_cli_run_test.go @@ -20,9 +20,7 @@ import ( "time" "github.com/docker/docker/nat" - "github.com/docker/docker/pkg/mount" "github.com/docker/docker/pkg/networkfs/resolvconf" - "github.com/kr/pty" ) // "test123" should be printed by docker run @@ -1240,45 +1238,6 @@ func TestRunDisallowBindMountingRootToRoot(t *testing.T) { logDone("run - bind mount /:/ as volume should fail") } -// Test recursive bind mount works by default -func TestRunWithVolumesIsRecursive(t *testing.T) { - tmpDir, err := ioutil.TempDir("", "docker_recursive_mount_test") - if err != nil { - t.Fatal(err) - } - - defer os.RemoveAll(tmpDir) - - // Create a temporary tmpfs mount. - tmpfsDir := filepath.Join(tmpDir, "tmpfs") - if err := os.MkdirAll(tmpfsDir, 0777); err != nil { - t.Fatalf("failed to mkdir at %s - %s", tmpfsDir, err) - } - if err := mount.Mount("tmpfs", tmpfsDir, "tmpfs", ""); err != nil { - t.Fatalf("failed to create a tmpfs mount at %s - %s", tmpfsDir, err) - } - defer mount.Unmount(tmpfsDir) - - f, err := ioutil.TempFile(tmpfsDir, "touch-me") - if err != nil { - t.Fatal(err) - } - defer f.Close() - - runCmd := exec.Command(dockerBinary, "run", "--name", "test-data", "--volume", fmt.Sprintf("%s:/tmp:ro", tmpDir), "busybox:latest", "ls", "/tmp/tmpfs") - out, stderr, exitCode, err := runCommandWithStdoutStderr(runCmd) - if err != nil && exitCode != 0 { - t.Fatal(out, stderr, err) - } - if !strings.Contains(out, filepath.Base(f.Name())) { - t.Fatal("Recursive bind mount test failed. Expected file not found") - } - - deleteAllContainers() - - logDone("run - volumes are bind mounted recursively") -} - func TestRunDnsDefaultOptions(t *testing.T) { // ci server has default resolv.conf // so rewrite it for the test @@ -2283,44 +2242,6 @@ func TestRunExecDir(t *testing.T) { logDone("run - check execdriver dir behavior") } -// #6509 -func TestRunRedirectStdout(t *testing.T) { - - defer deleteAllContainers() - - checkRedirect := func(command string) { - _, tty, err := pty.Open() - if err != nil { - t.Fatalf("Could not open pty: %v", err) - } - cmd := exec.Command("sh", "-c", command) - cmd.Stdin = tty - cmd.Stdout = tty - cmd.Stderr = tty - ch := make(chan struct{}) - if err := cmd.Start(); err != nil { - t.Fatalf("start err: %v", err) - } - go func() { - if err := cmd.Wait(); err != nil { - t.Fatalf("wait err=%v", err) - } - close(ch) - }() - - select { - case <-time.After(10 * time.Second): - t.Fatal("command timeout") - case <-ch: - } - } - - checkRedirect(dockerBinary + " run -i busybox cat /etc/passwd | grep -q root") - checkRedirect(dockerBinary + " run busybox cat /etc/passwd | grep -q root") - - logDone("run - redirect stdout") -} - // Regression test for https://github.com/docker/docker/issues/8259 func TestRunReuseBindVolumeThatIsSymlink(t *testing.T) { tmpDir, err := ioutil.TempDir(os.TempDir(), "testlink") diff --git a/integration-cli/docker_cli_run_test_unix.go b/integration-cli/docker_cli_run_test_unix.go new file mode 100644 index 000000000..e12611cec --- /dev/null +++ b/integration-cli/docker_cli_run_test_unix.go @@ -0,0 +1,93 @@ +// +build !windows + +package main + +import ( + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/docker/docker/pkg/mount" + "github.com/kr/pty" +) + +// #6509 +func TestRunRedirectStdout(t *testing.T) { + + defer deleteAllContainers() + + checkRedirect := func(command string) { + _, tty, err := pty.Open() + if err != nil { + t.Fatalf("Could not open pty: %v", err) + } + cmd := exec.Command("sh", "-c", command) + cmd.Stdin = tty + cmd.Stdout = tty + cmd.Stderr = tty + ch := make(chan struct{}) + if err := cmd.Start(); err != nil { + t.Fatalf("start err: %v", err) + } + go func() { + if err := cmd.Wait(); err != nil { + t.Fatalf("wait err=%v", err) + } + close(ch) + }() + + select { + case <-time.After(10 * time.Second): + t.Fatal("command timeout") + case <-ch: + } + } + + checkRedirect(dockerBinary + " run -i busybox cat /etc/passwd | grep -q root") + checkRedirect(dockerBinary + " run busybox cat /etc/passwd | grep -q root") + + logDone("run - redirect stdout") +} + +// Test recursive bind mount works by default +func TestRunWithVolumesIsRecursive(t *testing.T) { + tmpDir, err := ioutil.TempDir("", "docker_recursive_mount_test") + if err != nil { + t.Fatal(err) + } + + defer os.RemoveAll(tmpDir) + + // Create a temporary tmpfs mount. + tmpfsDir := filepath.Join(tmpDir, "tmpfs") + if err := os.MkdirAll(tmpfsDir, 0777); err != nil { + t.Fatalf("failed to mkdir at %s - %s", tmpfsDir, err) + } + if err := mount.Mount("tmpfs", tmpfsDir, "tmpfs", ""); err != nil { + t.Fatalf("failed to create a tmpfs mount at %s - %s", tmpfsDir, err) + } + + f, err := ioutil.TempFile(tmpfsDir, "touch-me") + if err != nil { + t.Fatal(err) + } + defer f.Close() + + runCmd := exec.Command(dockerBinary, "run", "--name", "test-data", "--volume", fmt.Sprintf("%s:/tmp:ro", tmpDir), "busybox:latest", "ls", "/tmp/tmpfs") + out, stderr, exitCode, err := runCommandWithStdoutStderr(runCmd) + if err != nil && exitCode != 0 { + t.Fatal(out, stderr, err) + } + if !strings.Contains(out, filepath.Base(f.Name())) { + t.Fatal("Recursive bind mount test failed. Expected file not found") + } + + deleteAllContainers() + + logDone("run - volumes are bind mounted recursively") +} diff --git a/integration-cli/docker_cli_save_load_test.go b/integration-cli/docker_cli_save_load_test.go index c745df69f..3130c1a26 100644 --- a/integration-cli/docker_cli_save_load_test.go +++ b/integration-cli/docker_cli_save_load_test.go @@ -1,7 +1,6 @@ package main import ( - "bytes" "fmt" "io/ioutil" "os" @@ -11,99 +10,8 @@ import ( "sort" "strings" "testing" - - "github.com/docker/docker/vendor/src/github.com/kr/pty" ) -// save a repo and try to load it using stdout -func TestSaveAndLoadRepoStdout(t *testing.T) { - runCmd := exec.Command(dockerBinary, "run", "-d", "busybox", "true") - out, _, err := runCommandWithOutput(runCmd) - if err != nil { - t.Fatalf("failed to create a container: %s, %v", out, err) - } - - cleanedContainerID := stripTrailingCharacters(out) - - repoName := "foobar-save-load-test" - - inspectCmd := exec.Command(dockerBinary, "inspect", cleanedContainerID) - if out, _, err = runCommandWithOutput(inspectCmd); err != nil { - t.Fatalf("output should've been a container id: %s, %v", out, err) - } - - commitCmd := exec.Command(dockerBinary, "commit", cleanedContainerID, repoName) - if out, _, err = runCommandWithOutput(commitCmd); err != nil { - t.Fatalf("failed to commit container: %s, %v", out, err) - } - - inspectCmd = exec.Command(dockerBinary, "inspect", repoName) - before, _, err := runCommandWithOutput(inspectCmd) - if err != nil { - t.Fatalf("the repo should exist before saving it: %s, %v", before, err) - } - - saveCmdTemplate := `%v save %v > /tmp/foobar-save-load-test.tar` - saveCmdFinal := fmt.Sprintf(saveCmdTemplate, dockerBinary, repoName) - saveCmd := exec.Command("bash", "-c", saveCmdFinal) - if out, _, err = runCommandWithOutput(saveCmd); err != nil { - t.Fatalf("failed to save repo: %s, %v", out, err) - } - - deleteImages(repoName) - - loadCmdFinal := `cat /tmp/foobar-save-load-test.tar | docker load` - loadCmd := exec.Command("bash", "-c", loadCmdFinal) - if out, _, err = runCommandWithOutput(loadCmd); err != nil { - t.Fatalf("failed to load repo: %s, %v", out, err) - } - - inspectCmd = exec.Command(dockerBinary, "inspect", repoName) - after, _, err := runCommandWithOutput(inspectCmd) - if err != nil { - t.Fatalf("the repo should exist after loading it: %s %v", after, err) - } - - if before != after { - t.Fatalf("inspect is not the same after a save / load") - } - - deleteContainer(cleanedContainerID) - deleteImages(repoName) - - os.Remove("/tmp/foobar-save-load-test.tar") - - logDone("save - save/load a repo using stdout") - - pty, tty, err := pty.Open() - if err != nil { - t.Fatalf("Could not open pty: %v", err) - } - cmd := exec.Command(dockerBinary, "save", repoName) - cmd.Stdin = tty - cmd.Stdout = tty - cmd.Stderr = tty - if err := cmd.Start(); err != nil { - t.Fatalf("start err: %v", err) - } - if err := cmd.Wait(); err == nil { - t.Fatal("did not break writing to a TTY") - } - - buf := make([]byte, 1024) - - n, err := pty.Read(buf) - if err != nil { - t.Fatal("could not read tty output") - } - - if !bytes.Contains(buf[:n], []byte("Cowardly refusing")) { - t.Fatal("help output is not being yielded", out) - } - - logDone("save - do not save to a tty") -} - // save a repo using gz compression and try to load it using stdout func TestSaveXzAndLoadRepoStdout(t *testing.T) { tempDir, err := ioutil.TempDir("", "test-save-xz-gz-load-repo-stdout") diff --git a/integration-cli/docker_cli_save_load_test_unix.go b/integration-cli/docker_cli_save_load_test_unix.go new file mode 100644 index 000000000..c6704877a --- /dev/null +++ b/integration-cli/docker_cli_save_load_test_unix.go @@ -0,0 +1,102 @@ +// +build !windows + +package main + +import ( + "bytes" + "fmt" + "os" + "os/exec" + "testing" + + "github.com/docker/docker/vendor/src/github.com/kr/pty" +) + +// save a repo and try to load it using stdout +func TestSaveAndLoadRepoStdout(t *testing.T) { + runCmd := exec.Command(dockerBinary, "run", "-d", "busybox", "true") + out, _, err := runCommandWithOutput(runCmd) + if err != nil { + t.Fatalf("failed to create a container: %s, %v", out, err) + } + + cleanedContainerID := stripTrailingCharacters(out) + + repoName := "foobar-save-load-test" + + inspectCmd := exec.Command(dockerBinary, "inspect", cleanedContainerID) + if out, _, err = runCommandWithOutput(inspectCmd); err != nil { + t.Fatalf("output should've been a container id: %s, %v", out, err) + } + + commitCmd := exec.Command(dockerBinary, "commit", cleanedContainerID, repoName) + if out, _, err = runCommandWithOutput(commitCmd); err != nil { + t.Fatalf("failed to commit container: %s, %v", out, err) + } + + inspectCmd = exec.Command(dockerBinary, "inspect", repoName) + before, _, err := runCommandWithOutput(inspectCmd) + if err != nil { + t.Fatalf("the repo should exist before saving it: %s, %v", before, err) + } + + saveCmdTemplate := `%v save %v > /tmp/foobar-save-load-test.tar` + saveCmdFinal := fmt.Sprintf(saveCmdTemplate, dockerBinary, repoName) + saveCmd := exec.Command("bash", "-c", saveCmdFinal) + if out, _, err = runCommandWithOutput(saveCmd); err != nil { + t.Fatalf("failed to save repo: %s, %v", out, err) + } + + deleteImages(repoName) + + loadCmdFinal := `cat /tmp/foobar-save-load-test.tar | docker load` + loadCmd := exec.Command("bash", "-c", loadCmdFinal) + if out, _, err = runCommandWithOutput(loadCmd); err != nil { + t.Fatalf("failed to load repo: %s, %v", out, err) + } + + inspectCmd = exec.Command(dockerBinary, "inspect", repoName) + after, _, err := runCommandWithOutput(inspectCmd) + if err != nil { + t.Fatalf("the repo should exist after loading it: %s %v", after, err) + } + + if before != after { + t.Fatalf("inspect is not the same after a save / load") + } + + deleteContainer(cleanedContainerID) + deleteImages(repoName) + + os.Remove("/tmp/foobar-save-load-test.tar") + + logDone("save - save/load a repo using stdout") + + pty, tty, err := pty.Open() + if err != nil { + t.Fatalf("Could not open pty: %v", err) + } + cmd := exec.Command(dockerBinary, "save", repoName) + cmd.Stdin = tty + cmd.Stdout = tty + cmd.Stderr = tty + if err := cmd.Start(); err != nil { + t.Fatalf("start err: %v", err) + } + if err := cmd.Wait(); err == nil { + t.Fatal("did not break writing to a TTY") + } + + buf := make([]byte, 1024) + + n, err := pty.Read(buf) + if err != nil { + t.Fatal("could not read tty output") + } + + if !bytes.Contains(buf[:n], []byte("Cowardly refusing")) { + t.Fatal("help output is not being yielded", out) + } + + logDone("save - do not save to a tty") +} diff --git a/integration-cli/docker_test_vars.go b/integration-cli/docker_test_vars.go index 78c481bd2..3bfb8ac03 100644 --- a/integration-cli/docker_test_vars.go +++ b/integration-cli/docker_test_vars.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "os/exec" + "runtime" ) var ( @@ -25,16 +26,26 @@ var ( workingDirectory string ) +func binarySearchCommand() *exec.Cmd { + if runtime.GOOS == "windows" { + // Windows where.exe is included since Windows Server 2003. It accepts + // wildcards, which we use here to match the development builds binary + // names (such as docker-$VERSION.exe). + return exec.Command("where.exe", "docker*.exe") + } + return exec.Command("which", "docker") +} + func init() { if dockerBin := os.Getenv("DOCKER_BINARY"); dockerBin != "" { dockerBinary = dockerBin } else { - whichCmd := exec.Command("which", "docker") + whichCmd := binarySearchCommand() out, _, err := runCommandWithOutput(whichCmd) if err == nil { dockerBinary = stripTrailingCharacters(out) } else { - fmt.Printf("ERROR: couldn't resolve full path to the Docker binary") + fmt.Printf("ERROR: couldn't resolve full path to the Docker binary (%v)", err) os.Exit(1) } } diff --git a/integration-cli/utils.go b/integration-cli/utils.go index 2de432549..fefd66f33 100644 --- a/integration-cli/utils.go +++ b/integration-cli/utils.go @@ -100,9 +100,7 @@ func logDone(message string) { } func stripTrailingCharacters(target string) string { - target = strings.Trim(target, "\n") - target = strings.Trim(target, " ") - return target + return strings.TrimSpace(target) } func unmarshalJSON(data []byte, result interface{}) error { diff --git a/project/make/.integration-daemon-start b/project/make/.integration-daemon-start index b974422cd..3c796399b 100644 --- a/project/make/.integration-daemon-start +++ b/project/make/.integration-daemon-start @@ -15,10 +15,14 @@ exec 41>&1 42>&2 DOCKER_GRAPHDRIVER=${DOCKER_GRAPHDRIVER:-vfs} DOCKER_EXECDRIVER=${DOCKER_EXECDRIVER:-native} -( set -x; exec \ - docker --daemon --debug \ - --storage-driver "$DOCKER_GRAPHDRIVER" \ - --exec-driver "$DOCKER_EXECDRIVER" \ - --pidfile "$DEST/docker.pid" \ - &> "$DEST/docker.log" -) & +if [ -z "$DOCKER_TEST_HOST" ]; then + ( set -x; exec \ + docker --daemon --debug \ + --storage-driver "$DOCKER_GRAPHDRIVER" \ + --exec-driver "$DOCKER_EXECDRIVER" \ + --pidfile "$DEST/docker.pid" \ + &> "$DEST/docker.log" + ) & +else + export DOCKER_HOST="$DOCKER_TEST_HOST" +fi From cc89b30d35edc02fc598a0b26fe7a1ed002238e4 Mon Sep 17 00:00:00 2001 From: Porjo Date: Fri, 27 Jun 2014 17:29:55 +1000 Subject: [PATCH 089/513] Move per-container forward rules to DOCKER chain Docker-DCO-1.1-Signed-off-by: Ian Bishop (github: porjo) --- daemon/networkdriver/bridge/driver.go | 58 ++++++++++------- daemon/networkdriver/portmapper/mapper.go | 2 +- links/links.go | 4 +- pkg/iptables/iptables.go | 77 ++++++++++++++++++++--- 4 files changed, 109 insertions(+), 32 deletions(-) diff --git a/daemon/networkdriver/bridge/driver.go b/daemon/networkdriver/bridge/driver.go index e0467b6bd..880eaefef 100644 --- a/daemon/networkdriver/bridge/driver.go +++ b/daemon/networkdriver/bridge/driver.go @@ -6,6 +6,7 @@ import ( "net" "os" "strconv" + "strings" "sync" log "github.com/Sirupsen/logrus" @@ -501,35 +502,48 @@ func AllocatePort(job *engine.Job) engine.Status { func LinkContainers(job *engine.Job) engine.Status { var ( action = job.Args[0] + nfAction iptables.Action childIP = job.Getenv("ChildIP") parentIP = job.Getenv("ParentIP") ignoreErrors = job.GetenvBool("IgnoreErrors") ports = job.GetenvList("Ports") + chain = iptables.Chain{} ) - for _, value := range ports { - port := nat.Port(value) - if output, err := iptables.Raw(action, "FORWARD", - "-i", bridgeIface, "-o", bridgeIface, - "-p", port.Proto(), - "-s", parentIP, - "--dport", strconv.Itoa(port.Int()), - "-d", childIP, - "-j", "ACCEPT"); !ignoreErrors && err != nil { - return job.Error(err) - } else if len(output) != 0 { - return job.Errorf("Error toggle iptables forward: %s", output) - } + split := func(p string) (string, string) { + parts := strings.Split(p, "/") + return parts[0], parts[1] + } - if output, err := iptables.Raw(action, "FORWARD", - "-i", bridgeIface, "-o", bridgeIface, - "-p", port.Proto(), - "-s", childIP, - "--sport", strconv.Itoa(port.Int()), - "-d", parentIP, - "-j", "ACCEPT"); !ignoreErrors && err != nil { + switch action { + case "-A": + nfAction = iptables.Append + case "-I": + nfAction = iptables.Insert + case "-D": + nfAction = iptables.Delete + default: + return job.Errorf("Invalid action '%s' specified", action) + } + + ip1 := net.ParseIP(parentIP) + if ip1 == nil { + return job.Errorf("parent IP '%s' is invalid", parentIP) + } + ip2 := net.ParseIP(childIP) + if ip2 == nil { + return job.Errorf("child IP '%s' is invalid", childIP) + } + + chain.Name = "DOCKER" + chain.Bridge = bridgeIface + for _, p := range ports { + portStr, proto := split(p) + port, err := strconv.Atoi(portStr) + if !ignoreErrors && err != nil { + return job.Errorf("port '%s' is invalid", portStr) + } + if err := chain.Link(nfAction, ip1, ip2, port, proto); !ignoreErrors && err != nil { return job.Error(err) - } else if len(output) != 0 { - return job.Errorf("Error toggle iptables forward: %s", output) } } return engine.StatusOK diff --git a/daemon/networkdriver/portmapper/mapper.go b/daemon/networkdriver/portmapper/mapper.go index 4bf8cd142..9f2ca5a75 100644 --- a/daemon/networkdriver/portmapper/mapper.go +++ b/daemon/networkdriver/portmapper/mapper.go @@ -93,7 +93,7 @@ func Map(container net.Addr, hostIP net.IP, hostPort int) (host net.Addr, err er } containerIP, containerPort := getIPAndPort(m.container) - if err := forward(iptables.Add, m.proto, hostIP, allocatedHostPort, containerIP.String(), containerPort); err != nil { + if err := forward(iptables.Append, m.proto, hostIP, allocatedHostPort, containerIP.String(), containerPort); err != nil { return nil, err } diff --git a/links/links.go b/links/links.go index fc4d95ab0..ab03eadf5 100644 --- a/links/links.go +++ b/links/links.go @@ -138,7 +138,8 @@ func (l *Link) getDefaultPort() *nat.Port { } func (l *Link) Enable() error { - if err := l.toggle("-I", false); err != nil { + // -A == iptables append flag + if err := l.toggle("-A", false); err != nil { return err } l.IsEnabled = true @@ -148,6 +149,7 @@ func (l *Link) Enable() error { func (l *Link) Disable() { // We do not care about errors here because the link may not // exist in iptables + // -D == iptables delete flag l.toggle("-D", true) l.IsEnabled = false diff --git a/pkg/iptables/iptables.go b/pkg/iptables/iptables.go index b783347fa..a7d216a97 100644 --- a/pkg/iptables/iptables.go +++ b/pkg/iptables/iptables.go @@ -15,8 +15,9 @@ import ( type Action string const ( - Add Action = "-A" + Append Action = "-A" Delete Action = "-D" + Insert Action = "-I" ) var ( @@ -54,10 +55,10 @@ func NewChain(name, bridge string) (*Chain, error) { Bridge: bridge, } - if err := chain.Prerouting(Add, "-m", "addrtype", "--dst-type", "LOCAL"); err != nil { + if err := chain.Prerouting(Append, "-m", "addrtype", "--dst-type", "LOCAL"); err != nil { return nil, fmt.Errorf("Failed to inject docker in PREROUTING chain: %s", err) } - if err := chain.Output(Add, "-m", "addrtype", "--dst-type", "LOCAL", "!", "--dst", "127.0.0.0/8"); err != nil { + if err := chain.Output(Append, "-m", "addrtype", "--dst-type", "LOCAL", "!", "--dst", "127.0.0.0/8"); err != nil { return nil, fmt.Errorf("Failed to inject docker in OUTPUT chain: %s", err) } return chain, nil @@ -78,7 +79,7 @@ func (c *Chain) Forward(action Action, ip net.IP, port int, proto, dest_addr str // value" by both iptables and ip6tables. daddr = "0/0" } - if output, err := Raw("-t", "nat", fmt.Sprint(action), c.Name, + if output, err := Raw("-t", "nat", string(action), c.Name, "-p", proto, "-d", daddr, "--dport", strconv.Itoa(port), @@ -90,11 +91,13 @@ func (c *Chain) Forward(action Action, ip net.IP, port int, proto, dest_addr str return &ChainError{Chain: "FORWARD", Output: output} } - fAction := action - if fAction == Add { - fAction = "-I" + if action != Delete { + if err := c.createForwardChain(); err != nil { + return err + } } - if output, err := Raw(string(fAction), "FORWARD", + + if output, err := Raw(string(action), c.Name, "!", "-i", c.Bridge, "-o", c.Bridge, "-p", proto, @@ -109,6 +112,39 @@ func (c *Chain) Forward(action Action, ip net.IP, port int, proto, dest_addr str return nil } +func (c *Chain) Link(action Action, ip1, ip2 net.IP, port int, proto string) error { + if action != Delete { + if err := c.createForwardChain(); err != nil { + return err + } + } + if output, err := Raw(string(action), c.Name, + "-i", c.Bridge, "-o", c.Bridge, + "-p", proto, + "-s", ip1.String(), + "--dport", strconv.Itoa(port), + "-d", ip2.String(), + "-j", "ACCEPT"); err != nil { + return err + } else if len(output) != 0 { + return fmt.Errorf("Error toggle iptables forward: %s", output) + } + + if output, err := Raw(string(action), c.Name, + "-i", c.Bridge, "-o", c.Bridge, + "-p", proto, + "-s", ip2.String(), + "--dport", strconv.Itoa(port), + "-d", ip1.String(), + "-j", "ACCEPT"); err != nil { + return err + } else if len(output) != 0 { + return fmt.Errorf("Error toggle iptables forward: %s", output) + } + + return nil +} + func (c *Chain) Prerouting(action Action, args ...string) error { a := append(nat, fmt.Sprint(action), "PREROUTING") if len(args) > 0 { @@ -199,3 +235,28 @@ func Raw(args ...string) ([]byte, error) { return output, err } + +func (c *Chain) createForwardChain() error { + // Add chain if doesn't exist + if _, err := Raw("-n", "-L", c.Name); err != nil { + output, err := Raw("-N", c.Name) + if err != nil { + return err + } else if len(output) != 0 { + return fmt.Errorf("Error iptables forward: %s", output) + } + } + // Add linking rule if it doesn't exist + if !Exists("FORWARD", + "-o", c.Bridge, + "-j", c.Name) { + if output2, err := Raw(string(Insert), "FORWARD", + "-o", c.Bridge, + "-j", c.Name); err != nil { + return err + } else if len(output2) != 0 { + return fmt.Errorf("Error iptables forward: %s", output2) + } + } + return nil +} From 2865373894f1532fa725481e8f04db4a5d7a0aa8 Mon Sep 17 00:00:00 2001 From: Porjo Date: Wed, 16 Jul 2014 22:03:01 +1000 Subject: [PATCH 090/513] Create DOCKER forward chain on driver init Docker-DCO-1.1-Signed-off-by: Ian Bishop (github: porjo) --- daemon/networkdriver/bridge/driver.go | 8 +- pkg/iptables/iptables.go | 157 ++++++++++++++------------ 2 files changed, 90 insertions(+), 75 deletions(-) diff --git a/daemon/networkdriver/bridge/driver.go b/daemon/networkdriver/bridge/driver.go index 880eaefef..9272d9ddd 100644 --- a/daemon/networkdriver/bridge/driver.go +++ b/daemon/networkdriver/bridge/driver.go @@ -145,12 +145,16 @@ func InitDriver(job *engine.Job) engine.Status { } // We can always try removing the iptables - if err := iptables.RemoveExistingChain("DOCKER"); err != nil { + if err := iptables.RemoveExistingChain("DOCKER", iptables.Nat); err != nil { return job.Error(err) } if enableIPTables { - chain, err := iptables.NewChain("DOCKER", bridgeIface) + _, err := iptables.NewChain("DOCKER", bridgeIface, iptables.Nat) + if err != nil { + return job.Error(err) + } + chain, err := iptables.NewChain("DOCKER", bridgeIface, iptables.Filter) if err != nil { return job.Error(err) } diff --git a/pkg/iptables/iptables.go b/pkg/iptables/iptables.go index a7d216a97..c58db9c68 100644 --- a/pkg/iptables/iptables.go +++ b/pkg/iptables/iptables.go @@ -13,15 +13,17 @@ import ( ) type Action string +type Table string const ( Append Action = "-A" Delete Action = "-D" Insert Action = "-I" + Nat Table = "nat" + Filter Table = "filter" ) var ( - nat = []string{"-t", "nat"} supportsXlock = false ErrIptablesNotFound = errors.New("Iptables not found") ) @@ -29,6 +31,7 @@ var ( type Chain struct { Name string Bridge string + Table Table } type ChainError struct { @@ -44,33 +47,73 @@ func init() { supportsXlock = exec.Command("iptables", "--wait", "-L", "-n").Run() == nil } -func NewChain(name, bridge string) (*Chain, error) { - if output, err := Raw("-t", "nat", "-N", name); err != nil { - return nil, err - } else if len(output) != 0 { - return nil, fmt.Errorf("Error creating new iptables chain: %s", output) - } - chain := &Chain{ +func NewChain(name, bridge string, table Table) (*Chain, error) { + c := &Chain{ Name: name, Bridge: bridge, + Table: table, } - if err := chain.Prerouting(Append, "-m", "addrtype", "--dst-type", "LOCAL"); err != nil { - return nil, fmt.Errorf("Failed to inject docker in PREROUTING chain: %s", err) + if string(c.Table) == "" { + c.Table = Filter } - if err := chain.Output(Append, "-m", "addrtype", "--dst-type", "LOCAL", "!", "--dst", "127.0.0.0/8"); err != nil { - return nil, fmt.Errorf("Failed to inject docker in OUTPUT chain: %s", err) + + // Add chain if it doesn't exist + if _, err := Raw("-t", string(c.Table), "-n", "-L", c.Name); err != nil { + if output, err := Raw("-t", string(c.Table), "-N", c.Name); err != nil { + return nil, err + } else if len(output) != 0 { + return nil, fmt.Errorf("Could not create %s/%s chain: %s", c.Table, c.Name, output) + } } - return chain, nil + + switch table { + case Nat: + preroute := []string{ + "-m", "addrtype", + "--dst-type", "LOCAL"} + if !Exists(preroute...) { + if err := c.Prerouting(Append, preroute...); err != nil { + return nil, fmt.Errorf("Failed to inject docker in PREROUTING chain: %s", err) + } + } + output := []string{ + "-m", "addrtype", + "--dst-type", "LOCAL", + "!", "--dst", "127.0.0.0/8"} + if !Exists(output...) { + if err := c.Output(Append, output...); err != nil { + return nil, fmt.Errorf("Failed to inject docker in OUTPUT chain: %s", err) + } + } + case Filter: + link := []string{"FORWARD", + "-o", c.Bridge, + "-j", c.Name} + if !Exists(link...) { + insert := append([]string{string(Insert)}, link...) + if output, err := Raw(insert...); err != nil { + return nil, err + } else if len(output) != 0 { + return nil, fmt.Errorf("Could not create linking rule to %s/%s: %s", c.Table, c.Name, output) + } + } + } + return c, nil } -func RemoveExistingChain(name string) error { - chain := &Chain{ - Name: name, +func RemoveExistingChain(name string, table Table) error { + c := &Chain{ + Name: name, + Table: table, } - return chain.Remove() + if string(c.Table) == "" { + c.Table = Filter + } + return c.Remove() } +// Add forwarding rule to 'filter' table and corresponding nat rule to 'nat' table func (c *Chain) Forward(action Action, ip net.IP, port int, proto, dest_addr string, dest_port int) error { daddr := ip.String() if ip.IsUnspecified() { @@ -79,7 +122,7 @@ func (c *Chain) Forward(action Action, ip net.IP, port int, proto, dest_addr str // value" by both iptables and ip6tables. daddr = "0/0" } - if output, err := Raw("-t", "nat", string(action), c.Name, + if output, err := Raw("-t", string(Nat), string(action), c.Name, "-p", proto, "-d", daddr, "--dport", strconv.Itoa(port), @@ -91,13 +134,7 @@ func (c *Chain) Forward(action Action, ip net.IP, port int, proto, dest_addr str return &ChainError{Chain: "FORWARD", Output: output} } - if action != Delete { - if err := c.createForwardChain(); err != nil { - return err - } - } - - if output, err := Raw(string(action), c.Name, + if output, err := Raw("-t", string(Filter), string(action), c.Name, "!", "-i", c.Bridge, "-o", c.Bridge, "-p", proto, @@ -112,13 +149,10 @@ func (c *Chain) Forward(action Action, ip net.IP, port int, proto, dest_addr str return nil } +// Add reciprocal ACCEPT rule for two supplied IP addresses. +// Traffic is allowed from ip1 to ip2 and vice-versa func (c *Chain) Link(action Action, ip1, ip2 net.IP, port int, proto string) error { - if action != Delete { - if err := c.createForwardChain(); err != nil { - return err - } - } - if output, err := Raw(string(action), c.Name, + if output, err := Raw("-t", string(Filter), string(action), c.Name, "-i", c.Bridge, "-o", c.Bridge, "-p", proto, "-s", ip1.String(), @@ -127,10 +161,9 @@ func (c *Chain) Link(action Action, ip1, ip2 net.IP, port int, proto string) err "-j", "ACCEPT"); err != nil { return err } else if len(output) != 0 { - return fmt.Errorf("Error toggle iptables forward: %s", output) + return fmt.Errorf("Error iptables forward: %s", output) } - - if output, err := Raw(string(action), c.Name, + if output, err := Raw("-t", string(Filter), string(action), c.Name, "-i", c.Bridge, "-o", c.Bridge, "-p", proto, "-s", ip2.String(), @@ -139,14 +172,14 @@ func (c *Chain) Link(action Action, ip1, ip2 net.IP, port int, proto string) err "-j", "ACCEPT"); err != nil { return err } else if len(output) != 0 { - return fmt.Errorf("Error toggle iptables forward: %s", output) + return fmt.Errorf("Error iptables forward: %s", output) } - return nil } +// Add linking rule to nat/PREROUTING chain. func (c *Chain) Prerouting(action Action, args ...string) error { - a := append(nat, fmt.Sprint(action), "PREROUTING") + a := []string{"-t", string(Nat), string(action), "PREROUTING"} if len(args) > 0 { a = append(a, args...) } @@ -158,8 +191,9 @@ func (c *Chain) Prerouting(action Action, args ...string) error { return nil } +// Add linking rule to an OUTPUT chain func (c *Chain) Output(action Action, args ...string) error { - a := append(nat, fmt.Sprint(action), "OUTPUT") + a := []string{"-t", string(c.Table), string(action), "OUTPUT"} if len(args) > 0 { a = append(a, args...) } @@ -172,21 +206,22 @@ func (c *Chain) Output(action Action, args ...string) error { } func (c *Chain) Remove() error { - // Ignore errors - This could mean the chains were never set up - c.Prerouting(Delete, "-m", "addrtype", "--dst-type", "LOCAL") - c.Output(Delete, "-m", "addrtype", "--dst-type", "LOCAL", "!", "--dst", "127.0.0.0/8") - c.Output(Delete, "-m", "addrtype", "--dst-type", "LOCAL") // Created in versions <= 0.1.6 + if c.Table == Nat { + // Ignore errors - This could mean the chains were never set up + c.Prerouting(Delete, "-m", "addrtype", "--dst-type", "LOCAL") + c.Output(Delete, "-m", "addrtype", "--dst-type", "LOCAL", "!", "--dst", "127.0.0.0/8") + c.Output(Delete, "-m", "addrtype", "--dst-type", "LOCAL") // Created in versions <= 0.1.6 - c.Prerouting(Delete) - c.Output(Delete) - - Raw("-t", "nat", "-F", c.Name) - Raw("-t", "nat", "-X", c.Name) + c.Prerouting(Delete) + c.Output(Delete) + Raw("-t", string(Nat), "-F", c.Name) + Raw("-t", string(Nat), "-X", c.Name) + } return nil } -// Check if an existing rule exists +// Check if a rule exists func Exists(args ...string) bool { // iptables -C, --check option was added in v.1.4.11 // http://ftp.netfilter.org/pub/iptables/changes-iptables-1.4.11.txt @@ -211,6 +246,7 @@ func Exists(args ...string) bool { ) } +// Call 'iptables' system command, passing supplied arguments func Raw(args ...string) ([]byte, error) { path, err := exec.LookPath("iptables") if err != nil { @@ -235,28 +271,3 @@ func Raw(args ...string) ([]byte, error) { return output, err } - -func (c *Chain) createForwardChain() error { - // Add chain if doesn't exist - if _, err := Raw("-n", "-L", c.Name); err != nil { - output, err := Raw("-N", c.Name) - if err != nil { - return err - } else if len(output) != 0 { - return fmt.Errorf("Error iptables forward: %s", output) - } - } - // Add linking rule if it doesn't exist - if !Exists("FORWARD", - "-o", c.Bridge, - "-j", c.Name) { - if output2, err := Raw(string(Insert), "FORWARD", - "-o", c.Bridge, - "-j", c.Name); err != nil { - return err - } else if len(output2) != 0 { - return fmt.Errorf("Error iptables forward: %s", output2) - } - } - return nil -} From 0da92633b4161ed1f8babe5ec4a9fe98257d34b5 Mon Sep 17 00:00:00 2001 From: Ian Bishop Date: Sat, 15 Nov 2014 11:36:38 +1000 Subject: [PATCH 091/513] Create tests for pkg/iptables Docker-DCO-1.1-Signed-off-by: Ian Bishop (github: porjo) --- daemon/networkdriver/bridge/driver.go | 16 +- daemon/networkdriver/bridge/driver_test.go | 41 +++++ integration-cli/docker_cli_links_test.go | 4 +- integration-cli/docker_cli_run_test.go | 2 +- pkg/iptables/iptables.go | 30 ++- pkg/iptables/iptables_test.go | 204 +++++++++++++++++++++ 6 files changed, 272 insertions(+), 25 deletions(-) create mode 100644 pkg/iptables/iptables_test.go diff --git a/daemon/networkdriver/bridge/driver.go b/daemon/networkdriver/bridge/driver.go index 9272d9ddd..228bd479f 100644 --- a/daemon/networkdriver/bridge/driver.go +++ b/daemon/networkdriver/bridge/driver.go @@ -5,8 +5,6 @@ import ( "io/ioutil" "net" "os" - "strconv" - "strings" "sync" log "github.com/Sirupsen/logrus" @@ -513,10 +511,6 @@ func LinkContainers(job *engine.Job) engine.Status { ports = job.GetenvList("Ports") chain = iptables.Chain{} ) - split := func(p string) (string, string) { - parts := strings.Split(p, "/") - return parts[0], parts[1] - } switch action { case "-A": @@ -541,13 +535,11 @@ func LinkContainers(job *engine.Job) engine.Status { chain.Name = "DOCKER" chain.Bridge = bridgeIface for _, p := range ports { - portStr, proto := split(p) - port, err := strconv.Atoi(portStr) - if !ignoreErrors && err != nil { - return job.Errorf("port '%s' is invalid", portStr) - } - if err := chain.Link(nfAction, ip1, ip2, port, proto); !ignoreErrors && err != nil { + port := nat.Port(p) + if err := chain.Link(nfAction, ip1, ip2, port.Int(), port.Proto()); !ignoreErrors && err != nil { + fmt.Print(err) return job.Error(err) + } } return engine.StatusOK diff --git a/daemon/networkdriver/bridge/driver_test.go b/daemon/networkdriver/bridge/driver_test.go index 1bda2f437..02bea9ce1 100644 --- a/daemon/networkdriver/bridge/driver_test.go +++ b/daemon/networkdriver/bridge/driver_test.go @@ -7,6 +7,7 @@ import ( "github.com/docker/docker/daemon/networkdriver/portmapper" "github.com/docker/docker/engine" + "github.com/docker/docker/pkg/iptables" ) func init() { @@ -118,3 +119,43 @@ func TestMacAddrGeneration(t *testing.T) { t.Fatal("Non-unique MAC address") } } + +func TestLinkContainers(t *testing.T) { + eng := engine.New() + eng.Logging = false + + // Init driver + job := eng.Job("initdriver") + if res := InitDriver(job); res != engine.StatusOK { + t.Fatal("Failed to initialize network driver") + } + + // Allocate interface + job = eng.Job("allocate_interface", "container_id") + if res := Allocate(job); res != engine.StatusOK { + t.Fatal("Failed to allocate network interface") + } + + job.Args[0] = "-I" + + job.Setenv("ChildIP", "172.17.0.2") + job.Setenv("ParentIP", "172.17.0.1") + job.SetenvBool("IgnoreErrors", false) + job.SetenvList("Ports", []string{"1234"}) + + bridgeIface = "lo" + _, err := iptables.NewChain("DOCKER", bridgeIface, iptables.Filter) + if err != nil { + t.Fatal(err) + } + + if res := LinkContainers(job); res != engine.StatusOK { + t.Fatalf("LinkContainers failed") + } + + // flush rules + if _, err = iptables.Raw([]string{"-F", "DOCKER"}...); err != nil { + t.Fatal(err) + } + +} diff --git a/integration-cli/docker_cli_links_test.go b/integration-cli/docker_cli_links_test.go index 5b81b7fec..49d46ed94 100644 --- a/integration-cli/docker_cli_links_test.go +++ b/integration-cli/docker_cli_links_test.go @@ -83,8 +83,8 @@ func TestLinksIpTablesRulesWhenLinkAndUnlink(t *testing.T) { childIP := findContainerIP(t, "child") parentIP := findContainerIP(t, "parent") - sourceRule := []string{"FORWARD", "-i", "docker0", "-o", "docker0", "-p", "tcp", "-s", childIP, "--sport", "80", "-d", parentIP, "-j", "ACCEPT"} - destinationRule := []string{"FORWARD", "-i", "docker0", "-o", "docker0", "-p", "tcp", "-s", parentIP, "--dport", "80", "-d", childIP, "-j", "ACCEPT"} + sourceRule := []string{"DOCKER", "-i", "docker0", "-o", "docker0", "-p", "tcp", "-s", childIP, "--sport", "80", "-d", parentIP, "-j", "ACCEPT"} + destinationRule := []string{"DOCKER", "-i", "docker0", "-o", "docker0", "-p", "tcp", "-s", parentIP, "--dport", "80", "-d", childIP, "-j", "ACCEPT"} if !iptables.Exists(sourceRule...) || !iptables.Exists(destinationRule...) { t.Fatal("Iptables rules not found") } diff --git a/integration-cli/docker_cli_run_test.go b/integration-cli/docker_cli_run_test.go index fafb31d89..cb5556bbc 100644 --- a/integration-cli/docker_cli_run_test.go +++ b/integration-cli/docker_cli_run_test.go @@ -2070,7 +2070,7 @@ func TestRunDeallocatePortOnMissingIptablesRule(t *testing.T) { if err != nil { t.Fatal(err) } - iptCmd := exec.Command("iptables", "-D", "FORWARD", "-d", fmt.Sprintf("%s/32", ip), + iptCmd := exec.Command("iptables", "-D", "DOCKER", "-d", fmt.Sprintf("%s/32", ip), "!", "-i", "docker0", "-o", "docker0", "-p", "tcp", "-m", "tcp", "--dport", "23", "-j", "ACCEPT") out, _, err = runCommandWithOutput(iptCmd) if err != nil { diff --git a/pkg/iptables/iptables.go b/pkg/iptables/iptables.go index c58db9c68..90ccbeff5 100644 --- a/pkg/iptables/iptables.go +++ b/pkg/iptables/iptables.go @@ -114,7 +114,7 @@ func RemoveExistingChain(name string, table Table) error { } // Add forwarding rule to 'filter' table and corresponding nat rule to 'nat' table -func (c *Chain) Forward(action Action, ip net.IP, port int, proto, dest_addr string, dest_port int) error { +func (c *Chain) Forward(action Action, ip net.IP, port int, proto, destAddr string, destPort int) error { daddr := ip.String() if ip.IsUnspecified() { // iptables interprets "0.0.0.0" as "0.0.0.0/32", whereas we @@ -128,7 +128,7 @@ func (c *Chain) Forward(action Action, ip net.IP, port int, proto, dest_addr str "--dport", strconv.Itoa(port), "!", "-i", c.Bridge, "-j", "DNAT", - "--to-destination", net.JoinHostPort(dest_addr, strconv.Itoa(dest_port))); err != nil { + "--to-destination", net.JoinHostPort(destAddr, strconv.Itoa(destPort))); err != nil { return err } else if len(output) != 0 { return &ChainError{Chain: "FORWARD", Output: output} @@ -138,14 +138,25 @@ func (c *Chain) Forward(action Action, ip net.IP, port int, proto, dest_addr str "!", "-i", c.Bridge, "-o", c.Bridge, "-p", proto, - "-d", dest_addr, - "--dport", strconv.Itoa(dest_port), + "-d", destAddr, + "--dport", strconv.Itoa(destPort), "-j", "ACCEPT"); err != nil { return err } else if len(output) != 0 { return &ChainError{Chain: "FORWARD", Output: output} } + if output, err := Raw("-t", string(Nat), string(action), "POSTROUTING", + "-p", proto, + "-s", destAddr, + "-d", destAddr, + "--dport", strconv.Itoa(destPort), + "-j", "MASQUERADE"); err != nil { + return err + } else if len(output) != 0 { + return &ChainError{Chain: "FORWARD", Output: output} + } + return nil } @@ -156,8 +167,8 @@ func (c *Chain) Link(action Action, ip1, ip2 net.IP, port int, proto string) err "-i", c.Bridge, "-o", c.Bridge, "-p", proto, "-s", ip1.String(), - "--dport", strconv.Itoa(port), "-d", ip2.String(), + "--dport", strconv.Itoa(port), "-j", "ACCEPT"); err != nil { return err } else if len(output) != 0 { @@ -167,8 +178,8 @@ func (c *Chain) Link(action Action, ip1, ip2 net.IP, port int, proto string) err "-i", c.Bridge, "-o", c.Bridge, "-p", proto, "-s", ip2.String(), - "--dport", strconv.Itoa(port), "-d", ip1.String(), + "--sport", strconv.Itoa(port), "-j", "ACCEPT"); err != nil { return err } else if len(output) != 0 { @@ -206,18 +217,17 @@ func (c *Chain) Output(action Action, args ...string) error { } func (c *Chain) Remove() error { + // Ignore errors - This could mean the chains were never set up if c.Table == Nat { - // Ignore errors - This could mean the chains were never set up c.Prerouting(Delete, "-m", "addrtype", "--dst-type", "LOCAL") c.Output(Delete, "-m", "addrtype", "--dst-type", "LOCAL", "!", "--dst", "127.0.0.0/8") c.Output(Delete, "-m", "addrtype", "--dst-type", "LOCAL") // Created in versions <= 0.1.6 c.Prerouting(Delete) c.Output(Delete) - - Raw("-t", string(Nat), "-F", c.Name) - Raw("-t", string(Nat), "-X", c.Name) } + Raw("-t", string(c.Table), "-F", c.Name) + Raw("-t", string(c.Table), "-X", c.Name) return nil } diff --git a/pkg/iptables/iptables_test.go b/pkg/iptables/iptables_test.go new file mode 100644 index 000000000..8aaf429c9 --- /dev/null +++ b/pkg/iptables/iptables_test.go @@ -0,0 +1,204 @@ +package iptables + +import ( + "net" + "os/exec" + "strconv" + "strings" + "testing" +) + +const chainName = "DOCKERTEST" + +var natChain *Chain +var filterChain *Chain + +func TestNewChain(t *testing.T) { + var err error + + natChain, err = NewChain(chainName, "lo", Nat) + if err != nil { + t.Fatal(err) + } + + filterChain, err = NewChain(chainName, "lo", Filter) + if err != nil { + t.Fatal(err) + } +} + +func TestForward(t *testing.T) { + ip := net.ParseIP("192.168.1.1") + port := 1234 + dstAddr := "172.17.0.1" + dstPort := 4321 + proto := "tcp" + + err := natChain.Forward(Insert, ip, port, proto, dstAddr, dstPort) + if err != nil { + t.Fatal(err) + } + + dnatRule := []string{natChain.Name, + "-t", string(natChain.Table), + "!", "-i", filterChain.Bridge, + "-d", ip.String(), + "-p", proto, + "--dport", strconv.Itoa(port), + "-j", "DNAT", + "--to-destination", dstAddr + ":" + strconv.Itoa(dstPort), + } + + if !Exists(dnatRule...) { + t.Fatalf("DNAT rule does not exist") + } + + filterRule := []string{filterChain.Name, + "-t", string(filterChain.Table), + "!", "-i", filterChain.Bridge, + "-o", filterChain.Bridge, + "-d", dstAddr, + "-p", proto, + "--dport", strconv.Itoa(dstPort), + "-j", "ACCEPT", + } + + if !Exists(filterRule...) { + t.Fatalf("filter rule does not exist") + } + + masqRule := []string{"POSTROUTING", + "-t", string(natChain.Table), + "-d", dstAddr, + "-s", dstAddr, + "-p", proto, + "--dport", strconv.Itoa(dstPort), + "-j", "MASQUERADE", + } + + if !Exists(masqRule...) { + t.Fatalf("MASQUERADE rule does not exist") + } +} + +func TestLink(t *testing.T) { + var err error + + ip1 := net.ParseIP("192.168.1.1") + ip2 := net.ParseIP("192.168.1.2") + port := 1234 + proto := "tcp" + + err = filterChain.Link(Append, ip1, ip2, port, proto) + if err != nil { + t.Fatal(err) + } + + rule1 := []string{filterChain.Name, + "-t", string(filterChain.Table), + "-i", filterChain.Bridge, + "-o", filterChain.Bridge, + "-p", proto, + "-s", ip1.String(), + "-d", ip2.String(), + "--dport", strconv.Itoa(port), + "-j", "ACCEPT"} + + if !Exists(rule1...) { + t.Fatalf("rule1 does not exist") + } + + rule2 := []string{filterChain.Name, + "-t", string(filterChain.Table), + "-i", filterChain.Bridge, + "-o", filterChain.Bridge, + "-p", proto, + "-s", ip2.String(), + "-d", ip1.String(), + "--sport", strconv.Itoa(port), + "-j", "ACCEPT"} + + if !Exists(rule2...) { + t.Fatalf("rule2 does not exist") + } +} + +func TestPrerouting(t *testing.T) { + args := []string{ + "-i", "lo", + "-d", "192.168.1.1"} + + err := natChain.Prerouting(Insert, args...) + if err != nil { + t.Fatal(err) + } + + rule := []string{"PREROUTING", + "-t", string(Nat), + "-j", natChain.Name} + + rule = append(rule, args...) + + if !Exists(rule...) { + t.Fatalf("rule does not exist") + } + + delRule := append([]string{"-D"}, rule...) + if _, err = Raw(delRule...); err != nil { + t.Fatal(err) + } +} + +func TestOutput(t *testing.T) { + args := []string{ + "-o", "lo", + "-d", "192.168.1.1"} + + err := natChain.Output(Insert, args...) + if err != nil { + t.Fatal(err) + } + + rule := []string{"OUTPUT", + "-t", string(natChain.Table), + "-j", natChain.Name} + + rule = append(rule, args...) + + if !Exists(rule...) { + t.Fatalf("rule does not exist") + } + + delRule := append([]string{"-D"}, rule...) + if _, err = Raw(delRule...); err != nil { + t.Fatal(err) + } +} + +func TestCleanup(t *testing.T) { + var err error + var rules []byte + + // Cleanup filter/FORWARD first otherwise output of iptables-save is dirty + link := []string{"-t", string(filterChain.Table), + string(Delete), "FORWARD", + "-o", filterChain.Bridge, + "-j", filterChain.Name} + if _, err = Raw(link...); err != nil { + t.Fatal(err) + } + filterChain.Remove() + + err = RemoveExistingChain(chainName, Nat) + if err != nil { + t.Fatal(err) + } + + rules, err = exec.Command("iptables-save").Output() + if err != nil { + t.Fatal(err) + } + if strings.Contains(string(rules), chainName) { + t.Fatalf("Removing chain failed. %s found in iptables-save", chainName) + } +} From 137ceae9138e535f8b3f4e76bda18f6db954c231 Mon Sep 17 00:00:00 2001 From: Ian Bishop Date: Mon, 1 Dec 2014 11:28:25 +1000 Subject: [PATCH 092/513] Update networking.md with new iptables behaviour Docker-DCO-1.1-Signed-off-by: Ian Bishop (github: porjo) --- docs/sources/articles/networking.md | 67 +++++++++++++++++++---------- 1 file changed, 44 insertions(+), 23 deletions(-) diff --git a/docs/sources/articles/networking.md b/docs/sources/articles/networking.md index 6587efc52..4bfbcfdad 100644 --- a/docs/sources/articles/networking.md +++ b/docs/sources/articles/networking.md @@ -184,30 +184,46 @@ running. The options then modify this default configuration. -Whether a container can talk to the world is governed by one main factor. +Whether a container can talk to the world is governed by two factors. -Is the host machine willing to forward IP packets? This is governed -by the `ip_forward` system parameter. Packets can only pass between -containers if this parameter is `1`. Usually you will simply leave -the Docker server at its default setting `--ip-forward=true` and -Docker will go set `ip_forward` to `1` for you when the server -starts up. To check the setting or turn it on manually: - - # Usually not necessary: turning on forwarding, - # on the host where your Docker server is running +1. Is the host machine willing to forward IP packets? This is governed + by the `ip_forward` system parameter. Packets can only pass between + containers if this parameter is `1`. Usually you will simply leave + the Docker server at its default setting `--ip-forward=true` and + Docker will go set `ip_forward` to `1` for you when the server + starts up. To check the setting or turn it on manually: + ``` $ cat /proc/sys/net/ipv4/ip_forward 0 - $ sudo echo 1 > /proc/sys/net/ipv4/ip_forward + $ echo 1 > /proc/sys/net/ipv4/ip_forward $ cat /proc/sys/net/ipv4/ip_forward 1 + ``` -Many using Docker will want `ip_forward` to be on, to at -least make communication *possible* between containers and -the wider world. + Many using Docker will want `ip_forward` to be on, to at + least make communication *possible* between containers and + the wider world. -May also be needed for inter-container communication if you are -in a multiple bridge setup. + May also be needed for inter-container communication if you are + in a multiple bridge setup. + +2. Do your `iptables` allow this particular connection? Docker will + never make changes to your system `iptables` rules if you set + `--iptables=false` when the daemon starts. Otherwise the Docker + server will append forwarding rules to the `DOCKER` filter chain. + +Docker will not delete or modify any pre-existing rules from the `DOCKER` +filter chain. This allows the user to create in advance any rules required +to further restrict access to the containers. + +Docker's forward rules permit all external source IPs by default. To allow +only a specific IP or network to access the containers, insert a negated +rule at the top of the `DOCKER` filter chain. For example, to restrict +external access such that *only* source IP 8.8.8.8 can access the +containers, the following rule could be added: + + $ iptables -I DOCKER -i ext_if ! -s 8.8.8.8 -j DROP ## Communication between containers @@ -222,12 +238,12 @@ system level, by two factors. between them. See the later sections of this document for other possible topologies. -2. Do your `iptables` allow this particular connection to be made? - Docker will never make changes to your system `iptables` rules if - you set `--iptables=false` when the daemon starts. Otherwise the - Docker server will add a default rule to the `FORWARD` chain with a - blanket `ACCEPT` policy if you retain the default `--icc=true`, or - else will set the policy to `DROP` if `--icc=false`. +2. Do your `iptables` allow this particular connection? Docker will never + make changes to your system `iptables` rules if you set + `--iptables=false` when the daemon starts. Otherwise the Docker server + will add a default rule to the `FORWARD` chain with a blanket `ACCEPT` + policy if you retain the default `--icc=true`, or else will set the + policy to `DROP` if `--icc=false`. It is a strategic question whether to leave `--icc=true` or change it to `--icc=false` (on Ubuntu, by editing the `DOCKER_OPTS` variable in @@ -267,6 +283,7 @@ the `FORWARD` chain has a default policy of `ACCEPT` or `DROP`: ... Chain FORWARD (policy ACCEPT) target prot opt source destination + DOCKER all -- 0.0.0.0/0 0.0.0.0/0 DROP all -- 0.0.0.0/0 0.0.0.0/0 ... @@ -278,9 +295,13 @@ the `FORWARD` chain has a default policy of `ACCEPT` or `DROP`: ... Chain FORWARD (policy ACCEPT) target prot opt source destination + DOCKER all -- 0.0.0.0/0 0.0.0.0/0 + DROP all -- 0.0.0.0/0 0.0.0.0/0 + + Chain DOCKER (1 references) + target prot opt source destination ACCEPT tcp -- 172.17.0.2 172.17.0.3 tcp spt:80 ACCEPT tcp -- 172.17.0.3 172.17.0.2 tcp dpt:80 - DROP all -- 0.0.0.0/0 0.0.0.0/0 > **Note**: > Docker is careful that its host-wide `iptables` rules fully expose From 38a595aec561b8a7b4325b6c1c4efd1b0b8e89c0 Mon Sep 17 00:00:00 2001 From: Ian Bishop Date: Sun, 21 Dec 2014 13:42:02 +1000 Subject: [PATCH 093/513] Tidy driver.go/LinkContainers Docker-DCO-1.1-Signed-off-by: Ian Bishop (github: porjo) --- daemon/networkdriver/bridge/driver.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/daemon/networkdriver/bridge/driver.go b/daemon/networkdriver/bridge/driver.go index 228bd479f..81624ad1d 100644 --- a/daemon/networkdriver/bridge/driver.go +++ b/daemon/networkdriver/bridge/driver.go @@ -509,7 +509,6 @@ func LinkContainers(job *engine.Job) engine.Status { parentIP = job.Getenv("ParentIP") ignoreErrors = job.GetenvBool("IgnoreErrors") ports = job.GetenvList("Ports") - chain = iptables.Chain{} ) switch action { @@ -532,14 +531,11 @@ func LinkContainers(job *engine.Job) engine.Status { return job.Errorf("child IP '%s' is invalid", childIP) } - chain.Name = "DOCKER" - chain.Bridge = bridgeIface + chain := iptables.Chain{Name: "DOCKER", Bridge: bridgeIface} for _, p := range ports { port := nat.Port(p) if err := chain.Link(nfAction, ip1, ip2, port.Int(), port.Proto()); !ignoreErrors && err != nil { - fmt.Print(err) return job.Error(err) - } } return engine.StatusOK From 62e8a93c3496791e8bfb2204f68b2a1cdf61fec9 Mon Sep 17 00:00:00 2001 From: unclejack Date: Thu, 11 Dec 2014 20:53:16 +0200 Subject: [PATCH 094/513] bump Go to 1.4 Signed-off-by: Cristian Staretu --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index a7b6bbc42..94666af86 100644 --- a/Dockerfile +++ b/Dockerfile @@ -61,7 +61,7 @@ RUN cd /usr/local/lvm2 && ./configure --enable-static_link && make device-mapper # see https://git.fedorahosted.org/cgit/lvm2.git/tree/INSTALL # Install Go -RUN curl -sSL https://golang.org/dl/go1.3.3.src.tar.gz | tar -v -C /usr/local -xz +RUN curl -sSL https://golang.org/dl/go1.4.src.tar.gz | tar -v -C /usr/local -xz ENV PATH /usr/local/go/bin:$PATH ENV GOPATH /go:/go/src/github.com/docker/docker/vendor ENV PATH /go/bin:$PATH From 6ac802ecd8e097f49c4b026fd7b5febfc683866c Mon Sep 17 00:00:00 2001 From: Alexander Morozov Date: Thu, 18 Dec 2014 09:06:04 -0800 Subject: [PATCH 095/513] Reinstall standard library with netgo Fixes #9449 Signed-off-by: Alexander Morozov --- Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Dockerfile b/Dockerfile index 94666af86..67e8d8d5a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -78,6 +78,9 @@ ENV DOCKER_CROSSPLATFORMS \ ENV GOARM 5 RUN cd /usr/local/go/src && bash -xc 'for platform in $DOCKER_CROSSPLATFORMS; do GOOS=${platform%/*} GOARCH=${platform##*/} ./make.bash --no-clean 2>&1; done' +# reinstall standard library with netgo +RUN go clean -i net && go install -tags netgo std + # Grab Go's cover tool for dead-simple code coverage testing RUN go get golang.org/x/tools/cmd/cover From a31c14cadca4052a0a141347b322d825f56b814b Mon Sep 17 00:00:00 2001 From: Alexander Morozov Date: Sat, 20 Dec 2014 20:09:35 -0800 Subject: [PATCH 096/513] Fix TestBuildWithTabs for go 1.4 Signed-off-by: Alexander Morozov --- integration-cli/docker_cli_build_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration-cli/docker_cli_build_test.go b/integration-cli/docker_cli_build_test.go index 0d527119c..e440bc770 100644 --- a/integration-cli/docker_cli_build_test.go +++ b/integration-cli/docker_cli_build_test.go @@ -3925,7 +3925,7 @@ func TestBuildWithTabs(t *testing.T) { if err != nil { t.Fatal(err) } - expected := "[\"/bin/sh\",\"-c\",\"echo\\u0009one\\u0009\\u0009two\"]" + expected := `["/bin/sh","-c","echo\tone\t\ttwo"]` if res != expected { t.Fatalf("Missing tabs.\nGot:%s\nExp:%s", res, expected) } From 800a8e896e810019b625ad0161c4fc4a04c26845 Mon Sep 17 00:00:00 2001 From: Jean-Tiare Le Bigot Date: Fri, 19 Dec 2014 09:20:00 +0100 Subject: [PATCH 097/513] start new API v1.17 docs Signed-off-by: Jean-Tiare Le Bigot --- .../reference/api/docker_remote_api.md | 19 +- .../reference/api/docker_remote_api_v1.16.md | 27 +- .../reference/api/docker_remote_api_v1.17.md | 1773 +++++++++++++++++ 3 files changed, 1794 insertions(+), 25 deletions(-) create mode 100644 docs/sources/reference/api/docker_remote_api_v1.17.md diff --git a/docs/sources/reference/api/docker_remote_api.md b/docs/sources/reference/api/docker_remote_api.md index 6e571d3fd..e44576cea 100644 --- a/docs/sources/reference/api/docker_remote_api.md +++ b/docs/sources/reference/api/docker_remote_api.md @@ -30,13 +30,26 @@ page_keywords: API, Docker, rcli, REST, documentation Client applications need to take this into account to ensure they will not break when talking to newer Docker daemons. -The current version of the API is v1.16 +The current version of the API is v1.17 Calling `/info` is the same as calling -`/v1.16/info`. +`/v1.17/info`. You can still call an old version of the API using -`/v1.15/info`. +`/v1.16/info`. + +## v1.17 + +### Full Documentation + +[*Docker Remote API v1.17*](/reference/api/docker_remote_api_v1.17/) + +### What's new + +`POST /containers/(id)/attach` and `POST /exec/(id)/start` + +**New!** +Docker client now hints potential proxies about connection hijacking using HTTP Upgrade headers. ## v1.16 diff --git a/docs/sources/reference/api/docker_remote_api_v1.16.md b/docs/sources/reference/api/docker_remote_api_v1.16.md index f9105f6da..e186a73e6 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.16.md +++ b/docs/sources/reference/api/docker_remote_api_v1.16.md @@ -393,10 +393,8 @@ Get stdout and stderr logs from the container ``id`` **Example response**: - HTTP/1.1 101 UPGRADED + HTTP/1.1 200 OK Content-Type: application/vnd.docker.raw-stream - Connection: Upgrade - Upgrade: tcp {{ STREAM }} @@ -411,8 +409,7 @@ Query Parameters: Status Codes: -- **101** – no error, hints proxy about hijacking -- **200** – no error, no upgrade header found +- **200** – no error - **404** – no such container - **500** – server error @@ -647,10 +644,8 @@ Attach to the container `id` **Example response**: - HTTP/1.1 101 UPGRADED + HTTP/1.1 200 OK Content-Type: application/vnd.docker.raw-stream - Connection: Upgrade - Upgrade: tcp {{ STREAM }} @@ -668,8 +663,7 @@ Query Parameters: Status Codes: -- **101** – no error, hints proxy about hijacking -- **200** – no error, no upgrade header found +- **200** – no error - **400** – bad parameter - **404** – no such container - **500** – server error @@ -1752,18 +1746,7 @@ As an example, the `docker run` command line makes the following API calls: ## 3.2 Hijacking In this version of the API, /attach, uses hijacking to transport stdin, -stdout and stderr on the same socket. - -To hint potential proxies about connection hijacking, Docker client sends -connection upgrade headers similarly to websocket. - - Upgrade: tcp - Connection: Upgrade - -When Docker daemon detects the `Upgrade` header, it will switch its status code -from **200 OK** to **101 UPGRADED** and resend the same headers. - -This might change in the future. +stdout and stderr on the same socket. This might change in the future. ## 3.3 CORS Requests diff --git a/docs/sources/reference/api/docker_remote_api_v1.17.md b/docs/sources/reference/api/docker_remote_api_v1.17.md new file mode 100644 index 000000000..6c544c96b --- /dev/null +++ b/docs/sources/reference/api/docker_remote_api_v1.17.md @@ -0,0 +1,1773 @@ +page_title: Remote API v1.17 +page_description: API Documentation for Docker +page_keywords: API, Docker, rcli, REST, documentation + +# Docker Remote API v1.17 + +## 1. Brief introduction + + - The Remote API has replaced `rcli`. + - The daemon listens on `unix:///var/run/docker.sock` but you can + [Bind Docker to another host/port or a Unix socket]( + /articles/basics/#bind-docker-to-another-hostport-or-a-unix-socket). + - 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 + +`GET /containers/json` + +List containers + +**Example request**: + + GET /containers/json?all=1&before=8dfafdbc3a40&size=1 HTTP/1.1 + +**Example response**: + + 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 Parameters: + +- **all** – 1/True/true or 0/False/false, Show all containers. + Only running containers are shown by default (i.e., this defaults to false) +- **limit** – Show `limit` last created + containers, include non-running ones. +- **since** – Show only containers created since Id, include + non-running ones. +- **before** – Show only containers created before Id, include + non-running ones. +- **size** – 1/True/true or 0/False/false, Show the containers + sizes +- **filters** - a json encoded value of the filters (a map[string][]string) to process on the containers list. Available filters: + - exited=<int> -- containers with exit code of <int> + - status=(restarting|running|paused|exited) + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **500** – server error + +### Create a container + +`POST /containers/create` + +Create a container + +**Example request**: + + POST /containers/create HTTP/1.1 + Content-Type: application/json + + { + "Hostname":"", + "Domainname": "", + "User":"", + "Memory":0, + "MemorySwap":0, + "CpuShares": 512, + "Cpuset": "0,1", + "AttachStdin":false, + "AttachStdout":true, + "AttachStderr":true, + "Tty":false, + "OpenStdin":false, + "StdinOnce":false, + "Env":null, + "Cmd":[ + "date" + ], + "Entrypoint": "", + "Image":"base", + "Volumes":{ + "/tmp": {} + }, + "WorkingDir":"", + "NetworkDisabled": false, + "MacAddress":"12:34:56:78:9a:bc", + "ExposedPorts":{ + "22/tcp": {} + }, + "SecurityOpts": [""], + "HostConfig": { + "Binds":["/tmp:/tmp"], + "Links":["redis3:redis"], + "LxcConf":{"lxc.utsname":"docker"}, + "PortBindings":{ "22/tcp": [{ "HostPort": "11022" }] }, + "PublishAllPorts":false, + "Privileged":false, + "Dns": ["8.8.8.8"], + "DnsSearch": [""], + "VolumesFrom": ["parent", "other:ro"], + "CapAdd": ["NET_ADMIN"], + "CapDrop": ["MKNOD"], + "RestartPolicy": { "Name": "", "MaximumRetryCount": 0 }, + "NetworkMode": "bridge", + "Devices": [] + } + } + +**Example response**: + + HTTP/1.1 201 Created + Content-Type: application/json + + { + "Id":"e90e34656806" + "Warnings":[] + } + +Json Parameters: + +- **Hostname** - A string value containing the desired hostname to use for the + container. +- **Domainname** - A string value containing the desired domain name to use + for the container. +- **User** - A string value containg the user to use inside the container. +- **Memory** - Memory limit in bytes. +- **MemorySwap**- Total memory usage (memory + swap); set `-1` to disable swap. +- **CpuShares** - An integer value containing the CPU Shares for container + (ie. the relative weight vs othercontainers). + **CpuSet** - String value containg the cgroups Cpuset to use. +- **AttachStdin** - Boolean value, attaches to stdin. +- **AttachStdout** - Boolean value, attaches to stdout. +- **AttachStderr** - Boolean value, attaches to stderr. +- **Tty** - Boolean value, Attach standard streams to a tty, including stdin if it is not closed. +- **OpenStdin** - Boolean value, opens stdin, +- **StdinOnce** - Boolean value, close stdin after the 1 attached client disconnects. +- **Env** - A list of environment variables in the form of `VAR=value` +- **Cmd** - Command to run specified as a string or an array of strings. +- **Entrypoint** - Set the entrypoint for the container a a string or an array + of strings +- **Image** - String value containing the image name to use for the container +- **Volumes** – An object mapping mountpoint paths (strings) inside the + container to empty objects. +- **WorkingDir** - A string value containing the working dir for commands to + run in. +- **NetworkDisabled** - Boolean value, when true disables neworking for the + container +- **ExposedPorts** - An object mapping ports to an empty object in the form of: + `"ExposedPorts": { "/: {}" }` +- **SecurityOpts**: A list of string values to customize labels for MLS + systems, such as SELinux. +- **HostConfig** + - **Binds** – A list of volume bindings for this container. Each volume + binding is a string of the form `container_path` (to create a new + volume for the container), `host_path:container_path` (to bind-mount + a host path into the container), or `host_path:container_path:ro` + (to make the bind-mount read-only inside the container). + - **Links** - A list of links for the container. Each link entry should be of + of the form "container_name:alias". + - **LxcConf** - LXC specific configurations. These configurations will only + work when using the `lxc` execution driver. + - **PortBindings** - A map of exposed container ports and the host port they + should map to. It should be specified in the form + `{ /: [{ "HostPort": "" }] }` + Take note that `port` is specified as a string and not an integer value. + - **PublishAllPorts** - Allocates a random host port for all of a container's + exposed ports. Specified as a boolean value. + - **Privileged** - Gives the container full access to the host. Specified as + a boolean value. + - **Dns** - A list of dns servers for the container to use. + - **DnsSearch** - A list of DNS search domains + - **VolumesFrom** - A list of volumes to inherit from another container. + Specified in the form `[:]` + - **CapAdd** - A list of kernel capabilties to add to the container. + - **Capdrop** - A list of kernel capabilties to drop from the container. + - **RestartPolicy** – The behavior to apply when the container exits. The + value is an object with a `Name` property of either `"always"` to + always restart or `"on-failure"` to restart only when the container + exit code is non-zero. If `on-failure` is used, `MaximumRetryCount` + controls the number of times to retry before giving up. + The default is not to restart. (optional) + - **NetworkMode** - Sets the networking mode for the container. Supported + values are: `bridge`, `host`, and `container:` + - **Devices** - A list of devices to add to the container specified in the + form + `{ "PathOnHost": "/dev/deviceName", "PathInContainer": "/dev/deviceName", "CgroupPermissions": "mrw"}` + +Query Parameters: + +- **name** – Assign the specified name to the container. Must + match `/?[a-zA-Z0-9_-]+`. + +Status Codes: + +- **201** – no error +- **404** – no such container +- **406** – impossible to attach (container not running) +- **500** – server error + +### Inspect a container + +`GET /containers/(id)/json` + +Return low-level information on the container `id` + + +**Example request**: + + GET /containers/4fa6e0f0c678/json HTTP/1.1 + +**Example response**: + + 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/docker/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": ["/name:alias"], + "PublishAllPorts": false, + "CapAdd: ["NET_ADMIN"], + "CapDrop: ["MKNOD"] + } + } + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### List processes running inside a container + +`GET /containers/(id)/top` + +List processes running inside the container `id` + +**Example request**: + + GET /containers/4fa6e0f0c678/top HTTP/1.1 + +**Example response**: + + 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 Parameters: + +- **ps_args** – ps arguments to use (e.g., aux) + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Get container logs + +`GET /containers/(id)/logs` + +Get stdout and stderr logs from the container ``id`` + +**Example request**: + + GET /containers/4fa6e0f0c678/logs?stderr=1&stdout=1×tamps=1&follow=1&tail=10 HTTP/1.1 + +**Example response**: + + HTTP/1.1 101 UPGRADED + Content-Type: application/vnd.docker.raw-stream + Connection: Upgrade + Upgrade: tcp + + {{ STREAM }} + +Query Parameters: + +- **follow** – 1/True/true or 0/False/false, return stream. Default false +- **stdout** – 1/True/true or 0/False/false, show stdout log. Default false +- **stderr** – 1/True/true or 0/False/false, show stderr log. Default false +- **timestamps** – 1/True/true or 0/False/false, print timestamps for + every log line. Default false +- **tail** – Output specified number of lines at the end of logs: `all` or ``. Default all + +Status Codes: + +- **101** – no error, hints proxy about hijacking +- **200** – no error, no upgrade header found +- **404** – no such container +- **500** – server error + +### Inspect changes on a container's filesystem + +`GET /containers/(id)/changes` + +Inspect changes on container `id`'s filesystem + +**Example request**: + + GET /containers/4fa6e0f0c678/changes HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Path":"/dev", + "Kind":0 + }, + { + "Path":"/dev/kmsg", + "Kind":1 + }, + { + "Path":"/test", + "Kind":1 + } + ] + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Export a container + +`GET /containers/(id)/export` + +Export the contents of container `id` + +**Example request**: + + GET /containers/4fa6e0f0c678/export HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/octet-stream + + {{ TAR STREAM }} + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Resize a container TTY + +`POST /containers/(id)/resize?h=&w=` + +Resize the TTY for container with `id`. The container must be restarted for the resize to take effect. + +**Example request**: + + POST /containers/4fa6e0f0c678/resize?h=40&w=80 HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Length: 0 + Content-Type: text/plain; charset=utf-8 + +Status Codes: + +- **200** – no error +- **404** – No such container +- **500** – Cannot resize container + +### Start a container + +`POST /containers/(id)/start` + +Start the container `id` + +**Example request**: + + POST /containers/(id)/start HTTP/1.1 + Content-Type: application/json + +**Example response**: + + HTTP/1.1 204 No Content + +Json Parameters: + +Status Codes: + +- **204** – no error +- **304** – container already started +- **404** – no such container +- **500** – server error + +### Stop a container + +`POST /containers/(id)/stop` + +Stop the container `id` + +**Example request**: + + POST /containers/e90e34656806/stop?t=5 HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters: + +- **t** – number of seconds to wait before killing the container + +Status Codes: + +- **204** – no error +- **304** – container already stopped +- **404** – no such container +- **500** – server error + +### Restart a container + +`POST /containers/(id)/restart` + +Restart the container `id` + +**Example request**: + + POST /containers/e90e34656806/restart?t=5 HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters: + +- **t** – number of seconds to wait before killing the container + +Status Codes: + +- **204** – no error +- **404** – no such container +- **500** – server error + +### Kill a container + +`POST /containers/(id)/kill` + +Kill the container `id` + +**Example request**: + + POST /containers/e90e34656806/kill HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters + +- **signal** - Signal to send to the container: integer or string like "SIGINT". + When not set, SIGKILL is assumed and the call will waits for the container to exit. + +Status Codes: + +- **204** – no error +- **404** – no such container +- **500** – server error + +### Pause a container + +`POST /containers/(id)/pause` + +Pause the container `id` + +**Example request**: + + POST /containers/e90e34656806/pause HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Status Codes: + +- **204** – no error +- **404** – no such container +- **500** – server error + +### Unpause a container + +`POST /containers/(id)/unpause` + +Unpause the container `id` + +**Example request**: + + POST /containers/e90e34656806/unpause HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Status Codes: + +- **204** – no error +- **404** – no such container +- **500** – server error + +### Attach to a container + +`POST /containers/(id)/attach` + +Attach to the container `id` + +**Example request**: + + POST /containers/16253994b7c4/attach?logs=1&stream=0&stdout=1 HTTP/1.1 + +**Example response**: + + HTTP/1.1 101 UPGRADED + Content-Type: application/vnd.docker.raw-stream + Connection: Upgrade + Upgrade: tcp + + {{ STREAM }} + +Query Parameters: + +- **logs** – 1/True/true or 0/False/false, return logs. Default false +- **stream** – 1/True/true or 0/False/false, return stream. + Default false +- **stdin** – 1/True/true or 0/False/false, if stream=true, attach + to stdin. Default false +- **stdout** – 1/True/true or 0/False/false, if logs=true, return + stdout log, if stream=true, attach to stdout. Default false +- **stderr** – 1/True/true or 0/False/false, if logs=true, return + stderr log, if stream=true, attach to stderr. Default false + +Status Codes: + +- **101** – no error, hints proxy about hijacking +- **200** – no error, no upgrade header found +- **400** – bad parameter +- **404** – no such container +- **500** – server error + + **Stream details**: + + When using the TTY setting is enabled in + [`POST /containers/create` + ](/reference/api/docker_remote_api_v1.9/#create-a-container "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 written 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 + +`POST /containers/(id)/wait` + +Block until container `id` stops, then returns the exit code + +**Example request**: + + POST /containers/16253994b7c4/wait HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"StatusCode":0} + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +### Remove a container + +`DELETE /containers/(id)` + +Remove the container `id` from the filesystem + +**Example request**: + + DELETE /containers/16253994b7c4?v=1 HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters: + +- **v** – 1/True/true or 0/False/false, Remove the volumes + associated to the container. Default false +- **force** - 1/True/true or 0/False/false, Kill then remove the container. + Default false + +Status Codes: + +- **204** – no error +- **400** – bad parameter +- **404** – no such container +- **500** – server error + +### Copy files or folders from a container + +`POST /containers/(id)/copy` + +Copy files or folders of container `id` + +**Example request**: + + POST /containers/4fa6e0f0c678/copy HTTP/1.1 + Content-Type: application/json + + { + "Resource":"test.txt" + } + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/x-tar + + {{ TAR STREAM }} + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + +## 2.2 Images + +### List Images + +`GET /images/json` + +**Example request**: + + GET /images/json?all=0 HTTP/1.1 + +**Example response**: + + 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 + } + ] + + +Query Parameters: + +- **all** – 1/True/true or 0/False/false, default false +- **filters** – a json encoded value of the filters (a map[string][]string) to process on the images list. Available filters: + - dangling=true + +### Create an image + +`POST /images/create` + +Create an image, either by pulling it from the registry or by importing it + +**Example request**: + + POST /images/create?fromImage=base HTTP/1.1 + +**Example response**: + + 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 Parameters: + +- **fromImage** – name of the image to pull +- **fromSrc** – source to import. The value may be a URL from which the image + can be retrieved or `-` to read the image from the request body. +- **repo** – repository +- **tag** – tag +- **registry** – the registry to pull from + + Request Headers: + +- **X-Registry-Auth** – base64-encoded AuthConfig object + +Status Codes: + +- **200** – no error +- **500** – server error + + + +### Inspect an image + +`GET /images/(name)/json` + +Return low-level information on the image `name` + +**Example request**: + + GET /images/base/json HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Created":"2013-03-23T22:24:18.818426-07:00", + "Container":"3d67245a8d72ecf13f33dffac9f79dcdf70f75acb84d308770391510e0c23ad0", + "ContainerConfig": + { + "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":"" + }, + "Id":"b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", + "Parent":"27cf784147099545", + "Size": 6824592 + } + +Status Codes: + +- **200** – no error +- **404** – no such image +- **500** – server error + +### Get the history of an image + +`GET /images/(name)/history` + +Return the history of the image `name` + +**Example request**: + + GET /images/base/history HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Id":"b750fe79269d", + "Created":1364102658, + "CreatedBy":"/bin/bash" + }, + { + "Id":"27cf78414709", + "Created":1364068391, + "CreatedBy":"" + } + ] + +Status Codes: + +- **200** – no error +- **404** – no such image +- **500** – server error + +### Push an image on the registry + +`POST /images/(name)/push` + +Push the image `name` on the registry + +**Example request**: + + POST /images/test/push HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"status":"Pushing..."} + {"status":"Pushing", "progress":"1/? (n/a)", "progressDetail":{"current":1}}} + {"error":"Invalid..."} + ... + + If you wish to push an image on to a private registry, that image must already have been tagged + into a repository which references that registry host name and port. This repository name should + then be used in the URL. This mirrors the flow of the CLI. + +**Example request**: + + POST /images/registry.acme.com:5000/test/push HTTP/1.1 + + +Query Parameters: + +- **tag** – the tag to associate with the image on the registry, optional + +Request Headers: + +- **X-Registry-Auth** – include a base64-encoded AuthConfig + object. + +Status Codes: + +- **200** – no error +- **404** – no such image +- **500** – server error + +### Tag an image into a repository + +`POST /images/(name)/tag` + +Tag the image `name` into a repository + +**Example request**: + + POST /images/test/tag?repo=myrepo&force=0&tag=v42 HTTP/1.1 + +**Example response**: + + HTTP/1.1 201 OK + +Query Parameters: + +- **repo** – The repository to tag in +- **force** – 1/True/true or 0/False/false, default false +- **tag** - The new tag name + +Status Codes: + +- **201** – no error +- **400** – bad parameter +- **404** – no such image +- **409** – conflict +- **500** – server error + +### Remove an image + +`DELETE /images/(name)` + +Remove the image `name` from the filesystem + +**Example request**: + + DELETE /images/test HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-type: application/json + + [ + {"Untagged":"3e2f21a89f"}, + {"Deleted":"3e2f21a89f"}, + {"Deleted":"53b4f83ac9"} + ] + +Query Parameters: + +- **force** – 1/True/true or 0/False/false, default false +- **noprune** – 1/True/true or 0/False/false, default false + +Status Codes: + +- **200** – no error +- **404** – no such image +- **409** – conflict +- **500** – server error + +### Search images + +`GET /images/search` + +Search for an image on [Docker Hub](https://hub.docker.com). + +> **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**: + + GET /images/search?term=sshd HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "description": "", + "is_official": false, + "is_automated": false, + "name": "wma55/u1210sshd", + "star_count": 0 + }, + { + "description": "", + "is_official": false, + "is_automated": false, + "name": "jdswinbank/sshd", + "star_count": 0 + }, + { + "description": "", + "is_official": false, + "is_automated": false, + "name": "vgauthier/sshd", + "star_count": 0 + } + ... + ] + +Query Parameters: + +- **term** – term to search + +Status Codes: + +- **200** – no error +- **500** – server error + +## 2.3 Misc + +### Build an image from Dockerfile via stdin + +`POST /build` + +Build an image from Dockerfile via stdin + +**Example request**: + + POST /build HTTP/1.1 + + {{ TAR STREAM }} + +**Example response**: + + 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 [*ADD build + command*](/reference/builder/#dockerbuilder)). + +Query Parameters: + +- **t** – repository name (and optionally a tag) to be applied to + the resulting image in case of success +- **q** – suppress verbose build output +- **nocache** – do not use the cache when building the image +- **pull** - attempt to pull the image even if an older image exists locally +- **rm** - remove intermediate containers after a successful build (default behavior) +- **forcerm** - always remove intermediate containers (includes rm) + + Request Headers: + +- **Content-type** – should be set to `"application/tar"`. +- **X-Registry-Config** – base64-encoded ConfigFile objec + +Status Codes: + +- **200** – no error +- **500** – server error + +### Check auth configuration + +`POST /auth` + +Get the default username and email + +**Example request**: + + 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**: + + HTTP/1.1 200 OK + +Status Codes: + +- **200** – no error +- **204** – no error +- **500** – server error + +### Display system-wide information + +`GET /info` + +Display system-wide information + +**Example request**: + + GET /info HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Containers":11, + "Images":16, + "Driver":"btrfs", + "ExecutionDriver":"native-0.1", + "KernelVersion":"3.12.0-1-amd64" + "NCPU":1, + "MemTotal":2099236864, + "Name":"prod-server-42", + "ID":"7TRN:IPZB:QYBB:VPBQ:UMPP:KARE:6ZNR:XE6T:7EWV:PKF4:ZOJD:TPYS", + "Debug":false, + "NFd": 11, + "NGoroutines":21, + "NEventsListener":0, + "InitPath":"/usr/bin/docker", + "IndexServerAddress":["https://index.docker.io/v1/"], + "MemoryLimit":true, + "SwapLimit":false, + "IPv4Forwarding":true, + "Labels":["storage=ssd"] + } + +Status Codes: + +- **200** – no error +- **500** – server error + +### Show the docker version information + +`GET /version` + +Show the docker version information + +**Example request**: + + GET /version HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "ApiVersion":"1.12", + "Version":"0.2.2", + "GitCommit":"5a2a5cc+CHANGES", + "GoVersion":"go1.0.3" + } + +Status Codes: + +- **200** – no error +- **500** – server error + +### Ping the docker server + +`GET /_ping` + +Ping the docker server + +**Example request**: + + GET /_ping HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: text/plain + + OK + +Status Codes: + +- **200** - no error +- **500** - server error + +### Create a new image from a container's changes + +`POST /commit` + +Create a new image from a container's changes + +**Example request**: + + POST /commit?container=44c004db4b17&comment=message&repo=myrepo HTTP/1.1 + Content-Type: application/json + + { + "Hostname":"", + "Domainname": "", + "User":"", + "Memory":0, + "MemorySwap":0, + "CpuShares": 512, + "Cpuset": "0,1", + "AttachStdin":false, + "AttachStdout":true, + "AttachStderr":true, + "PortSpecs":null, + "Tty":false, + "OpenStdin":false, + "StdinOnce":false, + "Env":null, + "Cmd":[ + "date" + ], + "Volumes":{ + "/tmp": {} + }, + "WorkingDir":"", + "NetworkDisabled": false, + "ExposedPorts":{ + "22/tcp": {} + } + } + +**Example response**: + + HTTP/1.1 201 Created + Content-Type: application/vnd.docker.raw-stream + + {"Id":"596069db4bf5"} + +Json Parameters: + +- **config** - the container's configuration + +Query Parameters: + +- **container** – source container +- **repo** – repository +- **tag** – tag +- **comment** – commit message +- **author** – author (e.g., "John Hannibal Smith + <[hannibal@a-team.com](mailto:hannibal%40a-team.com)>") + +Status Codes: + +- **201** – no error +- **404** – no such container +- **500** – server error + +### Monitor Docker's events + +`GET /events` + +Get container events from docker, either in real time via streaming, or via +polling (using since). + +Docker containers will report the following events: + + create, destroy, die, export, kill, pause, restart, start, stop, unpause + +and Docker images will report: + + untag, delete + +**Example request**: + + GET /events?since=1374067924 + +**Example response**: + + 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 Parameters: + +- **since** – timestamp used for polling +- **until** – timestamp used for polling +- **filters** – a json encoded value of the filters (a map[string][]string) to process on the event list. Available filters: + - event=<string> -- event to filter + - image=<string> -- image to filter + - container=<string> -- container to filter + +Status Codes: + +- **200** – no error +- **500** – server error + +### Get a tarball containing all images in a repository + +`GET /images/(name)/get` + +Get a tarball containing all images and metadata for the repository specified +by `name`. + +If `name` is a specific name and tag (e.g. ubuntu:latest), then only that image +(and its parents) are returned. If `name` is an image ID, similarly only tha +image (and its parents) are returned, but with the exclusion of the +'repositories' file in the tarball, as there were no image names referenced. + +See the [image tarball format](#image-tarball-format) for more details. + +**Example request** + + GET /images/ubuntu/get + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/x-tar + + Binary data stream + +Status Codes: + +- **200** – no error +- **500** – server error + +### Get a tarball containing all images. + +`GET /images/get` + +Get a tarball containing all images and metadata for one or more repositories. + +For each value of the `names` parameter: if it is a specific name and tag (e.g. +ubuntu:latest), then only that image (and its parents) are returned; if it is +an image ID, similarly only that image (and its parents) are returned and there +would be no names referenced in the 'repositories' file for this image ID. + +See the [image tarball format](#image-tarball-format) for more details. + +**Example request** + + GET /images/get?names=myname%2Fmyapp%3Alatest&names=busybox + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/x-tar + + Binary data stream + +Status Codes: + +- **200** – no error +- **500** – server error + +### Load a tarball with a set of images and tags into docker + +`POST /images/load` + +Load a set of images and tags into the docker repository. +See the [image tarball format](#image-tarball-format) for more details. + +**Example request** + + POST /images/load + + Tarball in body + +**Example response**: + + HTTP/1.1 200 OK + +Status Codes: + +- **200** – no error +- **500** – server error + +### Image tarball format + +An image tarball contains one directory per image layer (named using its long ID), +each containing three files: + +1. `VERSION`: currently `1.0` - the file format version +2. `json`: detailed layer information, similar to `docker inspect layer_id` +3. `layer.tar`: A tarfile containing the filesystem changes in this layer + +The `layer.tar` file will contain `aufs` style `.wh..wh.aufs` files and directories +for storing attribute changes and deletions. + +If the tarball defines a repository, there will also be a `repositories` file at +the root that contains a list of repository and tag names mapped to layer IDs. + +``` +{"hello-world": + {"latest":"565a9d68a73f6706862bfe8409a7f659776d4d60a8d096eb4a3cbce6999cc2a1"} +} +``` + +### Exec Create + +`POST /containers/(id)/exec` + +Sets up an exec instance in a running container `id` + +**Example request**: + + POST /containers/e90e34656806/exec HTTP/1.1 + Content-Type: application/json + + { + "AttachStdin":false, + "AttachStdout":true, + "AttachStderr":true, + "Tty":false, + "Cmd":[ + "date" + ], + } + +**Example response**: + + HTTP/1.1 201 OK + Content-Type: application/json + + { + "Id":"f90e34656806" + } + +Json Parameters: + +- **AttachStdin** - Boolean value, attaches to stdin of the exec command. +- **AttachStdout** - Boolean value, attaches to stdout of the exec command. +- **AttachStderr** - Boolean value, attaches to stderr of the exec command. +- **Tty** - Boolean value to allocate a pseudo-TTY +- **Cmd** - Command to run specified as a string or an array of strings. + + +Status Codes: + +- **201** – no error +- **404** – no such container + +### Exec Start + +`POST /exec/(id)/start` + +Starts a previously set up exec instance `id`. If `detach` is true, this API +returns after starting the `exec` command. Otherwise, this API sets up an +interactive session with the `exec` command. + +**Example request**: + + POST /exec/e90e34656806/start HTTP/1.1 + Content-Type: application/json + + { + "Detach":false, + "Tty":false, + } + +**Example response**: + + HTTP/1.1 201 OK + Content-Type: application/json + + {{ STREAM }} + +Json Parameters: + +- **Detach** - Detach from the exec command +- **Tty** - Boolean value to allocate a pseudo-TTY + +Status Codes: + +- **201** – no error +- **404** – no such exec instance + + **Stream details**: + Similar to the stream behavior of `POST /container/(id)/attach` API + +### Exec Resize + +`POST /exec/(id)/resize` + +Resizes the tty session used by the exec command `id`. +This API is valid only if `tty` was specified as part of creating and starting the exec command. + +**Example request**: + + POST /exec/e90e34656806/resize HTTP/1.1 + Content-Type: plain/text + +**Example response**: + + HTTP/1.1 201 OK + Content-Type: plain/text + +Query Parameters: + +- **h** – height of tty session +- **w** – width + +Status Codes: + +- **201** – no error +- **404** – no such exec instance + +### Exec Inspect + +`GET /exec/(id)/json` + +Return low-level information about the exec command `id`. + +**Example request**: + + GET /exec/11fb006128e8ceb3942e7c58d77750f24210e35f879dd204ac975c184b820b39/json HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: plain/text + + { + "ID" : "11fb006128e8ceb3942e7c58d77750f24210e35f879dd204ac975c184b820b39", + "Running" : false, + "ExitCode" : 2, + "ProcessConfig" : { + "privileged" : false, + "user" : "", + "tty" : false, + "entrypoint" : "sh", + "arguments" : [ + "-c", + "exit 2" + ] + }, + "OpenStdin" : false, + "OpenStderr" : false, + "OpenStdout" : false, + "Container" : { + "State" : { + "Running" : true, + "Paused" : false, + "Restarting" : false, + "OOMKilled" : false, + "Pid" : 3650, + "ExitCode" : 0, + "Error" : "", + "StartedAt" : "2014-11-17T22:26:03.717657531Z", + "FinishedAt" : "0001-01-01T00:00:00Z" + }, + "ID" : "8f177a186b977fb451136e0fdf182abff5599a08b3c7f6ef0d36a55aaf89634c", + "Created" : "2014-11-17T22:26:03.626304998Z", + "Path" : "date", + "Args" : [], + "Config" : { + "Hostname" : "8f177a186b97", + "Domainname" : "", + "User" : "", + "Memory" : 0, + "MemorySwap" : 0, + "CpuShares" : 0, + "Cpuset" : "", + "AttachStdin" : false, + "AttachStdout" : false, + "AttachStderr" : false, + "PortSpecs" : null, + "ExposedPorts" : null, + "Tty" : false, + "OpenStdin" : false, + "StdinOnce" : false, + "Env" : [ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" ], + "Cmd" : [ + "date" + ], + "Image" : "ubuntu", + "Volumes" : null, + "WorkingDir" : "", + "Entrypoint" : null, + "NetworkDisabled" : false, + "MacAddress" : "", + "OnBuild" : null, + "SecurityOpt" : null + }, + "Image" : "5506de2b643be1e6febbf3b8a240760c6843244c41e12aa2f60ccbb7153d17f5", + "NetworkSettings" : { + "IPAddress" : "172.17.0.2", + "IPPrefixLen" : 16, + "MacAddress" : "02:42:ac:11:00:02", + "Gateway" : "172.17.42.1", + "Bridge" : "docker0", + "PortMapping" : null, + "Ports" : {} + }, + "ResolvConfPath" : "/var/lib/docker/containers/8f177a186b977fb451136e0fdf182abff5599a08b3c7f6ef0d36a55aaf89634c/resolv.conf", + "HostnamePath" : "/var/lib/docker/containers/8f177a186b977fb451136e0fdf182abff5599a08b3c7f6ef0d36a55aaf89634c/hostname", + "HostsPath" : "/var/lib/docker/containers/8f177a186b977fb451136e0fdf182abff5599a08b3c7f6ef0d36a55aaf89634c/hosts", + "Name" : "/test", + "Driver" : "aufs", + "ExecDriver" : "native-0.2", + "MountLabel" : "", + "ProcessLabel" : "", + "AppArmorProfile" : "", + "RestartCount" : 0, + "Volumes" : {}, + "VolumesRW" : {} + } + } + +Status Codes: + +- **200** – no error +- **404** – no such exec instance +- **500** - server error + +# 3. Going further + +## 3.1 Inside `docker run` + +As an example, the `docker run` command line makes the following API calls: + +- Create the container + +- If the status code is 404, it means the image doesn't exist: + - 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. + +To hint potential proxies about connection hijacking, Docker client sends +connection upgrade headers similarly to websocket. + + Upgrade: tcp + Connection: Upgrade + +When Docker daemon detects the `Upgrade` header, it will switch its status code +from **200 OK** to **101 UPGRADED** and resend the same headers. + +This might change in the future. + +## 3.3 CORS Requests + +To enable cross origin requests to the remote api add the flag +"--api-enable-cors" when running docker in daemon mode. + + $ docker -d -H="192.168.1.9:2375" --api-enable-cors From aa9c9569c29e3696c9427d2d66341c95fe076e4f Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 22 Dec 2014 09:59:08 +0100 Subject: [PATCH 098/513] Improve error message for conflicting container name. This changes the error message that is returned by the daemon when a container-name already exists. The old message suggests that containers can be renamed, which is currently not possible. To prevent confusion, the part "(or rename)" is removed from the error-message. Message before this change; FATA[0000] Error response from daemon: Conflict, The name foobar is already assigned to 728ac36fb0ab. You have to delete (or rename) that container to be able to assign foobar to a container again. Message after this change; FATA[0000] Error response from daemon: Conflict. The name 'foobar' is already in use by container 728ac36fb0ab. You have to delete that container to be able to reuse that name. Relates to: https://github.com/docker/docker/issues/3036 Signed-off-by: Sebastiaan van Stijn --- daemon/daemon.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/daemon/daemon.go b/daemon/daemon.go index 1553f198d..632b9abc4 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -478,8 +478,8 @@ func (daemon *Daemon) reserveName(id, name string) (string, error) { } else { nameAsKnownByUser := strings.TrimPrefix(name, "/") return "", fmt.Errorf( - "Conflict, The name %s is already assigned to %s. You have to delete (or rename) that container to be able to assign %s to a container again.", nameAsKnownByUser, - utils.TruncateID(conflictingContainer.ID), nameAsKnownByUser) + "Conflict. The name %q is already in use by container %s. You have to delete that container to be able to reuse that name.", nameAsKnownByUser, + utils.TruncateID(conflictingContainer.ID)) } } return name, nil From c1e04fbb150d71c243d505b79136108cb2f31f5c Mon Sep 17 00:00:00 2001 From: Pierre Wacrenier Date: Tue, 23 Dec 2014 00:05:41 +0100 Subject: [PATCH 099/513] Remove unused function from vfs storage driver Signed-off-by: Pierre Wacrenier --- daemon/graphdriver/vfs/driver.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/daemon/graphdriver/vfs/driver.go b/daemon/graphdriver/vfs/driver.go index aa104500b..0cffb1ffd 100644 --- a/daemon/graphdriver/vfs/driver.go +++ b/daemon/graphdriver/vfs/driver.go @@ -1,10 +1,8 @@ package vfs import ( - "bytes" "fmt" "os" - "os/exec" "path" "github.com/docker/docker/daemon/graphdriver" @@ -39,14 +37,6 @@ func (d *Driver) Cleanup() error { return nil } -func isGNUcoreutils() bool { - if stdout, err := exec.Command("cp", "--version").Output(); err == nil { - return bytes.Contains(stdout, []byte("GNU coreutils")) - } - - return false -} - func (d *Driver) Create(id, parent string) error { dir := d.dir(id) if err := os.MkdirAll(path.Dir(dir), 0700); err != nil { From 491ff81a0b6653d75b2bc4ca81251d7bf5ee9f3f Mon Sep 17 00:00:00 2001 From: panticz Date: Thu, 18 Dec 2014 11:24:59 +0100 Subject: [PATCH 100/513] Update Debian version to 7.7 Replaces #9724 fix by: panticz (github: panticz) Signed-off-by: Sven Dowideit --- docs/sources/installation/debian.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/installation/debian.md b/docs/sources/installation/debian.md index 85ac82b8d..fec1c8808 100644 --- a/docs/sources/installation/debian.md +++ b/docs/sources/installation/debian.md @@ -7,7 +7,7 @@ page_keywords: Docker, Docker documentation, installation, debian Docker is supported on the following versions of Debian: - [*Debian 8.0 Jessie (64-bit)*](#debian-jessie-80-64-bit) - - [*Debian 7.5 Wheezy (64-bit)*](#debian-wheezystable-7x-64-bit) + - [*Debian 7.7 Wheezy (64-bit)*](#debian-wheezystable-7x-64-bit) ## Debian Jessie 8.0 (64-bit) From 948f33263e95997a9c6596d695c92ab03c630899 Mon Sep 17 00:00:00 2001 From: Krasimir Georgiev Date: Fri, 5 Dec 2014 20:08:23 +0000 Subject: [PATCH 101/513] Changing the windows installation doc to include instructions how to login using putty instead of CMD Signed-off-by: Krasimir Georgiev Docker-DCO-1.1-Signed-off-by: Krasimir Georgiev (github: SvenDowideit) --- docs/sources/installation/windows.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/sources/installation/windows.md b/docs/sources/installation/windows.md index 667ce2935..04f1d28d1 100644 --- a/docs/sources/installation/windows.md +++ b/docs/sources/installation/windows.md @@ -57,6 +57,14 @@ Let's try the `hello-world` example image. Run This should download the very small `hello-world` image and print a `Hello from Docker.` message. +# Login with PUTTY instead of using the CMD + +boot2docker generates and uses the public/private key pair in your %HOMEPATH%\.ssh directory so to login you need to use the private key from this same directory. + +The private key needs to be converted into a format that putty can use so for this purpose you can use puttygen +Open puttygen.exe and load the private key from %HOMEPATH%\.ssh\id_boot2docker (File->Load) , +then simply click : Save Private Key. +You can use the saved file to login with putty using docker@127.0.0.1:2022 # Further Details From db165f7d2c3e05bbd7bac8c1188145370c7b1171 Mon Sep 17 00:00:00 2001 From: Sven Dowideit Date: Fri, 19 Dec 2014 16:43:34 +1000 Subject: [PATCH 102/513] fixes as requested Docker-DCO-1.1-Signed-off-by: Sven Dowideit (github: SvenDowideit) --- docs/sources/installation/windows.md | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/sources/installation/windows.md b/docs/sources/installation/windows.md index 04f1d28d1..90268867b 100644 --- a/docs/sources/installation/windows.md +++ b/docs/sources/installation/windows.md @@ -57,14 +57,20 @@ Let's try the `hello-world` example image. Run This should download the very small `hello-world` image and print a `Hello from Docker.` message. -# Login with PUTTY instead of using the CMD +## Login with PUTTY instead of using the CMD -boot2docker generates and uses the public/private key pair in your %HOMEPATH%\.ssh directory so to login you need to use the private key from this same directory. +Boot2Docker generates and uses the public/private key pair in your `%HOMEPATH%\.ssh` +directory so to log in you need to use the private key from this same directory. -The private key needs to be converted into a format that putty can use so for this purpose you can use puttygen -Open puttygen.exe and load the private key from %HOMEPATH%\.ssh\id_boot2docker (File->Load) , -then simply click : Save Private Key. -You can use the saved file to login with putty using docker@127.0.0.1:2022 +The private key needs to be converted into the format PuTTY uses. + +You can do this with +[puttygen](http://www.chiark.greenend.org.uk/~sgtatham/putty/download.html): + +- Open `puttygen.exe` and load ("File"->"Load" menu) the private key from + `%HOMEPATH%\.ssh\id_boot2docker` +- then click: "Save Private Key". +- Then use the saved file to login with PuTTY using `docker@127.0.0.1:2022`. # Further Details From 973c3c768be19b80543631ad9515fe05b95b2d22 Mon Sep 17 00:00:00 2001 From: panticz Date: Thu, 18 Dec 2014 11:22:08 +0100 Subject: [PATCH 103/513] Debian Wheezy backports kernel updated to 3.16 Replaces #9723 fix by: panticz (github: panticz) Signed-off-by: Sven Dowideit --- docs/sources/installation/debian.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sources/installation/debian.md b/docs/sources/installation/debian.md index 85ac82b8d..826626ccc 100644 --- a/docs/sources/installation/debian.md +++ b/docs/sources/installation/debian.md @@ -41,13 +41,13 @@ Docker requires Kernel 3.8+, while Wheezy ships with Kernel 3.2 (for more detail on why 3.8 is required, see discussion on [bug #407](https://github.com/docker/docker/issues/407%20kernel%20versions)). -Fortunately, wheezy-backports currently has [Kernel 3.14 +Fortunately, wheezy-backports currently has [Kernel 3.16 ](https://packages.debian.org/search?suite=wheezy-backports§ion=all&arch=any&searchon=names&keywords=linux-image-amd64), which is officially supported by Docker. ### Installation -1. Install Kernel 3.14 from wheezy-backports +1. Install Kernel from wheezy-backports Add the following line to your `/etc/apt/sources.list` From 4fd2a9156cafba7d0747d7a049eda12aac86daf8 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Tue, 23 Dec 2014 11:01:19 -0800 Subject: [PATCH 104/513] api/server: commenting proppy from maintainer Signed-off-by: Johan Euphrosine --- api/server/MAINTAINERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/server/MAINTAINERS b/api/server/MAINTAINERS index c92a06114..dee1eec04 100644 --- a/api/server/MAINTAINERS +++ b/api/server/MAINTAINERS @@ -1,2 +1,2 @@ Victor Vieux (@vieux) -Johan Euphrosine (@proppy) +# Johan Euphrosine (@proppy) From 50905a6d6ce2fdd1ab0c33ec0b7a26895e0cbeea Mon Sep 17 00:00:00 2001 From: Alexander Morozov Date: Tue, 23 Dec 2014 12:10:03 -0800 Subject: [PATCH 105/513] Update libcontainer to 1597c68f7b941fd97881155d7f077852e2914e7b This commit contains changes for docker: * user.GetGroupFile to user.GetGroupPath docker/libcontainer#301 * Add systemd support for OOM docker/libcontainer#307 * Support for custom namespaces docker/libcontainer#279, docker/libcontainer#312 * Fixes #9699 docker/libcontainer#308 Signed-off-by: Alexander Morozov --- api/server/server.go | 2 +- daemon/execdriver/native/create.go | 11 +- daemon/execdriver/native/driver.go | 15 +- .../native/template/default_template.go | 12 +- project/vendor.sh | 2 +- .../github.com/docker/libcontainer/Makefile | 6 +- .../github.com/docker/libcontainer/SPEC.md | 25 ++++ .../docker/libcontainer/cgroups/cgroups.go | 1 + .../docker/libcontainer/cgroups/fs/cpuset.go | 17 ++- .../docker/libcontainer/cgroups/fs/memory.go | 9 +- .../cgroups/fs/stats_util_test.go | 4 + .../cgroups/systemd/apply_systemd.go | 2 +- .../docker/libcontainer/cgroups/utils.go | 30 +++- .../github.com/docker/libcontainer/config.go | 56 ++++++- .../docker/libcontainer/config_test.go | 4 +- .../libcontainer/integration/exec_test.go | 11 +- .../libcontainer/integration/execin_test.go | 140 ++++++++++++++++++ .../libcontainer/integration/init_test.go | 67 +++++++-- .../libcontainer/integration/template_test.go | 12 +- .../github.com/docker/libcontainer/ipc/ipc.go | 29 ---- .../docker/libcontainer/namespaces/execin.go | 4 + .../docker/libcontainer/namespaces/init.go | 35 ++++- .../libcontainer/namespaces/nsenter/nsenter.c | 17 ++- .../docker/libcontainer/namespaces/types.go | 50 ------- .../libcontainer/namespaces/types_linux.go | 16 -- .../libcontainer/namespaces/types_test.go | 30 ---- .../docker/libcontainer/namespaces/utils.go | 21 ++- .../libcontainer/netlink/netlink_linux.go | 5 +- .../docker/libcontainer/network/netns.go | 39 ----- .../docker/libcontainer/network/network.go | 12 ++ .../docker/libcontainer/network/strategy.go | 1 - .../docker/libcontainer/network/types.go | 5 - .../{cgroups/fs => }/notify_linux.go | 52 ++----- .../{cgroups/fs => }/notify_linux_test.go | 42 ++++-- .../libcontainer/sample_configs/apparmor.json | 14 +- .../sample_configs/attach_to_bridge.json | 14 +- .../libcontainer/sample_configs/minimal.json | 14 +- .../route_source_address_selection.json | 14 +- .../libcontainer/sample_configs/selinux.json | 14 +- .../docker/libcontainer/user/MAINTAINERS | 1 + .../docker/libcontainer/user/lookup_unix.go | 16 +- .../libcontainer/user/lookup_unsupported.go | 4 +- .../docker/libcontainer/user/user.go | 4 +- 43 files changed, 517 insertions(+), 362 deletions(-) create mode 100644 vendor/src/github.com/docker/libcontainer/integration/execin_test.go delete mode 100644 vendor/src/github.com/docker/libcontainer/ipc/ipc.go delete mode 100644 vendor/src/github.com/docker/libcontainer/namespaces/types.go delete mode 100644 vendor/src/github.com/docker/libcontainer/namespaces/types_linux.go delete mode 100644 vendor/src/github.com/docker/libcontainer/namespaces/types_test.go delete mode 100644 vendor/src/github.com/docker/libcontainer/network/netns.go rename vendor/src/github.com/docker/libcontainer/{cgroups/fs => }/notify_linux.go (54%) rename vendor/src/github.com/docker/libcontainer/{cgroups/fs => }/notify_linux_test.go (67%) diff --git a/api/server/server.go b/api/server/server.go index f1bfcb6ac..6b15962b2 100644 --- a/api/server/server.go +++ b/api/server/server.go @@ -1399,7 +1399,7 @@ func serveFd(addr string, job *engine.Job) error { } func lookupGidByName(nameOrGid string) (int, error) { - groupFile, err := user.GetGroupFile() + groupFile, err := user.GetGroupPath() if err != nil { return -1, err } diff --git a/daemon/execdriver/native/create.go b/daemon/execdriver/native/create.go index de103eca8..188f78ec0 100644 --- a/daemon/execdriver/native/create.go +++ b/daemon/execdriver/native/create.go @@ -82,7 +82,7 @@ func (d *driver) createContainer(c *execdriver.Command) (*libcontainer.Config, e func (d *driver) createNetwork(container *libcontainer.Config, c *execdriver.Command) error { if c.Network.HostNetworking { - container.Namespaces["NEWNET"] = false + container.Namespaces.Remove(libcontainer.NEWNET) return nil } @@ -119,10 +119,7 @@ func (d *driver) createNetwork(container *libcontainer.Config, c *execdriver.Com cmd := active.cmd nspath := filepath.Join("/proc", fmt.Sprint(cmd.Process.Pid), "ns", "net") - container.Networks = append(container.Networks, &libcontainer.Network{ - Type: "netns", - NsPath: nspath, - }) + container.Namespaces.Add(libcontainer.NEWNET, nspath) } return nil @@ -130,7 +127,7 @@ func (d *driver) createNetwork(container *libcontainer.Config, c *execdriver.Com func (d *driver) createIpc(container *libcontainer.Config, c *execdriver.Command) error { if c.Ipc.HostIpc { - container.Namespaces["NEWIPC"] = false + container.Namespaces.Remove(libcontainer.NEWIPC) return nil } @@ -144,7 +141,7 @@ func (d *driver) createIpc(container *libcontainer.Config, c *execdriver.Command } cmd := active.cmd - container.IpcNsPath = filepath.Join("/proc", fmt.Sprint(cmd.Process.Pid), "ns", "ipc") + container.Namespaces.Add(libcontainer.NEWIPC, filepath.Join("/proc", fmt.Sprint(cmd.Process.Pid), "ns", "ipc")) } return nil diff --git a/daemon/execdriver/native/driver.go b/daemon/execdriver/native/driver.go index a036abc21..f6099bd04 100644 --- a/daemon/execdriver/native/driver.go +++ b/daemon/execdriver/native/driver.go @@ -61,10 +61,6 @@ func NewDriver(root, initPath string) (*driver, error) { }, nil } -func (d *driver) notifyOnOOM(config *libcontainer.Config) (<-chan struct{}, error) { - return fs.NotifyOnOOM(config.Cgroups) -} - type execOutput struct { exitCode int err error @@ -152,11 +148,16 @@ func (d *driver) Run(c *execdriver.Command, pipes *execdriver.Pipes, startCallba } oomKill := false - oomKillNotification, err := d.notifyOnOOM(container) + state, err := libcontainer.GetState(filepath.Join(d.root, c.ID)) if err == nil { - _, oomKill = <-oomKillNotification + oomKillNotification, err := libcontainer.NotifyOnOOM(state) + if err == nil { + _, oomKill = <-oomKillNotification + } else { + log.Warnf("WARNING: Your kernel does not support OOM notifications: %s", err) + } } else { - log.Warnf("WARNING: Your kernel does not support OOM notifications: %s", err) + log.Warnf("Failed to get container state, oom notify will not work: %s", err) } // wait for the container to exit. execOutput := <-execOutputChan diff --git a/daemon/execdriver/native/template/default_template.go b/daemon/execdriver/native/template/default_template.go index be3dd5a5c..6aa213d6b 100644 --- a/daemon/execdriver/native/template/default_template.go +++ b/daemon/execdriver/native/template/default_template.go @@ -25,12 +25,12 @@ func New() *libcontainer.Config { "KILL", "AUDIT_WRITE", }, - Namespaces: map[string]bool{ - "NEWNS": true, - "NEWUTS": true, - "NEWIPC": true, - "NEWPID": true, - "NEWNET": true, + Namespaces: libcontainer.Namespaces{ + {Type: "NEWNS"}, + {Type: "NEWUTS"}, + {Type: "NEWIPC"}, + {Type: "NEWPID"}, + {Type: "NEWNET"}, }, Cgroups: &cgroups.Cgroup{ Parent: "docker", diff --git a/project/vendor.sh b/project/vendor.sh index 0b56cb1b6..2c84e45ee 100755 --- a/project/vendor.sh +++ b/project/vendor.sh @@ -66,7 +66,7 @@ if [ "$1" = '--go' ]; then mv tmp-tar src/code.google.com/p/go/src/pkg/archive/tar fi -clone git github.com/docker/libcontainer 53eca435e63db58b06cf796d3a9326db5fd42253 +clone git github.com/docker/libcontainer 1597c68f7b941fd97881155d7f077852e2914e7b # see src/github.com/docker/libcontainer/update-vendor.sh which is the "source of truth" for libcontainer deps (just like this file) rm -rf src/github.com/docker/libcontainer/vendor eval "$(grep '^clone ' src/github.com/docker/libcontainer/update-vendor.sh | grep -v 'github.com/codegangsta/cli')" diff --git a/vendor/src/github.com/docker/libcontainer/Makefile b/vendor/src/github.com/docker/libcontainer/Makefile index 0c4dda7c9..f94171b0f 100644 --- a/vendor/src/github.com/docker/libcontainer/Makefile +++ b/vendor/src/github.com/docker/libcontainer/Makefile @@ -1,13 +1,13 @@ all: - docker build -t docker/libcontainer . + docker build -t dockercore/libcontainer . test: # we need NET_ADMIN for the netlink tests and SYS_ADMIN for mounting - docker run --rm -it --privileged docker/libcontainer + docker run --rm -it --privileged dockercore/libcontainer sh: - docker run --rm -it --privileged -w /busybox docker/libcontainer nsinit exec sh + docker run --rm -it --privileged -w /busybox dockercore/libcontainer nsinit exec sh GO_PACKAGES = $(shell find . -not \( -wholename ./vendor -prune -o -wholename ./.git -prune \) -name '*.go' -print0 | xargs -0n1 dirname | sort -u) diff --git a/vendor/src/github.com/docker/libcontainer/SPEC.md b/vendor/src/github.com/docker/libcontainer/SPEC.md index f5afaadc5..d83d758dd 100644 --- a/vendor/src/github.com/docker/libcontainer/SPEC.md +++ b/vendor/src/github.com/docker/libcontainer/SPEC.md @@ -318,4 +318,29 @@ a container. | Resume | Resume all processes inside the container if paused | | Exec | Execute a new process inside of the container ( requires setns ) | +### Execute a new process inside of a running container. +User can execute a new process inside of a running container. Any binaries to be +executed must be accessible within the container's rootfs. + +The started process will run inside the container's rootfs. Any changes +made by the process to the container's filesystem will persist after the +process finished executing. + +The started process will join all the container's existing namespaces. When the +container is paused, the process will also be paused and will resume when +the container is unpaused. The started process will only run when the container's +primary process (PID 1) is running, and will not be restarted when the container +is restarted. + +#### Planned additions + +The started process will have its own cgroups nested inside the container's +cgroups. This is used for process tracking and optionally resource allocation +handling for the new process. Freezer cgroup is required, the rest of the cgroups +are optional. The process executor must place its pid inside the correct +cgroups before starting the process. This is done so that no child processes or +threads can escape the cgroups. + +When the process is stopped, the process executor will try (in a best-effort way) +to stop all its children and remove the sub-cgroups. diff --git a/vendor/src/github.com/docker/libcontainer/cgroups/cgroups.go b/vendor/src/github.com/docker/libcontainer/cgroups/cgroups.go index fe3600597..106698d18 100644 --- a/vendor/src/github.com/docker/libcontainer/cgroups/cgroups.go +++ b/vendor/src/github.com/docker/libcontainer/cgroups/cgroups.go @@ -50,6 +50,7 @@ type Cgroup struct { CpuQuota int64 `json:"cpu_quota,omitempty"` // CPU hardcap limit (in usecs). Allowed cpu time in a given period. CpuPeriod int64 `json:"cpu_period,omitempty"` // CPU period to be used for hardcapping (in usecs). 0 to use system default. CpusetCpus string `json:"cpuset_cpus,omitempty"` // CPU to use + CpusetMems string `json:"cpuset_mems,omitempty"` // MEM to use Freezer FreezerState `json:"freezer,omitempty"` // set the freeze value for the process Slice string `json:"slice,omitempty"` // Parent slice to use for systemd } diff --git a/vendor/src/github.com/docker/libcontainer/cgroups/fs/cpuset.go b/vendor/src/github.com/docker/libcontainer/cgroups/fs/cpuset.go index 54d2ed572..ff67a53e8 100644 --- a/vendor/src/github.com/docker/libcontainer/cgroups/fs/cpuset.go +++ b/vendor/src/github.com/docker/libcontainer/cgroups/fs/cpuset.go @@ -18,7 +18,7 @@ func (s *CpusetGroup) Set(d *data) error { if err != nil { return err } - return s.SetDir(dir, d.c.CpusetCpus, d.pid) + return s.SetDir(dir, d.c.CpusetCpus, d.c.CpusetMems, d.pid) } func (s *CpusetGroup) Remove(d *data) error { @@ -29,7 +29,7 @@ func (s *CpusetGroup) GetStats(path string, stats *cgroups.Stats) error { return nil } -func (s *CpusetGroup) SetDir(dir, value string, pid int) error { +func (s *CpusetGroup) SetDir(dir, cpus string, mems string, pid int) error { if err := s.ensureParent(dir); err != nil { return err } @@ -40,10 +40,15 @@ func (s *CpusetGroup) SetDir(dir, value string, pid int) error { return err } - // If we don't use --cpuset, the default cpuset.cpus is set in - // s.ensureParent, otherwise, use the value we set - if value != "" { - if err := writeFile(dir, "cpuset.cpus", value); err != nil { + // If we don't use --cpuset-xxx, the default value inherit from parent cgroup + // is set in s.ensureParent, otherwise, use the value we set + if cpus != "" { + if err := writeFile(dir, "cpuset.cpus", cpus); err != nil { + return err + } + } + if mems != "" { + if err := writeFile(dir, "cpuset.mems", mems); err != nil { return err } } diff --git a/vendor/src/github.com/docker/libcontainer/cgroups/fs/memory.go b/vendor/src/github.com/docker/libcontainer/cgroups/fs/memory.go index 3f9647c2f..01713fd79 100644 --- a/vendor/src/github.com/docker/libcontainer/cgroups/fs/memory.go +++ b/vendor/src/github.com/docker/libcontainer/cgroups/fs/memory.go @@ -38,12 +38,17 @@ func (s *MemoryGroup) Set(d *data) error { } } // By default, MemorySwap is set to twice the size of RAM. - // If you want to omit MemorySwap, set it to `-1'. - if d.c.MemorySwap != -1 { + // If you want to omit MemorySwap, set it to '-1'. + if d.c.MemorySwap == 0 { if err := writeFile(dir, "memory.memsw.limit_in_bytes", strconv.FormatInt(d.c.Memory*2, 10)); err != nil { return err } } + if d.c.MemorySwap > 0 { + if err := writeFile(dir, "memory.memsw.limit_in_bytes", strconv.FormatInt(d.c.MemorySwap, 10)); err != nil { + return err + } + } } return nil } diff --git a/vendor/src/github.com/docker/libcontainer/cgroups/fs/stats_util_test.go b/vendor/src/github.com/docker/libcontainer/cgroups/fs/stats_util_test.go index 1a9e590f5..6e237c040 100644 --- a/vendor/src/github.com/docker/libcontainer/cgroups/fs/stats_util_test.go +++ b/vendor/src/github.com/docker/libcontainer/cgroups/fs/stats_util_test.go @@ -90,4 +90,8 @@ func expectMemoryStatEquals(t *testing.T, expected, actual cgroups.MemoryStats) t.Fail() } } + if expected.Failcnt != actual.Failcnt { + log.Printf("Expected memory failcnt %d but found %d\n", expected.Failcnt, actual.Failcnt) + t.Fail() + } } diff --git a/vendor/src/github.com/docker/libcontainer/cgroups/systemd/apply_systemd.go b/vendor/src/github.com/docker/libcontainer/cgroups/systemd/apply_systemd.go index 3d8981143..41dce3117 100644 --- a/vendor/src/github.com/docker/libcontainer/cgroups/systemd/apply_systemd.go +++ b/vendor/src/github.com/docker/libcontainer/cgroups/systemd/apply_systemd.go @@ -313,5 +313,5 @@ func joinCpuset(c *cgroups.Cgroup, pid int) error { s := &fs.CpusetGroup{} - return s.SetDir(path, c.CpusetCpus, pid) + return s.SetDir(path, c.CpusetCpus, c.CpusetMems, pid) } diff --git a/vendor/src/github.com/docker/libcontainer/cgroups/utils.go b/vendor/src/github.com/docker/libcontainer/cgroups/utils.go index 224a20b9b..5753ca453 100644 --- a/vendor/src/github.com/docker/libcontainer/cgroups/utils.go +++ b/vendor/src/github.com/docker/libcontainer/cgroups/utils.go @@ -9,6 +9,7 @@ import ( "path/filepath" "strconv" "strings" + "time" "github.com/docker/docker/pkg/mount" ) @@ -193,13 +194,30 @@ func EnterPid(cgroupPaths map[string]string, pid int) error { } // RemovePaths iterates over the provided paths removing them. -// If an error is encountered the removal proceeds and the first error is -// returned to ensure a partial removal is not possible. +// We trying to remove all paths five times with increasing delay between tries. +// If after all there are not removed cgroups - appropriate error will be +// returned. func RemovePaths(paths map[string]string) (err error) { - for _, path := range paths { - if rerr := os.RemoveAll(path); err == nil { - err = rerr + delay := 10 * time.Millisecond + for i := 0; i < 5; i++ { + if i != 0 { + time.Sleep(delay) + delay *= 2 + } + for s, p := range paths { + os.RemoveAll(p) + // TODO: here probably should be logging + _, err := os.Stat(p) + // We need this strange way of checking cgroups existence because + // RemoveAll almost always returns error, even on already removed + // cgroups + if os.IsNotExist(err) { + delete(paths, s) + } + } + if len(paths) == 0 { + return nil } } - return err + return fmt.Errorf("Failed to remove paths: %s", paths) } diff --git a/vendor/src/github.com/docker/libcontainer/config.go b/vendor/src/github.com/docker/libcontainer/config.go index 915e00660..7f1bcc962 100644 --- a/vendor/src/github.com/docker/libcontainer/config.go +++ b/vendor/src/github.com/docker/libcontainer/config.go @@ -10,6 +10,57 @@ type MountConfig mount.MountConfig type Network network.Network +type NamespaceType string + +const ( + NEWNET NamespaceType = "NEWNET" + NEWPID NamespaceType = "NEWPID" + NEWNS NamespaceType = "NEWNS" + NEWUTS NamespaceType = "NEWUTS" + NEWIPC NamespaceType = "NEWIPC" + NEWUSER NamespaceType = "NEWUSER" +) + +// Namespace defines configuration for each namespace. It specifies an +// alternate path that is able to be joined via setns. +type Namespace struct { + Type NamespaceType `json:"type"` + Path string `json:"path,omitempty"` +} + +type Namespaces []Namespace + +func (n Namespaces) Remove(t NamespaceType) bool { + i := n.index(t) + if i == -1 { + return false + } + n = append(n[:i], n[i+1:]...) + return true +} + +func (n Namespaces) Add(t NamespaceType, path string) { + i := n.index(t) + if i == -1 { + n = append(n, Namespace{Type: t, Path: path}) + return + } + n[i].Path = path +} + +func (n Namespaces) index(t NamespaceType) int { + for i, ns := range n { + if ns.Type == t { + return i + } + } + return -1 +} + +func (n Namespaces) Contains(t NamespaceType) bool { + return n.index(t) != -1 +} + // Config defines configuration options for executing a process inside a contained environment. type Config struct { // Mount specific options. @@ -38,7 +89,7 @@ type Config struct { // Namespaces specifies the container's namespaces that it should setup when cloning the init process // If a namespace is not provided that namespace is shared from the container's parent process - Namespaces map[string]bool `json:"namespaces,omitempty"` + Namespaces Namespaces `json:"namespaces,omitempty"` // Capabilities specify the capabilities to keep when executing the process inside the container // All capbilities not specified will be dropped from the processes capability mask @@ -47,9 +98,6 @@ type Config struct { // Networks specifies the container's network setup to be created Networks []*Network `json:"networks,omitempty"` - // Ipc specifies the container's ipc setup to be created - IpcNsPath string `json:"ipc,omitempty"` - // Routes can be specified to create entries in the route table as the container is started Routes []*Route `json:"routes,omitempty"` diff --git a/vendor/src/github.com/docker/libcontainer/config_test.go b/vendor/src/github.com/docker/libcontainer/config_test.go index 598128115..5c73f84af 100644 --- a/vendor/src/github.com/docker/libcontainer/config_test.go +++ b/vendor/src/github.com/docker/libcontainer/config_test.go @@ -64,12 +64,12 @@ func TestConfigJsonFormat(t *testing.T) { t.Fail() } - if !container.Namespaces["NEWNET"] { + if !container.Namespaces.Contains(NEWNET) { t.Log("namespaces should contain NEWNET") t.Fail() } - if container.Namespaces["NEWUSER"] { + if container.Namespaces.Contains(NEWUSER) { t.Log("namespaces should not contain NEWUSER") t.Fail() } diff --git a/vendor/src/github.com/docker/libcontainer/integration/exec_test.go b/vendor/src/github.com/docker/libcontainer/integration/exec_test.go index 8f4dae0f9..f0728c581 100644 --- a/vendor/src/github.com/docker/libcontainer/integration/exec_test.go +++ b/vendor/src/github.com/docker/libcontainer/integration/exec_test.go @@ -4,6 +4,8 @@ import ( "os" "strings" "testing" + + "github.com/docker/libcontainer" ) func TestExecPS(t *testing.T) { @@ -55,7 +57,6 @@ func TestIPCPrivate(t *testing.T) { } config := newTemplateConfig(rootfs) - config.Namespaces["NEWIPC"] = true buffers, exitCode, err := runContainer(config, "", "readlink", "/proc/self/ns/ipc") if err != nil { t.Fatal(err) @@ -87,7 +88,7 @@ func TestIPCHost(t *testing.T) { } config := newTemplateConfig(rootfs) - config.Namespaces["NEWIPC"] = false + config.Namespaces.Remove(libcontainer.NEWIPC) buffers, exitCode, err := runContainer(config, "", "readlink", "/proc/self/ns/ipc") if err != nil { t.Fatal(err) @@ -119,8 +120,7 @@ func TestIPCJoinPath(t *testing.T) { } config := newTemplateConfig(rootfs) - config.Namespaces["NEWIPC"] = false - config.IpcNsPath = "/proc/1/ns/ipc" + config.Namespaces.Add(libcontainer.NEWIPC, "/proc/1/ns/ipc") buffers, exitCode, err := runContainer(config, "", "readlink", "/proc/self/ns/ipc") if err != nil { @@ -148,8 +148,7 @@ func TestIPCBadPath(t *testing.T) { defer remove(rootfs) config := newTemplateConfig(rootfs) - config.Namespaces["NEWIPC"] = false - config.IpcNsPath = "/proc/1/ns/ipcc" + config.Namespaces.Add(libcontainer.NEWIPC, "/proc/1/ns/ipcc") _, _, err = runContainer(config, "", "true") if err == nil { diff --git a/vendor/src/github.com/docker/libcontainer/integration/execin_test.go b/vendor/src/github.com/docker/libcontainer/integration/execin_test.go new file mode 100644 index 000000000..86d9c5c26 --- /dev/null +++ b/vendor/src/github.com/docker/libcontainer/integration/execin_test.go @@ -0,0 +1,140 @@ +package integration + +import ( + "os" + "os/exec" + "strings" + "sync" + "testing" + + "github.com/docker/libcontainer" + "github.com/docker/libcontainer/namespaces" +) + +func TestExecIn(t *testing.T) { + if testing.Short() { + return + } + + rootfs, err := newRootFs() + if err != nil { + t.Fatal(err) + } + defer remove(rootfs) + + config := newTemplateConfig(rootfs) + if err := writeConfig(config); err != nil { + t.Fatalf("failed to write config %s", err) + } + + containerCmd, statePath, containerErr := startLongRunningContainer(config) + defer func() { + // kill the container + if containerCmd.Process != nil { + containerCmd.Process.Kill() + } + if err := <-containerErr; err != nil { + t.Fatal(err) + } + }() + + // start the exec process + state, err := libcontainer.GetState(statePath) + if err != nil { + t.Fatalf("failed to get state %s", err) + } + buffers := newStdBuffers() + execErr := make(chan error, 1) + go func() { + _, err := namespaces.ExecIn(config, state, []string{"ps"}, + os.Args[0], "exec", buffers.Stdin, buffers.Stdout, buffers.Stderr, + "", nil) + execErr <- err + }() + if err := <-execErr; err != nil { + t.Fatalf("exec finished with error %s", err) + } + + out := buffers.Stdout.String() + if !strings.Contains(out, "sleep 10") || !strings.Contains(out, "ps") { + t.Fatalf("unexpected running process, output %q", out) + } +} + +func TestExecInRlimit(t *testing.T) { + if testing.Short() { + return + } + + rootfs, err := newRootFs() + if err != nil { + t.Fatal(err) + } + defer remove(rootfs) + + config := newTemplateConfig(rootfs) + if err := writeConfig(config); err != nil { + t.Fatalf("failed to write config %s", err) + } + + containerCmd, statePath, containerErr := startLongRunningContainer(config) + defer func() { + // kill the container + if containerCmd.Process != nil { + containerCmd.Process.Kill() + } + if err := <-containerErr; err != nil { + t.Fatal(err) + } + }() + + // start the exec process + state, err := libcontainer.GetState(statePath) + if err != nil { + t.Fatalf("failed to get state %s", err) + } + buffers := newStdBuffers() + execErr := make(chan error, 1) + go func() { + _, err := namespaces.ExecIn(config, state, []string{"/bin/sh", "-c", "ulimit -n"}, + os.Args[0], "exec", buffers.Stdin, buffers.Stdout, buffers.Stderr, + "", nil) + execErr <- err + }() + if err := <-execErr; err != nil { + t.Fatalf("exec finished with error %s", err) + } + + out := buffers.Stdout.String() + if limit := strings.TrimSpace(out); limit != "1024" { + t.Fatalf("expected rlimit to be 1024, got %s", limit) + } +} + +// start a long-running container so we have time to inspect execin processes +func startLongRunningContainer(config *libcontainer.Config) (*exec.Cmd, string, chan error) { + containerErr := make(chan error, 1) + containerCmd := &exec.Cmd{} + var statePath string + + createCmd := func(container *libcontainer.Config, console, dataPath, init string, + pipe *os.File, args []string) *exec.Cmd { + containerCmd = namespaces.DefaultCreateCommand(container, console, dataPath, init, pipe, args) + statePath = dataPath + return containerCmd + } + + var containerStart sync.WaitGroup + containerStart.Add(1) + go func() { + buffers := newStdBuffers() + _, err := namespaces.Exec(config, + buffers.Stdin, buffers.Stdout, buffers.Stderr, + "", config.RootFs, []string{"sleep", "10"}, + createCmd, containerStart.Done) + containerErr <- err + }() + containerStart.Wait() + + return containerCmd, statePath, containerErr +} diff --git a/vendor/src/github.com/docker/libcontainer/integration/init_test.go b/vendor/src/github.com/docker/libcontainer/integration/init_test.go index 9954c0f8e..3106a5fb1 100644 --- a/vendor/src/github.com/docker/libcontainer/integration/init_test.go +++ b/vendor/src/github.com/docker/libcontainer/integration/init_test.go @@ -1,33 +1,76 @@ package integration import ( + "encoding/json" "log" "os" "runtime" + "github.com/docker/libcontainer" "github.com/docker/libcontainer/namespaces" + _ "github.com/docker/libcontainer/namespaces/nsenter" ) // init runs the libcontainer initialization code because of the busybox style needs // to work around the go runtime and the issues with forking func init() { - if len(os.Args) < 2 || os.Args[1] != "init" { + if len(os.Args) < 2 { return } - runtime.LockOSThread() + // handle init + if len(os.Args) >= 2 && os.Args[1] == "init" { + runtime.LockOSThread() - container, err := loadConfig() - if err != nil { - log.Fatal(err) + container, err := loadConfig() + if err != nil { + log.Fatal(err) + } + + rootfs, err := os.Getwd() + if err != nil { + log.Fatal(err) + } + + if err := namespaces.Init(container, rootfs, "", os.NewFile(3, "pipe"), os.Args[3:]); err != nil { + log.Fatalf("unable to initialize for container: %s", err) + } + os.Exit(1) } - rootfs, err := os.Getwd() - if err != nil { - log.Fatal(err) - } + // handle execin + if len(os.Args) >= 2 && os.Args[0] == "nsenter-exec" { + runtime.LockOSThread() - if err := namespaces.Init(container, rootfs, "", os.NewFile(3, "pipe"), os.Args[3:]); err != nil { - log.Fatalf("unable to initialize for container: %s", err) + // User args are passed after '--' in the command line. + userArgs := findUserArgs() + + config, err := loadConfigFromFd() + if err != nil { + log.Fatalf("docker-exec: unable to receive config from sync pipe: %s", err) + } + + if err := namespaces.FinalizeSetns(config, userArgs); err != nil { + log.Fatalf("docker-exec: failed to exec: %s", err) + } + os.Exit(1) } - os.Exit(1) +} + +func findUserArgs() []string { + for i, a := range os.Args { + if a == "--" { + return os.Args[i+1:] + } + } + return []string{} +} + +// loadConfigFromFd loads a container's config from the sync pipe that is provided by +// fd 3 when running a process +func loadConfigFromFd() (*libcontainer.Config, error) { + var config *libcontainer.Config + if err := json.NewDecoder(os.NewFile(3, "child")).Decode(&config); err != nil { + return nil, err + } + return config, nil } diff --git a/vendor/src/github.com/docker/libcontainer/integration/template_test.go b/vendor/src/github.com/docker/libcontainer/integration/template_test.go index efcf6d5b9..7e56628c2 100644 --- a/vendor/src/github.com/docker/libcontainer/integration/template_test.go +++ b/vendor/src/github.com/docker/libcontainer/integration/template_test.go @@ -32,12 +32,12 @@ func newTemplateConfig(rootfs string) *libcontainer.Config { "KILL", "AUDIT_WRITE", }, - Namespaces: map[string]bool{ - "NEWNS": true, - "NEWUTS": true, - "NEWIPC": true, - "NEWPID": true, - "NEWNET": true, + Namespaces: libcontainer.Namespaces{ + {Type: libcontainer.NEWNS}, + {Type: libcontainer.NEWUTS}, + {Type: libcontainer.NEWIPC}, + {Type: libcontainer.NEWPID}, + {Type: libcontainer.NEWNET}, }, Cgroups: &cgroups.Cgroup{ Parent: "integration", diff --git a/vendor/src/github.com/docker/libcontainer/ipc/ipc.go b/vendor/src/github.com/docker/libcontainer/ipc/ipc.go deleted file mode 100644 index 147cf5571..000000000 --- a/vendor/src/github.com/docker/libcontainer/ipc/ipc.go +++ /dev/null @@ -1,29 +0,0 @@ -package ipc - -import ( - "fmt" - "os" - "syscall" - - "github.com/docker/libcontainer/system" -) - -// Join the IPC Namespace of specified ipc path if it exists. -// If the path does not exist then you are not joining a container. -func Initialize(nsPath string) error { - if nsPath == "" { - return nil - } - f, err := os.OpenFile(nsPath, os.O_RDONLY, 0) - if err != nil { - return fmt.Errorf("failed get IPC namespace fd: %v", err) - } - - err = system.Setns(f.Fd(), syscall.CLONE_NEWIPC) - f.Close() - - if err != nil { - return fmt.Errorf("failed to setns current IPC namespace: %v", err) - } - return nil -} diff --git a/vendor/src/github.com/docker/libcontainer/namespaces/execin.go b/vendor/src/github.com/docker/libcontainer/namespaces/execin.go index 430dc72fe..7ce82c81b 100644 --- a/vendor/src/github.com/docker/libcontainer/namespaces/execin.go +++ b/vendor/src/github.com/docker/libcontainer/namespaces/execin.go @@ -97,6 +97,10 @@ func FinalizeSetns(container *libcontainer.Config, args []string) error { return err } + if err := setupRlimits(container); err != nil { + return fmt.Errorf("setup rlimits %s", err) + } + if err := FinalizeNamespace(container); err != nil { return err } diff --git a/vendor/src/github.com/docker/libcontainer/namespaces/init.go b/vendor/src/github.com/docker/libcontainer/namespaces/init.go index 7c83b1376..a4400bddb 100644 --- a/vendor/src/github.com/docker/libcontainer/namespaces/init.go +++ b/vendor/src/github.com/docker/libcontainer/namespaces/init.go @@ -13,7 +13,6 @@ import ( "github.com/docker/libcontainer" "github.com/docker/libcontainer/apparmor" "github.com/docker/libcontainer/console" - "github.com/docker/libcontainer/ipc" "github.com/docker/libcontainer/label" "github.com/docker/libcontainer/mount" "github.com/docker/libcontainer/netlink" @@ -65,7 +64,10 @@ func Init(container *libcontainer.Config, uncleanRootfs, consolePath string, pip if err := json.NewDecoder(pipe).Decode(&networkState); err != nil { return err } - + // join any namespaces via a path to the namespace fd if provided + if err := joinExistingNamespaces(container.Namespaces); err != nil { + return err + } if consolePath != "" { if err := console.OpenAndDup(consolePath); err != nil { return err @@ -79,9 +81,7 @@ func Init(container *libcontainer.Config, uncleanRootfs, consolePath string, pip return fmt.Errorf("setctty %s", err) } } - if err := ipc.Initialize(container.IpcNsPath); err != nil { - return fmt.Errorf("setup IPC %s", err) - } + if err := setupNetwork(container, networkState); err != nil { return fmt.Errorf("setup networking %s", err) } @@ -178,17 +178,17 @@ func SetupUser(u string) error { Home: "/", } - passwdFile, err := user.GetPasswdFile() + passwdPath, err := user.GetPasswdPath() if err != nil { return err } - groupFile, err := user.GetGroupFile() + groupPath, err := user.GetGroupPath() if err != nil { return err } - execUser, err := user.GetExecUserFile(u, &defaultExecUser, passwdFile, groupFile) + execUser, err := user.GetExecUserPath(u, &defaultExecUser, passwdPath, groupPath) if err != nil { return fmt.Errorf("get supplementary groups %s", err) } @@ -308,3 +308,22 @@ func LoadContainerEnvironment(container *libcontainer.Config) error { } return nil } + +// joinExistingNamespaces gets all the namespace paths specified for the container and +// does a setns on the namespace fd so that the current process joins the namespace. +func joinExistingNamespaces(namespaces []libcontainer.Namespace) error { + for _, ns := range namespaces { + if ns.Path != "" { + f, err := os.OpenFile(ns.Path, os.O_RDONLY, 0) + if err != nil { + return err + } + err = system.Setns(f.Fd(), uintptr(namespaceInfo[ns.Type])) + f.Close() + if err != nil { + return err + } + } + } + return nil +} diff --git a/vendor/src/github.com/docker/libcontainer/namespaces/nsenter/nsenter.c b/vendor/src/github.com/docker/libcontainer/namespaces/nsenter/nsenter.c index f060f63b1..1a81c3157 100644 --- a/vendor/src/github.com/docker/libcontainer/namespaces/nsenter/nsenter.c +++ b/vendor/src/github.com/docker/libcontainer/namespaces/nsenter/nsenter.c @@ -15,6 +15,10 @@ #include #include +#ifndef PR_SET_CHILD_SUBREAPER +#define PR_SET_CHILD_SUBREAPER 36 +#endif + static const kBufSize = 256; static const char *kNsEnter = "nsenter"; @@ -32,8 +36,8 @@ void get_args(int *argc, char ***argv) contents_size += kBufSize; contents = (char *)realloc(contents, contents_size); bytes_read = - read(fd, contents + contents_offset, - contents_size - contents_offset); + read(fd, contents + contents_offset, + contents_size - contents_offset); contents_offset += bytes_read; } while (bytes_read > 0); @@ -90,16 +94,17 @@ void nsenter() } if (prctl(PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0) == -1) { - fprintf(stderr, "nsenter: failed to set child subreaper: %s", strerror(errno)); - exit(1); - } + fprintf(stderr, "nsenter: failed to set child subreaper: %s", + strerror(errno)); + exit(1); + } static const struct option longopts[] = { {"nspid", required_argument, NULL, 'n'}, {"console", required_argument, NULL, 't'}, {NULL, 0, NULL, 0} }; - + pid_t init_pid = -1; char *init_pid_str = NULL; char *console = NULL; diff --git a/vendor/src/github.com/docker/libcontainer/namespaces/types.go b/vendor/src/github.com/docker/libcontainer/namespaces/types.go deleted file mode 100644 index 16ce981e8..000000000 --- a/vendor/src/github.com/docker/libcontainer/namespaces/types.go +++ /dev/null @@ -1,50 +0,0 @@ -package namespaces - -import "errors" - -type ( - Namespace struct { - Key string `json:"key,omitempty"` - Value int `json:"value,omitempty"` - File string `json:"file,omitempty"` - } - Namespaces []*Namespace -) - -// namespaceList is used to convert the libcontainer types -// into the names of the files located in /proc//ns/* for -// each namespace -var ( - namespaceList = Namespaces{} - ErrUnkownNamespace = errors.New("Unknown namespace") - ErrUnsupported = errors.New("Unsupported method") -) - -func (ns *Namespace) String() string { - return ns.Key -} - -func GetNamespace(key string) *Namespace { - for _, ns := range namespaceList { - if ns.Key == key { - cpy := *ns - return &cpy - } - } - return nil -} - -// Contains returns true if the specified Namespace is -// in the slice -func (n Namespaces) Contains(ns string) bool { - return n.Get(ns) != nil -} - -func (n Namespaces) Get(ns string) *Namespace { - for _, nsp := range n { - if nsp != nil && nsp.Key == ns { - return nsp - } - } - return nil -} diff --git a/vendor/src/github.com/docker/libcontainer/namespaces/types_linux.go b/vendor/src/github.com/docker/libcontainer/namespaces/types_linux.go deleted file mode 100644 index d3079944c..000000000 --- a/vendor/src/github.com/docker/libcontainer/namespaces/types_linux.go +++ /dev/null @@ -1,16 +0,0 @@ -package namespaces - -import ( - "syscall" -) - -func init() { - namespaceList = Namespaces{ - {Key: "NEWNS", Value: syscall.CLONE_NEWNS, File: "mnt"}, - {Key: "NEWUTS", Value: syscall.CLONE_NEWUTS, File: "uts"}, - {Key: "NEWIPC", Value: syscall.CLONE_NEWIPC, File: "ipc"}, - {Key: "NEWUSER", Value: syscall.CLONE_NEWUSER, File: "user"}, - {Key: "NEWPID", Value: syscall.CLONE_NEWPID, File: "pid"}, - {Key: "NEWNET", Value: syscall.CLONE_NEWNET, File: "net"}, - } -} diff --git a/vendor/src/github.com/docker/libcontainer/namespaces/types_test.go b/vendor/src/github.com/docker/libcontainer/namespaces/types_test.go deleted file mode 100644 index 4d0a72c9b..000000000 --- a/vendor/src/github.com/docker/libcontainer/namespaces/types_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package namespaces - -import ( - "testing" -) - -func TestNamespacesContains(t *testing.T) { - ns := Namespaces{ - GetNamespace("NEWPID"), - GetNamespace("NEWNS"), - GetNamespace("NEWUTS"), - } - - if ns.Contains("NEWNET") { - t.Fatal("namespaces should not contain NEWNET") - } - - if !ns.Contains("NEWPID") { - t.Fatal("namespaces should contain NEWPID but does not") - } - - withNil := Namespaces{ - GetNamespace("UNDEFINED"), // this element will be nil - GetNamespace("NEWPID"), - } - - if !withNil.Contains("NEWPID") { - t.Fatal("namespaces should contain NEWPID but does not") - } -} diff --git a/vendor/src/github.com/docker/libcontainer/namespaces/utils.go b/vendor/src/github.com/docker/libcontainer/namespaces/utils.go index bf60cd8f0..de71a379f 100644 --- a/vendor/src/github.com/docker/libcontainer/namespaces/utils.go +++ b/vendor/src/github.com/docker/libcontainer/namespaces/utils.go @@ -5,6 +5,8 @@ package namespaces import ( "os" "syscall" + + "github.com/docker/libcontainer" ) type initError struct { @@ -15,6 +17,15 @@ func (i initError) Error() string { return i.Message } +var namespaceInfo = map[libcontainer.NamespaceType]int{ + libcontainer.NEWNET: syscall.CLONE_NEWNET, + libcontainer.NEWNS: syscall.CLONE_NEWNS, + libcontainer.NEWUSER: syscall.CLONE_NEWUSER, + libcontainer.NEWIPC: syscall.CLONE_NEWIPC, + libcontainer.NEWUTS: syscall.CLONE_NEWUTS, + libcontainer.NEWPID: syscall.CLONE_NEWPID, +} + // New returns a newly initialized Pipe for communication between processes func newInitPipe() (parent *os.File, child *os.File, err error) { fds, err := syscall.Socketpair(syscall.AF_LOCAL, syscall.SOCK_STREAM|syscall.SOCK_CLOEXEC, 0) @@ -26,13 +37,9 @@ func newInitPipe() (parent *os.File, child *os.File, err error) { // GetNamespaceFlags parses the container's Namespaces options to set the correct // flags on clone, unshare, and setns -func GetNamespaceFlags(namespaces map[string]bool) (flag int) { - for key, enabled := range namespaces { - if enabled { - if ns := GetNamespace(key); ns != nil { - flag |= ns.Value - } - } +func GetNamespaceFlags(namespaces libcontainer.Namespaces) (flag int) { + for _, v := range namespaces { + flag |= namespaceInfo[v.Type] } return flag } diff --git a/vendor/src/github.com/docker/libcontainer/netlink/netlink_linux.go b/vendor/src/github.com/docker/libcontainer/netlink/netlink_linux.go index 1bf70430f..3cc3cc94f 100644 --- a/vendor/src/github.com/docker/libcontainer/netlink/netlink_linux.go +++ b/vendor/src/github.com/docker/libcontainer/netlink/netlink_linux.go @@ -522,11 +522,10 @@ func NetworkSetMacAddress(iface *net.Interface, macaddr string) error { var ( MULTICAST byte = 0x1 - LOCALOUI byte = 0x2 ) - if hwaddr[0]&0x1 == MULTICAST || hwaddr[0]&0x2 != LOCALOUI { - return fmt.Errorf("Incorrect Local MAC Address specified: %s", macaddr) + if hwaddr[0]&0x1 == MULTICAST { + return fmt.Errorf("Multicast MAC Address is not supported: %s", macaddr) } wb := newNetlinkRequest(syscall.RTM_SETLINK, syscall.NLM_F_ACK) diff --git a/vendor/src/github.com/docker/libcontainer/network/netns.go b/vendor/src/github.com/docker/libcontainer/network/netns.go deleted file mode 100644 index 73cd8de53..000000000 --- a/vendor/src/github.com/docker/libcontainer/network/netns.go +++ /dev/null @@ -1,39 +0,0 @@ -// +build linux - -package network - -import ( - "fmt" - "os" - "syscall" - - "github.com/docker/libcontainer/system" -) - -// crosbymichael: could make a network strategy that instead of returning veth pair names it returns a pid to an existing network namespace -type NetNS struct { -} - -func (v *NetNS) Create(n *Network, nspid int, networkState *NetworkState) error { - networkState.NsPath = n.NsPath - return nil -} - -func (v *NetNS) Initialize(config *Network, networkState *NetworkState) error { - if networkState.NsPath == "" { - return fmt.Errorf("nspath does is not specified in NetworkState") - } - - f, err := os.OpenFile(networkState.NsPath, os.O_RDONLY, 0) - if err != nil { - return fmt.Errorf("failed get network namespace fd: %v", err) - } - - if err := system.Setns(f.Fd(), syscall.CLONE_NEWNET); err != nil { - f.Close() - return fmt.Errorf("failed to setns current network namespace: %v", err) - } - - f.Close() - return nil -} diff --git a/vendor/src/github.com/docker/libcontainer/network/network.go b/vendor/src/github.com/docker/libcontainer/network/network.go index ba8f6f74e..40b25b135 100644 --- a/vendor/src/github.com/docker/libcontainer/network/network.go +++ b/vendor/src/github.com/docker/libcontainer/network/network.go @@ -88,6 +88,18 @@ func SetInterfaceIp(name string, rawIp string) error { return netlink.NetworkLinkAddIp(iface, ip, ipNet) } +func DeleteInterfaceIp(name string, rawIp string) error { + iface, err := net.InterfaceByName(name) + if err != nil { + return err + } + ip, ipNet, err := net.ParseCIDR(rawIp) + if err != nil { + return err + } + return netlink.NetworkLinkDelIp(iface, ip, ipNet) +} + func SetMtu(name string, mtu int) error { iface, err := net.InterfaceByName(name) if err != nil { diff --git a/vendor/src/github.com/docker/libcontainer/network/strategy.go b/vendor/src/github.com/docker/libcontainer/network/strategy.go index be5ec93b7..019fe62f4 100644 --- a/vendor/src/github.com/docker/libcontainer/network/strategy.go +++ b/vendor/src/github.com/docker/libcontainer/network/strategy.go @@ -13,7 +13,6 @@ var ( var strategies = map[string]NetworkStrategy{ "veth": &Veth{}, "loopback": &Loopback{}, - "netns": &NetNS{}, } // NetworkStrategy represents a specific network configuration for diff --git a/vendor/src/github.com/docker/libcontainer/network/types.go b/vendor/src/github.com/docker/libcontainer/network/types.go index ea0741be1..dcf00420f 100644 --- a/vendor/src/github.com/docker/libcontainer/network/types.go +++ b/vendor/src/github.com/docker/libcontainer/network/types.go @@ -8,9 +8,6 @@ type Network struct { // Type sets the networks type, commonly veth and loopback Type string `json:"type,omitempty"` - // Path to network namespace - NsPath string `json:"ns_path,omitempty"` - // The bridge to use. Bridge string `json:"bridge,omitempty"` @@ -50,6 +47,4 @@ type NetworkState struct { VethHost string `json:"veth_host,omitempty"` // The name of the veth interface created inside the container for the child. VethChild string `json:"veth_child,omitempty"` - // Net namespace path. - NsPath string `json:"ns_path,omitempty"` } diff --git a/vendor/src/github.com/docker/libcontainer/cgroups/fs/notify_linux.go b/vendor/src/github.com/docker/libcontainer/notify_linux.go similarity index 54% rename from vendor/src/github.com/docker/libcontainer/cgroups/fs/notify_linux.go rename to vendor/src/github.com/docker/libcontainer/notify_linux.go index d92063bad..a4923273a 100644 --- a/vendor/src/github.com/docker/libcontainer/cgroups/fs/notify_linux.go +++ b/vendor/src/github.com/docker/libcontainer/notify_linux.go @@ -1,33 +1,29 @@ // +build linux -package fs +package libcontainer import ( "fmt" + "io/ioutil" "os" "path/filepath" "syscall" - - "github.com/docker/libcontainer/cgroups" ) -// NotifyOnOOM sends signals on the returned channel when the cgroup reaches -// its memory limit. The channel is closed when the cgroup is removed. -func NotifyOnOOM(c *cgroups.Cgroup) (<-chan struct{}, error) { - d, err := getCgroupData(c, 0) +const oomCgroupName = "memory" + +// NotifyOnOOM returns channel on which you can expect event about OOM, +// if process died without OOM this channel will be closed. +// s is current *libcontainer.State for container. +func NotifyOnOOM(s *State) (<-chan struct{}, error) { + dir := s.CgroupPaths[oomCgroupName] + if dir == "" { + return nil, fmt.Errorf("There is no path for %q in state", oomCgroupName) + } + oomControl, err := os.Open(filepath.Join(dir, "memory.oom_control")) if err != nil { return nil, err } - - return notifyOnOOM(d) -} - -func notifyOnOOM(d *data) (<-chan struct{}, error) { - dir, err := d.path("memory") - if err != nil { - return nil, err - } - fd, _, syserr := syscall.RawSyscall(syscall.SYS_EVENTFD2, 0, syscall.FD_CLOEXEC, 0) if syserr != 0 { return nil, syserr @@ -35,48 +31,32 @@ func notifyOnOOM(d *data) (<-chan struct{}, error) { eventfd := os.NewFile(fd, "eventfd") - oomControl, err := os.Open(filepath.Join(dir, "memory.oom_control")) - if err != nil { - eventfd.Close() - return nil, err - } - - var ( - eventControlPath = filepath.Join(dir, "cgroup.event_control") - data = fmt.Sprintf("%d %d", eventfd.Fd(), oomControl.Fd()) - ) - - if err := writeFile(dir, "cgroup.event_control", data); err != nil { + eventControlPath := filepath.Join(dir, "cgroup.event_control") + data := fmt.Sprintf("%d %d", eventfd.Fd(), oomControl.Fd()) + if err := ioutil.WriteFile(eventControlPath, []byte(data), 0700); err != nil { eventfd.Close() oomControl.Close() return nil, err } - ch := make(chan struct{}) - go func() { defer func() { close(ch) eventfd.Close() oomControl.Close() }() - buf := make([]byte, 8) - for { if _, err := eventfd.Read(buf); err != nil { return } - // When a cgroup is destroyed, an event is sent to eventfd. // So if the control path is gone, return instead of notifying. if _, err := os.Lstat(eventControlPath); os.IsNotExist(err) { return } - ch <- struct{}{} } }() - return ch, nil } diff --git a/vendor/src/github.com/docker/libcontainer/cgroups/fs/notify_linux_test.go b/vendor/src/github.com/docker/libcontainer/notify_linux_test.go similarity index 67% rename from vendor/src/github.com/docker/libcontainer/cgroups/fs/notify_linux_test.go rename to vendor/src/github.com/docker/libcontainer/notify_linux_test.go index a11880cb6..5d1d54576 100644 --- a/vendor/src/github.com/docker/libcontainer/cgroups/fs/notify_linux_test.go +++ b/vendor/src/github.com/docker/libcontainer/notify_linux_test.go @@ -1,38 +1,48 @@ // +build linux -package fs +package libcontainer import ( "encoding/binary" "fmt" + "io/ioutil" + "os" + "path/filepath" "syscall" "testing" "time" ) func TestNotifyOnOOM(t *testing.T) { - helper := NewCgroupTestUtil("memory", t) - defer helper.cleanup() - - helper.writeFileContents(map[string]string{ - "memory.oom_control": "", - "cgroup.event_control": "", - }) - + memoryPath, err := ioutil.TempDir("", "testnotifyoom-") + if err != nil { + t.Fatal(err) + } + oomPath := filepath.Join(memoryPath, "memory.oom_control") + eventPath := filepath.Join(memoryPath, "cgroup.event_control") + if err := ioutil.WriteFile(oomPath, []byte{}, 0700); err != nil { + t.Fatal(err) + } + if err := ioutil.WriteFile(eventPath, []byte{}, 0700); err != nil { + t.Fatal(err) + } var eventFd, oomControlFd int - - ooms, err := notifyOnOOM(helper.CgroupData) + st := &State{ + CgroupPaths: map[string]string{ + "memory": memoryPath, + }, + } + ooms, err := NotifyOnOOM(st) if err != nil { t.Fatal("expected no error, got:", err) } - memoryPath, _ := helper.CgroupData.path("memory") - data, err := readFile(memoryPath, "cgroup.event_control") + data, err := ioutil.ReadFile(eventPath) if err != nil { t.Fatal("couldn't read event control file:", err) } - if _, err := fmt.Sscanf(data, "%d %d", &eventFd, &oomControlFd); err != nil { + if _, err := fmt.Sscanf(string(data), "%d %d", &eventFd, &oomControlFd); err != nil { t.Fatalf("invalid control data %q: %s", data, err) } @@ -62,7 +72,9 @@ func TestNotifyOnOOM(t *testing.T) { // simulate what happens when a cgroup is destroyed by cleaning up and then // writing to the eventfd. - helper.cleanup() + if err := os.RemoveAll(memoryPath); err != nil { + t.Fatal(err) + } if _, err := syscall.Write(efd, buf); err != nil { t.Fatal("unable to write to eventfd:", err) } diff --git a/vendor/src/github.com/docker/libcontainer/sample_configs/apparmor.json b/vendor/src/github.com/docker/libcontainer/sample_configs/apparmor.json index f739df100..96f73cb79 100644 --- a/vendor/src/github.com/docker/libcontainer/sample_configs/apparmor.json +++ b/vendor/src/github.com/docker/libcontainer/sample_configs/apparmor.json @@ -176,13 +176,13 @@ "TERM=xterm" ], "hostname": "koye", - "namespaces": { - "NEWIPC": true, - "NEWNET": true, - "NEWNS": true, - "NEWPID": true, - "NEWUTS": true - }, + "namespaces": [ + {"type":"NEWIPC"}, + {"type": "NEWNET"}, + {"type": "NEWNS"}, + {"type": "NEWPID"}, + {"type": "NEWUTS"} + ], "networks": [ { "address": "127.0.0.1/0", diff --git a/vendor/src/github.com/docker/libcontainer/sample_configs/attach_to_bridge.json b/vendor/src/github.com/docker/libcontainer/sample_configs/attach_to_bridge.json index 0795e6c14..e5c03a7ef 100644 --- a/vendor/src/github.com/docker/libcontainer/sample_configs/attach_to_bridge.json +++ b/vendor/src/github.com/docker/libcontainer/sample_configs/attach_to_bridge.json @@ -175,13 +175,13 @@ "TERM=xterm" ], "hostname": "koye", - "namespaces": { - "NEWIPC": true, - "NEWNET": true, - "NEWNS": true, - "NEWPID": true, - "NEWUTS": true - }, + "namespaces": [ + {"type": "NEWIPC"}, + {"type": "NEWNET"}, + {"type": "NEWNS"}, + {"type": "NEWPID"}, + {"type": "NEWUTS"} + ], "networks": [ { "address": "127.0.0.1/0", diff --git a/vendor/src/github.com/docker/libcontainer/sample_configs/minimal.json b/vendor/src/github.com/docker/libcontainer/sample_configs/minimal.json index 8d85ddf7d..01de46746 100644 --- a/vendor/src/github.com/docker/libcontainer/sample_configs/minimal.json +++ b/vendor/src/github.com/docker/libcontainer/sample_configs/minimal.json @@ -181,13 +181,13 @@ "TERM=xterm" ], "hostname": "koye", - "namespaces": { - "NEWIPC": true, - "NEWNET": true, - "NEWNS": true, - "NEWPID": true, - "NEWUTS": true - }, + "namespaces": [ + {"type": "NEWIPC"}, + {"type": "NEWNET"}, + {"type": "NEWNS"}, + {"type": "NEWPID"}, + {"type": "NEWUTS"} + ], "networks": [ { "address": "127.0.0.1/0", diff --git a/vendor/src/github.com/docker/libcontainer/sample_configs/route_source_address_selection.json b/vendor/src/github.com/docker/libcontainer/sample_configs/route_source_address_selection.json index d4baf94cd..9c62045a4 100644 --- a/vendor/src/github.com/docker/libcontainer/sample_configs/route_source_address_selection.json +++ b/vendor/src/github.com/docker/libcontainer/sample_configs/route_source_address_selection.json @@ -175,13 +175,13 @@ "TERM=xterm" ], "hostname": "koye", - "namespaces": { - "NEWIPC": true, - "NEWNET": true, - "NEWNS": true, - "NEWPID": true, - "NEWUTS": true - }, + "namespaces": [ + {"type": "NEWIPC"}, + {"type": "NEWNET"}, + {"type": "NEWNS"}, + {"type": "NEWPID"}, + {"type": "NEWUTS"} + ], "networks": [ { "address": "127.0.0.1/0", diff --git a/vendor/src/github.com/docker/libcontainer/sample_configs/selinux.json b/vendor/src/github.com/docker/libcontainer/sample_configs/selinux.json index ce383e2cc..15556488a 100644 --- a/vendor/src/github.com/docker/libcontainer/sample_configs/selinux.json +++ b/vendor/src/github.com/docker/libcontainer/sample_configs/selinux.json @@ -177,13 +177,13 @@ "TERM=xterm" ], "hostname": "koye", - "namespaces": { - "NEWIPC": true, - "NEWNET": true, - "NEWNS": true, - "NEWPID": true, - "NEWUTS": true - }, + "namespaces": [ + {"type": "NEWIPC"}, + {"type": "NEWNET"}, + {"type": "NEWNS"}, + {"type": "NEWPID"}, + {"type": "NEWUTS"} + ], "networks": [ { "address": "127.0.0.1/0", diff --git a/vendor/src/github.com/docker/libcontainer/user/MAINTAINERS b/vendor/src/github.com/docker/libcontainer/user/MAINTAINERS index 18e05a307..edbe20066 100644 --- a/vendor/src/github.com/docker/libcontainer/user/MAINTAINERS +++ b/vendor/src/github.com/docker/libcontainer/user/MAINTAINERS @@ -1 +1,2 @@ Tianon Gravi (@tianon) +Aleksa Sarai (@cyphar) diff --git a/vendor/src/github.com/docker/libcontainer/user/lookup_unix.go b/vendor/src/github.com/docker/libcontainer/user/lookup_unix.go index 409c114e2..758b734c2 100644 --- a/vendor/src/github.com/docker/libcontainer/user/lookup_unix.go +++ b/vendor/src/github.com/docker/libcontainer/user/lookup_unix.go @@ -9,22 +9,22 @@ import ( // Unix-specific path to the passwd and group formatted files. const ( - unixPasswdFile = "/etc/passwd" - unixGroupFile = "/etc/group" + unixPasswdPath = "/etc/passwd" + unixGroupPath = "/etc/group" ) -func GetPasswdFile() (string, error) { - return unixPasswdFile, nil +func GetPasswdPath() (string, error) { + return unixPasswdPath, nil } func GetPasswd() (io.ReadCloser, error) { - return os.Open(unixPasswdFile) + return os.Open(unixPasswdPath) } -func GetGroupFile() (string, error) { - return unixGroupFile, nil +func GetGroupPath() (string, error) { + return unixGroupPath, nil } func GetGroup() (io.ReadCloser, error) { - return os.Open(unixGroupFile) + return os.Open(unixGroupPath) } diff --git a/vendor/src/github.com/docker/libcontainer/user/lookup_unsupported.go b/vendor/src/github.com/docker/libcontainer/user/lookup_unsupported.go index 0f15c57d8..721794887 100644 --- a/vendor/src/github.com/docker/libcontainer/user/lookup_unsupported.go +++ b/vendor/src/github.com/docker/libcontainer/user/lookup_unsupported.go @@ -4,7 +4,7 @@ package user import "io" -func GetPasswdFile() (string, error) { +func GetPasswdPath() (string, error) { return "", ErrUnsupported } @@ -12,7 +12,7 @@ func GetPasswd() (io.ReadCloser, error) { return nil, ErrUnsupported } -func GetGroupFile() (string, error) { +func GetGroupPath() (string, error) { return "", ErrUnsupported } diff --git a/vendor/src/github.com/docker/libcontainer/user/user.go b/vendor/src/github.com/docker/libcontainer/user/user.go index 69387f2ef..d7439f12e 100644 --- a/vendor/src/github.com/docker/libcontainer/user/user.go +++ b/vendor/src/github.com/docker/libcontainer/user/user.go @@ -197,11 +197,11 @@ type ExecUser struct { Home string } -// GetExecUserFile is a wrapper for GetExecUser. It reads data from each of the +// GetExecUserPath is a wrapper for GetExecUser. It reads data from each of the // given file paths and uses that data as the arguments to GetExecUser. If the // files cannot be opened for any reason, the error is ignored and a nil // io.Reader is passed instead. -func GetExecUserFile(userSpec string, defaults *ExecUser, passwdPath, groupPath string) (*ExecUser, error) { +func GetExecUserPath(userSpec string, defaults *ExecUser, passwdPath, groupPath string) (*ExecUser, error) { passwd, err := os.Open(passwdPath) if err != nil { passwd = nil From 7fdbd90f8805736aa78156faf7e6f8fdd2384af7 Mon Sep 17 00:00:00 2001 From: Jessica Frazelle Date: Tue, 23 Dec 2014 11:16:23 -0800 Subject: [PATCH 106/513] Return usage on parseExec error. Docker-DCO-1.1-Signed-off-by: Jessica Frazelle (github: jfrazelle) --- api/client/commands.go | 1 + integration-cli/docker_cli_exec_test.go | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/api/client/commands.go b/api/client/commands.go index 487b0b6cf..e666d4320 100644 --- a/api/client/commands.go +++ b/api/client/commands.go @@ -2589,6 +2589,7 @@ func (cli *DockerCli) CmdExec(args ...string) error { execConfig, err := runconfig.ParseExec(cmd, args) if err != nil { + cmd.Usage() return err } if execConfig.Container == "" { diff --git a/integration-cli/docker_cli_exec_test.go b/integration-cli/docker_cli_exec_test.go index 747ad4ff8..a3a6a2362 100644 --- a/integration-cli/docker_cli_exec_test.go +++ b/integration-cli/docker_cli_exec_test.go @@ -351,3 +351,19 @@ func TestExecTtyWithoutStdin(t *testing.T) { logDone("exec - forbid piped stdin to tty enabled container") } + +func TestExecParseError(t *testing.T) { + defer deleteAllContainers() + + runCmd := exec.Command(dockerBinary, "run", "-d", "--name", "top", "busybox", "top") + if out, _, err := runCommandWithOutput(runCmd); err != nil { + t.Fatal(out, err) + } + + // Test normal (non-detached) case first + cmd := exec.Command(dockerBinary, "exec", "top") + if out, _, err := runCommandWithOutput(cmd); err == nil || !strings.Contains(out, "Usage:") { + t.Fatalf("Should have thrown error & given usage: %s", out) + } + logDone("exec - error on parseExec should return usage") +} From 8dc86c0e36da3d3206220fffc75004759dd5d2cb Mon Sep 17 00:00:00 2001 From: Alexander Morozov Date: Tue, 23 Dec 2014 13:15:19 -0800 Subject: [PATCH 107/513] More graceful stop for testing daemon Fixes problem with TestDaemonAllocatesListeningPort Signed-off-by: Alexander Morozov --- integration-cli/docker_utils.go | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/integration-cli/docker_utils.go b/integration-cli/docker_utils.go index 93cb4a6b3..03d34b6d7 100644 --- a/integration-cli/docker_utils.go +++ b/integration-cli/docker_utils.go @@ -206,20 +206,33 @@ func (d *Daemon) Stop() error { if err := d.cmd.Process.Signal(os.Interrupt); err != nil { return fmt.Errorf("could not send signal: %v", err) } -out: +out1: for { select { case err := <-d.wait: return err - case <-time.After(20 * time.Second): + case <-time.After(15 * time.Second): + // time for stopping jobs and run onShutdown hooks d.t.Log("timeout") - break out + break out1 + } + } + +out2: + for { + select { + case err := <-d.wait: + return err case <-tick: - d.t.Logf("Attempt #%d: daemon is still running with pid %d", i+1, d.cmd.Process.Pid) + i++ + if i > 4 { + d.t.Log("tried to interrupt daemon for %d times, now try to kill it", i) + break out2 + } + d.t.Logf("Attempt #%d: daemon is still running with pid %d", i, d.cmd.Process.Pid) if err := d.cmd.Process.Signal(os.Interrupt); err != nil { return fmt.Errorf("could not send signal: %v", err) } - i++ } } From 4b43a6df7acd98228b8b287eedf38ba87dc8a388 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Tue, 23 Dec 2014 22:03:20 +0000 Subject: [PATCH 108/513] add ExecIDs in inspect Signed-off-by: Victor Vieux --- daemon/exec.go | 20 +++++++++++++++++--- daemon/inspect.go | 2 ++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/daemon/exec.go b/daemon/exec.go index ecdbc58d8..92f23f738 100644 --- a/daemon/exec.go +++ b/daemon/exec.go @@ -35,7 +35,7 @@ type execConfig struct { type execStore struct { s map[string]*execConfig - sync.Mutex + sync.RWMutex } func newExecStore() *execStore { @@ -49,9 +49,9 @@ func (e *execStore) Add(id string, execConfig *execConfig) { } func (e *execStore) Get(id string) *execConfig { - e.Lock() + e.RLock() res := e.s[id] - e.Unlock() + e.RUnlock() return res } @@ -61,6 +61,16 @@ func (e *execStore) Delete(id string) { e.Unlock() } +func (e *execStore) List() []string { + var IDs []string + e.RLock() + for id, _ := range e.s { + IDs = append(IDs, id) + } + e.RUnlock() + return IDs +} + func (execConfig *execConfig) Resize(h, w int) error { return execConfig.ProcessConfig.Terminal.Resize(h, w) } @@ -249,6 +259,10 @@ func (d *Daemon) Exec(c *Container, execConfig *execConfig, pipes *execdriver.Pi return exitStatus, err } +func (container *Container) GetExecIDs() []string { + return container.execCommands.List() +} + func (container *Container) Exec(execConfig *execConfig) error { container.Lock() defer container.Unlock() diff --git a/daemon/inspect.go b/daemon/inspect.go index 2bf1773d3..37d00573b 100644 --- a/daemon/inspect.go +++ b/daemon/inspect.go @@ -50,6 +50,8 @@ func (daemon *Daemon) ContainerInspect(job *engine.Job) engine.Status { out.SetJson("VolumesRW", container.VolumesRW) out.SetJson("AppArmorProfile", container.AppArmorProfile) + out.SetList("ExecIDs", container.GetExecIDs()) + if children, err := daemon.Children(container.Name); err == nil { for linkAlias, child := range children { container.hostConfig.Links = append(container.hostConfig.Links, fmt.Sprintf("%s:%s", child.Name, linkAlias)) From edbf1ed68085171649dd0118e2f6755e7037894c Mon Sep 17 00:00:00 2001 From: Tianon Gravi Date: Thu, 11 Dec 2014 14:52:52 -0700 Subject: [PATCH 109/513] Add github.com/go-fsnotify/fsnotify Docker-DCO-1.1-Signed-off-by: Tianon Gravi --- project/vendor.sh | 2 + .../go-fsnotify/fsnotify/.gitignore | 6 + .../go-fsnotify/fsnotify/.travis.yml | 13 + .../github.com/go-fsnotify/fsnotify/AUTHORS | 32 + .../go-fsnotify/fsnotify/CHANGELOG.md | 237 ++++ .../go-fsnotify/fsnotify/CONTRIBUTING.md | 56 + .../github.com/go-fsnotify/fsnotify/LICENSE | 28 + .../github.com/go-fsnotify/fsnotify/README.md | 53 + .../go-fsnotify/fsnotify/example_test.go | 42 + .../go-fsnotify/fsnotify/fsnotify.go | 56 + .../go-fsnotify/fsnotify/inotify.go | 239 ++++ .../go-fsnotify/fsnotify/integration_test.go | 1120 +++++++++++++++++ .../github.com/go-fsnotify/fsnotify/kqueue.go | 479 +++++++ .../go-fsnotify/fsnotify/open_mode_bsd.go | 11 + .../go-fsnotify/fsnotify/open_mode_darwin.go | 12 + .../go-fsnotify/fsnotify/windows.go | 561 +++++++++ 16 files changed, 2947 insertions(+) create mode 100644 vendor/src/github.com/go-fsnotify/fsnotify/.gitignore create mode 100644 vendor/src/github.com/go-fsnotify/fsnotify/.travis.yml create mode 100644 vendor/src/github.com/go-fsnotify/fsnotify/AUTHORS create mode 100644 vendor/src/github.com/go-fsnotify/fsnotify/CHANGELOG.md create mode 100644 vendor/src/github.com/go-fsnotify/fsnotify/CONTRIBUTING.md create mode 100644 vendor/src/github.com/go-fsnotify/fsnotify/LICENSE create mode 100644 vendor/src/github.com/go-fsnotify/fsnotify/README.md create mode 100644 vendor/src/github.com/go-fsnotify/fsnotify/example_test.go create mode 100644 vendor/src/github.com/go-fsnotify/fsnotify/fsnotify.go create mode 100644 vendor/src/github.com/go-fsnotify/fsnotify/inotify.go create mode 100644 vendor/src/github.com/go-fsnotify/fsnotify/integration_test.go create mode 100644 vendor/src/github.com/go-fsnotify/fsnotify/kqueue.go create mode 100644 vendor/src/github.com/go-fsnotify/fsnotify/open_mode_bsd.go create mode 100644 vendor/src/github.com/go-fsnotify/fsnotify/open_mode_darwin.go create mode 100644 vendor/src/github.com/go-fsnotify/fsnotify/windows.go diff --git a/project/vendor.sh b/project/vendor.sh index 0b56cb1b6..f87dd6501 100755 --- a/project/vendor.sh +++ b/project/vendor.sh @@ -55,6 +55,8 @@ clone git github.com/docker/libtrust 230dfd18c232 clone git github.com/Sirupsen/logrus v0.6.0 +clone git github.com/go-fsnotify/fsnotify v1.0.4 + # get Go tip's archive/tar, for xattr support and improved performance # TODO after Go 1.4 drops, bump our minimum supported version and drop this vendored dep if [ "$1" = '--go' ]; then diff --git a/vendor/src/github.com/go-fsnotify/fsnotify/.gitignore b/vendor/src/github.com/go-fsnotify/fsnotify/.gitignore new file mode 100644 index 000000000..4cd0cbaf4 --- /dev/null +++ b/vendor/src/github.com/go-fsnotify/fsnotify/.gitignore @@ -0,0 +1,6 @@ +# Setup a Global .gitignore for OS and editor generated files: +# https://help.github.com/articles/ignoring-files +# git config --global core.excludesfile ~/.gitignore_global + +.vagrant +*.sublime-project diff --git a/vendor/src/github.com/go-fsnotify/fsnotify/.travis.yml b/vendor/src/github.com/go-fsnotify/fsnotify/.travis.yml new file mode 100644 index 000000000..f8e76fc66 --- /dev/null +++ b/vendor/src/github.com/go-fsnotify/fsnotify/.travis.yml @@ -0,0 +1,13 @@ +language: go + +go: + - 1.2 + - tip + +# not yet https://github.com/travis-ci/travis-ci/issues/2318 +os: + - linux + - osx + +notifications: + email: false diff --git a/vendor/src/github.com/go-fsnotify/fsnotify/AUTHORS b/vendor/src/github.com/go-fsnotify/fsnotify/AUTHORS new file mode 100644 index 000000000..306091eda --- /dev/null +++ b/vendor/src/github.com/go-fsnotify/fsnotify/AUTHORS @@ -0,0 +1,32 @@ +# Names should be added to this file as +# Name or Organization +# The email address is not required for organizations. + +# You can update this list using the following command: +# +# $ git shortlog -se | awk '{print $2 " " $3 " " $4}' + +# Please keep the list sorted. + +Adrien Bustany +Caleb Spare +Case Nelson +Chris Howey +Christoffer Buchholz +Dave Cheney +Francisco Souza +Hari haran +John C Barstow +Kelvin Fo +Nathan Youngman +Paul Hammond +Pursuit92 +Rob Figueiredo +Soge Zhang +Tilak Sharma +Travis Cline +Tudor Golubenco +Yukang +bronze1man +debrando +henrikedwards diff --git a/vendor/src/github.com/go-fsnotify/fsnotify/CHANGELOG.md b/vendor/src/github.com/go-fsnotify/fsnotify/CHANGELOG.md new file mode 100644 index 000000000..79f4ddbaa --- /dev/null +++ b/vendor/src/github.com/go-fsnotify/fsnotify/CHANGELOG.md @@ -0,0 +1,237 @@ +# Changelog + +## v1.0.4 / 2014-09-07 + +* kqueue: add dragonfly to the build tags. +* Rename source code files, rearrange code so exported APIs are at the top. +* Add done channel to example code. [#37](https://github.com/go-fsnotify/fsnotify/pull/37) (thanks @chenyukang) + +## v1.0.3 / 2014-08-19 + +* [Fix] Windows MOVED_TO now translates to Create like on BSD and Linux. [#36](https://github.com/go-fsnotify/fsnotify/issues/36) + +## v1.0.2 / 2014-08-17 + +* [Fix] Missing create events on OS X. [#14](https://github.com/go-fsnotify/fsnotify/issues/14) (thanks @zhsso) +* [Fix] Make ./path and path equivalent. (thanks @zhsso) + +## v1.0.0 / 2014-08-15 + +* [API] Remove AddWatch on Windows, use Add. +* Improve documentation for exported identifiers. [#30](https://github.com/go-fsnotify/fsnotify/issues/30) +* Minor updates based on feedback from golint. + +## dev / 2014-07-09 + +* Moved to [github.com/go-fsnotify/fsnotify](https://github.com/go-fsnotify/fsnotify). +* Use os.NewSyscallError instead of returning errno (thanks @hariharan-uno) + +## dev / 2014-07-04 + +* kqueue: fix incorrect mutex used in Close() +* Update example to demonstrate usage of Op. + +## dev / 2014-06-28 + +* [API] Don't set the Write Op for attribute notifications [#4](https://github.com/go-fsnotify/fsnotify/issues/4) +* Fix for String() method on Event (thanks Alex Brainman) +* Don't build on Plan 9 or Solaris (thanks @4ad) + +## dev / 2014-06-21 + +* Events channel of type Event rather than *Event. +* [internal] use syscall constants directly for inotify and kqueue. +* [internal] kqueue: rename events to kevents and fileEvent to event. + +## dev / 2014-06-19 + +* Go 1.3+ required on Windows (uses syscall.ERROR_MORE_DATA internally). +* [internal] remove cookie from Event struct (unused). +* [internal] Event struct has the same definition across every OS. +* [internal] remove internal watch and removeWatch methods. + +## dev / 2014-06-12 + +* [API] Renamed Watch() to Add() and RemoveWatch() to Remove(). +* [API] Pluralized channel names: Events and Errors. +* [API] Renamed FileEvent struct to Event. +* [API] Op constants replace methods like IsCreate(). + +## dev / 2014-06-12 + +* Fix data race on kevent buffer (thanks @tilaks) [#98](https://github.com/howeyc/fsnotify/pull/98) + +## dev / 2014-05-23 + +* [API] Remove current implementation of WatchFlags. + * current implementation doesn't take advantage of OS for efficiency + * provides little benefit over filtering events as they are received, but has extra bookkeeping and mutexes + * no tests for the current implementation + * not fully implemented on Windows [#93](https://github.com/howeyc/fsnotify/issues/93#issuecomment-39285195) + +## v0.9.2 / 2014-08-17 + +* [Backport] Fix missing create events on OS X. [#14](https://github.com/go-fsnotify/fsnotify/issues/14) (thanks @zhsso) + +## v0.9.1 / 2014-06-12 + +* Fix data race on kevent buffer (thanks @tilaks) [#98](https://github.com/howeyc/fsnotify/pull/98) + +## v0.9.0 / 2014-01-17 + +* IsAttrib() for events that only concern a file's metadata [#79][] (thanks @abustany) +* [Fix] kqueue: fix deadlock [#77][] (thanks @cespare) +* [NOTICE] Development has moved to `code.google.com/p/go.exp/fsnotify` in preparation for inclusion in the Go standard library. + +## v0.8.12 / 2013-11-13 + +* [API] Remove FD_SET and friends from Linux adapter + +## v0.8.11 / 2013-11-02 + +* [Doc] Add Changelog [#72][] (thanks @nathany) +* [Doc] Spotlight and double modify events on OS X [#62][] (reported by @paulhammond) + +## v0.8.10 / 2013-10-19 + +* [Fix] kqueue: remove file watches when parent directory is removed [#71][] (reported by @mdwhatcott) +* [Fix] kqueue: race between Close and readEvents [#70][] (reported by @bernerdschaefer) +* [Doc] specify OS-specific limits in README (thanks @debrando) + +## v0.8.9 / 2013-09-08 + +* [Doc] Contributing (thanks @nathany) +* [Doc] update package path in example code [#63][] (thanks @paulhammond) +* [Doc] GoCI badge in README (Linux only) [#60][] +* [Doc] Cross-platform testing with Vagrant [#59][] (thanks @nathany) + +## v0.8.8 / 2013-06-17 + +* [Fix] Windows: handle `ERROR_MORE_DATA` on Windows [#49][] (thanks @jbowtie) + +## v0.8.7 / 2013-06-03 + +* [API] Make syscall flags internal +* [Fix] inotify: ignore event changes +* [Fix] race in symlink test [#45][] (reported by @srid) +* [Fix] tests on Windows +* lower case error messages + +## v0.8.6 / 2013-05-23 + +* kqueue: Use EVT_ONLY flag on Darwin +* [Doc] Update README with full example + +## v0.8.5 / 2013-05-09 + +* [Fix] inotify: allow monitoring of "broken" symlinks (thanks @tsg) + +## v0.8.4 / 2013-04-07 + +* [Fix] kqueue: watch all file events [#40][] (thanks @ChrisBuchholz) + +## v0.8.3 / 2013-03-13 + +* [Fix] inoitfy/kqueue memory leak [#36][] (reported by @nbkolchin) +* [Fix] kqueue: use fsnFlags for watching a directory [#33][] (reported by @nbkolchin) + +## v0.8.2 / 2013-02-07 + +* [Doc] add Authors +* [Fix] fix data races for map access [#29][] (thanks @fsouza) + +## v0.8.1 / 2013-01-09 + +* [Fix] Windows path separators +* [Doc] BSD License + +## v0.8.0 / 2012-11-09 + +* kqueue: directory watching improvements (thanks @vmirage) +* inotify: add `IN_MOVED_TO` [#25][] (requested by @cpisto) +* [Fix] kqueue: deleting watched directory [#24][] (reported by @jakerr) + +## v0.7.4 / 2012-10-09 + +* [Fix] inotify: fixes from https://codereview.appspot.com/5418045/ (ugorji) +* [Fix] kqueue: preserve watch flags when watching for delete [#21][] (reported by @robfig) +* [Fix] kqueue: watch the directory even if it isn't a new watch (thanks @robfig) +* [Fix] kqueue: modify after recreation of file + +## v0.7.3 / 2012-09-27 + +* [Fix] kqueue: watch with an existing folder inside the watched folder (thanks @vmirage) +* [Fix] kqueue: no longer get duplicate CREATE events + +## v0.7.2 / 2012-09-01 + +* kqueue: events for created directories + +## v0.7.1 / 2012-07-14 + +* [Fix] for renaming files + +## v0.7.0 / 2012-07-02 + +* [Feature] FSNotify flags +* [Fix] inotify: Added file name back to event path + +## v0.6.0 / 2012-06-06 + +* kqueue: watch files after directory created (thanks @tmc) + +## v0.5.1 / 2012-05-22 + +* [Fix] inotify: remove all watches before Close() + +## v0.5.0 / 2012-05-03 + +* [API] kqueue: return errors during watch instead of sending over channel +* kqueue: match symlink behavior on Linux +* inotify: add `DELETE_SELF` (requested by @taralx) +* [Fix] kqueue: handle EINTR (reported by @robfig) +* [Doc] Godoc example [#1][] (thanks @davecheney) + +## v0.4.0 / 2012-03-30 + +* Go 1 released: build with go tool +* [Feature] Windows support using winfsnotify +* Windows does not have attribute change notifications +* Roll attribute notifications into IsModify + +## v0.3.0 / 2012-02-19 + +* kqueue: add files when watch directory + +## v0.2.0 / 2011-12-30 + +* update to latest Go weekly code + +## v0.1.0 / 2011-10-19 + +* kqueue: add watch on file creation to match inotify +* kqueue: create file event +* inotify: ignore `IN_IGNORED` events +* event String() +* linux: common FileEvent functions +* initial commit + +[#79]: https://github.com/howeyc/fsnotify/pull/79 +[#77]: https://github.com/howeyc/fsnotify/pull/77 +[#72]: https://github.com/howeyc/fsnotify/issues/72 +[#71]: https://github.com/howeyc/fsnotify/issues/71 +[#70]: https://github.com/howeyc/fsnotify/issues/70 +[#63]: https://github.com/howeyc/fsnotify/issues/63 +[#62]: https://github.com/howeyc/fsnotify/issues/62 +[#60]: https://github.com/howeyc/fsnotify/issues/60 +[#59]: https://github.com/howeyc/fsnotify/issues/59 +[#49]: https://github.com/howeyc/fsnotify/issues/49 +[#45]: https://github.com/howeyc/fsnotify/issues/45 +[#40]: https://github.com/howeyc/fsnotify/issues/40 +[#36]: https://github.com/howeyc/fsnotify/issues/36 +[#33]: https://github.com/howeyc/fsnotify/issues/33 +[#29]: https://github.com/howeyc/fsnotify/issues/29 +[#25]: https://github.com/howeyc/fsnotify/issues/25 +[#24]: https://github.com/howeyc/fsnotify/issues/24 +[#21]: https://github.com/howeyc/fsnotify/issues/21 + diff --git a/vendor/src/github.com/go-fsnotify/fsnotify/CONTRIBUTING.md b/vendor/src/github.com/go-fsnotify/fsnotify/CONTRIBUTING.md new file mode 100644 index 000000000..2fd0423cc --- /dev/null +++ b/vendor/src/github.com/go-fsnotify/fsnotify/CONTRIBUTING.md @@ -0,0 +1,56 @@ +# Contributing + +* Send questions to [golang-dev@googlegroups.com](mailto:golang-dev@googlegroups.com). + +### Issues + +* Request features and report bugs using the [GitHub Issue Tracker](https://github.com/go-fsnotify/fsnotify/issues). +* Please indicate the platform you are running on. + +### Pull Requests + +A future version of Go will have [fsnotify in the standard library](https://code.google.com/p/go/issues/detail?id=4068), therefore fsnotify carries the same [LICENSE](https://github.com/go-fsnotify/fsnotify/blob/master/LICENSE) as Go. Contributors retain their copyright, so we need you to fill out a short form before we can accept your contribution: [Google Individual Contributor License Agreement](https://developers.google.com/open-source/cla/individual). + +Please indicate that you have signed the CLA in your pull request. + +To hack on fsnotify: + +1. Install as usual (`go get -u github.com/go-fsnotify/fsnotify`) +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Ensure everything works and the tests pass (see below) +4. Commit your changes (`git commit -am 'Add some feature'`) + +Contribute upstream: + +1. Fork fsnotify on GitHub +2. Add your remote (`git remote add fork git@github.com:mycompany/repo.git`) +3. Push to the branch (`git push fork my-new-feature`) +4. Create a new Pull Request on GitHub + +If other team members need your patch before I merge it: + +1. Install as usual (`go get -u github.com/go-fsnotify/fsnotify`) +2. Add your remote (`git remote add fork git@github.com:mycompany/repo.git`) +3. Pull your revisions (`git fetch fork; git checkout -b my-new-feature fork/my-new-feature`) + +Notice: For smooth sailing, always use the original import path. Installing with `go get` makes this easy. + +Note: The maintainers will update the CHANGELOG on your behalf. Please don't modify it in your pull request. + +### Testing + +fsnotify uses build tags to compile different code on Linux, BSD, OS X, and Windows. + +Before doing a pull request, please do your best to test your changes on multiple platforms, and list which platforms you were able/unable to test on. + +To make cross-platform testing easier, I've created a Vagrantfile for Linux and BSD. + +* Install [Vagrant](http://www.vagrantup.com/) and [VirtualBox](https://www.virtualbox.org/) +* Setup [Vagrant Gopher](https://github.com/nathany/vagrant-gopher) in your `src` folder. +* Run `vagrant up` from the project folder. You can also setup just one box with `vagrant up linux` or `vagrant up bsd` (note: the BSD box doesn't support Windows hosts at this time, and NFS may prompt for your host OS password) +* Once setup, you can run the test suite on a given OS with a single command `vagrant ssh linux -c 'cd go-fsnotify/fsnotify; go test'`. +* When you're done, you will want to halt or destroy the Vagrant boxes. + +Notice: fsnotify file system events don't work on shared folders. The tests get around this limitation by using a tmp directory, but it is something to be aware of. + +Right now I don't have an equivalent solution for Windows and OS X, but there are Windows VMs [freely available from Microsoft](http://www.modern.ie/en-us/virtualization-tools#downloads). diff --git a/vendor/src/github.com/go-fsnotify/fsnotify/LICENSE b/vendor/src/github.com/go-fsnotify/fsnotify/LICENSE new file mode 100644 index 000000000..f21e54080 --- /dev/null +++ b/vendor/src/github.com/go-fsnotify/fsnotify/LICENSE @@ -0,0 +1,28 @@ +Copyright (c) 2012 The Go Authors. All rights reserved. +Copyright (c) 2012 fsnotify Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/src/github.com/go-fsnotify/fsnotify/README.md b/vendor/src/github.com/go-fsnotify/fsnotify/README.md new file mode 100644 index 000000000..075928426 --- /dev/null +++ b/vendor/src/github.com/go-fsnotify/fsnotify/README.md @@ -0,0 +1,53 @@ +# File system notifications for Go + +[![Coverage](http://gocover.io/_badge/github.com/go-fsnotify/fsnotify)](http://gocover.io/github.com/go-fsnotify/fsnotify) [![GoDoc](https://godoc.org/gopkg.in/fsnotify.v1?status.svg)](https://godoc.org/gopkg.in/fsnotify.v1) + +Cross platform: Windows, Linux, BSD and OS X. + +|Adapter |OS |Status | +|----------|----------|----------| +|inotify |Linux, Android\*|Supported| +|kqueue |BSD, OS X, iOS\*|Supported| +|ReadDirectoryChangesW|Windows|Supported| +|FSEvents |OS X |[Planned](https://github.com/go-fsnotify/fsnotify/issues/11)| +|FEN |Solaris 11 |[Planned](https://github.com/go-fsnotify/fsnotify/issues/12)| +|fanotify |Linux 2.6.37+ | | +|Polling |*All* |[Maybe](https://github.com/go-fsnotify/fsnotify/issues/9)| +| |Plan 9 | | + +\* Android and iOS are untested. + +Please see [the documentation](https://godoc.org/gopkg.in/fsnotify.v1) for usage. Consult the [Wiki](https://github.com/go-fsnotify/fsnotify/wiki) for the FAQ and further information. + +## API stability + +Two major versions of fsnotify exist. + +**[fsnotify.v1](https://gopkg.in/fsnotify.v1)** provides [a new API](https://godoc.org/gopkg.in/fsnotify.v1) based on [this design document](http://goo.gl/MrYxyA). You can import v1 with: + +```go +import "gopkg.in/fsnotify.v1" +``` + +\* Refer to the package as fsnotify (without the .v1 suffix). + +**[fsnotify.v0](https://gopkg.in/fsnotify.v0)** is API-compatible with [howeyc/fsnotify](https://godoc.org/github.com/howeyc/fsnotify). Bugfixes *may* be backported, but I recommend upgrading to v1. + +```go +import "gopkg.in/fsnotify.v0" +``` + +Further API changes are [planned](https://github.com/go-fsnotify/fsnotify/milestones), but a new major revision will be tagged, so you can depend on the v1 API. + +## Contributing + +* Send questions to [golang-dev@googlegroups.com](mailto:golang-dev@googlegroups.com). +* Request features and report bugs using the [GitHub Issue Tracker](https://github.com/go-fsnotify/fsnotify/issues). + +A future version of Go will have [fsnotify in the standard library](https://code.google.com/p/go/issues/detail?id=4068), therefore fsnotify carries the same [LICENSE](https://github.com/go-fsnotify/fsnotify/blob/master/LICENSE) as Go. Contributors retain their copyright, so we need you to fill out a short form before we can accept your contribution: [Google Individual Contributor License Agreement](https://developers.google.com/open-source/cla/individual). + +Please read [CONTRIBUTING](https://github.com/go-fsnotify/fsnotify/blob/master/CONTRIBUTING.md) before opening a pull request. + +## Example + +See [example_test.go](https://github.com/go-fsnotify/fsnotify/blob/master/example_test.go). diff --git a/vendor/src/github.com/go-fsnotify/fsnotify/example_test.go b/vendor/src/github.com/go-fsnotify/fsnotify/example_test.go new file mode 100644 index 000000000..9f2c63f47 --- /dev/null +++ b/vendor/src/github.com/go-fsnotify/fsnotify/example_test.go @@ -0,0 +1,42 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build !plan9,!solaris + +package fsnotify_test + +import ( + "log" + + "gopkg.in/fsnotify.v1" +) + +func ExampleNewWatcher() { + watcher, err := fsnotify.NewWatcher() + if err != nil { + log.Fatal(err) + } + defer watcher.Close() + + done := make(chan bool) + go func() { + for { + select { + case event := <-watcher.Events: + log.Println("event:", event) + if event.Op&fsnotify.Write == fsnotify.Write { + log.Println("modified file:", event.Name) + } + case err := <-watcher.Errors: + log.Println("error:", err) + } + } + }() + + err = watcher.Add("/tmp/foo") + if err != nil { + log.Fatal(err) + } + <-done +} diff --git a/vendor/src/github.com/go-fsnotify/fsnotify/fsnotify.go b/vendor/src/github.com/go-fsnotify/fsnotify/fsnotify.go new file mode 100644 index 000000000..7b5233f4b --- /dev/null +++ b/vendor/src/github.com/go-fsnotify/fsnotify/fsnotify.go @@ -0,0 +1,56 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build !plan9,!solaris + +// Package fsnotify provides a platform-independent interface for file system notifications. +package fsnotify + +import "fmt" + +// Event represents a single file system notification. +type Event struct { + Name string // Relative path to the file or directory. + Op Op // File operation that triggered the event. +} + +// Op describes a set of file operations. +type Op uint32 + +// These are the generalized file operations that can trigger a notification. +const ( + Create Op = 1 << iota + Write + Remove + Rename + Chmod +) + +// String returns a string representation of the event in the form +// "file: REMOVE|WRITE|..." +func (e Event) String() string { + events := "" + + if e.Op&Create == Create { + events += "|CREATE" + } + if e.Op&Remove == Remove { + events += "|REMOVE" + } + if e.Op&Write == Write { + events += "|WRITE" + } + if e.Op&Rename == Rename { + events += "|RENAME" + } + if e.Op&Chmod == Chmod { + events += "|CHMOD" + } + + if len(events) > 0 { + events = events[1:] + } + + return fmt.Sprintf("%q: %s", e.Name, events) +} diff --git a/vendor/src/github.com/go-fsnotify/fsnotify/inotify.go b/vendor/src/github.com/go-fsnotify/fsnotify/inotify.go new file mode 100644 index 000000000..f5c0aaef0 --- /dev/null +++ b/vendor/src/github.com/go-fsnotify/fsnotify/inotify.go @@ -0,0 +1,239 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build linux + +package fsnotify + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "strings" + "sync" + "syscall" + "unsafe" +) + +// Watcher watches a set of files, delivering events to a channel. +type Watcher struct { + Events chan Event + Errors chan error + mu sync.Mutex // Map access + fd int // File descriptor (as returned by the inotify_init() syscall) + watches map[string]*watch // Map of inotify watches (key: path) + paths map[int]string // Map of watched paths (key: watch descriptor) + done chan bool // Channel for sending a "quit message" to the reader goroutine + isClosed bool // Set to true when Close() is first called +} + +// NewWatcher establishes a new watcher with the underlying OS and begins waiting for events. +func NewWatcher() (*Watcher, error) { + fd, errno := syscall.InotifyInit() + if fd == -1 { + return nil, os.NewSyscallError("inotify_init", errno) + } + w := &Watcher{ + fd: fd, + watches: make(map[string]*watch), + paths: make(map[int]string), + Events: make(chan Event), + Errors: make(chan error), + done: make(chan bool, 1), + } + + go w.readEvents() + return w, nil +} + +// Close removes all watches and closes the events channel. +func (w *Watcher) Close() error { + if w.isClosed { + return nil + } + w.isClosed = true + + // Remove all watches + for name := range w.watches { + w.Remove(name) + } + + // Send "quit" message to the reader goroutine + w.done <- true + + return nil +} + +// Add starts watching the named file or directory (non-recursively). +func (w *Watcher) Add(name string) error { + name = filepath.Clean(name) + if w.isClosed { + return errors.New("inotify instance already closed") + } + + const agnosticEvents = syscall.IN_MOVED_TO | syscall.IN_MOVED_FROM | + syscall.IN_CREATE | syscall.IN_ATTRIB | syscall.IN_MODIFY | + syscall.IN_MOVE_SELF | syscall.IN_DELETE | syscall.IN_DELETE_SELF + + var flags uint32 = agnosticEvents + + w.mu.Lock() + watchEntry, found := w.watches[name] + w.mu.Unlock() + if found { + watchEntry.flags |= flags + flags |= syscall.IN_MASK_ADD + } + wd, errno := syscall.InotifyAddWatch(w.fd, name, flags) + if wd == -1 { + return os.NewSyscallError("inotify_add_watch", errno) + } + + w.mu.Lock() + w.watches[name] = &watch{wd: uint32(wd), flags: flags} + w.paths[wd] = name + w.mu.Unlock() + + return nil +} + +// Remove stops watching the the named file or directory (non-recursively). +func (w *Watcher) Remove(name string) error { + name = filepath.Clean(name) + w.mu.Lock() + defer w.mu.Unlock() + watch, ok := w.watches[name] + if !ok { + return fmt.Errorf("can't remove non-existent inotify watch for: %s", name) + } + success, errno := syscall.InotifyRmWatch(w.fd, watch.wd) + if success == -1 { + return os.NewSyscallError("inotify_rm_watch", errno) + } + delete(w.watches, name) + return nil +} + +type watch struct { + wd uint32 // Watch descriptor (as returned by the inotify_add_watch() syscall) + flags uint32 // inotify flags of this watch (see inotify(7) for the list of valid flags) +} + +// readEvents reads from the inotify file descriptor, converts the +// received events into Event objects and sends them via the Events channel +func (w *Watcher) readEvents() { + var ( + buf [syscall.SizeofInotifyEvent * 4096]byte // Buffer for a maximum of 4096 raw events + n int // Number of bytes read with read() + errno error // Syscall errno + ) + + for { + // See if there is a message on the "done" channel + select { + case <-w.done: + syscall.Close(w.fd) + close(w.Events) + close(w.Errors) + return + default: + } + + n, errno = syscall.Read(w.fd, buf[:]) + + // If EOF is received + if n == 0 { + syscall.Close(w.fd) + close(w.Events) + close(w.Errors) + return + } + + if n < 0 { + w.Errors <- os.NewSyscallError("read", errno) + continue + } + if n < syscall.SizeofInotifyEvent { + w.Errors <- errors.New("inotify: short read in readEvents()") + continue + } + + var offset uint32 + // We don't know how many events we just read into the buffer + // While the offset points to at least one whole event... + for offset <= uint32(n-syscall.SizeofInotifyEvent) { + // Point "raw" to the event in the buffer + raw := (*syscall.InotifyEvent)(unsafe.Pointer(&buf[offset])) + + mask := uint32(raw.Mask) + nameLen := uint32(raw.Len) + // If the event happened to the watched directory or the watched file, the kernel + // doesn't append the filename to the event, but we would like to always fill the + // the "Name" field with a valid filename. We retrieve the path of the watch from + // the "paths" map. + w.mu.Lock() + name := w.paths[int(raw.Wd)] + w.mu.Unlock() + if nameLen > 0 { + // Point "bytes" at the first byte of the filename + bytes := (*[syscall.PathMax]byte)(unsafe.Pointer(&buf[offset+syscall.SizeofInotifyEvent])) + // The filename is padded with NULL bytes. TrimRight() gets rid of those. + name += "/" + strings.TrimRight(string(bytes[0:nameLen]), "\000") + } + + event := newEvent(name, mask) + + // Send the events that are not ignored on the events channel + if !event.ignoreLinux(mask) { + w.Events <- event + } + + // Move to the next event in the buffer + offset += syscall.SizeofInotifyEvent + nameLen + } + } +} + +// Certain types of events can be "ignored" and not sent over the Events +// channel. Such as events marked ignore by the kernel, or MODIFY events +// against files that do not exist. +func (e *Event) ignoreLinux(mask uint32) bool { + // Ignore anything the inotify API says to ignore + if mask&syscall.IN_IGNORED == syscall.IN_IGNORED { + return true + } + + // If the event is not a DELETE or RENAME, the file must exist. + // Otherwise the event is ignored. + // *Note*: this was put in place because it was seen that a MODIFY + // event was sent after the DELETE. This ignores that MODIFY and + // assumes a DELETE will come or has come if the file doesn't exist. + if !(e.Op&Remove == Remove || e.Op&Rename == Rename) { + _, statErr := os.Lstat(e.Name) + return os.IsNotExist(statErr) + } + return false +} + +// newEvent returns an platform-independent Event based on an inotify mask. +func newEvent(name string, mask uint32) Event { + e := Event{Name: name} + if mask&syscall.IN_CREATE == syscall.IN_CREATE || mask&syscall.IN_MOVED_TO == syscall.IN_MOVED_TO { + e.Op |= Create + } + if mask&syscall.IN_DELETE_SELF == syscall.IN_DELETE_SELF || mask&syscall.IN_DELETE == syscall.IN_DELETE { + e.Op |= Remove + } + if mask&syscall.IN_MODIFY == syscall.IN_MODIFY { + e.Op |= Write + } + if mask&syscall.IN_MOVE_SELF == syscall.IN_MOVE_SELF || mask&syscall.IN_MOVED_FROM == syscall.IN_MOVED_FROM { + e.Op |= Rename + } + if mask&syscall.IN_ATTRIB == syscall.IN_ATTRIB { + e.Op |= Chmod + } + return e +} diff --git a/vendor/src/github.com/go-fsnotify/fsnotify/integration_test.go b/vendor/src/github.com/go-fsnotify/fsnotify/integration_test.go new file mode 100644 index 000000000..ad51ab60b --- /dev/null +++ b/vendor/src/github.com/go-fsnotify/fsnotify/integration_test.go @@ -0,0 +1,1120 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build !plan9,!solaris + +package fsnotify + +import ( + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "runtime" + "sync/atomic" + "testing" + "time" +) + +// An atomic counter +type counter struct { + val int32 +} + +func (c *counter) increment() { + atomic.AddInt32(&c.val, 1) +} + +func (c *counter) value() int32 { + return atomic.LoadInt32(&c.val) +} + +func (c *counter) reset() { + atomic.StoreInt32(&c.val, 0) +} + +// tempMkdir makes a temporary directory +func tempMkdir(t *testing.T) string { + dir, err := ioutil.TempDir("", "fsnotify") + if err != nil { + t.Fatalf("failed to create test directory: %s", err) + } + return dir +} + +// newWatcher initializes an fsnotify Watcher instance. +func newWatcher(t *testing.T) *Watcher { + watcher, err := NewWatcher() + if err != nil { + t.Fatalf("NewWatcher() failed: %s", err) + } + return watcher +} + +// addWatch adds a watch for a directory +func addWatch(t *testing.T, watcher *Watcher, dir string) { + if err := watcher.Add(dir); err != nil { + t.Fatalf("watcher.Add(%q) failed: %s", dir, err) + } +} + +func TestFsnotifyMultipleOperations(t *testing.T) { + watcher := newWatcher(t) + + // Receive errors on the error channel on a separate goroutine + go func() { + for err := range watcher.Errors { + t.Fatalf("error received: %s", err) + } + }() + + // Create directory to watch + testDir := tempMkdir(t) + defer os.RemoveAll(testDir) + + // Create directory that's not watched + testDirToMoveFiles := tempMkdir(t) + defer os.RemoveAll(testDirToMoveFiles) + + testFile := filepath.Join(testDir, "TestFsnotifySeq.testfile") + testFileRenamed := filepath.Join(testDirToMoveFiles, "TestFsnotifySeqRename.testfile") + + addWatch(t, watcher, testDir) + + // Receive events on the event channel on a separate goroutine + eventstream := watcher.Events + var createReceived, modifyReceived, deleteReceived, renameReceived counter + done := make(chan bool) + go func() { + for event := range eventstream { + // Only count relevant events + if event.Name == filepath.Clean(testDir) || event.Name == filepath.Clean(testFile) { + t.Logf("event received: %s", event) + if event.Op&Remove == Remove { + deleteReceived.increment() + } + if event.Op&Write == Write { + modifyReceived.increment() + } + if event.Op&Create == Create { + createReceived.increment() + } + if event.Op&Rename == Rename { + renameReceived.increment() + } + } else { + t.Logf("unexpected event received: %s", event) + } + } + done <- true + }() + + // Create a file + // This should add at least one event to the fsnotify event queue + var f *os.File + f, err := os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + t.Fatalf("creating test file failed: %s", err) + } + f.Sync() + + time.Sleep(time.Millisecond) + f.WriteString("data") + f.Sync() + f.Close() + + time.Sleep(50 * time.Millisecond) // give system time to sync write change before delete + + if err := testRename(testFile, testFileRenamed); err != nil { + t.Fatalf("rename failed: %s", err) + } + + // Modify the file outside of the watched dir + f, err = os.Open(testFileRenamed) + if err != nil { + t.Fatalf("open test renamed file failed: %s", err) + } + f.WriteString("data") + f.Sync() + f.Close() + + time.Sleep(50 * time.Millisecond) // give system time to sync write change before delete + + // Recreate the file that was moved + f, err = os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + t.Fatalf("creating test file failed: %s", err) + } + f.Close() + time.Sleep(50 * time.Millisecond) // give system time to sync write change before delete + + // We expect this event to be received almost immediately, but let's wait 500 ms to be sure + time.Sleep(500 * time.Millisecond) + cReceived := createReceived.value() + if cReceived != 2 { + t.Fatalf("incorrect number of create events received after 500 ms (%d vs %d)", cReceived, 2) + } + mReceived := modifyReceived.value() + if mReceived != 1 { + t.Fatalf("incorrect number of modify events received after 500 ms (%d vs %d)", mReceived, 1) + } + dReceived := deleteReceived.value() + rReceived := renameReceived.value() + if dReceived+rReceived != 1 { + t.Fatalf("incorrect number of rename+delete events received after 500 ms (%d vs %d)", rReceived+dReceived, 1) + } + + // Try closing the fsnotify instance + t.Log("calling Close()") + watcher.Close() + t.Log("waiting for the event channel to become closed...") + select { + case <-done: + t.Log("event channel closed") + case <-time.After(2 * time.Second): + t.Fatal("event stream was not closed after 2 seconds") + } +} + +func TestFsnotifyMultipleCreates(t *testing.T) { + watcher := newWatcher(t) + + // Receive errors on the error channel on a separate goroutine + go func() { + for err := range watcher.Errors { + t.Fatalf("error received: %s", err) + } + }() + + // Create directory to watch + testDir := tempMkdir(t) + defer os.RemoveAll(testDir) + + testFile := filepath.Join(testDir, "TestFsnotifySeq.testfile") + + addWatch(t, watcher, testDir) + + // Receive events on the event channel on a separate goroutine + eventstream := watcher.Events + var createReceived, modifyReceived, deleteReceived counter + done := make(chan bool) + go func() { + for event := range eventstream { + // Only count relevant events + if event.Name == filepath.Clean(testDir) || event.Name == filepath.Clean(testFile) { + t.Logf("event received: %s", event) + if event.Op&Remove == Remove { + deleteReceived.increment() + } + if event.Op&Create == Create { + createReceived.increment() + } + if event.Op&Write == Write { + modifyReceived.increment() + } + } else { + t.Logf("unexpected event received: %s", event) + } + } + done <- true + }() + + // Create a file + // This should add at least one event to the fsnotify event queue + var f *os.File + f, err := os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + t.Fatalf("creating test file failed: %s", err) + } + f.Sync() + + time.Sleep(time.Millisecond) + f.WriteString("data") + f.Sync() + f.Close() + + time.Sleep(50 * time.Millisecond) // give system time to sync write change before delete + + os.Remove(testFile) + + time.Sleep(50 * time.Millisecond) // give system time to sync write change before delete + + // Recreate the file + f, err = os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + t.Fatalf("creating test file failed: %s", err) + } + f.Close() + time.Sleep(50 * time.Millisecond) // give system time to sync write change before delete + + // Modify + f, err = os.OpenFile(testFile, os.O_WRONLY, 0666) + if err != nil { + t.Fatalf("creating test file failed: %s", err) + } + f.Sync() + + time.Sleep(time.Millisecond) + f.WriteString("data") + f.Sync() + f.Close() + + time.Sleep(50 * time.Millisecond) // give system time to sync write change before delete + + // Modify + f, err = os.OpenFile(testFile, os.O_WRONLY, 0666) + if err != nil { + t.Fatalf("creating test file failed: %s", err) + } + f.Sync() + + time.Sleep(time.Millisecond) + f.WriteString("data") + f.Sync() + f.Close() + + time.Sleep(50 * time.Millisecond) // give system time to sync write change before delete + + // We expect this event to be received almost immediately, but let's wait 500 ms to be sure + time.Sleep(500 * time.Millisecond) + cReceived := createReceived.value() + if cReceived != 2 { + t.Fatalf("incorrect number of create events received after 500 ms (%d vs %d)", cReceived, 2) + } + mReceived := modifyReceived.value() + if mReceived < 3 { + t.Fatalf("incorrect number of modify events received after 500 ms (%d vs atleast %d)", mReceived, 3) + } + dReceived := deleteReceived.value() + if dReceived != 1 { + t.Fatalf("incorrect number of rename+delete events received after 500 ms (%d vs %d)", dReceived, 1) + } + + // Try closing the fsnotify instance + t.Log("calling Close()") + watcher.Close() + t.Log("waiting for the event channel to become closed...") + select { + case <-done: + t.Log("event channel closed") + case <-time.After(2 * time.Second): + t.Fatal("event stream was not closed after 2 seconds") + } +} + +func TestFsnotifyDirOnly(t *testing.T) { + watcher := newWatcher(t) + + // Create directory to watch + testDir := tempMkdir(t) + defer os.RemoveAll(testDir) + + // Create a file before watching directory + // This should NOT add any events to the fsnotify event queue + testFileAlreadyExists := filepath.Join(testDir, "TestFsnotifyEventsExisting.testfile") + { + var f *os.File + f, err := os.OpenFile(testFileAlreadyExists, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + t.Fatalf("creating test file failed: %s", err) + } + f.Sync() + f.Close() + } + + addWatch(t, watcher, testDir) + + // Receive errors on the error channel on a separate goroutine + go func() { + for err := range watcher.Errors { + t.Fatalf("error received: %s", err) + } + }() + + testFile := filepath.Join(testDir, "TestFsnotifyDirOnly.testfile") + + // Receive events on the event channel on a separate goroutine + eventstream := watcher.Events + var createReceived, modifyReceived, deleteReceived counter + done := make(chan bool) + go func() { + for event := range eventstream { + // Only count relevant events + if event.Name == filepath.Clean(testDir) || event.Name == filepath.Clean(testFile) || event.Name == filepath.Clean(testFileAlreadyExists) { + t.Logf("event received: %s", event) + if event.Op&Remove == Remove { + deleteReceived.increment() + } + if event.Op&Write == Write { + modifyReceived.increment() + } + if event.Op&Create == Create { + createReceived.increment() + } + } else { + t.Logf("unexpected event received: %s", event) + } + } + done <- true + }() + + // Create a file + // This should add at least one event to the fsnotify event queue + var f *os.File + f, err := os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + t.Fatalf("creating test file failed: %s", err) + } + f.Sync() + + time.Sleep(time.Millisecond) + f.WriteString("data") + f.Sync() + f.Close() + + time.Sleep(50 * time.Millisecond) // give system time to sync write change before delete + + os.Remove(testFile) + os.Remove(testFileAlreadyExists) + + // We expect this event to be received almost immediately, but let's wait 500 ms to be sure + time.Sleep(500 * time.Millisecond) + cReceived := createReceived.value() + if cReceived != 1 { + t.Fatalf("incorrect number of create events received after 500 ms (%d vs %d)", cReceived, 1) + } + mReceived := modifyReceived.value() + if mReceived != 1 { + t.Fatalf("incorrect number of modify events received after 500 ms (%d vs %d)", mReceived, 1) + } + dReceived := deleteReceived.value() + if dReceived != 2 { + t.Fatalf("incorrect number of delete events received after 500 ms (%d vs %d)", dReceived, 2) + } + + // Try closing the fsnotify instance + t.Log("calling Close()") + watcher.Close() + t.Log("waiting for the event channel to become closed...") + select { + case <-done: + t.Log("event channel closed") + case <-time.After(2 * time.Second): + t.Fatal("event stream was not closed after 2 seconds") + } +} + +func TestFsnotifyDeleteWatchedDir(t *testing.T) { + watcher := newWatcher(t) + defer watcher.Close() + + // Create directory to watch + testDir := tempMkdir(t) + defer os.RemoveAll(testDir) + + // Create a file before watching directory + testFileAlreadyExists := filepath.Join(testDir, "TestFsnotifyEventsExisting.testfile") + { + var f *os.File + f, err := os.OpenFile(testFileAlreadyExists, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + t.Fatalf("creating test file failed: %s", err) + } + f.Sync() + f.Close() + } + + addWatch(t, watcher, testDir) + + // Add a watch for testFile + addWatch(t, watcher, testFileAlreadyExists) + + // Receive errors on the error channel on a separate goroutine + go func() { + for err := range watcher.Errors { + t.Fatalf("error received: %s", err) + } + }() + + // Receive events on the event channel on a separate goroutine + eventstream := watcher.Events + var deleteReceived counter + go func() { + for event := range eventstream { + // Only count relevant events + if event.Name == filepath.Clean(testDir) || event.Name == filepath.Clean(testFileAlreadyExists) { + t.Logf("event received: %s", event) + if event.Op&Remove == Remove { + deleteReceived.increment() + } + } else { + t.Logf("unexpected event received: %s", event) + } + } + }() + + os.RemoveAll(testDir) + + // We expect this event to be received almost immediately, but let's wait 500 ms to be sure + time.Sleep(500 * time.Millisecond) + dReceived := deleteReceived.value() + if dReceived < 2 { + t.Fatalf("did not receive at least %d delete events, received %d after 500 ms", 2, dReceived) + } +} + +func TestFsnotifySubDir(t *testing.T) { + watcher := newWatcher(t) + + // Create directory to watch + testDir := tempMkdir(t) + defer os.RemoveAll(testDir) + + testFile1 := filepath.Join(testDir, "TestFsnotifyFile1.testfile") + testSubDir := filepath.Join(testDir, "sub") + testSubDirFile := filepath.Join(testDir, "sub/TestFsnotifyFile1.testfile") + + // Receive errors on the error channel on a separate goroutine + go func() { + for err := range watcher.Errors { + t.Fatalf("error received: %s", err) + } + }() + + // Receive events on the event channel on a separate goroutine + eventstream := watcher.Events + var createReceived, deleteReceived counter + done := make(chan bool) + go func() { + for event := range eventstream { + // Only count relevant events + if event.Name == filepath.Clean(testDir) || event.Name == filepath.Clean(testSubDir) || event.Name == filepath.Clean(testFile1) { + t.Logf("event received: %s", event) + if event.Op&Create == Create { + createReceived.increment() + } + if event.Op&Remove == Remove { + deleteReceived.increment() + } + } else { + t.Logf("unexpected event received: %s", event) + } + } + done <- true + }() + + addWatch(t, watcher, testDir) + + // Create sub-directory + if err := os.Mkdir(testSubDir, 0777); err != nil { + t.Fatalf("failed to create test sub-directory: %s", err) + } + + // Create a file + var f *os.File + f, err := os.OpenFile(testFile1, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + t.Fatalf("creating test file failed: %s", err) + } + f.Sync() + f.Close() + + // Create a file (Should not see this! we are not watching subdir) + var fs *os.File + fs, err = os.OpenFile(testSubDirFile, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + t.Fatalf("creating test file failed: %s", err) + } + fs.Sync() + fs.Close() + + time.Sleep(200 * time.Millisecond) + + // Make sure receive deletes for both file and sub-directory + os.RemoveAll(testSubDir) + os.Remove(testFile1) + + // We expect this event to be received almost immediately, but let's wait 500 ms to be sure + time.Sleep(500 * time.Millisecond) + cReceived := createReceived.value() + if cReceived != 2 { + t.Fatalf("incorrect number of create events received after 500 ms (%d vs %d)", cReceived, 2) + } + dReceived := deleteReceived.value() + if dReceived != 2 { + t.Fatalf("incorrect number of delete events received after 500 ms (%d vs %d)", dReceived, 2) + } + + // Try closing the fsnotify instance + t.Log("calling Close()") + watcher.Close() + t.Log("waiting for the event channel to become closed...") + select { + case <-done: + t.Log("event channel closed") + case <-time.After(2 * time.Second): + t.Fatal("event stream was not closed after 2 seconds") + } +} + +func TestFsnotifyRename(t *testing.T) { + watcher := newWatcher(t) + + // Create directory to watch + testDir := tempMkdir(t) + defer os.RemoveAll(testDir) + + addWatch(t, watcher, testDir) + + // Receive errors on the error channel on a separate goroutine + go func() { + for err := range watcher.Errors { + t.Fatalf("error received: %s", err) + } + }() + + testFile := filepath.Join(testDir, "TestFsnotifyEvents.testfile") + testFileRenamed := filepath.Join(testDir, "TestFsnotifyEvents.testfileRenamed") + + // Receive events on the event channel on a separate goroutine + eventstream := watcher.Events + var renameReceived counter + done := make(chan bool) + go func() { + for event := range eventstream { + // Only count relevant events + if event.Name == filepath.Clean(testDir) || event.Name == filepath.Clean(testFile) || event.Name == filepath.Clean(testFileRenamed) { + if event.Op&Rename == Rename { + renameReceived.increment() + } + t.Logf("event received: %s", event) + } else { + t.Logf("unexpected event received: %s", event) + } + } + done <- true + }() + + // Create a file + // This should add at least one event to the fsnotify event queue + var f *os.File + f, err := os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + t.Fatalf("creating test file failed: %s", err) + } + f.Sync() + + f.WriteString("data") + f.Sync() + f.Close() + + // Add a watch for testFile + addWatch(t, watcher, testFile) + + if err := testRename(testFile, testFileRenamed); err != nil { + t.Fatalf("rename failed: %s", err) + } + + // We expect this event to be received almost immediately, but let's wait 500 ms to be sure + time.Sleep(500 * time.Millisecond) + if renameReceived.value() == 0 { + t.Fatal("fsnotify rename events have not been received after 500 ms") + } + + // Try closing the fsnotify instance + t.Log("calling Close()") + watcher.Close() + t.Log("waiting for the event channel to become closed...") + select { + case <-done: + t.Log("event channel closed") + case <-time.After(2 * time.Second): + t.Fatal("event stream was not closed after 2 seconds") + } + + os.Remove(testFileRenamed) +} + +func TestFsnotifyRenameToCreate(t *testing.T) { + watcher := newWatcher(t) + + // Create directory to watch + testDir := tempMkdir(t) + defer os.RemoveAll(testDir) + + // Create directory to get file + testDirFrom := tempMkdir(t) + defer os.RemoveAll(testDirFrom) + + addWatch(t, watcher, testDir) + + // Receive errors on the error channel on a separate goroutine + go func() { + for err := range watcher.Errors { + t.Fatalf("error received: %s", err) + } + }() + + testFile := filepath.Join(testDirFrom, "TestFsnotifyEvents.testfile") + testFileRenamed := filepath.Join(testDir, "TestFsnotifyEvents.testfileRenamed") + + // Receive events on the event channel on a separate goroutine + eventstream := watcher.Events + var createReceived counter + done := make(chan bool) + go func() { + for event := range eventstream { + // Only count relevant events + if event.Name == filepath.Clean(testDir) || event.Name == filepath.Clean(testFile) || event.Name == filepath.Clean(testFileRenamed) { + if event.Op&Create == Create { + createReceived.increment() + } + t.Logf("event received: %s", event) + } else { + t.Logf("unexpected event received: %s", event) + } + } + done <- true + }() + + // Create a file + // This should add at least one event to the fsnotify event queue + var f *os.File + f, err := os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + t.Fatalf("creating test file failed: %s", err) + } + f.Sync() + f.Close() + + if err := testRename(testFile, testFileRenamed); err != nil { + t.Fatalf("rename failed: %s", err) + } + + // We expect this event to be received almost immediately, but let's wait 500 ms to be sure + time.Sleep(500 * time.Millisecond) + if createReceived.value() == 0 { + t.Fatal("fsnotify create events have not been received after 500 ms") + } + + // Try closing the fsnotify instance + t.Log("calling Close()") + watcher.Close() + t.Log("waiting for the event channel to become closed...") + select { + case <-done: + t.Log("event channel closed") + case <-time.After(2 * time.Second): + t.Fatal("event stream was not closed after 2 seconds") + } + + os.Remove(testFileRenamed) +} + +func TestFsnotifyRenameToOverwrite(t *testing.T) { + switch runtime.GOOS { + case "plan9", "windows": + t.Skipf("skipping test on %q (os.Rename over existing file does not create event).", runtime.GOOS) + } + + watcher := newWatcher(t) + + // Create directory to watch + testDir := tempMkdir(t) + defer os.RemoveAll(testDir) + + // Create directory to get file + testDirFrom := tempMkdir(t) + defer os.RemoveAll(testDirFrom) + + testFile := filepath.Join(testDirFrom, "TestFsnotifyEvents.testfile") + testFileRenamed := filepath.Join(testDir, "TestFsnotifyEvents.testfileRenamed") + + // Create a file + var fr *os.File + fr, err := os.OpenFile(testFileRenamed, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + t.Fatalf("creating test file failed: %s", err) + } + fr.Sync() + fr.Close() + + addWatch(t, watcher, testDir) + + // Receive errors on the error channel on a separate goroutine + go func() { + for err := range watcher.Errors { + t.Fatalf("error received: %s", err) + } + }() + + // Receive events on the event channel on a separate goroutine + eventstream := watcher.Events + var eventReceived counter + done := make(chan bool) + go func() { + for event := range eventstream { + // Only count relevant events + if event.Name == filepath.Clean(testFileRenamed) { + eventReceived.increment() + t.Logf("event received: %s", event) + } else { + t.Logf("unexpected event received: %s", event) + } + } + done <- true + }() + + // Create a file + // This should add at least one event to the fsnotify event queue + var f *os.File + f, err = os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + t.Fatalf("creating test file failed: %s", err) + } + f.Sync() + f.Close() + + if err := testRename(testFile, testFileRenamed); err != nil { + t.Fatalf("rename failed: %s", err) + } + + // We expect this event to be received almost immediately, but let's wait 500 ms to be sure + time.Sleep(500 * time.Millisecond) + if eventReceived.value() == 0 { + t.Fatal("fsnotify events have not been received after 500 ms") + } + + // Try closing the fsnotify instance + t.Log("calling Close()") + watcher.Close() + t.Log("waiting for the event channel to become closed...") + select { + case <-done: + t.Log("event channel closed") + case <-time.After(2 * time.Second): + t.Fatal("event stream was not closed after 2 seconds") + } + + os.Remove(testFileRenamed) +} + +func TestRemovalOfWatch(t *testing.T) { + // Create directory to watch + testDir := tempMkdir(t) + defer os.RemoveAll(testDir) + + // Create a file before watching directory + testFileAlreadyExists := filepath.Join(testDir, "TestFsnotifyEventsExisting.testfile") + { + var f *os.File + f, err := os.OpenFile(testFileAlreadyExists, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + t.Fatalf("creating test file failed: %s", err) + } + f.Sync() + f.Close() + } + + watcher := newWatcher(t) + defer watcher.Close() + + addWatch(t, watcher, testDir) + if err := watcher.Remove(testDir); err != nil { + t.Fatalf("Could not remove the watch: %v\n", err) + } + + go func() { + select { + case ev := <-watcher.Events: + t.Fatalf("We received event: %v\n", ev) + case <-time.After(500 * time.Millisecond): + t.Log("No event received, as expected.") + } + }() + + time.Sleep(200 * time.Millisecond) + // Modify the file outside of the watched dir + f, err := os.Open(testFileAlreadyExists) + if err != nil { + t.Fatalf("Open test file failed: %s", err) + } + f.WriteString("data") + f.Sync() + f.Close() + if err := os.Chmod(testFileAlreadyExists, 0700); err != nil { + t.Fatalf("chmod failed: %s", err) + } + time.Sleep(400 * time.Millisecond) +} + +func TestFsnotifyAttrib(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("attributes don't work on Windows.") + } + + watcher := newWatcher(t) + + // Create directory to watch + testDir := tempMkdir(t) + defer os.RemoveAll(testDir) + + // Receive errors on the error channel on a separate goroutine + go func() { + for err := range watcher.Errors { + t.Fatalf("error received: %s", err) + } + }() + + testFile := filepath.Join(testDir, "TestFsnotifyAttrib.testfile") + + // Receive events on the event channel on a separate goroutine + eventstream := watcher.Events + // The modifyReceived counter counts IsModify events that are not IsAttrib, + // and the attribReceived counts IsAttrib events (which are also IsModify as + // a consequence). + var modifyReceived counter + var attribReceived counter + done := make(chan bool) + go func() { + for event := range eventstream { + // Only count relevant events + if event.Name == filepath.Clean(testDir) || event.Name == filepath.Clean(testFile) { + if event.Op&Write == Write { + modifyReceived.increment() + } + if event.Op&Chmod == Chmod { + attribReceived.increment() + } + t.Logf("event received: %s", event) + } else { + t.Logf("unexpected event received: %s", event) + } + } + done <- true + }() + + // Create a file + // This should add at least one event to the fsnotify event queue + var f *os.File + f, err := os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + t.Fatalf("creating test file failed: %s", err) + } + f.Sync() + + f.WriteString("data") + f.Sync() + f.Close() + + // Add a watch for testFile + addWatch(t, watcher, testFile) + + if err := os.Chmod(testFile, 0700); err != nil { + t.Fatalf("chmod failed: %s", err) + } + + // We expect this event to be received almost immediately, but let's wait 500 ms to be sure + // Creating/writing a file changes also the mtime, so IsAttrib should be set to true here + time.Sleep(500 * time.Millisecond) + if modifyReceived.value() != 0 { + t.Fatal("received an unexpected modify event when creating a test file") + } + if attribReceived.value() == 0 { + t.Fatal("fsnotify attribute events have not received after 500 ms") + } + + // Modifying the contents of the file does not set the attrib flag (although eg. the mtime + // might have been modified). + modifyReceived.reset() + attribReceived.reset() + + f, err = os.OpenFile(testFile, os.O_WRONLY, 0) + if err != nil { + t.Fatalf("reopening test file failed: %s", err) + } + + f.WriteString("more data") + f.Sync() + f.Close() + + time.Sleep(500 * time.Millisecond) + + if modifyReceived.value() != 1 { + t.Fatal("didn't receive a modify event after changing test file contents") + } + + if attribReceived.value() != 0 { + t.Fatal("did receive an unexpected attrib event after changing test file contents") + } + + modifyReceived.reset() + attribReceived.reset() + + // Doing a chmod on the file should trigger an event with the "attrib" flag set (the contents + // of the file are not changed though) + if err := os.Chmod(testFile, 0600); err != nil { + t.Fatalf("chmod failed: %s", err) + } + + time.Sleep(500 * time.Millisecond) + + if attribReceived.value() != 1 { + t.Fatal("didn't receive an attribute change after 500ms") + } + + // Try closing the fsnotify instance + t.Log("calling Close()") + watcher.Close() + t.Log("waiting for the event channel to become closed...") + select { + case <-done: + t.Log("event channel closed") + case <-time.After(1e9): + t.Fatal("event stream was not closed after 1 second") + } + + os.Remove(testFile) +} + +func TestFsnotifyClose(t *testing.T) { + watcher := newWatcher(t) + watcher.Close() + + var done int32 + go func() { + watcher.Close() + atomic.StoreInt32(&done, 1) + }() + + time.Sleep(50e6) // 50 ms + if atomic.LoadInt32(&done) == 0 { + t.Fatal("double Close() test failed: second Close() call didn't return") + } + + testDir := tempMkdir(t) + defer os.RemoveAll(testDir) + + if err := watcher.Add(testDir); err == nil { + t.Fatal("expected error on Watch() after Close(), got nil") + } +} + +func TestFsnotifyFakeSymlink(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("symlinks don't work on Windows.") + } + + watcher := newWatcher(t) + + // Create directory to watch + testDir := tempMkdir(t) + defer os.RemoveAll(testDir) + + var errorsReceived counter + // Receive errors on the error channel on a separate goroutine + go func() { + for errors := range watcher.Errors { + t.Logf("Received error: %s", errors) + errorsReceived.increment() + } + }() + + // Count the CREATE events received + var createEventsReceived, otherEventsReceived counter + go func() { + for ev := range watcher.Events { + t.Logf("event received: %s", ev) + if ev.Op&Create == Create { + createEventsReceived.increment() + } else { + otherEventsReceived.increment() + } + } + }() + + addWatch(t, watcher, testDir) + + if err := os.Symlink(filepath.Join(testDir, "zzz"), filepath.Join(testDir, "zzznew")); err != nil { + t.Fatalf("Failed to create bogus symlink: %s", err) + } + t.Logf("Created bogus symlink") + + // We expect this event to be received almost immediately, but let's wait 500 ms to be sure + time.Sleep(500 * time.Millisecond) + + // Should not be error, just no events for broken links (watching nothing) + if errorsReceived.value() > 0 { + t.Fatal("fsnotify errors have been received.") + } + if otherEventsReceived.value() > 0 { + t.Fatal("fsnotify other events received on the broken link") + } + + // Except for 1 create event (for the link itself) + if createEventsReceived.value() == 0 { + t.Fatal("fsnotify create events were not received after 500 ms") + } + if createEventsReceived.value() > 1 { + t.Fatal("fsnotify more create events received than expected") + } + + // Try closing the fsnotify instance + t.Log("calling Close()") + watcher.Close() +} + +// TestConcurrentRemovalOfWatch tests that concurrent calls to RemoveWatch do not race. +// See https://codereview.appspot.com/103300045/ +// go test -test.run=TestConcurrentRemovalOfWatch -test.cpu=1,1,1,1,1 -race +func TestConcurrentRemovalOfWatch(t *testing.T) { + if runtime.GOOS != "darwin" { + t.Skip("regression test for race only present on darwin") + } + + // Create directory to watch + testDir := tempMkdir(t) + defer os.RemoveAll(testDir) + + // Create a file before watching directory + testFileAlreadyExists := filepath.Join(testDir, "TestFsnotifyEventsExisting.testfile") + { + var f *os.File + f, err := os.OpenFile(testFileAlreadyExists, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + t.Fatalf("creating test file failed: %s", err) + } + f.Sync() + f.Close() + } + + watcher := newWatcher(t) + defer watcher.Close() + + addWatch(t, watcher, testDir) + + // Test that RemoveWatch can be invoked concurrently, with no data races. + removed1 := make(chan struct{}) + go func() { + defer close(removed1) + watcher.Remove(testDir) + }() + removed2 := make(chan struct{}) + go func() { + close(removed2) + watcher.Remove(testDir) + }() + <-removed1 + <-removed2 +} + +func testRename(file1, file2 string) error { + switch runtime.GOOS { + case "windows", "plan9": + return os.Rename(file1, file2) + default: + cmd := exec.Command("mv", file1, file2) + return cmd.Run() + } +} diff --git a/vendor/src/github.com/go-fsnotify/fsnotify/kqueue.go b/vendor/src/github.com/go-fsnotify/fsnotify/kqueue.go new file mode 100644 index 000000000..5ef1346c0 --- /dev/null +++ b/vendor/src/github.com/go-fsnotify/fsnotify/kqueue.go @@ -0,0 +1,479 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build freebsd openbsd netbsd dragonfly darwin + +package fsnotify + +import ( + "errors" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "sync" + "syscall" +) + +// Watcher watches a set of files, delivering events to a channel. +type Watcher struct { + Events chan Event + Errors chan error + mu sync.Mutex // Mutex for the Watcher itself. + kq int // File descriptor (as returned by the kqueue() syscall). + watches map[string]int // Map of watched file descriptors (key: path). + wmut sync.Mutex // Protects access to watches. + enFlags map[string]uint32 // Map of watched files to evfilt note flags used in kqueue. + enmut sync.Mutex // Protects access to enFlags. + paths map[int]string // Map of watched paths (key: watch descriptor). + finfo map[int]os.FileInfo // Map of file information (isDir, isReg; key: watch descriptor). + pmut sync.Mutex // Protects access to paths and finfo. + fileExists map[string]bool // Keep track of if we know this file exists (to stop duplicate create events). + femut sync.Mutex // Protects access to fileExists. + externalWatches map[string]bool // Map of watches added by user of the library. + ewmut sync.Mutex // Protects access to externalWatches. + done chan bool // Channel for sending a "quit message" to the reader goroutine + isClosed bool // Set to true when Close() is first called +} + +// NewWatcher establishes a new watcher with the underlying OS and begins waiting for events. +func NewWatcher() (*Watcher, error) { + fd, errno := syscall.Kqueue() + if fd == -1 { + return nil, os.NewSyscallError("kqueue", errno) + } + w := &Watcher{ + kq: fd, + watches: make(map[string]int), + enFlags: make(map[string]uint32), + paths: make(map[int]string), + finfo: make(map[int]os.FileInfo), + fileExists: make(map[string]bool), + externalWatches: make(map[string]bool), + Events: make(chan Event), + Errors: make(chan error), + done: make(chan bool, 1), + } + + go w.readEvents() + return w, nil +} + +// Close removes all watches and closes the events channel. +func (w *Watcher) Close() error { + w.mu.Lock() + if w.isClosed { + w.mu.Unlock() + return nil + } + w.isClosed = true + w.mu.Unlock() + + // Send "quit" message to the reader goroutine: + w.done <- true + w.wmut.Lock() + ws := w.watches + w.wmut.Unlock() + for name := range ws { + w.Remove(name) + } + + return nil +} + +// Add starts watching the named file or directory (non-recursively). +func (w *Watcher) Add(name string) error { + w.ewmut.Lock() + w.externalWatches[name] = true + w.ewmut.Unlock() + return w.addWatch(name, noteAllEvents) +} + +// Remove stops watching the the named file or directory (non-recursively). +func (w *Watcher) Remove(name string) error { + name = filepath.Clean(name) + w.wmut.Lock() + watchfd, ok := w.watches[name] + w.wmut.Unlock() + if !ok { + return fmt.Errorf("can't remove non-existent kevent watch for: %s", name) + } + var kbuf [1]syscall.Kevent_t + watchEntry := &kbuf[0] + syscall.SetKevent(watchEntry, watchfd, syscall.EVFILT_VNODE, syscall.EV_DELETE) + entryFlags := watchEntry.Flags + success, errno := syscall.Kevent(w.kq, kbuf[:], nil, nil) + if success == -1 { + return os.NewSyscallError("kevent_rm_watch", errno) + } else if (entryFlags & syscall.EV_ERROR) == syscall.EV_ERROR { + return errors.New("kevent rm error") + } + syscall.Close(watchfd) + w.wmut.Lock() + delete(w.watches, name) + w.wmut.Unlock() + w.enmut.Lock() + delete(w.enFlags, name) + w.enmut.Unlock() + w.pmut.Lock() + delete(w.paths, watchfd) + fInfo := w.finfo[watchfd] + delete(w.finfo, watchfd) + w.pmut.Unlock() + + // Find all watched paths that are in this directory that are not external. + if fInfo.IsDir() { + var pathsToRemove []string + w.pmut.Lock() + for _, wpath := range w.paths { + wdir, _ := filepath.Split(wpath) + if filepath.Clean(wdir) == filepath.Clean(name) { + w.ewmut.Lock() + if !w.externalWatches[wpath] { + pathsToRemove = append(pathsToRemove, wpath) + } + w.ewmut.Unlock() + } + } + w.pmut.Unlock() + for _, name := range pathsToRemove { + // Since these are internal, not much sense in propagating error + // to the user, as that will just confuse them with an error about + // a path they did not explicitly watch themselves. + w.Remove(name) + } + } + + return nil +} + +const ( + // Watch all events (except NOTE_EXTEND, NOTE_LINK, NOTE_REVOKE) + noteAllEvents = syscall.NOTE_DELETE | syscall.NOTE_WRITE | syscall.NOTE_ATTRIB | syscall.NOTE_RENAME + + // Block for 100 ms on each call to kevent + keventWaitTime = 100e6 +) + +// addWatch adds path to the watched file set. +// The flags are interpreted as described in kevent(2). +func (w *Watcher) addWatch(path string, flags uint32) error { + path = filepath.Clean(path) + w.mu.Lock() + if w.isClosed { + w.mu.Unlock() + return errors.New("kevent instance already closed") + } + w.mu.Unlock() + + watchDir := false + + w.wmut.Lock() + watchfd, found := w.watches[path] + w.wmut.Unlock() + if !found { + fi, errstat := os.Lstat(path) + if errstat != nil { + return errstat + } + + // don't watch socket + if fi.Mode()&os.ModeSocket == os.ModeSocket { + return nil + } + + // Follow Symlinks + // Unfortunately, Linux can add bogus symlinks to watch list without + // issue, and Windows can't do symlinks period (AFAIK). To maintain + // consistency, we will act like everything is fine. There will simply + // be no file events for broken symlinks. + // Hence the returns of nil on errors. + if fi.Mode()&os.ModeSymlink == os.ModeSymlink { + path, err := filepath.EvalSymlinks(path) + if err != nil { + return nil + } + + fi, errstat = os.Lstat(path) + if errstat != nil { + return nil + } + } + + fd, errno := syscall.Open(path, openMode, 0700) + if fd == -1 { + return os.NewSyscallError("Open", errno) + } + watchfd = fd + + w.wmut.Lock() + w.watches[path] = watchfd + w.wmut.Unlock() + + w.pmut.Lock() + w.paths[watchfd] = path + w.finfo[watchfd] = fi + w.pmut.Unlock() + } + // Watch the directory if it has not been watched before. + w.pmut.Lock() + w.enmut.Lock() + if w.finfo[watchfd].IsDir() && + (flags&syscall.NOTE_WRITE) == syscall.NOTE_WRITE && + (!found || (w.enFlags[path]&syscall.NOTE_WRITE) != syscall.NOTE_WRITE) { + watchDir = true + } + w.enmut.Unlock() + w.pmut.Unlock() + + w.enmut.Lock() + w.enFlags[path] = flags + w.enmut.Unlock() + + var kbuf [1]syscall.Kevent_t + watchEntry := &kbuf[0] + watchEntry.Fflags = flags + syscall.SetKevent(watchEntry, watchfd, syscall.EVFILT_VNODE, syscall.EV_ADD|syscall.EV_CLEAR) + entryFlags := watchEntry.Flags + success, errno := syscall.Kevent(w.kq, kbuf[:], nil, nil) + if success == -1 { + return errno + } else if (entryFlags & syscall.EV_ERROR) == syscall.EV_ERROR { + return errors.New("kevent add error") + } + + if watchDir { + errdir := w.watchDirectoryFiles(path) + if errdir != nil { + return errdir + } + } + return nil +} + +// readEvents reads from the kqueue file descriptor, converts the +// received events into Event objects and sends them via the Events channel +func (w *Watcher) readEvents() { + var ( + keventbuf [10]syscall.Kevent_t // Event buffer + kevents []syscall.Kevent_t // Received events + twait *syscall.Timespec // Time to block waiting for events + n int // Number of events returned from kevent + errno error // Syscall errno + ) + kevents = keventbuf[0:0] + twait = new(syscall.Timespec) + *twait = syscall.NsecToTimespec(keventWaitTime) + + for { + // See if there is a message on the "done" channel + var done bool + select { + case done = <-w.done: + default: + } + + // If "done" message is received + if done { + errno := syscall.Close(w.kq) + if errno != nil { + w.Errors <- os.NewSyscallError("close", errno) + } + close(w.Events) + close(w.Errors) + return + } + + // Get new events + if len(kevents) == 0 { + n, errno = syscall.Kevent(w.kq, nil, keventbuf[:], twait) + + // EINTR is okay, basically the syscall was interrupted before + // timeout expired. + if errno != nil && errno != syscall.EINTR { + w.Errors <- os.NewSyscallError("kevent", errno) + continue + } + + // Received some events + if n > 0 { + kevents = keventbuf[0:n] + } + } + + // Flush the events we received to the Events channel + for len(kevents) > 0 { + watchEvent := &kevents[0] + mask := uint32(watchEvent.Fflags) + w.pmut.Lock() + name := w.paths[int(watchEvent.Ident)] + fileInfo := w.finfo[int(watchEvent.Ident)] + w.pmut.Unlock() + + event := newEvent(name, mask, false) + + if fileInfo != nil && fileInfo.IsDir() && !(event.Op&Remove == Remove) { + // Double check to make sure the directory exist. This can happen when + // we do a rm -fr on a recursively watched folders and we receive a + // modification event first but the folder has been deleted and later + // receive the delete event + if _, err := os.Lstat(event.Name); os.IsNotExist(err) { + // mark is as delete event + event.Op |= Remove + } + } + + if fileInfo != nil && fileInfo.IsDir() && event.Op&Write == Write && !(event.Op&Remove == Remove) { + w.sendDirectoryChangeEvents(event.Name) + } else { + // Send the event on the Events channel + w.Events <- event + } + + // Move to next event + kevents = kevents[1:] + + if event.Op&Rename == Rename { + w.Remove(event.Name) + w.femut.Lock() + delete(w.fileExists, event.Name) + w.femut.Unlock() + } + if event.Op&Remove == Remove { + w.Remove(event.Name) + w.femut.Lock() + delete(w.fileExists, event.Name) + w.femut.Unlock() + + // Look for a file that may have overwritten this + // (ie mv f1 f2 will delete f2 then create f2) + fileDir, _ := filepath.Split(event.Name) + fileDir = filepath.Clean(fileDir) + w.wmut.Lock() + _, found := w.watches[fileDir] + w.wmut.Unlock() + if found { + // make sure the directory exist before we watch for changes. When we + // do a recursive watch and perform rm -fr, the parent directory might + // have gone missing, ignore the missing directory and let the + // upcoming delete event remove the watch form the parent folder + if _, err := os.Lstat(fileDir); !os.IsNotExist(err) { + w.sendDirectoryChangeEvents(fileDir) + } + } + } + } + } +} + +// newEvent returns an platform-independent Event based on kqueue Fflags. +func newEvent(name string, mask uint32, create bool) Event { + e := Event{Name: name} + if create { + e.Op |= Create + } + if mask&syscall.NOTE_DELETE == syscall.NOTE_DELETE { + e.Op |= Remove + } + if mask&syscall.NOTE_WRITE == syscall.NOTE_WRITE { + e.Op |= Write + } + if mask&syscall.NOTE_RENAME == syscall.NOTE_RENAME { + e.Op |= Rename + } + if mask&syscall.NOTE_ATTRIB == syscall.NOTE_ATTRIB { + e.Op |= Chmod + } + return e +} + +func (w *Watcher) watchDirectoryFiles(dirPath string) error { + // Get all files + files, err := ioutil.ReadDir(dirPath) + if err != nil { + return err + } + + // Search for new files + for _, fileInfo := range files { + filePath := filepath.Join(dirPath, fileInfo.Name()) + + if fileInfo.IsDir() == false { + // Watch file to mimic linux fsnotify + e := w.addWatch(filePath, noteAllEvents) + if e != nil { + return e + } + } else { + // If the user is currently watching directory + // we want to preserve the flags used + w.enmut.Lock() + currFlags, found := w.enFlags[filePath] + w.enmut.Unlock() + var newFlags uint32 = syscall.NOTE_DELETE + if found { + newFlags |= currFlags + } + + // Linux gives deletes if not explicitly watching + e := w.addWatch(filePath, newFlags) + if e != nil { + return e + } + } + w.femut.Lock() + w.fileExists[filePath] = true + w.femut.Unlock() + } + + return nil +} + +// sendDirectoryEvents searches the directory for newly created files +// and sends them over the event channel. This functionality is to have +// the BSD version of fsnotify match linux fsnotify which provides a +// create event for files created in a watched directory. +func (w *Watcher) sendDirectoryChangeEvents(dirPath string) { + // Get all files + files, err := ioutil.ReadDir(dirPath) + if err != nil { + w.Errors <- err + } + + // Search for new files + for _, fileInfo := range files { + filePath := filepath.Join(dirPath, fileInfo.Name()) + w.femut.Lock() + _, doesExist := w.fileExists[filePath] + w.femut.Unlock() + if !doesExist { + // Send create event (mask=0) + event := newEvent(filePath, 0, true) + w.Events <- event + } + + // watchDirectoryFiles (but without doing another ReadDir) + if fileInfo.IsDir() == false { + // Watch file to mimic linux fsnotify + w.addWatch(filePath, noteAllEvents) + } else { + // If the user is currently watching directory + // we want to preserve the flags used + w.enmut.Lock() + currFlags, found := w.enFlags[filePath] + w.enmut.Unlock() + var newFlags uint32 = syscall.NOTE_DELETE + if found { + newFlags |= currFlags + } + + // Linux gives deletes if not explicitly watching + w.addWatch(filePath, newFlags) + } + + w.femut.Lock() + w.fileExists[filePath] = true + w.femut.Unlock() + } +} diff --git a/vendor/src/github.com/go-fsnotify/fsnotify/open_mode_bsd.go b/vendor/src/github.com/go-fsnotify/fsnotify/open_mode_bsd.go new file mode 100644 index 000000000..c57ccb427 --- /dev/null +++ b/vendor/src/github.com/go-fsnotify/fsnotify/open_mode_bsd.go @@ -0,0 +1,11 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build freebsd openbsd netbsd dragonfly + +package fsnotify + +import "syscall" + +const openMode = syscall.O_NONBLOCK | syscall.O_RDONLY diff --git a/vendor/src/github.com/go-fsnotify/fsnotify/open_mode_darwin.go b/vendor/src/github.com/go-fsnotify/fsnotify/open_mode_darwin.go new file mode 100644 index 000000000..174b2c331 --- /dev/null +++ b/vendor/src/github.com/go-fsnotify/fsnotify/open_mode_darwin.go @@ -0,0 +1,12 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build darwin + +package fsnotify + +import "syscall" + +// note: this constant is not defined on BSD +const openMode = syscall.O_EVTONLY diff --git a/vendor/src/github.com/go-fsnotify/fsnotify/windows.go b/vendor/src/github.com/go-fsnotify/fsnotify/windows.go new file mode 100644 index 000000000..811585227 --- /dev/null +++ b/vendor/src/github.com/go-fsnotify/fsnotify/windows.go @@ -0,0 +1,561 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build windows + +package fsnotify + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "runtime" + "sync" + "syscall" + "unsafe" +) + +// Watcher watches a set of files, delivering events to a channel. +type Watcher struct { + Events chan Event + Errors chan error + isClosed bool // Set to true when Close() is first called + mu sync.Mutex // Map access + port syscall.Handle // Handle to completion port + watches watchMap // Map of watches (key: i-number) + input chan *input // Inputs to the reader are sent on this channel + quit chan chan<- error +} + +// NewWatcher establishes a new watcher with the underlying OS and begins waiting for events. +func NewWatcher() (*Watcher, error) { + port, e := syscall.CreateIoCompletionPort(syscall.InvalidHandle, 0, 0, 0) + if e != nil { + return nil, os.NewSyscallError("CreateIoCompletionPort", e) + } + w := &Watcher{ + port: port, + watches: make(watchMap), + input: make(chan *input, 1), + Events: make(chan Event, 50), + Errors: make(chan error), + quit: make(chan chan<- error, 1), + } + go w.readEvents() + return w, nil +} + +// Close removes all watches and closes the events channel. +func (w *Watcher) Close() error { + if w.isClosed { + return nil + } + w.isClosed = true + + // Send "quit" message to the reader goroutine + ch := make(chan error) + w.quit <- ch + if err := w.wakeupReader(); err != nil { + return err + } + return <-ch +} + +// Add starts watching the named file or directory (non-recursively). +func (w *Watcher) Add(name string) error { + if w.isClosed { + return errors.New("watcher already closed") + } + in := &input{ + op: opAddWatch, + path: filepath.Clean(name), + flags: sys_FS_ALL_EVENTS, + reply: make(chan error), + } + w.input <- in + if err := w.wakeupReader(); err != nil { + return err + } + return <-in.reply +} + +// Remove stops watching the the named file or directory (non-recursively). +func (w *Watcher) Remove(name string) error { + in := &input{ + op: opRemoveWatch, + path: filepath.Clean(name), + reply: make(chan error), + } + w.input <- in + if err := w.wakeupReader(); err != nil { + return err + } + return <-in.reply +} + +const ( + // Options for AddWatch + sys_FS_ONESHOT = 0x80000000 + sys_FS_ONLYDIR = 0x1000000 + + // Events + sys_FS_ACCESS = 0x1 + sys_FS_ALL_EVENTS = 0xfff + sys_FS_ATTRIB = 0x4 + sys_FS_CLOSE = 0x18 + sys_FS_CREATE = 0x100 + sys_FS_DELETE = 0x200 + sys_FS_DELETE_SELF = 0x400 + sys_FS_MODIFY = 0x2 + sys_FS_MOVE = 0xc0 + sys_FS_MOVED_FROM = 0x40 + sys_FS_MOVED_TO = 0x80 + sys_FS_MOVE_SELF = 0x800 + + // Special events + sys_FS_IGNORED = 0x8000 + sys_FS_Q_OVERFLOW = 0x4000 +) + +func newEvent(name string, mask uint32) Event { + e := Event{Name: name} + if mask&sys_FS_CREATE == sys_FS_CREATE || mask&sys_FS_MOVED_TO == sys_FS_MOVED_TO { + e.Op |= Create + } + if mask&sys_FS_DELETE == sys_FS_DELETE || mask&sys_FS_DELETE_SELF == sys_FS_DELETE_SELF { + e.Op |= Remove + } + if mask&sys_FS_MODIFY == sys_FS_MODIFY { + e.Op |= Write + } + if mask&sys_FS_MOVE == sys_FS_MOVE || mask&sys_FS_MOVE_SELF == sys_FS_MOVE_SELF || mask&sys_FS_MOVED_FROM == sys_FS_MOVED_FROM { + e.Op |= Rename + } + if mask&sys_FS_ATTRIB == sys_FS_ATTRIB { + e.Op |= Chmod + } + return e +} + +const ( + opAddWatch = iota + opRemoveWatch +) + +const ( + provisional uint64 = 1 << (32 + iota) +) + +type input struct { + op int + path string + flags uint32 + reply chan error +} + +type inode struct { + handle syscall.Handle + volume uint32 + index uint64 +} + +type watch struct { + ov syscall.Overlapped + ino *inode // i-number + path string // Directory path + mask uint64 // Directory itself is being watched with these notify flags + names map[string]uint64 // Map of names being watched and their notify flags + rename string // Remembers the old name while renaming a file + buf [4096]byte +} + +type indexMap map[uint64]*watch +type watchMap map[uint32]indexMap + +func (w *Watcher) wakeupReader() error { + e := syscall.PostQueuedCompletionStatus(w.port, 0, 0, nil) + if e != nil { + return os.NewSyscallError("PostQueuedCompletionStatus", e) + } + return nil +} + +func getDir(pathname string) (dir string, err error) { + attr, e := syscall.GetFileAttributes(syscall.StringToUTF16Ptr(pathname)) + if e != nil { + return "", os.NewSyscallError("GetFileAttributes", e) + } + if attr&syscall.FILE_ATTRIBUTE_DIRECTORY != 0 { + dir = pathname + } else { + dir, _ = filepath.Split(pathname) + dir = filepath.Clean(dir) + } + return +} + +func getIno(path string) (ino *inode, err error) { + h, e := syscall.CreateFile(syscall.StringToUTF16Ptr(path), + syscall.FILE_LIST_DIRECTORY, + syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE, + nil, syscall.OPEN_EXISTING, + syscall.FILE_FLAG_BACKUP_SEMANTICS|syscall.FILE_FLAG_OVERLAPPED, 0) + if e != nil { + return nil, os.NewSyscallError("CreateFile", e) + } + var fi syscall.ByHandleFileInformation + if e = syscall.GetFileInformationByHandle(h, &fi); e != nil { + syscall.CloseHandle(h) + return nil, os.NewSyscallError("GetFileInformationByHandle", e) + } + ino = &inode{ + handle: h, + volume: fi.VolumeSerialNumber, + index: uint64(fi.FileIndexHigh)<<32 | uint64(fi.FileIndexLow), + } + return ino, nil +} + +// Must run within the I/O thread. +func (m watchMap) get(ino *inode) *watch { + if i := m[ino.volume]; i != nil { + return i[ino.index] + } + return nil +} + +// Must run within the I/O thread. +func (m watchMap) set(ino *inode, watch *watch) { + i := m[ino.volume] + if i == nil { + i = make(indexMap) + m[ino.volume] = i + } + i[ino.index] = watch +} + +// Must run within the I/O thread. +func (w *Watcher) addWatch(pathname string, flags uint64) error { + dir, err := getDir(pathname) + if err != nil { + return err + } + if flags&sys_FS_ONLYDIR != 0 && pathname != dir { + return nil + } + ino, err := getIno(dir) + if err != nil { + return err + } + w.mu.Lock() + watchEntry := w.watches.get(ino) + w.mu.Unlock() + if watchEntry == nil { + if _, e := syscall.CreateIoCompletionPort(ino.handle, w.port, 0, 0); e != nil { + syscall.CloseHandle(ino.handle) + return os.NewSyscallError("CreateIoCompletionPort", e) + } + watchEntry = &watch{ + ino: ino, + path: dir, + names: make(map[string]uint64), + } + w.mu.Lock() + w.watches.set(ino, watchEntry) + w.mu.Unlock() + flags |= provisional + } else { + syscall.CloseHandle(ino.handle) + } + if pathname == dir { + watchEntry.mask |= flags + } else { + watchEntry.names[filepath.Base(pathname)] |= flags + } + if err = w.startRead(watchEntry); err != nil { + return err + } + if pathname == dir { + watchEntry.mask &= ^provisional + } else { + watchEntry.names[filepath.Base(pathname)] &= ^provisional + } + return nil +} + +// Must run within the I/O thread. +func (w *Watcher) remWatch(pathname string) error { + dir, err := getDir(pathname) + if err != nil { + return err + } + ino, err := getIno(dir) + if err != nil { + return err + } + w.mu.Lock() + watch := w.watches.get(ino) + w.mu.Unlock() + if watch == nil { + return fmt.Errorf("can't remove non-existent watch for: %s", pathname) + } + if pathname == dir { + w.sendEvent(watch.path, watch.mask&sys_FS_IGNORED) + watch.mask = 0 + } else { + name := filepath.Base(pathname) + w.sendEvent(watch.path+"\\"+name, watch.names[name]&sys_FS_IGNORED) + delete(watch.names, name) + } + return w.startRead(watch) +} + +// Must run within the I/O thread. +func (w *Watcher) deleteWatch(watch *watch) { + for name, mask := range watch.names { + if mask&provisional == 0 { + w.sendEvent(watch.path+"\\"+name, mask&sys_FS_IGNORED) + } + delete(watch.names, name) + } + if watch.mask != 0 { + if watch.mask&provisional == 0 { + w.sendEvent(watch.path, watch.mask&sys_FS_IGNORED) + } + watch.mask = 0 + } +} + +// Must run within the I/O thread. +func (w *Watcher) startRead(watch *watch) error { + if e := syscall.CancelIo(watch.ino.handle); e != nil { + w.Errors <- os.NewSyscallError("CancelIo", e) + w.deleteWatch(watch) + } + mask := toWindowsFlags(watch.mask) + for _, m := range watch.names { + mask |= toWindowsFlags(m) + } + if mask == 0 { + if e := syscall.CloseHandle(watch.ino.handle); e != nil { + w.Errors <- os.NewSyscallError("CloseHandle", e) + } + w.mu.Lock() + delete(w.watches[watch.ino.volume], watch.ino.index) + w.mu.Unlock() + return nil + } + e := syscall.ReadDirectoryChanges(watch.ino.handle, &watch.buf[0], + uint32(unsafe.Sizeof(watch.buf)), false, mask, nil, &watch.ov, 0) + if e != nil { + err := os.NewSyscallError("ReadDirectoryChanges", e) + if e == syscall.ERROR_ACCESS_DENIED && watch.mask&provisional == 0 { + // Watched directory was probably removed + if w.sendEvent(watch.path, watch.mask&sys_FS_DELETE_SELF) { + if watch.mask&sys_FS_ONESHOT != 0 { + watch.mask = 0 + } + } + err = nil + } + w.deleteWatch(watch) + w.startRead(watch) + return err + } + return nil +} + +// readEvents reads from the I/O completion port, converts the +// received events into Event objects and sends them via the Events channel. +// Entry point to the I/O thread. +func (w *Watcher) readEvents() { + var ( + n, key uint32 + ov *syscall.Overlapped + ) + runtime.LockOSThread() + + for { + e := syscall.GetQueuedCompletionStatus(w.port, &n, &key, &ov, syscall.INFINITE) + watch := (*watch)(unsafe.Pointer(ov)) + + if watch == nil { + select { + case ch := <-w.quit: + w.mu.Lock() + var indexes []indexMap + for _, index := range w.watches { + indexes = append(indexes, index) + } + w.mu.Unlock() + for _, index := range indexes { + for _, watch := range index { + w.deleteWatch(watch) + w.startRead(watch) + } + } + var err error + if e := syscall.CloseHandle(w.port); e != nil { + err = os.NewSyscallError("CloseHandle", e) + } + close(w.Events) + close(w.Errors) + ch <- err + return + case in := <-w.input: + switch in.op { + case opAddWatch: + in.reply <- w.addWatch(in.path, uint64(in.flags)) + case opRemoveWatch: + in.reply <- w.remWatch(in.path) + } + default: + } + continue + } + + switch e { + case syscall.ERROR_MORE_DATA: + if watch == nil { + w.Errors <- errors.New("ERROR_MORE_DATA has unexpectedly null lpOverlapped buffer") + } else { + // The i/o succeeded but the buffer is full. + // In theory we should be building up a full packet. + // In practice we can get away with just carrying on. + n = uint32(unsafe.Sizeof(watch.buf)) + } + case syscall.ERROR_ACCESS_DENIED: + // Watched directory was probably removed + w.sendEvent(watch.path, watch.mask&sys_FS_DELETE_SELF) + w.deleteWatch(watch) + w.startRead(watch) + continue + case syscall.ERROR_OPERATION_ABORTED: + // CancelIo was called on this handle + continue + default: + w.Errors <- os.NewSyscallError("GetQueuedCompletionPort", e) + continue + case nil: + } + + var offset uint32 + for { + if n == 0 { + w.Events <- newEvent("", sys_FS_Q_OVERFLOW) + w.Errors <- errors.New("short read in readEvents()") + break + } + + // Point "raw" to the event in the buffer + raw := (*syscall.FileNotifyInformation)(unsafe.Pointer(&watch.buf[offset])) + buf := (*[syscall.MAX_PATH]uint16)(unsafe.Pointer(&raw.FileName)) + name := syscall.UTF16ToString(buf[:raw.FileNameLength/2]) + fullname := watch.path + "\\" + name + + var mask uint64 + switch raw.Action { + case syscall.FILE_ACTION_REMOVED: + mask = sys_FS_DELETE_SELF + case syscall.FILE_ACTION_MODIFIED: + mask = sys_FS_MODIFY + case syscall.FILE_ACTION_RENAMED_OLD_NAME: + watch.rename = name + case syscall.FILE_ACTION_RENAMED_NEW_NAME: + if watch.names[watch.rename] != 0 { + watch.names[name] |= watch.names[watch.rename] + delete(watch.names, watch.rename) + mask = sys_FS_MOVE_SELF + } + } + + sendNameEvent := func() { + if w.sendEvent(fullname, watch.names[name]&mask) { + if watch.names[name]&sys_FS_ONESHOT != 0 { + delete(watch.names, name) + } + } + } + if raw.Action != syscall.FILE_ACTION_RENAMED_NEW_NAME { + sendNameEvent() + } + if raw.Action == syscall.FILE_ACTION_REMOVED { + w.sendEvent(fullname, watch.names[name]&sys_FS_IGNORED) + delete(watch.names, name) + } + if w.sendEvent(fullname, watch.mask&toFSnotifyFlags(raw.Action)) { + if watch.mask&sys_FS_ONESHOT != 0 { + watch.mask = 0 + } + } + if raw.Action == syscall.FILE_ACTION_RENAMED_NEW_NAME { + fullname = watch.path + "\\" + watch.rename + sendNameEvent() + } + + // Move to the next event in the buffer + if raw.NextEntryOffset == 0 { + break + } + offset += raw.NextEntryOffset + + // Error! + if offset >= n { + w.Errors <- errors.New("Windows system assumed buffer larger than it is, events have likely been missed.") + break + } + } + + if err := w.startRead(watch); err != nil { + w.Errors <- err + } + } +} + +func (w *Watcher) sendEvent(name string, mask uint64) bool { + if mask == 0 { + return false + } + event := newEvent(name, uint32(mask)) + select { + case ch := <-w.quit: + w.quit <- ch + case w.Events <- event: + } + return true +} + +func toWindowsFlags(mask uint64) uint32 { + var m uint32 + if mask&sys_FS_ACCESS != 0 { + m |= syscall.FILE_NOTIFY_CHANGE_LAST_ACCESS + } + if mask&sys_FS_MODIFY != 0 { + m |= syscall.FILE_NOTIFY_CHANGE_LAST_WRITE + } + if mask&sys_FS_ATTRIB != 0 { + m |= syscall.FILE_NOTIFY_CHANGE_ATTRIBUTES + } + if mask&(sys_FS_MOVE|sys_FS_CREATE|sys_FS_DELETE) != 0 { + m |= syscall.FILE_NOTIFY_CHANGE_FILE_NAME | syscall.FILE_NOTIFY_CHANGE_DIR_NAME + } + return m +} + +func toFSnotifyFlags(action uint32) uint64 { + switch action { + case syscall.FILE_ACTION_ADDED: + return sys_FS_CREATE + case syscall.FILE_ACTION_REMOVED: + return sys_FS_DELETE + case syscall.FILE_ACTION_MODIFIED: + return sys_FS_MODIFY + case syscall.FILE_ACTION_RENAMED_OLD_NAME: + return sys_FS_MOVED_FROM + case syscall.FILE_ACTION_RENAMED_NEW_NAME: + return sys_FS_MOVED_TO + } + return 0 +} From 44cab4a4ff05cb1e499fba19394aadb20fc887b8 Mon Sep 17 00:00:00 2001 From: Phil Estes Date: Wed, 19 Nov 2014 14:50:16 -0500 Subject: [PATCH 110/513] Add "OOM killed" event based on OOM state information Docker-DCO-1.1-Signed-off-by: Phil Estes (github: estesp) --- daemon/monitor.go | 6 ++++++ docs/sources/reference/api/docker_remote_api_v1.17.md | 2 +- docs/sources/reference/commandline/cli.md | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/daemon/monitor.go b/daemon/monitor.go index 12a699633..1b5c4f624 100644 --- a/daemon/monitor.go +++ b/daemon/monitor.go @@ -154,6 +154,9 @@ func (m *containerMonitor) Start() error { if m.shouldRestart(exitStatus.ExitCode) { m.container.SetRestarting(&exitStatus) + if exitStatus.OOMKilled { + m.container.LogEvent("oom") + } m.container.LogEvent("die") m.resetContainer(true) @@ -170,6 +173,9 @@ func (m *containerMonitor) Start() error { continue } m.container.ExitCode = exitStatus.ExitCode + if exitStatus.OOMKilled { + m.container.LogEvent("oom") + } m.container.LogEvent("die") m.resetContainer(true) return err diff --git a/docs/sources/reference/api/docker_remote_api_v1.17.md b/docs/sources/reference/api/docker_remote_api_v1.17.md index 6c544c96b..1ffb1cbf9 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.17.md +++ b/docs/sources/reference/api/docker_remote_api_v1.17.md @@ -1375,7 +1375,7 @@ polling (using since). Docker containers will report the following events: - create, destroy, die, export, kill, pause, restart, start, stop, unpause + create, destroy, die, export, kill, oom, pause, restart, start, stop, unpause and Docker images will report: diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index f31f1bddd..379871fe5 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -761,7 +761,7 @@ For example: Docker containers will report the following events: - create, destroy, die, export, kill, pause, restart, start, stop, unpause + create, destroy, die, export, kill, oom, pause, restart, start, stop, unpause and Docker images will report: From bbb92e1436b5a288d07ef8ea0fad80f2c4715faa Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Wed, 24 Dec 2014 00:03:24 +0000 Subject: [PATCH 111/513] add docs Signed-off-by: Victor Vieux --- daemon/exec.go | 2 +- docs/sources/reference/api/docker_remote_api.md | 5 +++++ docs/sources/reference/api/docker_remote_api_v1.17.md | 3 +++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/daemon/exec.go b/daemon/exec.go index 92f23f738..616093c67 100644 --- a/daemon/exec.go +++ b/daemon/exec.go @@ -64,7 +64,7 @@ func (e *execStore) Delete(id string) { func (e *execStore) List() []string { var IDs []string e.RLock() - for id, _ := range e.s { + for id := range e.s { IDs = append(IDs, id) } e.RUnlock() diff --git a/docs/sources/reference/api/docker_remote_api.md b/docs/sources/reference/api/docker_remote_api.md index e44576cea..fa210d458 100644 --- a/docs/sources/reference/api/docker_remote_api.md +++ b/docs/sources/reference/api/docker_remote_api.md @@ -51,6 +51,11 @@ You can still call an old version of the API using **New!** Docker client now hints potential proxies about connection hijacking using HTTP Upgrade headers. +`GET /containers/(id)/json` + +**New!** +This endpoint now returns the list current execs associated with the container (`ExecIDs`). + ## v1.16 ### Full Documentation diff --git a/docs/sources/reference/api/docker_remote_api_v1.17.md b/docs/sources/reference/api/docker_remote_api_v1.17.md index 6c544c96b..772ba116e 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.17.md +++ b/docs/sources/reference/api/docker_remote_api_v1.17.md @@ -310,6 +310,9 @@ Return low-level information on the container `id` "SysInitPath": "/home/kitty/go/src/github.com/docker/docker/bin/docker", "ResolvConfPath": "/etc/resolv.conf", "Volumes": {}, + "ExecIDs": [ + "15f211491dced6a353a2e0f37fe3f3692ee2370a4782418e9bf7052865c10fde" + ], "HostConfig": { "Binds": null, "ContainerIDFile": "", From 6c7204393be3cd66c0bc0779d304323e8f8ac6c3 Mon Sep 17 00:00:00 2001 From: Arnaud Porterie Date: Tue, 23 Dec 2014 21:57:14 -0800 Subject: [PATCH 112/513] Fix indentation Signed-off-by: Arnaud Porterie --- project/make/.integration-daemon-start | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/project/make/.integration-daemon-start b/project/make/.integration-daemon-start index 3c796399b..f00bb6331 100644 --- a/project/make/.integration-daemon-start +++ b/project/make/.integration-daemon-start @@ -16,13 +16,13 @@ DOCKER_GRAPHDRIVER=${DOCKER_GRAPHDRIVER:-vfs} DOCKER_EXECDRIVER=${DOCKER_EXECDRIVER:-native} if [ -z "$DOCKER_TEST_HOST" ]; then - ( set -x; exec \ - docker --daemon --debug \ - --storage-driver "$DOCKER_GRAPHDRIVER" \ - --exec-driver "$DOCKER_EXECDRIVER" \ - --pidfile "$DEST/docker.pid" \ - &> "$DEST/docker.log" - ) & + ( set -x; exec \ + docker --daemon --debug \ + --storage-driver "$DOCKER_GRAPHDRIVER" \ + --exec-driver "$DOCKER_EXECDRIVER" \ + --pidfile "$DEST/docker.pid" \ + &> "$DEST/docker.log" + ) & else - export DOCKER_HOST="$DOCKER_TEST_HOST" + export DOCKER_HOST="$DOCKER_TEST_HOST" fi From 179e9deb1abcd1aa8e536aa314f46ea403fb89ba Mon Sep 17 00:00:00 2001 From: Tianon Gravi Date: Wed, 24 Dec 2014 00:12:27 -0700 Subject: [PATCH 113/513] Adjust Dockerfile style to be more consistent Signed-off-by: Andrew "Tianon" Page --- Dockerfile | 76 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 45 insertions(+), 31 deletions(-) diff --git a/Dockerfile b/Dockerfile index 67e8d8d5a..86130c4ca 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,11 +23,11 @@ # the case. Therefore, you don't have to disable it anymore. # -FROM ubuntu:14.04 -MAINTAINER Tianon Gravi (@tianon) +FROM ubuntu:14.04 +MAINTAINER Tianon Gravi (@tianon) # Packaged dependencies -RUN apt-get update && apt-get install -y \ +RUN apt-get update && apt-get install -y \ aufs-tools \ automake \ btrfs-tools \ @@ -52,72 +52,86 @@ RUN apt-get update && apt-get install -y \ --no-install-recommends # Get lvm2 source for compiling statically -RUN git clone --no-checkout https://git.fedorahosted.org/git/lvm2.git /usr/local/lvm2 && cd /usr/local/lvm2 && git checkout -q v2_02_103 +RUN git clone -b v2_02_103 https://git.fedorahosted.org/git/lvm2.git /usr/local/lvm2 # see https://git.fedorahosted.org/cgit/lvm2.git/refs/tags for release tags -# note: we don't use "git clone -b" above because it then spews big nasty warnings about 'detached HEAD' state that we can't silence as easily as we can silence them using "git checkout" directly # Compile and install lvm2 -RUN cd /usr/local/lvm2 && ./configure --enable-static_link && make device-mapper && make install_device-mapper +RUN cd /usr/local/lvm2 \ + && ./configure --enable-static_link \ + && make device-mapper \ + && make install_device-mapper # see https://git.fedorahosted.org/cgit/lvm2.git/tree/INSTALL # Install Go -RUN curl -sSL https://golang.org/dl/go1.4.src.tar.gz | tar -v -C /usr/local -xz -ENV PATH /usr/local/go/bin:$PATH -ENV GOPATH /go:/go/src/github.com/docker/docker/vendor +RUN curl -sSL https://golang.org/dl/go1.4.src.tar.gz | tar -v -C /usr/local -xz +ENV PATH /usr/local/go/bin:$PATH +ENV GOPATH /go:/go/src/github.com/docker/docker/vendor ENV PATH /go/bin:$PATH -RUN cd /usr/local/go/src && ./make.bash --no-clean 2>&1 +RUN cd /usr/local/go/src && ./make.bash --no-clean 2>&1 # Compile Go for cross compilation -ENV DOCKER_CROSSPLATFORMS \ +ENV DOCKER_CROSSPLATFORMS \ linux/386 linux/arm \ darwin/amd64 darwin/386 \ freebsd/amd64 freebsd/386 freebsd/arm \ windows/amd64 windows/386 # (set an explicit GOARM of 5 for maximum compatibility) -ENV GOARM 5 -RUN cd /usr/local/go/src && bash -xc 'for platform in $DOCKER_CROSSPLATFORMS; do GOOS=${platform%/*} GOARCH=${platform##*/} ./make.bash --no-clean 2>&1; done' +ENV GOARM 5 +RUN cd /usr/local/go/src \ + && set -x \ + && for platform in $DOCKER_CROSSPLATFORMS; do \ + GOOS=${platform%/*} \ + GOARCH=${platform##*/} \ + ./make.bash --no-clean 2>&1; \ + done # reinstall standard library with netgo RUN go clean -i net && go install -tags netgo std # Grab Go's cover tool for dead-simple code coverage testing -RUN go get golang.org/x/tools/cmd/cover +RUN go get golang.org/x/tools/cmd/cover # TODO replace FPM with some very minimal debhelper stuff -RUN gem install --no-rdoc --no-ri fpm --version 1.3.2 - -# Install man page generator -RUN mkdir -p /go/src/github.com/cpuguy83 \ - && git clone -b v1 https://github.com/cpuguy83/go-md2man.git /go/src/github.com/cpuguy83/go-md2man \ - && cd /go/src/github.com/cpuguy83/go-md2man \ - && go get -v ./... +RUN gem install --no-rdoc --no-ri fpm --version 1.3.2 # Get the "busybox" image source so we can build locally instead of pulling -RUN git clone -b buildroot-2014.02 https://github.com/jpetazzo/docker-busybox.git /docker-busybox +RUN git clone -b buildroot-2014.02 https://github.com/jpetazzo/docker-busybox.git /docker-busybox # Get the "cirros" image source so we can import it instead of fetching it during tests -RUN curl -sSL -o /cirros.tar.gz https://github.com/ewindisch/docker-cirros/raw/1cded459668e8b9dbf4ef976c94c05add9bbd8e9/cirros-0.3.0-x86_64-lxc.tar.gz +RUN curl -sSL -o /cirros.tar.gz https://github.com/ewindisch/docker-cirros/raw/1cded459668e8b9dbf4ef976c94c05add9bbd8e9/cirros-0.3.0-x86_64-lxc.tar.gz # Get the "docker-py" source so we can run their integration tests -RUN git clone -b 0.7.0 https://github.com/docker/docker-py.git /docker-py +RUN git clone -b 0.7.0 https://github.com/docker/docker-py.git /docker-py # Setup s3cmd config -RUN /bin/echo -e '[default]\naccess_key=$AWS_ACCESS_KEY\nsecret_key=$AWS_SECRET_KEY' > $HOME/.s3cfg +RUN { \ + echo '[default]'; \ + echo 'access_key=$AWS_ACCESS_KEY'; \ + echo 'secret_key=$AWS_SECRET_KEY'; \ + } > ~/.s3cfg # Set user.email so crosbymichael's in-container merge commits go smoothly -RUN git config --global user.email 'docker-dummy@example.com' +RUN git config --global user.email 'docker-dummy@example.com' # Add an unprivileged user to be used for tests which need it RUN groupadd -r docker RUN useradd --create-home --gid docker unprivilegeduser -VOLUME /var/lib/docker -WORKDIR /go/src/github.com/docker/docker -ENV DOCKER_BUILDTAGS apparmor selinux btrfs_noversion +VOLUME /var/lib/docker +WORKDIR /go/src/github.com/docker/docker +ENV DOCKER_BUILDTAGS apparmor selinux btrfs_noversion + +# Install man page generator +COPY vendor /go/src/github.com/docker/docker/vendor +# (copy vendor/ because go-md2man needs golang.org/x/net) +RUN set -x \ + && git clone -b v1 https://github.com/cpuguy83/go-md2man.git /go/src/github.com/cpuguy83/go-md2man \ + && git clone -b v1.2 https://github.com/russross/blackfriday.git /go/src/github.com/russross/blackfriday \ + && go install -v github.com/cpuguy83/go-md2man # Wrap all commands in the "docker-in-docker" script to allow nested containers -ENTRYPOINT ["hack/dind"] +ENTRYPOINT ["hack/dind"] # Upload docker source -COPY . /go/src/github.com/docker/docker +COPY . /go/src/github.com/docker/docker From f91650376a8e8af883790aa777e6ec9185aff461 Mon Sep 17 00:00:00 2001 From: Abin Shahab Date: Thu, 25 Dec 2014 17:40:31 +0000 Subject: [PATCH 114/513] LXC TEMPLATE ALLOWS IPV4 OVERRIDE This fixes the issue where an lxc.conf override of lxc.network.ipv4 was not being honored. Docker-DCO-1.1-Signed-off-by: Abin Shahab (github: ashahab-altiscale) --- daemon/execdriver/lxc/lxc_template.go | 39 ++++++------ .../execdriver/lxc/lxc_template_unit_test.go | 62 +++++++++++++++++++ 2 files changed, 83 insertions(+), 18 deletions(-) diff --git a/daemon/execdriver/lxc/lxc_template.go b/daemon/execdriver/lxc/lxc_template.go index 9402c0697..5f0294ea1 100644 --- a/daemon/execdriver/lxc/lxc_template.go +++ b/daemon/execdriver/lxc/lxc_template.go @@ -16,12 +16,6 @@ lxc.network.type = veth lxc.network.link = {{.Network.Interface.Bridge}} lxc.network.name = eth0 lxc.network.mtu = {{.Network.Mtu}} -{{if .Network.Interface.IPAddress}} -lxc.network.ipv4 = {{.Network.Interface.IPAddress}}/{{.Network.Interface.IPPrefixLen}} -{{end}} -{{if .Network.Interface.Gateway}} -lxc.network.ipv4.gateway = {{.Network.Interface.Gateway}} -{{end}} lxc.network.flags = up {{else if .Network.HostNetworking}} lxc.network.type = none @@ -86,18 +80,6 @@ lxc.mount.entry = {{$value.Source}} {{escapeFstabSpaces $ROOTFS}}/{{escapeFstabS {{end}} {{end}} -{{if .ProcessConfig.Env}} -lxc.utsname = {{getHostname .ProcessConfig.Env}} -{{end}} - -{{if .ProcessConfig.Privileged}} -# No cap values are needed, as lxc is starting in privileged mode -{{else}} -{{range $value := keepCapabilities .CapAdd .CapDrop}} -lxc.cap.keep = {{$value}} -{{end}} -{{end}} - {{if .ProcessConfig.Privileged}} {{if .AppArmor}} lxc.aa_profile = unconfined @@ -128,6 +110,27 @@ lxc.cgroup.cpuset.cpus = {{.Resources.Cpuset}} lxc.{{$value}} {{end}} {{end}} + +{{if .Network.Interface}} +{{if .Network.Interface.IPAddress}} +lxc.network.ipv4 = {{.Network.Interface.IPAddress}}/{{.Network.Interface.IPPrefixLen}} +{{end}} +{{if .Network.Interface.Gateway}} +lxc.network.ipv4.gateway = {{.Network.Interface.Gateway}} +{{end}} + +{{if .ProcessConfig.Env}} +lxc.utsname = {{getHostname .ProcessConfig.Env}} +{{end}} + +{{if .ProcessConfig.Privileged}} +# No cap values are needed, as lxc is starting in privileged mode +{{else}} +{{range $value := keepCapabilities .CapAdd .CapDrop}} +lxc.cap.keep = {{$value}} +{{end}} +{{end}} +{{end}} ` var LxcTemplateCompiled *template.Template diff --git a/daemon/execdriver/lxc/lxc_template_unit_test.go b/daemon/execdriver/lxc/lxc_template_unit_test.go index 77435114f..f1410db77 100644 --- a/daemon/execdriver/lxc/lxc_template_unit_test.go +++ b/daemon/execdriver/lxc/lxc_template_unit_test.go @@ -300,3 +300,65 @@ func TestCustomLxcConfigMisc(t *testing.T) { grepFileWithReverse(t, p, fmt.Sprintf("lxc.cap.keep = kill"), true) grepFileWithReverse(t, p, fmt.Sprintf("lxc.cap.keep = mknod"), true) } + +func TestCustomLxcConfigMiscOverride(t *testing.T) { + root, err := ioutil.TempDir("", "TestCustomLxcConfig") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(root) + os.MkdirAll(path.Join(root, "containers", "1"), 0777) + driver, err := NewDriver(root, "", false) + if err != nil { + t.Fatal(err) + } + processConfig := execdriver.ProcessConfig{ + Privileged: false, + } + + processConfig.Env = []string{"HOSTNAME=testhost"} + command := &execdriver.Command{ + ID: "1", + LxcConfig: []string{ + "lxc.cgroup.cpuset.cpus = 0,1", + "lxc.network.ipv4 = 172.0.0.1", + }, + Network: &execdriver.Network{ + Mtu: 1500, + Interface: &execdriver.NetworkInterface{ + Gateway: "10.10.10.1", + IPAddress: "10.10.10.10", + IPPrefixLen: 24, + Bridge: "docker0", + }, + }, + ProcessConfig: processConfig, + CapAdd: []string{"net_admin", "syslog"}, + CapDrop: []string{"kill", "mknod"}, + } + + p, err := driver.generateLXCConfig(command) + if err != nil { + t.Fatal(err) + } + // network + grepFile(t, p, "lxc.network.type = veth") + grepFile(t, p, "lxc.network.link = docker0") + grepFile(t, p, "lxc.network.name = eth0") + grepFile(t, p, "lxc.network.ipv4 = 172.0.0.1") + grepFile(t, p, "lxc.network.ipv4.gateway = 10.10.10.1") + grepFile(t, p, "lxc.network.flags = up") + + // hostname + grepFile(t, p, "lxc.utsname = testhost") + grepFile(t, p, "lxc.cgroup.cpuset.cpus = 0,1") + container := nativeTemplate.New() + for _, cap := range container.Capabilities { + cap = strings.ToLower(cap) + if cap != "mknod" && cap != "kill" { + grepFile(t, p, fmt.Sprintf("lxc.cap.keep = %s", cap)) + } + } + grepFileWithReverse(t, p, fmt.Sprintf("lxc.cap.keep = kill"), true) + grepFileWithReverse(t, p, fmt.Sprintf("lxc.cap.keep = mknod"), true) +} From 6e96dcec3c524dc05b7d72aa0aab4fd734c03a7d Mon Sep 17 00:00:00 2001 From: Tomasz Nurkiewicz Date: Fri, 26 Dec 2014 00:06:45 +0100 Subject: [PATCH 115/513] Stefan Banach in names-generator Signed-off-by: Tomasz Nurkiewicz --- pkg/namesgenerator/names-generator.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/namesgenerator/names-generator.go b/pkg/namesgenerator/names-generator.go index b641e915f..97b0c1a7c 100644 --- a/pkg/namesgenerator/names-generator.go +++ b/pkg/namesgenerator/names-generator.go @@ -69,6 +69,7 @@ var ( // Rosalyn Sussman Yalow - Rosalyn Sussman Yalow was an American medical physicist, and a co-winner of the 1977 Nobel Prize in Physiology or Medicine for development of the radioimmunoassay technique. http://en.wikipedia.org/wiki/Rosalyn_Sussman_Yalow // Sophie Kowalevski - Russian mathematician responsible for important original contributions to analysis, differential equations and mechanics - http://en.wikipedia.org/wiki/Sofia_Kovalevskaya // Sophie Wilson designed the first Acorn Micro-Computer and the instruction set for ARM processors. http://en.wikipedia.org/wiki/Sophie_Wilson + // Stefan Banach - Polish mathematician, was one of the founders of modern functional analysis. http://en.wikipedia.org/wiki/Stefan_Banach // Stephen Hawking pioneered the field of cosmology by combining general relativity and quantum mechanics. http://en.wikipedia.org/wiki/Stephen_Hawking // Steve Wozniak invented the Apple I and Apple II. http://en.wikipedia.org/wiki/Steve_Wozniak // Werner Heisenberg was a founding father of quantum mechanics. http://en.wikipedia.org/wiki/Werner_Heisenberg @@ -77,7 +78,7 @@ var ( // http://en.wikipedia.org/wiki/Walter_Houser_Brattain // http://en.wikipedia.org/wiki/William_Shockley // Yeong-Sil Jang was a Korean scientist and astronomer during the Joseon Dynasty; he invented the first metal printing press and water gauge. http://en.wikipedia.org/wiki/Jang_Yeong-sil - right = [...]string{"albattani", "almeida", "archimedes", "ardinghelli", "babbage", "bardeen", "bartik", "bell", "blackwell", "bohr", "brattain", "brown", "carson", "colden", "cori", "curie", "darwin", "davinci", "einstein", "elion", "engelbart", "euclid", "fermat", "fermi", "feynman", "franklin", "galileo", "goldstine", "goodall", "hawking", "heisenberg", "hodgkin", "hoover", "hopper", "hypatia", "jang", "jones", "kirch", "kowalevski", "lalande", "leakey", "lovelace", "lumiere", "mayer", "mccarthy", "mcclintock", "mclean", "meitner", "mestorf", "morse", "newton", "nobel", "pare", "pasteur", "perlman", "pike", "poincare", "ptolemy", "ritchie", "rosalind", "sammet", "shockley", "sinoussi", "stallman", "tesla", "thompson", "torvalds", "turing", "wilson", "wozniak", "wright", "yalow", "yonath"} + right = [...]string{"albattani", "almeida", "archimedes", "ardinghelli", "babbage", "banach", "bardeen", "bartik", "bell", "blackwell", "bohr", "brattain", "brown", "carson", "colden", "cori", "curie", "darwin", "davinci", "einstein", "elion", "engelbart", "euclid", "fermat", "fermi", "feynman", "franklin", "galileo", "goldstine", "goodall", "hawking", "heisenberg", "hodgkin", "hoover", "hopper", "hypatia", "jang", "jones", "kirch", "kowalevski", "lalande", "leakey", "lovelace", "lumiere", "mayer", "mccarthy", "mcclintock", "mclean", "meitner", "mestorf", "morse", "newton", "nobel", "pare", "pasteur", "perlman", "pike", "poincare", "ptolemy", "ritchie", "rosalind", "sammet", "shockley", "sinoussi", "stallman", "tesla", "thompson", "torvalds", "turing", "wilson", "wozniak", "wright", "yalow", "yonath"} ) func GetRandomName(retry int) string { From 6df1eff85e5614dfc1b09e1ff5c4e9cadb0dde0f Mon Sep 17 00:00:00 2001 From: Ankush Agarwal Date: Thu, 25 Dec 2014 22:37:43 -0800 Subject: [PATCH 116/513] Fix names-generator comment about name generation Signed-off-by: Ankush Agarwal --- pkg/namesgenerator/names-generator.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/namesgenerator/names-generator.go b/pkg/namesgenerator/names-generator.go index b641e915f..5ab746895 100644 --- a/pkg/namesgenerator/names-generator.go +++ b/pkg/namesgenerator/names-generator.go @@ -8,7 +8,7 @@ import ( var ( left = [...]string{"happy", "jolly", "dreamy", "sad", "angry", "pensive", "focused", "sleepy", "grave", "distracted", "determined", "stoic", "stupefied", "sharp", "agitated", "cocky", "tender", "goofy", "furious", "desperate", "hopeful", "compassionate", "silly", "lonely", "condescending", "naughty", "kickass", "drunk", "boring", "nostalgic", "ecstatic", "insane", "cranky", "mad", "jovial", "sick", "hungry", "thirsty", "elegant", "backstabbing", "clever", "trusting", "loving", "suspicious", "berserk", "high", "romantic", "prickly", "evil", "admiring", "adoring", "reverent", "serene", "fervent", "modest", "gloomy", "elated"} - // Docker 0.7.x generates names from notable scientists and hackers. + // Docker, starting from 0.7.x, generates names from notable scientists and hackers. // // Ada Lovelace invented the first algorithm. http://en.wikipedia.org/wiki/Ada_Lovelace (thanks James Turnbull) // Ada Yonath - an Israeli crystallographer, the first woman from the Middle East to win a Nobel prize in the sciences. http://en.wikipedia.org/wiki/Ada_Yonath From 8d7ee3697036d2d1f6c43efcb50b8d8b6c627584 Mon Sep 17 00:00:00 2001 From: Aaron Huslage Date: Fri, 26 Dec 2014 12:31:01 -0500 Subject: [PATCH 117/513] Remove -t="" and -m="". Make -t and -m options consistent with help text and other documentation. Docker-DCO-1.1-Signed-off-by: Aaron Huslage (github: huslage) --- docs/sources/userguide/dockerimages.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sources/userguide/dockerimages.md b/docs/sources/userguide/dockerimages.md index ead6d82db..2c4e14c70 100644 --- a/docs/sources/userguide/dockerimages.md +++ b/docs/sources/userguide/dockerimages.md @@ -192,7 +192,7 @@ Now we have a container with the change we want to make. We can then commit a copy of this container to an image using the `docker commit` command. - $ sudo docker commit -m="Added json gem" -a="Kate Smith" \ + $ sudo docker commit -m "Added json gem" -a "Kate Smith" \ 0b2616b0e5a8 ouruser/sinatra:v2 4f177bd27a9ff0f6dc2a830403925b5360bfe0b93d476f7fc3231110e7f71b1c @@ -273,7 +273,7 @@ Sinatra gem. Now let's take our `Dockerfile` and use the `docker build` command to build an image. - $ sudo docker build -t="ouruser/sinatra:v2" . + $ sudo docker build -t ouruser/sinatra:v2 . Sending build context to Docker daemon 2.048 kB Sending build context to Docker daemon Step 0 : FROM ubuntu:14.04 From 98d5720594d275071f43fbf90daf5fcd1166df8b Mon Sep 17 00:00:00 2001 From: Jessica Frazelle Date: Mon, 22 Dec 2014 14:54:55 -0800 Subject: [PATCH 118/513] Add test for non local mac address. Docker-DCO-1.1-Signed-off-by: Jessica Frazelle (github: jfrazelle) --- integration-cli/docker_cli_run_test.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/integration-cli/docker_cli_run_test.go b/integration-cli/docker_cli_run_test.go index 0e1d3aff4..6b32f1be2 100644 --- a/integration-cli/docker_cli_run_test.go +++ b/integration-cli/docker_cli_run_test.go @@ -2693,3 +2693,15 @@ func TestRunTtyWithPipe(t *testing.T) { logDone("run - forbid piped stdin with tty") } + +func TestRunNonLocalMacAddress(t *testing.T) { + defer deleteAllContainers() + addr := "00:16:3E:08:00:50" + + cmd := exec.Command(dockerBinary, "run", "--mac-address", addr, "busybox", "ifconfig") + if out, _, err := runCommandWithOutput(cmd); err != nil || !strings.Contains(out, addr) { + t.Fatalf("Output should have contained %q: %s, %v", addr, out, err) + } + + logDone("run - use non-local mac-address") +} From d30d12c1027e8f2bdb6cac01f8e7cc25d82b0d6a Mon Sep 17 00:00:00 2001 From: Tianon Gravi Date: Fri, 26 Dec 2014 14:50:18 -0700 Subject: [PATCH 119/513] Remove /etc/apt/apt.conf.d/01autoremove-kernels in mkimage/debootstrap This file is one APT creates to make sure we don't "autoremove" our currently in-use kernel, which doesn't really apply to debootstraps/Docker images that don't even have kernels installed. Signed-off-by: Andrew "Tianon" Page --- contrib/mkimage/debootstrap | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/contrib/mkimage/debootstrap b/contrib/mkimage/debootstrap index 65f154aa9..c7a2b6683 100755 --- a/contrib/mkimage/debootstrap +++ b/contrib/mkimage/debootstrap @@ -49,6 +49,11 @@ chmod +x "$rootfsDir/usr/sbin/policy-rc.d" # shrink a little, since apt makes us cache-fat (wheezy: ~157.5MB vs ~120MB) ( set -x; chroot "$rootfsDir" apt-get clean ) +# this file is one APT creates to make sure we don't "autoremove" our currently +# in-use kernel, which doesn't really apply to debootstraps/Docker images that +# don't even have kernels installed +rm -f "$rootfsDir/etc/apt/apt.conf.d/01autoremove-kernels" + # Ubuntu 10.04 sucks... :) if strings "$rootfsDir/usr/bin/dpkg" | grep -q unsafe-io; then # force dpkg not to call sync() after package extraction (speeding up installs) From 8803174e4f8a3f0a2e5d0d077e8bed0607f3cbe3 Mon Sep 17 00:00:00 2001 From: Tianon Gravi Date: Fri, 26 Dec 2014 14:59:25 -0700 Subject: [PATCH 120/513] Add CONFIG_POSIX_MQUEUE to check-config.sh Signed-off-by: Andrew "Tianon" Page --- contrib/check-config.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contrib/check-config.sh b/contrib/check-config.sh index 72e3108fe..4f1754073 100755 --- a/contrib/check-config.sh +++ b/contrib/check-config.sh @@ -138,6 +138,9 @@ flags=( NF_NAT_IPV4 IP_NF_FILTER IP_NF_TARGET_MASQUERADE NETFILTER_XT_MATCH_{ADDRTYPE,CONNTRACK} NF_NAT NF_NAT_NEEDED + + # required for bind-mounting /dev/mqueue into containers + POSIX_MQUEUE ) check_flags "${flags[@]}" echo From ed21031353ee188e2837a3030b3fbda88e1ce614 Mon Sep 17 00:00:00 2001 From: Tianon Gravi Date: Fri, 26 Dec 2014 16:39:59 -0700 Subject: [PATCH 121/513] Switch names-generator arrays to be long-form This way, we can embed the link/description lines directly in the array itself, conflicts between PRs to this section are minimized, new PRs are easier to review, and it's a lot easier to notice when people are missing a link/description (like the few that currently are). Signed-off-by: Andrew "Tianon" Page --- pkg/namesgenerator/names-generator.go | 347 ++++++++++++++++++++------ 1 file changed, 276 insertions(+), 71 deletions(-) diff --git a/pkg/namesgenerator/names-generator.go b/pkg/namesgenerator/names-generator.go index f91117f11..3f338f4b6 100644 --- a/pkg/namesgenerator/names-generator.go +++ b/pkg/namesgenerator/names-generator.go @@ -7,78 +7,283 @@ import ( ) var ( - left = [...]string{"happy", "jolly", "dreamy", "sad", "angry", "pensive", "focused", "sleepy", "grave", "distracted", "determined", "stoic", "stupefied", "sharp", "agitated", "cocky", "tender", "goofy", "furious", "desperate", "hopeful", "compassionate", "silly", "lonely", "condescending", "naughty", "kickass", "drunk", "boring", "nostalgic", "ecstatic", "insane", "cranky", "mad", "jovial", "sick", "hungry", "thirsty", "elegant", "backstabbing", "clever", "trusting", "loving", "suspicious", "berserk", "high", "romantic", "prickly", "evil", "admiring", "adoring", "reverent", "serene", "fervent", "modest", "gloomy", "elated"} + left = [...]string{ + "admiring", + "adoring", + "agitated", + "angry", + "backstabbing", + "berserk", + "boring", + "clever", + "cocky", + "compassionate", + "condescending", + "cranky", + "desperate", + "determined", + "distracted", + "dreamy", + "drunk", + "ecstatic", + "elated", + "elegant", + "evil", + "fervent", + "focused", + "furious", + "gloomy", + "goofy", + "grave", + "happy", + "high", + "hopeful", + "hungry", + "insane", + "jolly", + "jovial", + "kickass", + "lonely", + "loving", + "mad", + "modest", + "naughty", + "nostalgic", + "pensive", + "prickly", + "reverent", + "romantic", + "sad", + "serene", + "sharp", + "sick", + "silly", + "sleepy", + "stoic", + "stupefied", + "suspicious", + "tender", + "thirsty", + "trusting", + } + // Docker, starting from 0.7.x, generates names from notable scientists and hackers. - // - // Ada Lovelace invented the first algorithm. http://en.wikipedia.org/wiki/Ada_Lovelace (thanks James Turnbull) - // Ada Yonath - an Israeli crystallographer, the first woman from the Middle East to win a Nobel prize in the sciences. http://en.wikipedia.org/wiki/Ada_Yonath - // Adele Goldstine, born Adele Katz, wrote the complete technical description for the first electronic digital computer, ENIAC. http://en.wikipedia.org/wiki/Adele_Goldstine - // Alan Turing was a founding father of computer science. http://en.wikipedia.org/wiki/Alan_Turing. - // Albert Einstein invented the general theory of relativity. http://en.wikipedia.org/wiki/Albert_Einstein - // Ambroise Pare invented modern surgery. http://en.wikipedia.org/wiki/Ambroise_Par%C3%A9 - // Archimedes was a physicist, engineer and mathematician who invented too many things to list them here. http://en.wikipedia.org/wiki/Archimedes - // Barbara McClintock - a distinguished American cytogeneticist, 1983 Nobel Laureate in Physiology or Medicine for discovering transposons. http://en.wikipedia.org/wiki/Barbara_McClintock - // Benjamin Franklin is famous for his experiments in electricity and the invention of the lightning rod. - // Charles Babbage invented the concept of a programmable computer. http://en.wikipedia.org/wiki/Charles_Babbage. - // Charles Darwin established the principles of natural evolution. http://en.wikipedia.org/wiki/Charles_Darwin. - // Dennis Ritchie and Ken Thompson created UNIX and the C programming language. http://en.wikipedia.org/wiki/Dennis_Ritchie http://en.wikipedia.org/wiki/Ken_Thompson - // Dorothy Hodgkin was a British biochemist, credited with the development of protein crystallography. She was awarded the Nobel Prize in Chemistry in 1964. http://en.wikipedia.org/wiki/Dorothy_Hodgkin - // Douglas Engelbart gave the mother of all demos: http://en.wikipedia.org/wiki/Douglas_Engelbart - // Elizabeth Blackwell - American doctor and first American woman to receive a medical degree - http://en.wikipedia.org/wiki/Elizabeth_Blackwell - // Emmett Brown invented time travel. http://en.wikipedia.org/wiki/Emmett_Brown (thanks Brian Goff) - // Enrico Fermi invented the first nuclear reactor. http://en.wikipedia.org/wiki/Enrico_Fermi. - // Erna Schneider Hoover revolutionized modern communication by inventing a computerized telephon switching method. http://en.wikipedia.org/wiki/Erna_Schneider_Hoover - // Euclid invented geometry. http://en.wikipedia.org/wiki/Euclid - // Françoise Barré-Sinoussi - French virologist and Nobel Prize Laureate in Physiology or Medicine; her work was fundamental in identifying HIV as the cause of AIDS. http://en.wikipedia.org/wiki/Fran%C3%A7oise_Barr%C3%A9-Sinoussi - // Galileo was a founding father of modern astronomy, and faced politics and obscurantism to establish scientific truth. http://en.wikipedia.org/wiki/Galileo_Galilei - // Gertrude Elion - American biochemist, pharmacologist and the 1988 recipient of the Nobel Prize in Medicine - http://en.wikipedia.org/wiki/Gertrude_Elion - // Gerty Theresa Cori - American biochemist who became the third woman—and first American woman—to win a Nobel Prize in science, and the first woman to be awarded the Nobel Prize in Physiology or Medicine. Cori was born in Prague. http://en.wikipedia.org/wiki/Gerty_Cori - // Grace Hopper developed the first compiler for a computer programming language and is credited with popularizing the term "debugging" for fixing computer glitches. http://en.wikipedia.org/wiki/Grace_Hopper - // Henry Poincare made fundamental contributions in several fields of mathematics. http://en.wikipedia.org/wiki/Henri_Poincar%C3%A9 - // Hypatia - Greek Alexandrine Neoplatonist philosopher in Egypt who was one of the earliest mothers of mathematics - http://en.wikipedia.org/wiki/Hypatia - // Isaac Newton invented classic mechanics and modern optics. http://en.wikipedia.org/wiki/Isaac_Newton - // Jane Colden - American botanist widely considered the first female American botanist - http://en.wikipedia.org/wiki/Jane_Colden - // Jane Goodall - British primatologist, ethologist, and anthropologist who is considered to be the world's foremost expert on chimpanzees - http://en.wikipedia.org/wiki/Jane_Goodall - // Jean Bartik, born Betty Jean Jennings, was one of the original programmers for the ENIAC computer. http://en.wikipedia.org/wiki/Jean_Bartik - // Jean E. Sammet developed FORMAC, the first widely used computer language for symbolic manipulation of mathematical formulas. http://en.wikipedia.org/wiki/Jean_E._Sammet - // Johanna Mestorf - German prehistoric archaeologist and first female museum director in Germany - http://en.wikipedia.org/wiki/Johanna_Mestorf - // John McCarthy invented LISP: http://en.wikipedia.org/wiki/John_McCarthy_(computer_scientist) - // June Almeida - Scottish virologist who took the first pictures of the rubella virus - http://en.wikipedia.org/wiki/June_Almeida - // Karen Spärck Jones came up with the concept of inverse document frequency, which is used in most search engines today. http://en.wikipedia.org/wiki/Karen_Sp%C3%A4rck_Jones - // Leonardo Da Vinci invented too many things to list here. http://en.wikipedia.org/wiki/Leonardo_da_Vinci. - // Linus Torvalds invented Linux and Git. http://en.wikipedia.org/wiki/Linus_Torvalds - // Lise Meitner - Austrian/Swedish physicist who was involved in the discovery of nuclear fission. The element meitnerium is named after her - http://en.wikipedia.org/wiki/Lise_Meitner - // Louis Pasteur discovered vaccination, fermentation and pasteurization. http://en.wikipedia.org/wiki/Louis_Pasteur. - // Malcolm McLean invented the modern shipping container: http://en.wikipedia.org/wiki/Malcom_McLean - // Maria Ardinghelli - Italian translator, mathematician and physicist - http://en.wikipedia.org/wiki/Maria_Ardinghelli - // Maria Kirch - German astronomer and first woman to discover a comet - http://en.wikipedia.org/wiki/Maria_Margarethe_Kirch - // Maria Mayer - American theoretical physicist and Nobel laureate in Physics for proposing the nuclear shell model of the atomic nucleus - http://en.wikipedia.org/wiki/Maria_Mayer - // Marie Curie discovered radioactivity. http://en.wikipedia.org/wiki/Marie_Curie. - // Marie-Jeanne de Lalande - French astronomer, mathematician and cataloguer of stars - http://en.wikipedia.org/wiki/Marie-Jeanne_de_Lalande - // Mary Leakey - British paleoanthropologist who discovered the first fossilized Proconsul skull - http://en.wikipedia.org/wiki/Mary_Leakey - // Muhammad ibn Jābir al-Ḥarrānī al-Battānī was a founding father of astronomy. http://en.wikipedia.org/wiki/Mu%E1%B8%A5ammad_ibn_J%C4%81bir_al-%E1%B8%A4arr%C4%81n%C4%AB_al-Batt%C4%81n%C4%AB - // Niels Bohr is the father of quantum theory. http://en.wikipedia.org/wiki/Niels_Bohr. - // Nikola Tesla invented the AC electric system and every gadget ever used by a James Bond villain. http://en.wikipedia.org/wiki/Nikola_Tesla - // Pierre de Fermat pioneered several aspects of modern mathematics. http://en.wikipedia.org/wiki/Pierre_de_Fermat - // Rachel Carson - American marine biologist and conservationist, her book Silent Spring and other writings are credited with advancing the global environmental movement. http://en.wikipedia.org/wiki/Rachel_Carson - // Radia Perlman is a software designer and network engineer and most famous for her invention of the spanning-tree protocol (STP). http://en.wikipedia.org/wiki/Radia_Perlman - // Richard Feynman was a key contributor to quantum mechanics and particle physics. http://en.wikipedia.org/wiki/Richard_Feynman - // Richard Matthew Stallman - the founder of the Free Software movement, the GNU project, the Free Software Foundation, and the League for Programming Freedom. He also invented the concept of copyleft to protect the ideals of this movement, and enshrined this concept in the widely-used GPL (General Public License) for software. http://en.wikiquote.org/wiki/Richard_Stallman - // Rob Pike was a key contributor to Unix, Plan 9, the X graphic system, utf-8, and the Go programming language. http://en.wikipedia.org/wiki/Rob_Pike - // Rosalind Franklin - British biophysicist and X-ray crystallographer whose research was critical to the understanding of DNA - http://en.wikipedia.org/wiki/Rosalind_Franklin - // Rosalyn Sussman Yalow - Rosalyn Sussman Yalow was an American medical physicist, and a co-winner of the 1977 Nobel Prize in Physiology or Medicine for development of the radioimmunoassay technique. http://en.wikipedia.org/wiki/Rosalyn_Sussman_Yalow - // Sophie Kowalevski - Russian mathematician responsible for important original contributions to analysis, differential equations and mechanics - http://en.wikipedia.org/wiki/Sofia_Kovalevskaya - // Sophie Wilson designed the first Acorn Micro-Computer and the instruction set for ARM processors. http://en.wikipedia.org/wiki/Sophie_Wilson - // Stefan Banach - Polish mathematician, was one of the founders of modern functional analysis. http://en.wikipedia.org/wiki/Stefan_Banach - // Stephen Hawking pioneered the field of cosmology by combining general relativity and quantum mechanics. http://en.wikipedia.org/wiki/Stephen_Hawking - // Steve Wozniak invented the Apple I and Apple II. http://en.wikipedia.org/wiki/Steve_Wozniak - // Werner Heisenberg was a founding father of quantum mechanics. http://en.wikipedia.org/wiki/Werner_Heisenberg - // William Shockley, Walter Houser Brattain and John Bardeen co-invented the transistor (thanks Brian Goff). - // http://en.wikipedia.org/wiki/John_Bardeen - // http://en.wikipedia.org/wiki/Walter_Houser_Brattain - // http://en.wikipedia.org/wiki/William_Shockley - // Yeong-Sil Jang was a Korean scientist and astronomer during the Joseon Dynasty; he invented the first metal printing press and water gauge. http://en.wikipedia.org/wiki/Jang_Yeong-sil - right = [...]string{"albattani", "almeida", "archimedes", "ardinghelli", "babbage", "banach", "bardeen", "bartik", "bell", "blackwell", "bohr", "brattain", "brown", "carson", "colden", "cori", "curie", "darwin", "davinci", "einstein", "elion", "engelbart", "euclid", "fermat", "fermi", "feynman", "franklin", "galileo", "goldstine", "goodall", "hawking", "heisenberg", "hodgkin", "hoover", "hopper", "hypatia", "jang", "jones", "kirch", "kowalevski", "lalande", "leakey", "lovelace", "lumiere", "mayer", "mccarthy", "mcclintock", "mclean", "meitner", "mestorf", "morse", "newton", "nobel", "pare", "pasteur", "perlman", "pike", "poincare", "ptolemy", "ritchie", "rosalind", "sammet", "shockley", "sinoussi", "stallman", "tesla", "thompson", "torvalds", "turing", "wilson", "wozniak", "wright", "yalow", "yonath"} + right = [...]string{ + // Muhammad ibn Jābir al-Ḥarrānī al-Battānī was a founding father of astronomy. https://en.wikipedia.org/wiki/Mu%E1%B8%A5ammad_ibn_J%C4%81bir_al-%E1%B8%A4arr%C4%81n%C4%AB_al-Batt%C4%81n%C4%AB + "albattani", + + // June Almeida - Scottish virologist who took the first pictures of the rubella virus - https://en.wikipedia.org/wiki/June_Almeida + "almeida", + + // Archimedes was a physicist, engineer and mathematician who invented too many things to list them here. https://en.wikipedia.org/wiki/Archimedes + "archimedes", + + // Maria Ardinghelli - Italian translator, mathematician and physicist - https://en.wikipedia.org/wiki/Maria_Ardinghelli + "ardinghelli", + + // Charles Babbage invented the concept of a programmable computer. https://en.wikipedia.org/wiki/Charles_Babbage. + "babbage", + + // Stefan Banach - Polish mathematician, was one of the founders of modern functional analysis. https://en.wikipedia.org/wiki/Stefan_Banach + "banach", + + // William Shockley, Walter Houser Brattain and John Bardeen co-invented the transistor (thanks Brian Goff). + // - https://en.wikipedia.org/wiki/John_Bardeen + // - https://en.wikipedia.org/wiki/Walter_Houser_Brattain + // - https://en.wikipedia.org/wiki/William_Shockley + "bardeen", + "brattain", + "shockley", + + // Jean Bartik, born Betty Jean Jennings, was one of the original programmers for the ENIAC computer. https://en.wikipedia.org/wiki/Jean_Bartik + "bartik", + + "bell", + + // Elizabeth Blackwell - American doctor and first American woman to receive a medical degree - https://en.wikipedia.org/wiki/Elizabeth_Blackwell + "blackwell", + + // Niels Bohr is the father of quantum theory. https://en.wikipedia.org/wiki/Niels_Bohr. + "bohr", + + // Emmett Brown invented time travel. https://en.wikipedia.org/wiki/Emmett_Brown (thanks Brian Goff) + "brown", + + // Rachel Carson - American marine biologist and conservationist, her book Silent Spring and other writings are credited with advancing the global environmental movement. https://en.wikipedia.org/wiki/Rachel_Carson + "carson", + + // Jane Colden - American botanist widely considered the first female American botanist - https://en.wikipedia.org/wiki/Jane_Colden + "colden", + + // Gerty Theresa Cori - American biochemist who became the third woman—and first American woman—to win a Nobel Prize in science, and the first woman to be awarded the Nobel Prize in Physiology or Medicine. Cori was born in Prague. https://en.wikipedia.org/wiki/Gerty_Cori + "cori", + + // Marie Curie discovered radioactivity. https://en.wikipedia.org/wiki/Marie_Curie. + "curie", + + // Charles Darwin established the principles of natural evolution. https://en.wikipedia.org/wiki/Charles_Darwin. + "darwin", + + // Leonardo Da Vinci invented too many things to list here. https://en.wikipedia.org/wiki/Leonardo_da_Vinci. + "davinci", + + // Albert Einstein invented the general theory of relativity. https://en.wikipedia.org/wiki/Albert_Einstein + "einstein", + + // Gertrude Elion - American biochemist, pharmacologist and the 1988 recipient of the Nobel Prize in Medicine - https://en.wikipedia.org/wiki/Gertrude_Elion + "elion", + + // Douglas Engelbart gave the mother of all demos: https://en.wikipedia.org/wiki/Douglas_Engelbart + "engelbart", + + // Euclid invented geometry. https://en.wikipedia.org/wiki/Euclid + "euclid", + + // Pierre de Fermat pioneered several aspects of modern mathematics. https://en.wikipedia.org/wiki/Pierre_de_Fermat + "fermat", + + // Enrico Fermi invented the first nuclear reactor. https://en.wikipedia.org/wiki/Enrico_Fermi. + "fermi", + + // Richard Feynman was a key contributor to quantum mechanics and particle physics. https://en.wikipedia.org/wiki/Richard_Feynman + "feynman", + + // Benjamin Franklin is famous for his experiments in electricity and the invention of the lightning rod. + "franklin", + + // Galileo was a founding father of modern astronomy, and faced politics and obscurantism to establish scientific truth. https://en.wikipedia.org/wiki/Galileo_Galilei + "galileo", + + // Adele Goldstine, born Adele Katz, wrote the complete technical description for the first electronic digital computer, ENIAC. https://en.wikipedia.org/wiki/Adele_Goldstine + "goldstine", + + // Jane Goodall - British primatologist, ethologist, and anthropologist who is considered to be the world's foremost expert on chimpanzees - https://en.wikipedia.org/wiki/Jane_Goodall + "goodall", + + // Stephen Hawking pioneered the field of cosmology by combining general relativity and quantum mechanics. https://en.wikipedia.org/wiki/Stephen_Hawking + "hawking", + + // Werner Heisenberg was a founding father of quantum mechanics. https://en.wikipedia.org/wiki/Werner_Heisenberg + "heisenberg", + + // Dorothy Hodgkin was a British biochemist, credited with the development of protein crystallography. She was awarded the Nobel Prize in Chemistry in 1964. https://en.wikipedia.org/wiki/Dorothy_Hodgkin + "hodgkin", + + // Erna Schneider Hoover revolutionized modern communication by inventing a computerized telephon switching method. https://en.wikipedia.org/wiki/Erna_Schneider_Hoover + "hoover", + + // Grace Hopper developed the first compiler for a computer programming language and is credited with popularizing the term "debugging" for fixing computer glitches. https://en.wikipedia.org/wiki/Grace_Hopper + "hopper", + + // Hypatia - Greek Alexandrine Neoplatonist philosopher in Egypt who was one of the earliest mothers of mathematics - https://en.wikipedia.org/wiki/Hypatia + "hypatia", + + // Yeong-Sil Jang was a Korean scientist and astronomer during the Joseon Dynasty; he invented the first metal printing press and water gauge. https://en.wikipedia.org/wiki/Jang_Yeong-sil + "jang", + + // Karen Spärck Jones came up with the concept of inverse document frequency, which is used in most search engines today. https://en.wikipedia.org/wiki/Karen_Sp%C3%A4rck_Jones + "jones", + + // Maria Kirch - German astronomer and first woman to discover a comet - https://en.wikipedia.org/wiki/Maria_Margarethe_Kirch + "kirch", + + // Sophie Kowalevski - Russian mathematician responsible for important original contributions to analysis, differential equations and mechanics - https://en.wikipedia.org/wiki/Sofia_Kovalevskaya + "kowalevski", + + // Marie-Jeanne de Lalande - French astronomer, mathematician and cataloguer of stars - https://en.wikipedia.org/wiki/Marie-Jeanne_de_Lalande + "lalande", + + // Mary Leakey - British paleoanthropologist who discovered the first fossilized Proconsul skull - https://en.wikipedia.org/wiki/Mary_Leakey + "leakey", + + // Ada Lovelace invented the first algorithm. https://en.wikipedia.org/wiki/Ada_Lovelace (thanks James Turnbull) + "lovelace", + + "lumiere", + + // Maria Mayer - American theoretical physicist and Nobel laureate in Physics for proposing the nuclear shell model of the atomic nucleus - https://en.wikipedia.org/wiki/Maria_Mayer + "mayer", + + // John McCarthy invented LISP: https://en.wikipedia.org/wiki/John_McCarthy_(computer_scientist) + "mccarthy", + + // Barbara McClintock - a distinguished American cytogeneticist, 1983 Nobel Laureate in Physiology or Medicine for discovering transposons. https://en.wikipedia.org/wiki/Barbara_McClintock + "mcclintock", + + // Malcolm McLean invented the modern shipping container: https://en.wikipedia.org/wiki/Malcom_McLean + "mclean", + + // Lise Meitner - Austrian/Swedish physicist who was involved in the discovery of nuclear fission. The element meitnerium is named after her - https://en.wikipedia.org/wiki/Lise_Meitner + "meitner", + + // Johanna Mestorf - German prehistoric archaeologist and first female museum director in Germany - https://en.wikipedia.org/wiki/Johanna_Mestorf + "mestorf", + + "morse", + + // Isaac Newton invented classic mechanics and modern optics. https://en.wikipedia.org/wiki/Isaac_Newton + "newton", + + "nobel", + + // Ambroise Pare invented modern surgery. https://en.wikipedia.org/wiki/Ambroise_Par%C3%A9 + "pare", + + // Louis Pasteur discovered vaccination, fermentation and pasteurization. https://en.wikipedia.org/wiki/Louis_Pasteur. + "pasteur", + + // Radia Perlman is a software designer and network engineer and most famous for her invention of the spanning-tree protocol (STP). https://en.wikipedia.org/wiki/Radia_Perlman + "perlman", + + // Rob Pike was a key contributor to Unix, Plan 9, the X graphic system, utf-8, and the Go programming language. https://en.wikipedia.org/wiki/Rob_Pike + "pike", + + // Henri Poincaré made fundamental contributions in several fields of mathematics. https://en.wikipedia.org/wiki/Henri_Poincar%C3%A9 + "poincare", + + "ptolemy", + + // Dennis Ritchie and Ken Thompson created UNIX and the C programming language. + // - https://en.wikipedia.org/wiki/Dennis_Ritchie + // - https://en.wikipedia.org/wiki/Ken_Thompson + "ritchie", + "thompson", + + // Rosalind Franklin - British biophysicist and X-ray crystallographer whose research was critical to the understanding of DNA - https://en.wikipedia.org/wiki/Rosalind_Franklin + "rosalind", + + // Jean E. Sammet developed FORMAC, the first widely used computer language for symbolic manipulation of mathematical formulas. https://en.wikipedia.org/wiki/Jean_E._Sammet + "sammet", + + // Françoise Barré-Sinoussi - French virologist and Nobel Prize Laureate in Physiology or Medicine; her work was fundamental in identifying HIV as the cause of AIDS. https://en.wikipedia.org/wiki/Fran%C3%A7oise_Barr%C3%A9-Sinoussi + "sinoussi", + + // Richard Matthew Stallman - the founder of the Free Software movement, the GNU project, the Free Software Foundation, and the League for Programming Freedom. He also invented the concept of copyleft to protect the ideals of this movement, and enshrined this concept in the widely-used GPL (General Public License) for software. https://en.wikiquote.org/wiki/Richard_Stallman + "stallman", + + // Nikola Tesla invented the AC electric system and every gadget ever used by a James Bond villain. https://en.wikipedia.org/wiki/Nikola_Tesla + "tesla", + + // Linus Torvalds invented Linux and Git. https://en.wikipedia.org/wiki/Linus_Torvalds + "torvalds", + + // Alan Turing was a founding father of computer science. https://en.wikipedia.org/wiki/Alan_Turing. + "turing", + + // Sophie Wilson designed the first Acorn Micro-Computer and the instruction set for ARM processors. https://en.wikipedia.org/wiki/Sophie_Wilson + "wilson", + + // Steve Wozniak invented the Apple I and Apple II. https://en.wikipedia.org/wiki/Steve_Wozniak + "wozniak", + + "wright", + + // Rosalyn Sussman Yalow - Rosalyn Sussman Yalow was an American medical physicist, and a co-winner of the 1977 Nobel Prize in Physiology or Medicine for development of the radioimmunoassay technique. https://en.wikipedia.org/wiki/Rosalyn_Sussman_Yalow + "yalow", + + // Ada Yonath - an Israeli crystallographer, the first woman from the Middle East to win a Nobel prize in the sciences. https://en.wikipedia.org/wiki/Ada_Yonath + "yonath", + } ) func GetRandomName(retry int) string { From 587286bbaafa604078b4d72643c6a855a33e5aa1 Mon Sep 17 00:00:00 2001 From: Tianon Gravi Date: Fri, 26 Dec 2014 16:53:40 -0700 Subject: [PATCH 122/513] Add descriptions for bell, lumiere, morse, nobel, ptolemy, and wright Signed-off-by: Andrew "Tianon" Page --- pkg/namesgenerator/names-generator.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/namesgenerator/names-generator.go b/pkg/namesgenerator/names-generator.go index 3f338f4b6..c5bcd25b4 100644 --- a/pkg/namesgenerator/names-generator.go +++ b/pkg/namesgenerator/names-generator.go @@ -98,6 +98,7 @@ var ( // Jean Bartik, born Betty Jean Jennings, was one of the original programmers for the ENIAC computer. https://en.wikipedia.org/wiki/Jean_Bartik "bartik", + // Alexander Graham Bell - an eminent Scottish-born scientist, inventor, engineer and innovator who is credited with inventing the first practical telephone - https://en.wikipedia.org/wiki/Alexander_Graham_Bell "bell", // Elizabeth Blackwell - American doctor and first American woman to receive a medical degree - https://en.wikipedia.org/wiki/Elizabeth_Blackwell @@ -199,6 +200,7 @@ var ( // Ada Lovelace invented the first algorithm. https://en.wikipedia.org/wiki/Ada_Lovelace (thanks James Turnbull) "lovelace", + // Auguste and Louis Lumière - the first filmmakers in history - https://en.wikipedia.org/wiki/Auguste_and_Louis_Lumi%C3%A8re "lumiere", // Maria Mayer - American theoretical physicist and Nobel laureate in Physics for proposing the nuclear shell model of the atomic nucleus - https://en.wikipedia.org/wiki/Maria_Mayer @@ -219,11 +221,13 @@ var ( // Johanna Mestorf - German prehistoric archaeologist and first female museum director in Germany - https://en.wikipedia.org/wiki/Johanna_Mestorf "mestorf", + // Samuel Morse - contributed to the invention of a single-wire telegraph system based on European telegraphs and was a co-developer of the Morse code - https://en.wikipedia.org/wiki/Samuel_Morse "morse", // Isaac Newton invented classic mechanics and modern optics. https://en.wikipedia.org/wiki/Isaac_Newton "newton", + // Alfred Nobel - a Swedish chemist, engineer, innovator, and armaments manufacturer (inventor of dynamite) - https://en.wikipedia.org/wiki/Alfred_Nobel "nobel", // Ambroise Pare invented modern surgery. https://en.wikipedia.org/wiki/Ambroise_Par%C3%A9 @@ -241,6 +245,7 @@ var ( // Henri Poincaré made fundamental contributions in several fields of mathematics. https://en.wikipedia.org/wiki/Henri_Poincar%C3%A9 "poincare", + // Claudius Ptolemy - a Greco-Egyptian writer of Alexandria, known as a mathematician, astronomer, geographer, astrologer, and poet of a single epigram in the Greek Anthology - https://en.wikipedia.org/wiki/Ptolemy "ptolemy", // Dennis Ritchie and Ken Thompson created UNIX and the C programming language. @@ -276,6 +281,7 @@ var ( // Steve Wozniak invented the Apple I and Apple II. https://en.wikipedia.org/wiki/Steve_Wozniak "wozniak", + // The Wright brothers, Orville and Wilbur - credited with inventing and building the world's first successful airplane and making the first controlled, powered and sustained heavier-than-air human flight - https://en.wikipedia.org/wiki/Wright_brothers "wright", // Rosalyn Sussman Yalow - Rosalyn Sussman Yalow was an American medical physicist, and a co-winner of the 1977 Nobel Prize in Physiology or Medicine for development of the radioimmunoassay technique. https://en.wikipedia.org/wiki/Rosalyn_Sussman_Yalow From abfb7138871993c53050a2d159ca0fae07bf65e8 Mon Sep 17 00:00:00 2001 From: Seongyeol Lim Date: Mon, 6 Oct 2014 03:15:09 -0700 Subject: [PATCH 123/513] Add support file name with whitespace for ADD and COPY command Closes #8318 Signed-off-by: Seongyeol Lim --- builder/parser/parser.go | 4 +- .../testfiles/ADD-COPY-with-JSON/Dockerfile | 9 ++ .../testfiles/ADD-COPY-with-JSON/result | 8 ++ integration-cli/docker_cli_build_test.go | 120 ++++++++++++++++++ 4 files changed, 139 insertions(+), 2 deletions(-) create mode 100644 builder/parser/testfiles/ADD-COPY-with-JSON/Dockerfile create mode 100644 builder/parser/testfiles/ADD-COPY-with-JSON/result diff --git a/builder/parser/parser.go b/builder/parser/parser.go index ad42a1586..6cec614db 100644 --- a/builder/parser/parser.go +++ b/builder/parser/parser.go @@ -50,8 +50,8 @@ func init() { "env": parseEnv, "maintainer": parseString, "from": parseString, - "add": parseStringsWhitespaceDelimited, - "copy": parseStringsWhitespaceDelimited, + "add": parseMaybeJSONToList, + "copy": parseMaybeJSONToList, "run": parseMaybeJSON, "cmd": parseMaybeJSON, "entrypoint": parseMaybeJSON, diff --git a/builder/parser/testfiles/ADD-COPY-with-JSON/Dockerfile b/builder/parser/testfiles/ADD-COPY-with-JSON/Dockerfile new file mode 100644 index 000000000..49372b060 --- /dev/null +++ b/builder/parser/testfiles/ADD-COPY-with-JSON/Dockerfile @@ -0,0 +1,9 @@ +FROM ubuntu:14.04 +MAINTAINER Seongyeol Lim + +COPY . /go/src/github.com/docker/docker +ADD . / +ADD [ "vimrc", "/tmp" ] +COPY [ "bashrc", "/tmp" ] +COPY [ "test file", "/tmp" ] +ADD [ "test file", "/tmp/test file" ] diff --git a/builder/parser/testfiles/ADD-COPY-with-JSON/result b/builder/parser/testfiles/ADD-COPY-with-JSON/result new file mode 100644 index 000000000..86c3fef72 --- /dev/null +++ b/builder/parser/testfiles/ADD-COPY-with-JSON/result @@ -0,0 +1,8 @@ +(from "ubuntu:14.04") +(maintainer "Seongyeol Lim ") +(copy "." "/go/src/github.com/docker/docker") +(add "." "/") +(add "vimrc" "/tmp") +(copy "bashrc" "/tmp") +(copy "test file" "/tmp") +(add "test file" "/tmp/test file") diff --git a/integration-cli/docker_cli_build_test.go b/integration-cli/docker_cli_build_test.go index e440bc770..a3d2464d0 100644 --- a/integration-cli/docker_cli_build_test.go +++ b/integration-cli/docker_cli_build_test.go @@ -834,6 +834,126 @@ func TestBuildCopyMultipleFilesToFile(t *testing.T) { logDone("build - multiple copy files to file") } +func TestBuildAddFileWithWhitespace(t *testing.T) { + name := "testaddfilewithwhitespace" + defer deleteImages(name) + ctx, err := fakeContext(`FROM busybox +RUN mkdir "/test dir" +RUN mkdir "/test_dir" +ADD [ "test file1", "/test_file1" ] +ADD [ "test_file2", "/test file2" ] +ADD [ "test file3", "/test file3" ] +ADD [ "test dir/test_file4", "/test_dir/test_file4" ] +ADD [ "test_dir/test_file5", "/test dir/test_file5" ] +ADD [ "test dir/test_file6", "/test dir/test_file6" ] +RUN [ $(cat "/test_file1") = 'test1' ] +RUN [ $(cat "/test file2") = 'test2' ] +RUN [ $(cat "/test file3") = 'test3' ] +RUN [ $(cat "/test_dir/test_file4") = 'test4' ] +RUN [ $(cat "/test dir/test_file5") = 'test5' ] +RUN [ $(cat "/test dir/test_file6") = 'test6' ]`, + map[string]string{ + "test file1": "test1", + "test_file2": "test2", + "test file3": "test3", + "test dir/test_file4": "test4", + "test_dir/test_file5": "test5", + "test dir/test_file6": "test6", + }) + defer ctx.Close() + if err != nil { + t.Fatal(err) + } + + if _, err := buildImageFromContext(name, ctx, true); err != nil { + t.Fatal(err) + } + logDone("build - add file with whitespace") +} + +func TestBuildCopyFileWithWhitespace(t *testing.T) { + name := "testcopyfilewithwhitespace" + defer deleteImages(name) + ctx, err := fakeContext(`FROM busybox +RUN mkdir "/test dir" +RUN mkdir "/test_dir" +COPY [ "test file1", "/test_file1" ] +COPY [ "test_file2", "/test file2" ] +COPY [ "test file3", "/test file3" ] +COPY [ "test dir/test_file4", "/test_dir/test_file4" ] +COPY [ "test_dir/test_file5", "/test dir/test_file5" ] +COPY [ "test dir/test_file6", "/test dir/test_file6" ] +RUN [ $(cat "/test_file1") = 'test1' ] +RUN [ $(cat "/test file2") = 'test2' ] +RUN [ $(cat "/test file3") = 'test3' ] +RUN [ $(cat "/test_dir/test_file4") = 'test4' ] +RUN [ $(cat "/test dir/test_file5") = 'test5' ] +RUN [ $(cat "/test dir/test_file6") = 'test6' ]`, + map[string]string{ + "test file1": "test1", + "test_file2": "test2", + "test file3": "test3", + "test dir/test_file4": "test4", + "test_dir/test_file5": "test5", + "test dir/test_file6": "test6", + }) + defer ctx.Close() + if err != nil { + t.Fatal(err) + } + + if _, err := buildImageFromContext(name, ctx, true); err != nil { + t.Fatal(err) + } + logDone("build - copy file with whitespace") +} + +func TestBuildAddMultipleFilesToFileWithWhitespace(t *testing.T) { + name := "testaddmultiplefilestofilewithwhitespace" + defer deleteImages(name) + ctx, err := fakeContext(`FROM busybox + ADD [ "test file1", "test file2", "test" ] + `, + map[string]string{ + "test file1": "test1", + "test file2": "test2", + }) + defer ctx.Close() + if err != nil { + t.Fatal(err) + } + + expected := "When using ADD with more than one source file, the destination must be a directory and end with a /" + if _, err := buildImageFromContext(name, ctx, true); err == nil || !strings.Contains(err.Error(), expected) { + t.Fatalf("Wrong error: (should contain \"%s\") got:\n%v", expected, err) + } + + logDone("build - multiple add files to file with whitespace") +} + +func TestBuildCopyMultipleFilesToFileWithWhitespace(t *testing.T) { + name := "testcopymultiplefilestofilewithwhitespace" + defer deleteImages(name) + ctx, err := fakeContext(`FROM busybox + COPY [ "test file1", "test file2", "test" ] + `, + map[string]string{ + "test file1": "test1", + "test file2": "test2", + }) + defer ctx.Close() + if err != nil { + t.Fatal(err) + } + + expected := "When using COPY with more than one source file, the destination must be a directory and end with a /" + if _, err := buildImageFromContext(name, ctx, true); err == nil || !strings.Contains(err.Error(), expected) { + t.Fatalf("Wrong error: (should contain \"%s\") got:\n%v", expected, err) + } + + logDone("build - multiple copy files to file with whitespace") +} + func TestBuildCopyWildcard(t *testing.T) { name := "testcopywildcard" defer deleteImages(name) From eda92e8834f0438ab4ec29fa26c66547279413e5 Mon Sep 17 00:00:00 2001 From: Alexandr Morozov Date: Fri, 26 Dec 2014 17:19:23 -0800 Subject: [PATCH 124/513] Test for issue #9699 Signed-off-by: Alexandr Morozov --- integration-cli/docker_cli_exec_test.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/integration-cli/docker_cli_exec_test.go b/integration-cli/docker_cli_exec_test.go index a3a6a2362..5bb2dc4bb 100644 --- a/integration-cli/docker_cli_exec_test.go +++ b/integration-cli/docker_cli_exec_test.go @@ -367,3 +367,28 @@ func TestExecParseError(t *testing.T) { } logDone("exec - error on parseExec should return usage") } + +func TestExecStopNotHanging(t *testing.T) { + defer deleteAllContainers() + if out, err := exec.Command(dockerBinary, "run", "-d", "--name", "testing", "busybox", "top").CombinedOutput(); err != nil { + t.Fatal(out, err) + } + + if err := exec.Command(dockerBinary, "exec", "testing", "top").Start(); err != nil { + t.Fatal(err) + } + + wait := make(chan struct{}) + go func() { + if out, err := exec.Command(dockerBinary, "stop", "testing").CombinedOutput(); err != nil { + t.Fatal(out, err) + } + close(wait) + }() + select { + case <-time.After(3 * time.Second): + t.Fatal("Container stop timed out") + case <-wait: + } + logDone("exec - container with exec not hanging on stop") +} From c98ae1f88fccd89eb2184cd2b780f9827ad5ad41 Mon Sep 17 00:00:00 2001 From: Alexandr Morozov Date: Wed, 24 Dec 2014 19:09:38 -0800 Subject: [PATCH 125/513] Update libcontainer to 0f397d4e145fb4053792d42b3424dd2143fb23ad This fixes wrong behavior of mutating methods of Namespaces object Signed-off-by: Alexandr Morozov --- project/vendor.sh | 2 +- .../github.com/docker/libcontainer/MAINTAINERS | 1 + .../src/github.com/docker/libcontainer/config.go | 16 ++++++++-------- .../docker/libcontainer/config_test.go | 12 ++++++++++++ 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/project/vendor.sh b/project/vendor.sh index 2c84e45ee..c1074e887 100755 --- a/project/vendor.sh +++ b/project/vendor.sh @@ -66,7 +66,7 @@ if [ "$1" = '--go' ]; then mv tmp-tar src/code.google.com/p/go/src/pkg/archive/tar fi -clone git github.com/docker/libcontainer 1597c68f7b941fd97881155d7f077852e2914e7b +clone git github.com/docker/libcontainer 0f397d4e145fb4053792d42b3424dd2143fb23ad # see src/github.com/docker/libcontainer/update-vendor.sh which is the "source of truth" for libcontainer deps (just like this file) rm -rf src/github.com/docker/libcontainer/vendor eval "$(grep '^clone ' src/github.com/docker/libcontainer/update-vendor.sh | grep -v 'github.com/codegangsta/cli')" diff --git a/vendor/src/github.com/docker/libcontainer/MAINTAINERS b/vendor/src/github.com/docker/libcontainer/MAINTAINERS index 7295c6038..523513172 100644 --- a/vendor/src/github.com/docker/libcontainer/MAINTAINERS +++ b/vendor/src/github.com/docker/libcontainer/MAINTAINERS @@ -2,4 +2,5 @@ Michael Crosby (@crosbymichael) Rohit Jnagal (@rjnagal) Victor Marmol (@vmarmol) Mrunal Patel (@mrunalp) +Alexandr Morozov (@LK4D4) update-vendor.sh: Tianon Gravi (@tianon) diff --git a/vendor/src/github.com/docker/libcontainer/config.go b/vendor/src/github.com/docker/libcontainer/config.go index 7f1bcc962..7ab9a9a76 100644 --- a/vendor/src/github.com/docker/libcontainer/config.go +++ b/vendor/src/github.com/docker/libcontainer/config.go @@ -30,26 +30,26 @@ type Namespace struct { type Namespaces []Namespace -func (n Namespaces) Remove(t NamespaceType) bool { +func (n *Namespaces) Remove(t NamespaceType) bool { i := n.index(t) if i == -1 { return false } - n = append(n[:i], n[i+1:]...) + *n = append((*n)[:i], (*n)[i+1:]...) return true } -func (n Namespaces) Add(t NamespaceType, path string) { +func (n *Namespaces) Add(t NamespaceType, path string) { i := n.index(t) if i == -1 { - n = append(n, Namespace{Type: t, Path: path}) + *n = append(*n, Namespace{Type: t, Path: path}) return } - n[i].Path = path + (*n)[i].Path = path } -func (n Namespaces) index(t NamespaceType) int { - for i, ns := range n { +func (n *Namespaces) index(t NamespaceType) int { + for i, ns := range *n { if ns.Type == t { return i } @@ -57,7 +57,7 @@ func (n Namespaces) index(t NamespaceType) int { return -1 } -func (n Namespaces) Contains(t NamespaceType) bool { +func (n *Namespaces) Contains(t NamespaceType) bool { return n.index(t) != -1 } diff --git a/vendor/src/github.com/docker/libcontainer/config_test.go b/vendor/src/github.com/docker/libcontainer/config_test.go index 5c73f84af..f2287fc74 100644 --- a/vendor/src/github.com/docker/libcontainer/config_test.go +++ b/vendor/src/github.com/docker/libcontainer/config_test.go @@ -158,3 +158,15 @@ func TestSelinuxLabels(t *testing.T) { t.Fatalf("expected mount label %q but received %q", label, container.MountConfig.MountLabel) } } + +func TestRemoveNamespace(t *testing.T) { + ns := Namespaces{ + {Type: NEWNET}, + } + if !ns.Remove(NEWNET) { + t.Fatal("NEWNET was not removed") + } + if len(ns) != 0 { + t.Fatalf("namespaces should have 0 items but reports %d", len(ns)) + } +} From 732c94a502cbda9a60d76e7af83c0569e919cdb9 Mon Sep 17 00:00:00 2001 From: Alexandr Morozov Date: Wed, 24 Dec 2014 20:40:41 -0800 Subject: [PATCH 126/513] Test for host networking Signed-off-by: Alexandr Morozov --- integration-cli/docker_cli_run_test.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/integration-cli/docker_cli_run_test.go b/integration-cli/docker_cli_run_test.go index 6b32f1be2..718161d52 100644 --- a/integration-cli/docker_cli_run_test.go +++ b/integration-cli/docker_cli_run_test.go @@ -2705,3 +2705,21 @@ func TestRunNonLocalMacAddress(t *testing.T) { logDone("run - use non-local mac-address") } + +func TestRunNetHost(t *testing.T) { + defer deleteAllContainers() + iplinkHost, err := exec.Command("ip", "link", "list").CombinedOutput() + if err != nil { + t.Fatal(err) + } + + iplinkCont, err := exec.Command(dockerBinary, "run", "--net=host", "busybox", "ip", "link", "list").CombinedOutput() + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(iplinkHost, iplinkCont) { + t.Fatalf("Container network:\n%s\nis not equal to host network:\n%s", iplinkCont, iplinkHost) + } + logDone("run - host network") +} From a1add90d89bacd0f6a7814aa72298af576107dfc Mon Sep 17 00:00:00 2001 From: Paul Nasrat Date: Sat, 27 Dec 2014 10:56:03 -0500 Subject: [PATCH 127/513] Update release checklist to include announce mail. Releases should be announced on the announce list as well as -dev. Signed-off-by: Paul Nasrat --- project/RELEASE-CHECKLIST.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/project/RELEASE-CHECKLIST.md b/project/RELEASE-CHECKLIST.md index 61b0c2b07..c43b73e79 100644 --- a/project/RELEASE-CHECKLIST.md +++ b/project/RELEASE-CHECKLIST.md @@ -299,5 +299,6 @@ blue button to delete your branch. Congratulations! You're done. Go forth and announce the glad tidings of the new release in `#docker`, -`#docker-dev`, on the [mailing list](https://groups.google.com/forum/#!forum/docker-dev), +`#docker-dev`, on the [dev mailing list](https://groups.google.com/forum/#!forum/docker-dev), +the [announce mailing list](https://groups.google.com/forum/#!forum/docker-announce), and on Twitter! From d27248007f9f4034d1a630e0ac3826af3c3357ec Mon Sep 17 00:00:00 2001 From: Jessica Frazelle Date: Mon, 29 Dec 2014 11:02:12 -0800 Subject: [PATCH 128/513] Fix tabs in project/make.sh Docker-DCO-1.1-Signed-off-by: Jessica Frazelle (github: jfrazelle) --- project/make.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/make.sh b/project/make.sh index cf1201649..82b8f0c5d 100755 --- a/project/make.sh +++ b/project/make.sh @@ -102,7 +102,7 @@ LDFLAGS=' ' if [ -z "$DEBUG" ]; then - LDFLAGS="-w $LDFLAGS" + LDFLAGS="-w $LDFLAGS" fi LDFLAGS_STATIC='-linkmode external' From 74ee405a27b19f2d91caaf50029cea3b13ddcaf2 Mon Sep 17 00:00:00 2001 From: Alexander Morozov Date: Mon, 29 Dec 2014 12:34:59 -0800 Subject: [PATCH 129/513] Fix done messages and error message for ipc tests Signed-off-by: Alexander Morozov --- integration-cli/docker_cli_run_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/integration-cli/docker_cli_run_test.go b/integration-cli/docker_cli_run_test.go index 718161d52..d55d8d7f7 100644 --- a/integration-cli/docker_cli_run_test.go +++ b/integration-cli/docker_cli_run_test.go @@ -2520,11 +2520,11 @@ func TestRunModeIpcHost(t *testing.T) { out2 = strings.Trim(out2, "\n") if hostIpc == out2 { - t.Fatalf("IPC should be different without --ipc=host %s != %s\n", hostIpc, out2) + t.Fatalf("IPC should be different without --ipc=host %s == %s\n", hostIpc, out2) } deleteAllContainers() - logDone("run - hostname and several network modes") + logDone("run - ipc host mode") } func TestRunModeIpcContainer(t *testing.T) { @@ -2562,7 +2562,7 @@ func TestRunModeIpcContainer(t *testing.T) { } deleteAllContainers() - logDone("run - hostname and several network modes") + logDone("run - ipc container mode") } func TestContainerNetworkMode(t *testing.T) { From e98c08a88fb5f3eedde158a3647a731c31bd4faa Mon Sep 17 00:00:00 2001 From: Alexander Morozov Date: Mon, 29 Dec 2014 12:56:07 -0800 Subject: [PATCH 130/513] Rewrite TestRunNetHost to compare namespaces Signed-off-by: Alexander Morozov --- integration-cli/docker_cli_run_test.go | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/integration-cli/docker_cli_run_test.go b/integration-cli/docker_cli_run_test.go index d55d8d7f7..2a75b27d8 100644 --- a/integration-cli/docker_cli_run_test.go +++ b/integration-cli/docker_cli_run_test.go @@ -2708,18 +2708,32 @@ func TestRunNonLocalMacAddress(t *testing.T) { func TestRunNetHost(t *testing.T) { defer deleteAllContainers() - iplinkHost, err := exec.Command("ip", "link", "list").CombinedOutput() + hostNet, err := os.Readlink("/proc/1/ns/net") if err != nil { t.Fatal(err) } - iplinkCont, err := exec.Command(dockerBinary, "run", "--net=host", "busybox", "ip", "link", "list").CombinedOutput() + cmd := exec.Command(dockerBinary, "run", "--net=host", "busybox", "readlink", "/proc/self/ns/net") + out2, _, err := runCommandWithOutput(cmd) if err != nil { - t.Fatal(err) + t.Fatal(err, out2) } - if !bytes.Equal(iplinkHost, iplinkCont) { - t.Fatalf("Container network:\n%s\nis not equal to host network:\n%s", iplinkCont, iplinkHost) + out2 = strings.Trim(out2, "\n") + if hostNet != out2 { + t.Fatalf("Net namespace different with --net=host %s != %s\n", hostNet, out2) } - logDone("run - host network") + + cmd = exec.Command(dockerBinary, "run", "busybox", "readlink", "/proc/self/ns/net") + out2, _, err = runCommandWithOutput(cmd) + if err != nil { + t.Fatal(err, out2) + } + + out2 = strings.Trim(out2, "\n") + if hostNet == out2 { + t.Fatalf("Net namespace should be different without --net=host %s == %s\n", hostNet, out2) + } + + logDone("run - net host mode") } From 7b0519d1ec8edcec773ef5d1e8f24d6a1b0b29f3 Mon Sep 17 00:00:00 2001 From: Jessica Frazelle Date: Mon, 29 Dec 2014 11:43:18 -0800 Subject: [PATCH 131/513] Add Jenkins Build Status Docker-DCO-1.1-Signed-off-by: Jessica Frazelle (github: jfrazelle) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4cda72070..ed47e2e73 100644 --- a/README.md +++ b/README.md @@ -178,7 +178,7 @@ Contributing to Docker ====================== [![GoDoc](https://godoc.org/github.com/docker/docker?status.png)](https://godoc.org/github.com/docker/docker) -[![Build Status](https://ci.dockerproject.com/github.com/docker/docker/status.svg?branch=master)](https://ci.dockerproject.com/github.com/docker/docker) +[![Jenkins Build Status](https://jenkins.dockerproject.com/job/Docker%20Master/badge/icon)](https://jenkins.dockerproject.com/job/Docker%20Master/) Want to hack on Docker? Awesome! There are instructions to get you started [here](CONTRIBUTING.md). If you'd like to contribute to the From c0bb1c77ee2461f8bceb03202f60f5673815c026 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Sat, 27 Dec 2014 02:38:46 +0000 Subject: [PATCH 132/513] add test Signed-off-by: Victor Vieux --- integration-cli/docker_cli_inspect_test.go | 35 ++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/integration-cli/docker_cli_inspect_test.go b/integration-cli/docker_cli_inspect_test.go index cf42217ac..ee69a89a4 100644 --- a/integration-cli/docker_cli_inspect_test.go +++ b/integration-cli/docker_cli_inspect_test.go @@ -21,3 +21,38 @@ func TestInspectImage(t *testing.T) { logDone("inspect - inspect an image") } + +func TestInspectExecID(t *testing.T) { + defer deleteAllContainers() + + out, exitCode, err := runCommandWithOutput(exec.Command(dockerBinary, "run", "-d", "busybox", "top")) + if exitCode != 0 || err != nil { + t.Fatalf("failed to run container: %s, %v", out, err) + } + id := strings.TrimSuffix(out, "\n") + + out, err = inspectField(id, "ExecIDs") + if err != nil { + t.Fatalf("failed to inspect container: %s, %v", out, err) + } + if out != "" { + t.Fatalf("ExecIDs should be empty, got: %s", out) + } + + exitCode, err = runCommand(exec.Command(dockerBinary, "exec", "-d", id, "ls", "/")) + if exitCode != 0 || err != nil { + t.Fatalf("failed to exec in container: %s, %v", out, err) + } + + out, err = inspectField(id, "ExecIDs") + if err != nil { + t.Fatalf("failed to inspect container: %s, %v", out, err) + } + + out = strings.TrimSuffix(out, "\n") + if out == "[]" || out == "" { + t.Fatalf("ExecIDs should not be empty, got: %s", out) + } + + logDone("inspect - inspect a container with ExecIDs") +} From 35873e747d5681e42582f51ea48993382cb893a6 Mon Sep 17 00:00:00 2001 From: Sven Dowideit Date: Tue, 30 Dec 2014 12:14:08 +1000 Subject: [PATCH 133/513] Document that there is a delay before the --restart policy restart, and that its double the last one Docker-DCO-1.1-Signed-off-by: Sven Dowideit (github: SvenDowideit) Signed-off-by: Sven Dowideit --- docs/sources/reference/api/docker_remote_api_v1.14.md | 2 ++ docs/sources/reference/api/docker_remote_api_v1.15.md | 4 ++++ docs/sources/reference/api/docker_remote_api_v1.16.md | 2 ++ docs/sources/reference/api/docker_remote_api_v1.17.md | 2 ++ docs/sources/reference/commandline/cli.md | 10 ++++++++++ 5 files changed, 20 insertions(+) diff --git a/docs/sources/reference/api/docker_remote_api_v1.14.md b/docs/sources/reference/api/docker_remote_api_v1.14.md index 26dbae21e..8e5952bb5 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.14.md +++ b/docs/sources/reference/api/docker_remote_api_v1.14.md @@ -154,6 +154,8 @@ Json Parameters: exit code is non-zero. If `on-failure` is used, `MaximumRetryCount` controls the number of times to retry before giving up. The default is not to restart. (optional) + An ever increasing delay (double the previous delay, starting at 100mS) + is added before each restart to prevent flooding the server. - **config** – the container's configuration Query Parameters: diff --git a/docs/sources/reference/api/docker_remote_api_v1.15.md b/docs/sources/reference/api/docker_remote_api_v1.15.md index a3c5951e7..84039daad 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.15.md +++ b/docs/sources/reference/api/docker_remote_api_v1.15.md @@ -230,6 +230,8 @@ Json Parameters: exit code is non-zero. If `on-failure` is used, `MaximumRetryCount` controls the number of times to retry before giving up. The default is not to restart. (optional) + An ever increasing delay (double the previous delay, starting at 100mS) + is added before each restart to prevent flooding the server. - **NetworkMode** - Sets the networking mode for the container. Supported values are: `bridge`, `host`, and `container:` - **Devices** - A list of devices to add to the container specified in the @@ -557,6 +559,8 @@ Json Parameters: exit code is non-zero. If `on-failure` is used, `MaximumRetryCount` controls the number of times to retry before giving up. The default is not to restart. (optional) + An ever increasing delay (double the previous delay, starting at 100mS) + is added before each restart to prevent flooding the server. - **NetworkMode** - Sets the networking mode for the container. Supported values are: `bridge`, `host`, and `container:` - **Devices** - A list of devices to add to the container specified in the diff --git a/docs/sources/reference/api/docker_remote_api_v1.16.md b/docs/sources/reference/api/docker_remote_api_v1.16.md index e186a73e6..b4ef52b3e 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.16.md +++ b/docs/sources/reference/api/docker_remote_api_v1.16.md @@ -230,6 +230,8 @@ Json Parameters: exit code is non-zero. If `on-failure` is used, `MaximumRetryCount` controls the number of times to retry before giving up. The default is not to restart. (optional) + An ever increasing delay (double the previous delay, starting at 100mS) + is added before each restart to prevent flooding the server. - **NetworkMode** - Sets the networking mode for the container. Supported values are: `bridge`, `host`, and `container:` - **Devices** - A list of devices to add to the container specified in the diff --git a/docs/sources/reference/api/docker_remote_api_v1.17.md b/docs/sources/reference/api/docker_remote_api_v1.17.md index 7c81a7acb..0164b9581 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.17.md +++ b/docs/sources/reference/api/docker_remote_api_v1.17.md @@ -230,6 +230,8 @@ Json Parameters: exit code is non-zero. If `on-failure` is used, `MaximumRetryCount` controls the number of times to retry before giving up. The default is not to restart. (optional) + An ever increasing delay (double the previous delay, starting at 100mS) + is added before each restart to prevent flooding the server. - **NetworkMode** - Sets the networking mode for the container. Supported values are: `bridge`, `host`, and `container:` - **Devices** - A list of devices to add to the container specified in the diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index 379871fe5..6db4ef7f8 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -1739,6 +1739,16 @@ application change: Using the `--restart` flag on Docker run you can specify a restart policy for how a container should or should not be restarted on exit. +An ever increasing delay (double the previous delay, starting at 100 milliseconds) +is added before each restart to prevent flooding the server. This means the daemaon +will wait for 100 mS, then 200 mS, 400, 800, 1600, and so on until either the +`on-failure` limit is hit, or when you `docker stop` or even `docker rm -f` +the container. + +When a restart policy is active on a container, it will be shown in `docker ps` +as either `Up` or `Restarting` in `docker ps`. It can also be useful to use +`docker events` to see the restart policy in effect. + ** no ** - Do not restart the container when it exits. ** on-failure ** - Restart the container only if it exits with a non zero exit status. From cfaffd1ad26cf64330b87d0f99a883215b4c5a3e Mon Sep 17 00:00:00 2001 From: Sven Dowideit Date: Thu, 18 Dec 2014 16:46:43 +1000 Subject: [PATCH 134/513] Add docs Cloudfront cache invalidation Signed-off-by: Sven Dowideit Docker-DCO-1.1-Signed-off-by: Sven Dowideit (github: SvenDowideit) Signed-off-by: Sven Dowideit --- Makefile | 3 ++- docs/README.md | 8 ++++--- docs/release.sh | 46 ++++++++++++++++++++++++++++++++++++ project/RELEASE-CHECKLIST.md | 7 ++++-- 4 files changed, 58 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 180ae314d..43a6a11c5 100644 --- a/Makefile +++ b/Makefile @@ -53,7 +53,7 @@ docs-shell: docs-build $(DOCKER_RUN_DOCS) -p $(if $(DOCSPORT),$(DOCSPORT):)8000 "$(DOCKER_DOCS_IMAGE)" bash docs-release: docs-build - $(DOCKER_RUN_DOCS) -e OPTIONS -e BUILD_ROOT "$(DOCKER_DOCS_IMAGE)" ./release.sh + $(DOCKER_RUN_DOCS) -e OPTIONS -e BUILD_ROOT -e DISTRIBUTION_ID "$(DOCKER_DOCS_IMAGE)" ./release.sh docs-test: docs-build $(DOCKER_RUN_DOCS) "$(DOCKER_DOCS_IMAGE)" ./test.sh @@ -83,6 +83,7 @@ build: bundles docker build -t "$(DOCKER_IMAGE)" . docs-build: + git diff --name-status upstream/release..upstream/docs docs/ > docs/changed-files cp ./VERSION docs/VERSION echo "$(GIT_BRANCH)" > docs/GIT_BRANCH echo "$(AWS_S3_BUCKET)" > docs/AWS_S3_BUCKET diff --git a/docs/README.md b/docs/README.md index b3e9b3230..b730982e3 100755 --- a/docs/README.md +++ b/docs/README.md @@ -145,11 +145,13 @@ to view your results and make sure what you published is what you wanted. When you're happy with it, publish the docs to our live site: - make AWS_S3_BUCKET=docs.docker.com BUILD_ROOT=yes docs-release + make AWS_S3_BUCKET=docs.docker.com BUILD_ROOT=yes DISTRIBUTION_ID=C2K6......FL2F docs-release Test the uncached version of the live docs at http://docs.docker.com.s3-website-us-east-1.amazonaws.com/ Note that the new docs will not appear live on the site until the cache (a complex, -distributed CDN system) is flushed. This requires someone with S3 keys. Contact Docker -(Sven Dowideit or John Costa) for assistance. +distributed CDN system) is flushed. The `make docs-release` command will do this +_if_ the `DISTRIBUTION_ID` is set to the Cloudfront distribution ID (ask the meta +team) - this will take at least 15 minutes to run and you can check its progress +with the CDN Cloudfront Chrome addin. diff --git a/docs/release.sh b/docs/release.sh index 8df8960c7..f4f6c552f 100755 --- a/docs/release.sh +++ b/docs/release.sh @@ -97,6 +97,50 @@ upload_current_documentation() { $run } +invalidate_cache() { + if [ "" == "$DISTRIBUTION_ID" ]; then + echo "Skipping Cloudfront cache invalidation" + return + fi + + dst=$1 + + #aws cloudfront create-invalidation --profile docs.docker.com --distribution-id $DISTRIBUTION_ID --invalidation-batch '{"Paths":{"Quantity":1, "Items":["'+$file+'"]},"CallerReference":"19dec2014sventest1"}' + aws configure set preview.cloudfront true + + files=($(cat changed-files | grep 'sources/.*$' | sed -E 's#.*docs/sources##' | sed -E 's#index\.md#index.html#' | sed -E 's#\.md#/index.html#')) + files[${#files[@]}]="/index.html" + files[${#files[@]}]="/versions.html_fragment" + + len=${#files[@]} + + echo "aws cloudfront create-invalidation --profile docs.docker.com --distribution-id $DISTRIBUTION_ID --invalidation-batch '" > batchfile + echo "{\"Paths\":{\"Quantity\":$len," >> batchfile + echo "\"Items\": [" >> batchfile + + #for file in $(cat changed-files | grep 'sources/.*$' | sed -E 's#.*docs/sources##' | sed -E 's#index\.md#index.html#' | sed -E 's#\.md#/index.html#') + for file in "${files[@]}" + do + if [ "$file" == "${files[${#files[@]}-1]}" ]; then + comma="" + else + comma="," + fi + echo "\"$dst$file\"$comma" >> batchfile + done + + echo "]}, \"CallerReference\":" >> batchfile + echo "\"$(date)\"}'" >> batchfile + + + echo "-----" + cat batchfile + echo "-----" + sh batchfile + echo "-----" +} + + if [ "$OPTIONS" != "--dryrun" ]; then setup_s3 fi @@ -106,6 +150,7 @@ if [ "$BUILD_ROOT" == "yes" ]; then echo "Building root documentation" build_current_documentation upload_current_documentation + invalidate_cache fi #build again with /v1.0/ prefix @@ -113,3 +158,4 @@ sed -i "s/^site_url:.*/site_url: \/$MAJOR_MINOR\//" mkdocs.yml echo "Building the /$MAJOR_MINOR/ documentation" build_current_documentation upload_current_documentation "/$MAJOR_MINOR/" +invalidate_cache "/$MAJOR_MINOR" diff --git a/project/RELEASE-CHECKLIST.md b/project/RELEASE-CHECKLIST.md index 61b0c2b07..7eeaae8ce 100644 --- a/project/RELEASE-CHECKLIST.md +++ b/project/RELEASE-CHECKLIST.md @@ -267,14 +267,17 @@ git checkout -b docs release || git checkout docs git fetch git reset --hard origin/release git push -f origin docs -make AWS_S3_BUCKET=docs.docker.com BUILD_ROOT=yes docs-release +make AWS_S3_BUCKET=docs.docker.com BUILD_ROOT=yes DISTRIBUTION_ID=C2K6......FL2F docs-release ``` The docs will appear on http://docs.docker.com/ (though there may be cached versions, so its worth checking http://docs.docker.com.s3-website-us-east-1.amazonaws.com/). For more information about documentation releases, see `docs/README.md`. -Ask Sven, or JohnC to invalidate the cloudfront cache using the CND Planet chrome applet. +Note that the new docs will not appear live on the site until the cache (a complex, +distributed CDN system) is flushed. The `make docs-release` command will do this +_if_ the `DISTRIBUTION_ID` is set correctly - this will take at least 15 minutes to run +and you can check its progress with the CDN Cloudfront Chrome addin. ### 12. Create a new pull request to merge release back into master From 1d4a13867053b58971424f8caf219f935fc66fdd Mon Sep 17 00:00:00 2001 From: Sven Dowideit Date: Fri, 5 Dec 2014 14:31:54 +1000 Subject: [PATCH 135/513] Talk up the 1.4 change to initialise volumes at time Docker-DCO-1.1-Signed-off-by: Sven Dowideit (github: SvenDowideit) Signed-off-by: Sven Dowideit --- docs/sources/reference/commandline/cli.md | 31 ++++++++++++++++++++++- docs/sources/userguide/dockervolumes.md | 11 +++++--- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index 379871fe5..fb314e240 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -713,13 +713,42 @@ Note that volumes set by `create` may be over-ridden by options set with Please see the [run command](#run) section for more details. -#### Example +#### Examples $ sudo docker create -t -i fedora bash 6d8af538ec541dd581ebc2a24153a28329acb5268abe5ef868c1f1a261221752 $ sudo docker start -a -i 6d8af538ec5 bash-4.2# +As of v1.4.0 container volumes are initialized during the `docker create` +phase (i.e., `docker run` too). For example, this allows you to `create` the +`data` volume container, and then use it from another container: + + $ docker create -v /data --name data ubuntu + 240633dfbb98128fa77473d3d9018f6123b99c454b3251427ae190a7d951ad57 + $ docker run --rm --volumes-from data ubuntu ls -la /data + total 8 + drwxr-xr-x 2 root root 4096 Dec 5 04:10 . + drwxr-xr-x 48 root root 4096 Dec 5 04:11 .. + +Similarly, `create` a host directory bind mounted volume container, which +can then be used from the subsequent container: + + $ docker create -v /home/docker:/docker --name docker ubuntu + 9aa88c08f319cd1e4515c3c46b0de7cc9aa75e878357b1e96f91e2c773029f03 + $ docker run --rm --volumes-from docker ubuntu ls -la /docker + total 20 + drwxr-sr-x 5 1000 staff 180 Dec 5 04:00 . + drwxr-xr-x 48 root root 4096 Dec 5 04:13 .. + -rw-rw-r-- 1 1000 staff 3833 Dec 5 04:01 .ash_history + -rw-r--r-- 1 1000 staff 446 Nov 28 11:51 .ashrc + -rw-r--r-- 1 1000 staff 25 Dec 5 04:00 .gitconfig + drwxr-sr-x 3 1000 staff 60 Dec 1 03:28 .local + -rw-r--r-- 1 1000 staff 920 Nov 28 11:51 .profile + drwx--S--- 2 1000 staff 460 Dec 5 00:51 .ssh + drwxr-xr-x 32 1000 staff 1140 Dec 5 04:01 docker + + ## diff List the changed files and directories in a container᾿s filesystem diff --git a/docs/sources/userguide/dockervolumes.md b/docs/sources/userguide/dockervolumes.md index 4663683c5..0e0e7a787 100644 --- a/docs/sources/userguide/dockervolumes.md +++ b/docs/sources/userguide/dockervolumes.md @@ -24,6 +24,7 @@ containers that bypasses the [*Union File System*](/terms/layer/#union-file-system) to provide several useful features for persistent or shared data: +- Volumes are initialized when a container is created - Data volumes can be shared and reused between containers - Changes to a data volume are made directly - Changes to a data volume will not be included when you update an image @@ -32,9 +33,9 @@ persistent or shared data: ### Adding a data volume You can add a data volume to a container using the `-v` flag with the -`docker run` command. You can use the `-v` multiple times in a single -`docker run` to mount multiple data volumes. Let's mount a single volume -now in our web application container. +`docker create` and `docker run` command. You can use the `-v` multiple times +to mount multiple data volumes. Let's mount a single volume now in our web +application container. $ sudo docker run -d -P --name web -v /webapp training/webapp python app.py @@ -105,8 +106,10 @@ create a named Data Volume Container, and then to mount the data from it. Let's create a new named container with a volume to share. +While this container doesn't run an application, it reuses the `training/postgres` +image so that all containers are using layers in common, saveing disk space. - $ sudo docker run -d -v /dbdata --name dbdata training/postgres echo Data-only container for postgres + $ sudo docker create -v /dbdata --name dbdata training/postgres You can then use the `--volumes-from` flag to mount the `/dbdata` volume in another container. From e52988528d20a729ed71e47e4d0a5fcb35c92dfc Mon Sep 17 00:00:00 2001 From: Sven Dowideit Date: Thu, 4 Dec 2014 11:12:16 +1000 Subject: [PATCH 136/513] Add a note to point out to new users that B2D bind-mounts are special-ish Docker-DCO-1.1-Signed-off-by: Sven Dowideit (github: SvenDowideit) Signed-off-by: Sven Dowideit --- docs/sources/userguide/dockervolumes.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/sources/userguide/dockervolumes.md b/docs/sources/userguide/dockervolumes.md index 4663683c5..0af2694e9 100644 --- a/docs/sources/userguide/dockervolumes.md +++ b/docs/sources/userguide/dockervolumes.md @@ -47,7 +47,15 @@ This will create a new volume inside a container at `/webapp`. ### Mount a Host Directory as a Data Volume In addition to creating a volume using the `-v` flag you can also mount a -directory from your own host into a container. +directory from your Docker daemon's host into a container. + +> **Note:** +> If you are using Boot2Docker, your Docker daemon only has limited access to +> your OSX/Windows filesystem. Boot2Docker tries to auto-share your `/Users` +> (OSX) or `C:\Users` (Windows) directory - and so you can mount files or directories +> using `docker run -v /Users/:/ ...` (OSX) or +> `docker run -v /c/Users/:/ come from the Boot2Docker virtual machine's filesystem. $ sudo docker run -d -P --name web -v /src/webapp:/opt/webapp training/webapp python app.py @@ -67,8 +75,8 @@ create it for you. > **Note:** > This is not available from a `Dockerfile` due to the portability -> and sharing purpose of it. As the host directory is, by its nature, -> host-dependent, a host directory specified in a `Dockerfile` probably +> and sharing purpose of built images. The host directory is, by its nature, +> host-dependent, so a host directory specified in a `Dockerfile` probably > wouldn't work on all hosts. Docker defaults to a read-write volume but we can also mount a directory From e704dd31e79114a2156c4fdda3247a181ad6435d Mon Sep 17 00:00:00 2001 From: Eric Windisch Date: Tue, 30 Dec 2014 14:34:35 -0500 Subject: [PATCH 137/513] Improve security doc Moves some information around, expanding information on user namespaces, pull/load security, cap add/drop. Also includes various grammar improvements and edits. Signed-off-by: Eric Windisch --- docs/sources/articles/security.md | 110 ++++++++++++++++++------------ 1 file changed, 66 insertions(+), 44 deletions(-) diff --git a/docs/sources/articles/security.md b/docs/sources/articles/security.md index 731638025..8d7e9da53 100644 --- a/docs/sources/articles/security.md +++ b/docs/sources/articles/security.md @@ -4,21 +4,20 @@ page_keywords: Docker, Docker documentation, security # Docker Security -> *Adapted from* [Containers & Docker: How Secure are -> They?](http://blog.docker.com/2013/08/containers-docker-how-secure-are-they/) - There are three major areas to consider when reviewing Docker security: - - the intrinsic security of containers, as implemented by kernel + - the intrinsic security of the kernel and its support for namespaces and cgroups; - the attack surface of the Docker daemon itself; + - loopholes in the container configuration profile, either by default, + or when customized by users. - the "hardening" security features of the kernel and how they interact with containers. ## Kernel Namespaces -Docker containers are very similar to LXC containers, and they come with -the similar security features. When you start a container with `docker +Docker containers are very similar to LXC containers, and they have +similar security features. When you start a container with `docker run`, behind the scenes Docker creates a set of namespaces and control groups for the container. @@ -28,7 +27,7 @@ less affect, processes running in another container, or in the host system. **Each container also gets its own network stack**, meaning that a -container doesn't get a privileged access to the sockets or interfaces +container doesn't get privileged access to the sockets or interfaces of another container. Of course, if the host system is setup accordingly, containers can interact with each other through their respective network interfaces — just like they can interact with @@ -56,9 +55,9 @@ in 2005, so both the design and the implementation are pretty mature. ## Control Groups -Control Groups are the other key component of Linux Containers. They -implement resource accounting and limiting. They provide a lot of very -useful metrics, but they also help to ensure that each container gets +Control Groups are another key component of Linux Containers. They +implement resource accounting and limiting. They provide many +useful metrics, but they also help ensure that each container gets its fair share of memory, CPU, disk I/O; and, more importantly, that a single container cannot bring the system down by exhausting one of those resources. @@ -86,10 +85,9 @@ the Docker host and a guest container; and it allows you to do so without limiting the access rights of the container. This means that you can start a container where the `/host` directory will be the `/` directory on your host; and the container will be able to alter your host filesystem -without any restriction. This sounds crazy? Well, you have to know that -**all virtualization systems allowing filesystem resource sharing behave the -same way**. Nothing prevents you from sharing your root filesystem (or -even your root block device) with a virtual machine. +without any restriction. This is similar to how virtualization systems +allow filesystem resource sharing. Nothing prevents you from sharing your +root filesystem (or even your root block device) with a virtual machine. This has a strong security implication: for example, if you instrument Docker from a web server to provision containers through an API, you should be @@ -112,25 +110,21 @@ trusted network or VPN; or protected with e.g., `stunnel` and client SSL certificates. You can also secure them with [HTTPS and certificates](/articles/https/). -Recent improvements in Linux namespaces will soon allow to run -full-featured containers without root privileges, thanks to the new user -namespace. This is covered in detail [here]( -http://s3hh.wordpress.com/2013/07/19/creating-and-using-containers-without-privilege/). -Moreover, this will solve the problem caused by sharing filesystems -between host and guest, since the user namespace allows users within -containers (including the root user) to be mapped to other users in the -host system. +The daemon is also potentially vulnerable to other inputs, such as image +loading from either disk with 'docker load', or from the network with +'docker pull'. This has been a focus of improvement in the community, +especially for 'pull' security. While these overlap, it should be noted +that 'docker load' is a mechanism for backup and restore and is not +currently considered a secure mechanism for loading images. As of +Docker 1.3.2, images are now extracted in a chrooted subprocess on +Linux/Unix platforms, being the first-step in a wider effort toward +privilege separation. -The end goal for Docker is therefore to implement two additional -security improvements: - - - map the root user of a container to a non-root user of the Docker - host, to mitigate the effects of a container-to-host privilege - escalation; - - allow the Docker daemon to run without root privileges, and delegate - operations requiring those privileges to well-audited sub-processes, - each with its own (very limited) scope: virtual network setup, - filesystem management, etc. +Eventually, it is expected that the Docker daemon will run restricted +privileges, delegating operations well-audited sub-processes, +each with its own (very limited) scope of Linux capabilities, +virtual network setup, filesystem management, etc. That is, most likely, +pieces of the Docker engine itself will run inside of containers. Finally, if you run Docker on a server, it is recommended to run exclusively Docker in the server, and move all other services within @@ -140,7 +134,7 @@ existing monitoring/supervision processes (e.g., NRPE, collectd, etc). ## Linux Kernel Capabilities -By default, Docker starts containers with a very restricted set of +By default, Docker starts containers with a restricted set of capabilities. What does that mean? Capabilities turn the binary "root/non-root" dichotomy into a @@ -159,7 +153,7 @@ tools (e.g., to handle DHCP, WPA, or VPNs), and much more. A container is very different, because almost all of those tasks are handled by the infrastructure around the container: - - SSH access will typically be managed by a single server running in + - SSH access will typically be managed by a single server running on the Docker host; - `cron`, when necessary, should run as a user process, dedicated and tailored for the app that needs its @@ -201,11 +195,16 @@ a whitelist instead of a blacklist approach. You can see a full list of available capabilities in [Linux manpages](http://man7.org/linux/man-pages/man7/capabilities.7.html). -Of course, you can always enable extra capabilities if you really need -them (for instance, if you want to use a FUSE-based filesystem), but by -default, Docker containers use only a -[whitelist](https://github.com/docker/docker/blob/master/daemon/execdriver/native/template/default_template.go) -of kernel capabilities by default. +One primary risk with running Docker containers is that the default set +of capabilities and mounts given to a container may provide incomplete +isolation, either independently, or when used in combination with +kernel vulnerabilities. + +Docker supports the addition and removal of capabilities, allowing use +of a non-default profile. This may make Docker more secure through +capability removal, or less secure through the addition of capabilities. +The best practice for users would be to remove all capabilities except +those explicitly required for their processes. ## Other Kernel Security Features @@ -222,7 +221,7 @@ harden a Docker host. Here are a few examples. checks, both at compile-time and run-time; it will also defeat many exploits, thanks to techniques like address randomization. It doesn't require Docker-specific configuration, since those security features - apply system-wide, independently of containers. + apply system-wide, independent of containers. - If your distribution comes with security model templates for Docker containers, you can use them out of the box. For instance, we ship a template that works with AppArmor and Red Hat comes with SELinux @@ -236,6 +235,27 @@ with e.g., special network topologies or shared filesystems, you can expect to see tools to harden existing Docker containers without affecting Docker's core. +Recent improvements in Linux namespaces will soon allow to run +full-featured containers without root privileges, thanks to the new user +namespace. This is covered in detail [here]( +http://s3hh.wordpress.com/2013/07/19/creating-and-using-containers-without-privilege/). +Moreover, this will solve the problem caused by sharing filesystems +between host and guest, since the user namespace allows users within +containers (including the root user) to be mapped to other users in the +host system. + +Today, Docker does not directly support user namespaces, but they +may still be utilized by Docker containers on supported kernels, +by directly using the clone syscall, or utilizing the 'unshare' +utility. Using this, some users may find it possible to drop +more capabilities from their process as user namespaces provide +an artifical capabilities set. Likewise, however, this artifical +capabilities set may require use of 'capsh' to restrict the +user-namespace capabilities set when using 'unshare'. + +Eventually, it is expected that Docker will direct, native support +for user-namespaces, simplifying the process of hardening containers. + ## Conclusions Docker containers are, by default, quite secure; especially if you take @@ -246,9 +266,11 @@ You can add an extra layer of safety by enabling Apparmor, SELinux, GRSEC, or your favorite hardening solution. Last but not least, if you see interesting security features in other -containerization systems, you will be able to implement them as well -with Docker, since everything is provided by the kernel anyway. +containerization systems, these are simply kernels features that may +be implemented in Docker as well. We welcome users to submit issues, +pull requests, and communicate via the mailing list. -For more context and especially for comparisons with VMs and other -container systems, please also see the [original blog post]( +References: +* [Docker Containers: How Secure Are They? (2013)]( http://blog.docker.com/2013/08/containers-docker-how-secure-are-they/). +* [On the Security of Containers (2014)](https://medium.com/@ewindisch/on-the-security-of-containers-2c60ffe25a9e). From 4e679f218c5da33856df25069368b24e78d8bb85 Mon Sep 17 00:00:00 2001 From: Nate Eagleson Date: Tue, 30 Dec 2014 21:36:16 -0500 Subject: [PATCH 138/513] Grammar fix and style tweak in docs/sources/faq.md Just a few things I thought could be improved in the FAQ. Signed-off-by: Nate Eagleson --- docs/sources/faq.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/sources/faq.md b/docs/sources/faq.md index 5e1669843..169df37f5 100644 --- a/docs/sources/faq.md +++ b/docs/sources/faq.md @@ -8,7 +8,7 @@ page_keywords: faq, questions, documentation, docker ### How much does Docker cost? -Docker is 100% free, it is open source, so you can use it without +Docker is 100% free. It is open source, so you can use it without paying. ### What open source license are you using? @@ -19,12 +19,12 @@ https://github.com/docker/docker/blob/master/LICENSE) ### Does Docker run on Mac OS X or Windows? -Not at this time, Docker currently only runs on Linux, but you can use -VirtualBox to run Docker in a virtual machine on your box, and get the -best of both worlds. Check out the [*Mac OS X*](../installation/mac/#macosx) -and [*Microsoft Windows*](../installation/windows/#windows) installation -guides. The small Linux distribution boot2docker can be run inside virtual -machines on these two operating systems. +Docker currently runs only on Linux, but you can use VirtualBox to run +Docker in a virtual machine on your box, and get the best of both worlds. +Check out the [*Mac OS X*](../installation/mac/#macosx) and [*Microsoft +Windows*](../installation/windows/#windows) installation guides. The small +Linux distribution boot2docker can be run inside virtual machines on these +two operating systems. ### How do containers compare to virtual machines? From 50eb14244d3139e8aa9141d8f7076043ffecbc6e Mon Sep 17 00:00:00 2001 From: Harald Albers Date: Mon, 29 Dec 2014 19:21:45 +0100 Subject: [PATCH 139/513] Add missing options to bash completion Signed-off-by: Harald Albers --- contrib/completion/bash/docker | 51 ++++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/contrib/completion/bash/docker b/contrib/completion/bash/docker index 7fcbfca54..cf253e9ce 100755 --- a/contrib/completion/bash/docker +++ b/contrib/completion/bash/docker @@ -368,7 +368,18 @@ _docker_kill() { } _docker_load() { - return + case "$prev" in + -i|--input) + _filedir + return + ;; + esac + + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "-i --input" -- "$cur" ) ) + ;; + esac } _docker_login() { @@ -516,9 +527,11 @@ _docker_run() { --env-file --expose -h --hostname + --ipc --link --lxc-conf -m --memory + --mac-address --name --net -p --publish @@ -577,6 +590,21 @@ _docker_run() { compopt -o nospace return ;; + --ipc) + case "$cur" in + *:*) + cur="${cur#*:}" + __docker_containers_running + ;; + *) + COMPREPLY=( $( compgen -W 'host container:' -- "$cur" ) ) + if [ "$COMPREPLY" = "container:" ]; then + compopt -o nospace + fi + ;; + esac + return + ;; --link) case "$cur" in *:*) @@ -644,7 +672,7 @@ _docker_run() { esac return ;; - --entrypoint|-h|--hostname|-m|--memory|-u|--user|-w|--workdir|--cpuset|-c|--cpu-shares|-n|--name|-p|--publish|--expose|--dns|--lxc-conf|--dns-search) + --entrypoint|-h|--hostname|--mac-address|-m|--memory|-u|--user|-w|--workdir|--cpuset|-c|--cpu-shares|-n|--name|-p|--publish|--expose|--dns|--lxc-conf|--dns-search) return ;; esac @@ -664,10 +692,21 @@ _docker_run() { } _docker_save() { - local counter=$(__docker_pos_first_nonflag) - if [ $cword -eq $counter ]; then - __docker_image_repos_and_tags_and_ids - fi + case "$prev" in + -o|--output) + _filedir + return + ;; + esac + + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "-o --output" -- "$cur" ) ) + ;; + *) + __docker_image_repos_and_tags_and_ids + ;; + esac } _docker_search() { From 70161b4d45ec990a4e597e133092e389e168ad79 Mon Sep 17 00:00:00 2001 From: Harald Albers Date: Tue, 30 Dec 2014 19:18:21 +0100 Subject: [PATCH 140/513] Sort options in bash completion alphabetically This introduces a sort order for options: Arrange options sorted alphabetically by long name with the short options immediately following their corresponding long form. Signed-off-by: Harald Albers --- contrib/completion/bash/docker | 135 +++++++++++++++++---------------- 1 file changed, 70 insertions(+), 65 deletions(-) diff --git a/contrib/completion/bash/docker b/contrib/completion/bash/docker index cf253e9ce..7dd23b853 100755 --- a/contrib/completion/bash/docker +++ b/contrib/completion/bash/docker @@ -20,6 +20,11 @@ # bound to the default communication port/socket # If the docker daemon is using a unix socket for communication your user # must have access to the socket for the completions to function correctly +# +# Note for developers: +# Please arrange options sorted alphabetically by long name with the short +# options immediately following their corresponding long form. +# This order should be applied to lists, alternatives and code blocks. __docker_q() { docker 2>/dev/null "$@" @@ -181,7 +186,7 @@ _docker_attach() { _docker_build() { case "$prev" in - -t|--tag) + --tag|-t) __docker_image_repos_and_tags return ;; @@ -189,10 +194,10 @@ _docker_build() { case "$cur" in -*) - COMPREPLY=( $( compgen -W "-t --tag -q --quiet --no-cache --rm --force-rm" -- "$cur" ) ) + COMPREPLY=( $( compgen -W "--force-rm --no-cache --quiet -q --rm --tag -t" -- "$cur" ) ) ;; *) - local counter="$(__docker_pos_first_nonflag '-t|--tag')" + local counter="$(__docker_pos_first_nonflag '--tag|-t')" if [ $cword -eq $counter ]; then _filedir -d fi @@ -202,17 +207,17 @@ _docker_build() { _docker_commit() { case "$prev" in - -m|--message|-a|--author|--run) + --author|-a|--message|-m|--run) return ;; esac case "$cur" in -*) - COMPREPLY=( $( compgen -W "-m --message -a --author --run" -- "$cur" ) ) + COMPREPLY=( $( compgen -W "--author -a --message -m --run" -- "$cur" ) ) ;; *) - local counter=$(__docker_pos_first_nonflag '-m|--message|-a|--author|--run') + local counter=$(__docker_pos_first_nonflag '--author|-a|--message|-m|--run') if [ $cword -eq $counter ]; then __docker_containers_all @@ -279,7 +284,7 @@ _docker_events() { _docker_exec() { case "$cur" in -*) - COMPREPLY=( $( compgen -W "-d --detach -i --interactive -t --tty" -- "$cur" ) ) + COMPREPLY=( $( compgen -W "--detach -d --interactive -i -t --tty" -- "$cur" ) ) ;; *) __docker_containers_running @@ -304,7 +309,7 @@ _docker_help() { _docker_history() { case "$cur" in -*) - COMPREPLY=( $( compgen -W "-q --quiet --no-trunc" -- "$cur" ) ) + COMPREPLY=( $( compgen -W "--no-trunc --quiet -q" -- "$cur" ) ) ;; *) local counter=$(__docker_pos_first_nonflag) @@ -318,7 +323,7 @@ _docker_history() { _docker_images() { case "$cur" in -*) - COMPREPLY=( $( compgen -W "-q --quiet -a --all --no-trunc -v --viz -t --tree" -- "$cur" ) ) + COMPREPLY=( $( compgen -W "--all -a --no-trunc --quiet -q" -- "$cur" ) ) ;; *) local counter=$(__docker_pos_first_nonflag) @@ -348,14 +353,14 @@ _docker_info() { _docker_inspect() { case "$prev" in - -f|--format) + --format|-f) return ;; esac case "$cur" in -*) - COMPREPLY=( $( compgen -W "-f --format" -- "$cur" ) ) + COMPREPLY=( $( compgen -W "--format -f" -- "$cur" ) ) ;; *) __docker_containers_and_images @@ -369,7 +374,7 @@ _docker_kill() { _docker_load() { case "$prev" in - -i|--input) + --input|-i) _filedir return ;; @@ -377,21 +382,21 @@ _docker_load() { case "$cur" in -*) - COMPREPLY=( $( compgen -W "-i --input" -- "$cur" ) ) + COMPREPLY=( $( compgen -W "--input -i" -- "$cur" ) ) ;; esac } _docker_login() { case "$prev" in - -u|--username|-p|--password|-e|--email) + --email|-e|--password|-p|--username|-u) return ;; esac case "$cur" in -*) - COMPREPLY=( $( compgen -W "-u --username -p --password -e --email" -- "$cur" ) ) + COMPREPLY=( $( compgen -W "--email -e --password -p --username -u" -- "$cur" ) ) ;; esac } @@ -399,7 +404,7 @@ _docker_login() { _docker_logs() { case "$cur" in -*) - COMPREPLY=( $( compgen -W "-f --follow" -- "$cur" ) ) + COMPREPLY=( $( compgen -W "--follow -f" -- "$cur" ) ) ;; *) local counter=$(__docker_pos_first_nonflag) @@ -426,7 +431,7 @@ _docker_port() { _docker_ps() { case "$prev" in - --since|--before) + --before|--since) __docker_containers_all ;; -n) @@ -436,24 +441,24 @@ _docker_ps() { case "$cur" in -*) - COMPREPLY=( $( compgen -W "-q --quiet -s --size -a --all --no-trunc -l --latest --since --before -n" -- "$cur" ) ) + COMPREPLY=( $( compgen -W "--all -a --before --latest -l --no-trunc -n --quiet -q --size -s --since" -- "$cur" ) ) ;; esac } _docker_pull() { case "$prev" in - -t|--tag) + --tag|-t) return ;; esac case "$cur" in -*) - COMPREPLY=( $( compgen -W "-t --tag" -- "$cur" ) ) + COMPREPLY=( $( compgen -W "--tag -t" -- "$cur" ) ) ;; *) - local counter=$(__docker_pos_first_nonflag '-t|--tag') + local counter=$(__docker_pos_first_nonflag '--tag|-t') if [ $cword -eq $counter ]; then __docker_image_repos_and_tags fi @@ -470,14 +475,14 @@ _docker_push() { _docker_restart() { case "$prev" in - -t|--time) + --time|-t) return ;; esac case "$cur" in -*) - COMPREPLY=( $( compgen -W "-t --time" -- "$cur" ) ) + COMPREPLY=( $( compgen -W "--time -t" -- "$cur" ) ) ;; *) __docker_containers_all @@ -488,13 +493,13 @@ _docker_restart() { _docker_rm() { case "$cur" in -*) - COMPREPLY=( $( compgen -W "-f --force -l --link -v --volumes" -- "$cur" ) ) + COMPREPLY=( $( compgen -W "--force -f --link -l --volumes -v" -- "$cur" ) ) return ;; *) for arg in "${COMP_WORDS[@]}"; do case "$arg" in - -f|--force) + --force|-f) __docker_containers_all return ;; @@ -512,64 +517,72 @@ _docker_rmi() { _docker_run() { local options_with_args=" - -a --attach --add-host + --attach -a --cap-add --cap-drop - -c --cpu-shares --cidfile --cpuset + --cpu-shares -c --device --dns --dns-search - -e --env --entrypoint + --env -e --env-file --expose - -h --hostname + --hostname -h --ipc --link --lxc-conf - -m --memory --mac-address + --memory -m --name --net - -p --publish + --publish -p --restart --security-opt - -u --user + --user -u --volumes-from - -v --volume - -w --workdir + --volume -v + --workdir -w " local all_options="$options_with_args - -i --interactive - -P --publish-all + --interactive -i --privileged - -t --tty + --publish-all -P + --tty -t " [ "$command" = "run" ] && all_options="$all_options - -d --detach + --detach -d --rm --sig-proxy " case "$prev" in - -a|--attach) + --add-host) + case "$cur" in + *:) + __docker_resolve_hostname + return + ;; + esac + ;; + --attach|-a) COMPREPLY=( $( compgen -W 'stdin stdout stderr' -- "$cur" ) ) return ;; + --cap-add|--cap-drop) + __docker_capabilities + return + ;; --cidfile|--env-file) _filedir return ;; - --volumes-from) - __docker_containers_all - return - ;; - -v|--volume|--device) + --device|-d|--volume) case "$cur" in *:*) # TODO somehow do _filedir for stuff inside the image, if it's already specified (which is also somewhat difficult to determine) @@ -585,7 +598,7 @@ _docker_run() { esac return ;; - -e|--env) + --env|-e) COMPREPLY=( $( compgen -e -- "$cur" ) ) compopt -o nospace return @@ -617,18 +630,6 @@ _docker_run() { esac return ;; - --add-host) - case "$cur" in - *:) - __docker_resolve_hostname - return - ;; - esac - ;; - --cap-add|--cap-drop) - __docker_capabilities - return - ;; --net) case "$cur" in container:*) @@ -672,7 +673,11 @@ _docker_run() { esac return ;; - --entrypoint|-h|--hostname|--mac-address|-m|--memory|-u|--user|-w|--workdir|--cpuset|-c|--cpu-shares|-n|--name|-p|--publish|--expose|--dns|--lxc-conf|--dns-search) + --volumes-from) + __docker_containers_all + return + ;; + --cpuset|--cpu-shares|-c|--dns|--dns-search|--entrypoint|--expose|--hostname|-h|--lxc-conf|--mac-address|--memory|-m|--name|-n|--publish|-p|--user|-u|--workdir|-w) return ;; esac @@ -693,7 +698,7 @@ _docker_run() { _docker_save() { case "$prev" in - -o|--output) + --output|-o) _filedir return ;; @@ -711,14 +716,14 @@ _docker_save() { _docker_search() { case "$prev" in - -s|--stars) + --stars|-s) return ;; esac case "$cur" in -*) - COMPREPLY=( $( compgen -W "--no-trunc --automated -s --stars" -- "$cur" ) ) + COMPREPLY=( $( compgen -W "--automated --no-trunc --stars -s" -- "$cur" ) ) ;; esac } @@ -726,7 +731,7 @@ _docker_search() { _docker_start() { case "$cur" in -*) - COMPREPLY=( $( compgen -W "-a --attach -i --interactive" -- "$cur" ) ) + COMPREPLY=( $( compgen -W "--attach -a --interactive -i" -- "$cur" ) ) ;; *) __docker_containers_stopped @@ -736,14 +741,14 @@ _docker_start() { _docker_stop() { case "$prev" in - -t|--time) + --time|-t) return ;; esac case "$cur" in -*) - COMPREPLY=( $( compgen -W "-t --time" -- "$cur" ) ) + COMPREPLY=( $( compgen -W "--time -t" -- "$cur" ) ) ;; *) __docker_containers_running @@ -754,7 +759,7 @@ _docker_stop() { _docker_tag() { case "$cur" in -*) - COMPREPLY=( $( compgen -W "-f --force" -- "$cur" ) ) + COMPREPLY=( $( compgen -W "--force -f" -- "$cur" ) ) ;; *) local counter=$(__docker_pos_first_nonflag) From 3c03827e73647cad27a0656ce685c8aea8ed4d21 Mon Sep 17 00:00:00 2001 From: daehyeok mun Date: Sun, 16 Nov 2014 02:54:21 +0900 Subject: [PATCH 141/513] Add warnning log when other graphdrvier(storage driver) used before added warnning log when other graphdrvier(storage driver) used before for feature request #8270 Signed-off-by: Daehyeok Mun --- daemon/graphdriver/driver.go | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/daemon/graphdriver/driver.go b/daemon/graphdriver/driver.go index d96961472..7a0c0d1c5 100644 --- a/daemon/graphdriver/driver.go +++ b/daemon/graphdriver/driver.go @@ -5,7 +5,9 @@ import ( "fmt" "os" "path" + "strings" + log "github.com/Sirupsen/logrus" "github.com/docker/docker/pkg/archive" ) @@ -125,18 +127,37 @@ func New(root string, options []string) (driver Driver, err error) { } return nil, err } + checkPriorDriver(name, root) return driver, nil } // Check all registered drivers if no priority driver is found - for _, initFunc := range drivers { + for name, initFunc := range drivers { if driver, err = initFunc(root, options); err != nil { if err == ErrNotSupported || err == ErrPrerequisites || err == ErrIncompatibleFS { continue } return nil, err } + checkPriorDriver(name, root) return driver, nil } return nil, fmt.Errorf("No supported storage backend found") } + +func checkPriorDriver(name string, root string) error { + + var priorDrivers []string + + for prior := range drivers { + if _, err := os.Stat(path.Join(root, prior)); err == nil && prior != name { + priorDrivers = append(priorDrivers, prior) + } + } + + if len(priorDrivers) > 0 { + log.Warnf("graphdriver %s selected. Warning: your graphdriver directory %s already contains data managed by other graphdrivers: %s", name, root, strings.Join(priorDrivers, ",")) + } + + return nil +} From 4669d98bff6cde7a54cff702c7adbb723aab0687 Mon Sep 17 00:00:00 2001 From: Fred Lifton Date: Fri, 2 Jan 2015 11:18:26 -0800 Subject: [PATCH 142/513] Corrects link to point to Fig. Docker-DCO-1.1-Signed-off-by: Fred Lifton (github: fredlf) --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 710e967d0..9f932e104 100644 --- a/README.md +++ b/README.md @@ -213,9 +213,9 @@ license text. Other Docker Related Projects ============================= -Leveraging Docker as the core technology for managing Linux containers on a -single host, the following projects are also under development to provide a -more comprehensive set of tooling to help round out the Docker platform: +There are a number of projects under development that are based on Docker's +core technology. These projects expand the tooling built around the +Docker platform to broaden its application and utility. * [Docker Registry](https://github.com/docker/docker-registry): Registry server for Docker (hosting/delivering of repositories and images) @@ -223,6 +223,6 @@ server for Docker (hosting/delivering of repositories and images) for a container-centric world * [Docker Swarm](https://github.com/docker/swarm): A Docker-native clustering system -* [Docker Compose](https://github.com/docker/docker/issues/9694): +* [Docker Compose, aka Fig](https://github.com/docker/fig): Multi-container application management From 686585f33a80f7dfaa45400b700235d0615b7fce Mon Sep 17 00:00:00 2001 From: Nathan Hsieh Date: Mon, 29 Dec 2014 10:39:09 -0800 Subject: [PATCH 143/513] updated search api docs to include pagination changes Signed-off-by: Nathan Hsieh --- docs/sources/reference/api/registry_api.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/sources/reference/api/registry_api.md b/docs/sources/reference/api/registry_api.md index 43a463cd5..864816214 100644 --- a/docs/sources/reference/api/registry_api.md +++ b/docs/sources/reference/api/registry_api.md @@ -514,7 +514,7 @@ Search the Index given a search term. It accepts **Example request**: - GET /v1/search?q=search_term HTTP/1.1 + GET /v1/search?q=search_term&page=1&n=25 HTTP/1.1 Host: index.docker.io Accept: application/json @@ -536,6 +536,8 @@ Search the Index given a search term. It accepts Query Parameters: - **q** – what you want to search for +- **n** - number of results you want returned per page (default: 25) +- **page** - page number of results Status Codes: From 2338a9cf5a1ba5576b92e49065335a9c9251ade0 Mon Sep 17 00:00:00 2001 From: Srini Brahmaroutu Date: Mon, 3 Nov 2014 18:15:55 +0000 Subject: [PATCH 144/513] add ability to publish range of ports Closes #8899 Signed-off-by: Srini Brahmaroutu --- docs/man/docker-create.1.md | 4 +- docs/man/docker-run.1.md | 6 +- docs/sources/reference/commandline/cli.md | 6 +- docs/sources/reference/run.md | 9 ++- integration-cli/docker_cli_create_test.go | 99 +++++++++++++++++++++++ integration-cli/docker_cli_run_test.go | 24 ++++++ nat/nat.go | 47 +++++++---- nat/nat_test.go | 98 +++++++++++++++++++++- pkg/parsers/parsers.go | 25 ++++++ pkg/parsers/parsers_test.go | 33 ++++++++ runconfig/config_test.go | 4 +- runconfig/parse.go | 9 ++- 12 files changed, 332 insertions(+), 32 deletions(-) diff --git a/docs/man/docker-create.1.md b/docs/man/docker-create.1.md index a83873794..96a049672 100644 --- a/docs/man/docker-create.1.md +++ b/docs/man/docker-create.1.md @@ -121,8 +121,10 @@ IMAGE [COMMAND] [ARG...] Publish all exposed ports to the host interfaces. The default is *false*. **-p**, **--publish**=[] - Publish a container's port to the host + Publish a container's port, or a range of ports, to the host format: ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort | containerPort + Both hostPort and containerPort can be specified as a range of ports. + When specifying ranges for both, the number of container ports in the range must match the number of host ports in the range. (e.g., `-p 1234-1236:1234-1236/tcp`) (use 'docker port' to see the actual mapping) **--privileged**=*true*|*false* diff --git a/docs/man/docker-run.1.md b/docs/man/docker-run.1.md index 659d3d321..b9571dbe2 100644 --- a/docs/man/docker-run.1.md +++ b/docs/man/docker-run.1.md @@ -146,7 +146,7 @@ ENTRYPOINT. Read in a line delimited file of environment variables **--expose**=[] - Expose a port or a range of ports (e.g. --expose=3300-3310) from the container without publishing it to your host + Expose a port, or a range of ports (e.g. --expose=3300-3310), from the container without publishing it to your host **-h**, **--hostname**="" Container host name @@ -224,8 +224,10 @@ ports to a random port on the host between 49153 and 65535. To find the mapping between the host ports and the exposed ports, use **docker port**. **-p**, **--publish**=[] - Publish a container's port to the host + Publish a container's port, or range of ports, to the host. format: ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort | containerPort + Both hostPort and containerPort can be specified as a range of ports. + When specifying ranges for both, the number of container ports in the range must match the number of host ports in the range. (e.g., `-p 1234-1236:1234-1236/tcp`) (use 'docker port' to see the actual mapping) **--privileged**=*true*|*false* diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index aab3d6af4..e48a393b7 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -686,8 +686,10 @@ Creates a new container. 'container:': reuses another container network stack 'host': use the host network stack inside the container. Note: the host mode gives the container full access to local system services such as D-bus and is therefore considered insecure. -P, --publish-all=false Publish all exposed ports to the host interfaces - -p, --publish=[] Publish a container's port to the host + -p, --publish=[] Publish a container's port, or a range of ports (e.g., `-p 3300-3310`), to the host format: ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort | containerPort + Both hostPort and containerPort can be specified as a range of ports. + When specifying ranges for both, the number of container ports in the range must match the number of host ports in the range. (e.g., `-p 1234-1236:1234-1236/tcp`) (use 'docker port' to see the actual mapping) --privileged=false Give extended privileges to this container --restart="" Restart policy to apply when a container exits (no, on-failure[:max-retry], always) @@ -1514,6 +1516,8 @@ removed before the image is removed. -P, --publish-all=false Publish all exposed ports to the host interfaces -p, --publish=[] Publish a container's port to the host format: ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort | containerPort + Both hostPort and containerPort can be specified as a range of ports. + When specifying ranges for both, the number of container ports in the range must match the number of host ports in the range. (e.g., `-p 1234-1236:1234-1236/tcp`) (use 'docker port' to see the actual mapping) --privileged=false Give extended privileges to this container --restart="" Restart policy to apply when a container exits (no, on-failure[:max-retry], always) diff --git a/docs/sources/reference/run.md b/docs/sources/reference/run.md index d13284b5d..1cd623175 100644 --- a/docs/sources/reference/run.md +++ b/docs/sources/reference/run.md @@ -487,10 +487,11 @@ or override the Dockerfile's exposed defaults: --expose=[]: Expose a port or a range of ports from the container without publishing it to your host -P=false : Publish all exposed ports to the host interfaces - -p=[] : Publish a container᾿s port to the host (format: - ip:hostPort:containerPort | ip::containerPort | - hostPort:containerPort | containerPort) - (use 'docker port' to see the actual mapping) + -p=[] : Publish a container᾿s port or a range of ports to the host + format: ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort | containerPort + Both hostPort and containerPort can be specified as a range of ports. + When specifying ranges for both, the number of container ports in the range must match the number of host ports in the range. (e.g., `-p 1234-1236:1234-1236/tcp`) + (use 'docker port' to see the actual mapping) --link="" : Add link to another container (name:alias) As mentioned previously, `EXPOSE` (and `--expose`) makes ports available diff --git a/integration-cli/docker_cli_create_test.go b/integration-cli/docker_cli_create_test.go index 0dc799379..1192f3647 100644 --- a/integration-cli/docker_cli_create_test.go +++ b/integration-cli/docker_cli_create_test.go @@ -2,6 +2,7 @@ package main import ( "encoding/json" + "github.com/docker/docker/nat" "os" "os/exec" "testing" @@ -102,6 +103,104 @@ func TestCreateHostConfig(t *testing.T) { logDone("create - hostconfig") } +func TestCreateWithPortRange(t *testing.T) { + runCmd := exec.Command(dockerBinary, "create", "-p", "3300-3303:3300-3303/tcp", "busybox", "echo") + out, _, _, err := runCommandWithStdoutStderr(runCmd) + if err != nil { + t.Fatal(out, err) + } + + cleanedContainerID := stripTrailingCharacters(out) + + inspectCmd := exec.Command(dockerBinary, "inspect", cleanedContainerID) + out, _, err = runCommandWithOutput(inspectCmd) + if err != nil { + t.Fatalf("out should've been a container id: %s, %v", out, err) + } + + containers := []struct { + HostConfig *struct { + PortBindings map[nat.Port][]nat.PortBinding + } + }{} + if err := json.Unmarshal([]byte(out), &containers); err != nil { + t.Fatalf("Error inspecting the container: %s", err) + } + if len(containers) != 1 { + t.Fatalf("Unexpected container count. Expected 0, received: %d", len(containers)) + } + + c := containers[0] + if c.HostConfig == nil { + t.Fatalf("Expected HostConfig, got none") + } + + if len(c.HostConfig.PortBindings) != 4 { + t.Fatalf("Expected 4 ports bindings, got %d", len(c.HostConfig.PortBindings)) + } + for k, v := range c.HostConfig.PortBindings { + if len(v) != 1 { + t.Fatalf("Expected 1 ports binding, for the port %s but found %s", k, v) + } + if k.Port() != v[0].HostPort { + t.Fatalf("Expected host port %d to match published port %d", k.Port(), v[0].HostPort) + } + } + + deleteAllContainers() + + logDone("create - port range") +} + +func TestCreateWithiLargePortRange(t *testing.T) { + runCmd := exec.Command(dockerBinary, "create", "-p", "1-65535:1-65535/tcp", "busybox", "echo") + out, _, _, err := runCommandWithStdoutStderr(runCmd) + if err != nil { + t.Fatal(out, err) + } + + cleanedContainerID := stripTrailingCharacters(out) + + inspectCmd := exec.Command(dockerBinary, "inspect", cleanedContainerID) + out, _, err = runCommandWithOutput(inspectCmd) + if err != nil { + t.Fatalf("out should've been a container id: %s, %v", out, err) + } + + containers := []struct { + HostConfig *struct { + PortBindings map[nat.Port][]nat.PortBinding + } + }{} + if err := json.Unmarshal([]byte(out), &containers); err != nil { + t.Fatalf("Error inspecting the container: %s", err) + } + if len(containers) != 1 { + t.Fatalf("Unexpected container count. Expected 0, received: %d", len(containers)) + } + + c := containers[0] + if c.HostConfig == nil { + t.Fatalf("Expected HostConfig, got none") + } + + if len(c.HostConfig.PortBindings) != 65535 { + t.Fatalf("Expected 65535 ports bindings, got %d", len(c.HostConfig.PortBindings)) + } + for k, v := range c.HostConfig.PortBindings { + if len(v) != 1 { + t.Fatalf("Expected 1 ports binding, for the port %s but found %s", k, v) + } + if k.Port() != v[0].HostPort { + t.Fatalf("Expected host port %d to match published port %d", k.Port(), v[0].HostPort) + } + } + + deleteAllContainers() + + logDone("create - large port range") +} + // "test123" should be printed by docker create + start func TestCreateEchoStdout(t *testing.T) { runCmd := exec.Command(dockerBinary, "create", "busybox", "echo", "test123") diff --git a/integration-cli/docker_cli_run_test.go b/integration-cli/docker_cli_run_test.go index 2a75b27d8..748c4d681 100644 --- a/integration-cli/docker_cli_run_test.go +++ b/integration-cli/docker_cli_run_test.go @@ -2737,3 +2737,27 @@ func TestRunNetHost(t *testing.T) { logDone("run - net host mode") } + +func TestRunAllowPortRangeThroughPublish(t *testing.T) { + cmd := exec.Command(dockerBinary, "run", "-d", "--expose", "3000-3003", "-p", "3000-3003", "busybox", "top") + out, _, err := runCommandWithOutput(cmd) + defer deleteAllContainers() + + id := strings.TrimSpace(out) + portstr, err := inspectFieldJSON(id, "NetworkSettings.Ports") + if err != nil { + t.Fatal(err) + } + var ports nat.PortMap + err = unmarshalJSON([]byte(portstr), &ports) + for port, binding := range ports { + portnum, _ := strconv.Atoi(strings.Split(string(port), "/")[0]) + if portnum < 3000 || portnum > 3003 { + t.Fatalf("Port is out of range ", portnum, binding, out) + } + if binding == nil || len(binding) != 1 || len(binding[0].HostPort) == 0 { + t.Fatal("Port is not mapped for the port "+port, out) + } + } + logDone("run - allow port range through --expose flag") +} diff --git a/nat/nat.go b/nat/nat.go index 1246626b0..8f2e90e66 100644 --- a/nat/nat.go +++ b/nat/nat.go @@ -122,31 +122,48 @@ func ParsePortSpecs(ports []string) (map[Port]struct{}, map[Port][]PortBinding, if containerPort == "" { return nil, nil, fmt.Errorf("No port specified: %s", rawPort) } - if _, err := strconv.ParseUint(containerPort, 10, 16); err != nil { + + startPort, endPort, err := parsers.ParsePortRange(containerPort) + if err != nil { return nil, nil, fmt.Errorf("Invalid containerPort: %s", containerPort) } - if _, err := strconv.ParseUint(hostPort, 10, 16); hostPort != "" && err != nil { - return nil, nil, fmt.Errorf("Invalid hostPort: %s", hostPort) + + var startHostPort, endHostPort uint64 = 0, 0 + if len(hostPort) > 0 { + startHostPort, endHostPort, err = parsers.ParsePortRange(hostPort) + if err != nil { + return nil, nil, fmt.Errorf("Invalid hostPort: %s", hostPort) + } + } + + if hostPort != "" && (endPort-startPort) != (endHostPort-startHostPort) { + return nil, nil, fmt.Errorf("Invalid ranges specified for container and host Ports: %s and %s", containerPort, hostPort) } if !validateProto(proto) { return nil, nil, fmt.Errorf("Invalid proto: %s", proto) } - port := NewPort(proto, containerPort) - if _, exists := exposedPorts[port]; !exists { - exposedPorts[port] = struct{}{} - } + for i := uint64(0); i <= (endPort - startPort); i++ { + containerPort = strconv.FormatUint(startPort+i, 10) + if len(hostPort) > 0 { + hostPort = strconv.FormatUint(startHostPort+i, 10) + } + port := NewPort(proto, containerPort) + if _, exists := exposedPorts[port]; !exists { + exposedPorts[port] = struct{}{} + } - binding := PortBinding{ - HostIp: rawIp, - HostPort: hostPort, + binding := PortBinding{ + HostIp: rawIp, + HostPort: hostPort, + } + bslice, exists := bindings[port] + if !exists { + bslice = []PortBinding{} + } + bindings[port] = append(bslice, binding) } - bslice, exists := bindings[port] - if !exists { - bslice = []PortBinding{} - } - bindings[port] = append(bslice, binding) } return exposedPorts, bindings, nil } diff --git a/nat/nat_test.go b/nat/nat_test.go index 4ae9f4ece..34c210e6e 100644 --- a/nat/nat_test.go +++ b/nat/nat_test.go @@ -108,7 +108,7 @@ func TestParsePortSpecs(t *testing.T) { portMap, bindingMap, err = ParsePortSpecs([]string{"1234/tcp", "2345/udp"}) if err != nil { - t.Fatalf("Error while processing ParsePortSpecs: %s", err.Error()) + t.Fatalf("Error while processing ParsePortSpecs: %s", err) } if _, ok := portMap[Port("1234/tcp")]; !ok { @@ -136,7 +136,7 @@ func TestParsePortSpecs(t *testing.T) { portMap, bindingMap, err = ParsePortSpecs([]string{"1234:1234/tcp", "2345:2345/udp"}) if err != nil { - t.Fatalf("Error while processing ParsePortSpecs: %s", err.Error()) + t.Fatalf("Error while processing ParsePortSpecs: %s", err) } if _, ok := portMap[Port("1234/tcp")]; !ok { @@ -166,7 +166,7 @@ func TestParsePortSpecs(t *testing.T) { portMap, bindingMap, err = ParsePortSpecs([]string{"0.0.0.0:1234:1234/tcp", "0.0.0.0:2345:2345/udp"}) if err != nil { - t.Fatalf("Error while processing ParsePortSpecs: %s", err.Error()) + t.Fatalf("Error while processing ParsePortSpecs: %s", err) } if _, ok := portMap[Port("1234/tcp")]; !ok { @@ -199,3 +199,95 @@ func TestParsePortSpecs(t *testing.T) { t.Fatal("Received no error while trying to parse a hostname instead of ip") } } + +func TestParsePortSpecsWithRange(t *testing.T) { + var ( + portMap map[Port]struct{} + bindingMap map[Port][]PortBinding + err error + ) + + portMap, bindingMap, err = ParsePortSpecs([]string{"1234-1236/tcp", "2345-2347/udp"}) + + if err != nil { + t.Fatalf("Error while processing ParsePortSpecs: %s", err) + } + + if _, ok := portMap[Port("1235/tcp")]; !ok { + t.Fatal("1234/tcp was not parsed properly") + } + + if _, ok := portMap[Port("2346/udp")]; !ok { + t.Fatal("2345/udp was not parsed properly") + } + + for portspec, bindings := range bindingMap { + if len(bindings) != 1 { + t.Fatalf("%s should have exactly one binding", portspec) + } + + if bindings[0].HostIp != "" { + t.Fatalf("HostIp should not be set for %s", portspec) + } + + if bindings[0].HostPort != "" { + t.Fatalf("HostPort should not be set for %s", portspec) + } + } + + portMap, bindingMap, err = ParsePortSpecs([]string{"1234-1236:1234-1236/tcp", "2345-2347:2345-2347/udp"}) + + if err != nil { + t.Fatalf("Error while processing ParsePortSpecs: %s", err) + } + + if _, ok := portMap[Port("1235/tcp")]; !ok { + t.Fatal("1234/tcp was not parsed properly") + } + + if _, ok := portMap[Port("2346/udp")]; !ok { + t.Fatal("2345/udp was not parsed properly") + } + + for portspec, bindings := range bindingMap { + _, port := SplitProtoPort(string(portspec)) + if len(bindings) != 1 { + t.Fatalf("%s should have exactly one binding", portspec) + } + + if bindings[0].HostIp != "" { + t.Fatalf("HostIp should not be set for %s", portspec) + } + + if bindings[0].HostPort != port { + t.Fatalf("HostPort should be %s for %s", port, portspec) + } + } + + portMap, bindingMap, err = ParsePortSpecs([]string{"0.0.0.0:1234-1236:1234-1236/tcp", "0.0.0.0:2345-2347:2345-2347/udp"}) + + if err != nil { + t.Fatalf("Error while processing ParsePortSpecs: %s", err) + } + + if _, ok := portMap[Port("1235/tcp")]; !ok { + t.Fatal("1234/tcp was not parsed properly") + } + + if _, ok := portMap[Port("2346/udp")]; !ok { + t.Fatal("2345/udp was not parsed properly") + } + + for portspec, bindings := range bindingMap { + _, port := SplitProtoPort(string(portspec)) + if len(bindings) != 1 || bindings[0].HostIp != "0.0.0.0" || bindings[0].HostPort != port { + t.Fatalf("Expect single binding to port %d but found %s", port, bindings) + } + } + + _, _, err = ParsePortSpecs([]string{"localhost:1234-1236:1234-1236/tcp"}) + + if err == nil { + t.Fatal("Received no error while trying to parse a hostname instead of ip") + } +} diff --git a/pkg/parsers/parsers.go b/pkg/parsers/parsers.go index 2851fe163..656319041 100644 --- a/pkg/parsers/parsers.go +++ b/pkg/parsers/parsers.go @@ -104,3 +104,28 @@ func ParseKeyValueOpt(opt string) (string, string, error) { } return strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]), nil } + +func ParsePortRange(ports string) (uint64, uint64, error) { + if ports == "" { + return 0, 0, fmt.Errorf("Empty string specified for ports.") + } + if !strings.Contains(ports, "-") { + start, err := strconv.ParseUint(ports, 10, 16) + end := start + return start, end, err + } + + parts := strings.Split(ports, "-") + start, err := strconv.ParseUint(parts[0], 10, 16) + if err != nil { + return 0, 0, err + } + end, err := strconv.ParseUint(parts[1], 10, 16) + if err != nil { + return 0, 0, err + } + if end < start { + return 0, 0, fmt.Errorf("Invalid range specified for the Port: %s", ports) + } + return start, end, nil +} diff --git a/pkg/parsers/parsers_test.go b/pkg/parsers/parsers_test.go index 12b8df570..aac1e33e3 100644 --- a/pkg/parsers/parsers_test.go +++ b/pkg/parsers/parsers_test.go @@ -1,6 +1,7 @@ package parsers import ( + "strings" "testing" ) @@ -81,3 +82,35 @@ func TestParsePortMapping(t *testing.T) { t.Fail() } } + +func TestParsePortRange(t *testing.T) { + if start, end, err := ParsePortRange("8000-8080"); err != nil || start != 8000 || end != 8080 { + t.Fatalf("Error: %s or Expecting {start,end} values {8000,8080} but found {%d,%d}.", err, start, end) + } +} + +func TestParsePortRangeIncorrectRange(t *testing.T) { + if _, _, err := ParsePortRange("9000-8080"); err == nil || !strings.Contains(err.Error(), "Invalid range specified for the Port") { + t.Fatalf("Expecting error 'Invalid range specified for the Port' but received %s.", err) + } +} + +func TestParsePortRangeIncorrectEndRange(t *testing.T) { + if _, _, err := ParsePortRange("8000-a"); err == nil || !strings.Contains(err.Error(), "invalid syntax") { + t.Fatalf("Expecting error 'Invalid range specified for the Port' but received %s.", err) + } + + if _, _, err := ParsePortRange("8000-30a"); err == nil || !strings.Contains(err.Error(), "invalid syntax") { + t.Fatalf("Expecting error 'Invalid range specified for the Port' but received %s.", err) + } +} + +func TestParsePortRangeIncorrectStartRange(t *testing.T) { + if _, _, err := ParsePortRange("a-8000"); err == nil || !strings.Contains(err.Error(), "invalid syntax") { + t.Fatalf("Expecting error 'Invalid range specified for the Port' but received %s.", err) + } + + if _, _, err := ParsePortRange("30a-8000"); err == nil || !strings.Contains(err.Error(), "invalid syntax") { + t.Fatalf("Expecting error 'Invalid range specified for the Port' but received %s.", err) + } +} diff --git a/runconfig/config_test.go b/runconfig/config_test.go index f856c87f5..accbd9107 100644 --- a/runconfig/config_test.go +++ b/runconfig/config_test.go @@ -256,8 +256,8 @@ func TestMerge(t *testing.T) { t.Fatalf("Expected 4 ExposedPorts, 0000, 1111, 2222 and 3333, found %d", len(configUser.ExposedPorts)) } for portSpecs := range configUser.ExposedPorts { - if portSpecs.Port() != "0000" && portSpecs.Port() != "1111" && portSpecs.Port() != "2222" && portSpecs.Port() != "3333" { - t.Fatalf("Expected 0000 or 1111 or 2222 or 3333, found %s", portSpecs) + if portSpecs.Port() != "0" && portSpecs.Port() != "1111" && portSpecs.Port() != "2222" && portSpecs.Port() != "3333" { + t.Fatalf("Expected %q or %q or %q or %q, found %s", 0, 1111, 2222, 3333, portSpecs) } } diff --git a/runconfig/parse.go b/runconfig/parse.go index 0d682f35d..5c684e346 100644 --- a/runconfig/parse.go +++ b/runconfig/parse.go @@ -197,11 +197,12 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe if strings.Contains(e, "-") { proto, port := nat.SplitProtoPort(e) //parse the start and end port and create a sequence of ports to expose - parts := strings.Split(port, "-") - start, _ := strconv.Atoi(parts[0]) - end, _ := strconv.Atoi(parts[1]) + start, end, err := parsers.ParsePortRange(port) + if err != nil { + return nil, nil, cmd, fmt.Errorf("Invalid range format for --expose: %s, error: %s", e, err) + } for i := start; i <= end; i++ { - p := nat.NewPort(proto, strconv.Itoa(i)) + p := nat.NewPort(proto, strconv.FormatUint(i, 10)) if _, exists := ports[p]; !exists { ports[p] = struct{}{} } From 1aac999f707773e4530c1d126ce51ec19cf9d243 Mon Sep 17 00:00:00 2001 From: Nathan Hsieh Date: Fri, 2 Jan 2015 16:21:51 -0800 Subject: [PATCH 145/513] updated docs with information regarding search pagination Signed-off-by: Nathan Hsieh --- docs/sources/reference/api/registry_api.md | 25 ++++++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/docs/sources/reference/api/registry_api.md b/docs/sources/reference/api/registry_api.md index 864816214..54a158934 100644 --- a/docs/sources/reference/api/registry_api.md +++ b/docs/sources/reference/api/registry_api.md @@ -518,26 +518,37 @@ Search the Index given a search term. It accepts Host: index.docker.io Accept: application/json +Query Parameters: + +- **q** – what you want to search for +- **n** - number of results you want returned per page (default: 25, min:1, max:100) +- **page** - page number of results + **Example response**: HTTP/1.1 200 OK Vary: Accept Content-Type: application/json - {"query":"search_term", + {"num_pages": 1, "num_results": 3, "results" : [ {"name": "ubuntu", "description": "An ubuntu image..."}, {"name": "centos", "description": "A centos image..."}, {"name": "fedora", "description": "A fedora image..."} - ] + ], + "page_size": 25, + "query":"search_term", + "page": 1 } -Query Parameters: - -- **q** – what you want to search for -- **n** - number of results you want returned per page (default: 25) -- **page** - page number of results +Response Items: +- **num_pages** - Total number of pages returned by query +- **num_results** - Total number of results returned by query +- **results** - List of results for the current page +- **page_size** - How many results returned per page +- **query** - Your search term +- **page** - Current page number Status Codes: From 1edf16e977a3dd18f9ff15b4acf40b0c1ac64788 Mon Sep 17 00:00:00 2001 From: Tianon Gravi Date: Fri, 2 Jan 2015 22:36:58 -0700 Subject: [PATCH 146/513] Simplify builder ast.Dump by using strconv.Quote instead of a custom QuoteString function (the only change to existing files is literal tabs becoming \t, but future files may use nonprintable characters and the like now) Signed-off-by: Andrew "Tianon" Page --- .../testfiles/brimstone-consuldock/result | 2 +- .../testfiles/brimstone-docker-consul/result | 6 ++--- builder/parser/testfiles/docker/result | 4 ++-- builder/parser/utils.go | 24 ++----------------- 4 files changed, 8 insertions(+), 28 deletions(-) diff --git a/builder/parser/testfiles/brimstone-consuldock/result b/builder/parser/testfiles/brimstone-consuldock/result index cc8fab213..227f748cd 100644 --- a/builder/parser/testfiles/brimstone-consuldock/result +++ b/builder/parser/testfiles/brimstone-consuldock/result @@ -2,4 +2,4 @@ (maintainer "brimstone@the.narro.ws") (env "GOPATH" "/go") (entrypoint "/usr/local/bin/consuldock") -(run "apt-get update && dpkg -l | awk '/^ii/ {print $2}' > /tmp/dpkg.clean && apt-get install -y --no-install-recommends git golang ca-certificates && apt-get clean && rm -rf /var/lib/apt/lists && go get -v github.com/brimstone/consuldock && mv $GOPATH/bin/consuldock /usr/local/bin/consuldock && dpkg -l | awk '/^ii/ {print $2}' > /tmp/dpkg.dirty && apt-get remove --purge -y $(diff /tmp/dpkg.clean /tmp/dpkg.dirty | awk '/^>/ {print $2}') && rm /tmp/dpkg.* && rm -rf $GOPATH") +(run "apt-get update \t&& dpkg -l | awk '/^ii/ {print $2}' > /tmp/dpkg.clean && apt-get install -y --no-install-recommends git golang ca-certificates && apt-get clean && rm -rf /var/lib/apt/lists \t&& go get -v github.com/brimstone/consuldock && mv $GOPATH/bin/consuldock /usr/local/bin/consuldock \t&& dpkg -l | awk '/^ii/ {print $2}' > /tmp/dpkg.dirty \t&& apt-get remove --purge -y $(diff /tmp/dpkg.clean /tmp/dpkg.dirty | awk '/^>/ {print $2}') \t&& rm /tmp/dpkg.* \t&& rm -rf $GOPATH") diff --git a/builder/parser/testfiles/brimstone-docker-consul/result b/builder/parser/testfiles/brimstone-docker-consul/result index 8c989e621..16492e516 100644 --- a/builder/parser/testfiles/brimstone-docker-consul/result +++ b/builder/parser/testfiles/brimstone-docker-consul/result @@ -2,8 +2,8 @@ (cmd) (entrypoint "/usr/bin/consul" "agent" "-server" "-data-dir=/consul" "-client=0.0.0.0" "-ui-dir=/webui") (expose "8500" "8600" "8400" "8301" "8302") -(run "apt-get update && apt-get install -y unzip wget && apt-get clean && rm -rf /var/lib/apt/lists") +(run "apt-get update && apt-get install -y unzip wget \t&& apt-get clean \t&& rm -rf /var/lib/apt/lists") (run "cd /tmp && wget https://dl.bintray.com/mitchellh/consul/0.3.1_web_ui.zip -O web_ui.zip && unzip web_ui.zip && mv dist /webui && rm web_ui.zip") -(run "apt-get update && dpkg -l | awk '/^ii/ {print $2}' > /tmp/dpkg.clean && apt-get install -y --no-install-recommends unzip wget && apt-get clean && rm -rf /var/lib/apt/lists && cd /tmp && wget https://dl.bintray.com/mitchellh/consul/0.3.1_web_ui.zip -O web_ui.zip && unzip web_ui.zip && mv dist /webui && rm web_ui.zip && dpkg -l | awk '/^ii/ {print $2}' > /tmp/dpkg.dirty && apt-get remove --purge -y $(diff /tmp/dpkg.clean /tmp/dpkg.dirty | awk '/^>/ {print $2}') && rm /tmp/dpkg.*") +(run "apt-get update \t&& dpkg -l | awk '/^ii/ {print $2}' > /tmp/dpkg.clean && apt-get install -y --no-install-recommends unzip wget && apt-get clean && rm -rf /var/lib/apt/lists && cd /tmp && wget https://dl.bintray.com/mitchellh/consul/0.3.1_web_ui.zip -O web_ui.zip && unzip web_ui.zip && mv dist /webui && rm web_ui.zip \t&& dpkg -l | awk '/^ii/ {print $2}' > /tmp/dpkg.dirty \t&& apt-get remove --purge -y $(diff /tmp/dpkg.clean /tmp/dpkg.dirty | awk '/^>/ {print $2}') \t&& rm /tmp/dpkg.*") (env "GOPATH" "/go") -(run "apt-get update && dpkg -l | awk '/^ii/ {print $2}' > /tmp/dpkg.clean && apt-get install -y --no-install-recommends git golang ca-certificates build-essential && apt-get clean && rm -rf /var/lib/apt/lists && go get -v github.com/hashicorp/consul && mv $GOPATH/bin/consul /usr/bin/consul && dpkg -l | awk '/^ii/ {print $2}' > /tmp/dpkg.dirty && apt-get remove --purge -y $(diff /tmp/dpkg.clean /tmp/dpkg.dirty | awk '/^>/ {print $2}') && rm /tmp/dpkg.* && rm -rf $GOPATH") +(run "apt-get update \t&& dpkg -l | awk '/^ii/ {print $2}' > /tmp/dpkg.clean && apt-get install -y --no-install-recommends git golang ca-certificates build-essential && apt-get clean && rm -rf /var/lib/apt/lists \t&& go get -v github.com/hashicorp/consul \t&& mv $GOPATH/bin/consul /usr/bin/consul \t&& dpkg -l | awk '/^ii/ {print $2}' > /tmp/dpkg.dirty \t&& apt-get remove --purge -y $(diff /tmp/dpkg.clean /tmp/dpkg.dirty | awk '/^>/ {print $2}') \t&& rm /tmp/dpkg.* \t&& rm -rf $GOPATH") diff --git a/builder/parser/testfiles/docker/result b/builder/parser/testfiles/docker/result index 80f219ecb..773b640a9 100644 --- a/builder/parser/testfiles/docker/result +++ b/builder/parser/testfiles/docker/result @@ -1,13 +1,13 @@ (from "ubuntu:14.04") (maintainer "Tianon Gravi (@tianon)") -(run "apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -yq apt-utils aufs-tools automake btrfs-tools build-essential curl dpkg-sig git iptables libapparmor-dev libcap-dev libsqlite3-dev lxc=1.0* mercurial pandoc parallel reprepro ruby1.9.1 ruby1.9.1-dev s3cmd=1.1.0* --no-install-recommends") +(run "apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -yq \tapt-utils \taufs-tools \tautomake \tbtrfs-tools \tbuild-essential \tcurl \tdpkg-sig \tgit \tiptables \tlibapparmor-dev \tlibcap-dev \tlibsqlite3-dev \tlxc=1.0* \tmercurial \tpandoc \tparallel \treprepro \truby1.9.1 \truby1.9.1-dev \ts3cmd=1.1.0* \t--no-install-recommends") (run "git clone --no-checkout https://git.fedorahosted.org/git/lvm2.git /usr/local/lvm2 && cd /usr/local/lvm2 && git checkout -q v2_02_103") (run "cd /usr/local/lvm2 && ./configure --enable-static_link && make device-mapper && make install_device-mapper") (run "curl -sSL https://golang.org/dl/go1.3.src.tar.gz | tar -v -C /usr/local -xz") (env "PATH" "/usr/local/go/bin:$PATH") (env "GOPATH" "/go:/go/src/github.com/docker/docker/vendor") (run "cd /usr/local/go/src && ./make.bash --no-clean 2>&1") -(env "DOCKER_CROSSPLATFORMS" "linux/386 linux/arm darwin/amd64 darwin/386 freebsd/amd64 freebsd/386 freebsd/arm") +(env "DOCKER_CROSSPLATFORMS" "linux/386 linux/arm \tdarwin/amd64 darwin/386 \tfreebsd/amd64 freebsd/386 freebsd/arm") (env "GOARM" "5") (run "cd /usr/local/go/src && bash -xc 'for platform in $DOCKER_CROSSPLATFORMS; do GOOS=${platform%/*} GOARCH=${platform##*/} ./make.bash --no-clean 2>&1; done'") (run "go get golang.org/x/tools/cmd/cover") diff --git a/builder/parser/utils.go b/builder/parser/utils.go index 096c4e31e..3a8cd24e8 100644 --- a/builder/parser/utils.go +++ b/builder/parser/utils.go @@ -2,30 +2,10 @@ package parser import ( "fmt" + "strconv" "strings" ) -// QuoteString walks characters (after trimming), escapes any quotes and -// escapes, then wraps the whole thing in quotes. Very useful for generating -// argument output in nodes. -func QuoteString(str string) string { - result := "" - chars := strings.Split(strings.TrimSpace(str), "") - - for _, char := range chars { - switch char { - case `"`: - result += `\"` - case `\`: - result += `\\` - default: - result += char - } - } - - return `"` + result + `"` -} - // dumps the AST defined by `node` as a list of sexps. Returns a string // suitable for printing. func (node *Node) Dump() string { @@ -41,7 +21,7 @@ func (node *Node) Dump() string { if len(n.Children) > 0 { str += " " + n.Dump() } else { - str += " " + QuoteString(n.Value) + str += " " + strconv.Quote(n.Value) } } } From f6cb1ea85a7a75193644b8ba4516cb586c5dc52f Mon Sep 17 00:00:00 2001 From: Tianon Gravi Date: Fri, 2 Jan 2015 22:38:52 -0700 Subject: [PATCH 147/513] Simplify builder TestTestData slightly by using ioutil.ReadFile instead of os.Open+ioutil.ReadAll Signed-off-by: Andrew "Tianon" Page --- builder/parser/parser_test.go | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/builder/parser/parser_test.go b/builder/parser/parser_test.go index 1b517fcc1..daceb9839 100644 --- a/builder/parser/parser_test.go +++ b/builder/parser/parser_test.go @@ -54,18 +54,14 @@ func TestTestData(t *testing.T) { if err != nil { t.Fatalf("Dockerfile missing for %s: %s", dir.Name(), err.Error()) } - - rf, err := os.Open(resultfile) - if err != nil { - t.Fatalf("Result file missing for %s: %s", dir.Name(), err.Error()) - } + defer df.Close() ast, err := Parse(df) if err != nil { t.Fatalf("Error parsing %s's dockerfile: %s", dir.Name(), err.Error()) } - content, err := ioutil.ReadAll(rf) + content, err := ioutil.ReadFile(resultfile) if err != nil { t.Fatalf("Error reading %s's result file: %s", dir.Name(), err.Error()) } @@ -75,8 +71,5 @@ func TestTestData(t *testing.T) { fmt.Fprintln(os.Stderr, "Expected:\n"+string(content)) t.Fatalf("%s: AST dump of dockerfile does not match result", dir.Name()) } - - df.Close() - rf.Close() } } From 0c21905a3d7cdae98ebad3155e4a1b3fe98f29a7 Mon Sep 17 00:00:00 2001 From: Tianon Gravi Date: Fri, 2 Jan 2015 23:42:45 -0700 Subject: [PATCH 148/513] Add initial new IRC administration cheat sheet of sorts Signed-off-by: Andrew "Tianon" Page --- project/IRC-ADMINISTRATION.md | 37 +++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 project/IRC-ADMINISTRATION.md diff --git a/project/IRC-ADMINISTRATION.md b/project/IRC-ADMINISTRATION.md new file mode 100644 index 000000000..3cec9f758 --- /dev/null +++ b/project/IRC-ADMINISTRATION.md @@ -0,0 +1,37 @@ +# Freenode IRC Administration Guidelines and Tips + +This is not meant to be a general "Here's how to IRC" document, so if you're +looking for that, check Google instead. ♥ + +If you've been charged with helping maintain one of Docker's now many IRC +channels, this might turn out to be useful. If there's information that you +wish you'd known about how a particular channel is organized, you should add +deets here! :) + +## `ChanServ` + +Most channel maintenance happens by talking to Freenode's `ChanServ` bot. For +example, `/msg ChanServ ACCESS LIST` will show you a list of everyone +with "access" privileges for a particular channel. + +A similar command is used to give someone a particular access level. For +example, to add a new maintainer to the `#docker-maintainers` access list so +that they can contribute to the dicsussions (after they've been merged +appropriately in a `MAINTAINERS` file, of course), one would use `/msg ChanServ +ACCESS #docker-maintainers ADD maintainer`. + +To setup a new channel with a similar `maintainer` access template, use a +command like `/msg ChanServ TEMPLATE maintainer +AV` (`+A` for letting +them view the `ACCESS LIST`, `+V` for auto-voice; see `/msg ChanServ HELP FLAGS` +for more details). + +## Troubleshooting + +The most common cause of not-getting-auto-`+v` woes is people not being +`IDENTIFY`ed with `NickServ` (or their current nickname not being `GROUP`ed with +their main nickname) -- often manifested by `ChanServ` responding to an `ACCESS +ADD` request with something like `xyz is not registered.`. + +This is easily fixed by doing `/msg NickServ IDENTIFY OldNick SecretPassword` +followed by `/msg NickServ GROUP` to group the two nicknames together. See +`/msg NickServ HELP GROUP` for more information. From e3d813f37f48ed52330e3dc26aa363e58401fbf5 Mon Sep 17 00:00:00 2001 From: "Daehyeok.Mun" Date: Fri, 14 Nov 2014 20:34:59 +0900 Subject: [PATCH 149/513] Add exec event create/start log added exec event log follwing issue #8662 proposal. logging events for exec create and start API Signed-off-by: daehyeok mun --- daemon/exec.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/daemon/exec.go b/daemon/exec.go index 616093c67..0448252ea 100644 --- a/daemon/exec.go +++ b/daemon/exec.go @@ -154,6 +154,8 @@ func (d *Daemon) ContainerExecCreate(job *engine.Job) engine.Status { Running: false, } + container.LogEvent("exec_create: " + execConfig.ProcessConfig.Entrypoint + " " + strings.Join(execConfig.ProcessConfig.Arguments, " ")) + d.registerExecCommand(execConfig) job.Printf("%s\n", execConfig.ID) @@ -192,6 +194,8 @@ func (d *Daemon) ContainerExecStart(job *engine.Job) engine.Status { log.Debugf("starting exec command %s in container %s", execConfig.ID, execConfig.Container.ID) container := execConfig.Container + container.LogEvent("exec_start: " + execConfig.ProcessConfig.Entrypoint + " " + strings.Join(execConfig.ProcessConfig.Arguments, " ")) + if execConfig.OpenStdin { r, w := io.Pipe() go func() { From fc7f0550965d06dd8dd31fb55c74fe02e9a436dc Mon Sep 17 00:00:00 2001 From: Daehyeok Mun Date: Wed, 24 Dec 2014 03:48:12 +0900 Subject: [PATCH 150/513] Add list of events in remote API docs Add exec create and exec start to list of events in remote API docs Signed-off-by: Daehyeok Mun --- docs/sources/reference/api/docker_remote_api_v1.17.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/reference/api/docker_remote_api_v1.17.md b/docs/sources/reference/api/docker_remote_api_v1.17.md index 0164b9581..fca3998dd 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.17.md +++ b/docs/sources/reference/api/docker_remote_api_v1.17.md @@ -1380,7 +1380,7 @@ polling (using since). Docker containers will report the following events: - create, destroy, die, export, kill, oom, pause, restart, start, stop, unpause + create, destroy, die, exec_create, exec_start, export, kill, oom, pause, restart, start, stop, unpause and Docker images will report: From e583cc1eb47c29d7473f1dac7d4f39ddeb8ae1e6 Mon Sep 17 00:00:00 2001 From: Lorenz Leutgeb Date: Sat, 3 Jan 2015 18:15:40 +0100 Subject: [PATCH 151/513] doc: Try to standardise JSON examples Fixed: * Invalid JSON * Inconsistent spacing at colon Expression for binary data streams (line 468 vs. 1474) remain inconsistent. Could fix that too, if you like. Signed-off-by: Lorenz Leutgeb --- .../reference/api/docker_remote_api_v1.15.md | 287 +++++++++--------- 1 file changed, 143 insertions(+), 144 deletions(-) diff --git a/docs/sources/reference/api/docker_remote_api_v1.15.md b/docs/sources/reference/api/docker_remote_api_v1.15.md index 84039daad..4d27a6150 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.15.md +++ b/docs/sources/reference/api/docker_remote_api_v1.15.md @@ -40,9 +40,9 @@ List containers "Command": "echo 1", "Created": 1367854155, "Status": "Exit 0", - "Ports":[{"PrivatePort": 2222, "PublicPort": 3333, "Type": "tcp"}], - "SizeRw":12288, - "SizeRootFs":0 + "Ports": [{"PrivatePort": 2222, "PublicPort": 3333, "Type": "tcp"}], + "SizeRw": 12288, + "SizeRootFs": 0 }, { "Id": "9cd87474be90", @@ -50,9 +50,9 @@ List containers "Command": "echo 222222", "Created": 1367854155, "Status": "Exit 0", - "Ports":[], - "SizeRw":12288, - "SizeRootFs":0 + "Ports": [], + "SizeRw": 12288, + "SizeRootFs": 0 }, { "Id": "3176a2479c92", @@ -70,9 +70,9 @@ List containers "Command": "echo 444444444444444444444444444444444", "Created": 1367854152, "Status": "Exit 0", - "Ports":[], - "SizeRw":12288, - "SizeRootFs":0 + "Ports": [], + "SizeRw": 12288, + "SizeRootFs": 0 } ] @@ -110,42 +110,42 @@ Create a container Content-Type: application/json { - "Hostname":"", + "Hostname": "", "Domainname": "", - "User":"", - "Memory":0, - "MemorySwap":0, + "User": "", + "Memory": 0, + "MemorySwap": 0, "CpuShares": 512, "Cpuset": "0,1", - "AttachStdin":false, - "AttachStdout":true, - "AttachStderr":true, - "Tty":false, - "OpenStdin":false, - "StdinOnce":false, - "Env":null, - "Cmd":[ + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": null, + "Cmd": [ "date" ], "Entrypoint": "", - "Image":"base", - "Volumes":{ + "Image": "base", + "Volumes": { "/tmp": {} }, - "WorkingDir":"", + "WorkingDir": "", "NetworkDisabled": false, - "MacAddress":"12:34:56:78:9a:bc", - "ExposedPorts":{ + "MacAddress": "12:34:56:78:9a:bc", + "ExposedPorts": { "22/tcp": {} }, "SecurityOpts": [""], "HostConfig": { - "Binds":["/tmp:/tmp"], - "Links":["redis3:redis"], - "LxcConf":{"lxc.utsname":"docker"}, - "PortBindings":{ "22/tcp": [{ "HostPort": "11022" }] }, - "PublishAllPorts":false, - "Privileged":false, + "Binds": ["/tmp:/tmp"], + "Links": ["redis3:redis"], + "LxcConf": {"lxc.utsname":"docker"}, + "PortBindings": { "22/tcp": [{ "HostPort": "11022" }] }, + "PublishAllPorts": false, + "Privileged": false, "Dns": ["8.8.8.8"], "DnsSearch": [""], "VolumesFrom": ["parent", "other:ro"], @@ -163,8 +163,8 @@ Create a container Content-Type: application/json { - "Id":"f91ddc4b01e079c4481a8340bbbeca4dbd33d6e4a10662e499f8eacbb5bf252b" - "Warnings":[] + "Id": "f91ddc4b01e079c4481a8340bbbeca4dbd33d6e4a10662e499f8eacbb5bf252b" + "Warnings": [] } Json Parameters: @@ -291,8 +291,7 @@ Return low-level information on the container `id` "Image": "base", "Volumes": {}, "VolumesFrom": "", - "WorkingDir":"" - + "WorkingDir": "" }, "State": { "Running": false, @@ -327,8 +326,8 @@ Return low-level information on the container `id` }, "Links": ["/name:alias"], "PublishAllPorts": false, - "CapAdd: ["NET_ADMIN"], - "CapDrop: ["MKNOD"] + "CapAdd": ["NET_ADMIN"], + "CapDrop": ["MKNOD"] } } @@ -354,7 +353,7 @@ List processes running inside the container `id` Content-Type: application/json { - "Titles":[ + "Titles": [ "USER", "PID", "%CPU", @@ -367,7 +366,7 @@ List processes running inside the container `id` "TIME", "COMMAND" ], - "Processes":[ + "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"] ] @@ -432,16 +431,16 @@ Inspect changes on container `id`'s filesystem [ { - "Path":"/dev", - "Kind":0 + "Path": "/dev", + "Kind": 0 }, { - "Path":"/dev/kmsg", - "Kind":1 + "Path": "/dev/kmsg", + "Kind": 1 }, { - "Path":"/test", - "Kind":1 + "Path": "/test", + "Kind": 1 } ] @@ -508,12 +507,12 @@ Start the container `id` Content-Type: application/json { - "Binds":["/tmp:/tmp"], - "Links":["redis3:redis"], - "LxcConf":{"lxc.utsname":"docker"}, - "PortBindings":{ "22/tcp": [{ "HostPort": "11022" }] }, - "PublishAllPorts":false, - "Privileged":false, + "Binds": ["/tmp:/tmp"], + "Links": ["redis3:redis"], + "LxcConf": {"lxc.utsname":"docker"}, + "PortBindings": { "22/tcp": [{ "HostPort": "11022" }] }, + "PublishAllPorts": false, + "Privileged": false, "Dns": ["8.8.8.8"], "DnsSearch": [""], "VolumesFrom": ["parent", "other:ro"], @@ -783,7 +782,7 @@ Block until container `id` stops, then returns the exit code HTTP/1.1 200 OK Content-Type: application/json - {"StatusCode":0} + {"StatusCode": 0} Status Codes: @@ -831,7 +830,7 @@ Copy files or folders of container `id` Content-Type: application/json { - "Resource":"test.txt" + "Resource": "test.txt" } **Example response**: @@ -909,9 +908,9 @@ Create an image, either by pulling it from the registry or by importing it 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..."} + {"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 @@ -954,31 +953,31 @@ Return low-level information on the image `name` Content-Type: application/json { - "Created":"2013-03-23T22:24:18.818426-07:00", - "Container":"3d67245a8d72ecf13f33dffac9f79dcdf70f75acb84d308770391510e0c23ad0", + "Created": "2013-03-23T22:24:18.818426-07:00", + "Container": "3d67245a8d72ecf13f33dffac9f79dcdf70f75acb84d308770391510e0c23ad0", "ContainerConfig": { - "Hostname":"", - "User":"", - "Memory":0, - "MemorySwap":0, - "AttachStdin":false, - "AttachStdout":false, - "AttachStderr":false, - "PortSpecs":null, - "Tty":true, - "OpenStdin":true, - "StdinOnce":false, - "Env":null, + "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":"" + "Dns": null, + "Image": "base", + "Volumes": null, + "VolumesFrom": "", + "WorkingDir": "" }, - "Id":"b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", - "Parent":"27cf784147099545", + "Id": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", + "Parent": "27cf784147099545", "Size": 6824592 } @@ -1005,14 +1004,14 @@ Return the history of the image `name` [ { - "Id":"b750fe79269d", - "Created":1364102658, - "CreatedBy":"/bin/bash" + "Id": "b750fe79269d", + "Created": 1364102658, + "CreatedBy": "/bin/bash" }, { - "Id":"27cf78414709", - "Created":1364068391, - "CreatedBy":"" + "Id": "27cf78414709", + "Created": 1364068391, + "CreatedBy": "" } ] @@ -1037,9 +1036,9 @@ Push the image `name` on the registry HTTP/1.1 200 OK Content-Type: application/json - {"status":"Pushing..."} - {"status":"Pushing", "progress":"1/? (n/a)", "progressDetail":{"current":1}}} - {"error":"Invalid..."} + {"status": "Pushing..."} + {"status": "Pushing", "progress": "1/? (n/a)", "progressDetail": {"current": 1}}} + {"error": "Invalid..."} ... If you wish to push an image on to a private registry, that image must already have been tagged @@ -1110,9 +1109,9 @@ Remove the image `name` from the filesystem Content-type: application/json [ - {"Untagged":"3e2f21a89f"}, - {"Deleted":"3e2f21a89f"}, - {"Deleted":"53b4f83ac9"} + {"Untagged": "3e2f21a89f"}, + {"Deleted": "3e2f21a89f"}, + {"Deleted": "53b4f83ac9"} ] Query Parameters: @@ -1199,9 +1198,9 @@ Build an image from Dockerfile via stdin HTTP/1.1 200 OK Content-Type: application/json - {"stream":"Step 1..."} - {"stream":"..."} - {"error":"Error...", "errorDetail":{"code": 123, "message": "Error..."}} + {"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. @@ -1242,10 +1241,10 @@ Get the default username and email Content-Type: application/json { - "username":"hannibal", - "password:"xxxx", - "email":"hannibal@a-team.com", - "serveraddress":"https://index.docker.io/v1/" + "username":" hannibal", + "password: "xxxx", + "email": "hannibal@a-team.com", + "serveraddress": "https://index.docker.io/v1/" } **Example response**: @@ -1274,20 +1273,20 @@ Display system-wide information Content-Type: application/json { - "Containers":11, - "Images":16, - "Driver":"btrfs", - "ExecutionDriver":"native-0.1", - "KernelVersion":"3.12.0-1-amd64" - "Debug":false, + "Containers": 11, + "Images": 16, + "Driver": "btrfs", + "ExecutionDriver": "native-0.1", + "KernelVersion": "3.12.0-1-amd64" + "Debug": false, "NFd": 11, - "NGoroutines":21, - "NEventsListener":0, - "InitPath":"/usr/bin/docker", - "IndexServerAddress":["https://index.docker.io/v1/"], - "MemoryLimit":true, - "SwapLimit":false, - "IPv4Forwarding":true + "NGoroutines": 21, + "NEventsListener": 0, + "InitPath": "/usr/bin/docker", + "IndexServerAddress": ["https://index.docker.io/v1/"], + "MemoryLimit": true, + "SwapLimit": false, + "IPv4Forwarding": true } Status Codes: @@ -1311,10 +1310,10 @@ Show the docker version information Content-Type: application/json { - "ApiVersion":"1.12", - "Version":"0.2.2", - "GitCommit":"5a2a5cc+CHANGES", - "GoVersion":"go1.0.3" + "ApiVersion": "1.12", + "Version": "0.2.2", + "GitCommit": "5a2a5cc+CHANGES", + "GoVersion": "go1.0.3" } Status Codes: @@ -1356,30 +1355,30 @@ Create a new image from a container's changes Content-Type: application/json { - "Hostname":"", + "Hostname": "", "Domainname": "", - "User":"", - "Memory":0, - "MemorySwap":0, + "User": "", + "Memory": 0, + "MemorySwap": 0, "CpuShares": 512, "Cpuset": "0,1", - "AttachStdin":false, - "AttachStdout":true, - "AttachStderr":true, - "PortSpecs":null, - "Tty":false, - "OpenStdin":false, - "StdinOnce":false, - "Env":null, - "Cmd":[ + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "PortSpecs": null, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": null, + "Cmd": [ "date" ], - "Volumes":{ + "Volumes": { "/tmp": {} }, - "WorkingDir":"", + "WorkingDir": "", "NetworkDisabled": false, - "ExposedPorts":{ + "ExposedPorts": { "22/tcp": {} } } @@ -1389,7 +1388,7 @@ Create a new image from a container's changes HTTP/1.1 201 Created Content-Type: application/vnd.docker.raw-stream - {"Id":"596069db4bf5"} + {"Id": "596069db4bf5"} Json Parameters: @@ -1434,10 +1433,10 @@ and Docker images will report: 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} + {"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 Parameters: @@ -1547,7 +1546,7 @@ the root that contains a list of repository and tag names mapped to layer IDs. ``` {"hello-world": - {"latest":"565a9d68a73f6706862bfe8409a7f659776d4d60a8d096eb4a3cbce6999cc2a1"} + {"latest": "565a9d68a73f6706862bfe8409a7f659776d4d60a8d096eb4a3cbce6999cc2a1"} } ``` @@ -1563,11 +1562,11 @@ Sets up an exec instance in a running container `id` Content-Type: application/json { - "AttachStdin":false, - "AttachStdout":true, - "AttachStderr":true, - "Tty":false, - "Cmd":[ + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "Tty": false, + "Cmd": [ "date" ], } @@ -1578,7 +1577,7 @@ Sets up an exec instance in a running container `id` Content-Type: application/json { - "Id":"f90e34656806" + "Id": "f90e34656806" } Json Parameters: @@ -1609,8 +1608,8 @@ interactive session with the `exec` command. Content-Type: application/json { - "Detach":false, - "Tty":false, + "Detach": false, + "Tty": false, } **Example response**: From 9bbed5ab4ceaff5e78c21f0fa2d84de5ffd41f94 Mon Sep 17 00:00:00 2001 From: Derek Date: Sat, 3 Jan 2015 14:07:24 -0800 Subject: [PATCH 152/513] change to lazy Unmount syscall.Unmount failed sometimes when user interrupted exporting, for example a Ctrl-C, or pipe to commands which closed the pipe early, like "docker export | file -"; this syscall.Unmount could sometimes return EBUSY and didn't actually umount the filesystem; which would cause a following export command fail to mount; change to lazy Unmount with MNT_DETACH can fix the problem, this is the same behavior as in Shutdown; ```text time="2015-01-03T21:27:26Z" level=error msg="Warning: error unmounting device 34a3e77cdbca17ceffd0636aee0415bb412996adb12360bfe2585ce30467fa8e: device or resource busy" ``` ``` $ docker export thirsty_ardinghelli | file - /dev/stdin: POSIX tar archive time="2015-01-03T21:58:17Z" level=fatal msg="write /dev/stdout: broken pipe" $ docker export thirsty_ardinghelli time="2015-01-03T21:54:33Z" level=fatal msg="Error: thirsty_ardinghelli: Error getting container 34a3e77cdbca17ceffd0636aee0415bb412996adb12360bfe2585ce30467fa8e from driver devicemapper: Error mounting '/dev/mapper/docker-253:0-3148372-34a3e77cdbca17ceffd0636aee0415bb412996adb12360bfe2585ce30467fa8e' on '/var/lib/docker/devicemapper/mnt/34a3e77cdbca17ceffd0636aee0415bb412996adb12360bfe2585ce30467fa8e': device or resource busy" ``` Signed-off-by: Derek Che --- daemon/graphdriver/devmapper/deviceset.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/graphdriver/devmapper/deviceset.go b/daemon/graphdriver/devmapper/deviceset.go index 658000d75..078e31a1e 100644 --- a/daemon/graphdriver/devmapper/deviceset.go +++ b/daemon/graphdriver/devmapper/deviceset.go @@ -1433,7 +1433,7 @@ func (devices *DeviceSet) UnmountDevice(hash string) error { } log.Debugf("[devmapper] Unmount(%s)", info.mountPath) - if err := syscall.Unmount(info.mountPath, 0); err != nil { + if err := syscall.Unmount(info.mountPath, syscall.MNT_DETACH); err != nil { return err } log.Debugf("[devmapper] Unmount done") From f957f258d722fa563ead0a14978acca7c6745d3f Mon Sep 17 00:00:00 2001 From: Lorenz Leutgeb Date: Sun, 4 Jan 2015 20:57:20 +0100 Subject: [PATCH 153/513] doc: Do not encrypt private keys Do not encrypt private keys in the first place, if the encryption is stripped anyway. Signed-off-by: Lorenz Leutgeb --- docs/sources/articles/https.md | 27 ++++++--------------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/docs/sources/articles/https.md b/docs/sources/articles/https.md index 2fe5162d6..ab5ed2095 100644 --- a/docs/sources/articles/https.md +++ b/docs/sources/articles/https.md @@ -15,13 +15,13 @@ In the daemon mode, it will only allow connections from clients authenticated by a certificate signed by that CA. In the client mode, it will only connect to servers with a certificate signed by that CA. -> **Warning**: +> **Warning**: > Using TLS and managing a CA is an advanced topic. Please familiarize yourself > with OpenSSL, x509 and TLS before using it in production. > **Warning**: > These TLS commands will only generate a working set of certificates on Linux. -> Mac OS X comes with a version of OpenSSL that is incompatible with the +> Mac OS X comes with a version of OpenSSL that is incompatible with the > certificates that Docker requires. ## Create a CA, server and client keys with OpenSSL @@ -58,15 +58,12 @@ Now that we have a CA, you can create a server key and certificate signing request (CSR). Make sure that "Common Name" (i.e. server FQDN or YOUR name) matches the hostname you will use to connect to Docker: - $ openssl genrsa -des3 -out server-key.pem 2048 + $ openssl genrsa -out server-key.pem 2048 Generating RSA private key, 2048 bit long modulus ......................................................+++ ............................................+++ e is 65537 (0x10001) - Enter pass phrase for server-key.pem: - Verifying - Enter pass phrase for server-key.pem: $ openssl req -subj '/CN=' -new -key server-key.pem -out server.csr - Enter pass phrase for server-key.pem: Next, we're going to sign the key with our CA: @@ -80,15 +77,12 @@ Next, we're going to sign the key with our CA: For client authentication, create a client key and certificate signing request: - $ openssl genrsa -des3 -out key.pem 2048 + $ openssl genrsa -out key.pem 2048 Generating RSA private key, 2048 bit long modulus ...............................................+++ ...............................................................+++ e is 65537 (0x10001) - Enter pass phrase for key.pem: - Verifying - Enter pass phrase for key.pem: $ openssl req -subj '/CN=client' -new -key key.pem -out client.csr - Enter pass phrase for key.pem: To make the key suitable for client authentication, create an extensions config file: @@ -104,15 +98,6 @@ Now sign the key: Getting CA Private Key Enter pass phrase for ca-key.pem: -Finally, you need to remove the passphrase from the client and server key: - - $ openssl rsa -in server-key.pem -out server-key.pem - Enter pass phrase for server-key.pem: - writing RSA key - $ openssl rsa -in key.pem -out key.pem - Enter pass phrase for key.pem: - writing RSA key - Now you can make the Docker daemon only accept connections from clients providing a certificate trusted by our CA: @@ -128,7 +113,7 @@ need to provide your client keys, certificates and trusted CA: > **Note**: > Docker over TLS should run on TCP port 2376. -> **Warning**: +> **Warning**: > As shown in the example above, you don't have to run the `docker` client > with `sudo` or the `docker` group when you use certificate authentication. > That means anyone with the keys can give any instructions to your Docker @@ -137,7 +122,7 @@ need to provide your client keys, certificates and trusted CA: ## Secure by default -If you want to secure your Docker client connections by default, you can move +If you want to secure your Docker client connections by default, you can move the files to the `.docker` directory in your home directory - and set the `DOCKER_HOST` and `DOCKER_TLS_VERIFY` variables as well (instead of passing `-H=tcp://:2376` and `--tlsverify` on every call). From a3d5f874c108d3e7d58a7f86c0ef0eea6fcca85f Mon Sep 17 00:00:00 2001 From: Lorenz Leutgeb Date: Sun, 4 Jan 2015 21:15:30 +0100 Subject: [PATCH 154/513] doc: Spice up generated CA Use AES (the successor of DES) to encrypt private key. Further reading: * http://csrc.nist.gov/publications/nistpubs/800-131A/sp800-131A.pdf * https://ssllabs.com/downloads/SSL_TLS_Deployment_Best_Practices.pdf "3DES provides about 112 bits of security. This is below the recommended minimum of 128 bits, but it's still strong enough. A bigger practical problem is that 3DES is much slower than the alternatives. Thus, we don't recommend it for performance reasons, but it can be kept at the end of the cipher list for interoperability with very old clients." * http://csrc.nist.gov/publications/nistpubs/800-67-Rev1/SP-800-67-Rev1.pdf Use SHA256 for our CA. This avoids accidental use of SHA1 or MD5 which could be default values. Signed-off-by: Lorenz Leutgeb --- docs/sources/articles/https.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sources/articles/https.md b/docs/sources/articles/https.md index ab5ed2095..834206b0b 100644 --- a/docs/sources/articles/https.md +++ b/docs/sources/articles/https.md @@ -30,14 +30,14 @@ First, initialize the CA serial file and generate CA private and public keys: $ echo 01 > ca.srl - $ openssl genrsa -des3 -out ca-key.pem 2048 + $ openssl genrsa -aes256 -out ca-key.pem 2048 Generating RSA private key, 2048 bit long modulus ......+++ ...............+++ e is 65537 (0x10001) Enter pass phrase for ca-key.pem: Verifying - Enter pass phrase for ca-key.pem: - $ openssl req -new -x509 -days 365 -key ca-key.pem -out ca.pem + $ openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -out ca.pem Enter pass phrase for ca-key.pem: You are about to be asked to enter information that will be incorporated into your certificate request. From 131c62d7661ace86453de540cb1a58956b59e347 Mon Sep 17 00:00:00 2001 From: Lorenz Leutgeb Date: Sun, 4 Jan 2015 21:49:16 +0100 Subject: [PATCH 155/513] doc: Let OpenSSL handle serial file With -CAcreateserial the serial file will be automatically created and initialized if it is missing. Signed-off-by: Lorenz Leutgeb --- docs/sources/articles/https.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/sources/articles/https.md b/docs/sources/articles/https.md index 834206b0b..925ba8f8f 100644 --- a/docs/sources/articles/https.md +++ b/docs/sources/articles/https.md @@ -26,10 +26,8 @@ it will only connect to servers with a certificate signed by that CA. ## Create a CA, server and client keys with OpenSSL -First, initialize the CA serial file and generate CA private and public -keys: +First generate CA private and public keys: - $ echo 01 > ca.srl $ openssl genrsa -aes256 -out ca-key.pem 2048 Generating RSA private key, 2048 bit long modulus ......+++ @@ -68,7 +66,7 @@ name) matches the hostname you will use to connect to Docker: Next, we're going to sign the key with our CA: $ openssl x509 -req -days 365 -in server.csr -CA ca.pem -CAkey ca-key.pem \ - -out server-cert.pem + -CAcreateserial -out server-cert.pem Signature ok subject=/CN=your.host.com Getting CA Private Key @@ -92,7 +90,7 @@ config file: Now sign the key: $ openssl x509 -req -days 365 -in client.csr -CA ca.pem -CAkey ca-key.pem \ - -out cert.pem -extfile extfile.cnf + -CAcreateserial -out cert.pem -extfile extfile.cnf Signature ok subject=/CN=client Getting CA Private Key From 26187bd851141236a909c0bada5a2743fc237e0e Mon Sep 17 00:00:00 2001 From: Lorenz Leutgeb Date: Mon, 5 Jan 2015 01:24:33 +0100 Subject: [PATCH 156/513] doc: Fix curl invocation Using --insecure is (you guessed it) *insecure* as the server side certificate is not being validated. To offer the same degree of security as invocations of the docker client in "Secure by default" with cURL, the trusted CA certificate must be supplied. Signed-off-by: Lorenz Leutgeb --- docs/sources/articles/https.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/sources/articles/https.md b/docs/sources/articles/https.md index 925ba8f8f..cf1ccaef6 100644 --- a/docs/sources/articles/https.md +++ b/docs/sources/articles/https.md @@ -167,4 +167,7 @@ location using the environment variable `DOCKER_CERT_PATH`. To use `curl` to make test API requests, you need to use three extra command line flags: - $ curl --insecure --cert ~/.docker/cert.pem --key ~/.docker/key.pem https://boot2docker:2376/images/json` + $ curl https://boot2docker:2376/images/json \ + --cert ~/.docker/cert.pem \ + --key ~/.docker/key.pem \ + --cacert ~/.docker/ca.pem From e3c5e0ceb0e83c393472ed2e2f12ca754d47e6b3 Mon Sep 17 00:00:00 2001 From: PatrickJS Date: Sun, 4 Jan 2015 16:59:34 -0800 Subject: [PATCH 157/513] Update License year to range 2013-2015 Copyright notices must reflect the current year. This commit updates the listed year to 2015 with a starting year of 2013 from https://github.com/docker/docker/commit/a27b4b8cb8e838d03a99b6d2b30f76bdaf2f9e5d Docker-DCO-1.1-Signed-off-by: Patrick Stapleton (github: gdi2290) --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 27448585a..508036ef4 100644 --- a/LICENSE +++ b/LICENSE @@ -176,7 +176,7 @@ END OF TERMS AND CONDITIONS - Copyright 2014 Docker, Inc. + Copyright 2013-2015 Docker, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 975f5b0c281fe579336fc0ffeb49ac0907465a5a Mon Sep 17 00:00:00 2001 From: Lorenz Leutgeb Date: Mon, 5 Jan 2015 11:45:10 +0100 Subject: [PATCH 158/513] doc: Broaden JSON standardisation by patching This is: git format-patch -1 --stdout HEAD \ | patch -p1 docs/sources/reference/api/docker_remote_api_v1.*.md Applying the changes I initially made on the docs for v1.15 to all other versions led to acceptable results. Signed-off-by: Lorenz Leutgeb --- .../reference/api/docker_remote_api_v1.0.md | 28 +- .../reference/api/docker_remote_api_v1.1.md | 28 +- .../reference/api/docker_remote_api_v1.10.md | 94 +++---- .../reference/api/docker_remote_api_v1.11.md | 123 +++++---- .../reference/api/docker_remote_api_v1.12.md | 205 ++++++++------- .../reference/api/docker_remote_api_v1.13.md | 205 ++++++++------- .../reference/api/docker_remote_api_v1.14.md | 209 ++++++++------- .../reference/api/docker_remote_api_v1.16.md | 245 +++++++++--------- .../reference/api/docker_remote_api_v1.17.md | 245 +++++++++--------- .../reference/api/docker_remote_api_v1.2.md | 22 +- .../reference/api/docker_remote_api_v1.3.md | 34 +-- .../reference/api/docker_remote_api_v1.4.md | 51 ++-- .../reference/api/docker_remote_api_v1.5.md | 4 +- .../reference/api/docker_remote_api_v1.6.md | 77 +++--- .../reference/api/docker_remote_api_v1.7.md | 77 +++--- .../reference/api/docker_remote_api_v1.8.md | 97 ++++--- .../reference/api/docker_remote_api_v1.9.md | 97 ++++--- 17 files changed, 915 insertions(+), 926 deletions(-) diff --git a/docs/sources/reference/api/docker_remote_api_v1.0.md b/docs/sources/reference/api/docker_remote_api_v1.0.md index 3d8eedacf..49ff939d6 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.0.md +++ b/docs/sources/reference/api/docker_remote_api_v1.0.md @@ -218,16 +218,16 @@ Inspect changes on container `id`'s filesystem [ { - "Path":"/dev", - "Kind":0 + "Path": "/dev", + "Kind": 0 }, { - "Path":"/dev/kmsg", - "Kind":1 + "Path": "/dev/kmsg", + "Kind": 1 }, { - "Path":"/test", - "Kind":1 + "Path": "/test", + "Kind": 1 } ] @@ -400,7 +400,7 @@ Block until container `id` stops, then returns the exit code HTTP/1.1 200 OK Content-Type: application/json - {"StatusCode":0} + {"StatusCode": 0} Status Codes: @@ -625,14 +625,14 @@ Return the history of the image `name` [ { - "Id":"b750fe79269d", - "Created":1364102658, - "CreatedBy":"/bin/bash" + "Id": "b750fe79269d", + "Created": 1364102658, + "CreatedBy": "/bin/bash" }, { - "Id":"27cf78414709", - "Created":1364068391, - "CreatedBy":"" + "Id": "27cf78414709", + "Created": 1364068391, + "CreatedBy": "" } ] @@ -906,7 +906,7 @@ Create a new image from a container's changes HTTP/1.1 201 OK Content-Type: application/vnd.docker.raw-stream - {"Id":"596069db4bf5"} + {"Id": "596069db4bf5"} Query Parameters: diff --git a/docs/sources/reference/api/docker_remote_api_v1.1.md b/docs/sources/reference/api/docker_remote_api_v1.1.md index 705544bd9..6cf7ed74b 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.1.md +++ b/docs/sources/reference/api/docker_remote_api_v1.1.md @@ -218,16 +218,16 @@ Inspect changes on container `id`'s filesystem [ { - "Path":"/dev", - "Kind":0 + "Path": "/dev", + "Kind": 0 }, { - "Path":"/dev/kmsg", - "Kind":1 + "Path": "/dev/kmsg", + "Kind": 1 }, { - "Path":"/test", - "Kind":1 + "Path": "/test", + "Kind": 1 } ] @@ -400,7 +400,7 @@ Block until container `id` stops, then returns the exit code HTTP/1.1 200 OK Content-Type: application/json - {"StatusCode":0} + {"StatusCode": 0} Status Codes: @@ -632,14 +632,14 @@ Return the history of the image `name` [ { - "Id":"b750fe79269d", - "Created":1364102658, - "CreatedBy":"/bin/bash" + "Id": "b750fe79269d", + "Created": 1364102658, + "CreatedBy": "/bin/bash" }, { - "Id":"27cf78414709", - "Created":1364068391, - "CreatedBy":"" + "Id": "27cf78414709", + "Created": 1364068391, + "CreatedBy": "" } ] @@ -919,7 +919,7 @@ Create a new image from a container's changes HTTP/1.1 201 OK Content-Type: application/vnd.docker.raw-stream - {"Id":"596069db4bf5"} + {"Id": "596069db4bf5"} Query Parameters: diff --git a/docs/sources/reference/api/docker_remote_api_v1.10.md b/docs/sources/reference/api/docker_remote_api_v1.10.md index 1855ccad6..2358da101 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.10.md +++ b/docs/sources/reference/api/docker_remote_api_v1.10.md @@ -39,9 +39,9 @@ List containers "Command": "echo 1", "Created": 1367854155, "Status": "Exit 0", - "Ports":[{"PrivatePort": 2222, "PublicPort": 3333, "Type": "tcp"}], - "SizeRw":12288, - "SizeRootFs":0 + "Ports": [{"PrivatePort": 2222, "PublicPort": 3333, "Type": "tcp"}], + "SizeRw": 12288, + "SizeRootFs": 0 }, { "Id": "9cd87474be90", @@ -49,9 +49,9 @@ List containers "Command": "echo 222222", "Created": 1367854155, "Status": "Exit 0", - "Ports":[], - "SizeRw":12288, - "SizeRootFs":0 + "Ports": [], + "SizeRw": 12288, + "SizeRootFs": 0 }, { "Id": "3176a2479c92", @@ -69,9 +69,9 @@ List containers "Command": "echo 444444444444444444444444444444444", "Created": 1367854152, "Status": "Exit 0", - "Ports":[], - "SizeRw":12288, - "SizeRootFs":0 + "Ports": [], + "SizeRw": 12288, + "SizeRootFs": 0 } ] @@ -257,7 +257,7 @@ List processes running inside the container `id` Content-Type: application/json { - "Titles":[ + "Titles": [ "USER", "PID", "%CPU", @@ -270,7 +270,7 @@ List processes running inside the container `id` "TIME", "COMMAND" ], - "Processes":[ + "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"] ] @@ -305,16 +305,16 @@ Inspect changes on container `id` 's filesystem [ { - "Path":"/dev", - "Kind":0 + "Path": "/dev", + "Kind": 0 }, { - "Path":"/dev/kmsg", - "Kind":1 + "Path": "/dev/kmsg", + "Kind": 1 }, { - "Path":"/test", - "Kind":1 + "Path": "/test", + "Kind": 1 } ] @@ -555,7 +555,7 @@ Block until container `id` stops, then returns HTTP/1.1 200 OK Content-Type: application/json - {"StatusCode":0} + {"StatusCode": 0} Status Codes: @@ -602,7 +602,7 @@ Copy files or folders of container `id` Content-Type: application/json { - "Resource":"test.txt" + "Resource": "test.txt" } **Example response**: @@ -674,9 +674,9 @@ Create an image, either by pull it from the registry or by importing 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..."} + {"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 @@ -796,14 +796,14 @@ Return the history of the image `name` [ { - "Id":"b750fe79269d", - "Created":1364102658, - "CreatedBy":"/bin/bash" + "Id": "b750fe79269d", + "Created": 1364102658, + "CreatedBy": "/bin/bash" }, { - "Id":"27cf78414709", - "Created":1364068391, - "CreatedBy":"" + "Id": "27cf78414709", + "Created": 1364068391, + "CreatedBy": "" } ] @@ -828,9 +828,9 @@ Push the image `name` on the registry HTTP/1.1 200 OK Content-Type: application/json - {"status":"Pushing..."} - {"status":"Pushing", "progress":"1/? (n/a)", "progressDetail":{"current":1}}} - {"error":"Invalid..."} + {"status": "Pushing..."} + {"status": "Pushing", "progress": "1/? (n/a)", "progressDetail": {"current": 1}}} + {"error": "Invalid..."} ... If you wish to push an image on to a private registry, that image must already have been tagged @@ -899,9 +899,9 @@ Status Codes: Content-type: application/json [ - {"Untagged":"3e2f21a89f"}, - {"Deleted":"3e2f21a89f"}, - {"Deleted":"53b4f83ac9"} + {"Untagged": "3e2f21a89f"}, + {"Deleted": "3e2f21a89f"}, + {"Deleted": "53b4f83ac9"} ] Query Parameters: @@ -988,9 +988,9 @@ Build an image from Dockerfile via stdin HTTP/1.1 200 OK Content-Type: application/json - {"stream":"Step 1..."} - {"stream":"..."} - {"error":"Error...", "errorDetail":{"code": 123, "message": "Error..."}} + {"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. @@ -1030,10 +1030,10 @@ Get the default username and email Content-Type: application/json { - "username":"hannibal", - "password:"xxxx", - "email":"hannibal@a-team.com", - "serveraddress":"https://index.docker.io/v1/" + "username":" hannibal", + "password: "xxxx", + "email": "hannibal@a-team.com", + "serveraddress": "https://index.docker.io/v1/" } **Example response**: @@ -1146,7 +1146,7 @@ Create a new image from a container's changes HTTP/1.1 201 OK Content-Type: application/vnd.docker.raw-stream - {"Id":"596069db4bf5"} + {"Id": "596069db4bf5"} Json Parameters: @@ -1194,10 +1194,10 @@ and Docker images will report: 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} + {"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 Parameters: @@ -1273,7 +1273,7 @@ the root that contains a list of repository and tag names mapped to layer IDs. ``` {"hello-world": - {"latest":"565a9d68a73f6706862bfe8409a7f659776d4d60a8d096eb4a3cbce6999cc2a1"} + {"latest": "565a9d68a73f6706862bfe8409a7f659776d4d60a8d096eb4a3cbce6999cc2a1"} } ``` diff --git a/docs/sources/reference/api/docker_remote_api_v1.11.md b/docs/sources/reference/api/docker_remote_api_v1.11.md index dcf566ffb..6303f708e 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.11.md +++ b/docs/sources/reference/api/docker_remote_api_v1.11.md @@ -39,9 +39,9 @@ List containers "Command": "echo 1", "Created": 1367854155, "Status": "Exit 0", - "Ports":[{"PrivatePort": 2222, "PublicPort": 3333, "Type": "tcp"}], - "SizeRw":12288, - "SizeRootFs":0 + "Ports": [{"PrivatePort": 2222, "PublicPort": 3333, "Type": "tcp"}], + "SizeRw": 12288, + "SizeRootFs": 0 }, { "Id": "9cd87474be90", @@ -49,9 +49,9 @@ List containers "Command": "echo 222222", "Created": 1367854155, "Status": "Exit 0", - "Ports":[], - "SizeRw":12288, - "SizeRootFs":0 + "Ports": [], + "SizeRw": 12288, + "SizeRootFs": 0 }, { "Id": "3176a2479c92", @@ -69,9 +69,9 @@ List containers "Command": "echo 444444444444444444444444444444444", "Created": 1367854152, "Status": "Exit 0", - "Ports":[], - "SizeRw":12288, - "SizeRootFs":0 + "Ports": [], + "SizeRw": 12288, + "SizeRootFs": 0 } ] @@ -198,8 +198,7 @@ Return low-level information on the container `id` "Image": "base", "Volumes": {}, "VolumesFrom": "", - "WorkingDir":"" - + "WorkingDir": "" }, "State": { "Running": false, @@ -259,7 +258,7 @@ List processes running inside the container `id` Content-Type: application/json { - "Titles":[ + "Titles": [ "USER", "PID", "%CPU", @@ -272,7 +271,7 @@ List processes running inside the container `id` "TIME", "COMMAND" ], - "Processes":[ + "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"] ] @@ -341,16 +340,16 @@ Inspect changes on container `id`'s filesystem [ { - "Path":"/dev", - "Kind":0 + "Path": "/dev", + "Kind": 0 }, { - "Path":"/dev/kmsg", - "Kind":1 + "Path": "/dev/kmsg", + "Kind": 1 }, { - "Path":"/test", - "Kind":1 + "Path": "/test", + "Kind": 1 } ] @@ -590,7 +589,7 @@ Block until container `id` stops, then returns the exit code HTTP/1.1 200 OK Content-Type: application/json - {"StatusCode":0} + {"StatusCode": 0} Status Codes: @@ -638,7 +637,7 @@ Copy files or folders of container `id` Content-Type: application/json { - "Resource":"test.txt" + "Resource": "test.txt" } **Example response**: @@ -709,9 +708,9 @@ Create an image, either by pull it from the registry or by importing i 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..."} + {"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 @@ -802,14 +801,14 @@ Return the history of the image `name` [ { - "Id":"b750fe79269d", - "Created":1364102658, - "CreatedBy":"/bin/bash" + "Id": "b750fe79269d", + "Created": 1364102658, + "CreatedBy": "/bin/bash" }, { - "Id":"27cf78414709", - "Created":1364068391, - "CreatedBy":"" + "Id": "27cf78414709", + "Created": 1364068391, + "CreatedBy": "" } ] @@ -834,9 +833,9 @@ Push the image `name` on the registry HTTP/1.1 200 OK Content-Type: application/json - {"status":"Pushing..."} - {"status":"Pushing", "progress":"1/? (n/a)", "progressDetail":{"current":1}}} - {"error":"Invalid..."} + {"status": "Pushing..."} + {"status": "Pushing", "progress": "1/? (n/a)", "progressDetail": {"current": 1}}} + {"error": "Invalid..."} ... If you wish to push an image on to a private registry, that image must already have been tagged @@ -906,9 +905,9 @@ Remove the image `name` from the filesystem Content-type: application/json [ - {"Untagged":"3e2f21a89f"}, - {"Deleted":"3e2f21a89f"}, - {"Deleted":"53b4f83ac9"} + {"Untagged": "3e2f21a89f"}, + {"Deleted": "3e2f21a89f"}, + {"Deleted": "53b4f83ac9"} ] Query Parameters: @@ -995,9 +994,9 @@ Build an image from Dockerfile via stdin HTTP/1.1 200 OK Content-Type: application/json - {"stream":"Step 1..."} - {"stream":"..."} - {"error":"Error...", "errorDetail":{"code": 123, "message": "Error..."}} + {"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. @@ -1037,10 +1036,10 @@ Get the default username and email Content-Type: application/json { - "username":"hannibal", - "password:"xxxx", - "email":"hannibal@a-team.com", - "serveraddress":"https://index.docker.io/v1/" + "username":" hannibal", + "password: "xxxx", + "email": "hannibal@a-team.com", + "serveraddress": "https://index.docker.io/v1/" } **Example response**: @@ -1069,20 +1068,20 @@ Display system-wide information Content-Type: application/json { - "Containers":11, - "Images":16, - "Driver":"btrfs", - "ExecutionDriver":"native-0.1", - "KernelVersion":"3.12.0-1-amd64" - "Debug":false, + "Containers": 11, + "Images": 16, + "Driver": "btrfs", + "ExecutionDriver": "native-0.1", + "KernelVersion": "3.12.0-1-amd64" + "Debug": false, "NFd": 11, - "NGoroutines":21, - "NEventsListener":0, - "InitPath":"/usr/bin/docker", - "IndexServerAddress":["https://index.docker.io/v1/"], - "MemoryLimit":true, - "SwapLimit":false, - "IPv4Forwarding":true + "NGoroutines": 21, + "NEventsListener": 0, + "InitPath": "/usr/bin/docker", + "IndexServerAddress": ["https://index.docker.io/v1/"], + "MemoryLimit": true, + "SwapLimit": false, + "IPv4Forwarding": true } Status Codes: @@ -1180,7 +1179,7 @@ Create a new image from a container's changes HTTP/1.1 201 Created Content-Type: application/vnd.docker.raw-stream - {"Id":"596069db4bf5"} + {"Id": "596069db4bf5"} Json Parameters: @@ -1225,10 +1224,10 @@ and Docker images will report: 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} + {"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 Parameters: @@ -1305,7 +1304,7 @@ the root that contains a list of repository and tag names mapped to layer IDs. ``` {"hello-world": - {"latest":"565a9d68a73f6706862bfe8409a7f659776d4d60a8d096eb4a3cbce6999cc2a1"} + {"latest": "565a9d68a73f6706862bfe8409a7f659776d4d60a8d096eb4a3cbce6999cc2a1"} } ``` diff --git a/docs/sources/reference/api/docker_remote_api_v1.12.md b/docs/sources/reference/api/docker_remote_api_v1.12.md index e0676d133..685d43ee5 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.12.md +++ b/docs/sources/reference/api/docker_remote_api_v1.12.md @@ -40,9 +40,9 @@ List containers "Command": "echo 1", "Created": 1367854155, "Status": "Exit 0", - "Ports":[{"PrivatePort": 2222, "PublicPort": 3333, "Type": "tcp"}], - "SizeRw":12288, - "SizeRootFs":0 + "Ports": [{"PrivatePort": 2222, "PublicPort": 3333, "Type": "tcp"}], + "SizeRw": 12288, + "SizeRootFs": 0 }, { "Id": "9cd87474be90", @@ -50,9 +50,9 @@ List containers "Command": "echo 222222", "Created": 1367854155, "Status": "Exit 0", - "Ports":[], - "SizeRw":12288, - "SizeRootFs":0 + "Ports": [], + "SizeRw": 12288, + "SizeRootFs": 0 }, { "Id": "3176a2479c92", @@ -70,9 +70,9 @@ List containers "Command": "echo 444444444444444444444444444444444", "Created": 1367854152, "Status": "Exit 0", - "Ports":[], - "SizeRw":12288, - "SizeRootFs":0 + "Ports": [], + "SizeRw": 12288, + "SizeRootFs": 0 } ] @@ -207,8 +207,7 @@ Return low-level information on the container `id` "Image": "base", "Volumes": {}, "VolumesFrom": "", - "WorkingDir":"" - + "WorkingDir": "" }, "State": { "Running": false, @@ -268,7 +267,7 @@ List processes running inside the container `id` Content-Type: application/json { - "Titles":[ + "Titles": [ "USER", "PID", "%CPU", @@ -281,7 +280,7 @@ List processes running inside the container `id` "TIME", "COMMAND" ], - "Processes":[ + "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"] ] @@ -350,16 +349,16 @@ Inspect changes on container `id`'s filesystem [ { - "Path":"/dev", - "Kind":0 + "Path": "/dev", + "Kind": 0 }, { - "Path":"/dev/kmsg", - "Kind":1 + "Path": "/dev/kmsg", + "Kind": 1 }, { - "Path":"/test", - "Kind":1 + "Path": "/test", + "Kind": 1 } ] @@ -638,7 +637,7 @@ Block until container `id` stops, then returns the exit code HTTP/1.1 200 OK Content-Type: application/json - {"StatusCode":0} + {"StatusCode": 0} Status Codes: @@ -686,7 +685,7 @@ Copy files or folders of container `id` Content-Type: application/json { - "Resource":"test.txt" + "Resource": "test.txt" } **Example response**: @@ -768,9 +767,9 @@ Create an image, either by pull it from the registry or by importing i 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..."} + {"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 @@ -812,31 +811,31 @@ Return low-level information on the image `name` Content-Type: application/json { - "Created":"2013-03-23T22:24:18.818426-07:00", - "Container":"3d67245a8d72ecf13f33dffac9f79dcdf70f75acb84d308770391510e0c23ad0", + "Created": "2013-03-23T22:24:18.818426-07:00", + "Container": "3d67245a8d72ecf13f33dffac9f79dcdf70f75acb84d308770391510e0c23ad0", "ContainerConfig": { - "Hostname":"", - "User":"", - "Memory":0, - "MemorySwap":0, - "AttachStdin":false, - "AttachStdout":false, - "AttachStderr":false, - "PortSpecs":null, - "Tty":true, - "OpenStdin":true, - "StdinOnce":false, - "Env":null, + "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":"" + "Dns": null, + "Image": "base", + "Volumes": null, + "VolumesFrom": "", + "WorkingDir": "" }, - "Id":"b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", - "Parent":"27cf784147099545", + "Id": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", + "Parent": "27cf784147099545", "Size": 6824592 } @@ -863,14 +862,14 @@ Return the history of the image `name` [ { - "Id":"b750fe79269d", - "Created":1364102658, - "CreatedBy":"/bin/bash" + "Id": "b750fe79269d", + "Created": 1364102658, + "CreatedBy": "/bin/bash" }, { - "Id":"27cf78414709", - "Created":1364068391, - "CreatedBy":"" + "Id": "27cf78414709", + "Created": 1364068391, + "CreatedBy": "" } ] @@ -895,9 +894,9 @@ Push the image `name` on the registry HTTP/1.1 200 OK Content-Type: application/json - {"status":"Pushing..."} - {"status":"Pushing", "progress":"1/? (n/a)", "progressDetail":{"current":1}}} - {"error":"Invalid..."} + {"status": "Pushing..."} + {"status": "Pushing", "progress": "1/? (n/a)", "progressDetail": {"current": 1}}} + {"error": "Invalid..."} ... If you wish to push an image on to a private registry, that image must already have been tagged @@ -967,9 +966,9 @@ Remove the image `name` from the filesystem Content-type: application/json [ - {"Untagged":"3e2f21a89f"}, - {"Deleted":"3e2f21a89f"}, - {"Deleted":"53b4f83ac9"} + {"Untagged": "3e2f21a89f"}, + {"Deleted": "3e2f21a89f"}, + {"Deleted": "53b4f83ac9"} ] Query Parameters: @@ -1056,9 +1055,9 @@ Build an image from Dockerfile via stdin HTTP/1.1 200 OK Content-Type: application/json - {"stream":"Step 1..."} - {"stream":"..."} - {"error":"Error...", "errorDetail":{"code": 123, "message": "Error..."}} + {"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. @@ -1099,10 +1098,10 @@ Get the default username and email Content-Type: application/json { - "username":"hannibal", - "password:"xxxx", - "email":"hannibal@a-team.com", - "serveraddress":"https://index.docker.io/v1/" + "username":" hannibal", + "password: "xxxx", + "email": "hannibal@a-team.com", + "serveraddress": "https://index.docker.io/v1/" } **Example response**: @@ -1131,20 +1130,20 @@ Display system-wide information Content-Type: application/json { - "Containers":11, - "Images":16, - "Driver":"btrfs", - "ExecutionDriver":"native-0.1", - "KernelVersion":"3.12.0-1-amd64" - "Debug":false, + "Containers": 11, + "Images": 16, + "Driver": "btrfs", + "ExecutionDriver": "native-0.1", + "KernelVersion": "3.12.0-1-amd64" + "Debug": false, "NFd": 11, - "NGoroutines":21, - "NEventsListener":0, - "InitPath":"/usr/bin/docker", - "IndexServerAddress":["https://index.docker.io/v1/"], - "MemoryLimit":true, - "SwapLimit":false, - "IPv4Forwarding":true + "NGoroutines": 21, + "NEventsListener": 0, + "InitPath": "/usr/bin/docker", + "IndexServerAddress": ["https://index.docker.io/v1/"], + "MemoryLimit": true, + "SwapLimit": false, + "IPv4Forwarding": true } Status Codes: @@ -1168,10 +1167,10 @@ Show the docker version information Content-Type: application/json { - "ApiVersion":"1.12", - "Version":"0.2.2", - "GitCommit":"5a2a5cc+CHANGES", - "GoVersion":"go1.0.3" + "ApiVersion": "1.12", + "Version": "0.2.2", + "GitCommit": "5a2a5cc+CHANGES", + "GoVersion": "go1.0.3" } Status Codes: @@ -1213,30 +1212,30 @@ Create a new image from a container's changes Content-Type: application/json { - "Hostname":"", + "Hostname": "", "Domainname": "", - "User":"", - "Memory":0, - "MemorySwap":0, + "User": "", + "Memory": 0, + "MemorySwap": 0, "CpuShares": 512, "Cpuset": "0,1", - "AttachStdin":false, - "AttachStdout":true, - "AttachStderr":true, - "PortSpecs":null, - "Tty":false, - "OpenStdin":false, - "StdinOnce":false, - "Env":null, - "Cmd":[ + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "PortSpecs": null, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": null, + "Cmd": [ "date" ], - "Volumes":{ + "Volumes": { "/tmp": {} }, - "WorkingDir":"", + "WorkingDir": "", "NetworkDisabled": false, - "ExposedPorts":{ + "ExposedPorts": { "22/tcp": {} } } @@ -1246,7 +1245,7 @@ Create a new image from a container's changes HTTP/1.1 201 Created Content-Type: application/vnd.docker.raw-stream - {"Id":"596069db4bf5"} + {"Id": "596069db4bf5"} Json Parameters: @@ -1291,10 +1290,10 @@ and Docker images will report: 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} + {"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 Parameters: @@ -1370,7 +1369,7 @@ the root that contains a list of repository and tag names mapped to layer IDs. ``` {"hello-world": - {"latest":"565a9d68a73f6706862bfe8409a7f659776d4d60a8d096eb4a3cbce6999cc2a1"} + {"latest": "565a9d68a73f6706862bfe8409a7f659776d4d60a8d096eb4a3cbce6999cc2a1"} } ``` diff --git a/docs/sources/reference/api/docker_remote_api_v1.13.md b/docs/sources/reference/api/docker_remote_api_v1.13.md index 47aa02a98..2c38c9aa1 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.13.md +++ b/docs/sources/reference/api/docker_remote_api_v1.13.md @@ -40,9 +40,9 @@ List containers "Command": "echo 1", "Created": 1367854155, "Status": "Exit 0", - "Ports":[{"PrivatePort": 2222, "PublicPort": 3333, "Type": "tcp"}], - "SizeRw":12288, - "SizeRootFs":0 + "Ports": [{"PrivatePort": 2222, "PublicPort": 3333, "Type": "tcp"}], + "SizeRw": 12288, + "SizeRootFs": 0 }, { "Id": "9cd87474be90", @@ -50,9 +50,9 @@ List containers "Command": "echo 222222", "Created": 1367854155, "Status": "Exit 0", - "Ports":[], - "SizeRw":12288, - "SizeRootFs":0 + "Ports": [], + "SizeRw": 12288, + "SizeRootFs": 0 }, { "Id": "3176a2479c92", @@ -70,9 +70,9 @@ List containers "Command": "echo 444444444444444444444444444444444", "Created": 1367854152, "Status": "Exit 0", - "Ports":[], - "SizeRw":12288, - "SizeRootFs":0 + "Ports": [], + "SizeRw": 12288, + "SizeRootFs": 0 } ] @@ -201,8 +201,7 @@ Return low-level information on the container `id` "Image": "base", "Volumes": {}, "VolumesFrom": "", - "WorkingDir":"" - + "WorkingDir": "" }, "State": { "Running": false, @@ -262,7 +261,7 @@ List processes running inside the container `id` Content-Type: application/json { - "Titles":[ + "Titles": [ "USER", "PID", "%CPU", @@ -275,7 +274,7 @@ List processes running inside the container `id` "TIME", "COMMAND" ], - "Processes":[ + "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"] ] @@ -341,16 +340,16 @@ Inspect changes on container `id`'s filesystem [ { - "Path":"/dev", - "Kind":0 + "Path": "/dev", + "Kind": 0 }, { - "Path":"/dev/kmsg", - "Kind":1 + "Path": "/dev/kmsg", + "Kind": 1 }, { - "Path":"/test", - "Kind":1 + "Path": "/test", + "Kind": 1 } ] @@ -631,7 +630,7 @@ Block until container `id` stops, then returns the exit code HTTP/1.1 200 OK Content-Type: application/json - {"StatusCode":0} + {"StatusCode": 0} Status Codes: @@ -679,7 +678,7 @@ Copy files or folders of container `id` Content-Type: application/json { - "Resource":"test.txt" + "Resource": "test.txt" } **Example response**: @@ -757,9 +756,9 @@ Create an image, either by pulling it from the registry or by importing it 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..."} + {"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 @@ -801,31 +800,31 @@ Return low-level information on the image `name` Content-Type: application/json { - "Created":"2013-03-23T22:24:18.818426-07:00", - "Container":"3d67245a8d72ecf13f33dffac9f79dcdf70f75acb84d308770391510e0c23ad0", + "Created": "2013-03-23T22:24:18.818426-07:00", + "Container": "3d67245a8d72ecf13f33dffac9f79dcdf70f75acb84d308770391510e0c23ad0", "ContainerConfig": { - "Hostname":"", - "User":"", - "Memory":0, - "MemorySwap":0, - "AttachStdin":false, - "AttachStdout":false, - "AttachStderr":false, - "PortSpecs":null, - "Tty":true, - "OpenStdin":true, - "StdinOnce":false, - "Env":null, + "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":"" + "Dns": null, + "Image": "base", + "Volumes": null, + "VolumesFrom": "", + "WorkingDir": "" }, - "Id":"b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", - "Parent":"27cf784147099545", + "Id": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", + "Parent": "27cf784147099545", "Size": 6824592 } @@ -852,14 +851,14 @@ Return the history of the image `name` [ { - "Id":"b750fe79269d", - "Created":1364102658, - "CreatedBy":"/bin/bash" + "Id": "b750fe79269d", + "Created": 1364102658, + "CreatedBy": "/bin/bash" }, { - "Id":"27cf78414709", - "Created":1364068391, - "CreatedBy":"" + "Id": "27cf78414709", + "Created": 1364068391, + "CreatedBy": "" } ] @@ -884,9 +883,9 @@ Push the image `name` on the registry HTTP/1.1 200 OK Content-Type: application/json - {"status":"Pushing..."} - {"status":"Pushing", "progress":"1/? (n/a)", "progressDetail":{"current":1}}} - {"error":"Invalid..."} + {"status": "Pushing..."} + {"status": "Pushing", "progress": "1/? (n/a)", "progressDetail": {"current": 1}}} + {"error": "Invalid..."} ... If you wish to push an image on to a private registry, that image must already have been tagged @@ -956,9 +955,9 @@ Remove the image `name` from the filesystem Content-type: application/json [ - {"Untagged":"3e2f21a89f"}, - {"Deleted":"3e2f21a89f"}, - {"Deleted":"53b4f83ac9"} + {"Untagged": "3e2f21a89f"}, + {"Deleted": "3e2f21a89f"}, + {"Deleted": "53b4f83ac9"} ] Query Parameters: @@ -1045,9 +1044,9 @@ Build an image from Dockerfile via stdin HTTP/1.1 200 OK Content-Type: application/json - {"stream":"Step 1..."} - {"stream":"..."} - {"error":"Error...", "errorDetail":{"code": 123, "message": "Error..."}} + {"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. @@ -1088,10 +1087,10 @@ Get the default username and email Content-Type: application/json { - "username":"hannibal", - "password:"xxxx", - "email":"hannibal@a-team.com", - "serveraddress":"https://index.docker.io/v1/" + "username":" hannibal", + "password: "xxxx", + "email": "hannibal@a-team.com", + "serveraddress": "https://index.docker.io/v1/" } **Example response**: @@ -1120,20 +1119,20 @@ Display system-wide information Content-Type: application/json { - "Containers":11, - "Images":16, - "Driver":"btrfs", - "ExecutionDriver":"native-0.1", - "KernelVersion":"3.12.0-1-amd64" - "Debug":false, + "Containers": 11, + "Images": 16, + "Driver": "btrfs", + "ExecutionDriver": "native-0.1", + "KernelVersion": "3.12.0-1-amd64" + "Debug": false, "NFd": 11, - "NGoroutines":21, - "NEventsListener":0, - "InitPath":"/usr/bin/docker", - "IndexServerAddress":["https://index.docker.io/v1/"], - "MemoryLimit":true, - "SwapLimit":false, - "IPv4Forwarding":true + "NGoroutines": 21, + "NEventsListener": 0, + "InitPath": "/usr/bin/docker", + "IndexServerAddress": ["https://index.docker.io/v1/"], + "MemoryLimit": true, + "SwapLimit": false, + "IPv4Forwarding": true } Status Codes: @@ -1157,10 +1156,10 @@ Show the docker version information Content-Type: application/json { - "ApiVersion":"1.12", - "Version":"0.2.2", - "GitCommit":"5a2a5cc+CHANGES", - "GoVersion":"go1.0.3" + "ApiVersion": "1.12", + "Version": "0.2.2", + "GitCommit": "5a2a5cc+CHANGES", + "GoVersion": "go1.0.3" } Status Codes: @@ -1202,30 +1201,30 @@ Create a new image from a container's changes Content-Type: application/json { - "Hostname":"", + "Hostname": "", "Domainname": "", - "User":"", - "Memory":0, - "MemorySwap":0, + "User": "", + "Memory": 0, + "MemorySwap": 0, "CpuShares": 512, "Cpuset": "0,1", - "AttachStdin":false, - "AttachStdout":true, - "AttachStderr":true, - "PortSpecs":null, - "Tty":false, - "OpenStdin":false, - "StdinOnce":false, - "Env":null, - "Cmd":[ + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "PortSpecs": null, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": null, + "Cmd": [ "date" ], - "Volumes":{ + "Volumes": { "/tmp": {} }, - "WorkingDir":"", + "WorkingDir": "", "NetworkDisabled": false, - "ExposedPorts":{ + "ExposedPorts": { "22/tcp": {} } } @@ -1235,7 +1234,7 @@ Create a new image from a container's changes HTTP/1.1 201 Created Content-Type: application/vnd.docker.raw-stream - {"Id":"596069db4bf5"} + {"Id": "596069db4bf5"} Json Parameters: @@ -1280,10 +1279,10 @@ and Docker images will report: 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} + {"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 Parameters: @@ -1360,7 +1359,7 @@ the root that contains a list of repository and tag names mapped to layer IDs. ``` {"hello-world": - {"latest":"565a9d68a73f6706862bfe8409a7f659776d4d60a8d096eb4a3cbce6999cc2a1"} + {"latest": "565a9d68a73f6706862bfe8409a7f659776d4d60a8d096eb4a3cbce6999cc2a1"} } ``` diff --git a/docs/sources/reference/api/docker_remote_api_v1.14.md b/docs/sources/reference/api/docker_remote_api_v1.14.md index 8e5952bb5..7ce0df677 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.14.md +++ b/docs/sources/reference/api/docker_remote_api_v1.14.md @@ -40,9 +40,9 @@ List containers "Command": "echo 1", "Created": 1367854155, "Status": "Exit 0", - "Ports":[{"PrivatePort": 2222, "PublicPort": 3333, "Type": "tcp"}], - "SizeRw":12288, - "SizeRootFs":0 + "Ports": [{"PrivatePort": 2222, "PublicPort": 3333, "Type": "tcp"}], + "SizeRw": 12288, + "SizeRootFs": 0 }, { "Id": "9cd87474be90", @@ -50,9 +50,9 @@ List containers "Command": "echo 222222", "Created": 1367854155, "Status": "Exit 0", - "Ports":[], - "SizeRw":12288, - "SizeRootFs":0 + "Ports": [], + "SizeRw": 12288, + "SizeRootFs": 0 }, { "Id": "3176a2479c92", @@ -70,9 +70,9 @@ List containers "Command": "echo 444444444444444444444444444444444", "Created": 1367854152, "Status": "Exit 0", - "Ports":[], - "SizeRw":12288, - "SizeRootFs":0 + "Ports": [], + "SizeRw": 12288, + "SizeRootFs": 0 } ] @@ -210,8 +210,7 @@ Return low-level information on the container `id` "Image": "base", "Volumes": {}, "VolumesFrom": "", - "WorkingDir":"" - + "WorkingDir": "" }, "State": { "Running": false, @@ -246,8 +245,8 @@ Return low-level information on the container `id` }, "Links": ["/name:alias"], "PublishAllPorts": false, - "CapAdd: ["NET_ADMIN"], - "CapDrop: ["MKNOD"] + "CapAdd": ["NET_ADMIN"], + "CapDrop": ["MKNOD"] } } @@ -273,7 +272,7 @@ List processes running inside the container `id` Content-Type: application/json { - "Titles":[ + "Titles": [ "USER", "PID", "%CPU", @@ -286,7 +285,7 @@ List processes running inside the container `id` "TIME", "COMMAND" ], - "Processes":[ + "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"] ] @@ -352,16 +351,16 @@ Inspect changes on container `id`'s filesystem [ { - "Path":"/dev", - "Kind":0 + "Path": "/dev", + "Kind": 0 }, { - "Path":"/dev/kmsg", - "Kind":1 + "Path": "/dev/kmsg", + "Kind": 1 }, { - "Path":"/test", - "Kind":1 + "Path": "/test", + "Kind": 1 } ] @@ -641,7 +640,7 @@ Block until container `id` stops, then returns the exit code HTTP/1.1 200 OK Content-Type: application/json - {"StatusCode":0} + {"StatusCode": 0} Status Codes: @@ -689,7 +688,7 @@ Copy files or folders of container `id` Content-Type: application/json { - "Resource":"test.txt" + "Resource": "test.txt" } **Example response**: @@ -767,9 +766,9 @@ Create an image, either by pulling it from the registry or by importing it 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..."} + {"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 @@ -811,31 +810,31 @@ Return low-level information on the image `name` Content-Type: application/json { - "Created":"2013-03-23T22:24:18.818426-07:00", - "Container":"3d67245a8d72ecf13f33dffac9f79dcdf70f75acb84d308770391510e0c23ad0", + "Created": "2013-03-23T22:24:18.818426-07:00", + "Container": "3d67245a8d72ecf13f33dffac9f79dcdf70f75acb84d308770391510e0c23ad0", "ContainerConfig": { - "Hostname":"", - "User":"", - "Memory":0, - "MemorySwap":0, - "AttachStdin":false, - "AttachStdout":false, - "AttachStderr":false, - "PortSpecs":null, - "Tty":true, - "OpenStdin":true, - "StdinOnce":false, - "Env":null, + "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":"" + "Dns": null, + "Image": "base", + "Volumes": null, + "VolumesFrom": "", + "WorkingDir": "" }, - "Id":"b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", - "Parent":"27cf784147099545", + "Id": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", + "Parent": "27cf784147099545", "Size": 6824592 } @@ -862,14 +861,14 @@ Return the history of the image `name` [ { - "Id":"b750fe79269d", - "Created":1364102658, - "CreatedBy":"/bin/bash" + "Id": "b750fe79269d", + "Created": 1364102658, + "CreatedBy": "/bin/bash" }, { - "Id":"27cf78414709", - "Created":1364068391, - "CreatedBy":"" + "Id": "27cf78414709", + "Created": 1364068391, + "CreatedBy": "" } ] @@ -894,9 +893,9 @@ Push the image `name` on the registry HTTP/1.1 200 OK Content-Type: application/json - {"status":"Pushing..."} - {"status":"Pushing", "progress":"1/? (n/a)", "progressDetail":{"current":1}}} - {"error":"Invalid..."} + {"status": "Pushing..."} + {"status": "Pushing", "progress": "1/? (n/a)", "progressDetail": {"current": 1}}} + {"error": "Invalid..."} ... If you wish to push an image on to a private registry, that image must already have been tagged @@ -966,9 +965,9 @@ Remove the image `name` from the filesystem Content-type: application/json [ - {"Untagged":"3e2f21a89f"}, - {"Deleted":"3e2f21a89f"}, - {"Deleted":"53b4f83ac9"} + {"Untagged": "3e2f21a89f"}, + {"Deleted": "3e2f21a89f"}, + {"Deleted": "53b4f83ac9"} ] Query Parameters: @@ -1055,9 +1054,9 @@ Build an image from Dockerfile via stdin HTTP/1.1 200 OK Content-Type: application/json - {"stream":"Step 1..."} - {"stream":"..."} - {"error":"Error...", "errorDetail":{"code": 123, "message": "Error..."}} + {"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. @@ -1098,10 +1097,10 @@ Get the default username and email Content-Type: application/json { - "username":"hannibal", - "password:"xxxx", - "email":"hannibal@a-team.com", - "serveraddress":"https://index.docker.io/v1/" + "username":" hannibal", + "password: "xxxx", + "email": "hannibal@a-team.com", + "serveraddress": "https://index.docker.io/v1/" } **Example response**: @@ -1130,20 +1129,20 @@ Display system-wide information Content-Type: application/json { - "Containers":11, - "Images":16, - "Driver":"btrfs", - "ExecutionDriver":"native-0.1", - "KernelVersion":"3.12.0-1-amd64" - "Debug":false, + "Containers": 11, + "Images": 16, + "Driver": "btrfs", + "ExecutionDriver": "native-0.1", + "KernelVersion": "3.12.0-1-amd64" + "Debug": false, "NFd": 11, - "NGoroutines":21, - "NEventsListener":0, - "InitPath":"/usr/bin/docker", - "IndexServerAddress":["https://index.docker.io/v1/"], - "MemoryLimit":true, - "SwapLimit":false, - "IPv4Forwarding":true + "NGoroutines": 21, + "NEventsListener": 0, + "InitPath": "/usr/bin/docker", + "IndexServerAddress": ["https://index.docker.io/v1/"], + "MemoryLimit": true, + "SwapLimit": false, + "IPv4Forwarding": true } Status Codes: @@ -1167,10 +1166,10 @@ Show the docker version information Content-Type: application/json { - "ApiVersion":"1.12", - "Version":"0.2.2", - "GitCommit":"5a2a5cc+CHANGES", - "GoVersion":"go1.0.3" + "ApiVersion": "1.12", + "Version": "0.2.2", + "GitCommit": "5a2a5cc+CHANGES", + "GoVersion": "go1.0.3" } Status Codes: @@ -1212,30 +1211,30 @@ Create a new image from a container's changes Content-Type: application/json { - "Hostname":"", + "Hostname": "", "Domainname": "", - "User":"", - "Memory":0, - "MemorySwap":0, + "User": "", + "Memory": 0, + "MemorySwap": 0, "CpuShares": 512, "Cpuset": "0,1", - "AttachStdin":false, - "AttachStdout":true, - "AttachStderr":true, - "PortSpecs":null, - "Tty":false, - "OpenStdin":false, - "StdinOnce":false, - "Env":null, - "Cmd":[ + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "PortSpecs": null, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": null, + "Cmd": [ "date" ], - "Volumes":{ + "Volumes": { "/tmp": {} }, - "WorkingDir":"", + "WorkingDir": "", "NetworkDisabled": false, - "ExposedPorts":{ + "ExposedPorts": { "22/tcp": {} } } @@ -1245,7 +1244,7 @@ Create a new image from a container's changes HTTP/1.1 201 Created Content-Type: application/vnd.docker.raw-stream - {"Id":"596069db4bf5"} + {"Id": "596069db4bf5"} Json Parameters: @@ -1290,10 +1289,10 @@ and Docker images will report: 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} + {"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 Parameters: @@ -1369,7 +1368,7 @@ the root that contains a list of repository and tag names mapped to layer IDs. ``` {"hello-world": - {"latest":"565a9d68a73f6706862bfe8409a7f659776d4d60a8d096eb4a3cbce6999cc2a1"} + {"latest": "565a9d68a73f6706862bfe8409a7f659776d4d60a8d096eb4a3cbce6999cc2a1"} } ``` diff --git a/docs/sources/reference/api/docker_remote_api_v1.16.md b/docs/sources/reference/api/docker_remote_api_v1.16.md index b4ef52b3e..43df4306c 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.16.md +++ b/docs/sources/reference/api/docker_remote_api_v1.16.md @@ -40,9 +40,9 @@ List containers "Command": "echo 1", "Created": 1367854155, "Status": "Exit 0", - "Ports":[{"PrivatePort": 2222, "PublicPort": 3333, "Type": "tcp"}], - "SizeRw":12288, - "SizeRootFs":0 + "Ports": [{"PrivatePort": 2222, "PublicPort": 3333, "Type": "tcp"}], + "SizeRw": 12288, + "SizeRootFs": 0 }, { "Id": "9cd87474be90", @@ -50,9 +50,9 @@ List containers "Command": "echo 222222", "Created": 1367854155, "Status": "Exit 0", - "Ports":[], - "SizeRw":12288, - "SizeRootFs":0 + "Ports": [], + "SizeRw": 12288, + "SizeRootFs": 0 }, { "Id": "3176a2479c92", @@ -70,9 +70,9 @@ List containers "Command": "echo 444444444444444444444444444444444", "Created": 1367854152, "Status": "Exit 0", - "Ports":[], - "SizeRw":12288, - "SizeRootFs":0 + "Ports": [], + "SizeRw": 12288, + "SizeRootFs": 0 } ] @@ -110,42 +110,42 @@ Create a container Content-Type: application/json { - "Hostname":"", + "Hostname": "", "Domainname": "", - "User":"", - "Memory":0, - "MemorySwap":0, + "User": "", + "Memory": 0, + "MemorySwap": 0, "CpuShares": 512, "Cpuset": "0,1", - "AttachStdin":false, - "AttachStdout":true, - "AttachStderr":true, - "Tty":false, - "OpenStdin":false, - "StdinOnce":false, - "Env":null, - "Cmd":[ + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": null, + "Cmd": [ "date" ], "Entrypoint": "", - "Image":"base", - "Volumes":{ + "Image": "base", + "Volumes": { "/tmp": {} }, - "WorkingDir":"", + "WorkingDir": "", "NetworkDisabled": false, - "MacAddress":"12:34:56:78:9a:bc", - "ExposedPorts":{ + "MacAddress": "12:34:56:78:9a:bc", + "ExposedPorts": { "22/tcp": {} }, "SecurityOpts": [""], "HostConfig": { - "Binds":["/tmp:/tmp"], - "Links":["redis3:redis"], - "LxcConf":{"lxc.utsname":"docker"}, - "PortBindings":{ "22/tcp": [{ "HostPort": "11022" }] }, - "PublishAllPorts":false, - "Privileged":false, + "Binds": ["/tmp:/tmp"], + "Links": ["redis3:redis"], + "LxcConf": {"lxc.utsname":"docker"}, + "PortBindings": { "22/tcp": [{ "HostPort": "11022" }] }, + "PublishAllPorts": false, + "Privileged": false, "Dns": ["8.8.8.8"], "DnsSearch": [""], "VolumesFrom": ["parent", "other:ro"], @@ -291,8 +291,7 @@ Return low-level information on the container `id` "Image": "base", "Volumes": {}, "VolumesFrom": "", - "WorkingDir":"" - + "WorkingDir": "" }, "State": { "Running": false, @@ -327,8 +326,8 @@ Return low-level information on the container `id` }, "Links": ["/name:alias"], "PublishAllPorts": false, - "CapAdd: ["NET_ADMIN"], - "CapDrop: ["MKNOD"] + "CapAdd": ["NET_ADMIN"], + "CapDrop": ["MKNOD"] } } @@ -354,7 +353,7 @@ List processes running inside the container `id` Content-Type: application/json { - "Titles":[ + "Titles": [ "USER", "PID", "%CPU", @@ -367,7 +366,7 @@ List processes running inside the container `id` "TIME", "COMMAND" ], - "Processes":[ + "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"] ] @@ -432,16 +431,16 @@ Inspect changes on container `id`'s filesystem [ { - "Path":"/dev", - "Kind":0 + "Path": "/dev", + "Kind": 0 }, { - "Path":"/dev/kmsg", - "Kind":1 + "Path": "/dev/kmsg", + "Kind": 1 }, { - "Path":"/test", - "Kind":1 + "Path": "/test", + "Kind": 1 } ] @@ -729,7 +728,7 @@ Block until container `id` stops, then returns the exit code HTTP/1.1 200 OK Content-Type: application/json - {"StatusCode":0} + {"StatusCode": 0} Status Codes: @@ -777,7 +776,7 @@ Copy files or folders of container `id` Content-Type: application/json { - "Resource":"test.txt" + "Resource": "test.txt" } **Example response**: @@ -855,9 +854,9 @@ Create an image, either by pulling it from the registry or by importing it 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..."} + {"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 @@ -900,31 +899,31 @@ Return low-level information on the image `name` Content-Type: application/json { - "Created":"2013-03-23T22:24:18.818426-07:00", - "Container":"3d67245a8d72ecf13f33dffac9f79dcdf70f75acb84d308770391510e0c23ad0", + "Created": "2013-03-23T22:24:18.818426-07:00", + "Container": "3d67245a8d72ecf13f33dffac9f79dcdf70f75acb84d308770391510e0c23ad0", "ContainerConfig": { - "Hostname":"", - "User":"", - "Memory":0, - "MemorySwap":0, - "AttachStdin":false, - "AttachStdout":false, - "AttachStderr":false, - "PortSpecs":null, - "Tty":true, - "OpenStdin":true, - "StdinOnce":false, - "Env":null, + "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":"" + "Dns": null, + "Image": "base", + "Volumes": null, + "VolumesFrom": "", + "WorkingDir": "" }, - "Id":"b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", - "Parent":"27cf784147099545", + "Id": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", + "Parent": "27cf784147099545", "Size": 6824592 } @@ -951,14 +950,14 @@ Return the history of the image `name` [ { - "Id":"b750fe79269d", - "Created":1364102658, - "CreatedBy":"/bin/bash" + "Id": "b750fe79269d", + "Created": 1364102658, + "CreatedBy": "/bin/bash" }, { - "Id":"27cf78414709", - "Created":1364068391, - "CreatedBy":"" + "Id": "27cf78414709", + "Created": 1364068391, + "CreatedBy": "" } ] @@ -983,9 +982,9 @@ Push the image `name` on the registry HTTP/1.1 200 OK Content-Type: application/json - {"status":"Pushing..."} - {"status":"Pushing", "progress":"1/? (n/a)", "progressDetail":{"current":1}}} - {"error":"Invalid..."} + {"status": "Pushing..."} + {"status": "Pushing", "progress": "1/? (n/a)", "progressDetail": {"current": 1}}} + {"error": "Invalid..."} ... If you wish to push an image on to a private registry, that image must already have been tagged @@ -1056,9 +1055,9 @@ Remove the image `name` from the filesystem Content-type: application/json [ - {"Untagged":"3e2f21a89f"}, - {"Deleted":"3e2f21a89f"}, - {"Deleted":"53b4f83ac9"} + {"Untagged": "3e2f21a89f"}, + {"Deleted": "3e2f21a89f"}, + {"Deleted": "53b4f83ac9"} ] Query Parameters: @@ -1145,9 +1144,9 @@ Build an image from Dockerfile via stdin HTTP/1.1 200 OK Content-Type: application/json - {"stream":"Step 1..."} - {"stream":"..."} - {"error":"Error...", "errorDetail":{"code": 123, "message": "Error..."}} + {"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. @@ -1189,10 +1188,10 @@ Get the default username and email Content-Type: application/json { - "username":"hannibal", - "password:"xxxx", - "email":"hannibal@a-team.com", - "serveraddress":"https://index.docker.io/v1/" + "username":" hannibal", + "password: "xxxx", + "email": "hannibal@a-team.com", + "serveraddress": "https://index.docker.io/v1/" } **Example response**: @@ -1263,10 +1262,10 @@ Show the docker version information Content-Type: application/json { - "ApiVersion":"1.12", - "Version":"0.2.2", - "GitCommit":"5a2a5cc+CHANGES", - "GoVersion":"go1.0.3" + "ApiVersion": "1.12", + "Version": "0.2.2", + "GitCommit": "5a2a5cc+CHANGES", + "GoVersion": "go1.0.3" } Status Codes: @@ -1308,30 +1307,30 @@ Create a new image from a container's changes Content-Type: application/json { - "Hostname":"", + "Hostname": "", "Domainname": "", - "User":"", - "Memory":0, - "MemorySwap":0, + "User": "", + "Memory": 0, + "MemorySwap": 0, "CpuShares": 512, "Cpuset": "0,1", - "AttachStdin":false, - "AttachStdout":true, - "AttachStderr":true, - "PortSpecs":null, - "Tty":false, - "OpenStdin":false, - "StdinOnce":false, - "Env":null, - "Cmd":[ + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "PortSpecs": null, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": null, + "Cmd": [ "date" ], - "Volumes":{ + "Volumes": { "/tmp": {} }, - "WorkingDir":"", + "WorkingDir": "", "NetworkDisabled": false, - "ExposedPorts":{ + "ExposedPorts": { "22/tcp": {} } } @@ -1341,7 +1340,7 @@ Create a new image from a container's changes HTTP/1.1 201 Created Content-Type: application/vnd.docker.raw-stream - {"Id":"596069db4bf5"} + {"Id": "596069db4bf5"} Json Parameters: @@ -1386,10 +1385,10 @@ and Docker images will report: 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} + {"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 Parameters: @@ -1503,7 +1502,7 @@ the root that contains a list of repository and tag names mapped to layer IDs. ``` {"hello-world": - {"latest":"565a9d68a73f6706862bfe8409a7f659776d4d60a8d096eb4a3cbce6999cc2a1"} + {"latest": "565a9d68a73f6706862bfe8409a7f659776d4d60a8d096eb4a3cbce6999cc2a1"} } ``` @@ -1519,11 +1518,11 @@ Sets up an exec instance in a running container `id` Content-Type: application/json { - "AttachStdin":false, - "AttachStdout":true, - "AttachStderr":true, - "Tty":false, - "Cmd":[ + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "Tty": false, + "Cmd": [ "date" ], } @@ -1534,7 +1533,7 @@ Sets up an exec instance in a running container `id` Content-Type: application/json { - "Id":"f90e34656806" + "Id": "f90e34656806" } Json Parameters: @@ -1565,8 +1564,8 @@ interactive session with the `exec` command. Content-Type: application/json { - "Detach":false, - "Tty":false, + "Detach": false, + "Tty": false, } **Example response**: diff --git a/docs/sources/reference/api/docker_remote_api_v1.17.md b/docs/sources/reference/api/docker_remote_api_v1.17.md index 0164b9581..506cb89aa 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.17.md +++ b/docs/sources/reference/api/docker_remote_api_v1.17.md @@ -40,9 +40,9 @@ List containers "Command": "echo 1", "Created": 1367854155, "Status": "Exit 0", - "Ports":[{"PrivatePort": 2222, "PublicPort": 3333, "Type": "tcp"}], - "SizeRw":12288, - "SizeRootFs":0 + "Ports": [{"PrivatePort": 2222, "PublicPort": 3333, "Type": "tcp"}], + "SizeRw": 12288, + "SizeRootFs": 0 }, { "Id": "9cd87474be90", @@ -50,9 +50,9 @@ List containers "Command": "echo 222222", "Created": 1367854155, "Status": "Exit 0", - "Ports":[], - "SizeRw":12288, - "SizeRootFs":0 + "Ports": [], + "SizeRw": 12288, + "SizeRootFs": 0 }, { "Id": "3176a2479c92", @@ -70,9 +70,9 @@ List containers "Command": "echo 444444444444444444444444444444444", "Created": 1367854152, "Status": "Exit 0", - "Ports":[], - "SizeRw":12288, - "SizeRootFs":0 + "Ports": [], + "SizeRw": 12288, + "SizeRootFs": 0 } ] @@ -110,42 +110,42 @@ Create a container Content-Type: application/json { - "Hostname":"", + "Hostname": "", "Domainname": "", - "User":"", - "Memory":0, - "MemorySwap":0, + "User": "", + "Memory": 0, + "MemorySwap": 0, "CpuShares": 512, "Cpuset": "0,1", - "AttachStdin":false, - "AttachStdout":true, - "AttachStderr":true, - "Tty":false, - "OpenStdin":false, - "StdinOnce":false, - "Env":null, - "Cmd":[ + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": null, + "Cmd": [ "date" ], "Entrypoint": "", - "Image":"base", - "Volumes":{ + "Image": "base", + "Volumes": { "/tmp": {} }, - "WorkingDir":"", + "WorkingDir": "", "NetworkDisabled": false, - "MacAddress":"12:34:56:78:9a:bc", - "ExposedPorts":{ + "MacAddress": "12:34:56:78:9a:bc", + "ExposedPorts": { "22/tcp": {} }, "SecurityOpts": [""], "HostConfig": { - "Binds":["/tmp:/tmp"], - "Links":["redis3:redis"], - "LxcConf":{"lxc.utsname":"docker"}, - "PortBindings":{ "22/tcp": [{ "HostPort": "11022" }] }, - "PublishAllPorts":false, - "Privileged":false, + "Binds": ["/tmp:/tmp"], + "Links": ["redis3:redis"], + "LxcConf": {"lxc.utsname":"docker"}, + "PortBindings": { "22/tcp": [{ "HostPort": "11022" }] }, + "PublishAllPorts": false, + "Privileged": false, "Dns": ["8.8.8.8"], "DnsSearch": [""], "VolumesFrom": ["parent", "other:ro"], @@ -291,8 +291,7 @@ Return low-level information on the container `id` "Image": "base", "Volumes": {}, "VolumesFrom": "", - "WorkingDir":"" - + "WorkingDir": "" }, "State": { "Running": false, @@ -330,8 +329,8 @@ Return low-level information on the container `id` }, "Links": ["/name:alias"], "PublishAllPorts": false, - "CapAdd: ["NET_ADMIN"], - "CapDrop: ["MKNOD"] + "CapAdd": ["NET_ADMIN"], + "CapDrop": ["MKNOD"] } } @@ -357,7 +356,7 @@ List processes running inside the container `id` Content-Type: application/json { - "Titles":[ + "Titles": [ "USER", "PID", "%CPU", @@ -370,7 +369,7 @@ List processes running inside the container `id` "TIME", "COMMAND" ], - "Processes":[ + "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"] ] @@ -438,16 +437,16 @@ Inspect changes on container `id`'s filesystem [ { - "Path":"/dev", - "Kind":0 + "Path": "/dev", + "Kind": 0 }, { - "Path":"/dev/kmsg", - "Kind":1 + "Path": "/dev/kmsg", + "Kind": 1 }, { - "Path":"/test", - "Kind":1 + "Path": "/test", + "Kind": 1 } ] @@ -738,7 +737,7 @@ Block until container `id` stops, then returns the exit code HTTP/1.1 200 OK Content-Type: application/json - {"StatusCode":0} + {"StatusCode": 0} Status Codes: @@ -786,7 +785,7 @@ Copy files or folders of container `id` Content-Type: application/json { - "Resource":"test.txt" + "Resource": "test.txt" } **Example response**: @@ -864,9 +863,9 @@ Create an image, either by pulling it from the registry or by importing it 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..."} + {"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 @@ -909,31 +908,31 @@ Return low-level information on the image `name` Content-Type: application/json { - "Created":"2013-03-23T22:24:18.818426-07:00", - "Container":"3d67245a8d72ecf13f33dffac9f79dcdf70f75acb84d308770391510e0c23ad0", + "Created": "2013-03-23T22:24:18.818426-07:00", + "Container": "3d67245a8d72ecf13f33dffac9f79dcdf70f75acb84d308770391510e0c23ad0", "ContainerConfig": { - "Hostname":"", - "User":"", - "Memory":0, - "MemorySwap":0, - "AttachStdin":false, - "AttachStdout":false, - "AttachStderr":false, - "PortSpecs":null, - "Tty":true, - "OpenStdin":true, - "StdinOnce":false, - "Env":null, + "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":"" + "Dns": null, + "Image": "base", + "Volumes": null, + "VolumesFrom": "", + "WorkingDir": "" }, - "Id":"b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", - "Parent":"27cf784147099545", + "Id": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", + "Parent": "27cf784147099545", "Size": 6824592 } @@ -960,14 +959,14 @@ Return the history of the image `name` [ { - "Id":"b750fe79269d", - "Created":1364102658, - "CreatedBy":"/bin/bash" + "Id": "b750fe79269d", + "Created": 1364102658, + "CreatedBy": "/bin/bash" }, { - "Id":"27cf78414709", - "Created":1364068391, - "CreatedBy":"" + "Id": "27cf78414709", + "Created": 1364068391, + "CreatedBy": "" } ] @@ -992,9 +991,9 @@ Push the image `name` on the registry HTTP/1.1 200 OK Content-Type: application/json - {"status":"Pushing..."} - {"status":"Pushing", "progress":"1/? (n/a)", "progressDetail":{"current":1}}} - {"error":"Invalid..."} + {"status": "Pushing..."} + {"status": "Pushing", "progress": "1/? (n/a)", "progressDetail": {"current": 1}}} + {"error": "Invalid..."} ... If you wish to push an image on to a private registry, that image must already have been tagged @@ -1065,9 +1064,9 @@ Remove the image `name` from the filesystem Content-type: application/json [ - {"Untagged":"3e2f21a89f"}, - {"Deleted":"3e2f21a89f"}, - {"Deleted":"53b4f83ac9"} + {"Untagged": "3e2f21a89f"}, + {"Deleted": "3e2f21a89f"}, + {"Deleted": "53b4f83ac9"} ] Query Parameters: @@ -1154,9 +1153,9 @@ Build an image from Dockerfile via stdin HTTP/1.1 200 OK Content-Type: application/json - {"stream":"Step 1..."} - {"stream":"..."} - {"error":"Error...", "errorDetail":{"code": 123, "message": "Error..."}} + {"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. @@ -1198,10 +1197,10 @@ Get the default username and email Content-Type: application/json { - "username":"hannibal", - "password:"xxxx", - "email":"hannibal@a-team.com", - "serveraddress":"https://index.docker.io/v1/" + "username":" hannibal", + "password: "xxxx", + "email": "hannibal@a-team.com", + "serveraddress": "https://index.docker.io/v1/" } **Example response**: @@ -1272,10 +1271,10 @@ Show the docker version information Content-Type: application/json { - "ApiVersion":"1.12", - "Version":"0.2.2", - "GitCommit":"5a2a5cc+CHANGES", - "GoVersion":"go1.0.3" + "ApiVersion": "1.12", + "Version": "0.2.2", + "GitCommit": "5a2a5cc+CHANGES", + "GoVersion": "go1.0.3" } Status Codes: @@ -1317,30 +1316,30 @@ Create a new image from a container's changes Content-Type: application/json { - "Hostname":"", + "Hostname": "", "Domainname": "", - "User":"", - "Memory":0, - "MemorySwap":0, + "User": "", + "Memory": 0, + "MemorySwap": 0, "CpuShares": 512, "Cpuset": "0,1", - "AttachStdin":false, - "AttachStdout":true, - "AttachStderr":true, - "PortSpecs":null, - "Tty":false, - "OpenStdin":false, - "StdinOnce":false, - "Env":null, - "Cmd":[ + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "PortSpecs": null, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": null, + "Cmd": [ "date" ], - "Volumes":{ + "Volumes": { "/tmp": {} }, - "WorkingDir":"", + "WorkingDir": "", "NetworkDisabled": false, - "ExposedPorts":{ + "ExposedPorts": { "22/tcp": {} } } @@ -1350,7 +1349,7 @@ Create a new image from a container's changes HTTP/1.1 201 Created Content-Type: application/vnd.docker.raw-stream - {"Id":"596069db4bf5"} + {"Id": "596069db4bf5"} Json Parameters: @@ -1395,10 +1394,10 @@ and Docker images will report: 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} + {"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 Parameters: @@ -1512,7 +1511,7 @@ the root that contains a list of repository and tag names mapped to layer IDs. ``` {"hello-world": - {"latest":"565a9d68a73f6706862bfe8409a7f659776d4d60a8d096eb4a3cbce6999cc2a1"} + {"latest": "565a9d68a73f6706862bfe8409a7f659776d4d60a8d096eb4a3cbce6999cc2a1"} } ``` @@ -1528,11 +1527,11 @@ Sets up an exec instance in a running container `id` Content-Type: application/json { - "AttachStdin":false, - "AttachStdout":true, - "AttachStderr":true, - "Tty":false, - "Cmd":[ + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "Tty": false, + "Cmd": [ "date" ], } @@ -1543,7 +1542,7 @@ Sets up an exec instance in a running container `id` Content-Type: application/json { - "Id":"f90e34656806" + "Id": "f90e34656806" } Json Parameters: @@ -1574,8 +1573,8 @@ interactive session with the `exec` command. Content-Type: application/json { - "Detach":false, - "Tty":false, + "Detach": false, + "Tty": false, } **Example response**: diff --git a/docs/sources/reference/api/docker_remote_api_v1.2.md b/docs/sources/reference/api/docker_remote_api_v1.2.md index 4a518aea9..46f428bc9 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.2.md +++ b/docs/sources/reference/api/docker_remote_api_v1.2.md @@ -230,16 +230,16 @@ Inspect changes on container `id`'s filesystem [ { - "Path":"/dev", - "Kind":0 + "Path": "/dev", + "Kind": 0 }, { - "Path":"/dev/kmsg", - "Kind":1 + "Path": "/dev/kmsg", + "Kind": 1 }, { - "Path":"/test", - "Kind":1 + "Path": "/test", + "Kind": 1 } ] @@ -412,7 +412,7 @@ Block until container `id` stops, then returns the exit code HTTP/1.1 200 OK Content-Type: application/json - {"StatusCode":0} + {"StatusCode": 0} Status Codes: @@ -738,9 +738,9 @@ Remove the image `name` from the filesystem Content-type: application/json [ - {"Untagged":"3e2f21a89f"}, - {"Deleted":"3e2f21a89f"}, - {"Deleted":"53b4f83ac9"} + {"Untagged": "3e2f21a89f"}, + {"Deleted": "3e2f21a89f"}, + {"Deleted": "53b4f83ac9"} ] Status Codes: @@ -930,7 +930,7 @@ Create a new image from a container's changes HTTP/1.1 201 OK Content-Type: application/vnd.docker.raw-stream - {"Id":"596069db4bf5"} + {"Id": "596069db4bf5"} Query Parameters: diff --git a/docs/sources/reference/api/docker_remote_api_v1.3.md b/docs/sources/reference/api/docker_remote_api_v1.3.md index 7ae7462bf..3a0ea7ba1 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.3.md +++ b/docs/sources/reference/api/docker_remote_api_v1.3.md @@ -266,16 +266,16 @@ Inspect changes on container `id`'s filesystem [ { - "Path":"/dev", - "Kind":0 + "Path": "/dev", + "Kind": 0 }, { - "Path":"/dev/kmsg", - "Kind":1 + "Path": "/dev/kmsg", + "Kind": 1 }, { - "Path":"/test", - "Kind":1 + "Path": "/test", + "Kind": 1 } ] @@ -460,7 +460,7 @@ Block until container `id` stops, then returns the exit code HTTP/1.1 200 OK Content-Type: application/json - {"StatusCode":0} + {"StatusCode": 0} Status Codes: @@ -697,14 +697,14 @@ Return the history of the image `name` [ { - "Id":"b750fe79269d", - "Created":1364102658, - "CreatedBy":"/bin/bash" + "Id": "b750fe79269d", + "Created": 1364102658, + "CreatedBy": "/bin/bash" }, { - "Id":"27cf78414709", - "Created":1364068391, - "CreatedBy":"" + "Id": "27cf78414709", + "Created": 1364068391, + "CreatedBy": "" } ] @@ -785,9 +785,9 @@ Remove the image `name` from the filesystem Content-type: application/json [ - {"Untagged":"3e2f21a89f"}, - {"Deleted":"3e2f21a89f"}, - {"Deleted":"53b4f83ac9"} + {"Untagged": "3e2f21a89f"}, + {"Deleted": "3e2f21a89f"}, + {"Deleted": "53b4f83ac9"} ] Status Codes: @@ -978,7 +978,7 @@ Create a new image from a container's changes HTTP/1.1 201 OK Content-Type: application/vnd.docker.raw-stream - {"Id":"596069db4bf5"} + {"Id": "596069db4bf5"} Query Parameters: diff --git a/docs/sources/reference/api/docker_remote_api_v1.4.md b/docs/sources/reference/api/docker_remote_api_v1.4.md index 5c0a015cc..ac18cd481 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.4.md +++ b/docs/sources/reference/api/docker_remote_api_v1.4.md @@ -189,8 +189,7 @@ Return low-level information on the container `id` "Image": "ubuntu", "Volumes": {}, "VolumesFrom": "", - "WorkingDir":"" - + "WorkingDir": "" }, "State": { "Running": false, @@ -235,7 +234,7 @@ List processes running inside the container `id` Content-Type: application/json { - "Titles":[ + "Titles": [ "USER", "PID", "%CPU", @@ -248,7 +247,7 @@ List processes running inside the container `id` "TIME", "COMMAND" ], - "Processes":[ + "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"] ] @@ -281,16 +280,16 @@ Inspect changes on container `id`'s filesystem [ { - "Path":"/dev", - "Kind":0 + "Path": "/dev", + "Kind": 0 }, { - "Path":"/dev/kmsg", - "Kind":1 + "Path": "/dev/kmsg", + "Kind": 1 }, { - "Path":"/test", - "Kind":1 + "Path": "/test", + "Kind": 1 } ] @@ -476,7 +475,7 @@ Block until container `id` stops, then returns the exit code HTTP/1.1 200 OK Content-Type: application/json - {"StatusCode":0} + {"StatusCode": 0} Status Codes: @@ -522,7 +521,7 @@ Copy files or folders of container `id` Content-Type: application/json { - "Resource":"test.txt" + "Resource": "test.txt" } **Example response**: @@ -743,14 +742,14 @@ Return the history of the image `name` [ { - "Id":"b750fe79269d", - "Created":1364102658, - "CreatedBy":"/bin/bash" + "Id": "b750fe79269d", + "Created": 1364102658, + "CreatedBy": "/bin/bash" }, { - "Id":"27cf78414709", - "Created":1364068391, - "CreatedBy":"" + "Id": "27cf78414709", + "Created": 1364068391, + "CreatedBy": "" } ] @@ -828,9 +827,9 @@ Remove the image `name` from the filesystem Content-type: application/json [ - {"Untagged":"3e2f21a89f"}, - {"Deleted":"3e2f21a89f"}, - {"Deleted":"53b4f83ac9"} + {"Untagged": "3e2f21a89f"}, + {"Deleted": "3e2f21a89f"}, + {"Deleted": "53b4f83ac9"} ] Status Codes: @@ -926,10 +925,10 @@ Get the default username and email Content-Type: application/json { - "username":"hannibal", - "password:"xxxx", - "email":"hannibal@a-team.com", - "serveraddress":"https://index.docker.io/v1/" + "username":" hannibal", + "password: "xxxx", + "email": "hannibal@a-team.com", + "serveraddress": "https://index.docker.io/v1/" } **Example response**: @@ -1022,7 +1021,7 @@ Create a new image from a container's changes HTTP/1.1 201 OK Content-Type: application/vnd.docker.raw-stream - {"Id":"596069db4bf5"} + {"Id": "596069db4bf5"} Query Parameters: diff --git a/docs/sources/reference/api/docker_remote_api_v1.5.md b/docs/sources/reference/api/docker_remote_api_v1.5.md index 56245c303..8e0ad9f49 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.5.md +++ b/docs/sources/reference/api/docker_remote_api_v1.5.md @@ -473,7 +473,7 @@ Block until container `id` stops, then returns the exit code HTTP/1.1 200 OK Content-Type: application/json - {"StatusCode":0} + {"StatusCode": 0} Status Codes: @@ -1032,7 +1032,7 @@ Create a new image from a container's changes HTTP/1.1 201 OK Content-Type: application/vnd.docker.raw-stream - {"Id":"596069db4bf5"} + {"Id": "596069db4bf5"} Query Parameters: diff --git a/docs/sources/reference/api/docker_remote_api_v1.6.md b/docs/sources/reference/api/docker_remote_api_v1.6.md index d92983a54..f55c114b0 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.6.md +++ b/docs/sources/reference/api/docker_remote_api_v1.6.md @@ -39,9 +39,9 @@ List containers "Command": "echo 1", "Created": 1367854155, "Status": "Exit 0", - "Ports":[{"PrivatePort": 2222, "PublicPort": 3333, "Type": "tcp"}], - "SizeRw":12288, - "SizeRootFs":0 + "Ports": [{"PrivatePort": 2222, "PublicPort": 3333, "Type": "tcp"}], + "SizeRw": 12288, + "SizeRootFs": 0 }, { "Id": "9cd87474be90", @@ -49,9 +49,9 @@ List containers "Command": "echo 222222", "Created": 1367854155, "Status": "Exit 0", - "Ports":[], - "SizeRw":12288, - "SizeRootFs":0 + "Ports": [], + "SizeRw": 12288, + "SizeRootFs": 0 }, { "Id": "3176a2479c92", @@ -69,9 +69,9 @@ List containers "Command": "echo 444444444444444444444444444444444", "Created": 1367854152, "Status": "Exit 0", - "Ports":[], - "SizeRw":12288, - "SizeRootFs":0 + "Ports": [], + "SizeRw": 12288, + "SizeRootFs": 0 } ] @@ -237,8 +237,7 @@ Return low-level information on the container `id` "Image": "base", "Volumes": {}, "VolumesFrom": "", - "WorkingDir":"" - + "WorkingDir": "" }, "State": { "Running": false, @@ -282,7 +281,7 @@ List processes running inside the container `id` Content-Type: application/json { - "Titles":[ + "Titles": [ "USER", "PID", "%CPU", @@ -295,7 +294,7 @@ List processes running inside the container `id` "TIME", "COMMAND" ], - "Processes":[ + "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"] ] @@ -328,16 +327,16 @@ Inspect changes on container `id`'s filesystem [ { - "Path":"/dev", - "Kind":0 + "Path": "/dev", + "Kind": 0 }, { - "Path":"/dev/kmsg", - "Kind":1 + "Path": "/dev/kmsg", + "Kind": 1 }, { - "Path":"/test", - "Kind":1 + "Path": "/test", + "Kind": 1 } ] @@ -580,7 +579,7 @@ Block until container `id` stops, then returns the exit code HTTP/1.1 200 OK Content-Type: application/json - {"StatusCode":0} + {"StatusCode": 0} Status Codes: @@ -626,7 +625,7 @@ Copy files or folders of container `id` Content-Type: application/json { - "Resource":"test.txt" + "Resource": "test.txt" } **Example response**: @@ -850,14 +849,14 @@ Return the history of the image `name` [ { - "Id":"b750fe79269d", - "Created":1364102658, - "CreatedBy":"/bin/bash" + "Id": "b750fe79269d", + "Created": 1364102658, + "CreatedBy": "/bin/bash" }, { - "Id":"27cf78414709", - "Created":1364068391, - "CreatedBy":"" + "Id": "27cf78414709", + "Created": 1364068391, + "CreatedBy": "" } ] @@ -937,9 +936,9 @@ Remove the image `name` from the filesystem Content-type: application/json [ - {"Untagged":"3e2f21a89f"}, - {"Deleted":"3e2f21a89f"}, - {"Deleted":"53b4f83ac9"} + {"Untagged": "3e2f21a89f"}, + {"Deleted": "3e2f21a89f"}, + {"Deleted": "53b4f83ac9"} ] Status Codes: @@ -1035,10 +1034,10 @@ Get the default username and email Content-Type: application/json { - "username":"hannibal", - "password:"xxxx", - "email":"hannibal@a-team.com", - "serveraddress":"https://index.docker.io/v1/" + "username":" hannibal", + "password: "xxxx", + "email": "hannibal@a-team.com", + "serveraddress": "https://index.docker.io/v1/" } **Example response**: @@ -1130,7 +1129,7 @@ Create a new image from a container's changes HTTP/1.1 201 OK Content-Type: application/vnd.docker.raw-stream - {"Id":"596069db4bf5"} + {"Id": "596069db4bf5"} Query Parameters: @@ -1171,10 +1170,10 @@ and Docker images will report: 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} + {"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 Parameters: diff --git a/docs/sources/reference/api/docker_remote_api_v1.7.md b/docs/sources/reference/api/docker_remote_api_v1.7.md index 7660d744f..69562dbbe 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.7.md +++ b/docs/sources/reference/api/docker_remote_api_v1.7.md @@ -39,9 +39,9 @@ List containers "Command": "echo 1", "Created": 1367854155, "Status": "Exit 0", - "Ports":[{"PrivatePort": 2222, "PublicPort": 3333, "Type": "tcp"}], - "SizeRw":12288, - "SizeRootFs":0 + "Ports": [{"PrivatePort": 2222, "PublicPort": 3333, "Type": "tcp"}], + "SizeRw": 12288, + "SizeRootFs": 0 }, { "Id": "9cd87474be90", @@ -49,9 +49,9 @@ List containers "Command": "echo 222222", "Created": 1367854155, "Status": "Exit 0", - "Ports":[], - "SizeRw":12288, - "SizeRootFs":0 + "Ports": [], + "SizeRw": 12288, + "SizeRootFs": 0 }, { "Id": "3176a2479c92", @@ -69,9 +69,9 @@ List containers "Command": "echo 444444444444444444444444444444444", "Created": 1367854152, "Status": "Exit 0", - "Ports":[], - "SizeRw":12288, - "SizeRootFs":0 + "Ports": [], + "SizeRw": 12288, + "SizeRootFs": 0 } ] @@ -191,8 +191,7 @@ Return low-level information on the container `id` "Image": "base", "Volumes": {}, "VolumesFrom": "", - "WorkingDir":"" - + "WorkingDir": "" }, "State": { "Running": false, @@ -236,7 +235,7 @@ List processes running inside the container `id` Content-Type: application/json { - "Titles":[ + "Titles": [ "USER", "PID", "%CPU", @@ -249,7 +248,7 @@ List processes running inside the container `id` "TIME", "COMMAND" ], - "Processes":[ + "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"] ] @@ -282,16 +281,16 @@ Inspect changes on container `id`'s filesystem [ { - "Path":"/dev", - "Kind":0 + "Path": "/dev", + "Kind": 0 }, { - "Path":"/dev/kmsg", - "Kind":1 + "Path": "/dev/kmsg", + "Kind": 1 }, { - "Path":"/test", - "Kind":1 + "Path": "/test", + "Kind": 1 } ] @@ -525,7 +524,7 @@ Block until container `id` stops, then returns the exit code HTTP/1.1 200 OK Content-Type: application/json - {"StatusCode":0} + {"StatusCode": 0} Status Codes: @@ -571,7 +570,7 @@ Copy files or folders of container `id` Content-Type: application/json { - "Resource":"test.txt" + "Resource": "test.txt" } **Example response**: @@ -765,14 +764,14 @@ Return the history of the image `name` [ { - "Id":"b750fe79269d", - "Created":1364102658, - "CreatedBy":"/bin/bash" + "Id": "b750fe79269d", + "Created": 1364102658, + "CreatedBy": "/bin/bash" }, { - "Id":"27cf78414709", - "Created":1364068391, - "CreatedBy":"" + "Id": "27cf78414709", + "Created": 1364068391, + "CreatedBy": "" } ] @@ -859,9 +858,9 @@ Remove the image `name` from the filesystem Content-type: application/json [ - {"Untagged":"3e2f21a89f"}, - {"Deleted":"3e2f21a89f"}, - {"Deleted":"53b4f83ac9"} + {"Untagged": "3e2f21a89f"}, + {"Deleted": "3e2f21a89f"}, + {"Deleted": "53b4f83ac9"} ] Status Codes: @@ -984,10 +983,10 @@ Get the default username and email Content-Type: application/json { - "username":"hannibal", - "password:"xxxx", - "email":"hannibal@a-team.com", - "serveraddress":"https://index.docker.io/v1/" + "username":" hannibal", + "password: "xxxx", + "email": "hannibal@a-team.com", + "serveraddress": "https://index.docker.io/v1/" } **Example response**: @@ -1073,7 +1072,7 @@ Create a new image from a container's changes HTTP/1.1 201 OK Content-Type: application/vnd.docker.raw-stream - {"Id":"596069db4bf5"} + {"Id": "596069db4bf5"} Query Parameters: @@ -1116,10 +1115,10 @@ and Docker images will report: 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} + {"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 Parameters: diff --git a/docs/sources/reference/api/docker_remote_api_v1.8.md b/docs/sources/reference/api/docker_remote_api_v1.8.md index e0bdaa661..2176a334a 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.8.md +++ b/docs/sources/reference/api/docker_remote_api_v1.8.md @@ -39,9 +39,9 @@ List containers "Command": "echo 1", "Created": 1367854155, "Status": "Exit 0", - "Ports":[{"PrivatePort": 2222, "PublicPort": 3333, "Type": "tcp"}], - "SizeRw":12288, - "SizeRootFs":0 + "Ports": [{"PrivatePort": 2222, "PublicPort": 3333, "Type": "tcp"}], + "SizeRw": 12288, + "SizeRootFs": 0 }, { "Id": "9cd87474be90", @@ -49,9 +49,9 @@ List containers "Command": "echo 222222", "Created": 1367854155, "Status": "Exit 0", - "Ports":[], - "SizeRw":12288, - "SizeRootFs":0 + "Ports": [], + "SizeRw": 12288, + "SizeRootFs": 0 }, { "Id": "3176a2479c92", @@ -69,9 +69,9 @@ List containers "Command": "echo 444444444444444444444444444444444", "Created": 1367854152, "Status": "Exit 0", - "Ports":[], - "SizeRw":12288, - "SizeRootFs":0 + "Ports": [], + "SizeRw": 12288, + "SizeRootFs": 0 } ] @@ -215,8 +215,7 @@ Return low-level information on the container `id` "Image": "base", "Volumes": {}, "VolumesFrom": "", - "WorkingDir":"" - + "WorkingDir": "" }, "State": { "Running": false, @@ -276,7 +275,7 @@ List processes running inside the container `id` Content-Type: application/json { - "Titles":[ + "Titles": [ "USER", "PID", "%CPU", @@ -289,7 +288,7 @@ List processes running inside the container `id` "TIME", "COMMAND" ], - "Processes":[ + "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"] ] @@ -322,16 +321,16 @@ Inspect changes on container `id`'s filesystem [ { - "Path":"/dev", - "Kind":0 + "Path": "/dev", + "Kind": 0 }, { - "Path":"/dev/kmsg", - "Kind":1 + "Path": "/dev/kmsg", + "Kind": 1 }, { - "Path":"/test", - "Kind":1 + "Path": "/test", + "Kind": 1 } ] @@ -573,7 +572,7 @@ Block until container `id` stops, then returns the exit code HTTP/1.1 200 OK Content-Type: application/json - {"StatusCode":0} + {"StatusCode": 0} Status Codes: @@ -619,7 +618,7 @@ Copy files or folders of container `id` Content-Type: application/json { - "Resource":"test.txt" + "Resource": "test.txt" } **Example response**: @@ -690,9 +689,9 @@ Create an image, either by pull it from the registry or by importing i 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..."} + {"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 @@ -813,14 +812,14 @@ Return the history of the image `name` [ { - "Id":"b750fe79269d", - "Created":1364102658, - "CreatedBy":"/bin/bash" + "Id": "b750fe79269d", + "Created": 1364102658, + "CreatedBy": "/bin/bash" }, { - "Id":"27cf78414709", - "Created":1364068391, - "CreatedBy":"" + "Id": "27cf78414709", + "Created": 1364068391, + "CreatedBy": "" } ] @@ -845,9 +844,9 @@ Push the image `name` on the registry HTTP/1.1 200 OK Content-Type: application/json - {"status":"Pushing..."} - {"status":"Pushing", "progress":"1/? (n/a)", "progressDetail":{"current":1}}} - {"error":"Invalid..."} + {"status": "Pushing..."} + {"status": "Pushing", "progress": "1/? (n/a)", "progressDetail": {"current": 1}}} + {"error": "Invalid..."} ... Request Headers: @@ -907,9 +906,9 @@ Remove the image `name` from the filesystem Content-type: application/json [ - {"Untagged":"3e2f21a89f"}, - {"Deleted":"3e2f21a89f"}, - {"Deleted":"53b4f83ac9"} + {"Untagged": "3e2f21a89f"}, + {"Deleted": "3e2f21a89f"}, + {"Deleted": "53b4f83ac9"} ] Status Codes: @@ -991,9 +990,9 @@ Build an image from Dockerfile via stdin HTTP/1.1 200 OK Content-Type: application/json - {"stream":"Step 1..."} - {"stream":"..."} - {"error":"Error...", "errorDetail":{"code": 123, "message": "Error..."}} + {"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. @@ -1035,10 +1034,10 @@ Get the default username and email Content-Type: application/json { - "username":"hannibal", - "password:"xxxx", - "email":"hannibal@a-team.com", - "serveraddress":"https://index.docker.io/v1/" + "username":" hannibal", + "password: "xxxx", + "email": "hannibal@a-team.com", + "serveraddress": "https://index.docker.io/v1/" } **Example response**: @@ -1124,7 +1123,7 @@ Create a new image from a container's changes HTTP/1.1 201 OK Content-Type: application/vnd.docker.raw-stream - {"Id":"596069db4bf5"} + {"Id": "596069db4bf5"} Query Parameters: @@ -1167,10 +1166,10 @@ and Docker images will report: 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} + {"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 Parameters: @@ -1245,7 +1244,7 @@ the root that contains a list of repository and tag names mapped to layer IDs. ``` {"hello-world": - {"latest":"565a9d68a73f6706862bfe8409a7f659776d4d60a8d096eb4a3cbce6999cc2a1"} + {"latest": "565a9d68a73f6706862bfe8409a7f659776d4d60a8d096eb4a3cbce6999cc2a1"} } ``` diff --git a/docs/sources/reference/api/docker_remote_api_v1.9.md b/docs/sources/reference/api/docker_remote_api_v1.9.md index 7ddefc4f2..61102083d 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.9.md +++ b/docs/sources/reference/api/docker_remote_api_v1.9.md @@ -39,9 +39,9 @@ List containers. "Command": "echo 1", "Created": 1367854155, "Status": "Exit 0", - "Ports":[{"PrivatePort": 2222, "PublicPort": 3333, "Type": "tcp"}], - "SizeRw":12288, - "SizeRootFs":0 + "Ports": [{"PrivatePort": 2222, "PublicPort": 3333, "Type": "tcp"}], + "SizeRw": 12288, + "SizeRootFs": 0 }, { "Id": "9cd87474be90", @@ -49,9 +49,9 @@ List containers. "Command": "echo 222222", "Created": 1367854155, "Status": "Exit 0", - "Ports":[], - "SizeRw":12288, - "SizeRootFs":0 + "Ports": [], + "SizeRw": 12288, + "SizeRootFs": 0 }, { "Id": "3176a2479c92", @@ -69,9 +69,9 @@ List containers. "Command": "echo 444444444444444444444444444444444", "Created": 1367854152, "Status": "Exit 0", - "Ports":[], - "SizeRw":12288, - "SizeRootFs":0 + "Ports": [], + "SizeRw": 12288, + "SizeRootFs": 0 } ] @@ -215,8 +215,7 @@ Return low-level information on the container `id` "Image": "base", "Volumes": {}, "VolumesFrom": "", - "WorkingDir":"" - + "WorkingDir": "" }, "State": { "Running": false, @@ -276,7 +275,7 @@ List processes running inside the container `id` Content-Type: application/json { - "Titles":[ + "Titles": [ "USER", "PID", "%CPU", @@ -289,7 +288,7 @@ List processes running inside the container `id` "TIME", "COMMAND" ], - "Processes":[ + "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"] ] @@ -322,16 +321,16 @@ Inspect changes on container `id`'s filesystem [ { - "Path":"/dev", - "Kind":0 + "Path": "/dev", + "Kind": 0 }, { - "Path":"/dev/kmsg", - "Kind":1 + "Path": "/dev/kmsg", + "Kind": 1 }, { - "Path":"/test", - "Kind":1 + "Path": "/test", + "Kind": 1 } ] @@ -577,7 +576,7 @@ Block until container `id` stops, then returns the exit code HTTP/1.1 200 OK Content-Type: application/json - {"StatusCode":0} + {"StatusCode": 0} Status Codes: @@ -623,7 +622,7 @@ Copy files or folders of container `id` Content-Type: application/json { - "Resource":"test.txt" + "Resource": "test.txt" } **Example response**: @@ -694,9 +693,9 @@ Create an image, either by pull it from the registry or by importing i 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..."} + {"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 @@ -817,14 +816,14 @@ Return the history of the image `name` [ { - "Id":"b750fe79269d", - "Created":1364102658, - "CreatedBy":"/bin/bash" + "Id": "b750fe79269d", + "Created": 1364102658, + "CreatedBy": "/bin/bash" }, { - "Id":"27cf78414709", - "Created":1364068391, - "CreatedBy":"" + "Id": "27cf78414709", + "Created": 1364068391, + "CreatedBy": "" } ] @@ -849,9 +848,9 @@ Push the image `name` on the registry HTTP/1.1 200 OK Content-Type: application/json - {"status":"Pushing..."} - {"status":"Pushing", "progress":"1/? (n/a)", "progressDetail":{"current":1}}} - {"error":"Invalid..."} + {"status": "Pushing..."} + {"status": "Pushing", "progress": "1/? (n/a)", "progressDetail": {"current": 1}}} + {"error": "Invalid..."} ... Request Headers: @@ -910,9 +909,9 @@ Status Codes: Content-type: application/json [ - {"Untagged":"3e2f21a89f"}, - {"Deleted":"3e2f21a89f"}, - {"Deleted":"53b4f83ac9"} + {"Untagged": "3e2f21a89f"}, + {"Deleted": "3e2f21a89f"}, + {"Deleted": "53b4f83ac9"} ] Status Codes: @@ -994,9 +993,9 @@ Build an image from Dockerfile using a POST body. HTTP/1.1 200 OK Content-Type: application/json - {"stream":"Step 1..."} - {"stream":"..."} - {"error":"Error...", "errorDetail":{"code": 123, "message": "Error..."}} + {"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. @@ -1036,10 +1035,10 @@ Get the default username and email Content-Type: application/json { - "username":"hannibal", - "password:"xxxx", - "email":"hannibal@a-team.com", - "serveraddress":"https://index.docker.io/v1/" + "username":" hannibal", + "password: "xxxx", + "email": "hannibal@a-team.com", + "serveraddress": "https://index.docker.io/v1/" } **Example response**: @@ -1152,7 +1151,7 @@ Create a new image from a container's changes HTTP/1.1 201 Created Content-Type: application/vnd.docker.raw-stream - {"Id":"596069db4bf5"} + {"Id": "596069db4bf5"} Json Parameters: @@ -1197,10 +1196,10 @@ and Docker images will report: 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} + {"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 Parameters: @@ -1275,7 +1274,7 @@ the root that contains a list of repository and tag names mapped to layer IDs. ``` {"hello-world": - {"latest":"565a9d68a73f6706862bfe8409a7f659776d4d60a8d096eb4a3cbce6999cc2a1"} + {"latest": "565a9d68a73f6706862bfe8409a7f659776d4d60a8d096eb4a3cbce6999cc2a1"} } ``` From 04ee071692c320ac9dd8e66475c02c6ec3166251 Mon Sep 17 00:00:00 2001 From: Doug Davis Date: Mon, 5 Jan 2015 10:28:36 -0800 Subject: [PATCH 159/513] Modify MAINTAINERS per erikh's suggestion Signed-off-by: Doug Davis --- builder/MAINTAINERS | 1 + 1 file changed, 1 insertion(+) diff --git a/builder/MAINTAINERS b/builder/MAINTAINERS index 4d158aa20..e170c235a 100644 --- a/builder/MAINTAINERS +++ b/builder/MAINTAINERS @@ -1,2 +1,3 @@ Tibor Vass (@tiborvass) Erik Hollensbe (@erikh) +Doug Davis (@duglin) From 3011aa4e9984b0631b67f640a191677e2f3d0a8f Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Mon, 5 Jan 2015 10:34:28 -0800 Subject: [PATCH 160/513] Remove error return from check graph driver func Signed-off-by: Michael Crosby --- daemon/graphdriver/driver.go | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/daemon/graphdriver/driver.go b/daemon/graphdriver/driver.go index 7a0c0d1c5..1c0601278 100644 --- a/daemon/graphdriver/driver.go +++ b/daemon/graphdriver/driver.go @@ -145,19 +145,16 @@ func New(root string, options []string) (driver Driver, err error) { return nil, fmt.Errorf("No supported storage backend found") } -func checkPriorDriver(name string, root string) error { - - var priorDrivers []string - +func checkPriorDriver(name, root string) { + priorDrivers := []string{} for prior := range drivers { - if _, err := os.Stat(path.Join(root, prior)); err == nil && prior != name { - priorDrivers = append(priorDrivers, prior) + if prior != name { + if _, err := os.Stat(path.Join(root, prior)); err == nil { + priorDrivers = append(priorDrivers, prior) + } } } - if len(priorDrivers) > 0 { log.Warnf("graphdriver %s selected. Warning: your graphdriver directory %s already contains data managed by other graphdrivers: %s", name, root, strings.Join(priorDrivers, ",")) } - - return nil } From 99a5da5ada7facc79336b3e0cc62f36c5bfa0620 Mon Sep 17 00:00:00 2001 From: Fred Lifton Date: Mon, 29 Dec 2014 12:42:20 -0800 Subject: [PATCH 161/513] Minor copy edits and updates to README. Added CTA for keeping the projects list current. Docker-DCO-1.1-Signed-off-by: Fred Lifton (github: fredlf) --- README.md | 44 +++++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 9f932e104..ef93a0832 100644 --- a/README.md +++ b/README.md @@ -5,14 +5,14 @@ Docker is an open source project to pack, ship and run any application as a lightweight container Docker containers are both *hardware-agnostic* and *platform-agnostic*. -This means that they can run anywhere, from your laptop to the largest +This means they can run anywhere, from your laptop to the largest EC2 compute instance and everything in between - and they don't require -that you use a particular language, framework or packaging system. That +you to use a particular language, framework or packaging system. That makes them great building blocks for deploying and scaling web apps, -databases and backend services without depending on a particular stack +databases, and backend services without depending on a particular stack or provider. -Docker is an open-source implementation of the deployment engine which +Docker began as an open-source implementation of the deployment engine which powers [dotCloud](http://dotcloud.com), a popular Platform-as-a-Service. It benefits directly from the experience accumulated over several years of large-scale operation and support of hundreds of thousands of @@ -22,7 +22,7 @@ applications and databases. ## Security Disclosure -Security is very important to us. If you have any issue regarding security, +Security is very important to us. If you have any issue regarding security, please disclose the information responsibly by sending an email to security@docker.com and not by creating a github issue. @@ -59,24 +59,24 @@ now support the primitives necessary for containerization, including Linux with [openvz](http://openvz.org), [vserver](http://linux-vserver.org) and more recently [lxc](http://lxc.sourceforge.net), Solaris with -[zones](http://docs.oracle.com/cd/E26502_01/html/E29024/preface-1.html#scrolltoc) +[zones](http://docs.oracle.com/cd/E26502_01/html/E29024/preface-1.html#scrolltoc), and FreeBSD with [Jails](http://www.freebsd.org/doc/handbook/jails.html). Docker builds on top of these low-level primitives to offer developers a -portable format and runtime environment that solves all 4 problems. +portable format and runtime environment that solves all four problems. Docker containers are small (and their transfer can be optimized with layers), they have basically zero memory and cpu overhead, they are -completely portable and are designed from the ground up with an +completely portable, and are designed from the ground up with an application-centric design. -The best part: because Docker operates at the OS level, it can still be +Perhaps best of all, because Docker operates at the OS level, it can still be run inside a VM! ## Plays well with others -Docker does not require that you buy into a particular programming -language, framework, packaging system or configuration language. +Docker does not require you to buy into a particular programming +language, framework, packaging system, or configuration language. Is your application a Unix process? Does it use files, tcp connections, environment variables, standard Unix streams and command-line arguments @@ -112,9 +112,9 @@ This is usually difficult for several reasons: of them handle it differently. -Docker solves dependency hell by giving the developer a simple way to -express *all* their application's dependencies in one place, and -streamline the process of assembling them. If this makes you think of +Docker solves the problem of dependency hell by giving the developer a simple +way to express *all* their application's dependencies in one place, while +streamlining the process of assembling them. If this makes you think of [XKCD 927](http://xkcd.com/927/), don't worry. Docker doesn't *replace* your favorite packaging systems. It simply orchestrates their use in a simple and repeatable way. How does it do that? With @@ -180,12 +180,12 @@ Contributing to Docker [![GoDoc](https://godoc.org/github.com/docker/docker?status.png)](https://godoc.org/github.com/docker/docker) [![Jenkins Build Status](https://jenkins.dockerproject.com/job/Docker%20Master/badge/icon)](https://jenkins.dockerproject.com/job/Docker%20Master/) -Want to hack on Docker? Awesome! There are instructions to get you -started [here](CONTRIBUTING.md). If you'd like to contribute to the +Want to hack on Docker? Awesome! We have [instructions to help you get +started](CONTRIBUTING.md). If you'd like to contribute to the documentation, please take a look at this [README.md](https://github.com/docker/docker/blob/master/docs/README.md). These instructions are probably not perfect, please let us know if anything -feels wrong or incomplete. +feels wrong or incomplete. Better yet, submit a PR and improve them yourself. Want to run Docker from a master build? You can download master builds at [master.dockerproject.com](https://master.dockerproject.com). @@ -194,7 +194,7 @@ They are updated with each commit merged into the master branch. ### Legal *Brought to you courtesy of our legal counsel. For more context, -please see the Notice document.* +please see the "NOTICE" document in this repo.* Use and transfer of Docker may be subject to certain restrictions by the United States and other governments. @@ -212,10 +212,12 @@ license text. Other Docker Related Projects ============================= - There are a number of projects under development that are based on Docker's -core technology. These projects expand the tooling built around the -Docker platform to broaden its application and utility. +core technology. These projects seek to expand the tooling built around the +Docker platform to broaden its application and utility. + +If you know of another project underway that should be listed here, please help +us keep this list up-to-date by submitting a PR. * [Docker Registry](https://github.com/docker/docker-registry): Registry server for Docker (hosting/delivering of repositories and images) From 5ad41ce01a3489d2a09d86cd8a8bdf847bf70cf9 Mon Sep 17 00:00:00 2001 From: Fred Lifton Date: Fri, 2 Jan 2015 11:00:27 -0800 Subject: [PATCH 162/513] Revises link to point to Fig Docker-DCO-1.1-Signed-off-by: Fred Lifton (github: fredlf) Signed-off-by: Michael Crosby --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ef93a0832..58f121a11 100644 --- a/README.md +++ b/README.md @@ -213,7 +213,7 @@ license text. Other Docker Related Projects ============================= There are a number of projects under development that are based on Docker's -core technology. These projects seek to expand the tooling built around the +core technology. These projects expand the tooling built around the Docker platform to broaden its application and utility. If you know of another project underway that should be listed here, please help From 6c126d443b3ee3bbb6d0a437a1b5c51cbf9e47f2 Mon Sep 17 00:00:00 2001 From: Matthew Riley Date: Tue, 4 Nov 2014 15:02:06 -0800 Subject: [PATCH 163/513] Allow hyphens in namespaces. Signed-off-by: Matthew Riley --- registry/registry.go | 15 ++++++++-- registry/registry_test.go | 59 +++++++++++++++++++++++++++++---------- 2 files changed, 56 insertions(+), 18 deletions(-) diff --git a/registry/registry.go b/registry/registry.go index d503a63d6..a12291897 100644 --- a/registry/registry.go +++ b/registry/registry.go @@ -23,7 +23,7 @@ var ( ErrInvalidRepositoryName = errors.New("Invalid repository name (ex: \"registry.domain.tld/myrepos\")") ErrDoesNotExist = errors.New("Image does not exist") errLoginRequired = errors.New("Authentication is required.") - validNamespace = regexp.MustCompile(`^([a-z0-9_]{4,30})$`) + validNamespaceChars = regexp.MustCompile(`^([a-z0-9-_]*)$`) validRepo = regexp.MustCompile(`^([a-z0-9-_.]+)$`) ) @@ -178,8 +178,17 @@ func validateRepositoryName(repositoryName string) error { namespace = nameParts[0] name = nameParts[1] } - if !validNamespace.MatchString(namespace) { - return fmt.Errorf("Invalid namespace name (%s), only [a-z0-9_] are allowed, size between 4 and 30", namespace) + if !validNamespaceChars.MatchString(namespace) { + return fmt.Errorf("Invalid namespace name (%s). Only [a-z0-9-_] are allowed.", namespace) + } + if len(namespace) < 4 || len(namespace) > 30 { + return fmt.Errorf("Invalid namespace name (%s). Cannot be fewer than 4 or more than 30 characters.", namespace) + } + if strings.HasPrefix(namespace, "-") || strings.HasSuffix(namespace, "-") { + return fmt.Errorf("Invalid namespace name (%s). Cannot begin or end with a hyphen.", namespace) + } + if strings.Contains(namespace, "--") { + return fmt.Errorf("Invalid namespace name (%s). Cannot contain consecutive hyphens.", namespace) } if !validRepo.MatchString(name) { return fmt.Errorf("Invalid repository name (%s), only [a-z0-9-_.] are allowed", name) diff --git a/registry/registry_test.go b/registry/registry_test.go index 52b8b32c5..c1bb97d65 100644 --- a/registry/registry_test.go +++ b/registry/registry_test.go @@ -233,24 +233,53 @@ func TestSearchRepositories(t *testing.T) { } func TestValidRepositoryName(t *testing.T) { - if err := validateRepositoryName("docker/docker"); err != nil { - t.Fatal(err) + validRepositoryNames := []string{ + // Sanity check. + "docker/docker", + + // Allow 64-character non-hexadecimal names (hexadecimal names are forbidden). + "thisisthesongthatneverendsitgoesonandonandonthisisthesongthatnev", + + // Allow embedded hyphens. + "docker-rules/docker", + + // Allow underscores everywhere (as opposed to hyphens). + "____/____", } - // Support 64-byte non-hexadecimal names (hexadecimal names are forbidden) - if err := validateRepositoryName("thisisthesongthatneverendsitgoesonandonandonthisisthesongthatnev"); err != nil { - t.Fatal(err) + for _, repositoryName := range validRepositoryNames { + if err := validateRepositoryName(repositoryName); err != nil { + t.Errorf("Repository name should be valid: %v. Error: %v", repositoryName, err) + } } - if err := validateRepositoryName("docker/Docker"); err == nil { - t.Log("Repository name should be invalid") - t.Fail() + + invalidRepositoryNames := []string{ + // Disallow capital letters. + "docker/Docker", + + // Only allow one slash. + "docker///docker", + + // Disallow 64-character hexadecimal. + "1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a", + + // Disallow leading and trailing hyphens in namespace. + "-docker/docker", + "docker-/docker", + "-docker-/docker", + + // Disallow consecutive hyphens. + "dock--er/docker", + + // Namespace too short. + "doc/docker", + + // No repository. + "docker/", } - if err := validateRepositoryName("docker///docker"); err == nil { - t.Log("Repository name should be invalid") - t.Fail() - } - if err := validateRepositoryName("1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a"); err == nil { - t.Log("Repository name should be invalid, 64-byte hexadecimal names forbidden") - t.Fail() + for _, repositoryName := range invalidRepositoryNames { + if err := validateRepositoryName(repositoryName); err == nil { + t.Errorf("Repository name should be invalid: %v", repositoryName) + } } } From 8d1485bd3f1c1c8c94b8673ba818ed374fe10022 Mon Sep 17 00:00:00 2001 From: Evan Carmi Date: Mon, 5 Jan 2015 11:02:17 -0800 Subject: [PATCH 164/513] Update syntax/vim README with Vundle instructions Signed-off-by: Evan Carmi --- contrib/syntax/vim/README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/contrib/syntax/vim/README.md b/contrib/syntax/vim/README.md index b78246617..5aa9bd825 100644 --- a/contrib/syntax/vim/README.md +++ b/contrib/syntax/vim/README.md @@ -5,8 +5,11 @@ Syntax highlighting for Dockerfiles Installation ------------ +With [pathogen](https://github.com/tpope/vim-pathogen), the usual way... -Via pathogen, the usual way... +With [Vundle](https://github.com/gmarik/Vundle.vim) + + Plugin 'docker/docker' , {'rtp': '/contrib/syntax/vim/'} Features -------- From 367e66e9721c33a64ab091bd5430ab4a9af12083 Mon Sep 17 00:00:00 2001 From: Jessica Frazelle Date: Mon, 5 Jan 2015 11:13:37 -0800 Subject: [PATCH 165/513] Change url of robots.txt test to dockerproject.com Docker-DCO-1.1-Signed-off-by: Jessica Frazelle (github: jfrazelle) --- integration-cli/docker_cli_build_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration-cli/docker_cli_build_test.go b/integration-cli/docker_cli_build_test.go index e440bc770..bb53b68ac 100644 --- a/integration-cli/docker_cli_build_test.go +++ b/integration-cli/docker_cli_build_test.go @@ -737,7 +737,7 @@ RUN mkdir /exists RUN touch /exists/exists_file RUN chown -R dockerio.dockerio /exists COPY test_file1 test_file2 /exists/ -ADD test_file3 test_file4 https://docker.com/robots.txt /exists/ +ADD test_file3 test_file4 https://dockerproject.com/robots.txt /exists/ RUN [ $(ls -l / | grep exists | awk '{print $3":"$4}') = 'dockerio:dockerio' ] RUN [ $(ls -l /exists/test_file1 | awk '{print $3":"$4}') = 'root:root' ] RUN [ $(ls -l /exists/test_file2 | awk '{print $3":"$4}') = 'root:root' ] From 05c2d2db9a95266217a639e2109be6fc6482a716 Mon Sep 17 00:00:00 2001 From: Tianon Gravi Date: Fri, 2 Jan 2015 22:40:43 -0700 Subject: [PATCH 166/513] Adjust builder to validate that JSON in Dockerfiles are arrays of strings and nothing else to match how we describe them to people (and what all our existing tests already assumed) This also adds more tests to help verify this, including unicode and nonprintable characters (hence the earlier commit switching to strconv.Quote). As a bonus, this fixes a subtle bug where [] was turned into [""] and then turned back into [] (and thus [""] was impossible to actually round-trip correctly in a Dockerfile). Signed-off-by: Andrew "Tianon" Page --- builder/parser/json_test.go | 55 ++++++++++++++++++++++++ builder/parser/line_parsers.go | 40 +++++++---------- builder/parser/parser.go | 5 +-- builder/parser/testfiles/json/Dockerfile | 8 ++++ builder/parser/testfiles/json/result | 8 ++++ 5 files changed, 88 insertions(+), 28 deletions(-) create mode 100644 builder/parser/json_test.go create mode 100644 builder/parser/testfiles/json/Dockerfile create mode 100644 builder/parser/testfiles/json/result diff --git a/builder/parser/json_test.go b/builder/parser/json_test.go new file mode 100644 index 000000000..a256f845d --- /dev/null +++ b/builder/parser/json_test.go @@ -0,0 +1,55 @@ +package parser + +import ( + "testing" +) + +var invalidJSONArraysOfStrings = []string{ + `["a",42,"b"]`, + `["a",123.456,"b"]`, + `["a",{},"b"]`, + `["a",{"c": "d"},"b"]`, + `["a",["c"],"b"]`, + `["a",true,"b"]`, + `["a",false,"b"]`, + `["a",null,"b"]`, +} + +var validJSONArraysOfStrings = map[string][]string{ + `[]`: {}, + `[""]`: {""}, + `["a"]`: {"a"}, + `["a","b"]`: {"a", "b"}, + `[ "a", "b" ]`: {"a", "b"}, + `[ "a", "b" ]`: {"a", "b"}, + ` [ "a", "b" ] `: {"a", "b"}, + `["abc 123", "♥", "☃", "\" \\ \/ \b \f \n \r \t \u0000"]`: {"abc 123", "♥", "☃", "\" \\ / \b \f \n \r \t \u0000"}, +} + +func TestJSONArraysOfStrings(t *testing.T) { + for json, expected := range validJSONArraysOfStrings { + if node, _, err := parseJSON(json); err != nil { + t.Fatalf("%q should be a valid JSON array of strings, but wasn't! (err: %q)", json, err) + } else { + i := 0 + for node != nil { + if i >= len(expected) { + t.Fatalf("expected result is shorter than parsed result (%d vs %d+) in %q", len(expected), i+1, json) + } + if node.Value != expected[i] { + t.Fatalf("expected %q (not %q) in %q at pos %d", expected[i], node.Value, json, i) + } + node = node.Next + i++ + } + if i != len(expected) { + t.Fatalf("expected result is longer than parsed result (%d vs %d) in %q", len(expected), i+1, json) + } + } + } + for _, json := range invalidJSONArraysOfStrings { + if _, _, err := parseJSON(json); err != errDockerfileNotStringArray { + t.Fatalf("%q should be an invalid JSON array of strings, but wasn't!", json) + } + } +} diff --git a/builder/parser/line_parsers.go b/builder/parser/line_parsers.go index abde85d29..8a94e1e5d 100644 --- a/builder/parser/line_parsers.go +++ b/builder/parser/line_parsers.go @@ -10,13 +10,12 @@ import ( "encoding/json" "errors" "fmt" - "strconv" "strings" "unicode" ) var ( - errDockerfileJSONNesting = errors.New("You may not nest arrays in Dockerfile statements.") + errDockerfileNotStringArray = errors.New("When using JSON array syntax, arrays must be comprised of strings only.") ) // ignore the current argument. This will still leave a command parsed, but @@ -209,34 +208,27 @@ func parseString(rest string) (*Node, map[string]bool, error) { // parseJSON converts JSON arrays to an AST. func parseJSON(rest string) (*Node, map[string]bool, error) { - var ( - myJson []interface{} - next = &Node{} - orignext = next - prevnode = next - ) - + var myJson []interface{} if err := json.Unmarshal([]byte(rest), &myJson); err != nil { return nil, nil, err } + var top, prev *Node for _, str := range myJson { - switch str.(type) { - case string: - case float64: - str = strconv.FormatFloat(str.(float64), 'G', -1, 64) - default: - return nil, nil, errDockerfileJSONNesting + if s, ok := str.(string); !ok { + return nil, nil, errDockerfileNotStringArray + } else { + node := &Node{Value: s} + if prev == nil { + top = node + } else { + prev.Next = node + } + prev = node } - next.Value = str.(string) - next.Next = &Node{} - prevnode = next - next = next.Next } - prevnode.Next = nil - - return orignext, map[string]bool{"json": true}, nil + return top, map[string]bool{"json": true}, nil } // parseMaybeJSON determines if the argument appears to be a JSON array. If @@ -250,7 +242,7 @@ func parseMaybeJSON(rest string) (*Node, map[string]bool, error) { if err == nil { return node, attrs, nil } - if err == errDockerfileJSONNesting { + if err == errDockerfileNotStringArray { return nil, nil, err } @@ -270,7 +262,7 @@ func parseMaybeJSONToList(rest string) (*Node, map[string]bool, error) { if err == nil { return node, attrs, nil } - if err == errDockerfileJSONNesting { + if err == errDockerfileNotStringArray { return nil, nil, err } diff --git a/builder/parser/parser.go b/builder/parser/parser.go index ad42a1586..4360623e6 100644 --- a/builder/parser/parser.go +++ b/builder/parser/parser.go @@ -85,10 +85,7 @@ func parseLine(line string) (string, *Node, error) { return "", nil, err } - if sexp.Value != "" || sexp.Next != nil || sexp.Children != nil { - node.Next = sexp - } - + node.Next = sexp node.Attributes = attrs node.Original = line diff --git a/builder/parser/testfiles/json/Dockerfile b/builder/parser/testfiles/json/Dockerfile new file mode 100644 index 000000000..a58691711 --- /dev/null +++ b/builder/parser/testfiles/json/Dockerfile @@ -0,0 +1,8 @@ +CMD [] +CMD [""] +CMD ["a"] +CMD ["a","b"] +CMD [ "a", "b" ] +CMD [ "a", "b" ] +CMD [ "a", "b" ] +CMD ["abc 123", "♥", "☃", "\" \\ \/ \b \f \n \r \t \u0000"] diff --git a/builder/parser/testfiles/json/result b/builder/parser/testfiles/json/result new file mode 100644 index 000000000..c6553e6e1 --- /dev/null +++ b/builder/parser/testfiles/json/result @@ -0,0 +1,8 @@ +(cmd) +(cmd "") +(cmd "a") +(cmd "a" "b") +(cmd "a" "b") +(cmd "a" "b") +(cmd "a" "b") +(cmd "abc 123" "♥" "☃" "\" \\ / \b \f \n \r \t \x00") From a1bfe3aa420d458cb84be1770202fe1b3f546ebe Mon Sep 17 00:00:00 2001 From: Jessica Frazelle Date: Mon, 5 Jan 2015 14:15:16 -0800 Subject: [PATCH 167/513] Add docker-py tests to drone. Docker-DCO-1.1-Signed-off-by: Jessica Frazelle (github: jfrazelle) --- .drone.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index 53e00e0fd..8da7133ee 100755 --- a/.drone.yml +++ b/.drone.yml @@ -11,4 +11,4 @@ script: - rm integration-cli/docker_cli_exec_test.go # Validate and test. - hack/make.sh validate-dco validate-gofmt - - hack/make.sh binary cross test-unit test-integration-cli test-integration + - hack/make.sh binary cross test-unit test-integration-cli test-integration test-docker-py From 21e44d7a21014f6f0d5e159221f9b9165874a2e1 Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Thu, 4 Dec 2014 16:12:29 -0500 Subject: [PATCH 168/513] Refactor daemon.attach() Also makes streamConfig Pipe methods not return error, since there was no error for them to be able to return anyway. Signed-off-by: Brian Goff --- daemon/attach.go | 171 ++++++++++++++-------------------- daemon/container.go | 17 ++-- integration/commands_test.go | 2 +- integration/container_test.go | 40 ++------ integration/runtime_test.go | 2 +- integration/utils_test.go | 15 +-- 6 files changed, 88 insertions(+), 159 deletions(-) diff --git a/daemon/attach.go b/daemon/attach.go index 599b27247..144c5c777 100644 --- a/daemon/attach.go +++ b/daemon/attach.go @@ -8,7 +8,6 @@ import ( log "github.com/Sirupsen/logrus" "github.com/docker/docker/engine" - "github.com/docker/docker/pkg/ioutils" "github.com/docker/docker/pkg/jsonlog" "github.com/docker/docker/pkg/promise" "github.com/docker/docker/utils" @@ -114,131 +113,97 @@ func (daemon *Daemon) ContainerAttach(job *engine.Job) engine.Status { func (daemon *Daemon) attach(streamConfig *StreamConfig, openStdin, stdinOnce, tty bool, stdin io.ReadCloser, stdout io.Writer, stderr io.Writer) chan error { var ( cStdout, cStderr io.ReadCloser + cStdin io.WriteCloser nJobs int - errors = make(chan error, 3) ) + if stdin != nil && openStdin { + cStdin = streamConfig.StdinPipe() + nJobs++ + } + + if stdout != nil { + cStdout = streamConfig.StdoutPipe() + nJobs++ + } + + if stderr != nil { + cStderr = streamConfig.StderrPipe() + nJobs++ + } + + errors := make(chan error, nJobs) + // Connect stdin of container to the http conn. if stdin != nil && openStdin { - nJobs++ // Get the stdin pipe. - if cStdin, err := streamConfig.StdinPipe(); err != nil { - errors <- err - } else { - go func() { - log.Debugf("attach: stdin: begin") - defer log.Debugf("attach: stdin: end") + cStdin = streamConfig.StdinPipe() + go func() { + log.Debugf("attach: stdin: begin") + defer func() { if stdinOnce && !tty { defer cStdin.Close() } else { // No matter what, when stdin is closed (io.Copy unblock), close stdout and stderr - defer func() { - if cStdout != nil { - cStdout.Close() - } - if cStderr != nil { - cStderr.Close() - } - }() + if cStdout != nil { + cStdout.Close() + } + if cStderr != nil { + cStderr.Close() + } } - if tty { - _, err = utils.CopyEscapable(cStdin, stdin) - } else { - _, err = io.Copy(cStdin, stdin) + log.Debugf("attach: stdin: end") + }() + var err error + if tty { + _, err = utils.CopyEscapable(cStdin, stdin) + } else { + _, err = io.Copy(cStdin, stdin) - } - if err == io.ErrClosedPipe { - err = nil - } - if err != nil { - log.Errorf("attach: stdin: %s", err) - } - errors <- err - }() - } - } - if stdout != nil { - nJobs++ - // Get a reader end of a pipe that is attached as stdout to the container. - if p, err := streamConfig.StdoutPipe(); err != nil { - errors <- err - } else { - cStdout = p - go func() { - log.Debugf("attach: stdout: begin") - defer log.Debugf("attach: stdout: end") - // If we are in StdinOnce mode, then close stdin - if stdinOnce && stdin != nil { - defer stdin.Close() - } - _, err := io.Copy(stdout, cStdout) - if err == io.ErrClosedPipe { - err = nil - } - if err != nil { - log.Errorf("attach: stdout: %s", err) - } - errors <- err - }() - } - } else { - // Point stdout of container to a no-op writer. - go func() { - if cStdout, err := streamConfig.StdoutPipe(); err != nil { - log.Errorf("attach: stdout pipe: %s", err) - } else { - io.Copy(&ioutils.NopWriter{}, cStdout) } + if err == io.ErrClosedPipe { + err = nil + } + if err != nil { + log.Errorf("attach: stdin: %s", err) + } + errors <- err }() } - if stderr != nil { - nJobs++ - if p, err := streamConfig.StderrPipe(); err != nil { - errors <- err - } else { - cStderr = p - go func() { - log.Debugf("attach: stderr: begin") - defer log.Debugf("attach: stderr: end") - // If we are in StdinOnce mode, then close stdin - // Why are we closing stdin here and above while handling stdout? - if stdinOnce && stdin != nil { - defer stdin.Close() - } - _, err := io.Copy(stderr, cStderr) - if err == io.ErrClosedPipe { - err = nil - } - if err != nil { - log.Errorf("attach: stderr: %s", err) - } - errors <- err - }() + + attachStream := func(name string, stream io.Writer, streamPipe io.ReadCloser) { + if stream == nil { + return } - } else { - // Point stderr at a no-op writer. - go func() { - if cStderr, err := streamConfig.StderrPipe(); err != nil { - log.Errorf("attach: stdout pipe: %s", err) - } else { - io.Copy(&ioutils.NopWriter{}, cStderr) + defer func() { + // Make sure stdin gets closed + if stdinOnce && cStdin != nil { + stdin.Close() + cStdin.Close() } + streamPipe.Close() }() + + log.Debugf("attach: %s: begin", name) + defer log.Debugf("attach: %s: end", name) + _, err := io.Copy(stream, streamPipe) + if err == io.ErrClosedPipe { + err = nil + } + if err != nil { + log.Errorf("attach: %s: %v", name, err) + } + errors <- err } + go attachStream("stdout", stdout, cStdout) + go attachStream("stderr", stderr, cStderr) + return promise.Go(func() error { - defer func() { - if cStdout != nil { - cStdout.Close() - } - if cStderr != nil { - cStderr.Close() - } - }() - for i := 0; i < nJobs; i++ { log.Debugf("attach: waiting for job %d/%d", i+1, nJobs) - if err := <-errors; err != nil { + err := <-errors + if err != nil { log.Errorf("attach: job %d returned error %s, aborting all jobs", i+1, err) return err } diff --git a/daemon/container.go b/daemon/container.go index 75cd133fe..2b584abef 100644 --- a/daemon/container.go +++ b/daemon/container.go @@ -370,10 +370,7 @@ func (container *Container) Run() error { } func (container *Container) Output() (output []byte, err error) { - pipe, err := container.StdoutPipe() - if err != nil { - return nil, err - } + pipe := container.StdoutPipe() defer pipe.Close() if err := container.Start(); err != nil { return nil, err @@ -391,20 +388,20 @@ func (container *Container) Output() (output []byte, err error) { // copied and delivered to all StdoutPipe and StderrPipe consumers, using // a kind of "broadcaster". -func (streamConfig *StreamConfig) StdinPipe() (io.WriteCloser, error) { - return streamConfig.stdinPipe, nil +func (streamConfig *StreamConfig) StdinPipe() io.WriteCloser { + return streamConfig.stdinPipe } -func (streamConfig *StreamConfig) StdoutPipe() (io.ReadCloser, error) { +func (streamConfig *StreamConfig) StdoutPipe() io.ReadCloser { reader, writer := io.Pipe() streamConfig.stdout.AddWriter(writer, "") - return ioutils.NewBufReader(reader), nil + return ioutils.NewBufReader(reader) } -func (streamConfig *StreamConfig) StderrPipe() (io.ReadCloser, error) { +func (streamConfig *StreamConfig) StderrPipe() io.ReadCloser { reader, writer := io.Pipe() streamConfig.stderr.AddWriter(writer, "") - return ioutils.NewBufReader(reader), nil + return ioutils.NewBufReader(reader) } func (streamConfig *StreamConfig) StdoutLogPipe() io.ReadCloser { diff --git a/integration/commands_test.go b/integration/commands_test.go index 79ed13211..bd91716d8 100644 --- a/integration/commands_test.go +++ b/integration/commands_test.go @@ -412,7 +412,7 @@ func TestAttachDisconnect(t *testing.T) { } // Try to avoid the timeout in destroy. Best effort, don't check error - cStdin, _ := container.StdinPipe() + cStdin := container.StdinPipe() cStdin.Close() container.WaitStop(-1 * time.Second) } diff --git a/integration/container_test.go b/integration/container_test.go index ab94cbc67..5de4bcc6f 100644 --- a/integration/container_test.go +++ b/integration/container_test.go @@ -26,14 +26,8 @@ func TestRestartStdin(t *testing.T) { } defer daemon.Destroy(container) - stdin, err := container.StdinPipe() - if err != nil { - t.Fatal(err) - } - stdout, err := container.StdoutPipe() - if err != nil { - t.Fatal(err) - } + stdin := container.StdinPipe() + stdout := container.StdoutPipe() if err := container.Start(); err != nil { t.Fatal(err) } @@ -56,14 +50,8 @@ func TestRestartStdin(t *testing.T) { } // Restart and try again - stdin, err = container.StdinPipe() - if err != nil { - t.Fatal(err) - } - stdout, err = container.StdoutPipe() - if err != nil { - t.Fatal(err) - } + stdin = container.StdinPipe() + stdout = container.StdoutPipe() if err := container.Start(); err != nil { t.Fatal(err) } @@ -103,14 +91,8 @@ func TestStdin(t *testing.T) { } defer daemon.Destroy(container) - stdin, err := container.StdinPipe() - if err != nil { - t.Fatal(err) - } - stdout, err := container.StdoutPipe() - if err != nil { - t.Fatal(err) - } + stdin := container.StdinPipe() + stdout := container.StdoutPipe() if err := container.Start(); err != nil { t.Fatal(err) } @@ -149,14 +131,8 @@ func TestTty(t *testing.T) { } defer daemon.Destroy(container) - stdin, err := container.StdinPipe() - if err != nil { - t.Fatal(err) - } - stdout, err := container.StdoutPipe() - if err != nil { - t.Fatal(err) - } + stdin := container.StdinPipe() + stdout := container.StdoutPipe() if err := container.Start(); err != nil { t.Fatal(err) } diff --git a/integration/runtime_test.go b/integration/runtime_test.go index 93a32e9f7..a436995fd 100644 --- a/integration/runtime_test.go +++ b/integration/runtime_test.go @@ -610,7 +610,7 @@ func TestRestore(t *testing.T) { } // Simulate a crash/manual quit of dockerd: process dies, states stays 'Running' - cStdin, _ := container2.StdinPipe() + cStdin := container2.StdinPipe() cStdin.Close() if _, err := container2.WaitStop(2 * time.Second); err != nil { t.Fatal(err) diff --git a/integration/utils_test.go b/integration/utils_test.go index 0c78a7617..0cb22ee1c 100644 --- a/integration/utils_test.go +++ b/integration/utils_test.go @@ -85,14 +85,8 @@ func containerFileExists(eng *engine.Engine, id, dir string, t Fataler) bool { func containerAttach(eng *engine.Engine, id string, t Fataler) (io.WriteCloser, io.ReadCloser) { c := getContainer(eng, id, t) - i, err := c.StdinPipe() - if err != nil { - t.Fatal(err) - } - o, err := c.StdoutPipe() - if err != nil { - t.Fatal(err) - } + i := c.StdinPipe() + o := c.StdoutPipe() return i, o } @@ -292,10 +286,7 @@ func runContainer(eng *engine.Engine, r *daemon.Daemon, args []string, t *testin return "", err } defer r.Destroy(container) - stdout, err := container.StdoutPipe() - if err != nil { - return "", err - } + stdout := container.StdoutPipe() defer stdout.Close() job := eng.Job("start", container.ID) From e6c9343457c501c1da718c25bb9601f87d82cd7d Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Mon, 5 Jan 2015 17:30:38 -0800 Subject: [PATCH 169/513] Use waitgroup instead of iterating errors chan Signed-off-by: Brian Goff --- daemon/attach.go | 92 ++++++++++++++++++++++++------------------------ 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/daemon/attach.go b/daemon/attach.go index 144c5c777..dc7ffa307 100644 --- a/daemon/attach.go +++ b/daemon/attach.go @@ -4,6 +4,7 @@ import ( "encoding/json" "io" "os" + "sync" "time" log "github.com/Sirupsen/logrus" @@ -114,62 +115,63 @@ func (daemon *Daemon) attach(streamConfig *StreamConfig, openStdin, stdinOnce, t var ( cStdout, cStderr io.ReadCloser cStdin io.WriteCloser - nJobs int + wg sync.WaitGroup + errors = make(chan error, 3) ) if stdin != nil && openStdin { cStdin = streamConfig.StdinPipe() - nJobs++ + wg.Add(1) } if stdout != nil { cStdout = streamConfig.StdoutPipe() - nJobs++ + wg.Add(1) } if stderr != nil { cStderr = streamConfig.StderrPipe() - nJobs++ + wg.Add(1) } - errors := make(chan error, nJobs) - // Connect stdin of container to the http conn. - if stdin != nil && openStdin { - // Get the stdin pipe. - cStdin = streamConfig.StdinPipe() - go func() { - log.Debugf("attach: stdin: begin") - defer func() { - if stdinOnce && !tty { - defer cStdin.Close() - } else { - // No matter what, when stdin is closed (io.Copy unblock), close stdout and stderr - if cStdout != nil { - cStdout.Close() - } - if cStderr != nil { - cStderr.Close() - } - } - log.Debugf("attach: stdin: end") - }() - var err error - if tty { - _, err = utils.CopyEscapable(cStdin, stdin) + go func() { + if stdin == nil || !openStdin { + return + } + log.Debugf("attach: stdin: begin") + defer func() { + if stdinOnce && !tty { + cStdin.Close() } else { - _, err = io.Copy(cStdin, stdin) - + // No matter what, when stdin is closed (io.Copy unblock), close stdout and stderr + if cStdout != nil { + cStdout.Close() + } + if cStderr != nil { + cStderr.Close() + } } - if err == io.ErrClosedPipe { - err = nil - } - if err != nil { - log.Errorf("attach: stdin: %s", err) - } - errors <- err + wg.Done() + log.Debugf("attach: stdin: end") }() - } + + var err error + if tty { + _, err = utils.CopyEscapable(cStdin, stdin) + } else { + _, err = io.Copy(cStdin, stdin) + + } + if err == io.ErrClosedPipe { + err = nil + } + if err != nil { + log.Errorf("attach: stdin: %s", err) + errors <- err + return + } + }() attachStream := func(name string, stream io.Writer, streamPipe io.ReadCloser) { if stream == nil { @@ -182,34 +184,32 @@ func (daemon *Daemon) attach(streamConfig *StreamConfig, openStdin, stdinOnce, t cStdin.Close() } streamPipe.Close() + wg.Done() + log.Debugf("attach: %s: end", name) }() log.Debugf("attach: %s: begin", name) - defer log.Debugf("attach: %s: end", name) _, err := io.Copy(stream, streamPipe) if err == io.ErrClosedPipe { err = nil } if err != nil { log.Errorf("attach: %s: %v", name, err) + errors <- err } - errors <- err } go attachStream("stdout", stdout, cStdout) go attachStream("stderr", stderr, cStderr) return promise.Go(func() error { - for i := 0; i < nJobs; i++ { - log.Debugf("attach: waiting for job %d/%d", i+1, nJobs) - err := <-errors + wg.Wait() + close(errors) + for err := range errors { if err != nil { - log.Errorf("attach: job %d returned error %s, aborting all jobs", i+1, err) return err } - log.Debugf("attach: job %d completed successfully", i+1) } - log.Debugf("attach: all jobs completed successfully") return nil }) } From af053ccf6b3179978e087defd0062664152697ef Mon Sep 17 00:00:00 2001 From: HuKeping Date: Tue, 6 Jan 2015 11:33:53 +0800 Subject: [PATCH 170/513] restart: Fix an error about arguments missing Function shouldRestart() checks the restart policy and records the debug info and there should be two arguments in the log.Debugf(). Prior to the this patch, the logs were something like this: - client: $ docker run --restart=on-failure:3 ubuntu /bin/sh -c 'exit 1' - daemon: INFO[0168] ... DEBU[0168] stopping restart of container %!s(int=3) because maximum failure could of %!d(MISSING) has been reached INFO[0086] ... Btw, fix a spelling error in the same file: - cotnainer -> container ---------------------------------------- Signed-off-by: Hu Keping --- daemon/monitor.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/daemon/monitor.go b/daemon/monitor.go index 1b5c4f624..081ca391b 100644 --- a/daemon/monitor.go +++ b/daemon/monitor.go @@ -9,12 +9,13 @@ import ( log "github.com/Sirupsen/logrus" "github.com/docker/docker/daemon/execdriver" "github.com/docker/docker/runconfig" + "github.com/docker/docker/utils" ) const defaultTimeIncrement = 100 // containerMonitor monitors the execution of a container's main process. -// If a restart policy is specified for the cotnainer the monitor will ensure that the +// If a restart policy is specified for the container the monitor will ensure that the // process is restarted based on the rules of the policy. When the container is finally stopped // the monitor will reset and cleanup any of the container resources such as networking allocations // and the rootfs @@ -230,7 +231,8 @@ func (m *containerMonitor) shouldRestart(exitCode int) bool { case "on-failure": // the default value of 0 for MaximumRetryCount means that we will not enforce a maximum count if max := m.restartPolicy.MaximumRetryCount; max != 0 && m.failureCount >= max { - log.Debugf("stopping restart of container %s because maximum failure could of %d has been reached", max) + log.Debugf("stopping restart of container %s because maximum failure could of %d has been reached", + utils.TruncateID(m.container.ID), max) return false } From d1e5078f318f69b24ed47f2bf283903650ced4d9 Mon Sep 17 00:00:00 2001 From: Sven Dowideit Date: Mon, 5 Jan 2015 10:53:39 +1000 Subject: [PATCH 171/513] Add @ArikaChen's tip for using HTTPS proxy to pull Docker-DCO-1.1-Signed-off-by: Sven Dowideit (github: SvenDowideit) Signed-off-by: Sven Dowideit --- docs/sources/reference/commandline/cli.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index e48a393b7..9ef77736d 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -364,6 +364,22 @@ flag to the Docker daemon as described above. Local registries, whose IP address falls in the 127.0.0.0/8 range, are automatically marked as insecure as of Docker 1.3.2. It is not recommended to rely on this, as it may change in the future. +### Running a Docker daemon behind a HTTPS_PROXY + +When running inside a LAN that uses a `HTTPS` proxy, the Docker Hub certificates +will be replaced by the proxy's certificates. These certificates need to be added +to your Docker host's configuration: + +1. Install the `ca-certificates` package for your distribution +2. Ask your network admin for the proxy's CA certificate and append them to + `/etc/pki/tls/certs/ca-bundle.crt` +3. Then start your Docker daemon with `HTTPS_PROXY=http://username:password@proxy:port/ docker -d`. + The `username:` and `password@` are optional - and are only needed if your proxy + is set up to require authentication. + +This will only add the proxy and authentication to the Docker daemon's requests - +your `docker build`s and running containers will need extra configuration to use +the proxy ### Miscellaneous options From 7b2331061e428176cf513487745ac496e2bd7027 Mon Sep 17 00:00:00 2001 From: Sven Dowideit Date: Tue, 6 Jan 2015 17:01:10 +1000 Subject: [PATCH 172/513] Explicitly mention that '-P' maps to random ports as noted in https://github.com/boot2docker/boot2docker/issues/690 Signed-off-by: Sven Dowideit Docker-DCO-1.1-Signed-off-by: Sven Dowideit (github: SvenDowideit) --- contrib/completion/fish/docker.fish | 4 ++-- docs/man/docker-create.1.md | 2 +- docs/man/docker-run.1.md | 2 +- docs/sources/reference/commandline/cli.md | 4 ++-- runconfig/parse.go | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/contrib/completion/fish/docker.fish b/contrib/completion/fish/docker.fish index 5eef6f30b..c0a5725a1 100644 --- a/contrib/completion/fish/docker.fish +++ b/contrib/completion/fish/docker.fish @@ -122,7 +122,7 @@ complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l lxc-conf -d complete -c docker -A -f -n '__fish_seen_subcommand_from create' -s m -l memory -d 'Memory limit (format: , where unit = b, k, m or g)' complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l name -d 'Assign a name to the container' complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l net -d 'Set the Network mode for the container' -complete -c docker -A -f -n '__fish_seen_subcommand_from create' -s P -l publish-all -d 'Publish all exposed ports to the host interfaces' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -s P -l publish-all -d 'Publish all exposed ports to random ports on the host interfaces' complete -c docker -A -f -n '__fish_seen_subcommand_from create' -s p -l publish -d "Publish a container's port to the host" complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l privileged -d 'Give extended privileges to this container' complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l restart -d 'Restart policy to apply when a container exits (no, on-failure[:max-retry], always)' @@ -278,7 +278,7 @@ complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l lxc-conf -d '(l complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s m -l memory -d 'Memory limit (format: , where unit = b, k, m or g)' complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l name -d 'Assign a name to the container' complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l net -d 'Set the Network mode for the container' -complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s P -l publish-all -d 'Publish all exposed ports to the host interfaces' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s P -l publish-all -d 'Publish all exposed ports to random ports on the host interfaces' complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s p -l publish -d "Publish a container's port to the host" complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l privileged -d 'Give extended privileges to this container' complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l restart -d 'Restart policy to apply when a container exits (no, on-failure[:max-retry], always)' diff --git a/docs/man/docker-create.1.md b/docs/man/docker-create.1.md index 96a049672..f6448258a 100644 --- a/docs/man/docker-create.1.md +++ b/docs/man/docker-create.1.md @@ -118,7 +118,7 @@ IMAGE [COMMAND] [ARG...] 'host': use the host network stack inside the container. Note: the host mode gives the container full access to local system services such as D-bus and is therefore considered insecure. **-P**, **--publish-all**=*true*|*false* - Publish all exposed ports to the host interfaces. The default is *false*. + Publish all exposed ports to random ports on the host interfaces. The default is *false*. **-p**, **--publish**=[] Publish a container's port, or a range of ports, to the host diff --git a/docs/man/docker-run.1.md b/docs/man/docker-run.1.md index b9571dbe2..b85bf2164 100644 --- a/docs/man/docker-run.1.md +++ b/docs/man/docker-run.1.md @@ -214,7 +214,7 @@ and foreground Docker containers. 'host': use the host network stack inside the container. Note: the host mode gives the container full access to local system services such as D-bus and is therefore considered insecure. **-P**, **--publish-all**=*true*|*false* - Publish all exposed ports to the host interfaces. The default is *false*. + Publish all exposed ports to random ports on the host interfaces. The default is *false*. When set to true publish all exposed ports to the host interfaces. The default is false. If the operator uses -P (or -p) then Docker will make the diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index e48a393b7..5b225ca1e 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -685,7 +685,7 @@ Creates a new container. 'none': no networking for this container 'container:': reuses another container network stack 'host': use the host network stack inside the container. Note: the host mode gives the container full access to local system services such as D-bus and is therefore considered insecure. - -P, --publish-all=false Publish all exposed ports to the host interfaces + -P, --publish-all=false Publish all exposed ports to random ports on the host interfaces -p, --publish=[] Publish a container's port, or a range of ports (e.g., `-p 3300-3310`), to the host format: ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort | containerPort Both hostPort and containerPort can be specified as a range of ports. @@ -1513,7 +1513,7 @@ removed before the image is removed. 'none': no networking for this container 'container:': reuses another container network stack 'host': use the host network stack inside the container. Note: the host mode gives the container full access to local system services such as D-bus and is therefore considered insecure. - -P, --publish-all=false Publish all exposed ports to the host interfaces + -P, --publish-all=false Publish all exposed ports to random ports on the host interfaces -p, --publish=[] Publish a container's port to the host format: ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort | containerPort Both hostPort and containerPort can be specified as a range of ports. diff --git a/runconfig/parse.go b/runconfig/parse.go index 5c684e346..732c60ed1 100644 --- a/runconfig/parse.go +++ b/runconfig/parse.go @@ -46,7 +46,7 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe flNetwork = cmd.Bool([]string{"#n", "#-networking"}, true, "Enable networking for this container") flPrivileged = cmd.Bool([]string{"#privileged", "-privileged"}, false, "Give extended privileges to this container") - flPublishAll = cmd.Bool([]string{"P", "-publish-all"}, false, "Publish all exposed ports to the host interfaces") + flPublishAll = cmd.Bool([]string{"P", "-publish-all"}, false, "Publish all exposed ports to random ports on the host interfaces") flStdin = cmd.Bool([]string{"i", "-interactive"}, false, "Keep STDIN open even if not attached") flTty = cmd.Bool([]string{"t", "-tty"}, false, "Allocate a pseudo-TTY") flContainerIDFile = cmd.String([]string{"#cidfile", "-cidfile"}, "", "Write the container ID to the file") From a2b529ead21e6ab9eafcb1b1d2437c725c43a06a Mon Sep 17 00:00:00 2001 From: Dan Walsh Date: Wed, 15 Oct 2014 17:14:12 -0400 Subject: [PATCH 173/513] --help option and help command should print to stdout not stderr --help and help are successful commands so output should not go to error. QE teams have requested this change, also users doing docker help | less or docker run --help | less would expect this to work. Usage statement should only be printed when the user asks for it. Errors should print error message and then suggest the docker COMMAND --help command to see usage information. The current behaviour causes the user to have to search for the error message and sometimes scrolls right off the screen. For example a error on a "docker run" command is very difficult to diagnose. Finally erros should always exit with a non 0 exit code, if the user makes a CLI error. Docker-DCO-1.1-Signed-off-by: Dan Walsh (github: rhatdan) --- api/client/cli.go | 9 +- api/client/commands.go | 248 ++++++++++++++++------ docker/flags.go | 6 +- docs/man/docker-attach.1.md | 4 + docs/man/docker-build.1.md | 4 + docs/man/docker-commit.1.md | 4 + docs/man/docker-cp.1.md | 4 +- docs/man/docker-create.1.md | 4 + docs/man/docker-diff.1.md | 4 +- docs/man/docker-events.1.md | 4 + docs/man/docker-exec.1.md | 4 + docs/man/docker-export.1.md | 4 +- docs/man/docker-history.1.md | 4 + docs/man/docker-images.1.md | 4 + docs/man/docker-import.1.md | 4 +- docs/man/docker-info.1.md | 4 +- docs/man/docker-inspect.1.md | 4 + docs/man/docker-kill.1.md | 4 + docs/man/docker-load.1.md | 4 + docs/man/docker-login.1.md | 4 + docs/man/docker-logs.1.md | 4 + docs/man/docker-port.1.md | 4 +- docs/man/docker-ps.1.md | 4 + docs/man/docker-pull.1.md | 5 +- docs/man/docker-push.1.md | 4 +- docs/man/docker-restart.1.md | 4 + docs/man/docker-rm.1.md | 3 + docs/man/docker-rmi.1.md | 4 + docs/man/docker-run.1.md | 4 + docs/man/docker-save.1.md | 4 + docs/man/docker-search.1.md | 4 + docs/man/docker-start.1.md | 4 + docs/man/docker-stop.1.md | 4 + docs/man/docker-tag.1.md | 1 + docs/man/docker-top.1.md | 4 +- docs/man/docker-wait.1.md | 4 +- docs/man/docker.1.md | 3 + docs/sources/reference/commandline/cli.md | 13 ++ pkg/mflag/flag.go | 45 +++- runconfig/exec.go | 12 +- runconfig/parse.go | 14 +- 41 files changed, 390 insertions(+), 89 deletions(-) diff --git a/api/client/cli.go b/api/client/cli.go index e54eb8056..aec8ebb86 100644 --- a/api/client/cli.go +++ b/api/client/cli.go @@ -75,8 +75,8 @@ func (cli *DockerCli) Cmd(args ...string) error { if len(args) > 0 { method, exists := cli.getMethod(args[0]) if !exists { - fmt.Println("Error: Command not found:", args[0]) - return cli.CmdHelp() + fmt.Fprintf(cli.err, "docker: '%s' is not a docker command. See 'docker --help'.\n", args[0]) + os.Exit(1) } return method(args[1:]...) } @@ -90,9 +90,10 @@ func (cli *DockerCli) Subcmd(name, signature, description string) *flag.FlagSet if flags.FlagCountUndeprecated() > 0 { options = "[OPTIONS] " } - fmt.Fprintf(cli.err, "\nUsage: docker %s %s%s\n\n%s\n\n", name, options, signature, description) + fmt.Fprintf(cli.out, "\nUsage: docker %s %s%s\n\n%s\n\n", name, options, signature, description) + flags.SetOutput(cli.out) flags.PrintDefaults() - os.Exit(2) + os.Exit(0) } return flags } diff --git a/api/client/commands.go b/api/client/commands.go index e666d4320..14bcc8ffa 100644 --- a/api/client/commands.go +++ b/api/client/commands.go @@ -64,6 +64,8 @@ func (cli *DockerCli) CmdHelp(args ...string) error { method, exists := cli.getMethod(args[0]) if !exists { fmt.Fprintf(cli.err, "Error: Command not found: %s\n", args[0]) + fmt.Fprintf(cli.err, "docker: '%s' is not a docker command. See 'docker --help'.\n", args[0]) + os.Exit(1) } else { method("--help") return nil @@ -83,13 +85,18 @@ func (cli *DockerCli) CmdBuild(args ...string) error { rm := cmd.Bool([]string{"#rm", "-rm"}, true, "Remove intermediate containers after a successful build") forceRm := cmd.Bool([]string{"-force-rm"}, false, "Always remove intermediate containers, even after unsuccessful builds") pull := cmd.Bool([]string{"-pull"}, false, "Always attempt to pull a newer version of the image") + help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") + if err := cmd.Parse(args); err != nil { return nil } - if cmd.NArg() != 1 { + if *help { cmd.Usage() return nil } + if cmd.BadArgs(flag.Exact, 1) { + os.Exit(1) + } var ( context archive.Archive @@ -254,10 +261,16 @@ func (cli *DockerCli) CmdLogin(args ...string) error { cmd.StringVar(&username, []string{"u", "-username"}, "", "Username") cmd.StringVar(&password, []string{"p", "-password"}, "", "Password") cmd.StringVar(&email, []string{"e", "-email"}, "", "Email") + help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") + err := cmd.Parse(args) if err != nil { return nil } + if *help { + cmd.Usage() + return nil + } serverAddress := registry.IndexServerAddress() if len(cmd.Args()) > 0 { serverAddress = cmd.Arg(0) @@ -390,13 +403,18 @@ func (cli *DockerCli) CmdLogout(args ...string) error { // 'docker wait': block until a container stops func (cli *DockerCli) CmdWait(args ...string) error { cmd := cli.Subcmd("wait", "CONTAINER [CONTAINER...]", "Block until a container stops, then print its exit code.") + help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") + if err := cmd.Parse(args); err != nil { return nil } - if cmd.NArg() < 1 { + if *help { cmd.Usage() return nil } + if cmd.BadArgs(flag.Min, 1) { + os.Exit(1) + } var encounteredError error for _, name := range cmd.Args() { status, err := waitForExit(cli, name) @@ -416,10 +434,8 @@ func (cli *DockerCli) CmdVersion(args ...string) error { if err := cmd.Parse(args); err != nil { return nil } - - if cmd.NArg() > 0 { - cmd.Usage() - return nil + if cmd.BadArgs(flag.Exact, 0) { + os.Exit(1) } if dockerversion.VERSION != "" { fmt.Fprintf(cli.out, "Client version: %s\n", dockerversion.VERSION) @@ -462,9 +478,8 @@ func (cli *DockerCli) CmdInfo(args ...string) error { if err := cmd.Parse(args); err != nil { return nil } - if cmd.NArg() > 0 { - cmd.Usage() - return nil + if cmd.BadArgs(flag.Exact, 0) { + os.Exit(1) } body, _, err := readBody(cli.call("GET", "/info", nil, false)) @@ -579,13 +594,18 @@ func (cli *DockerCli) CmdInfo(args ...string) error { func (cli *DockerCli) CmdStop(args ...string) error { cmd := cli.Subcmd("stop", "CONTAINER [CONTAINER...]", "Stop a running container by sending SIGTERM and then SIGKILL after a grace period") nSeconds := cmd.Int([]string{"t", "-time"}, 10, "Number of seconds to wait for the container to stop before killing it. Default is 10 seconds.") + help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") + if err := cmd.Parse(args); err != nil { return nil } - if cmd.NArg() < 1 { + if *help { cmd.Usage() return nil } + if cmd.BadArgs(flag.Min, 1) { + os.Exit(1) + } v := url.Values{} v.Set("t", strconv.Itoa(*nSeconds)) @@ -606,13 +626,18 @@ func (cli *DockerCli) CmdStop(args ...string) error { func (cli *DockerCli) CmdRestart(args ...string) error { cmd := cli.Subcmd("restart", "CONTAINER [CONTAINER...]", "Restart a running container") nSeconds := cmd.Int([]string{"t", "-time"}, 10, "Number of seconds to try to stop for before killing the container. Once killed it will then be restarted. Default is 10 seconds.") + help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") + if err := cmd.Parse(args); err != nil { return nil } - if cmd.NArg() < 1 { + if *help { cmd.Usage() return nil } + if cmd.BadArgs(flag.Min, 1) { + os.Exit(1) + } v := url.Values{} v.Set("t", strconv.Itoa(*nSeconds)) @@ -664,15 +689,19 @@ func (cli *DockerCli) CmdStart(args ...string) error { cmd = cli.Subcmd("start", "CONTAINER [CONTAINER...]", "Restart a stopped container") attach = cmd.Bool([]string{"a", "-attach"}, false, "Attach container's STDOUT and STDERR and forward all signals to the process") openStdin = cmd.Bool([]string{"i", "-interactive"}, false, "Attach container's STDIN") + help = cmd.Bool([]string{"#help", "-help"}, false, "Print usage") ) if err := cmd.Parse(args); err != nil { return nil } - if cmd.NArg() < 1 { + if *help { cmd.Usage() return nil } + if cmd.BadArgs(flag.Min, 1) { + os.Exit(1) + } hijacked := make(chan io.Closer) @@ -778,10 +807,8 @@ func (cli *DockerCli) CmdUnpause(args ...string) error { if err := cmd.Parse(args); err != nil { return nil } - - if cmd.NArg() != 1 { - cmd.Usage() - return nil + if cmd.BadArgs(flag.Exact, 1) { + os.Exit(1) } var encounteredError error @@ -801,10 +828,8 @@ func (cli *DockerCli) CmdPause(args ...string) error { if err := cmd.Parse(args); err != nil { return nil } - - if cmd.NArg() != 1 { - cmd.Usage() - return nil + if cmd.BadArgs(flag.Exact, 1) { + os.Exit(1) } var encounteredError error @@ -822,13 +847,18 @@ func (cli *DockerCli) CmdPause(args ...string) error { func (cli *DockerCli) CmdInspect(args ...string) error { cmd := cli.Subcmd("inspect", "CONTAINER|IMAGE [CONTAINER|IMAGE...]", "Return low-level information on a container or image") tmplStr := cmd.String([]string{"f", "#format", "-format"}, "", "Format the output using the given go template.") + help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") + if err := cmd.Parse(args); err != nil { return nil } - if cmd.NArg() < 1 { + if *help { cmd.Usage() return nil } + if cmd.BadArgs(flag.Min, 1) { + os.Exit(1) + } var tmpl *template.Template if *tmplStr != "" { @@ -901,13 +931,18 @@ func (cli *DockerCli) CmdInspect(args ...string) error { func (cli *DockerCli) CmdTop(args ...string) error { cmd := cli.Subcmd("top", "CONTAINER [ps OPTIONS]", "Display the running processes of a container") + help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") + if err := cmd.Parse(args); err != nil { return nil } - if cmd.NArg() == 0 { + if *help { cmd.Usage() return nil } + if cmd.BadArgs(flag.Min, 1) { + os.Exit(1) + } val := url.Values{} if cmd.NArg() > 1 { val.Set("ps_args", strings.Join(cmd.Args()[1:], " ")) @@ -936,13 +971,17 @@ func (cli *DockerCli) CmdTop(args ...string) error { func (cli *DockerCli) CmdPort(args ...string) error { cmd := cli.Subcmd("port", "CONTAINER [PRIVATE_PORT[/PROTO]]", "List port mappings for the CONTAINER, or lookup the public-facing port that is NAT-ed to the PRIVATE_PORT") + help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") if err := cmd.Parse(args); err != nil { return nil } - if cmd.NArg() < 1 { + if *help { cmd.Usage() return nil } + if cmd.BadArgs(flag.Min, 1) { + os.Exit(1) + } stream, _, err := cli.call("GET", "/containers/"+cmd.Arg(0)+"/json", nil, false) if err != nil { @@ -995,13 +1034,18 @@ func (cli *DockerCli) CmdRmi(args ...string) error { force = cmd.Bool([]string{"f", "-force"}, false, "Force removal of the image") noprune = cmd.Bool([]string{"-no-prune"}, false, "Do not delete untagged parents") ) + help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") + if err := cmd.Parse(args); err != nil { return nil } - if cmd.NArg() < 1 { + if *help { cmd.Usage() return nil } + if cmd.BadArgs(flag.Min, 1) { + os.Exit(1) + } v := url.Values{} if *force { @@ -1040,14 +1084,18 @@ func (cli *DockerCli) CmdHistory(args ...string) error { cmd := cli.Subcmd("history", "IMAGE", "Show the history of an image") quiet := cmd.Bool([]string{"q", "-quiet"}, false, "Only show numeric IDs") noTrunc := cmd.Bool([]string{"#notrunc", "-no-trunc"}, false, "Don't truncate output") + help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") if err := cmd.Parse(args); err != nil { return nil } - if cmd.NArg() != 1 { + if *help { cmd.Usage() return nil } + if cmd.BadArgs(flag.Exact, 1) { + os.Exit(1) + } body, _, err := readBody(cli.call("GET", "/images/"+cmd.Arg(0)+"/history", nil, false)) if err != nil { @@ -1098,14 +1146,18 @@ func (cli *DockerCli) CmdRm(args ...string) error { v := cmd.Bool([]string{"v", "-volumes"}, false, "Remove the volumes associated with the container") link := cmd.Bool([]string{"l", "#link", "-link"}, false, "Remove the specified link and not the underlying container") force := cmd.Bool([]string{"f", "-force"}, false, "Force the removal of a running container (uses SIGKILL)") + help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") if err := cmd.Parse(args); err != nil { return nil } - if cmd.NArg() < 1 { + if *help { cmd.Usage() return nil } + if cmd.BadArgs(flag.Min, 1) { + os.Exit(1) + } val := url.Values{} if *v { @@ -1136,14 +1188,18 @@ func (cli *DockerCli) CmdRm(args ...string) error { func (cli *DockerCli) CmdKill(args ...string) error { cmd := cli.Subcmd("kill", "CONTAINER [CONTAINER...]", "Kill a running container using SIGKILL or a specified signal") signal := cmd.String([]string{"s", "-signal"}, "KILL", "Signal to send to the container") + help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") if err := cmd.Parse(args); err != nil { return nil } - if cmd.NArg() < 1 { + if *help { cmd.Usage() return nil } + if cmd.BadArgs(flag.Min, 1) { + os.Exit(1) + } var encounteredError error for _, name := range cmd.Args() { @@ -1159,15 +1215,18 @@ func (cli *DockerCli) CmdKill(args ...string) error { func (cli *DockerCli) CmdImport(args ...string) error { cmd := cli.Subcmd("import", "URL|- [REPOSITORY[:TAG]]", "Create an empty filesystem image and import the contents of the tarball (.tar, .tar.gz, .tgz, .bzip, .tar.xz, .txz) into it, then optionally tag it.") + help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") if err := cmd.Parse(args); err != nil { return nil } - if cmd.NArg() < 1 { + if *help { cmd.Usage() return nil } - + if cmd.BadArgs(flag.Min, 1) { + os.Exit(1) + } var ( v = url.Values{} src = cmd.Arg(0) @@ -1201,15 +1260,19 @@ func (cli *DockerCli) CmdImport(args ...string) error { func (cli *DockerCli) CmdPush(args ...string) error { cmd := cli.Subcmd("push", "NAME[:TAG]", "Push an image or a repository to the registry") + help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") + if err := cmd.Parse(args); err != nil { return nil } - name := cmd.Arg(0) - - if name == "" { + if *help { cmd.Usage() return nil } + if cmd.BadArgs(flag.Exact, 1) { + os.Exit(1) + } + name := cmd.Arg(0) cli.LoadConfigFile() @@ -1267,14 +1330,19 @@ func (cli *DockerCli) CmdPush(args ...string) error { func (cli *DockerCli) CmdPull(args ...string) error { cmd := cli.Subcmd("pull", "NAME[:TAG]", "Pull an image or a repository from the registry") allTags := cmd.Bool([]string{"a", "-all-tags"}, false, "Download all tagged images in the repository") + help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") + if err := cmd.Parse(args); err != nil { return nil } - - if cmd.NArg() != 1 { + if *help { cmd.Usage() return nil } + + if cmd.BadArgs(flag.Exact, 1) { + os.Exit(1) + } var ( v = url.Values{} remote = cmd.Arg(0) @@ -1338,6 +1406,7 @@ func (cli *DockerCli) CmdImages(args ...string) error { // FIXME: --viz and --tree are deprecated. Remove them in a future version. flViz := cmd.Bool([]string{"#v", "#viz", "#-viz"}, false, "Output graph in graphviz format") flTree := cmd.Bool([]string{"#t", "#tree", "#-tree"}, false, "Output graph in tree format") + help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") flFilter := opts.NewListOpts(nil) cmd.Var(&flFilter, []string{"f", "-filter"}, "Provide filter values (i.e. 'dangling=true')") @@ -1345,10 +1414,13 @@ func (cli *DockerCli) CmdImages(args ...string) error { if err := cmd.Parse(args); err != nil { return nil } - if cmd.NArg() > 1 { + if *help { cmd.Usage() return nil } + if cmd.BadArgs(flag.Max, 1) { + os.Exit(1) + } // Consolidate all filter flags, and sanity check them early. // They'll get process in the daemon/server. @@ -1578,6 +1650,7 @@ func (cli *DockerCli) CmdPs(args ...string) error { quiet = cmd.Bool([]string{"q", "-quiet"}, false, "Only display numeric IDs") size = cmd.Bool([]string{"s", "-size"}, false, "Display total file sizes") all = cmd.Bool([]string{"a", "-all"}, false, "Show all containers. Only running containers are shown by default.") + help = cmd.Bool([]string{"#help", "-help"}, false, "Print usage") noTrunc = cmd.Bool([]string{"#notrunc", "-no-trunc"}, false, "Don't truncate output") nLatest = cmd.Bool([]string{"l", "-latest"}, false, "Show only the latest created container, include non-running ones.") since = cmd.String([]string{"#sinceId", "#-since-id", "-since"}, "", "Show only containers created since Id or Name, include non-running ones.") @@ -1591,7 +1664,10 @@ func (cli *DockerCli) CmdPs(args ...string) error { if err := cmd.Parse(args); err != nil { return nil } - + if *help { + cmd.Usage() + return nil + } if *last == -1 && *nLatest { *last = 1 } @@ -1732,20 +1808,28 @@ func (cli *DockerCli) CmdCommit(args ...string) error { flAuthor := cmd.String([]string{"a", "#author", "-author"}, "", "Author (e.g., \"John Hannibal Smith \")") // FIXME: --run is deprecated, it will be replaced with inline Dockerfile commands. flConfig := cmd.String([]string{"#run", "#-run"}, "", "This option is deprecated and will be removed in a future version in favor of inline Dockerfile-compatible commands") + help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") if err := cmd.Parse(args); err != nil { return nil } + if *help { + cmd.Usage() + return nil + } + + if cmd.BadArgs(flag.Max, 2) { + os.Exit(1) + } + + if cmd.BadArgs(flag.Min, 1) { + os.Exit(1) + } var ( name = cmd.Arg(0) repository, tag = parsers.ParseRepositoryTag(cmd.Arg(1)) ) - if name == "" || len(cmd.Args()) > 2 { - cmd.Usage() - return nil - } - //Check if the given image name can be resolved if repository != "" { if _, _, err := registry.ResolveRepositoryName(repository); err != nil { @@ -1790,18 +1874,21 @@ func (cli *DockerCli) CmdEvents(args ...string) error { cmd := cli.Subcmd("events", "", "Get real time events from the server") since := cmd.String([]string{"#since", "-since"}, "", "Show all events created since timestamp") until := cmd.String([]string{"-until"}, "", "Stream events until this timestamp") - flFilter := opts.NewListOpts(nil) cmd.Var(&flFilter, []string{"f", "-filter"}, "Provide filter values (i.e. 'event=stop')") + help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") if err := cmd.Parse(args); err != nil { return nil } - - if cmd.NArg() != 0 { + if *help { cmd.Usage() return nil } + if cmd.BadArgs(flag.Exact, 0) { + os.Exit(1) + } + var ( v = url.Values{} loc = time.FixedZone(time.Now().Zone()) @@ -1849,14 +1936,18 @@ func (cli *DockerCli) CmdEvents(args ...string) error { func (cli *DockerCli) CmdExport(args ...string) error { cmd := cli.Subcmd("export", "CONTAINER", "Export the contents of a filesystem as a tar archive to STDOUT") + help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") + if err := cmd.Parse(args); err != nil { return nil } - - if cmd.NArg() != 1 { + if *help { cmd.Usage() return nil } + if cmd.BadArgs(flag.Exact, 1) { + os.Exit(1) + } if err := cli.stream("GET", "/containers/"+cmd.Arg(0)+"/export", nil, cli.out, nil); err != nil { return err @@ -1866,13 +1957,18 @@ func (cli *DockerCli) CmdExport(args ...string) error { func (cli *DockerCli) CmdDiff(args ...string) error { cmd := cli.Subcmd("diff", "CONTAINER", "Inspect changes on a container's filesystem") + help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") + if err := cmd.Parse(args); err != nil { return nil } - if cmd.NArg() != 1 { + if *help { cmd.Usage() return nil } + if cmd.BadArgs(flag.Exact, 1) { + os.Exit(1) + } body, _, err := readBody(cli.call("GET", "/containers/"+cmd.Arg(0)+"/changes", nil, false)) @@ -1905,16 +2001,20 @@ func (cli *DockerCli) CmdLogs(args ...string) error { follow = cmd.Bool([]string{"f", "-follow"}, false, "Follow log output") times = cmd.Bool([]string{"t", "-timestamps"}, false, "Show timestamps") tail = cmd.String([]string{"-tail"}, "all", "Output the specified number of lines at the end of logs (defaults to all logs)") + help = cmd.Bool([]string{"#help", "-help"}, false, "Print usage") ) if err := cmd.Parse(args); err != nil { return nil } - if cmd.NArg() != 1 { + if *help { cmd.Usage() return nil } + if cmd.BadArgs(flag.Exact, 1) { + os.Exit(1) + } name := cmd.Arg(0) stream, _, err := cli.call("GET", "/containers/"+name+"/json", nil, false) @@ -1948,16 +2048,19 @@ func (cli *DockerCli) CmdAttach(args ...string) error { cmd = cli.Subcmd("attach", "CONTAINER", "Attach to a running container") noStdin = cmd.Bool([]string{"#nostdin", "-no-stdin"}, false, "Do not attach STDIN") proxy = cmd.Bool([]string{"#sig-proxy", "-sig-proxy"}, true, "Proxy all received signals to the process (non-TTY mode only). SIGCHLD, SIGKILL, and SIGSTOP are not proxied.") + help = cmd.Bool([]string{"#help", "-help"}, false, "Print usage") ) if err := cmd.Parse(args); err != nil { return nil } - - if cmd.NArg() != 1 { + if *help { cmd.Usage() return nil } + if cmd.BadArgs(flag.Exact, 1) { + os.Exit(1) + } name := cmd.Arg(0) stream, _, err := cli.call("GET", "/containers/"+name+"/json", nil, false) @@ -2027,13 +2130,18 @@ func (cli *DockerCli) CmdSearch(args ...string) error { trusted := cmd.Bool([]string{"#t", "#trusted", "#-trusted"}, false, "Only show trusted builds") automated := cmd.Bool([]string{"-automated"}, false, "Only show automated builds") stars := cmd.Int([]string{"s", "#stars", "-stars"}, 0, "Only displays with at least x stars") + help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") + if err := cmd.Parse(args); err != nil { return nil } - if cmd.NArg() != 1 { + if *help { cmd.Usage() return nil } + if cmd.BadArgs(flag.Exact, 1) { + os.Exit(1) + } v := url.Values{} v.Set("term", cmd.Arg(0)) @@ -2079,13 +2187,18 @@ type ports []int func (cli *DockerCli) CmdTag(args ...string) error { cmd := cli.Subcmd("tag", "IMAGE[:TAG] [REGISTRYHOST/][USERNAME/]NAME[:TAG]", "Tag an image into a repository") force := cmd.Bool([]string{"f", "#force", "-force"}, false, "Force") + help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") + if err := cmd.Parse(args); err != nil { return nil } - if cmd.NArg() != 2 { + if *help { cmd.Usage() return nil } + if cmd.BadArgs(flag.Exact, 2) { + os.Exit(1) + } var ( repository, tag = parsers.ParseRepositoryTag(cmd.Arg(1)) @@ -2158,6 +2271,7 @@ func newCIDFile(path string) (*cidFile, error) { if _, err := os.Stat(path); err == nil { return nil, fmt.Errorf("Container ID file found, make sure the other container isn't running or delete %s", path) } + f, err := os.Create(path) if err != nil { return nil, fmt.Errorf("Failed to create the container ID file: %s", err) @@ -2474,14 +2588,18 @@ func (cli *DockerCli) CmdRun(args ...string) error { func (cli *DockerCli) CmdCp(args ...string) error { cmd := cli.Subcmd("cp", "CONTAINER:PATH HOSTPATH", "Copy files/folders from the PATH to the HOSTPATH") + help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") + if err := cmd.Parse(args); err != nil { return nil } - - if cmd.NArg() != 2 { + if *help { cmd.Usage() return nil } + if cmd.BadArgs(flag.Exact, 2) { + os.Exit(1) + } var copyData engine.Env info := strings.Split(cmd.Arg(0), ":") @@ -2514,16 +2632,20 @@ func (cli *DockerCli) CmdCp(args ...string) error { func (cli *DockerCli) CmdSave(args ...string) error { cmd := cli.Subcmd("save", "IMAGE [IMAGE...]", "Save an image(s) to a tar archive (streamed to STDOUT by default)") - outfile := cmd.String([]string{"o", "-output"}, "", "Write to a file, instead of STDOUT") + help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") + outfile := cmd.String([]string{"o", "-output"}, "", "Write to an file, instead of STDOUT") if err := cmd.Parse(args); err != nil { return err } - if cmd.NArg() < 1 { + if *help { cmd.Usage() return nil } + if cmd.BadArgs(flag.Min, 1) { + os.Exit(1) + } var ( output io.Writer = cli.out @@ -2558,15 +2680,18 @@ func (cli *DockerCli) CmdSave(args ...string) error { func (cli *DockerCli) CmdLoad(args ...string) error { cmd := cli.Subcmd("load", "", "Load an image from a tar archive on STDIN") infile := cmd.String([]string{"i", "-input"}, "", "Read from a tar archive file, instead of STDIN") + help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") if err := cmd.Parse(args); err != nil { return err } - - if cmd.NArg() != 0 { + if *help { cmd.Usage() return nil } + if cmd.BadArgs(flag.Exact, 0) { + os.Exit(1) + } var ( input io.Reader = cli.in @@ -2588,14 +2713,9 @@ func (cli *DockerCli) CmdExec(args ...string) error { cmd := cli.Subcmd("exec", "CONTAINER COMMAND [ARG...]", "Run a command in a running container") execConfig, err := runconfig.ParseExec(cmd, args) - if err != nil { - cmd.Usage() + if execConfig.Container == "" || err != nil { return err } - if execConfig.Container == "" { - cmd.Usage() - return nil - } stream, _, err := cli.call("POST", "/containers/"+execConfig.Container+"/exec", execConfig, false) if err != nil { diff --git a/docker/flags.go b/docker/flags.go index 6601b4fe8..d6c9f3c19 100644 --- a/docker/flags.go +++ b/docker/flags.go @@ -36,6 +36,7 @@ var ( flLogLevel = flag.String([]string{"l", "-log-level"}, "info", "Set the logging level") flEnableCors = flag.Bool([]string{"#api-enable-cors", "-api-enable-cors"}, false, "Enable CORS headers in the remote API") flTls = flag.Bool([]string{"-tls"}, false, "Use TLS; implied by --tlsverify flag") + flHelp = flag.Bool([]string{"h", "-help"}, false, "Print usage") flTlsVerify = flag.Bool([]string{"-tlsverify"}, dockerTlsVerify, "Use TLS and verify the remote (daemon: verify client, client: verify daemon)") // these are initialized in init() below since their default values depend on dockerCertPath which isn't fully initialized until init() runs @@ -57,8 +58,9 @@ func init() { opts.HostListVar(&flHosts, []string{"H", "-host"}, "The socket(s) to bind to in daemon mode or connect to in client mode, specified using one or more tcp://host:port, unix:///path/to/socket, fd://* or fd://socketfd.") flag.Usage = func() { - fmt.Fprint(os.Stderr, "Usage: docker [OPTIONS] COMMAND [arg...]\n\nA self-sufficient runtime for linux containers.\n\nOptions:\n") + fmt.Fprint(os.Stdout, "Usage: docker [OPTIONS] COMMAND [arg...]\n\nA self-sufficient runtime for linux containers.\n\nOptions:\n") + flag.CommandLine.SetOutput(os.Stdout) flag.PrintDefaults() help := "\nCommands:\n" @@ -105,6 +107,6 @@ func init() { help += fmt.Sprintf(" %-10.10s%s\n", command[0], command[1]) } help += "\nRun 'docker COMMAND --help' for more information on a command." - fmt.Fprintf(os.Stderr, "%s\n", help) + fmt.Fprintf(os.Stdout, "%s\n", help) } } diff --git a/docs/man/docker-attach.1.md b/docs/man/docker-attach.1.md index 19fbaceb4..0d35a8a5b 100644 --- a/docs/man/docker-attach.1.md +++ b/docs/man/docker-attach.1.md @@ -6,6 +6,7 @@ docker-attach - Attach to a running container # SYNOPSIS **docker attach** +[**--help**]/ [**--no-stdin**[=*false*]] [**--sig-proxy**[=*true*]] CONTAINER @@ -24,6 +25,9 @@ It is forbidden to redirect the standard input of a docker attach command while attaching to a tty-enabled container (i.e.: launched with -t`). # OPTIONS +**--help** + Print usage statement + **--no-stdin**=*true*|*false* Do not attach STDIN. The default is *false*. diff --git a/docs/man/docker-build.1.md b/docs/man/docker-build.1.md index 3fed99640..c5dfa706c 100644 --- a/docs/man/docker-build.1.md +++ b/docs/man/docker-build.1.md @@ -6,6 +6,7 @@ docker-build - Build a new image from the source code at PATH # SYNOPSIS **docker build** +[**--help**] [**--force-rm**[=*false*]] [**--no-cache**[=*false*]] [**-q**|**--quiet**[=*false*]] @@ -36,6 +37,9 @@ as context. **--no-cache**=*true*|*false* Do not use cache when building the image. The default is *false*. +**--help** + Print usage statement + **-q**, **--quiet**=*true*|*false* Suppress the verbose output generated by the containers. The default is *false*. diff --git a/docs/man/docker-commit.1.md b/docs/man/docker-commit.1.md index 0d1d5406c..d7619133d 100644 --- a/docs/man/docker-commit.1.md +++ b/docs/man/docker-commit.1.md @@ -7,6 +7,7 @@ docker-commit - Create a new image from a container's changes # SYNOPSIS **docker commit** [**-a**|**--author**[=*AUTHOR*]] +[**--help**] [**-m**|**--message**[=*MESSAGE*]] [**-p**|**--pause**[=*true*]] CONTAINER [REPOSITORY[:TAG]] @@ -18,6 +19,9 @@ Using an existing container's name or ID you can create a new image. **-a**, **--author**="" Author (e.g., "John Hannibal Smith ") +**--help** + Print usage statement + **-m**, **--message**="" Commit message diff --git a/docs/man/docker-cp.1.md b/docs/man/docker-cp.1.md index dc8f295bb..ac49a47a5 100644 --- a/docs/man/docker-cp.1.md +++ b/docs/man/docker-cp.1.md @@ -6,6 +6,7 @@ docker-cp - Copy files/folders from the PATH to the HOSTPATH # SYNOPSIS **docker cp** +[**--help**] CONTAINER:PATH HOSTPATH # DESCRIPTION @@ -14,7 +15,8 @@ path. Paths are relative to the root of the filesystem. Files can be copied from a running or stopped container. # OPTIONS -There are no available options. +**--help** + Print usage statement # EXAMPLES An important shell script file, created in a bash shell, is copied from diff --git a/docs/man/docker-create.1.md b/docs/man/docker-create.1.md index 96a049672..18e582100 100644 --- a/docs/man/docker-create.1.md +++ b/docs/man/docker-create.1.md @@ -21,6 +21,7 @@ docker-create - Create a new container [**--env-file**[=*[]*]] [**--expose**[=*[]*]] [**-h**|**--hostname**[=*HOSTNAME*]] +[**--help**] [**-i**|**--interactive**[=*false*]] [**--ipc**[=*IPC*]] [**--link**[=*[]*]] @@ -87,6 +88,9 @@ IMAGE [COMMAND] [ARG...] **-h**, **--hostname**="" Container host name +**--help** + Print usage statement + **-i**, **--interactive**=*true*|*false* Keep STDIN open even if not attached. The default is *false*. diff --git a/docs/man/docker-diff.1.md b/docs/man/docker-diff.1.md index acf0911b0..6c6c50253 100644 --- a/docs/man/docker-diff.1.md +++ b/docs/man/docker-diff.1.md @@ -6,6 +6,7 @@ docker-diff - Inspect changes on a container's filesystem # SYNOPSIS **docker diff** +[**--help**] CONTAINER # DESCRIPTION @@ -14,7 +15,8 @@ shortened container ID or the container name set using **docker run --name** option. # OPTIONS -There are no available options. +**--help** + Print usage statement # EXAMPLES Inspect the changes to on a nginx container: diff --git a/docs/man/docker-events.1.md b/docs/man/docker-events.1.md index c88843970..5b056ecbc 100644 --- a/docs/man/docker-events.1.md +++ b/docs/man/docker-events.1.md @@ -6,6 +6,7 @@ docker-events - Get real time events from the server # SYNOPSIS **docker events** +[**--help**] [**--since**[=*SINCE*]] [**--until**[=*UNTIL*]] @@ -23,6 +24,9 @@ and Docker images will report: untag, delete # OPTIONS +**--help** + Print usage statement + **--since**="" Show all events created since timestamp diff --git a/docs/man/docker-exec.1.md b/docs/man/docker-exec.1.md index 3db296ed7..e7554419e 100644 --- a/docs/man/docker-exec.1.md +++ b/docs/man/docker-exec.1.md @@ -7,6 +7,7 @@ docker-exec - Run a command in a running container # SYNOPSIS **docker exec** [**-d**|**--detach**[=*false*]] +[**--help**] [**-i**|**--interactive**[=*false*]] [**-t**|**--tty**[=*false*]] CONTAINER COMMAND [ARG...] @@ -25,6 +26,9 @@ container is unpaused, and then run **-d**, **--detach**=*true*|*false* Detached mode: run command in the background. The default is *false*. +**--help** + Print usage statement + **-i**, **--interactive**=*true*|*false* Keep STDIN open even if not attached. The default is *false*. diff --git a/docs/man/docker-export.1.md b/docs/man/docker-export.1.md index 8fd7834a1..d2b22d221 100644 --- a/docs/man/docker-export.1.md +++ b/docs/man/docker-export.1.md @@ -6,6 +6,7 @@ docker-export - Export the contents of a filesystem as a tar archive to STDOUT # SYNOPSIS **docker export** +[**--help**] CONTAINER # DESCRIPTION @@ -14,7 +15,8 @@ container ID or container name. The output is exported to STDOUT and can be redirected to a tar file. # OPTIONS -There are no available options. +**--help** + Print usage statement # EXAMPLES Export the contents of the container called angry_bell to a tar file diff --git a/docs/man/docker-history.1.md b/docs/man/docker-history.1.md index 65ec9cd17..47350f887 100644 --- a/docs/man/docker-history.1.md +++ b/docs/man/docker-history.1.md @@ -6,6 +6,7 @@ docker-history - Show the history of an image # SYNOPSIS **docker history** +[**--help**] [**--no-trunc**[=*false*]] [**-q**|**--quiet**[=*false*]] IMAGE @@ -15,6 +16,9 @@ IMAGE Show the history of when and how an image was created. # OPTIONS +**--help** + Print usage statement + **--no-trunc**=*true*|*false* Don't truncate output. The default is *false*. diff --git a/docs/man/docker-images.1.md b/docs/man/docker-images.1.md index 6c9e6a60b..b0e8daddd 100644 --- a/docs/man/docker-images.1.md +++ b/docs/man/docker-images.1.md @@ -6,6 +6,7 @@ docker-images - List images # SYNOPSIS **docker images** +[**--help**] [**-a**|**--all**[=*false*]] [**-f**|**--filter**[=*[]*]] [**--no-trunc**[=*false*]] @@ -35,6 +36,9 @@ versions. **-f**, **--filter**=[] Provide filter values (i.e. 'dangling=true') +**--help** + Print usage statement + **--no-trunc**=*true*|*false* Don't truncate output. The default is *false*. diff --git a/docs/man/docker-import.1.md b/docs/man/docker-import.1.md index 2d67b8bc7..974288c72 100644 --- a/docs/man/docker-import.1.md +++ b/docs/man/docker-import.1.md @@ -6,6 +6,7 @@ docker-import - Create an empty filesystem image and import the contents of the # SYNOPSIS **docker import** +[**--help**] URL|- [REPOSITORY[:TAG]] # DESCRIPTION @@ -13,7 +14,8 @@ Create a new filesystem image from the contents of a tarball (`.tar`, `.tar.gz`, `.tgz`, `.bzip`, `.tar.xz`, `.txz`) into it, then optionally tag it. # OPTIONS -There are no available options. +**--help** + Print usage statement # EXAMPLES diff --git a/docs/man/docker-info.1.md b/docs/man/docker-info.1.md index 0547b44b0..346df866a 100644 --- a/docs/man/docker-info.1.md +++ b/docs/man/docker-info.1.md @@ -6,6 +6,7 @@ docker-info - Display system-wide information # SYNOPSIS **docker info** +[**--help**] # DESCRIPTION @@ -20,7 +21,8 @@ allocates a certain amount of data space and meta data space from the space available on the volume where `/var/lib/docker` is mounted. # OPTIONS -There are no available options. +**--help** + Print usage statement # EXAMPLES diff --git a/docs/man/docker-inspect.1.md b/docs/man/docker-inspect.1.md index a52d57c97..8cbef0f91 100644 --- a/docs/man/docker-inspect.1.md +++ b/docs/man/docker-inspect.1.md @@ -6,6 +6,7 @@ docker-inspect - Return low-level information on a container or image # SYNOPSIS **docker inspect** +[**--help**] [**-f**|**--format**[=*FORMAT*]] CONTAINER|IMAGE [CONTAINER|IMAGE...] @@ -17,6 +18,9 @@ array. If a format is specified, the given template will be executed for each result. # OPTIONS +**--help** + Print usage statement + **-f**, **--format**="" Format the output using the given go template. diff --git a/docs/man/docker-kill.1.md b/docs/man/docker-kill.1.md index d1d0ee7ad..cfab3f8e4 100644 --- a/docs/man/docker-kill.1.md +++ b/docs/man/docker-kill.1.md @@ -6,6 +6,7 @@ docker-kill - Kill a running container using SIGKILL or a specified signal # SYNOPSIS **docker kill** +[**--help**] [**-s**|**--signal**[=*"KILL"*]] CONTAINER [CONTAINER...] @@ -15,6 +16,9 @@ The main process inside each container specified will be sent SIGKILL, or any signal specified with option --signal. # OPTIONS +**--help** + Print usage statement + **-s**, **--signal**="KILL" Signal to send to the container diff --git a/docs/man/docker-load.1.md b/docs/man/docker-load.1.md index 07dac4613..71bd28adf 100644 --- a/docs/man/docker-load.1.md +++ b/docs/man/docker-load.1.md @@ -6,6 +6,7 @@ docker-load - Load an image from a tar archive on STDIN # SYNOPSIS **docker load** +[**--help**] [**-i**|**--input**[=*INPUT*]] @@ -15,6 +16,9 @@ Loads a tarred repository from a file or the standard input stream. Restores both images and tags. # OPTIONS +**--help** + Print usage statement + **-i**, **--input**="" Read from a tar archive file, instead of STDIN diff --git a/docs/man/docker-login.1.md b/docs/man/docker-login.1.md index e367050be..5ee6aa1c6 100644 --- a/docs/man/docker-login.1.md +++ b/docs/man/docker-login.1.md @@ -7,6 +7,7 @@ docker-login - Register or log in to a Docker registry server, if no server is s # SYNOPSIS **docker login** [**-e**|**--email**[=*EMAIL*]] +[**--help**] [**-p**|**--password**[=*PASSWORD*]] [**-u**|**--username**[=*USERNAME*]] [SERVER] @@ -20,6 +21,9 @@ login to a private registry you can specify this by adding the server name. **-e**, **--email**="" Email +**--help** + Print usage statement + **-p**, **--password**="" Password diff --git a/docs/man/docker-logs.1.md b/docs/man/docker-logs.1.md index 1fbd229d5..c89652672 100644 --- a/docs/man/docker-logs.1.md +++ b/docs/man/docker-logs.1.md @@ -7,6 +7,7 @@ docker-logs - Fetch the logs of a container # SYNOPSIS **docker logs** [**-f**|**--follow**[=*false*]] +[**--help**] [**-t**|**--timestamps**[=*false*]] [**--tail**[=*"all"*]] CONTAINER @@ -22,6 +23,9 @@ The **docker logs --follow** command combines commands **docker logs** and then continue streaming new output from the container’s stdout and stderr. # OPTIONS +**--help** + Print usage statement + **-f**, **--follow**=*true*|*false* Follow log output. The default is *false*. diff --git a/docs/man/docker-port.1.md b/docs/man/docker-port.1.md index 8c4c870dc..a297c3921 100644 --- a/docs/man/docker-port.1.md +++ b/docs/man/docker-port.1.md @@ -6,13 +6,15 @@ docker-port - List port mappings for the CONTAINER, or lookup the public-facing # SYNOPSIS **docker port** +[**--help**] CONTAINER [PRIVATE_PORT[/PROTO]] # DESCRIPTION List port mappings for the CONTAINER, or lookup the public-facing port that is NAT-ed to the PRIVATE_PORT # OPTIONS -There are no available options. +**--help** + Print usage statement # EXAMPLES You can find out all the ports mapped by not specifying a `PRIVATE_PORT`, or diff --git a/docs/man/docker-ps.1.md b/docs/man/docker-ps.1.md index d34d98396..4c94545e3 100644 --- a/docs/man/docker-ps.1.md +++ b/docs/man/docker-ps.1.md @@ -8,6 +8,7 @@ docker-ps - List containers **docker ps** [**-a**|**--all**[=*false*]] [**--before**[=*BEFORE*]] +[**--help**] [**-f**|**--filter**[=*[]*]] [**-l**|**--latest**[=*false*]] [**-n**[=*-1*]] @@ -29,6 +30,9 @@ the running containers. **--before**="" Show only container created before Id or Name, include non-running ones. +**--help** + Print usage statement + **-f**, **--filter**=[] Provide filter values. Valid filters: exited= - containers with exit code of diff --git a/docs/man/docker-pull.1.md b/docs/man/docker-pull.1.md index 01c664f56..f1963df55 100644 --- a/docs/man/docker-pull.1.md +++ b/docs/man/docker-pull.1.md @@ -7,6 +7,7 @@ docker-pull - Pull an image or a repository from the registry # SYNOPSIS **docker pull** [**-a**|**--all-tags**[=*false*]] +[**--help**] NAME[:TAG] # DESCRIPTION @@ -19,8 +20,10 @@ It is also possible to specify a non-default registry to pull from. # OPTIONS **-a**, **--all-tags**=*true*|*false* Download all tagged images in the repository. The default is *false*. +**--help** + Print usage statement -# EXAMPLES +# EXAMPLE # Pull a repository with multiple images # Note that if the image is previously downloaded then the status would be diff --git a/docs/man/docker-push.1.md b/docs/man/docker-push.1.md index 8523cb539..2d4dc8f89 100644 --- a/docs/man/docker-push.1.md +++ b/docs/man/docker-push.1.md @@ -6,6 +6,7 @@ docker-push - Push an image or a repository to the registry # SYNOPSIS **docker push** +[**--help**] NAME[:TAG] # DESCRIPTION @@ -15,7 +16,8 @@ image can be pushed to another, perhaps private, registry as demonstrated in the example below. # OPTIONS -There are no available options. +**--help** + Print usage statement # EXAMPLES diff --git a/docs/man/docker-restart.1.md b/docs/man/docker-restart.1.md index 9a2268800..77f99d51a 100644 --- a/docs/man/docker-restart.1.md +++ b/docs/man/docker-restart.1.md @@ -6,6 +6,7 @@ docker-restart - Restart a running container # SYNOPSIS **docker restart** +[**--help**] [**-t**|**--time**[=*10*]] CONTAINER [CONTAINER...] @@ -13,6 +14,9 @@ CONTAINER [CONTAINER...] Restart each container listed. # OPTIONS +**--help** + Print usage statement + **-t**, **--time**=10 Number of seconds to try to stop for before killing the container. Once killed it will then be restarted. Default is 10 seconds. diff --git a/docs/man/docker-rm.1.md b/docs/man/docker-rm.1.md index b8f31bd68..82850a395 100644 --- a/docs/man/docker-rm.1.md +++ b/docs/man/docker-rm.1.md @@ -19,6 +19,9 @@ remove a running container unless you use the \fB-f\fR option. To see all containers on a host use the **docker ps -a** command. # OPTIONS +**--help** + Print usage statement + **-f**, **--force**=*true*|*false* Force the removal of a running container (uses SIGKILL). The default is *false*. diff --git a/docs/man/docker-rmi.1.md b/docs/man/docker-rmi.1.md index 08d740a3b..c1f131f40 100644 --- a/docs/man/docker-rmi.1.md +++ b/docs/man/docker-rmi.1.md @@ -7,6 +7,7 @@ docker-rmi - Remove one or more images # SYNOPSIS **docker rmi** [**-f**|**--force**[=*false*]] +[**--help**] [**--no-prune**[=*false*]] IMAGE [IMAGE...] @@ -21,6 +22,9 @@ use the **docker images** command. **-f**, **--force**=*true*|*false* Force removal of the image. The default is *false*. +**--help** + Print usage statement + **--no-prune**=*true*|*false* Do not delete untagged parents. The default is *false*. diff --git a/docs/man/docker-run.1.md b/docs/man/docker-run.1.md index b9571dbe2..6c8bd59c4 100644 --- a/docs/man/docker-run.1.md +++ b/docs/man/docker-run.1.md @@ -22,6 +22,7 @@ docker-run - Run a command in a new container [**--env-file**[=*[]*]] [**--expose**[=*[]*]] [**-h**|**--hostname**[=*HOSTNAME*]] +[**--help**] [**-i**|**--interactive**[=*false*]] [**--ipc**[=*IPC*]] [**--link**[=*[]*]] @@ -153,6 +154,9 @@ ENTRYPOINT. Sets the container host name that is available inside the container. +**--help** + Print usage statement + **-i**, **--interactive**=*true*|*false* Keep STDIN open even if not attached. The default is *false*. diff --git a/docs/man/docker-save.1.md b/docs/man/docker-save.1.md index c02ffb101..987d18b84 100644 --- a/docs/man/docker-save.1.md +++ b/docs/man/docker-save.1.md @@ -6,6 +6,7 @@ docker-save - Save an image(s) to a tar archive (streamed to STDOUT by default) # SYNOPSIS **docker save** +[**--help**] [**-o**|**--output**[=*OUTPUT*]] IMAGE [IMAGE...] @@ -16,6 +17,9 @@ parent layers, and all tags + versions, or specified repo:tag. Stream to a file instead of STDOUT by using **-o**. # OPTIONS +**--help** + Print usage statement + **-o**, **--output**="" Write to a file, instead of STDOUT diff --git a/docs/man/docker-search.1.md b/docs/man/docker-search.1.md index 3937b870a..2d23e34cf 100644 --- a/docs/man/docker-search.1.md +++ b/docs/man/docker-search.1.md @@ -7,6 +7,7 @@ docker-search - Search the Docker Hub for images # SYNOPSIS **docker search** [**--automated**[=*false*]] +[**--help**] [**--no-trunc**[=*false*]] [**-s**|**--stars**[=*0*]] TERM @@ -22,6 +23,9 @@ is automated. **--automated**=*true*|*false* Only show automated builds. The default is *false*. +**--help** + Print usage statement + **--no-trunc**=*true*|*false* Don't truncate output. The default is *false*. diff --git a/docs/man/docker-start.1.md b/docs/man/docker-start.1.md index e23fd70ab..965c5bcaf 100644 --- a/docs/man/docker-start.1.md +++ b/docs/man/docker-start.1.md @@ -7,6 +7,7 @@ docker-start - Restart a stopped container # SYNOPSIS **docker start** [**-a**|**--attach**[=*false*]] +[**--help**] [**-i**|**--interactive**[=*false*]] CONTAINER [CONTAINER...] @@ -18,6 +19,9 @@ Start a stopped container. **-a**, **--attach**=*true*|*false* Attach container's STDOUT and STDERR and forward all signals to the process. The default is *false*. +**--help** + Print usage statement + **-i**, **--interactive**=*true*|*false* Attach container's STDIN. The default is *false*. diff --git a/docs/man/docker-stop.1.md b/docs/man/docker-stop.1.md index 1b73e387e..09972347a 100644 --- a/docs/man/docker-stop.1.md +++ b/docs/man/docker-stop.1.md @@ -6,6 +6,7 @@ docker-stop - Stop a running container by sending SIGTERM and then SIGKILL after # SYNOPSIS **docker stop** +[**--help**] [**-t**|**--time**[=*10*]] CONTAINER [CONTAINER...] @@ -14,6 +15,9 @@ Stop a running container (Send SIGTERM, and then SIGKILL after grace period) # OPTIONS +**--help** + Print usage statement + **-t**, **--time**=10 Number of seconds to wait for the container to stop before killing it. Default is 10 seconds. diff --git a/docs/man/docker-tag.1.md b/docs/man/docker-tag.1.md index e8550ec55..20125e5df 100644 --- a/docs/man/docker-tag.1.md +++ b/docs/man/docker-tag.1.md @@ -7,6 +7,7 @@ docker-tag - Tag an image into a repository # SYNOPSIS **docker tag** [**-f**|**--force**[=*false*]] +[**--help**] IMAGE[:TAG] [REGISTRYHOST/][USERNAME/]NAME[:TAG] # DESCRIPTION diff --git a/docs/man/docker-top.1.md b/docs/man/docker-top.1.md index 9781739cd..be2bed221 100644 --- a/docs/man/docker-top.1.md +++ b/docs/man/docker-top.1.md @@ -6,6 +6,7 @@ docker-top - Display the running processes of a container # SYNOPSIS **docker top** +[**--help**] CONTAINER [ps OPTIONS] # DESCRIPTION @@ -14,7 +15,8 @@ Look up the running process of the container. ps-OPTION can be any of the options you would pass to a Linux ps command. # OPTIONS -There are no available options. +**--help** + Print usage statement # EXAMPLES diff --git a/docs/man/docker-wait.1.md b/docs/man/docker-wait.1.md index 798f6d652..a1e2aa212 100644 --- a/docs/man/docker-wait.1.md +++ b/docs/man/docker-wait.1.md @@ -6,6 +6,7 @@ docker-wait - Block until a container stops, then print its exit code. # SYNOPSIS **docker wait** +[**--help**] CONTAINER [CONTAINER...] # DESCRIPTION @@ -13,7 +14,8 @@ CONTAINER [CONTAINER...] Block until a container stops, then print its exit code. # OPTIONS -There are no available options. +**--help** + Print usage statement # EXAMPLES diff --git a/docs/man/docker.1.md b/docs/man/docker.1.md index e07687c18..f3bcdb671 100644 --- a/docs/man/docker.1.md +++ b/docs/man/docker.1.md @@ -26,6 +26,9 @@ To see the man page for a command run **man docker **. **-D**=*true*|*false* Enable debug mode. Default is false. +**--help** + Print usage statement + **-H**, **--host**=[unix:///var/run/docker.sock]: tcp://[host:port] to bind or unix://[/path/to/socket] to use. The socket(s) to bind to in daemon mode specified using one or more diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index e48a393b7..8d10bcf9c 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -15,6 +15,19 @@ or execute `docker help`: ... +## Help +To list the help on any command just execute the command, followed by the `--help` option. + + $ sudo docker run --help + + Usage: docker run [OPTIONS] IMAGE [COMMAND] [ARG...] + + Run a command in a new container + + -a, --attach=[] Attach to STDIN, STDOUT or STDERR. + -c, --cpu-shares=0 CPU shares (relative weight) + ... + ## Option types Single character commandline options can be combined, so rather than diff --git a/pkg/mflag/flag.go b/pkg/mflag/flag.go index a30c41b04..ffc509a59 100644 --- a/pkg/mflag/flag.go +++ b/pkg/mflag/flag.go @@ -410,6 +410,47 @@ func IsSet(name string) bool { return CommandLine.IsSet(name) } +// Indicator used to pass to BadArgs function +const ( + Exact = 1 + Max = 2 + Min = 3 +) + +// Bad Args takes two arguments. +// The first one indicates whether the number of arguments should, be +// A Minimal number of arguments, a maximum number of arguments or +// The exact number of arguments required +// If the actuall number of arguments is not valid and error message +// prints and true is returned, otherwise false is returned +func (f *FlagSet) BadArgs(arg_type, nargs int) bool { + if arg_type == Max && f.NArg() > nargs { + if nargs == 1 { + fmt.Fprintf(f.out(), "docker: '%s' requires a maximum of 1 argument. See 'docker %s --help'.\n", f.name, f.name) + } else { + fmt.Fprintf(f.out(), "docker: '%s' requires a maximum of %d arguments. See 'docker %s --help'.\n", f.name, nargs, f.name) + } + return true + } + if arg_type == Exact && f.NArg() != nargs { + if nargs == 1 { + fmt.Fprintf(f.out(), "docker: '%s' requires 1 argument. See 'docker %s --help'.\n", f.name, f.name) + } else { + fmt.Fprintf(f.out(), "docker: '%s' requires %d arguments. See 'docker %s --help'.\n", f.name, nargs, f.name) + } + return true + } + if arg_type == Min && f.NArg() < nargs { + if nargs == 1 { + fmt.Fprintf(f.out(), "docker: '%s' requires a minimum of 1 argument. See 'docker %s --help'.\n", f.name, f.name) + } else { + fmt.Fprintf(f.out(), "docker: '%s' requires a minimum of %d arguments. See 'docker %s --help'.\n", f.name, nargs, f.name) + } + return true + } + return false +} + // Set sets the value of the named flag. func (f *FlagSet) Set(name, value string) error { flag, ok := f.formal[name] @@ -483,7 +524,7 @@ func defaultUsage(f *FlagSet) { // Usage prints to standard error a usage message documenting all defined command-line flags. // The function is a variable that may be changed to point to a custom function. var Usage = func() { - fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) + fmt.Fprintf(CommandLine.output, "Usage of %s:\n", os.Args[0]) PrintDefaults() } @@ -789,7 +830,7 @@ func Var(value Value, names []string, usage string) { func (f *FlagSet) failf(format string, a ...interface{}) error { err := fmt.Errorf(format, a...) fmt.Fprintln(f.out(), err) - f.usage() + fmt.Fprintf(f.out(), "See 'docker %s --help'.\n", f.name) return err } diff --git a/runconfig/exec.go b/runconfig/exec.go index 1ced70a86..4e23481e5 100644 --- a/runconfig/exec.go +++ b/runconfig/exec.go @@ -5,6 +5,7 @@ import ( "github.com/docker/docker/engine" flag "github.com/docker/docker/pkg/mflag" + "os" ) type ExecConfig struct { @@ -45,17 +46,22 @@ func ParseExec(cmd *flag.FlagSet, args []string) (*ExecConfig, error) { flStdin = cmd.Bool([]string{"i", "-interactive"}, false, "Keep STDIN open even if not attached") flTty = cmd.Bool([]string{"t", "-tty"}, false, "Allocate a pseudo-TTY") flDetach = cmd.Bool([]string{"d", "-detach"}, false, "Detached mode: run command in the background") + help = cmd.Bool([]string{"#help", "-help"}, false, "Print usage") execCmd []string container string ) if err := cmd.Parse(args); err != nil { return nil, err } - parsedArgs := cmd.Args() - if len(parsedArgs) < 2 { - return nil, fmt.Errorf("not enough arguments to create exec command") + if *help { + cmd.Usage() + return nil, nil + } + if cmd.BadArgs(flag.Min, 2) { + os.Exit(1) } container = cmd.Arg(0) + parsedArgs := cmd.Args() execCmd = parsedArgs[1:] execConfig := &ExecConfig{ diff --git a/runconfig/parse.go b/runconfig/parse.go index 5c684e346..d2e4c7384 100644 --- a/runconfig/parse.go +++ b/runconfig/parse.go @@ -2,6 +2,7 @@ package runconfig import ( "fmt" + "os" "path" "strconv" "strings" @@ -61,6 +62,7 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe flMacAddress = cmd.String([]string{"-mac-address"}, "", "Container MAC address (e.g. 92:d0:c6:0a:29:33)") flIpcMode = cmd.String([]string{"-ipc"}, "", "Default is to create a private IPC namespace (POSIX SysV IPC) for the container\n'container:': reuses another container shared memory, semaphores and message queues\n'host': use the host shared memory,semaphores and message queues inside the container. Note: the host mode gives the container full access to local shared memory and is therefore considered insecure.") flRestartPolicy = cmd.String([]string{"-restart"}, "", "Restart policy to apply when a container exits (no, on-failure[:max-retry], always)") + help = cmd.Bool([]string{"#help", "-help"}, false, "Print usage") ) cmd.Var(&flAttach, []string{"a", "-attach"}, "Attach to STDIN, STDOUT or STDERR.") @@ -86,6 +88,13 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe if err := cmd.Parse(args); err != nil { return nil, nil, cmd, err } + if *help { + cmd.Usage() + return nil, nil, cmd, nil + } + if cmd.BadArgs(flag.Min, 1) { + os.Exit(1) + } // Validate input params if *flWorkingDir != "" && !path.IsAbs(*flWorkingDir) { @@ -156,11 +165,8 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe parsedArgs = cmd.Args() runCmd []string entrypoint []string - image string + image = cmd.Arg(0) ) - if len(parsedArgs) >= 1 { - image = cmd.Arg(0) - } if len(parsedArgs) > 1 { runCmd = parsedArgs[1:] } From 4dc962d09a05b5422c692f4762a32c24cb506e6b Mon Sep 17 00:00:00 2001 From: Dan Walsh Date: Thu, 4 Sep 2014 16:49:01 -0400 Subject: [PATCH 174/513] Remove TestUsage, since Usage will no longer be shown on failure to parse Docker-DCO-1.1-Signed-off-by: Dan Walsh (github: rhatdan) --- pkg/mflag/flag_test.go | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/pkg/mflag/flag_test.go b/pkg/mflag/flag_test.go index 622e8a9bf..b8c0b305d 100644 --- a/pkg/mflag/flag_test.go +++ b/pkg/mflag/flag_test.go @@ -151,17 +151,6 @@ func TestGet(t *testing.T) { VisitAll(visitor) } -func TestUsage(t *testing.T) { - called := false - ResetForTesting(func() { called = true }) - if CommandLine.Parse([]string{"-x"}) == nil { - t.Error("parse did not fail for unknown flag") - } - if !called { - t.Error("did not call Usage for unknown flag") - } -} - func testParse(f *FlagSet, t *testing.T) { if f.Parsed() { t.Error("f.Parse() = true before Parse") From 41be2f73c7ce2cbb5450ee8bb35bf7235125ca63 Mon Sep 17 00:00:00 2001 From: Tibor Vass Date: Wed, 5 Nov 2014 11:57:51 -0500 Subject: [PATCH 175/513] refactor redundant code around calls to cmd.Parse Signed-off-by: Tibor Vass --- api/client/commands.go | 355 +++++++++-------------------------------- pkg/mflag/flag.go | 114 +++++++------ runconfig/exec.go | 13 +- runconfig/parse.go | 13 +- utils/flags.go | 34 ++++ 5 files changed, 184 insertions(+), 345 deletions(-) create mode 100644 utils/flags.go diff --git a/api/client/commands.go b/api/client/commands.go index 14bcc8ffa..cfbedc835 100644 --- a/api/client/commands.go +++ b/api/client/commands.go @@ -85,18 +85,11 @@ func (cli *DockerCli) CmdBuild(args ...string) error { rm := cmd.Bool([]string{"#rm", "-rm"}, true, "Remove intermediate containers after a successful build") forceRm := cmd.Bool([]string{"-force-rm"}, false, "Always remove intermediate containers, even after unsuccessful builds") pull := cmd.Bool([]string{"-pull"}, false, "Always attempt to pull a newer version of the image") - help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") + cmd.Require(flag.Exact, 1) - if err := cmd.Parse(args); err != nil { + if err := utils.ParseFlags(cmd, args, true); err != nil { return nil } - if *help { - cmd.Usage() - return nil - } - if cmd.BadArgs(flag.Exact, 1) { - os.Exit(1) - } var ( context archive.Archive @@ -255,22 +248,18 @@ func (cli *DockerCli) CmdBuild(args ...string) error { // 'docker login': login / register a user to registry service. func (cli *DockerCli) CmdLogin(args ...string) error { cmd := cli.Subcmd("login", "[SERVER]", "Register or log in to a Docker registry server, if no server is specified \""+registry.IndexServerAddress()+"\" is the default.") + cmd.Require(flag.Max, 1) var username, password, email string cmd.StringVar(&username, []string{"u", "-username"}, "", "Username") cmd.StringVar(&password, []string{"p", "-password"}, "", "Password") cmd.StringVar(&email, []string{"e", "-email"}, "", "Email") - help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") - err := cmd.Parse(args) - if err != nil { - return nil - } - if *help { - cmd.Usage() + if err := utils.ParseFlags(cmd, args, true); err != nil { return nil } + serverAddress := registry.IndexServerAddress() if len(cmd.Args()) > 0 { serverAddress = cmd.Arg(0) @@ -377,8 +366,9 @@ func (cli *DockerCli) CmdLogin(args ...string) error { // log out from a Docker registry func (cli *DockerCli) CmdLogout(args ...string) error { cmd := cli.Subcmd("logout", "[SERVER]", "Log out from a Docker registry, if no server is specified \""+registry.IndexServerAddress()+"\" is the default.") + cmd.Require(flag.Max, 1) - if err := cmd.Parse(args); err != nil { + if err := utils.ParseFlags(cmd, args, false); err != nil { return nil } serverAddress := registry.IndexServerAddress() @@ -403,18 +393,12 @@ func (cli *DockerCli) CmdLogout(args ...string) error { // 'docker wait': block until a container stops func (cli *DockerCli) CmdWait(args ...string) error { cmd := cli.Subcmd("wait", "CONTAINER [CONTAINER...]", "Block until a container stops, then print its exit code.") - help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") + cmd.Require(flag.Min, 1) - if err := cmd.Parse(args); err != nil { + if err := utils.ParseFlags(cmd, args, true); err != nil { return nil } - if *help { - cmd.Usage() - return nil - } - if cmd.BadArgs(flag.Min, 1) { - os.Exit(1) - } + var encounteredError error for _, name := range cmd.Args() { status, err := waitForExit(cli, name) @@ -431,12 +415,12 @@ func (cli *DockerCli) CmdWait(args ...string) error { // 'docker version': show version information func (cli *DockerCli) CmdVersion(args ...string) error { cmd := cli.Subcmd("version", "", "Show the Docker version information.") - if err := cmd.Parse(args); err != nil { + cmd.Require(flag.Exact, 0) + + if err := utils.ParseFlags(cmd, args, false); err != nil { return nil } - if cmd.BadArgs(flag.Exact, 0) { - os.Exit(1) - } + if dockerversion.VERSION != "" { fmt.Fprintf(cli.out, "Client version: %s\n", dockerversion.VERSION) } @@ -475,12 +459,10 @@ func (cli *DockerCli) CmdVersion(args ...string) error { // 'docker info': display system-wide information. func (cli *DockerCli) CmdInfo(args ...string) error { cmd := cli.Subcmd("info", "", "Display system-wide information") - if err := cmd.Parse(args); err != nil { + cmd.Require(flag.Exact, 0) + if err := utils.ParseFlags(cmd, args, false); err != nil { return nil } - if cmd.BadArgs(flag.Exact, 0) { - os.Exit(1) - } body, _, err := readBody(cli.call("GET", "/info", nil, false)) if err != nil { @@ -594,18 +576,11 @@ func (cli *DockerCli) CmdInfo(args ...string) error { func (cli *DockerCli) CmdStop(args ...string) error { cmd := cli.Subcmd("stop", "CONTAINER [CONTAINER...]", "Stop a running container by sending SIGTERM and then SIGKILL after a grace period") nSeconds := cmd.Int([]string{"t", "-time"}, 10, "Number of seconds to wait for the container to stop before killing it. Default is 10 seconds.") - help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") + cmd.Require(flag.Min, 1) - if err := cmd.Parse(args); err != nil { + if err := utils.ParseFlags(cmd, args, true); err != nil { return nil } - if *help { - cmd.Usage() - return nil - } - if cmd.BadArgs(flag.Min, 1) { - os.Exit(1) - } v := url.Values{} v.Set("t", strconv.Itoa(*nSeconds)) @@ -626,18 +601,11 @@ func (cli *DockerCli) CmdStop(args ...string) error { func (cli *DockerCli) CmdRestart(args ...string) error { cmd := cli.Subcmd("restart", "CONTAINER [CONTAINER...]", "Restart a running container") nSeconds := cmd.Int([]string{"t", "-time"}, 10, "Number of seconds to try to stop for before killing the container. Once killed it will then be restarted. Default is 10 seconds.") - help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") + cmd.Require(flag.Min, 1) - if err := cmd.Parse(args); err != nil { + if err := utils.ParseFlags(cmd, args, true); err != nil { return nil } - if *help { - cmd.Usage() - return nil - } - if cmd.BadArgs(flag.Min, 1) { - os.Exit(1) - } v := url.Values{} v.Set("t", strconv.Itoa(*nSeconds)) @@ -689,19 +657,12 @@ func (cli *DockerCli) CmdStart(args ...string) error { cmd = cli.Subcmd("start", "CONTAINER [CONTAINER...]", "Restart a stopped container") attach = cmd.Bool([]string{"a", "-attach"}, false, "Attach container's STDOUT and STDERR and forward all signals to the process") openStdin = cmd.Bool([]string{"i", "-interactive"}, false, "Attach container's STDIN") - help = cmd.Bool([]string{"#help", "-help"}, false, "Print usage") ) - if err := cmd.Parse(args); err != nil { + cmd.Require(flag.Min, 1) + if err := utils.ParseFlags(cmd, args, true); err != nil { return nil } - if *help { - cmd.Usage() - return nil - } - if cmd.BadArgs(flag.Min, 1) { - os.Exit(1) - } hijacked := make(chan io.Closer) @@ -804,12 +765,10 @@ func (cli *DockerCli) CmdStart(args ...string) error { func (cli *DockerCli) CmdUnpause(args ...string) error { cmd := cli.Subcmd("unpause", "CONTAINER", "Unpause all processes within a container") - if err := cmd.Parse(args); err != nil { + cmd.Require(flag.Exact, 1) + if err := utils.ParseFlags(cmd, args, false); err != nil { return nil } - if cmd.BadArgs(flag.Exact, 1) { - os.Exit(1) - } var encounteredError error for _, name := range cmd.Args() { @@ -825,12 +784,10 @@ func (cli *DockerCli) CmdUnpause(args ...string) error { func (cli *DockerCli) CmdPause(args ...string) error { cmd := cli.Subcmd("pause", "CONTAINER", "Pause all processes within a container") - if err := cmd.Parse(args); err != nil { + cmd.Require(flag.Exact, 1) + if err := utils.ParseFlags(cmd, args, false); err != nil { return nil } - if cmd.BadArgs(flag.Exact, 1) { - os.Exit(1) - } var encounteredError error for _, name := range cmd.Args() { @@ -847,18 +804,11 @@ func (cli *DockerCli) CmdPause(args ...string) error { func (cli *DockerCli) CmdInspect(args ...string) error { cmd := cli.Subcmd("inspect", "CONTAINER|IMAGE [CONTAINER|IMAGE...]", "Return low-level information on a container or image") tmplStr := cmd.String([]string{"f", "#format", "-format"}, "", "Format the output using the given go template.") - help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") + cmd.Require(flag.Min, 1) - if err := cmd.Parse(args); err != nil { + if err := utils.ParseFlags(cmd, args, true); err != nil { return nil } - if *help { - cmd.Usage() - return nil - } - if cmd.BadArgs(flag.Min, 1) { - os.Exit(1) - } var tmpl *template.Template if *tmplStr != "" { @@ -931,18 +881,12 @@ func (cli *DockerCli) CmdInspect(args ...string) error { func (cli *DockerCli) CmdTop(args ...string) error { cmd := cli.Subcmd("top", "CONTAINER [ps OPTIONS]", "Display the running processes of a container") - help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") + cmd.Require(flag.Min, 1) - if err := cmd.Parse(args); err != nil { + if err := utils.ParseFlags(cmd, args, true); err != nil { return nil } - if *help { - cmd.Usage() - return nil - } - if cmd.BadArgs(flag.Min, 1) { - os.Exit(1) - } + val := url.Values{} if cmd.NArg() > 1 { val.Set("ps_args", strings.Join(cmd.Args()[1:], " ")) @@ -971,17 +915,10 @@ func (cli *DockerCli) CmdTop(args ...string) error { func (cli *DockerCli) CmdPort(args ...string) error { cmd := cli.Subcmd("port", "CONTAINER [PRIVATE_PORT[/PROTO]]", "List port mappings for the CONTAINER, or lookup the public-facing port that is NAT-ed to the PRIVATE_PORT") - help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") - if err := cmd.Parse(args); err != nil { + cmd.Require(flag.Min, 1) + if err := utils.ParseFlags(cmd, args, true); err != nil { return nil } - if *help { - cmd.Usage() - return nil - } - if cmd.BadArgs(flag.Min, 1) { - os.Exit(1) - } stream, _, err := cli.call("GET", "/containers/"+cmd.Arg(0)+"/json", nil, false) if err != nil { @@ -1034,18 +971,11 @@ func (cli *DockerCli) CmdRmi(args ...string) error { force = cmd.Bool([]string{"f", "-force"}, false, "Force removal of the image") noprune = cmd.Bool([]string{"-no-prune"}, false, "Do not delete untagged parents") ) - help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") + cmd.Require(flag.Min, 1) - if err := cmd.Parse(args); err != nil { + if err := utils.ParseFlags(cmd, args, true); err != nil { return nil } - if *help { - cmd.Usage() - return nil - } - if cmd.BadArgs(flag.Min, 1) { - os.Exit(1) - } v := url.Values{} if *force { @@ -1084,18 +1014,11 @@ func (cli *DockerCli) CmdHistory(args ...string) error { cmd := cli.Subcmd("history", "IMAGE", "Show the history of an image") quiet := cmd.Bool([]string{"q", "-quiet"}, false, "Only show numeric IDs") noTrunc := cmd.Bool([]string{"#notrunc", "-no-trunc"}, false, "Don't truncate output") - help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") + cmd.Require(flag.Exact, 1) - if err := cmd.Parse(args); err != nil { + if err := utils.ParseFlags(cmd, args, true); err != nil { return nil } - if *help { - cmd.Usage() - return nil - } - if cmd.BadArgs(flag.Exact, 1) { - os.Exit(1) - } body, _, err := readBody(cli.call("GET", "/images/"+cmd.Arg(0)+"/history", nil, false)) if err != nil { @@ -1146,18 +1069,11 @@ func (cli *DockerCli) CmdRm(args ...string) error { v := cmd.Bool([]string{"v", "-volumes"}, false, "Remove the volumes associated with the container") link := cmd.Bool([]string{"l", "#link", "-link"}, false, "Remove the specified link and not the underlying container") force := cmd.Bool([]string{"f", "-force"}, false, "Force the removal of a running container (uses SIGKILL)") - help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") + cmd.Require(flag.Min, 1) - if err := cmd.Parse(args); err != nil { + if err := utils.ParseFlags(cmd, args, true); err != nil { return nil } - if *help { - cmd.Usage() - return nil - } - if cmd.BadArgs(flag.Min, 1) { - os.Exit(1) - } val := url.Values{} if *v { @@ -1188,18 +1104,11 @@ func (cli *DockerCli) CmdRm(args ...string) error { func (cli *DockerCli) CmdKill(args ...string) error { cmd := cli.Subcmd("kill", "CONTAINER [CONTAINER...]", "Kill a running container using SIGKILL or a specified signal") signal := cmd.String([]string{"s", "-signal"}, "KILL", "Signal to send to the container") - help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") + cmd.Require(flag.Min, 1) - if err := cmd.Parse(args); err != nil { + if err := utils.ParseFlags(cmd, args, true); err != nil { return nil } - if *help { - cmd.Usage() - return nil - } - if cmd.BadArgs(flag.Min, 1) { - os.Exit(1) - } var encounteredError error for _, name := range cmd.Args() { @@ -1215,18 +1124,12 @@ func (cli *DockerCli) CmdKill(args ...string) error { func (cli *DockerCli) CmdImport(args ...string) error { cmd := cli.Subcmd("import", "URL|- [REPOSITORY[:TAG]]", "Create an empty filesystem image and import the contents of the tarball (.tar, .tar.gz, .tgz, .bzip, .tar.xz, .txz) into it, then optionally tag it.") - help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") + cmd.Require(flag.Min, 1) - if err := cmd.Parse(args); err != nil { + if err := utils.ParseFlags(cmd, args, true); err != nil { return nil } - if *help { - cmd.Usage() - return nil - } - if cmd.BadArgs(flag.Min, 1) { - os.Exit(1) - } + var ( v = url.Values{} src = cmd.Arg(0) @@ -1260,18 +1163,12 @@ func (cli *DockerCli) CmdImport(args ...string) error { func (cli *DockerCli) CmdPush(args ...string) error { cmd := cli.Subcmd("push", "NAME[:TAG]", "Push an image or a repository to the registry") - help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") + cmd.Require(flag.Exact, 1) - if err := cmd.Parse(args); err != nil { + if err := utils.ParseFlags(cmd, args, true); err != nil { return nil } - if *help { - cmd.Usage() - return nil - } - if cmd.BadArgs(flag.Exact, 1) { - os.Exit(1) - } + name := cmd.Arg(0) cli.LoadConfigFile() @@ -1330,19 +1227,12 @@ func (cli *DockerCli) CmdPush(args ...string) error { func (cli *DockerCli) CmdPull(args ...string) error { cmd := cli.Subcmd("pull", "NAME[:TAG]", "Pull an image or a repository from the registry") allTags := cmd.Bool([]string{"a", "-all-tags"}, false, "Download all tagged images in the repository") - help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") + cmd.Require(flag.Exact, 1) - if err := cmd.Parse(args); err != nil { - return nil - } - if *help { - cmd.Usage() + if err := utils.ParseFlags(cmd, args, true); err != nil { return nil } - if cmd.BadArgs(flag.Exact, 1) { - os.Exit(1) - } var ( v = url.Values{} remote = cmd.Arg(0) @@ -1406,21 +1296,14 @@ func (cli *DockerCli) CmdImages(args ...string) error { // FIXME: --viz and --tree are deprecated. Remove them in a future version. flViz := cmd.Bool([]string{"#v", "#viz", "#-viz"}, false, "Output graph in graphviz format") flTree := cmd.Bool([]string{"#t", "#tree", "#-tree"}, false, "Output graph in tree format") - help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") flFilter := opts.NewListOpts(nil) cmd.Var(&flFilter, []string{"f", "-filter"}, "Provide filter values (i.e. 'dangling=true')") + cmd.Require(flag.Max, 1) - if err := cmd.Parse(args); err != nil { + if err := utils.ParseFlags(cmd, args, true); err != nil { return nil } - if *help { - cmd.Usage() - return nil - } - if cmd.BadArgs(flag.Max, 1) { - os.Exit(1) - } // Consolidate all filter flags, and sanity check them early. // They'll get process in the daemon/server. @@ -1650,7 +1533,6 @@ func (cli *DockerCli) CmdPs(args ...string) error { quiet = cmd.Bool([]string{"q", "-quiet"}, false, "Only display numeric IDs") size = cmd.Bool([]string{"s", "-size"}, false, "Display total file sizes") all = cmd.Bool([]string{"a", "-all"}, false, "Show all containers. Only running containers are shown by default.") - help = cmd.Bool([]string{"#help", "-help"}, false, "Print usage") noTrunc = cmd.Bool([]string{"#notrunc", "-no-trunc"}, false, "Don't truncate output") nLatest = cmd.Bool([]string{"l", "-latest"}, false, "Show only the latest created container, include non-running ones.") since = cmd.String([]string{"#sinceId", "#-since-id", "-since"}, "", "Show only containers created since Id or Name, include non-running ones.") @@ -1658,14 +1540,11 @@ func (cli *DockerCli) CmdPs(args ...string) error { last = cmd.Int([]string{"n"}, -1, "Show n last created containers, include non-running ones.") flFilter = opts.NewListOpts(nil) ) + cmd.Require(flag.Exact, 0) cmd.Var(&flFilter, []string{"f", "-filter"}, "Provide filter values. Valid filters:\nexited= - containers with exit code of \nstatus=(restarting|running|paused|exited)") - if err := cmd.Parse(args); err != nil { - return nil - } - if *help { - cmd.Usage() + if err := utils.ParseFlags(cmd, args, true); err != nil { return nil } if *last == -1 && *nLatest { @@ -1808,22 +1687,11 @@ func (cli *DockerCli) CmdCommit(args ...string) error { flAuthor := cmd.String([]string{"a", "#author", "-author"}, "", "Author (e.g., \"John Hannibal Smith \")") // FIXME: --run is deprecated, it will be replaced with inline Dockerfile commands. flConfig := cmd.String([]string{"#run", "#-run"}, "", "This option is deprecated and will be removed in a future version in favor of inline Dockerfile-compatible commands") - help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") - if err := cmd.Parse(args); err != nil { + cmd.Require(flag.Max, 2) + cmd.Require(flag.Min, 1) + if err := utils.ParseFlags(cmd, args, true); err != nil { return nil } - if *help { - cmd.Usage() - return nil - } - - if cmd.BadArgs(flag.Max, 2) { - os.Exit(1) - } - - if cmd.BadArgs(flag.Min, 1) { - os.Exit(1) - } var ( name = cmd.Arg(0) @@ -1876,18 +1744,11 @@ func (cli *DockerCli) CmdEvents(args ...string) error { until := cmd.String([]string{"-until"}, "", "Stream events until this timestamp") flFilter := opts.NewListOpts(nil) cmd.Var(&flFilter, []string{"f", "-filter"}, "Provide filter values (i.e. 'event=stop')") - help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") + cmd.Require(flag.Exact, 0) - if err := cmd.Parse(args); err != nil { + if err := utils.ParseFlags(cmd, args, true); err != nil { return nil } - if *help { - cmd.Usage() - return nil - } - if cmd.BadArgs(flag.Exact, 0) { - os.Exit(1) - } var ( v = url.Values{} @@ -1936,18 +1797,11 @@ func (cli *DockerCli) CmdEvents(args ...string) error { func (cli *DockerCli) CmdExport(args ...string) error { cmd := cli.Subcmd("export", "CONTAINER", "Export the contents of a filesystem as a tar archive to STDOUT") - help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") + cmd.Require(flag.Exact, 1) - if err := cmd.Parse(args); err != nil { + if err := utils.ParseFlags(cmd, args, true); err != nil { return nil } - if *help { - cmd.Usage() - return nil - } - if cmd.BadArgs(flag.Exact, 1) { - os.Exit(1) - } if err := cli.stream("GET", "/containers/"+cmd.Arg(0)+"/export", nil, cli.out, nil); err != nil { return err @@ -1957,18 +1811,11 @@ func (cli *DockerCli) CmdExport(args ...string) error { func (cli *DockerCli) CmdDiff(args ...string) error { cmd := cli.Subcmd("diff", "CONTAINER", "Inspect changes on a container's filesystem") - help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") + cmd.Require(flag.Exact, 1) - if err := cmd.Parse(args); err != nil { + if err := utils.ParseFlags(cmd, args, true); err != nil { return nil } - if *help { - cmd.Usage() - return nil - } - if cmd.BadArgs(flag.Exact, 1) { - os.Exit(1) - } body, _, err := readBody(cli.call("GET", "/containers/"+cmd.Arg(0)+"/changes", nil, false)) @@ -2001,20 +1848,13 @@ func (cli *DockerCli) CmdLogs(args ...string) error { follow = cmd.Bool([]string{"f", "-follow"}, false, "Follow log output") times = cmd.Bool([]string{"t", "-timestamps"}, false, "Show timestamps") tail = cmd.String([]string{"-tail"}, "all", "Output the specified number of lines at the end of logs (defaults to all logs)") - help = cmd.Bool([]string{"#help", "-help"}, false, "Print usage") ) + cmd.Require(flag.Exact, 1) - if err := cmd.Parse(args); err != nil { + if err := utils.ParseFlags(cmd, args, true); err != nil { return nil } - if *help { - cmd.Usage() - return nil - } - if cmd.BadArgs(flag.Exact, 1) { - os.Exit(1) - } name := cmd.Arg(0) stream, _, err := cli.call("GET", "/containers/"+name+"/json", nil, false) @@ -2048,19 +1888,12 @@ func (cli *DockerCli) CmdAttach(args ...string) error { cmd = cli.Subcmd("attach", "CONTAINER", "Attach to a running container") noStdin = cmd.Bool([]string{"#nostdin", "-no-stdin"}, false, "Do not attach STDIN") proxy = cmd.Bool([]string{"#sig-proxy", "-sig-proxy"}, true, "Proxy all received signals to the process (non-TTY mode only). SIGCHLD, SIGKILL, and SIGSTOP are not proxied.") - help = cmd.Bool([]string{"#help", "-help"}, false, "Print usage") ) + cmd.Require(flag.Exact, 1) - if err := cmd.Parse(args); err != nil { + if err := utils.ParseFlags(cmd, args, true); err != nil { return nil } - if *help { - cmd.Usage() - return nil - } - if cmd.BadArgs(flag.Exact, 1) { - os.Exit(1) - } name := cmd.Arg(0) stream, _, err := cli.call("GET", "/containers/"+name+"/json", nil, false) @@ -2130,18 +1963,11 @@ func (cli *DockerCli) CmdSearch(args ...string) error { trusted := cmd.Bool([]string{"#t", "#trusted", "#-trusted"}, false, "Only show trusted builds") automated := cmd.Bool([]string{"-automated"}, false, "Only show automated builds") stars := cmd.Int([]string{"s", "#stars", "-stars"}, 0, "Only displays with at least x stars") - help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") + cmd.Require(flag.Exact, 1) - if err := cmd.Parse(args); err != nil { + if err := utils.ParseFlags(cmd, args, true); err != nil { return nil } - if *help { - cmd.Usage() - return nil - } - if cmd.BadArgs(flag.Exact, 1) { - os.Exit(1) - } v := url.Values{} v.Set("term", cmd.Arg(0)) @@ -2187,18 +2013,11 @@ type ports []int func (cli *DockerCli) CmdTag(args ...string) error { cmd := cli.Subcmd("tag", "IMAGE[:TAG] [REGISTRYHOST/][USERNAME/]NAME[:TAG]", "Tag an image into a repository") force := cmd.Bool([]string{"f", "#force", "-force"}, false, "Force") - help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") + cmd.Require(flag.Exact, 2) - if err := cmd.Parse(args); err != nil { + if err := utils.ParseFlags(cmd, args, true); err != nil { return nil } - if *help { - cmd.Usage() - return nil - } - if cmd.BadArgs(flag.Exact, 2) { - os.Exit(1) - } var ( repository, tag = parsers.ParseRepositoryTag(cmd.Arg(1)) @@ -2588,18 +2407,11 @@ func (cli *DockerCli) CmdRun(args ...string) error { func (cli *DockerCli) CmdCp(args ...string) error { cmd := cli.Subcmd("cp", "CONTAINER:PATH HOSTPATH", "Copy files/folders from the PATH to the HOSTPATH") - help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") + cmd.Require(flag.Exact, 2) - if err := cmd.Parse(args); err != nil { + if err := utils.ParseFlags(cmd, args, true); err != nil { return nil } - if *help { - cmd.Usage() - return nil - } - if cmd.BadArgs(flag.Exact, 2) { - os.Exit(1) - } var copyData engine.Env info := strings.Split(cmd.Arg(0), ":") @@ -2632,21 +2444,13 @@ func (cli *DockerCli) CmdCp(args ...string) error { func (cli *DockerCli) CmdSave(args ...string) error { cmd := cli.Subcmd("save", "IMAGE [IMAGE...]", "Save an image(s) to a tar archive (streamed to STDOUT by default)") - help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") outfile := cmd.String([]string{"o", "-output"}, "", "Write to an file, instead of STDOUT") + cmd.Require(flag.Min, 1) - if err := cmd.Parse(args); err != nil { + if err := utils.ParseFlags(cmd, args, true); err != nil { return err } - if *help { - cmd.Usage() - return nil - } - if cmd.BadArgs(flag.Min, 1) { - os.Exit(1) - } - var ( output io.Writer = cli.out err error @@ -2680,18 +2484,11 @@ func (cli *DockerCli) CmdSave(args ...string) error { func (cli *DockerCli) CmdLoad(args ...string) error { cmd := cli.Subcmd("load", "", "Load an image from a tar archive on STDIN") infile := cmd.String([]string{"i", "-input"}, "", "Read from a tar archive file, instead of STDIN") - help := cmd.Bool([]string{"#help", "-help"}, false, "Print usage") + cmd.Require(flag.Exact, 0) - if err := cmd.Parse(args); err != nil { + if err := utils.ParseFlags(cmd, args, true); err != nil { return err } - if *help { - cmd.Usage() - return nil - } - if cmd.BadArgs(flag.Exact, 0) { - os.Exit(1) - } var ( input io.Reader = cli.in diff --git a/pkg/mflag/flag.go b/pkg/mflag/flag.go index ffc509a59..42711b763 100644 --- a/pkg/mflag/flag.go +++ b/pkg/mflag/flag.go @@ -284,13 +284,14 @@ type FlagSet struct { // a custom error handler. Usage func() - name string - parsed bool - actual map[string]*Flag - formal map[string]*Flag - args []string // arguments after flags - errorHandling ErrorHandling - output io.Writer // nil means stderr; use out() accessor + name string + parsed bool + actual map[string]*Flag + formal map[string]*Flag + args []string // arguments after flags + errorHandling ErrorHandling + output io.Writer // nil means stderr; use Out() accessor + nArgRequirements []nArgRequirement } // A Flag represents the state of a flag. @@ -348,7 +349,13 @@ func sortFlags(flags map[string]*Flag) []*Flag { return result } -func (f *FlagSet) out() io.Writer { +// Name returns the name of the FlagSet. +func (f *FlagSet) Name() string { + return f.name +} + +// Out returns the destination for usage and error messages. +func (f *FlagSet) Out() io.Writer { if f.output == nil { return os.Stderr } @@ -410,45 +417,60 @@ func IsSet(name string) bool { return CommandLine.IsSet(name) } +type nArgRequirementType int + // Indicator used to pass to BadArgs function const ( - Exact = 1 - Max = 2 - Min = 3 + Exact nArgRequirementType = iota + Max + Min ) -// Bad Args takes two arguments. -// The first one indicates whether the number of arguments should, be -// A Minimal number of arguments, a maximum number of arguments or -// The exact number of arguments required -// If the actuall number of arguments is not valid and error message -// prints and true is returned, otherwise false is returned -func (f *FlagSet) BadArgs(arg_type, nargs int) bool { - if arg_type == Max && f.NArg() > nargs { - if nargs == 1 { - fmt.Fprintf(f.out(), "docker: '%s' requires a maximum of 1 argument. See 'docker %s --help'.\n", f.name, f.name) +type nArgRequirement struct { + Type nArgRequirementType + N int +} + +// Require adds a requirement about the number of arguments for the FlagSet. +// The first parameter can be Exact, Max, or Min to respectively specify the exact, +// the maximum, or the minimal number of arguments required. +// The actual check is done in FlagSet.CheckArgs(). +func (f *FlagSet) Require(nArgRequirementType nArgRequirementType, nArg int) { + f.nArgRequirements = append(f.nArgRequirements, nArgRequirement{nArgRequirementType, nArg}) +} + +// CheckArgs uses the requirements set by FlagSet.Require() to validate +// the number of arguments. If the requirements are not met, +// an error message string is returned. +func (f *FlagSet) CheckArgs() (message string) { + for _, req := range f.nArgRequirements { + var arguments string + if req.N == 1 { + arguments = "1 argument" } else { - fmt.Fprintf(f.out(), "docker: '%s' requires a maximum of %d arguments. See 'docker %s --help'.\n", f.name, nargs, f.name) + arguments = fmt.Sprintf("%d arguments", req.N) } - return true - } - if arg_type == Exact && f.NArg() != nargs { - if nargs == 1 { - fmt.Fprintf(f.out(), "docker: '%s' requires 1 argument. See 'docker %s --help'.\n", f.name, f.name) - } else { - fmt.Fprintf(f.out(), "docker: '%s' requires %d arguments. See 'docker %s --help'.\n", f.name, nargs, f.name) + + str := func(kind string) string { + return fmt.Sprintf("%q requires %s%s", f.name, kind, arguments) } - return true - } - if arg_type == Min && f.NArg() < nargs { - if nargs == 1 { - fmt.Fprintf(f.out(), "docker: '%s' requires a minimum of 1 argument. See 'docker %s --help'.\n", f.name, f.name) - } else { - fmt.Fprintf(f.out(), "docker: '%s' requires a minimum of %d arguments. See 'docker %s --help'.\n", f.name, nargs, f.name) + + switch req.Type { + case Exact: + if f.NArg() != req.N { + return str("") + } + case Max: + if f.NArg() > req.N { + return str("a maximum of ") + } + case Min: + if f.NArg() < req.N { + return str("a minimum of ") + } } - return true } - return false + return "" } // Set sets the value of the named flag. @@ -476,7 +498,7 @@ func Set(name, value string) error { // PrintDefaults prints, to standard error unless configured // otherwise, the default values of all defined flags in the set. func (f *FlagSet) PrintDefaults() { - writer := tabwriter.NewWriter(f.out(), 20, 1, 3, ' ', 0) + writer := tabwriter.NewWriter(f.Out(), 20, 1, 3, ' ', 0) f.VisitAll(func(flag *Flag) { format := " -%s=%s" if _, ok := flag.Value.(*stringValue); ok { @@ -510,9 +532,9 @@ func PrintDefaults() { // defaultUsage is the default function to print a usage message. func defaultUsage(f *FlagSet) { if f.name == "" { - fmt.Fprintf(f.out(), "Usage:\n") + fmt.Fprintf(f.Out(), "Usage:\n") } else { - fmt.Fprintf(f.out(), "Usage of %s:\n", f.name) + fmt.Fprintf(f.Out(), "Usage of %s:\n", f.name) } f.PrintDefaults() } @@ -805,7 +827,7 @@ func (f *FlagSet) Var(value Value, names []string, usage string) { } else { msg = fmt.Sprintf("%s flag redefined: %s", f.name, name) } - fmt.Fprintln(f.out(), msg) + fmt.Fprintln(f.Out(), msg) panic(msg) // Happens only if flags are declared with identical names } if f.formal == nil { @@ -829,8 +851,8 @@ func Var(value Value, names []string, usage string) { // returns the error. func (f *FlagSet) failf(format string, a ...interface{}) error { err := fmt.Errorf(format, a...) - fmt.Fprintln(f.out(), err) - fmt.Fprintf(f.out(), "See 'docker %s --help'.\n", f.name) + fmt.Fprintln(f.Out(), err) + fmt.Fprintf(f.Out(), "See 'docker %s --help'.\n", f.name) return err } @@ -956,9 +978,9 @@ func (f *FlagSet) parseOne() (bool, string, error) { } } if replacement != "" { - fmt.Fprintf(f.out(), "Warning: '-%s' is deprecated, it will be replaced by '-%s' soon. See usage.\n", name, replacement) + fmt.Fprintf(f.Out(), "Warning: '-%s' is deprecated, it will be replaced by '-%s' soon. See usage.\n", name, replacement) } else { - fmt.Fprintf(f.out(), "Warning: '-%s' is deprecated, it will be removed soon. See usage.\n", name) + fmt.Fprintf(f.Out(), "Warning: '-%s' is deprecated, it will be removed soon. See usage.\n", name) } } } diff --git a/runconfig/exec.go b/runconfig/exec.go index 4e23481e5..9390781a4 100644 --- a/runconfig/exec.go +++ b/runconfig/exec.go @@ -5,7 +5,7 @@ import ( "github.com/docker/docker/engine" flag "github.com/docker/docker/pkg/mflag" - "os" + "github.com/docker/docker/utils" ) type ExecConfig struct { @@ -46,20 +46,13 @@ func ParseExec(cmd *flag.FlagSet, args []string) (*ExecConfig, error) { flStdin = cmd.Bool([]string{"i", "-interactive"}, false, "Keep STDIN open even if not attached") flTty = cmd.Bool([]string{"t", "-tty"}, false, "Allocate a pseudo-TTY") flDetach = cmd.Bool([]string{"d", "-detach"}, false, "Detached mode: run command in the background") - help = cmd.Bool([]string{"#help", "-help"}, false, "Print usage") execCmd []string container string ) - if err := cmd.Parse(args); err != nil { + cmd.Require(flag.Min, 2) + if err := utils.ParseFlags(cmd, args, true); err != nil { return nil, err } - if *help { - cmd.Usage() - return nil, nil - } - if cmd.BadArgs(flag.Min, 2) { - os.Exit(1) - } container = cmd.Arg(0) parsedArgs := cmd.Args() execCmd = parsedArgs[1:] diff --git a/runconfig/parse.go b/runconfig/parse.go index d2e4c7384..911ab7e52 100644 --- a/runconfig/parse.go +++ b/runconfig/parse.go @@ -2,7 +2,6 @@ package runconfig import ( "fmt" - "os" "path" "strconv" "strings" @@ -62,7 +61,6 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe flMacAddress = cmd.String([]string{"-mac-address"}, "", "Container MAC address (e.g. 92:d0:c6:0a:29:33)") flIpcMode = cmd.String([]string{"-ipc"}, "", "Default is to create a private IPC namespace (POSIX SysV IPC) for the container\n'container:': reuses another container shared memory, semaphores and message queues\n'host': use the host shared memory,semaphores and message queues inside the container. Note: the host mode gives the container full access to local shared memory and is therefore considered insecure.") flRestartPolicy = cmd.String([]string{"-restart"}, "", "Restart policy to apply when a container exits (no, on-failure[:max-retry], always)") - help = cmd.Bool([]string{"#help", "-help"}, false, "Print usage") ) cmd.Var(&flAttach, []string{"a", "-attach"}, "Attach to STDIN, STDOUT or STDERR.") @@ -85,16 +83,11 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe cmd.Var(&flCapDrop, []string{"-cap-drop"}, "Drop Linux capabilities") cmd.Var(&flSecurityOpt, []string{"-security-opt"}, "Security Options") - if err := cmd.Parse(args); err != nil { + cmd.Require(flag.Min, 1) + + if err := utils.ParseFlags(cmd, args, true); err != nil { return nil, nil, cmd, err } - if *help { - cmd.Usage() - return nil, nil, cmd, nil - } - if cmd.BadArgs(flag.Min, 1) { - os.Exit(1) - } // Validate input params if *flWorkingDir != "" && !path.IsAbs(*flWorkingDir) { diff --git a/utils/flags.go b/utils/flags.go new file mode 100644 index 000000000..d88104b63 --- /dev/null +++ b/utils/flags.go @@ -0,0 +1,34 @@ +package utils + +import ( + "fmt" + "os" + + flag "github.com/docker/docker/pkg/mflag" +) + +// ParseFlags is a utility function that adds a help flag if withHelp is true, +// calls cmd.Parse(args) and prints a relevant error message if there are incorrect number of arguments. +// TODO: move this to a better package than utils +func ParseFlags(cmd *flag.FlagSet, args []string, withHelp bool) error { + var help *bool + if withHelp { + help = cmd.Bool([]string{"#help", "-help"}, false, "Print usage") + } + if err := cmd.Parse(args); err != nil { + return err + } + if help != nil && *help { + cmd.Usage() + // just in case Usage does not exit + os.Exit(0) + } + if str := cmd.CheckArgs(); str != "" { + if withHelp { + str += ". See 'docker " + cmd.Name() + " --help'" + } + fmt.Fprintf(cmd.Out(), "docker: %s.\n", str) + os.Exit(1) + } + return nil +} From 1d09fc22ca14b0b83bdc81cba06d3d7ac886a3e6 Mon Sep 17 00:00:00 2001 From: Michal Minar Date: Thu, 13 Nov 2014 15:39:51 +0100 Subject: [PATCH 176/513] Handle bad options better * Do not log bad options error message twice, e.g.: $ docker run --pouet flag provided but not defined: --pouet See 'docker run --help'. 2014/11/05 21:41:23 flag provided but not defined: --pouet With this patch just the first two lines will be produced. * Print 'docker' just once when run without a command, e.g.: $ docker --hel flag provided but not defined: --hel See 'docker docker --help'. Signed-off-by: Michal Minar --- api/client/commands.go | 6 +++--- pkg/mflag/flag.go | 6 +++++- utils/flags.go | 6 +++++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/api/client/commands.go b/api/client/commands.go index cfbedc835..6c919e106 100644 --- a/api/client/commands.go +++ b/api/client/commands.go @@ -2187,7 +2187,7 @@ func (cli *DockerCli) CmdCreate(args ...string) error { config, hostConfig, cmd, err := runconfig.Parse(cmd, args) if err != nil { - return err + return &utils.StatusError{StatusCode: 1} } if config.Image == "" { cmd.Usage() @@ -2223,7 +2223,7 @@ func (cli *DockerCli) CmdRun(args ...string) error { config, hostConfig, cmd, err := runconfig.Parse(cmd, args) if err != nil { - return err + return &utils.StatusError{StatusCode: 1} } if config.Image == "" { cmd.Usage() @@ -2511,7 +2511,7 @@ func (cli *DockerCli) CmdExec(args ...string) error { execConfig, err := runconfig.ParseExec(cmd, args) if execConfig.Container == "" || err != nil { - return err + return &utils.StatusError{StatusCode: 1} } stream, _, err := cli.call("POST", "/containers/"+execConfig.Container+"/exec", execConfig, false) diff --git a/pkg/mflag/flag.go b/pkg/mflag/flag.go index 42711b763..72addae63 100644 --- a/pkg/mflag/flag.go +++ b/pkg/mflag/flag.go @@ -852,7 +852,11 @@ func Var(value Value, names []string, usage string) { func (f *FlagSet) failf(format string, a ...interface{}) error { err := fmt.Errorf(format, a...) fmt.Fprintln(f.Out(), err) - fmt.Fprintf(f.Out(), "See 'docker %s --help'.\n", f.name) + if os.Args[0] == f.name { + fmt.Fprintf(f.Out(), "See '%s --help'.\n", os.Args[0]) + } else { + fmt.Fprintf(f.Out(), "See '%s %s --help'.\n", os.Args[0], f.name) + } return err } diff --git a/utils/flags.go b/utils/flags.go index d88104b63..fdf57b1b2 100644 --- a/utils/flags.go +++ b/utils/flags.go @@ -25,7 +25,11 @@ func ParseFlags(cmd *flag.FlagSet, args []string, withHelp bool) error { } if str := cmd.CheckArgs(); str != "" { if withHelp { - str += ". See 'docker " + cmd.Name() + " --help'" + if os.Args[0] == cmd.Name() { + str += ". See '" + os.Args[0] + " " + cmd.Name() + " --help'" + } else { + str += ". See '" + os.Args[0] + " --help'" + } } fmt.Fprintf(cmd.Out(), "docker: %s.\n", str) os.Exit(1) From 08f0f1ee1db2dea79e622764891cb8b1432bcab1 Mon Sep 17 00:00:00 2001 From: Michal Minar Date: Wed, 19 Nov 2014 09:42:10 +0100 Subject: [PATCH 177/513] Fixed error reporting Removed redundant print line and fixed handling of command-less docker invocation. Signed-off-by: Michal Minar --- api/client/commands.go | 1 - utils/flags.go | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/api/client/commands.go b/api/client/commands.go index 6c919e106..5b0ee1175 100644 --- a/api/client/commands.go +++ b/api/client/commands.go @@ -63,7 +63,6 @@ func (cli *DockerCli) CmdHelp(args ...string) error { if len(args) > 0 { method, exists := cli.getMethod(args[0]) if !exists { - fmt.Fprintf(cli.err, "Error: Command not found: %s\n", args[0]) fmt.Fprintf(cli.err, "docker: '%s' is not a docker command. See 'docker --help'.\n", args[0]) os.Exit(1) } else { diff --git a/utils/flags.go b/utils/flags.go index fdf57b1b2..92449070e 100644 --- a/utils/flags.go +++ b/utils/flags.go @@ -26,9 +26,9 @@ func ParseFlags(cmd *flag.FlagSet, args []string, withHelp bool) error { if str := cmd.CheckArgs(); str != "" { if withHelp { if os.Args[0] == cmd.Name() { - str += ". See '" + os.Args[0] + " " + cmd.Name() + " --help'" - } else { str += ". See '" + os.Args[0] + " --help'" + } else { + str += ". See '" + os.Args[0] + " " + cmd.Name() + " --help'" } } fmt.Fprintf(cmd.Out(), "docker: %s.\n", str) From 8a785792cd3c3fb4494bced475263aef5fa4534b Mon Sep 17 00:00:00 2001 From: Michal Minar Date: Mon, 24 Nov 2014 17:34:13 +0100 Subject: [PATCH 178/513] Exit with non-zero code on first argument parsing error Ignoring return value of ParseFlags leads to exit code 0 if bad arguments are supplied. This patch makes sure that subcommands exit with non-zero code in such a case. Signed-off-by: Michal Minar --- api/client/cli.go | 10 +- api/client/commands.go | 212 ++++++++++++++--------------------------- utils/flags.go | 5 +- 3 files changed, 85 insertions(+), 142 deletions(-) diff --git a/api/client/cli.go b/api/client/cli.go index aec8ebb86..9f034d6c4 100644 --- a/api/client/cli.go +++ b/api/client/cli.go @@ -83,8 +83,14 @@ func (cli *DockerCli) Cmd(args ...string) error { return cli.CmdHelp() } -func (cli *DockerCli) Subcmd(name, signature, description string) *flag.FlagSet { - flags := flag.NewFlagSet(name, flag.ContinueOnError) +func (cli *DockerCli) Subcmd(name, signature, description string, exitOnError bool) *flag.FlagSet { + var errorHandling flag.ErrorHandling + if exitOnError { + errorHandling = flag.ExitOnError + } else { + errorHandling = flag.ContinueOnError + } + flags := flag.NewFlagSet(name, errorHandling) flags.Usage = func() { options := "" if flags.FlagCountUndeprecated() > 0 { diff --git a/api/client/commands.go b/api/client/commands.go index 5b0ee1175..fa2f63df7 100644 --- a/api/client/commands.go +++ b/api/client/commands.go @@ -77,7 +77,7 @@ func (cli *DockerCli) CmdHelp(args ...string) error { } func (cli *DockerCli) CmdBuild(args ...string) error { - cmd := cli.Subcmd("build", "PATH | URL | -", "Build a new image from the source code at PATH") + cmd := cli.Subcmd("build", "PATH | URL | -", "Build a new image from the source code at PATH", true) 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") @@ -86,9 +86,7 @@ func (cli *DockerCli) CmdBuild(args ...string) error { pull := cmd.Bool([]string{"-pull"}, false, "Always attempt to pull a newer version of the image") cmd.Require(flag.Exact, 1) - if err := utils.ParseFlags(cmd, args, true); err != nil { - return nil - } + utils.ParseFlags(cmd, args, true) var ( context archive.Archive @@ -246,7 +244,7 @@ func (cli *DockerCli) CmdBuild(args ...string) error { // 'docker login': login / register a user to registry service. func (cli *DockerCli) CmdLogin(args ...string) error { - cmd := cli.Subcmd("login", "[SERVER]", "Register or log in to a Docker registry server, if no server is specified \""+registry.IndexServerAddress()+"\" is the default.") + cmd := cli.Subcmd("login", "[SERVER]", "Register or log in to a Docker registry server, if no server is specified \""+registry.IndexServerAddress()+"\" is the default.", true) cmd.Require(flag.Max, 1) var username, password, email string @@ -255,9 +253,7 @@ func (cli *DockerCli) CmdLogin(args ...string) error { cmd.StringVar(&password, []string{"p", "-password"}, "", "Password") cmd.StringVar(&email, []string{"e", "-email"}, "", "Email") - if err := utils.ParseFlags(cmd, args, true); err != nil { - return nil - } + utils.ParseFlags(cmd, args, true) serverAddress := registry.IndexServerAddress() if len(cmd.Args()) > 0 { @@ -364,12 +360,10 @@ func (cli *DockerCli) CmdLogin(args ...string) error { // log out from a Docker registry func (cli *DockerCli) CmdLogout(args ...string) error { - cmd := cli.Subcmd("logout", "[SERVER]", "Log out from a Docker registry, if no server is specified \""+registry.IndexServerAddress()+"\" is the default.") + cmd := cli.Subcmd("logout", "[SERVER]", "Log out from a Docker registry, if no server is specified \""+registry.IndexServerAddress()+"\" is the default.", true) cmd.Require(flag.Max, 1) - if err := utils.ParseFlags(cmd, args, false); err != nil { - return nil - } + utils.ParseFlags(cmd, args, false) serverAddress := registry.IndexServerAddress() if len(cmd.Args()) > 0 { serverAddress = cmd.Arg(0) @@ -391,12 +385,10 @@ func (cli *DockerCli) CmdLogout(args ...string) error { // 'docker wait': block until a container stops func (cli *DockerCli) CmdWait(args ...string) error { - cmd := cli.Subcmd("wait", "CONTAINER [CONTAINER...]", "Block until a container stops, then print its exit code.") + cmd := cli.Subcmd("wait", "CONTAINER [CONTAINER...]", "Block until a container stops, then print its exit code.", true) cmd.Require(flag.Min, 1) - if err := utils.ParseFlags(cmd, args, true); err != nil { - return nil - } + utils.ParseFlags(cmd, args, true) var encounteredError error for _, name := range cmd.Args() { @@ -413,12 +405,10 @@ func (cli *DockerCli) CmdWait(args ...string) error { // 'docker version': show version information func (cli *DockerCli) CmdVersion(args ...string) error { - cmd := cli.Subcmd("version", "", "Show the Docker version information.") + cmd := cli.Subcmd("version", "", "Show the Docker version information.", true) cmd.Require(flag.Exact, 0) - if err := utils.ParseFlags(cmd, args, false); err != nil { - return nil - } + utils.ParseFlags(cmd, args, false) if dockerversion.VERSION != "" { fmt.Fprintf(cli.out, "Client version: %s\n", dockerversion.VERSION) @@ -457,11 +447,9 @@ func (cli *DockerCli) CmdVersion(args ...string) error { // 'docker info': display system-wide information. func (cli *DockerCli) CmdInfo(args ...string) error { - cmd := cli.Subcmd("info", "", "Display system-wide information") + cmd := cli.Subcmd("info", "", "Display system-wide information", true) cmd.Require(flag.Exact, 0) - if err := utils.ParseFlags(cmd, args, false); err != nil { - return nil - } + utils.ParseFlags(cmd, args, false) body, _, err := readBody(cli.call("GET", "/info", nil, false)) if err != nil { @@ -573,13 +561,11 @@ func (cli *DockerCli) CmdInfo(args ...string) error { } func (cli *DockerCli) CmdStop(args ...string) error { - cmd := cli.Subcmd("stop", "CONTAINER [CONTAINER...]", "Stop a running container by sending SIGTERM and then SIGKILL after a grace period") + cmd := cli.Subcmd("stop", "CONTAINER [CONTAINER...]", "Stop a running container by sending SIGTERM and then SIGKILL after a grace period", true) nSeconds := cmd.Int([]string{"t", "-time"}, 10, "Number of seconds to wait for the container to stop before killing it. Default is 10 seconds.") cmd.Require(flag.Min, 1) - if err := utils.ParseFlags(cmd, args, true); err != nil { - return nil - } + utils.ParseFlags(cmd, args, true) v := url.Values{} v.Set("t", strconv.Itoa(*nSeconds)) @@ -598,13 +584,11 @@ func (cli *DockerCli) CmdStop(args ...string) error { } func (cli *DockerCli) CmdRestart(args ...string) error { - cmd := cli.Subcmd("restart", "CONTAINER [CONTAINER...]", "Restart a running container") + cmd := cli.Subcmd("restart", "CONTAINER [CONTAINER...]", "Restart a running container", true) nSeconds := cmd.Int([]string{"t", "-time"}, 10, "Number of seconds to try to stop for before killing the container. Once killed it will then be restarted. Default is 10 seconds.") cmd.Require(flag.Min, 1) - if err := utils.ParseFlags(cmd, args, true); err != nil { - return nil - } + utils.ParseFlags(cmd, args, true) v := url.Values{} v.Set("t", strconv.Itoa(*nSeconds)) @@ -653,15 +637,13 @@ func (cli *DockerCli) CmdStart(args ...string) error { cErr chan error tty bool - cmd = cli.Subcmd("start", "CONTAINER [CONTAINER...]", "Restart a stopped container") + cmd = cli.Subcmd("start", "CONTAINER [CONTAINER...]", "Restart a stopped container", true) attach = cmd.Bool([]string{"a", "-attach"}, false, "Attach container's STDOUT and STDERR and forward all signals to the process") openStdin = cmd.Bool([]string{"i", "-interactive"}, false, "Attach container's STDIN") ) cmd.Require(flag.Min, 1) - if err := utils.ParseFlags(cmd, args, true); err != nil { - return nil - } + utils.ParseFlags(cmd, args, true) hijacked := make(chan io.Closer) @@ -763,11 +745,9 @@ func (cli *DockerCli) CmdStart(args ...string) error { } func (cli *DockerCli) CmdUnpause(args ...string) error { - cmd := cli.Subcmd("unpause", "CONTAINER", "Unpause all processes within a container") + cmd := cli.Subcmd("unpause", "CONTAINER", "Unpause all processes within a container", true) cmd.Require(flag.Exact, 1) - if err := utils.ParseFlags(cmd, args, false); err != nil { - return nil - } + utils.ParseFlags(cmd, args, false) var encounteredError error for _, name := range cmd.Args() { @@ -782,11 +762,9 @@ func (cli *DockerCli) CmdUnpause(args ...string) error { } func (cli *DockerCli) CmdPause(args ...string) error { - cmd := cli.Subcmd("pause", "CONTAINER", "Pause all processes within a container") + cmd := cli.Subcmd("pause", "CONTAINER", "Pause all processes within a container", true) cmd.Require(flag.Exact, 1) - if err := utils.ParseFlags(cmd, args, false); err != nil { - return nil - } + utils.ParseFlags(cmd, args, false) var encounteredError error for _, name := range cmd.Args() { @@ -801,13 +779,11 @@ func (cli *DockerCli) CmdPause(args ...string) error { } func (cli *DockerCli) CmdInspect(args ...string) error { - cmd := cli.Subcmd("inspect", "CONTAINER|IMAGE [CONTAINER|IMAGE...]", "Return low-level information on a container or image") + cmd := cli.Subcmd("inspect", "CONTAINER|IMAGE [CONTAINER|IMAGE...]", "Return low-level information on a container or image", true) tmplStr := cmd.String([]string{"f", "#format", "-format"}, "", "Format the output using the given go template.") cmd.Require(flag.Min, 1) - if err := utils.ParseFlags(cmd, args, true); err != nil { - return nil - } + utils.ParseFlags(cmd, args, true) var tmpl *template.Template if *tmplStr != "" { @@ -879,12 +855,10 @@ func (cli *DockerCli) CmdInspect(args ...string) error { } func (cli *DockerCli) CmdTop(args ...string) error { - cmd := cli.Subcmd("top", "CONTAINER [ps OPTIONS]", "Display the running processes of a container") + cmd := cli.Subcmd("top", "CONTAINER [ps OPTIONS]", "Display the running processes of a container", true) cmd.Require(flag.Min, 1) - if err := utils.ParseFlags(cmd, args, true); err != nil { - return nil - } + utils.ParseFlags(cmd, args, true) val := url.Values{} if cmd.NArg() > 1 { @@ -913,11 +887,9 @@ func (cli *DockerCli) CmdTop(args ...string) error { } func (cli *DockerCli) CmdPort(args ...string) error { - cmd := cli.Subcmd("port", "CONTAINER [PRIVATE_PORT[/PROTO]]", "List port mappings for the CONTAINER, or lookup the public-facing port that is NAT-ed to the PRIVATE_PORT") + cmd := cli.Subcmd("port", "CONTAINER [PRIVATE_PORT[/PROTO]]", "List port mappings for the CONTAINER, or lookup the public-facing port that is NAT-ed to the PRIVATE_PORT", true) cmd.Require(flag.Min, 1) - if err := utils.ParseFlags(cmd, args, true); err != nil { - return nil - } + utils.ParseFlags(cmd, args, true) stream, _, err := cli.call("GET", "/containers/"+cmd.Arg(0)+"/json", nil, false) if err != nil { @@ -966,15 +938,13 @@ func (cli *DockerCli) CmdPort(args ...string) error { // 'docker rmi IMAGE' removes all images with the name IMAGE func (cli *DockerCli) CmdRmi(args ...string) error { var ( - cmd = cli.Subcmd("rmi", "IMAGE [IMAGE...]", "Remove one or more images") + cmd = cli.Subcmd("rmi", "IMAGE [IMAGE...]", "Remove one or more images", true) force = cmd.Bool([]string{"f", "-force"}, false, "Force removal of the image") noprune = cmd.Bool([]string{"-no-prune"}, false, "Do not delete untagged parents") ) cmd.Require(flag.Min, 1) - if err := utils.ParseFlags(cmd, args, true); err != nil { - return nil - } + utils.ParseFlags(cmd, args, true) v := url.Values{} if *force { @@ -1010,14 +980,12 @@ func (cli *DockerCli) CmdRmi(args ...string) error { } func (cli *DockerCli) CmdHistory(args ...string) error { - cmd := cli.Subcmd("history", "IMAGE", "Show the history of an image") + cmd := cli.Subcmd("history", "IMAGE", "Show the history of an image", true) quiet := cmd.Bool([]string{"q", "-quiet"}, false, "Only show numeric IDs") noTrunc := cmd.Bool([]string{"#notrunc", "-no-trunc"}, false, "Don't truncate output") cmd.Require(flag.Exact, 1) - if err := utils.ParseFlags(cmd, args, true); err != nil { - return nil - } + utils.ParseFlags(cmd, args, true) body, _, err := readBody(cli.call("GET", "/images/"+cmd.Arg(0)+"/history", nil, false)) if err != nil { @@ -1064,15 +1032,13 @@ func (cli *DockerCli) CmdHistory(args ...string) error { } func (cli *DockerCli) CmdRm(args ...string) error { - cmd := cli.Subcmd("rm", "CONTAINER [CONTAINER...]", "Remove one or more containers") + cmd := cli.Subcmd("rm", "CONTAINER [CONTAINER...]", "Remove one or more containers", true) v := cmd.Bool([]string{"v", "-volumes"}, false, "Remove the volumes associated with the container") link := cmd.Bool([]string{"l", "#link", "-link"}, false, "Remove the specified link and not the underlying container") force := cmd.Bool([]string{"f", "-force"}, false, "Force the removal of a running container (uses SIGKILL)") cmd.Require(flag.Min, 1) - if err := utils.ParseFlags(cmd, args, true); err != nil { - return nil - } + utils.ParseFlags(cmd, args, true) val := url.Values{} if *v { @@ -1101,13 +1067,11 @@ func (cli *DockerCli) CmdRm(args ...string) error { // 'docker kill NAME' kills a running container func (cli *DockerCli) CmdKill(args ...string) error { - cmd := cli.Subcmd("kill", "CONTAINER [CONTAINER...]", "Kill a running container using SIGKILL or a specified signal") + cmd := cli.Subcmd("kill", "CONTAINER [CONTAINER...]", "Kill a running container using SIGKILL or a specified signal", true) signal := cmd.String([]string{"s", "-signal"}, "KILL", "Signal to send to the container") cmd.Require(flag.Min, 1) - if err := utils.ParseFlags(cmd, args, true); err != nil { - return nil - } + utils.ParseFlags(cmd, args, true) var encounteredError error for _, name := range cmd.Args() { @@ -1122,12 +1086,10 @@ func (cli *DockerCli) CmdKill(args ...string) error { } func (cli *DockerCli) CmdImport(args ...string) error { - cmd := cli.Subcmd("import", "URL|- [REPOSITORY[:TAG]]", "Create an empty filesystem image and import the contents of the tarball (.tar, .tar.gz, .tgz, .bzip, .tar.xz, .txz) into it, then optionally tag it.") + cmd := cli.Subcmd("import", "URL|- [REPOSITORY[:TAG]]", "Create an empty filesystem image and import the contents of the tarball (.tar, .tar.gz, .tgz, .bzip, .tar.xz, .txz) into it, then optionally tag it.", true) cmd.Require(flag.Min, 1) - if err := utils.ParseFlags(cmd, args, true); err != nil { - return nil - } + utils.ParseFlags(cmd, args, true) var ( v = url.Values{} @@ -1161,12 +1123,10 @@ func (cli *DockerCli) CmdImport(args ...string) error { } func (cli *DockerCli) CmdPush(args ...string) error { - cmd := cli.Subcmd("push", "NAME[:TAG]", "Push an image or a repository to the registry") + cmd := cli.Subcmd("push", "NAME[:TAG]", "Push an image or a repository to the registry", true) cmd.Require(flag.Exact, 1) - if err := utils.ParseFlags(cmd, args, true); err != nil { - return nil - } + utils.ParseFlags(cmd, args, true) name := cmd.Arg(0) @@ -1224,13 +1184,11 @@ func (cli *DockerCli) CmdPush(args ...string) error { } func (cli *DockerCli) CmdPull(args ...string) error { - cmd := cli.Subcmd("pull", "NAME[:TAG]", "Pull an image or a repository from the registry") + cmd := cli.Subcmd("pull", "NAME[:TAG]", "Pull an image or a repository from the registry", true) allTags := cmd.Bool([]string{"a", "-all-tags"}, false, "Download all tagged images in the repository") cmd.Require(flag.Exact, 1) - if err := utils.ParseFlags(cmd, args, true); err != nil { - return nil - } + utils.ParseFlags(cmd, args, true) var ( v = url.Values{} @@ -1288,7 +1246,7 @@ func (cli *DockerCli) CmdPull(args ...string) error { } func (cli *DockerCli) CmdImages(args ...string) error { - cmd := cli.Subcmd("images", "[REPOSITORY]", "List images") + cmd := cli.Subcmd("images", "[REPOSITORY]", "List images", true) quiet := cmd.Bool([]string{"q", "-quiet"}, false, "Only show numeric IDs") all := cmd.Bool([]string{"a", "-all"}, false, "Show all images (by default filter out the intermediate image layers)") noTrunc := cmd.Bool([]string{"#notrunc", "-no-trunc"}, false, "Don't truncate output") @@ -1300,9 +1258,7 @@ func (cli *DockerCli) CmdImages(args ...string) error { cmd.Var(&flFilter, []string{"f", "-filter"}, "Provide filter values (i.e. 'dangling=true')") cmd.Require(flag.Max, 1) - if err := utils.ParseFlags(cmd, args, true); err != nil { - return nil - } + utils.ParseFlags(cmd, args, true) // Consolidate all filter flags, and sanity check them early. // They'll get process in the daemon/server. @@ -1528,7 +1484,7 @@ func (cli *DockerCli) CmdPs(args ...string) error { psFilterArgs = filters.Args{} v = url.Values{} - cmd = cli.Subcmd("ps", "", "List containers") + cmd = cli.Subcmd("ps", "", "List containers", true) quiet = cmd.Bool([]string{"q", "-quiet"}, false, "Only display numeric IDs") size = cmd.Bool([]string{"s", "-size"}, false, "Display total file sizes") all = cmd.Bool([]string{"a", "-all"}, false, "Show all containers. Only running containers are shown by default.") @@ -1543,9 +1499,7 @@ func (cli *DockerCli) CmdPs(args ...string) error { cmd.Var(&flFilter, []string{"f", "-filter"}, "Provide filter values. Valid filters:\nexited= - containers with exit code of \nstatus=(restarting|running|paused|exited)") - if err := utils.ParseFlags(cmd, args, true); err != nil { - return nil - } + utils.ParseFlags(cmd, args, true) if *last == -1 && *nLatest { *last = 1 } @@ -1680,7 +1634,7 @@ func (cli *DockerCli) CmdPs(args ...string) error { } func (cli *DockerCli) CmdCommit(args ...string) error { - cmd := cli.Subcmd("commit", "CONTAINER [REPOSITORY[:TAG]]", "Create a new image from a container's changes") + cmd := cli.Subcmd("commit", "CONTAINER [REPOSITORY[:TAG]]", "Create a new image from a container's changes", true) flPause := cmd.Bool([]string{"p", "-pause"}, true, "Pause container during commit") flComment := cmd.String([]string{"m", "-message"}, "", "Commit message") flAuthor := cmd.String([]string{"a", "#author", "-author"}, "", "Author (e.g., \"John Hannibal Smith \")") @@ -1688,9 +1642,7 @@ func (cli *DockerCli) CmdCommit(args ...string) error { flConfig := cmd.String([]string{"#run", "#-run"}, "", "This option is deprecated and will be removed in a future version in favor of inline Dockerfile-compatible commands") cmd.Require(flag.Max, 2) cmd.Require(flag.Min, 1) - if err := utils.ParseFlags(cmd, args, true); err != nil { - return nil - } + utils.ParseFlags(cmd, args, true) var ( name = cmd.Arg(0) @@ -1738,16 +1690,14 @@ func (cli *DockerCli) CmdCommit(args ...string) error { } func (cli *DockerCli) CmdEvents(args ...string) error { - cmd := cli.Subcmd("events", "", "Get real time events from the server") + cmd := cli.Subcmd("events", "", "Get real time events from the server", true) since := cmd.String([]string{"#since", "-since"}, "", "Show all events created since timestamp") until := cmd.String([]string{"-until"}, "", "Stream events until this timestamp") flFilter := opts.NewListOpts(nil) cmd.Var(&flFilter, []string{"f", "-filter"}, "Provide filter values (i.e. 'event=stop')") cmd.Require(flag.Exact, 0) - if err := utils.ParseFlags(cmd, args, true); err != nil { - return nil - } + utils.ParseFlags(cmd, args, true) var ( v = url.Values{} @@ -1795,12 +1745,10 @@ func (cli *DockerCli) CmdEvents(args ...string) error { } func (cli *DockerCli) CmdExport(args ...string) error { - cmd := cli.Subcmd("export", "CONTAINER", "Export the contents of a filesystem as a tar archive to STDOUT") + cmd := cli.Subcmd("export", "CONTAINER", "Export the contents of a filesystem as a tar archive to STDOUT", true) cmd.Require(flag.Exact, 1) - if err := utils.ParseFlags(cmd, args, true); err != nil { - return nil - } + utils.ParseFlags(cmd, args, true) if err := cli.stream("GET", "/containers/"+cmd.Arg(0)+"/export", nil, cli.out, nil); err != nil { return err @@ -1809,12 +1757,10 @@ func (cli *DockerCli) CmdExport(args ...string) error { } func (cli *DockerCli) CmdDiff(args ...string) error { - cmd := cli.Subcmd("diff", "CONTAINER", "Inspect changes on a container's filesystem") + cmd := cli.Subcmd("diff", "CONTAINER", "Inspect changes on a container's filesystem", true) cmd.Require(flag.Exact, 1) - if err := utils.ParseFlags(cmd, args, true); err != nil { - return nil - } + utils.ParseFlags(cmd, args, true) body, _, err := readBody(cli.call("GET", "/containers/"+cmd.Arg(0)+"/changes", nil, false)) @@ -1843,16 +1789,14 @@ func (cli *DockerCli) CmdDiff(args ...string) error { func (cli *DockerCli) CmdLogs(args ...string) error { var ( - cmd = cli.Subcmd("logs", "CONTAINER", "Fetch the logs of a container") + cmd = cli.Subcmd("logs", "CONTAINER", "Fetch the logs of a container", true) follow = cmd.Bool([]string{"f", "-follow"}, false, "Follow log output") times = cmd.Bool([]string{"t", "-timestamps"}, false, "Show timestamps") tail = cmd.String([]string{"-tail"}, "all", "Output the specified number of lines at the end of logs (defaults to all logs)") ) cmd.Require(flag.Exact, 1) - if err := utils.ParseFlags(cmd, args, true); err != nil { - return nil - } + utils.ParseFlags(cmd, args, true) name := cmd.Arg(0) @@ -1884,15 +1828,13 @@ func (cli *DockerCli) CmdLogs(args ...string) error { func (cli *DockerCli) CmdAttach(args ...string) error { var ( - cmd = cli.Subcmd("attach", "CONTAINER", "Attach to a running container") + cmd = cli.Subcmd("attach", "CONTAINER", "Attach to a running container", true) noStdin = cmd.Bool([]string{"#nostdin", "-no-stdin"}, false, "Do not attach STDIN") proxy = cmd.Bool([]string{"#sig-proxy", "-sig-proxy"}, true, "Proxy all received signals to the process (non-TTY mode only). SIGCHLD, SIGKILL, and SIGSTOP are not proxied.") ) cmd.Require(flag.Exact, 1) - if err := utils.ParseFlags(cmd, args, true); err != nil { - return nil - } + utils.ParseFlags(cmd, args, true) name := cmd.Arg(0) stream, _, err := cli.call("GET", "/containers/"+name+"/json", nil, false) @@ -1957,16 +1899,14 @@ func (cli *DockerCli) CmdAttach(args ...string) error { } func (cli *DockerCli) CmdSearch(args ...string) error { - cmd := cli.Subcmd("search", "TERM", "Search the Docker Hub for images") + cmd := cli.Subcmd("search", "TERM", "Search the Docker Hub for images", true) noTrunc := cmd.Bool([]string{"#notrunc", "-no-trunc"}, false, "Don't truncate output") trusted := cmd.Bool([]string{"#t", "#trusted", "#-trusted"}, false, "Only show trusted builds") automated := cmd.Bool([]string{"-automated"}, false, "Only show automated builds") stars := cmd.Int([]string{"s", "#stars", "-stars"}, 0, "Only displays with at least x stars") cmd.Require(flag.Exact, 1) - if err := utils.ParseFlags(cmd, args, true); err != nil { - return nil - } + utils.ParseFlags(cmd, args, true) v := url.Values{} v.Set("term", cmd.Arg(0)) @@ -2010,13 +1950,11 @@ func (cli *DockerCli) CmdSearch(args ...string) error { type ports []int func (cli *DockerCli) CmdTag(args ...string) error { - cmd := cli.Subcmd("tag", "IMAGE[:TAG] [REGISTRYHOST/][USERNAME/]NAME[:TAG]", "Tag an image into a repository") + cmd := cli.Subcmd("tag", "IMAGE[:TAG] [REGISTRYHOST/][USERNAME/]NAME[:TAG]", "Tag an image into a repository", true) force := cmd.Bool([]string{"f", "#force", "-force"}, false, "Force") cmd.Require(flag.Exact, 2) - if err := utils.ParseFlags(cmd, args, true); err != nil { - return nil - } + utils.ParseFlags(cmd, args, true) var ( repository, tag = parsers.ParseRepositoryTag(cmd.Arg(1)) @@ -2177,7 +2115,7 @@ func (cli *DockerCli) createContainer(config *runconfig.Config, hostConfig *runc } func (cli *DockerCli) CmdCreate(args ...string) error { - cmd := cli.Subcmd("create", "IMAGE [COMMAND] [ARG...]", "Create a new container") + cmd := cli.Subcmd("create", "IMAGE [COMMAND] [ARG...]", "Create a new container", true) // These are flags not stored in Config/HostConfig var ( @@ -2205,7 +2143,7 @@ func (cli *DockerCli) CmdCreate(args ...string) error { func (cli *DockerCli) CmdRun(args ...string) error { // FIXME: just use runconfig.Parse already - cmd := cli.Subcmd("run", "IMAGE [COMMAND] [ARG...]", "Run a command in a new container") + cmd := cli.Subcmd("run", "IMAGE [COMMAND] [ARG...]", "Run a command in a new container", true) // These are flags not stored in Config/HostConfig var ( @@ -2221,6 +2159,7 @@ func (cli *DockerCli) CmdRun(args ...string) error { ) config, hostConfig, cmd, err := runconfig.Parse(cmd, args) + // just in case the Parse does not exit if err != nil { return &utils.StatusError{StatusCode: 1} } @@ -2405,12 +2344,10 @@ func (cli *DockerCli) CmdRun(args ...string) error { } func (cli *DockerCli) CmdCp(args ...string) error { - cmd := cli.Subcmd("cp", "CONTAINER:PATH HOSTPATH", "Copy files/folders from the PATH to the HOSTPATH") + cmd := cli.Subcmd("cp", "CONTAINER:PATH HOSTPATH", "Copy files/folders from the PATH to the HOSTPATH", true) cmd.Require(flag.Exact, 2) - if err := utils.ParseFlags(cmd, args, true); err != nil { - return nil - } + utils.ParseFlags(cmd, args, true) var copyData engine.Env info := strings.Split(cmd.Arg(0), ":") @@ -2442,13 +2379,11 @@ func (cli *DockerCli) CmdCp(args ...string) error { } func (cli *DockerCli) CmdSave(args ...string) error { - cmd := cli.Subcmd("save", "IMAGE [IMAGE...]", "Save an image(s) to a tar archive (streamed to STDOUT by default)") + cmd := cli.Subcmd("save", "IMAGE [IMAGE...]", "Save an image(s) to a tar archive (streamed to STDOUT by default)", true) outfile := cmd.String([]string{"o", "-output"}, "", "Write to an file, instead of STDOUT") cmd.Require(flag.Min, 1) - if err := utils.ParseFlags(cmd, args, true); err != nil { - return err - } + utils.ParseFlags(cmd, args, true) var ( output io.Writer = cli.out @@ -2481,13 +2416,11 @@ func (cli *DockerCli) CmdSave(args ...string) error { } func (cli *DockerCli) CmdLoad(args ...string) error { - cmd := cli.Subcmd("load", "", "Load an image from a tar archive on STDIN") + cmd := cli.Subcmd("load", "", "Load an image from a tar archive on STDIN", true) infile := cmd.String([]string{"i", "-input"}, "", "Read from a tar archive file, instead of STDIN") cmd.Require(flag.Exact, 0) - if err := utils.ParseFlags(cmd, args, true); err != nil { - return err - } + utils.ParseFlags(cmd, args, true) var ( input io.Reader = cli.in @@ -2506,9 +2439,10 @@ func (cli *DockerCli) CmdLoad(args ...string) error { } func (cli *DockerCli) CmdExec(args ...string) error { - cmd := cli.Subcmd("exec", "CONTAINER COMMAND [ARG...]", "Run a command in a running container") + cmd := cli.Subcmd("exec", "CONTAINER COMMAND [ARG...]", "Run a command in a running container", true) execConfig, err := runconfig.ParseExec(cmd, args) + // just in case the ParseExec does not exit if execConfig.Container == "" || err != nil { return &utils.StatusError{StatusCode: 1} } diff --git a/utils/flags.go b/utils/flags.go index 92449070e..8f47780bc 100644 --- a/utils/flags.go +++ b/utils/flags.go @@ -8,7 +8,10 @@ import ( ) // ParseFlags is a utility function that adds a help flag if withHelp is true, -// calls cmd.Parse(args) and prints a relevant error message if there are incorrect number of arguments. +// calls cmd.Parse(args) and prints a relevant error message if there are +// incorrect number of arguments. It returns error only if error handling is +// set to ContinueOnError and parsing fails. If error handling is set to +// ExitOnError, it's safe to ignore the return value. // TODO: move this to a better package than utils func ParseFlags(cmd *flag.FlagSet, args []string, withHelp bool) error { var help *bool From 1db927113f3e7af83fac548c0e2da05bccbd8d56 Mon Sep 17 00:00:00 2001 From: Michal Minar Date: Tue, 6 Jan 2015 16:44:36 +0100 Subject: [PATCH 179/513] Amended TestExecParseError test Usage string isn't printed anymore. User is adviced to see help instead - according to coreutils standard. Signed-off-by: Michal Minar --- integration-cli/docker_cli_exec_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/integration-cli/docker_cli_exec_test.go b/integration-cli/docker_cli_exec_test.go index 5bb2dc4bb..2ff39fc83 100644 --- a/integration-cli/docker_cli_exec_test.go +++ b/integration-cli/docker_cli_exec_test.go @@ -362,10 +362,10 @@ func TestExecParseError(t *testing.T) { // Test normal (non-detached) case first cmd := exec.Command(dockerBinary, "exec", "top") - if out, _, err := runCommandWithOutput(cmd); err == nil || !strings.Contains(out, "Usage:") { - t.Fatalf("Should have thrown error & given usage: %s", out) + if _, stderr, code, err := runCommandWithStdoutStderr(cmd); err == nil || !strings.Contains(stderr, "See '"+dockerBinary+" exec --help'") || code == 0 { + t.Fatalf("Should have thrown error & point to help: %s", stderr) } - logDone("exec - error on parseExec should return usage") + logDone("exec - error on parseExec should point to help") } func TestExecStopNotHanging(t *testing.T) { From 32ac770f753a6435cd83f957c1e5d73722d09974 Mon Sep 17 00:00:00 2001 From: Christian Stefanescu Date: Tue, 6 Jan 2015 12:34:14 +0100 Subject: [PATCH 180/513] Fixed minor typo Signed-off-by: Christian Stefanescu --- docs/sources/reference/run.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/reference/run.md b/docs/sources/reference/run.md index 1cd623175..012a6e71f 100644 --- a/docs/sources/reference/run.md +++ b/docs/sources/reference/run.md @@ -254,7 +254,7 @@ the container exits**, you can add the `--rm` flag: --security-opt="label:type:TYPE" : Set the label type for the container --security-opt="label:level:LEVEL" : Set the label level for the container --security-opt="label:disable" : Turn off label confinement for the container - --secutity-opt="apparmor:PROFILE" : Set the apparmor profile to be applied + --security-opt="apparmor:PROFILE" : Set the apparmor profile to be applied to the container You can override the default labeling scheme for each container by specifying From f339d3bceb4634bba1bc27e455d0fd8505df17bf Mon Sep 17 00:00:00 2001 From: Jessica Frazelle Date: Tue, 30 Dec 2014 15:26:12 -0800 Subject: [PATCH 181/513] Add plus to build Docker-DCO-1.1-Signed-off-by: Jessica Frazelle (github: jfrazelle) --- daemon/exec.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/daemon/exec.go b/daemon/exec.go index 616093c67..84d25ae5a 100644 --- a/daemon/exec.go +++ b/daemon/exec.go @@ -1,5 +1,3 @@ -// build linux - package daemon import ( From 6d801a3caa54ad7ef574bc426aa1ffc412c5af82 Mon Sep 17 00:00:00 2001 From: Doug Davis Date: Thu, 23 Oct 2014 14:30:11 -0700 Subject: [PATCH 182/513] Have .dockerignore support Dockerfile/.dockerignore If .dockerignore mentions either then the client will send them to the daemon but the daemon will erase them after the Dockerfile has been parsed to simulate them never being sent in the first place. an events test kept failing for me so I tried to fix that too Closes #8330 Signed-off-by: Doug Davis --- api/client/commands.go | 42 +++++------ builder/evaluator.go | 70 +++++++++++------ builder/internals.go | 10 ++- daemon/container.go | 4 +- daemon/graphdriver/aufs/aufs.go | 4 +- docs/sources/reference/builder.md | 6 ++ graph/load.go | 2 +- integration-cli/docker_cli_build_test.go | 92 +++++++++++++++++++++-- integration-cli/docker_cli_events_test.go | 5 +- pkg/archive/archive.go | 48 ++++++++---- pkg/archive/archive_test.go | 8 +- pkg/chrootarchive/archive.go | 4 +- pkg/chrootarchive/archive_test.go | 2 +- pkg/tarsum/builder_context.go | 20 +++++ utils/utils.go | 32 ++++++++ volumes/volume.go | 6 +- 16 files changed, 271 insertions(+), 84 deletions(-) create mode 100644 pkg/tarsum/builder_context.go diff --git a/api/client/commands.go b/api/client/commands.go index e666d4320..14b01f560 100644 --- a/api/client/commands.go +++ b/api/client/commands.go @@ -14,7 +14,6 @@ import ( "os" "os/exec" "path" - "path/filepath" "runtime" "strconv" "strings" @@ -30,6 +29,7 @@ import ( "github.com/docker/docker/nat" "github.com/docker/docker/opts" "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/fileutils" flag "github.com/docker/docker/pkg/mflag" "github.com/docker/docker/pkg/parsers" "github.com/docker/docker/pkg/parsers/filters" @@ -143,32 +143,32 @@ func (cli *DockerCli) CmdBuild(args ...string) error { if _, err = os.Stat(filename); os.IsNotExist(err) { return fmt.Errorf("no Dockerfile found in %s", cmd.Arg(0)) } - var excludes []string - ignore, err := ioutil.ReadFile(path.Join(root, ".dockerignore")) - if err != nil && !os.IsNotExist(err) { - return fmt.Errorf("Error reading .dockerignore: '%s'", err) + var includes []string = []string{"."} + + excludes, err := utils.ReadDockerIgnore(path.Join(root, ".dockerignore")) + if err != nil { + return err } - for _, pattern := range strings.Split(string(ignore), "\n") { - pattern = strings.TrimSpace(pattern) - if pattern == "" { - continue - } - pattern = filepath.Clean(pattern) - ok, err := filepath.Match(pattern, "Dockerfile") - if err != nil { - return fmt.Errorf("Bad .dockerignore pattern: '%s', error: %s", pattern, err) - } - if ok { - return fmt.Errorf("Dockerfile was excluded by .dockerignore pattern '%s'", pattern) - } - excludes = append(excludes, pattern) + + // If .dockerignore mentions .dockerignore or Dockerfile + // then make sure we send both files over to the daemon + // because Dockerfile is, obviously, needed no matter what, and + // .dockerignore is needed to know if either one needs to be + // removed. The deamon will remove them for us, if needed, after it + // parses the Dockerfile. + keepThem1, _ := fileutils.Matches(".dockerignore", excludes) + keepThem2, _ := fileutils.Matches("Dockerfile", excludes) + if keepThem1 || keepThem2 { + includes = append(includes, ".dockerignore", "Dockerfile") } + if err = utils.ValidateContextDirectory(root, excludes); err != nil { return fmt.Errorf("Error checking context is accessible: '%s'. Please check permissions and try again.", err) } options := &archive.TarOptions{ - Compression: archive.Uncompressed, - Excludes: excludes, + Compression: archive.Uncompressed, + ExcludePatterns: excludes, + IncludeFiles: includes, } context, err = archive.TarWithOptions(root, options) if err != nil { diff --git a/builder/evaluator.go b/builder/evaluator.go index eef222b94..43fb419fc 100644 --- a/builder/evaluator.go +++ b/builder/evaluator.go @@ -31,6 +31,7 @@ import ( "github.com/docker/docker/builder/parser" "github.com/docker/docker/daemon" "github.com/docker/docker/engine" + "github.com/docker/docker/pkg/fileutils" "github.com/docker/docker/pkg/tarsum" "github.com/docker/docker/registry" "github.com/docker/docker/runconfig" @@ -136,30 +137,10 @@ func (b *Builder) Run(context io.Reader) (string, error) { } }() - filename := path.Join(b.contextPath, "Dockerfile") - - fi, err := os.Stat(filename) - if os.IsNotExist(err) { - return "", fmt.Errorf("Cannot build a directory without a Dockerfile") - } - if fi.Size() == 0 { - return "", ErrDockerfileEmpty - } - - f, err := os.Open(filename) - if err != nil { + if err := b.readDockerfile("Dockerfile"); err != nil { return "", err } - defer f.Close() - - ast, err := parser.Parse(f) - if err != nil { - return "", err - } - - b.dockerfile = ast - // some initializations that would not have been supplied by the caller. b.Config = &runconfig.Config{} b.TmpContainers = map[string]struct{}{} @@ -185,6 +166,53 @@ func (b *Builder) Run(context io.Reader) (string, error) { return b.image, nil } +// Reads a Dockerfile from the current context. It assumes that the +// 'filename' is a relative path from the root of the context +func (b *Builder) readDockerfile(filename string) error { + filename = path.Join(b.contextPath, filename) + + fi, err := os.Stat(filename) + if os.IsNotExist(err) { + return fmt.Errorf("Cannot build a directory without a Dockerfile") + } + if fi.Size() == 0 { + return ErrDockerfileEmpty + } + + f, err := os.Open(filename) + if err != nil { + return err + } + + b.dockerfile, err = parser.Parse(f) + f.Close() + + if err != nil { + return err + } + + // After the Dockerfile has been parsed, we need to check the .dockerignore + // file for either "Dockerfile" or ".dockerignore", and if either are + // present then erase them from the build context. These files should never + // have been sent from the client but we did send them to make sure that + // we had the Dockerfile to actually parse, and then we also need the + // .dockerignore file to know whether either file should be removed. + // Note that this assumes the Dockerfile has been read into memory and + // is now safe to be removed. + + excludes, _ := utils.ReadDockerIgnore(path.Join(b.contextPath, ".dockerignore")) + if rm, _ := fileutils.Matches(".dockerignore", excludes); rm == true { + os.Remove(path.Join(b.contextPath, ".dockerignore")) + b.context.(tarsum.BuilderContext).Remove(".dockerignore") + } + if rm, _ := fileutils.Matches("Dockerfile", excludes); rm == true { + os.Remove(path.Join(b.contextPath, "Dockerfile")) + b.context.(tarsum.BuilderContext).Remove("Dockerfile") + } + + return nil +} + // This method is the entrypoint to all statement handling routines. // // Almost all nodes will have this structure: diff --git a/builder/internals.go b/builder/internals.go index 1caa33141..909e7a8d1 100644 --- a/builder/internals.go +++ b/builder/internals.go @@ -390,7 +390,15 @@ func calcCopyInfo(b *Builder, cmdName string, cInfos *[]*copyInfo, origPath stri for _, fileInfo := range b.context.GetSums() { absFile := path.Join(b.contextPath, fileInfo.Name()) - if strings.HasPrefix(absFile, absOrigPath) || absFile == absOrigPathNoSlash { + // Any file in the context that starts with the given path will be + // picked up and its hashcode used. However, we'll exclude the + // root dir itself. We do this for a coupel of reasons: + // 1 - ADD/COPY will not copy the dir itself, just its children + // so there's no reason to include it in the hash calc + // 2 - the metadata on the dir will change when any child file + // changes. This will lead to a miss in the cache check if that + // child file is in the .dockerignore list. + if strings.HasPrefix(absFile, absOrigPath) && absFile != absOrigPathNoSlash { subfiles = append(subfiles, fileInfo.Sum()) } } diff --git a/daemon/container.go b/daemon/container.go index 75cd133fe..45aaa8be5 100644 --- a/daemon/container.go +++ b/daemon/container.go @@ -891,8 +891,8 @@ func (container *Container) Copy(resource string) (io.ReadCloser, error) { } archive, err := archive.TarWithOptions(basePath, &archive.TarOptions{ - Compression: archive.Uncompressed, - Includes: filter, + Compression: archive.Uncompressed, + IncludeFiles: filter, }) if err != nil { container.Unmount() diff --git a/daemon/graphdriver/aufs/aufs.go b/daemon/graphdriver/aufs/aufs.go index 82a5c8905..220be2de5 100644 --- a/daemon/graphdriver/aufs/aufs.go +++ b/daemon/graphdriver/aufs/aufs.go @@ -300,8 +300,8 @@ func (a *Driver) Put(id string) { func (a *Driver) Diff(id, parent string) (archive.Archive, error) { // AUFS doesn't need the parent layer to produce a diff. return archive.TarWithOptions(path.Join(a.rootPath(), "diff", id), &archive.TarOptions{ - Compression: archive.Uncompressed, - Excludes: []string{".wh..wh.*"}, + Compression: archive.Uncompressed, + ExcludePatterns: []string{".wh..wh.*"}, }) } diff --git a/docs/sources/reference/builder.md b/docs/sources/reference/builder.md index fa7393efa..90862334e 100644 --- a/docs/sources/reference/builder.md +++ b/docs/sources/reference/builder.md @@ -154,6 +154,12 @@ Exclusion patterns match files or directories relative to the source repository that will be excluded from the context. Globbing is done using Go's [filepath.Match](http://golang.org/pkg/path/filepath#Match) rules. +> **Note**: +> The `.dockerignore` file can even be used to ignore the `Dockerfile` and +> `.dockerignore` files. This might be useful if you are copying files from +> the root of the build context into your new containter but do not want to +> include the `Dockerfile` or `.dockerignore` files (e.g. `ADD . /someDir/`). + The following example shows the use of the `.dockerignore` file to exclude the `.git` directory from the context. Its effect can be seen in the changed size of the uploaded context. diff --git a/graph/load.go b/graph/load.go index 6ef219c07..4399da25d 100644 --- a/graph/load.go +++ b/graph/load.go @@ -57,7 +57,7 @@ func (s *TagStore) CmdLoad(job *engine.Job) engine.Status { excludes[i] = k i++ } - if err := chrootarchive.Untar(repoFile, repoDir, &archive.TarOptions{Excludes: excludes}); err != nil { + if err := chrootarchive.Untar(repoFile, repoDir, &archive.TarOptions{ExcludePatterns: excludes}); err != nil { return job.Error(err) } diff --git a/integration-cli/docker_cli_build_test.go b/integration-cli/docker_cli_build_test.go index bb53b68ac..c4d7ff7c5 100644 --- a/integration-cli/docker_cli_build_test.go +++ b/integration-cli/docker_cli_build_test.go @@ -3131,28 +3131,106 @@ func TestBuildDockerignoringDockerfile(t *testing.T) { name := "testbuilddockerignoredockerfile" defer deleteImages(name) dockerfile := ` - FROM scratch` + FROM busybox + ADD . /tmp/ + RUN ! ls /tmp/Dockerfile + RUN ls /tmp/.dockerignore` ctx, err := fakeContext(dockerfile, map[string]string{ - "Dockerfile": "FROM scratch", + "Dockerfile": dockerfile, ".dockerignore": "Dockerfile\n", }) if err != nil { t.Fatal(err) } - defer ctx.Close() - if _, err = buildImageFromContext(name, ctx, true); err == nil { - t.Fatalf("Didn't get expected error from ignoring Dockerfile") + if _, err = buildImageFromContext(name, ctx, true); err != nil { + t.Fatalf("Didn't ignore Dockerfile correctly:%s", err) } // now try it with ./Dockerfile ctx.Add(".dockerignore", "./Dockerfile\n") - if _, err = buildImageFromContext(name, ctx, true); err == nil { - t.Fatalf("Didn't get expected error from ignoring ./Dockerfile") + if _, err = buildImageFromContext(name, ctx, true); err != nil { + t.Fatalf("Didn't ignore ./Dockerfile correctly:%s", err) } logDone("build - test .dockerignore of Dockerfile") } +func TestBuildDockerignoringDockerignore(t *testing.T) { + name := "testbuilddockerignoredockerignore" + defer deleteImages(name) + dockerfile := ` + FROM busybox + ADD . /tmp/ + RUN ! ls /tmp/.dockerignore + RUN ls /tmp/Dockerfile` + ctx, err := fakeContext(dockerfile, map[string]string{ + "Dockerfile": dockerfile, + ".dockerignore": ".dockerignore\n", + }) + defer ctx.Close() + if err != nil { + t.Fatal(err) + } + if _, err = buildImageFromContext(name, ctx, true); err != nil { + t.Fatalf("Didn't ignore .dockerignore correctly:%s", err) + } + logDone("build - test .dockerignore of .dockerignore") +} + +func TestBuildDockerignoreTouchDockerfile(t *testing.T) { + var id1 string + var id2 string + + name := "testbuilddockerignoretouchdockerfile" + defer deleteImages(name) + dockerfile := ` + FROM busybox + ADD . /tmp/` + ctx, err := fakeContext(dockerfile, map[string]string{ + "Dockerfile": dockerfile, + ".dockerignore": "Dockerfile\n", + }) + defer ctx.Close() + if err != nil { + t.Fatal(err) + } + + if id1, err = buildImageFromContext(name, ctx, true); err != nil { + t.Fatalf("Didn't build it correctly:%s", err) + } + + if id2, err = buildImageFromContext(name, ctx, true); err != nil { + t.Fatalf("Didn't build it correctly:%s", err) + } + if id1 != id2 { + t.Fatalf("Didn't use the cache - 1") + } + + // Now make sure touching Dockerfile doesn't invalidate the cache + if err = ctx.Add("Dockerfile", dockerfile+"\n# hi"); err != nil { + t.Fatalf("Didn't add Dockerfile: %s", err) + } + if id2, err = buildImageFromContext(name, ctx, true); err != nil { + t.Fatalf("Didn't build it correctly:%s", err) + } + if id1 != id2 { + t.Fatalf("Didn't use the cache - 2") + } + + // One more time but just 'touch' it instead of changing the content + if err = ctx.Add("Dockerfile", dockerfile+"\n# hi"); err != nil { + t.Fatalf("Didn't add Dockerfile: %s", err) + } + if id2, err = buildImageFromContext(name, ctx, true); err != nil { + t.Fatalf("Didn't build it correctly:%s", err) + } + if id1 != id2 { + t.Fatalf("Didn't use the cache - 3") + } + + logDone("build - test .dockerignore touch dockerfile") +} + func TestBuildDockerignoringWholeDir(t *testing.T) { name := "testbuilddockerignorewholedir" defer deleteImages(name) diff --git a/integration-cli/docker_cli_events_test.go b/integration-cli/docker_cli_events_test.go index be6f1202c..322d622b5 100644 --- a/integration-cli/docker_cli_events_test.go +++ b/integration-cli/docker_cli_events_test.go @@ -10,14 +10,13 @@ import ( ) func TestEventsUntag(t *testing.T) { - out, _, _ := dockerCmd(t, "images", "-q") - image := strings.Split(out, "\n")[0] + image := "busybox" dockerCmd(t, "tag", image, "utest:tag1") dockerCmd(t, "tag", image, "utest:tag2") dockerCmd(t, "rmi", "utest:tag1") dockerCmd(t, "rmi", "utest:tag2") eventsCmd := exec.Command("timeout", "0.2", dockerBinary, "events", "--since=1") - out, _, _ = runCommandWithOutput(eventsCmd) + out, _, _ := runCommandWithOutput(eventsCmd) events := strings.Split(out, "\n") nEvents := len(events) // The last element after the split above will be an empty string, so we diff --git a/pkg/archive/archive.go b/pkg/archive/archive.go index ec45d8546..35566520b 100644 --- a/pkg/archive/archive.go +++ b/pkg/archive/archive.go @@ -30,11 +30,11 @@ type ( ArchiveReader io.Reader Compression int TarOptions struct { - Includes []string - Excludes []string - Compression Compression - NoLchown bool - Name string + IncludeFiles []string + ExcludePatterns []string + Compression Compression + NoLchown bool + Name string } // Archiver allows the reuse of most utility functions of this package @@ -378,7 +378,7 @@ func escapeName(name string) string { } // TarWithOptions creates an archive from the directory at `path`, only including files whose relative -// paths are included in `options.Includes` (if non-nil) or not in `options.Excludes`. +// paths are included in `options.IncludeFiles` (if non-nil) or not in `options.ExcludePatterns`. func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error) { pipeReader, pipeWriter := io.Pipe() @@ -401,12 +401,14 @@ func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error) // mutating the filesystem and we can see transient errors // from this - if options.Includes == nil { - options.Includes = []string{"."} + if options.IncludeFiles == nil { + options.IncludeFiles = []string{"."} } + seen := make(map[string]bool) + var renamedRelFilePath string // For when tar.Options.Name is set - for _, include := range options.Includes { + for _, include := range options.IncludeFiles { filepath.Walk(filepath.Join(srcPath, include), func(filePath string, f os.FileInfo, err error) error { if err != nil { log.Debugf("Tar: Can't stat file %s to tar: %s", srcPath, err) @@ -420,10 +422,19 @@ func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error) return nil } - skip, err := fileutils.Matches(relFilePath, options.Excludes) - if err != nil { - log.Debugf("Error matching %s", relFilePath, err) - return err + skip := false + + // If "include" is an exact match for the current file + // then even if there's an "excludePatterns" pattern that + // matches it, don't skip it. IOW, assume an explicit 'include' + // is asking for that file no matter what - which is true + // for some files, like .dockerignore and Dockerfile (sometimes) + if include != relFilePath { + skip, err = fileutils.Matches(relFilePath, options.ExcludePatterns) + if err != nil { + log.Debugf("Error matching %s", relFilePath, err) + return err + } } if skip { @@ -433,6 +444,11 @@ func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error) return nil } + if seen[relFilePath] { + return nil + } + seen[relFilePath] = true + // Rename the base resource if options.Name != "" && filePath == srcPath+"/"+filepath.Base(relFilePath) { renamedRelFilePath = relFilePath @@ -487,7 +503,7 @@ loop: // This keeps "../" as-is, but normalizes "/../" to "/" hdr.Name = filepath.Clean(hdr.Name) - for _, exclude := range options.Excludes { + for _, exclude := range options.ExcludePatterns { if strings.HasPrefix(hdr.Name, exclude) { continue loop } @@ -563,8 +579,8 @@ func Untar(archive io.Reader, dest string, options *TarOptions) error { if options == nil { options = &TarOptions{} } - if options.Excludes == nil { - options.Excludes = []string{} + if options.ExcludePatterns == nil { + options.ExcludePatterns = []string{} } decompressedArchive, err := DecompressStream(archive) if err != nil { diff --git a/pkg/archive/archive_test.go b/pkg/archive/archive_test.go index fdba6fb87..6cd95d5ad 100644 --- a/pkg/archive/archive_test.go +++ b/pkg/archive/archive_test.go @@ -165,8 +165,8 @@ func TestTarUntar(t *testing.T) { Gzip, } { changes, err := tarUntar(t, origin, &TarOptions{ - Compression: c, - Excludes: []string{"3"}, + Compression: c, + ExcludePatterns: []string{"3"}, }) if err != nil { @@ -196,8 +196,8 @@ func TestTarWithOptions(t *testing.T) { opts *TarOptions numChanges int }{ - {&TarOptions{Includes: []string{"1"}}, 1}, - {&TarOptions{Excludes: []string{"2"}}, 1}, + {&TarOptions{IncludeFiles: []string{"1"}}, 1}, + {&TarOptions{ExcludePatterns: []string{"2"}}, 1}, } for _, testCase := range cases { changes, err := tarUntar(t, origin, testCase.opts) diff --git a/pkg/chrootarchive/archive.go b/pkg/chrootarchive/archive.go index 66f837314..ae15a2a54 100644 --- a/pkg/chrootarchive/archive.go +++ b/pkg/chrootarchive/archive.go @@ -50,8 +50,8 @@ func Untar(tarArchive io.Reader, dest string, options *archive.TarOptions) error if options == nil { options = &archive.TarOptions{} } - if options.Excludes == nil { - options.Excludes = []string{} + if options.ExcludePatterns == nil { + options.ExcludePatterns = []string{} } var ( diff --git a/pkg/chrootarchive/archive_test.go b/pkg/chrootarchive/archive_test.go index bb8a22dc7..b3f7d5768 100644 --- a/pkg/chrootarchive/archive_test.go +++ b/pkg/chrootarchive/archive_test.go @@ -40,7 +40,7 @@ func TestChrootTarUntar(t *testing.T) { if err := os.MkdirAll(dest, 0700); err != nil { t.Fatal(err) } - if err := Untar(stream, dest, &archive.TarOptions{Excludes: []string{"lolo"}}); err != nil { + if err := Untar(stream, dest, &archive.TarOptions{ExcludePatterns: []string{"lolo"}}); err != nil { t.Fatal(err) } } diff --git a/pkg/tarsum/builder_context.go b/pkg/tarsum/builder_context.go new file mode 100644 index 000000000..06a42825e --- /dev/null +++ b/pkg/tarsum/builder_context.go @@ -0,0 +1,20 @@ +package tarsum + +// This interface extends TarSum by adding the Remove method. In general +// there was concern about adding this method to TarSum itself so instead +// it is being added just to "BuilderContext" which will then only be used +// during the .dockerignore file processing - see builder/evaluator.go +type BuilderContext interface { + TarSum + Remove(string) +} + +func (bc *tarSum) Remove(filename string) { + for i, fis := range bc.sums { + if fis.Name() == filename { + bc.sums = append(bc.sums[:i], bc.sums[i+1:]...) + // Note, we don't just return because there could be + // more than one with this name + } + } +} diff --git a/utils/utils.go b/utils/utils.go index 8d3b3eb73..4e36b4061 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -1,6 +1,7 @@ package utils import ( + "bufio" "bytes" "crypto/rand" "crypto/sha1" @@ -492,3 +493,34 @@ func StringsContainsNoCase(slice []string, s string) bool { } return false } + +// Reads a .dockerignore file and returns the list of file patterns +// to ignore. Note this will trim whitespace from each line as well +// as use GO's "clean" func to get the shortest/cleanest path for each. +func ReadDockerIgnore(path string) ([]string, error) { + // Note that a missing .dockerignore file isn't treated as an error + reader, err := os.Open(path) + if err != nil { + if !os.IsNotExist(err) { + return nil, fmt.Errorf("Error reading '%s': %v", path, err) + } + return nil, nil + } + defer reader.Close() + + scanner := bufio.NewScanner(reader) + var excludes []string + + for scanner.Scan() { + pattern := strings.TrimSpace(scanner.Text()) + if pattern == "" { + continue + } + pattern = filepath.Clean(pattern) + excludes = append(excludes, pattern) + } + if err = scanner.Err(); err != nil { + return nil, fmt.Errorf("Error reading '%s': %v", path, err) + } + return excludes, nil +} diff --git a/volumes/volume.go b/volumes/volume.go index d718b07d7..db99aed5d 100644 --- a/volumes/volume.go +++ b/volumes/volume.go @@ -47,9 +47,9 @@ func (v *Volume) Export(resource, name string) (io.ReadCloser, error) { basePath = path.Dir(basePath) } return archive.TarWithOptions(basePath, &archive.TarOptions{ - Compression: archive.Uncompressed, - Name: name, - Includes: filter, + Compression: archive.Uncompressed, + Name: name, + IncludeFiles: filter, }) } From 32f1025b22d16872ead5ec2e3650bf76622fae99 Mon Sep 17 00:00:00 2001 From: Jessica Frazelle Date: Sun, 28 Dec 2014 15:08:42 -0800 Subject: [PATCH 183/513] Add error when running overlay over btrfs. Docker-DCO-1.1-Signed-off-by: Jessica Frazelle (github: jfrazelle) --- daemon/graphdriver/overlay/overlay.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/daemon/graphdriver/overlay/overlay.go b/daemon/graphdriver/overlay/overlay.go index 68b6b0ed3..c59d0ea8f 100644 --- a/daemon/graphdriver/overlay/overlay.go +++ b/daemon/graphdriver/overlay/overlay.go @@ -99,6 +99,21 @@ func Init(home string, options []string) (graphdriver.Driver, error) { return nil, graphdriver.ErrNotSupported } + // check if they are running over btrfs + var buf syscall.Statfs_t + if err := syscall.Statfs(path.Dir(home), &buf); err != nil { + return nil, err + } + + switch graphdriver.FsMagic(buf.Type) { + case graphdriver.FsMagicBtrfs: + log.Error("'overlay' is not supported over btrfs.") + return nil, graphdriver.ErrIncompatibleFS + case graphdriver.FsMagicAufs: + log.Error("'overlay' is not supported over aufs.") + return nil, graphdriver.ErrIncompatibleFS + } + // Create the driver home dir if err := os.MkdirAll(home, 0755); err != nil && !os.IsExist(err) { return nil, err From 83ef40cbe98e450e1a66bda6eb663be02e5eb7a8 Mon Sep 17 00:00:00 2001 From: Jessica Frazelle Date: Tue, 6 Jan 2015 11:16:33 -0800 Subject: [PATCH 184/513] Cleanup unnecessary var. Docker-DCO-1.1-Signed-off-by: Jessica Frazelle (github: jfrazelle) --- daemon/daemon.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/daemon/daemon.go b/daemon/daemon.go index f02b99485..8ad677bed 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -151,12 +151,8 @@ func (daemon *Daemon) Install(eng *engine.Engine) error { // Get looks for a container by the specified ID or name, and returns it. // If the container is not found, or if an error occurs, nil is returned. func (daemon *Daemon) Get(name string) *Container { - var ( - id string - err error - ) - - if id, err = daemon.idIndex.Get(name); err == nil { + id, err := daemon.idIndex.Get(name) + if err == nil { return daemon.containers.Get(id) } From cc3bf34c787f4fdf067a459158f38f8413ec4e33 Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Tue, 6 Jan 2015 15:26:04 -0800 Subject: [PATCH 185/513] Update API docs to reflect correct values for /info Signed-off-by: Brian Goff --- docs/sources/reference/api/docker_remote_api_v1.16.md | 6 +++++- docs/sources/reference/api/docker_remote_api_v1.17.md | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/sources/reference/api/docker_remote_api_v1.16.md b/docs/sources/reference/api/docker_remote_api_v1.16.md index 43df4306c..500f1bea3 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.16.md +++ b/docs/sources/reference/api/docker_remote_api_v1.16.md @@ -1223,6 +1223,7 @@ Display system-wide information "Containers":11, "Images":16, "Driver":"btrfs", + "DriverStatus": [[""]], "ExecutionDriver":"native-0.1", "KernelVersion":"3.12.0-1-amd64" "NCPU":1, @@ -1234,11 +1235,14 @@ Display system-wide information "NGoroutines":21, "NEventsListener":0, "InitPath":"/usr/bin/docker", + "InitSha1":"", "IndexServerAddress":["https://index.docker.io/v1/"], "MemoryLimit":true, "SwapLimit":false, "IPv4Forwarding":true, - "Labels":["storage=ssd"] + "Labels":["storage=ssd"], + "DockerRootDir": "/var/lib/docker", + "OperatingSystem": "Boot2Docker", } Status Codes: diff --git a/docs/sources/reference/api/docker_remote_api_v1.17.md b/docs/sources/reference/api/docker_remote_api_v1.17.md index e0ed08478..645eabe9c 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.17.md +++ b/docs/sources/reference/api/docker_remote_api_v1.17.md @@ -1232,6 +1232,7 @@ Display system-wide information "Containers":11, "Images":16, "Driver":"btrfs", + "DriverStatus": [[""]], "ExecutionDriver":"native-0.1", "KernelVersion":"3.12.0-1-amd64" "NCPU":1, @@ -1243,11 +1244,14 @@ Display system-wide information "NGoroutines":21, "NEventsListener":0, "InitPath":"/usr/bin/docker", + "InitSha1":"", "IndexServerAddress":["https://index.docker.io/v1/"], "MemoryLimit":true, "SwapLimit":false, "IPv4Forwarding":true, - "Labels":["storage=ssd"] + "Labels":["storage=ssd"], + "DockerRootDir": "/var/lib/docker", + "OperatingSystem": "Boot2Docker", } Status Codes: From eb3ea3b43c716ad727521a7d0bc20d7321bb0867 Mon Sep 17 00:00:00 2001 From: Doug Davis Date: Thu, 11 Sep 2014 07:42:17 -0700 Subject: [PATCH 186/513] Allow for Dockerfile to be named something else. Add a check to make sure Dockerfile is in the build context Add docs and a testcase Make -f relative to current dir, not build context Signed-off-by: Doug Davis --- api/client/commands.go | 52 ++++++- api/common.go | 7 +- api/server/server.go | 1 + builder/evaluator.go | 23 +-- builder/job.go | 10 +- docs/man/docker-build.1.md | 4 + .../reference/api/docker_remote_api_v1.17.md | 17 ++- docs/sources/reference/commandline/cli.md | 40 +++++- integration-cli/docker_cli_build_test.go | 131 ++++++++++++++++++ 9 files changed, 253 insertions(+), 32 deletions(-) diff --git a/api/client/commands.go b/api/client/commands.go index 3b22c722c..7701e17da 100644 --- a/api/client/commands.go +++ b/api/client/commands.go @@ -14,6 +14,7 @@ import ( "os" "os/exec" "path" + "path/filepath" "runtime" "strconv" "strings" @@ -84,6 +85,8 @@ func (cli *DockerCli) CmdBuild(args ...string) error { rm := cmd.Bool([]string{"#rm", "-rm"}, true, "Remove intermediate containers after a successful build") forceRm := cmd.Bool([]string{"-force-rm"}, false, "Always remove intermediate containers, even after unsuccessful builds") pull := cmd.Bool([]string{"-pull"}, false, "Always attempt to pull a newer version of the image") + dockerfileName := cmd.String([]string{"f", "-file"}, "", "Name of the Dockerfile(Default is 'Dockerfile' at context root)") + cmd.Require(flag.Exact, 1) utils.ParseFlags(cmd, args, true) @@ -109,7 +112,10 @@ func (cli *DockerCli) CmdBuild(args ...string) error { if err != nil { return fmt.Errorf("failed to read Dockerfile from STDIN: %v", err) } - context, err = archive.Generate("Dockerfile", string(dockerfile)) + if *dockerfileName == "" { + *dockerfileName = api.DefaultDockerfileName + } + context, err = archive.Generate(*dockerfileName, string(dockerfile)) } else { context = ioutil.NopCloser(buf) } @@ -136,9 +142,40 @@ func (cli *DockerCli) CmdBuild(args ...string) error { if _, err := os.Stat(root); err != nil { return err } - filename := path.Join(root, "Dockerfile") + + absRoot, err := filepath.Abs(root) + if err != nil { + return err + } + + var filename string // path to Dockerfile + var origDockerfile string // used for error msg + + if *dockerfileName == "" { + // No -f/--file was specified so use the default + origDockerfile = api.DefaultDockerfileName + *dockerfileName = origDockerfile + filename = path.Join(absRoot, *dockerfileName) + } else { + origDockerfile = *dockerfileName + if filename, err = filepath.Abs(*dockerfileName); err != nil { + return err + } + + // Verify that 'filename' is within the build context + if !strings.HasSuffix(absRoot, string(os.PathSeparator)) { + absRoot += string(os.PathSeparator) + } + if !strings.HasPrefix(filename, absRoot) { + return fmt.Errorf("The Dockerfile (%s) must be within the build context (%s)", *dockerfileName, root) + } + + // Now reset the dockerfileName to be relative to the build context + *dockerfileName = filename[len(absRoot):] + } + if _, err = os.Stat(filename); os.IsNotExist(err) { - return fmt.Errorf("no Dockerfile found in %s", cmd.Arg(0)) + return fmt.Errorf("Can not locate Dockerfile: %s", origDockerfile) } var includes []string = []string{"."} @@ -147,16 +184,16 @@ func (cli *DockerCli) CmdBuild(args ...string) error { return err } - // If .dockerignore mentions .dockerignore or Dockerfile + // If .dockerignore mentions .dockerignore or the Dockerfile // then make sure we send both files over to the daemon // because Dockerfile is, obviously, needed no matter what, and // .dockerignore is needed to know if either one needs to be // removed. The deamon will remove them for us, if needed, after it // parses the Dockerfile. keepThem1, _ := fileutils.Matches(".dockerignore", excludes) - keepThem2, _ := fileutils.Matches("Dockerfile", excludes) + keepThem2, _ := fileutils.Matches(*dockerfileName, excludes) if keepThem1 || keepThem2 { - includes = append(includes, ".dockerignore", "Dockerfile") + includes = append(includes, ".dockerignore", *dockerfileName) } if err = utils.ValidateContextDirectory(root, excludes); err != nil { @@ -219,6 +256,9 @@ func (cli *DockerCli) CmdBuild(args ...string) error { if *pull { v.Set("pull", "1") } + + v.Set("dockerfile", *dockerfileName) + cli.LoadConfigFile() headers := http.Header(make(map[string][]string)) diff --git a/api/common.go b/api/common.go index 71e72f69e..b8e7c84b3 100644 --- a/api/common.go +++ b/api/common.go @@ -15,9 +15,10 @@ import ( ) const ( - APIVERSION version.Version = "1.16" - DEFAULTHTTPHOST = "127.0.0.1" - DEFAULTUNIXSOCKET = "/var/run/docker.sock" + APIVERSION version.Version = "1.16" + DEFAULTHTTPHOST = "127.0.0.1" + DEFAULTUNIXSOCKET = "/var/run/docker.sock" + DefaultDockerfileName string = "Dockerfile" ) func ValidateHost(val string) (string, error) { diff --git a/api/server/server.go b/api/server/server.go index 6b15962b2..cfaa5f43a 100644 --- a/api/server/server.go +++ b/api/server/server.go @@ -1035,6 +1035,7 @@ func postBuild(eng *engine.Engine, version version.Version, w http.ResponseWrite } job.Stdin.Add(r.Body) job.Setenv("remote", r.FormValue("remote")) + job.Setenv("dockerfile", r.FormValue("dockerfile")) job.Setenv("t", r.FormValue("t")) job.Setenv("q", r.FormValue("q")) job.Setenv("nocache", r.FormValue("nocache")) diff --git a/builder/evaluator.go b/builder/evaluator.go index 43fb419fc..3149bd0df 100644 --- a/builder/evaluator.go +++ b/builder/evaluator.go @@ -105,13 +105,14 @@ type Builder struct { // both of these are controlled by the Remove and ForceRemove options in BuildOpts TmpContainers map[string]struct{} // a map of containers used for removes - dockerfile *parser.Node // the syntax tree of the dockerfile - image string // image name for commit processing - maintainer string // maintainer name. could probably be removed. - cmdSet bool // indicates is CMD was set in current Dockerfile - context tarsum.TarSum // the context is a tarball that is uploaded by the client - contextPath string // the path of the temporary directory the local context is unpacked to (server side) - noBaseImage bool // indicates that this build does not start from any base image, but is being built from an empty file system. + dockerfileName string // name of Dockerfile + dockerfile *parser.Node // the syntax tree of the dockerfile + image string // image name for commit processing + maintainer string // maintainer name. could probably be removed. + cmdSet bool // indicates is CMD was set in current Dockerfile + context tarsum.TarSum // the context is a tarball that is uploaded by the client + contextPath string // the path of the temporary directory the local context is unpacked to (server side) + noBaseImage bool // indicates that this build does not start from any base image, but is being built from an empty file system. } // Run the builder with the context. This is the lynchpin of this package. This @@ -137,7 +138,7 @@ func (b *Builder) Run(context io.Reader) (string, error) { } }() - if err := b.readDockerfile("Dockerfile"); err != nil { + if err := b.readDockerfile(b.dockerfileName); err != nil { return "", err } @@ -205,9 +206,9 @@ func (b *Builder) readDockerfile(filename string) error { os.Remove(path.Join(b.contextPath, ".dockerignore")) b.context.(tarsum.BuilderContext).Remove(".dockerignore") } - if rm, _ := fileutils.Matches("Dockerfile", excludes); rm == true { - os.Remove(path.Join(b.contextPath, "Dockerfile")) - b.context.(tarsum.BuilderContext).Remove("Dockerfile") + if rm, _ := fileutils.Matches(b.dockerfileName, excludes); rm == true { + os.Remove(path.Join(b.contextPath, b.dockerfileName)) + b.context.(tarsum.BuilderContext).Remove(b.dockerfileName) } return nil diff --git a/builder/job.go b/builder/job.go index 20299d490..905a8cc99 100644 --- a/builder/job.go +++ b/builder/job.go @@ -6,6 +6,7 @@ import ( "os" "os/exec" + "github.com/docker/docker/api" "github.com/docker/docker/daemon" "github.com/docker/docker/engine" "github.com/docker/docker/graph" @@ -30,6 +31,7 @@ func (b *BuilderJob) CmdBuild(job *engine.Job) engine.Status { return job.Errorf("Usage: %s\n", job.Name) } var ( + dockerfileName = job.Getenv("dockerfile") remoteURL = job.Getenv("remote") repoName = job.Getenv("t") suppressOutput = job.GetenvBool("q") @@ -42,6 +44,7 @@ func (b *BuilderJob) CmdBuild(job *engine.Job) engine.Status { tag string context io.ReadCloser ) + job.GetenvJson("authConfig", authConfig) job.GetenvJson("configFile", configFile) @@ -57,6 +60,10 @@ func (b *BuilderJob) CmdBuild(job *engine.Job) engine.Status { } } + if dockerfileName == "" { + dockerfileName = api.DefaultDockerfileName + } + if remoteURL == "" { context = ioutil.NopCloser(job.Stdin) } else if urlutil.IsGitURL(remoteURL) { @@ -88,7 +95,7 @@ func (b *BuilderJob) CmdBuild(job *engine.Job) engine.Status { if err != nil { return job.Error(err) } - c, err := archive.Generate("Dockerfile", string(dockerFile)) + c, err := archive.Generate(dockerfileName, string(dockerFile)) if err != nil { return job.Error(err) } @@ -118,6 +125,7 @@ func (b *BuilderJob) CmdBuild(job *engine.Job) engine.Status { StreamFormatter: sf, AuthConfig: authConfig, AuthConfigFile: configFile, + dockerfileName: dockerfileName, } id, err := builder.Run(context) diff --git a/docs/man/docker-build.1.md b/docs/man/docker-build.1.md index c5dfa706c..56e0807df 100644 --- a/docs/man/docker-build.1.md +++ b/docs/man/docker-build.1.md @@ -7,6 +7,7 @@ docker-build - Build a new image from the source code at PATH # SYNOPSIS **docker build** [**--help**] +[**-f**|**--file**[=*Dockerfile*]] [**--force-rm**[=*false*]] [**--no-cache**[=*false*]] [**-q**|**--quiet**[=*false*]] @@ -31,6 +32,9 @@ When a Git repository is set as the **URL**, the repository is used as context. # OPTIONS +**-f**, **--file**=*Dockerfile* + Path to the Dockerfile to use. If the path is a relative path then it must be relative to the current directory. The file must be within the build context. The default is *Dockerfile*. + **--force-rm**=*true*|*false* Always remove intermediate containers, even after unsuccessful builds. The default is *false*. diff --git a/docs/sources/reference/api/docker_remote_api_v1.17.md b/docs/sources/reference/api/docker_remote_api_v1.17.md index e0ed08478..1e4acd7aa 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.17.md +++ b/docs/sources/reference/api/docker_remote_api_v1.17.md @@ -1157,16 +1157,21 @@ Build an image from Dockerfile via stdin {"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 input 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 [*ADD build - command*](/reference/builder/#dockerbuilder)). +The archive must include a build instructions file, typically called +`Dockerfile` at the root of the archive. The `f` parameter may be used +to specify a different build instructions file by having its value be +the path to the alternate build instructions file to use. + +The archive may include any number of other files, +which will be accessible in the build context (See the [*ADD build +command*](/reference/builder/#dockerbuilder)). Query Parameters: +- **dockerfile** - path within the build context to the Dockerfile - **t** – repository name (and optionally a tag) to be applied to the resulting image in case of success - **q** – suppress verbose build output diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index 2d7caedc7..799c0148c 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -461,11 +461,12 @@ To kill the container, use `docker kill`. Build a new image from the source code at PATH - --force-rm=false Always remove intermediate containers, even after unsuccessful builds - --no-cache=false Do not use cache when building the image - -q, --quiet=false Suppress the verbose output generated by the containers - --rm=true Remove intermediate containers after a successful build - -t, --tag="" Repository name (and optionally a tag) to be applied to the resulting image in case of success + -f, --file="" Location of the Dockerfile to use. Default is 'Dockerfile' at the root of the build context + --force-rm=false Always remove intermediate containers, even after unsuccessful builds + --no-cache=false Do not use cache when building the image + -q, --quiet=false Suppress the verbose output generated by the containers + --rm=true Remove intermediate containers after a successful build + -t, --tag="" Repository name (and optionally a tag) to be applied to the resulting image in case of success Use this command to build Docker images from a Dockerfile and a "context". @@ -510,6 +511,13 @@ For example, the files `tempa`, `tempb` are ignored from the root directory. Currently there is no support for regular expressions. Formats like `[^temp*]` are ignored. +By default the `docker build` command will look for a `Dockerfile` at the +root of the build context. The `-f`, `--file`, option lets you specify +the path to an alternative file to use instead. This is useful +in cases where the same set of files are used for multiple builds. The path +must be to a file within the build context. If a relative path is specified +then it must to be relative to the current directory. + See also: @@ -612,6 +620,28 @@ repository is used as Dockerfile. Note that you can specify an arbitrary Git repository by using the `git://` or `git@` schema. + $ sudo docker build -f Dockerfile.debug . + +This will use a file called `Dockerfile.debug` for the build +instructions instead of `Dockerfile`. + + $ sudo docker build -f dockerfiles/Dockerfile.debug -t myapp_debug . + $ sudo docker build -f dockerfiles/Dockerfile.prod -t myapp_prod . + +The above commands will build the current build context (as specified by +the `.`) twice, once using a debug version of a `Dockerfile` and once using +a production version. + + $ cd /home/me/myapp/some/dir/really/deep + $ sudo docker build -f /home/me/myapp/dockerfiles/debug /home/me/myapp + $ sudo docker build -f ../../../../dockerfiles/debug /home/me/myapp + +These two `docker build` commands do the exact same thing. They both +use the contents of the `debug` file instead of looking for a `Dockerfile` +and will use `/home/me/myapp` as the root of the build context. Note that +`debug` is in the directory structure of the build context, regardless of how +you refer to it on the command line. + > **Note:** `docker build` will return a `no such file or directory` error > if the file or directory does not exist in the uploaded context. This may > happen if there is no context, or if you specify a file that is elsewhere diff --git a/integration-cli/docker_cli_build_test.go b/integration-cli/docker_cli_build_test.go index c4d7ff7c5..a27aecc56 100644 --- a/integration-cli/docker_cli_build_test.go +++ b/integration-cli/docker_cli_build_test.go @@ -3155,6 +3155,36 @@ func TestBuildDockerignoringDockerfile(t *testing.T) { logDone("build - test .dockerignore of Dockerfile") } +func TestBuildDockerignoringRenamedDockerfile(t *testing.T) { + name := "testbuilddockerignoredockerfile" + defer deleteImages(name) + dockerfile := ` + FROM busybox + ADD . /tmp/ + RUN ls /tmp/Dockerfile + RUN ! ls /tmp/MyDockerfile + RUN ls /tmp/.dockerignore` + ctx, err := fakeContext(dockerfile, map[string]string{ + "Dockerfile": "Should not use me", + "MyDockerfile": dockerfile, + ".dockerignore": "MyDockerfile\n", + }) + if err != nil { + t.Fatal(err) + } + if _, err = buildImageFromContext(name, ctx, true); err != nil { + t.Fatalf("Didn't ignore MyDockerfile correctly:%s", err) + } + + // now try it with ./MyDockerfile + ctx.Add(".dockerignore", "./MyDockerfile\n") + if _, err = buildImageFromContext(name, ctx, true); err != nil { + t.Fatalf("Didn't ignore ./MyDockerfile correctly:%s", err) + } + + logDone("build - test .dockerignore of renamed Dockerfile") +} + func TestBuildDockerignoringDockerignore(t *testing.T) { name := "testbuilddockerignoredockerignore" defer deleteImages(name) @@ -4170,3 +4200,104 @@ CMD cat /foo/file`, logDone("build - volumes retain contents in build") } + +func TestBuildRenamedDockerfile(t *testing.T) { + defer deleteAllContainers() + + ctx, err := fakeContext(`FROM busybox + RUN echo from Dockerfile`, + map[string]string{ + "Dockerfile": "FROM busybox\nRUN echo from Dockerfile", + "files/Dockerfile": "FROM busybox\nRUN echo from files/Dockerfile", + "files/dFile": "FROM busybox\nRUN echo from files/dFile", + "dFile": "FROM busybox\nRUN echo from dFile", + }) + defer ctx.Close() + if err != nil { + t.Fatal(err) + } + + out, _, err := dockerCmdInDir(t, ctx.Dir, "build", "-t", "test1", ".") + + if err != nil { + t.Fatalf("Failed to build: %s\n%s", out, err) + } + if !strings.Contains(out, "from Dockerfile") { + t.Fatalf("Should have used Dockerfile, output:%s", out) + } + + out, _, err = dockerCmdInDir(t, ctx.Dir, "build", "-f", "files/Dockerfile", "-t", "test2", ".") + + if err != nil { + t.Fatal(err) + } + if !strings.Contains(out, "from files/Dockerfile") { + t.Fatalf("Should have used files/Dockerfile, output:%s", out) + } + + out, _, err = dockerCmdInDir(t, ctx.Dir, "build", "--file=files/dFile", "-t", "test3", ".") + + if err != nil { + t.Fatal(err) + } + if !strings.Contains(out, "from files/dFile") { + t.Fatalf("Should have used files/dFile, output:%s", out) + } + + out, _, err = dockerCmdInDir(t, ctx.Dir, "build", "--file=dFile", "-t", "test4", ".") + + if err != nil { + t.Fatal(err) + } + if !strings.Contains(out, "from dFile") { + t.Fatalf("Should have used dFile, output:%s", out) + } + + out, _, err = dockerCmdInDir(t, ctx.Dir, "build", "--file=/etc/passwd", "-t", "test5", ".") + + if err == nil { + t.Fatalf("Was supposed to fail to find passwd") + } + + if !strings.Contains(out, "The Dockerfile (/etc/passwd) must be within the build context (.)") { + t.Fatalf("Wrong error message for passwd:%v", out) + } + + out, _, err = dockerCmdInDir(t, ctx.Dir+"/files", "build", "-f", "../Dockerfile", "-t", "test5", "..") + + if err != nil { + t.Fatal(err) + } + + if !strings.Contains(out, "from Dockerfile") { + t.Fatalf("Should have used root Dockerfile, output:%s", out) + } + + out, _, err = dockerCmdInDir(t, ctx.Dir+"/files", "build", "-f", ctx.Dir+"/files/Dockerfile", "-t", "test6", "..") + + if err != nil { + t.Fatal(err) + } + + if !strings.Contains(out, "from files/Dockerfile") { + t.Fatalf("Should have used files Dockerfile - 2, output:%s", out) + } + + out, _, err = dockerCmdInDir(t, ctx.Dir+"/files", "build", "-f", "../Dockerfile", "-t", "test7", ".") + + if err == nil || !strings.Contains(out, "must be within the build context") { + t.Fatalf("Should have failed with Dockerfile out of context") + } + + out, _, err = dockerCmdInDir(t, "/tmp", "build", "-t", "test6", ctx.Dir) + + if err != nil { + t.Fatal(err) + } + + if !strings.Contains(out, "from Dockerfile") { + t.Fatalf("Should have used root Dockerfile, output:%s", out) + } + + logDone("build - rename dockerfile") +} From eeefa2dc8c45a9be4cfb3a52029e6b46570e5ebe Mon Sep 17 00:00:00 2001 From: Arnaud Porterie Date: Tue, 6 Jan 2015 17:58:30 -0800 Subject: [PATCH 187/513] Fix cli echoing container ID on start -a|-i The cli now doesn't echo the container ID when started using either -a or -i. Also fixes `TestStartAttachCorrectExitCode` which incorrectly called start with the result of wait rather than the container ID. Signed-off-by: Arnaud Porterie --- api/client/commands.go | 4 +-- integration-cli/docker_cli_start_test.go | 32 ++++++++++++++++++++++-- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/api/client/commands.go b/api/client/commands.go index 3b22c722c..3c59d6450 100644 --- a/api/client/commands.go +++ b/api/client/commands.go @@ -707,12 +707,12 @@ func (cli *DockerCli) CmdStart(args ...string) error { for _, name := range cmd.Args() { _, _, err := readBody(cli.call("POST", "/containers/"+name+"/start", nil, false)) if err != nil { - if !*attach || !*openStdin { + if !*attach && !*openStdin { fmt.Fprintf(cli.err, "%s\n", err) } encounteredError = fmt.Errorf("Error: failed to start one or more containers") } else { - if !*attach || !*openStdin { + if !*attach && !*openStdin { fmt.Fprintf(cli.out, "%s\n", name) } } diff --git a/integration-cli/docker_cli_start_test.go b/integration-cli/docker_cli_start_test.go index 8041c01c6..05a262ba5 100644 --- a/integration-cli/docker_cli_start_test.go +++ b/integration-cli/docker_cli_start_test.go @@ -53,8 +53,8 @@ func TestStartAttachCorrectExitCode(t *testing.T) { // make sure the container has exited before trying the "start -a" waitCmd := exec.Command(dockerBinary, "wait", out) - if out, _, err = runCommandWithOutput(waitCmd); err != nil { - t.Fatal(out, err) + if _, _, err = runCommandWithOutput(waitCmd); err != nil { + t.Fatalf("Failed to wait on container: %v", err) } startCmd := exec.Command(dockerBinary, "start", "-a", out) @@ -69,6 +69,34 @@ func TestStartAttachCorrectExitCode(t *testing.T) { logDone("start - correct exit code returned with -a") } +func TestStartSilentAttach(t *testing.T) { + defer deleteAllContainers() + + name := "teststartattachcorrectexitcode" + runCmd := exec.Command(dockerBinary, "run", "--name", name, "busybox", "echo", "test") + out, _, _, err := runCommandWithStdoutStderr(runCmd) + if err != nil { + t.Fatalf("failed to run container: %v, output: %q", err, out) + } + + // make sure the container has exited before trying the "start -a" + waitCmd := exec.Command(dockerBinary, "wait", name) + if _, _, err = runCommandWithOutput(waitCmd); err != nil { + t.Fatalf("wait command failed with error: %v", err) + } + + startCmd := exec.Command(dockerBinary, "start", "-a", name) + startOut, _, err := runCommandWithOutput(startCmd) + if err != nil { + t.Fatalf("start command failed unexpectedly with error: %v, output: %q", err, startOut) + } + if expected := "test\n"; startOut != expected { + t.Fatalf("start -a produced unexpected output: expected %q, got %q", expected, startOut) + } + + logDone("start - don't echo container ID when attaching") +} + func TestStartRecordError(t *testing.T) { defer deleteAllContainers() From e23d07a110fcf615bf36defd76e7be06cd7e80df Mon Sep 17 00:00:00 2001 From: Sven Dowideit Date: Tue, 6 Jan 2015 14:18:36 +1000 Subject: [PATCH 188/513] Centos project does not support custom kernels - see #9696 Signed-off-by: Sven Dowideit Docker-DCO-1.1-Signed-off-by: Sven Dowideit (github: SvenDowideit) --- docs/sources/installation/centos.md | 7 +++++++ docs/sources/installation/rhel.md | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/docs/sources/installation/centos.md b/docs/sources/installation/centos.md index 707afc959..7f158f996 100644 --- a/docs/sources/installation/centos.md +++ b/docs/sources/installation/centos.md @@ -27,6 +27,13 @@ simply run the following command. $ sudo yum install docker +## Kernel support + +Currently the CentOS project will only support Docker via the EPEL package when +running on kernels shipped by the distribution. There are things like namespace +changes which will cause issues if one decides to step outside that box and run +non-distro kernel packages. + ### Manual installation of latest version While using a package is the recommended way of installing Docker, diff --git a/docs/sources/installation/rhel.md b/docs/sources/installation/rhel.md index 59ab04964..70e441356 100644 --- a/docs/sources/installation/rhel.md +++ b/docs/sources/installation/rhel.md @@ -40,6 +40,13 @@ You will need [RHEL a RHEL 6 kernel version 2.6.32-431 or higher as this has specific kernel fixes to allow Docker to work. +## Kernel support + +RHEL will only support Docker via the *extras* channel or EPEL package when +running on kernels shipped by the distribution. There are things like namespace +changes which will cause issues if one decides to step outside that box and run +non-distro kernel packages. + ## Installation Firstly, you need to install the EPEL repository. Please follow the From cf27b310c4fc8d2c13ba181398a628d03e1e3c58 Mon Sep 17 00:00:00 2001 From: Sven Dowideit Date: Wed, 7 Jan 2015 10:32:23 +1000 Subject: [PATCH 189/513] Add a containerised test for the https cert doc Docker-DCO-1.1-Signed-off-by: Sven Dowideit (github: SvenDowideit) --- docs/sources/articles/https.md | 22 +++++++++++++------ docs/sources/articles/https/Dockerfile | 10 +++++++++ docs/sources/articles/https/Makefile | 23 ++++++++++++++++++++ docs/sources/articles/https/README.md | 26 +++++++++++++++++++++++ docs/sources/articles/https/make_certs.sh | 23 ++++++++++++++++++++ docs/sources/articles/https/parsedocs.sh | 4 ++++ 6 files changed, 102 insertions(+), 6 deletions(-) create mode 100644 docs/sources/articles/https/Dockerfile create mode 100644 docs/sources/articles/https/Makefile create mode 100644 docs/sources/articles/https/README.md create mode 100755 docs/sources/articles/https/make_certs.sh create mode 100755 docs/sources/articles/https/parsedocs.sh diff --git a/docs/sources/articles/https.md b/docs/sources/articles/https.md index cf1ccaef6..8d0a12c6c 100644 --- a/docs/sources/articles/https.md +++ b/docs/sources/articles/https.md @@ -1,8 +1,8 @@ -page_title: Running Docker with HTTPS +page_title: Protecting the Docker daemon Socket with HTTPS page_description: How to setup and run Docker with HTTPS page_keywords: docker, docs, article, example, https, daemon, tls, ca, certificate -# Running Docker with https +# Protecting the Docker daemon Socket with HTTPS By default, Docker runs via a non-networked Unix socket. It can also optionally communicate using a HTTP socket. @@ -26,6 +26,9 @@ it will only connect to servers with a certificate signed by that CA. ## Create a CA, server and client keys with OpenSSL +> **Note:** replace all instances of `$HOST` in the following example with the +> DNS name of your Docker daemon's host. + First generate CA private and public keys: $ openssl genrsa -aes256 -out ca-key.pem 2048 @@ -49,19 +52,22 @@ First generate CA private and public keys: Locality Name (eg, city) []:Brisbane Organization Name (eg, company) [Internet Widgits Pty Ltd]:Docker Inc Organizational Unit Name (eg, section) []:Boot2Docker - Common Name (e.g. server FQDN or YOUR name) []:your.host.com + Common Name (e.g. server FQDN or YOUR name) []:$HOST Email Address []:Sven@home.org.au Now that we have a CA, you can create a server key and certificate signing request (CSR). Make sure that "Common Name" (i.e. server FQDN or YOUR name) matches the hostname you will use to connect to Docker: +> **Note:** replace all instances of `$HOST` in the following example with the +> DNS name of your Docker daemon's host. + $ openssl genrsa -out server-key.pem 2048 Generating RSA private key, 2048 bit long modulus ......................................................+++ ............................................+++ e is 65537 (0x10001) - $ openssl req -subj '/CN=' -new -key server-key.pem -out server.csr + $ openssl req -subj "/CN=$HOST" -new -key server-key.pem -out server.csr Next, we're going to sign the key with our CA: @@ -105,8 +111,11 @@ providing a certificate trusted by our CA: To be able to connect to Docker and validate its certificate, you now need to provide your client keys, certificates and trusted CA: +> **Note:** replace all instances of `$HOST` in the following example with the +> DNS name of your Docker daemon's host. + $ docker --tlsverify --tlscacert=ca.pem --tlscert=cert.pem --tlskey=key.pem \ - -H=dns-name-of-docker-host:2376 version + -H=$HOST:2376 version > **Note**: > Docker over TLS should run on TCP port 2376. @@ -125,6 +134,7 @@ the files to the `.docker` directory in your home directory - and set the `DOCKER_HOST` and `DOCKER_TLS_VERIFY` variables as well (instead of passing `-H=tcp://:2376` and `--tlsverify` on every call). + $ mkdir -p ~/.docker $ cp ca.pem ~/.docker/ca.pem $ cp cert.pem ~/.docker/cert.pem $ cp key.pem ~/.docker/key.pem @@ -167,7 +177,7 @@ location using the environment variable `DOCKER_CERT_PATH`. To use `curl` to make test API requests, you need to use three extra command line flags: - $ curl https://boot2docker:2376/images/json \ + $ curl https://$HOST:2376/images/json \ --cert ~/.docker/cert.pem \ --key ~/.docker/key.pem \ --cacert ~/.docker/ca.pem diff --git a/docs/sources/articles/https/Dockerfile b/docs/sources/articles/https/Dockerfile new file mode 100644 index 000000000..494aa3030 --- /dev/null +++ b/docs/sources/articles/https/Dockerfile @@ -0,0 +1,10 @@ +FROM debian + +RUN apt-get update && apt-get install -yq openssl + +ADD make_certs.sh / + + +WORKDIR /data +VOLUMES ["/data"] +CMD /make_certs.sh diff --git a/docs/sources/articles/https/Makefile b/docs/sources/articles/https/Makefile new file mode 100644 index 000000000..48fe49f2b --- /dev/null +++ b/docs/sources/articles/https/Makefile @@ -0,0 +1,23 @@ + +HOST:=boot2docker + +makescript: + ./parsedocs.sh > make_certs.sh + +build: makescript + docker build -t makecerts . + +cert: build + docker run --rm -it -v $(CURDIR):/data -e HOST=$(HOST) makecerts + +certs: cert + +run: + docker -d -D --tlsverify --tlscacert=ca.pem --tlscert=server-cert.pem --tlskey=server-key.pem -H=0.0.0.0:6666 --pidfile=$(pwd)/docker.pid --graph=$(pwd)/graph + +client: + docker --tls --tlscacert=ca.pem --tlscert=cert.pem --tlskey=key.pem -H=$(HOST):6666 version + docker --tlsverify --tlscacert=ca.pem --tlscert=cert.pem --tlskey=key.pem -H=$(HOST):6666 info + +clean: + rm ca-key.pem ca.pem ca.srl cert.pem client.csr extfile.cnf key.pem server-cert.pem server-key.pem server.csr diff --git a/docs/sources/articles/https/README.md b/docs/sources/articles/https/README.md new file mode 100644 index 000000000..3e1dd27f6 --- /dev/null +++ b/docs/sources/articles/https/README.md @@ -0,0 +1,26 @@ + + +This is an initial attempt to make it easier to test the examples in the https.md +doc + +at this point, it has to be a manual thing, and I've been running it in boot2docker + +so my process is + +$ boot2docker ssh +$$ git clone https://github.com/docker/docker +$$ cd docker/docs/sources/articles/https +$$ make cert +lots of things to see and manually answer, as openssl wants to be interactive +**NOTE:** make sure you enter the hostname (`boot2docker` in my case) when prompted for `Computer Name`) +$$ sudo make run + +start another terminal + +$ boot2docker ssh +$$ cd docker/docs/sources/articles/https +$$ make client + +the last will connect first with `--tls` and then with `--tlsverify` + +both should succeed diff --git a/docs/sources/articles/https/make_certs.sh b/docs/sources/articles/https/make_certs.sh new file mode 100755 index 000000000..85b7ae153 --- /dev/null +++ b/docs/sources/articles/https/make_certs.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +openssl genrsa -aes256 -out ca-key.pem 2048 + +echo "enter your Docker daemon's hostname as the 'Common Name'= ($HOST)" + +#TODO add this as an ENV to docker run? +openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -out ca.pem + + +# server cert +openssl genrsa -out server-key.pem 2048 +openssl req -subj "/CN=$HOST" -new -key server-key.pem -out server.csr +openssl x509 -req -days 365 -in server.csr -CA ca.pem -CAkey ca-key.pem \ + -CAcreateserial -out server-cert.pem + +#client cert +openssl genrsa -out key.pem 2048 +openssl req -subj '/CN=client' -new -key key.pem -out client.csr + +echo extendedKeyUsage = clientAuth > extfile.cnf +openssl x509 -req -days 365 -in client.csr -CA ca.pem -CAkey ca-key.pem \ + -CAcreateserial -out cert.pem -extfile extfile.cnf diff --git a/docs/sources/articles/https/parsedocs.sh b/docs/sources/articles/https/parsedocs.sh new file mode 100755 index 000000000..56be4103a --- /dev/null +++ b/docs/sources/articles/https/parsedocs.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +echo "#!/bin/sh" +cat ../https.md | awk '{if (sub(/\\$/,"")) printf "%s", $0; else print $0}' | grep ' $ ' | sed 's/ $ //g' | sed 's/2375/7777/g' | sed 's/2376/7778/g' From d9ec04e18d5e1fede1afcec27a0d2c69d514a123 Mon Sep 17 00:00:00 2001 From: tangicolin Date: Wed, 7 Jan 2015 10:50:30 +0100 Subject: [PATCH 190/513] Improve networking documentation with default mac address range since we can control it with --mac-address. Signed-off-by: Tangi COLIN --- docs/sources/articles/networking.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/sources/articles/networking.md b/docs/sources/articles/networking.md index 4bfbcfdad..9503f9be3 100644 --- a/docs/sources/articles/networking.md +++ b/docs/sources/articles/networking.md @@ -12,7 +12,9 @@ private range defined by [RFC 1918](http://tools.ietf.org/html/rfc1918) that are not in use on the host machine, and assigns it to `docker0`. Docker made the choice `172.17.42.1/16` when I started it a few minutes ago, for example — a 16-bit netmask providing 65,534 addresses for the -host machine and its containers. +host machine and its containers. Mac address is generated from ip to +avoid arp collisions and uses a range from 02:42:ac:11:00:00 to +02:42:ac:11:ff:ff. > **Note:** > This document discusses advanced networking configuration From 736558b6ae15afb3f5f0d7ba7801fa44bec9f285 Mon Sep 17 00:00:00 2001 From: Evgeny Vereshchagin Date: Wed, 7 Jan 2015 18:12:02 +0300 Subject: [PATCH 191/513] Update Ubuntu image tag to 14.04 `apt-get update` for non-supported 12.10 doesn't work. Building failed with ``` INFO[0011] The command [/bin/sh -c apt-get update && apt-get install -y redis-server] returned a non-zero code: 100 ``` Signed-off-by: Evgeny Vereshchagin --- docs/sources/examples/running_redis_service.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sources/examples/running_redis_service.md b/docs/sources/examples/running_redis_service.md index 6d052da09..99036a042 100644 --- a/docs/sources/examples/running_redis_service.md +++ b/docs/sources/examples/running_redis_service.md @@ -12,7 +12,7 @@ using a link. Firstly, we create a `Dockerfile` for our new Redis image. - FROM ubuntu:12.10 + FROM ubuntu:14.04 RUN apt-get update && apt-get install -y redis-server EXPOSE 6379 ENTRYPOINT ["/usr/bin/redis-server"] @@ -43,7 +43,7 @@ created with an alias of `db`. This will create a secure tunnel to the `redis` container and expose the Redis instance running inside that container to only this container. - $ sudo docker run --link redis:db -i -t ubuntu:12.10 /bin/bash + $ sudo docker run --link redis:db -i -t ubuntu:14.04 /bin/bash Once inside our freshly created container we need to install Redis to get the `redis-cli` binary to test our connection. From 66387aee5970a9b100f0d14612d7b7726eaf7b9d Mon Sep 17 00:00:00 2001 From: Jan Koprowski Date: Mon, 15 Dec 2014 13:28:42 +0100 Subject: [PATCH 192/513] Specify ENV variables are also used for CMD. Signed-off-by: Michael Crosby --- docs/man/Dockerfile.5.md | 3 ++- docs/sources/reference/builder.md | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/man/Dockerfile.5.md b/docs/man/Dockerfile.5.md index 0114f30ba..9d6f6c815 100644 --- a/docs/man/Dockerfile.5.md +++ b/docs/man/Dockerfile.5.md @@ -120,7 +120,8 @@ or **ENV** --**ENV ** The ENV instruction sets the environment variable to - the value . This value is passed to all future RUN instructions. This is + the value . This value is passed to all future + RUN, ENTRYPOINT, and CMD instructions. This is functionally equivalent to prefixing the command with **=**. The environment variables that are set with ENV persist when a container is run from the resulting image. Use docker inspect to inspect these values, and diff --git a/docs/sources/reference/builder.md b/docs/sources/reference/builder.md index 90862334e..73af35c6f 100644 --- a/docs/sources/reference/builder.md +++ b/docs/sources/reference/builder.md @@ -349,7 +349,8 @@ accessible from the host by default. To expose ports to the host, at runtime, ENV = ... The `ENV` instruction sets the environment variable `` to the value -``. This value will be passed to all future `RUN` instructions. This is +``. This value will be passed to all future +`RUN`, `ENTRYPOINT`, and `CMD` instructions. This is functionally equivalent to prefixing the command with `=` The `ENV` instruction has two forms. The first form, `ENV `, From 7275cd4bbccb9a3b3ea6b9422a59a092fb08bf8d Mon Sep 17 00:00:00 2001 From: Doug Davis Date: Wed, 7 Jan 2015 20:14:45 -0800 Subject: [PATCH 193/513] Make API docs v1.17 visible w/o this one-liner the v1.17 docs didn't appear in the Reference dropdown and I would get a 404 when I tried to access .../reference/api/docker_remote_api_v1.17/ Not sure if there are other spots that need to be fixed but this seemed to fix it for me. Signed-off-by: Doug Davis --- docs/mkdocs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 96c89c231..6d2ecac3a 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -113,6 +113,7 @@ pages: - ['reference/api/registry_api_client_libraries.md', 'Reference', 'Docker Registry API Client Libraries'] - ['reference/api/hub_registry_spec.md', 'Reference', 'Docker Hub and Registry Spec'] - ['reference/api/docker_remote_api.md', 'Reference', 'Docker Remote API'] +- ['reference/api/docker_remote_api_v1.17.md', 'Reference', 'Docker Remote API v1.17'] - ['reference/api/docker_remote_api_v1.16.md', 'Reference', 'Docker Remote API v1.16'] - ['reference/api/docker_remote_api_v1.15.md', 'Reference', 'Docker Remote API v1.15'] - ['reference/api/docker_remote_api_v1.14.md', 'Reference', 'Docker Remote API v1.14'] From b7cb29137bd9c8d7de73993cc7415b62acd498cd Mon Sep 17 00:00:00 2001 From: Doug Davis Date: Wed, 7 Jan 2015 20:27:09 -0800 Subject: [PATCH 194/513] Move docs on the build API out of 'misc' and under 'Images' section It seems odd to have such an important API hidden under 'misc'. While in there I noticed that during the "-f Dockerfile" PR I changed the query param from f to dockerfile and missed this one spot in the docs. Signed-off-by: Doug Davis --- .../reference/api/docker_remote_api_v1.17.md | 108 +++++++++--------- 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/docs/sources/reference/api/docker_remote_api_v1.17.md b/docs/sources/reference/api/docker_remote_api_v1.17.md index 6dbe2045b..aaaffda85 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.17.md +++ b/docs/sources/reference/api/docker_remote_api_v1.17.md @@ -848,6 +848,60 @@ Query Parameters: - **filters** – a json encoded value of the filters (a map[string][]string) to process on the images list. Available filters: - dangling=true +### Build image from a Dockerfile + +`POST /build` + +Build an image from a Dockerfile + +**Example request**: + + POST /build HTTP/1.1 + + {{ TAR STREAM }} + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + {"stream": "Step 1..."} + {"stream": "..."} + {"error": "Error...", "errorDetail": {"code": 123, "message": "Error..."}} + +The input stream must be a tar archive compressed with one of the +following algorithms: identity (no compression), gzip, bzip2, xz. + +The archive must include a build instructions file, typically called +`Dockerfile` at the root of the archive. The `dockerfile` parameter may be +used to specify a different build instructions file by having its value be +the path to the alternate build instructions file to use. + +The archive may include any number of other files, +which will be accessible in the build context (See the [*ADD build +command*](/reference/builder/#dockerbuilder)). + +Query Parameters: + +- **dockerfile** - path within the build context to the Dockerfile +- **t** – repository name (and optionally a tag) to be applied to + the resulting image in case of success +- **q** – suppress verbose build output +- **nocache** – do not use the cache when building the image +- **pull** - attempt to pull the image even if an older image exists locally +- **rm** - remove intermediate containers after a successful build (default behavior) +- **forcerm** - always remove intermediate containers (includes rm) + + Request Headers: + +- **Content-type** – should be set to `"application/tar"`. +- **X-Registry-Config** – base64-encoded ConfigFile objec + +Status Codes: + +- **200** – no error +- **500** – server error + ### Create an image `POST /images/create` @@ -1136,60 +1190,6 @@ Status Codes: ## 2.3 Misc -### Build an image from Dockerfile via stdin - -`POST /build` - -Build an image from Dockerfile via stdin - -**Example request**: - - POST /build HTTP/1.1 - - {{ TAR STREAM }} - -**Example response**: - - HTTP/1.1 200 OK - Content-Type: application/json - - {"stream": "Step 1..."} - {"stream": "..."} - {"error": "Error...", "errorDetail": {"code": 123, "message": "Error..."}} - -The input stream must be a tar archive compressed with one of the -following algorithms: identity (no compression), gzip, bzip2, xz. - -The archive must include a build instructions file, typically called -`Dockerfile` at the root of the archive. The `f` parameter may be used -to specify a different build instructions file by having its value be -the path to the alternate build instructions file to use. - -The archive may include any number of other files, -which will be accessible in the build context (See the [*ADD build -command*](/reference/builder/#dockerbuilder)). - -Query Parameters: - -- **dockerfile** - path within the build context to the Dockerfile -- **t** – repository name (and optionally a tag) to be applied to - the resulting image in case of success -- **q** – suppress verbose build output -- **nocache** – do not use the cache when building the image -- **pull** - attempt to pull the image even if an older image exists locally -- **rm** - remove intermediate containers after a successful build (default behavior) -- **forcerm** - always remove intermediate containers (includes rm) - - Request Headers: - -- **Content-type** – should be set to `"application/tar"`. -- **X-Registry-Config** – base64-encoded ConfigFile objec - -Status Codes: - -- **200** – no error -- **500** – server error - ### Check auth configuration `POST /auth` From 43d45e601fa13bc43228ae3b76b1b5c5737b0558 Mon Sep 17 00:00:00 2001 From: Anders Janmyr Date: Wed, 7 Jan 2015 12:57:24 +0100 Subject: [PATCH 195/513] Add ProxyFromEnvironment enables client via proxy. Signed-off-by: Anders Janmyr --- api/client/cli.go | 1 + 1 file changed, 1 insertion(+) diff --git a/api/client/cli.go b/api/client/cli.go index 9f034d6c4..8c91f5fd7 100644 --- a/api/client/cli.go +++ b/api/client/cli.go @@ -155,6 +155,7 @@ func NewDockerCli(in io.ReadCloser, out, err io.Writer, key libtrust.PrivateKey, // The transport is created here for reuse during the client session tr := &http.Transport{ + Proxy: http.ProxyFromEnvironment, TLSClientConfig: tlsConfig, } From b69580615f82efe1fccecf15fd09a6c5dfbb01f1 Mon Sep 17 00:00:00 2001 From: Tangi COLIN Date: Thu, 8 Jan 2015 09:22:42 +0100 Subject: [PATCH 196/513] Rewritten as the requested SvenDowideit Signed-off-by: Tangi COLIN --- docs/sources/articles/networking.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/sources/articles/networking.md b/docs/sources/articles/networking.md index 9503f9be3..ad998b006 100644 --- a/docs/sources/articles/networking.md +++ b/docs/sources/articles/networking.md @@ -12,9 +12,9 @@ private range defined by [RFC 1918](http://tools.ietf.org/html/rfc1918) that are not in use on the host machine, and assigns it to `docker0`. Docker made the choice `172.17.42.1/16` when I started it a few minutes ago, for example — a 16-bit netmask providing 65,534 addresses for the -host machine and its containers. Mac address is generated from ip to -avoid arp collisions and uses a range from 02:42:ac:11:00:00 to -02:42:ac:11:ff:ff. +host machine and its containers. The MAC address is generated using the +IP address allocated to the container to avoid ARP collisions, using a +range from `02:42:ac:11:00:00` to `02:42:ac:11:ff:ff`. > **Note:** > This document discusses advanced networking configuration From 6f20b957b09c0a7c81013088db7bcec6b3222613 Mon Sep 17 00:00:00 2001 From: Doug Davis Date: Thu, 8 Jan 2015 06:56:30 -0800 Subject: [PATCH 197/513] Make sure that ADD/COPY still populate the cache even if they don't use it Closes #9880 Signed-off-by: Doug Davis --- builder/internals.go | 34 ++++++++------------ integration-cli/docker_cli_build_test.go | 40 ++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 21 deletions(-) diff --git a/builder/internals.go b/builder/internals.go index 909e7a8d1..e7c14559b 100644 --- a/builder/internals.go +++ b/builder/internals.go @@ -308,22 +308,20 @@ func calcCopyInfo(b *Builder, cmdName string, cInfos *[]*copyInfo, origPath stri ci.destPath = ci.destPath + filename } - // Calc the checksum, only if we're using the cache - if b.UtilizeCache { - r, err := archive.Tar(tmpFileName, archive.Uncompressed) - if err != nil { - return err - } - tarSum, err := tarsum.NewTarSum(r, true, tarsum.Version0) - if err != nil { - return err - } - if _, err := io.Copy(ioutil.Discard, tarSum); err != nil { - return err - } - ci.hash = tarSum.Sum(nil) - r.Close() + // Calc the checksum, even if we're using the cache + r, err := archive.Tar(tmpFileName, archive.Uncompressed) + if err != nil { + return err } + tarSum, err := tarsum.NewTarSum(r, true, tarsum.Version0) + if err != nil { + return err + } + if _, err := io.Copy(ioutil.Discard, tarSum); err != nil { + return err + } + ci.hash = tarSum.Sum(nil) + r.Close() return nil } @@ -358,12 +356,6 @@ func calcCopyInfo(b *Builder, cmdName string, cInfos *[]*copyInfo, origPath stri ci.decompress = allowDecompression *cInfos = append(*cInfos, &ci) - // If not using cache don't need to do anything else. - // If we are using a cache then calc the hash for the src file/dir - if !b.UtilizeCache { - return nil - } - // Deal with the single file case if !fi.IsDir() { // This will match first file in sums of the archive diff --git a/integration-cli/docker_cli_build_test.go b/integration-cli/docker_cli_build_test.go index a27aecc56..8fa77c26e 100644 --- a/integration-cli/docker_cli_build_test.go +++ b/integration-cli/docker_cli_build_test.go @@ -2281,6 +2281,46 @@ func TestBuildWithoutCache(t *testing.T) { logDone("build - without cache") } +func TestBuildConditionalCache(t *testing.T) { + name := "testbuildconditionalcache" + name2 := "testbuildconditionalcache2" + defer deleteImages(name, name2) + + dockerfile := ` + FROM busybox + ADD foo /tmp/` + ctx, err := fakeContext(dockerfile, map[string]string{ + "foo": "hello", + }) + + id1, err := buildImageFromContext(name, ctx, true) + if err != nil { + t.Fatalf("Error building #1: %s", err) + } + + if err := ctx.Add("foo", "bye"); err != nil { + t.Fatalf("Error modifying foo: %s", err) + } + + id2, err := buildImageFromContext(name, ctx, false) + if err != nil { + t.Fatalf("Error building #2: %s", err) + } + if id2 == id1 { + t.Fatal("Should not have used the cache") + } + + id3, err := buildImageFromContext(name, ctx, true) + if err != nil { + t.Fatalf("Error building #3: %s", err) + } + if id3 != id2 { + t.Fatal("Should have used the cache") + } + + logDone("build - conditional cache") +} + func TestBuildADDLocalFileWithCache(t *testing.T) { name := "testbuildaddlocalfilewithcache" name2 := "testbuildaddlocalfilewithcache2" From f4551b8a48bdc7a135466398eecfb103fcde25c6 Mon Sep 17 00:00:00 2001 From: Malte Janduda Date: Thu, 8 Jan 2015 16:21:01 +0100 Subject: [PATCH 198/513] Remove BridgeIP from ipallocation pool Closes #9938 Signed-off-by: Malte Janduda --- daemon/networkdriver/bridge/driver.go | 3 +++ daemon/networkdriver/ipallocator/allocator.go | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/daemon/networkdriver/bridge/driver.go b/daemon/networkdriver/bridge/driver.go index 81624ad1d..2f94f055b 100644 --- a/daemon/networkdriver/bridge/driver.go +++ b/daemon/networkdriver/bridge/driver.go @@ -171,6 +171,9 @@ func InitDriver(job *engine.Job) engine.Status { } } + // Block BridgeIP in IP allocator + ipallocator.RequestIP(bridgeNetwork, bridgeNetwork.IP) + // https://github.com/docker/docker/issues/2768 job.Eng.Hack_SetGlobalVar("httpapi.bridgeIP", bridgeNetwork.IP) diff --git a/daemon/networkdriver/ipallocator/allocator.go b/daemon/networkdriver/ipallocator/allocator.go index a8625c030..40c3eb823 100644 --- a/daemon/networkdriver/ipallocator/allocator.go +++ b/daemon/networkdriver/ipallocator/allocator.go @@ -121,7 +121,6 @@ func (allocated *allocatedMap) checkIP(ip net.IP) (net.IP, error) { // Register the IP. allocated.p[ip.String()] = struct{}{} - allocated.last.Set(pos) return ip, nil } From e54d8c47e45ca19ab9548a7e3689aa1584733210 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20Mart=C3=ADnez=20de=20Bartolom=C3=A9=20Izquierdo?= Date: Thu, 8 Jan 2015 16:15:45 +0100 Subject: [PATCH 199/513] Add c++ client library MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Luis Martínez de Bartolomé Izquierdo Signed-off-by: Luis Martínez de Bartolomé Izquierdo --- docs/sources/reference/api/remote_api_client_libraries.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/sources/reference/api/remote_api_client_libraries.md b/docs/sources/reference/api/remote_api_client_libraries.md index bff2fa30c..d79bbd89a 100644 --- a/docs/sources/reference/api/remote_api_client_libraries.md +++ b/docs/sources/reference/api/remote_api_client_libraries.md @@ -30,6 +30,12 @@ will add the libraries here. https://github.com/ahmetalpbalkan/Docker.DotNet Active + + C++ + lasote/docker_client + http://www.biicode.com/lasote/docker_client (Biicode C++ dependency manager) + Active + Erlang erldocker From 16dbd84f6386519dbbcefeb81cdfa708bca8cfa8 Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Thu, 8 Jan 2015 10:29:24 -0800 Subject: [PATCH 200/513] Update libcontainer to be02944484da197166020d6b3f0 Signed-off-by: Michael Crosby --- project/vendor.sh | 2 +- vendor/src/github.com/docker/libcontainer/Dockerfile | 3 +-- .../docker/libcontainer/cgroups/fs/stats_util_test.go | 2 +- .../docker/libcontainer/integration/template_test.go | 4 ++-- .../docker/libcontainer/namespaces/nsenter/nsenter.c | 6 ++---- 5 files changed, 7 insertions(+), 10 deletions(-) diff --git a/project/vendor.sh b/project/vendor.sh index c1074e887..91196d703 100755 --- a/project/vendor.sh +++ b/project/vendor.sh @@ -66,7 +66,7 @@ if [ "$1" = '--go' ]; then mv tmp-tar src/code.google.com/p/go/src/pkg/archive/tar fi -clone git github.com/docker/libcontainer 0f397d4e145fb4053792d42b3424dd2143fb23ad +clone git github.com/docker/libcontainer be02944484da197166020d6b3f08a19d7d7d244c # see src/github.com/docker/libcontainer/update-vendor.sh which is the "source of truth" for libcontainer deps (just like this file) rm -rf src/github.com/docker/libcontainer/vendor eval "$(grep '^clone ' src/github.com/docker/libcontainer/update-vendor.sh | grep -v 'github.com/codegangsta/cli')" diff --git a/vendor/src/github.com/docker/libcontainer/Dockerfile b/vendor/src/github.com/docker/libcontainer/Dockerfile index 614e5979b..0771c808e 100644 --- a/vendor/src/github.com/docker/libcontainer/Dockerfile +++ b/vendor/src/github.com/docker/libcontainer/Dockerfile @@ -1,6 +1,5 @@ -FROM crosbymichael/golang +FROM golang:1.4 -RUN apt-get update && apt-get install -y gcc make RUN go get golang.org/x/tools/cmd/cover ENV GOPATH $GOPATH:/go/src/github.com/docker/libcontainer/vendor diff --git a/vendor/src/github.com/docker/libcontainer/cgroups/fs/stats_util_test.go b/vendor/src/github.com/docker/libcontainer/cgroups/fs/stats_util_test.go index 6e237c040..c55ba938c 100644 --- a/vendor/src/github.com/docker/libcontainer/cgroups/fs/stats_util_test.go +++ b/vendor/src/github.com/docker/libcontainer/cgroups/fs/stats_util_test.go @@ -53,7 +53,7 @@ func expectBlkioStatsEquals(t *testing.T, expected, actual cgroups.BlkioStats) { } if err := blkioStatEntryEquals(expected.IoMergedRecursive, actual.IoMergedRecursive); err != nil { - log.Printf("blkio IoMergedRecursive do not match - %s vs %s\n", expected.IoMergedRecursive, actual.IoMergedRecursive) + log.Printf("blkio IoMergedRecursive do not match - %v vs %v\n", expected.IoMergedRecursive, actual.IoMergedRecursive) t.Fail() } diff --git a/vendor/src/github.com/docker/libcontainer/integration/template_test.go b/vendor/src/github.com/docker/libcontainer/integration/template_test.go index 7e56628c2..98846eb19 100644 --- a/vendor/src/github.com/docker/libcontainer/integration/template_test.go +++ b/vendor/src/github.com/docker/libcontainer/integration/template_test.go @@ -32,13 +32,13 @@ func newTemplateConfig(rootfs string) *libcontainer.Config { "KILL", "AUDIT_WRITE", }, - Namespaces: libcontainer.Namespaces{ + Namespaces: libcontainer.Namespaces([]libcontainer.Namespace{ {Type: libcontainer.NEWNS}, {Type: libcontainer.NEWUTS}, {Type: libcontainer.NEWIPC}, {Type: libcontainer.NEWPID}, {Type: libcontainer.NEWNET}, - }, + }), Cgroups: &cgroups.Cgroup{ Parent: "integration", AllowAllDevices: false, diff --git a/vendor/src/github.com/docker/libcontainer/namespaces/nsenter/nsenter.c b/vendor/src/github.com/docker/libcontainer/namespaces/nsenter/nsenter.c index 1a81c3157..b735b1fa2 100644 --- a/vendor/src/github.com/docker/libcontainer/namespaces/nsenter/nsenter.c +++ b/vendor/src/github.com/docker/libcontainer/namespaces/nsenter/nsenter.c @@ -15,10 +15,6 @@ #include #include -#ifndef PR_SET_CHILD_SUBREAPER -#define PR_SET_CHILD_SUBREAPER 36 -#endif - static const kBufSize = 256; static const char *kNsEnter = "nsenter"; @@ -93,11 +89,13 @@ void nsenter() return; } + #ifdef PR_SET_CHILD_SUBREAPER if (prctl(PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0) == -1) { fprintf(stderr, "nsenter: failed to set child subreaper: %s", strerror(errno)); exit(1); } + #endif static const struct option longopts[] = { {"nspid", required_argument, NULL, 'n'}, From 63a7ccdd2372d87f56f7a86da07c72ea51332c2a Mon Sep 17 00:00:00 2001 From: Phil Estes Date: Wed, 10 Dec 2014 00:55:09 -0500 Subject: [PATCH 201/513] Update container resolv.conf when host network changes /etc/resolv.conf Only modifies non-running containers resolv.conf bind mount, and only if the container has an unmodified resolv.conf compared to its contents at container start time (so we don't overwrite manual/automated changes within the container runtime). For containers which are running when the host resolv.conf changes, the update will only be applied to the container version of resolv.conf when the container is "bounced" down and back up (e.g. stop/start or restart) Docker-DCO-1.1-Signed-off-by: Phil Estes (github: estesp) --- daemon/container.go | 107 +++++++++++++- daemon/daemon.go | 65 ++++++++- daemon/utils_test.go | 31 ---- docs/sources/articles/networking.md | 17 ++- integration-cli/docker_cli_run_test.go | 151 ++++++++++++++++++++ pkg/networkfs/resolvconf/resolvconf.go | 67 ++++++++- pkg/networkfs/resolvconf/resolvconf_test.go | 31 ++++ utils/utils.go | 8 -- 8 files changed, 426 insertions(+), 51 deletions(-) diff --git a/daemon/container.go b/daemon/container.go index 75cd133fe..0b47c6206 100644 --- a/daemon/container.go +++ b/daemon/container.go @@ -81,6 +81,7 @@ type Container struct { MountLabel, ProcessLabel string AppArmorProfile string RestartCount int + UpdateDns bool // Maps container paths to volume paths. The key in this is the path to which // the volume is being mounted inside the container. Value is the path of the @@ -945,6 +946,29 @@ func (container *Container) DisableLink(name string) { func (container *Container) setupContainerDns() error { if container.ResolvConfPath != "" { + // check if this is an existing container that needs DNS update: + if container.UpdateDns { + // read the host's resolv.conf, get the hash and call updateResolvConf + log.Debugf("Check container (%s) for update to resolv.conf - UpdateDns flag was set", container.ID) + latestResolvConf, latestHash := resolvconf.GetLastModified() + + // because the new host resolv.conf might have localhost nameservers.. + updatedResolvConf, modified := resolvconf.RemoveReplaceLocalDns(latestResolvConf) + if modified { + // changes have occurred during resolv.conf localhost cleanup: generate an updated hash + newHash, err := utils.HashData(bytes.NewReader(updatedResolvConf)) + if err != nil { + return err + } + latestHash = newHash + } + + if err := container.updateResolvConf(updatedResolvConf, latestHash); err != nil { + return err + } + // successful update of the restarting container; set the flag off + container.UpdateDns = false + } return nil } @@ -983,17 +1007,86 @@ func (container *Container) setupContainerDns() error { } // replace any localhost/127.* nameservers - resolvConf = utils.RemoveLocalDns(resolvConf) - // if the resulting resolvConf is empty, use DefaultDns - if !bytes.Contains(resolvConf, []byte("nameserver")) { - log.Infof("No non localhost DNS resolver found in resolv.conf and containers can't use it. Using default external servers : %v", DefaultDns) - // prefix the default dns options with nameserver - resolvConf = append(resolvConf, []byte("\nnameserver "+strings.Join(DefaultDns, "\nnameserver "))...) - } + resolvConf, _ = resolvconf.RemoveReplaceLocalDns(resolvConf) + } + //get a sha256 hash of the resolv conf at this point so we can check + //for changes when the host resolv.conf changes (e.g. network update) + resolvHash, err := utils.HashData(bytes.NewReader(resolvConf)) + if err != nil { + return err + } + resolvHashFile := container.ResolvConfPath + ".hash" + if err = ioutil.WriteFile(resolvHashFile, []byte(resolvHash), 0644); err != nil { + return err } return ioutil.WriteFile(container.ResolvConfPath, resolvConf, 0644) } +// called when the host's resolv.conf changes to check whether container's resolv.conf +// is unchanged by the container "user" since container start: if unchanged, the +// container's resolv.conf will be updated to match the host's new resolv.conf +func (container *Container) updateResolvConf(updatedResolvConf []byte, newResolvHash string) error { + + if container.ResolvConfPath == "" { + return nil + } + if container.Running { + //set a marker in the hostConfig to update on next start/restart + container.UpdateDns = true + return nil + } + + resolvHashFile := container.ResolvConfPath + ".hash" + + //read the container's current resolv.conf and compute the hash + resolvBytes, err := ioutil.ReadFile(container.ResolvConfPath) + if err != nil { + return err + } + curHash, err := utils.HashData(bytes.NewReader(resolvBytes)) + if err != nil { + return err + } + + //read the hash from the last time we wrote resolv.conf in the container + hashBytes, err := ioutil.ReadFile(resolvHashFile) + if err != nil { + return err + } + + //if the user has not modified the resolv.conf of the container since we wrote it last + //we will replace it with the updated resolv.conf from the host + if string(hashBytes) == curHash { + log.Debugf("replacing %q with updated host resolv.conf", container.ResolvConfPath) + + // for atomic updates to these files, use temporary files with os.Rename: + dir := path.Dir(container.ResolvConfPath) + tmpHashFile, err := ioutil.TempFile(dir, "hash") + if err != nil { + return err + } + tmpResolvFile, err := ioutil.TempFile(dir, "resolv") + if err != nil { + return err + } + + // write the updates to the temp files + if err = ioutil.WriteFile(tmpHashFile.Name(), []byte(newResolvHash), 0644); err != nil { + return err + } + if err = ioutil.WriteFile(tmpResolvFile.Name(), updatedResolvConf, 0644); err != nil { + return err + } + + // rename the temp files for atomic replace + if err = os.Rename(tmpHashFile.Name(), resolvHashFile); err != nil { + return err + } + return os.Rename(tmpResolvFile.Name(), container.ResolvConfPath) + } + return nil +} + func (container *Container) updateParentsHosts() error { parents, err := container.daemon.Parents(container.Name) if err != nil { diff --git a/daemon/daemon.go b/daemon/daemon.go index 632b9abc4..ee8702a39 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -1,6 +1,7 @@ package daemon import ( + "bytes" "fmt" "io" "io/ioutil" @@ -32,6 +33,7 @@ import ( "github.com/docker/docker/pkg/graphdb" "github.com/docker/docker/pkg/ioutils" "github.com/docker/docker/pkg/namesgenerator" + "github.com/docker/docker/pkg/networkfs/resolvconf" "github.com/docker/docker/pkg/parsers" "github.com/docker/docker/pkg/parsers/kernel" "github.com/docker/docker/pkg/sysinfo" @@ -40,10 +42,11 @@ import ( "github.com/docker/docker/trust" "github.com/docker/docker/utils" "github.com/docker/docker/volumes" + + "github.com/go-fsnotify/fsnotify" ) var ( - DefaultDns = []string{"8.8.8.8", "8.8.4.4"} validContainerNameChars = `[a-zA-Z0-9][a-zA-Z0-9_.-]` validContainerNamePattern = regexp.MustCompile(`^/?` + validContainerNameChars + `+$`) ) @@ -402,6 +405,60 @@ func (daemon *Daemon) restore() error { return nil } +// set up the watch on the host's /etc/resolv.conf so that we can update container's +// live resolv.conf when the network changes on the host +func (daemon *Daemon) setupResolvconfWatcher() error { + + watcher, err := fsnotify.NewWatcher() + if err != nil { + return err + } + + //this goroutine listens for the events on the watch we add + //on the resolv.conf file on the host + go func() { + for { + select { + case event := <-watcher.Events: + if event.Op&fsnotify.Write == fsnotify.Write { + // verify a real change happened before we go further--a file write may have happened + // without an actual change to the file + updatedResolvConf, newResolvConfHash, err := resolvconf.GetIfChanged() + if err != nil { + log.Debugf("Error retrieving updated host resolv.conf: %v", err) + } else if updatedResolvConf != nil { + // because the new host resolv.conf might have localhost nameservers.. + updatedResolvConf, modified := resolvconf.RemoveReplaceLocalDns(updatedResolvConf) + if modified { + // changes have occurred during localhost cleanup: generate an updated hash + newHash, err := utils.HashData(bytes.NewReader(updatedResolvConf)) + if err != nil { + log.Debugf("Error generating hash of new resolv.conf: %v", err) + } else { + newResolvConfHash = newHash + } + } + log.Debugf("host network resolv.conf changed--walking container list for updates") + contList := daemon.containers.List() + for _, container := range contList { + if err := container.updateResolvConf(updatedResolvConf, newResolvConfHash); err != nil { + log.Debugf("Error on resolv.conf update check for container ID: %s: %v", container.ID, err) + } + } + } + } + case err := <-watcher.Errors: + log.Debugf("host resolv.conf notify error: %v", err) + } + } + }() + + if err := watcher.Add("/etc/resolv.conf"); err != nil { + return err + } + return nil +} + func (daemon *Daemon) checkDeprecatedExpose(config *runconfig.Config) bool { if config != nil { if config.PortSpecs != nil { @@ -924,6 +981,12 @@ func NewDaemonFromDirectory(config *Config, eng *engine.Engine) (*Daemon, error) if err := daemon.restore(); err != nil { return nil, err } + + // set up filesystem watch on resolv.conf for network changes + if err := daemon.setupResolvconfWatcher(); err != nil { + return nil, err + } + // Setup shutdown handlers // FIXME: can these shutdown handlers be registered closer to their source? eng.OnShutdown(func() { diff --git a/daemon/utils_test.go b/daemon/utils_test.go index 28a15c64e..ff5b082ba 100644 --- a/daemon/utils_test.go +++ b/daemon/utils_test.go @@ -24,34 +24,3 @@ func TestMergeLxcConfig(t *testing.T) { t.Fatalf("expected %s got %s", expected, cpuset) } } - -func TestRemoveLocalDns(t *testing.T) { - ns0 := "nameserver 10.16.60.14\nnameserver 10.16.60.21\n" - - if result := utils.RemoveLocalDns([]byte(ns0)); result != nil { - if ns0 != string(result) { - t.Fatalf("Failed No Localhost: expected \n<%s> got \n<%s>", ns0, string(result)) - } - } - - ns1 := "nameserver 10.16.60.14\nnameserver 10.16.60.21\nnameserver 127.0.0.1\n" - if result := utils.RemoveLocalDns([]byte(ns1)); result != nil { - if ns0 != string(result) { - t.Fatalf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result)) - } - } - - ns1 = "nameserver 10.16.60.14\nnameserver 127.0.0.1\nnameserver 10.16.60.21\n" - if result := utils.RemoveLocalDns([]byte(ns1)); result != nil { - if ns0 != string(result) { - t.Fatalf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result)) - } - } - - ns1 = "nameserver 127.0.1.1\nnameserver 10.16.60.14\nnameserver 10.16.60.21\n" - if result := utils.RemoveLocalDns([]byte(ns1)); result != nil { - if ns0 != string(result) { - t.Fatalf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result)) - } - } -} diff --git a/docs/sources/articles/networking.md b/docs/sources/articles/networking.md index 4bfbcfdad..05e59816b 100644 --- a/docs/sources/articles/networking.md +++ b/docs/sources/articles/networking.md @@ -130,7 +130,7 @@ information. You can see this by running `mount` inside a container: ... /dev/disk/by-uuid/1fec...ebdf on /etc/hostname type ext4 ... /dev/disk/by-uuid/1fec...ebdf on /etc/hosts type ext4 ... - tmpfs on /etc/resolv.conf type tmpfs ... + /dev/disk/by-uuid/1fec...ebdf on /etc/resolv.conf type ext4 ... ... This arrangement allows Docker to do clever things like keep @@ -178,7 +178,20 @@ Four different options affect container domain name services. Note that Docker, in the absence of either of the last two options above, will make `/etc/resolv.conf` inside of each container look like the `/etc/resolv.conf` of the host machine where the `docker` daemon is -running. The options then modify this default configuration. +running. You might wonder what happens when the host machine's +`/etc/resolv.conf` file changes. The `docker` daemon has a file change +notifier active which will watch for changes to the host DNS configuration. +When the host file changes, all stopped containers which have a matching +`resolv.conf` to the host will be updated immediately to this newest host +configuration. Containers which are running when the host configuration +changes will need to stop and start to pick up the host changes due to lack +of a facility to ensure atomic writes of the `resolv.conf` file while the +container is running. If the container's `resolv.conf` has been edited since +it was started with the default configuration, no replacement will be +attempted as it would overwrite the changes performed by the container. +If the options (`--dns` or `--dns-search`) have been used to modify the +default host configuration, then the replacement with an updated host's +`/etc/resolv.conf` will not happen as well. ## Communication between containers and the wider world diff --git a/integration-cli/docker_cli_run_test.go b/integration-cli/docker_cli_run_test.go index 0e1d3aff4..0c5e1100b 100644 --- a/integration-cli/docker_cli_run_test.go +++ b/integration-cli/docker_cli_run_test.go @@ -1403,6 +1403,157 @@ func TestRunDnsOptionsBasedOnHostResolvConf(t *testing.T) { logDone("run - dns options based on host resolv.conf") } +// Test the file watch notifier on docker host's /etc/resolv.conf +// A go-routine is responsible for auto-updating containers which are +// stopped and have an unmodified copy of resolv.conf, as well as +// marking running containers as requiring an update on next restart +func TestRunResolvconfUpdater(t *testing.T) { + + tmpResolvConf := []byte("search pommesfrites.fr\nnameserver 12.34.56.78") + tmpLocalhostResolvConf := []byte("nameserver 127.0.0.1") + + //take a copy of resolv.conf for restoring after test completes + resolvConfSystem, err := ioutil.ReadFile("/etc/resolv.conf") + if err != nil { + t.Fatal(err) + } + + //cleanup + defer func() { + deleteAllContainers() + if err := ioutil.WriteFile("/etc/resolv.conf", resolvConfSystem, 0644); err != nil { + t.Fatal(err) + } + }() + + //1. test that a non-running container gets an updated resolv.conf + cmd := exec.Command(dockerBinary, "run", "--name='first'", "busybox", "true") + if _, err := runCommand(cmd); err != nil { + t.Fatal(err) + } + containerID1, err := getIDByName("first") + if err != nil { + t.Fatal(err) + } + + // replace resolv.conf with our temporary copy + bytesResolvConf := []byte(tmpResolvConf) + if err := ioutil.WriteFile("/etc/resolv.conf", bytesResolvConf, 0644); err != nil { + t.Fatal(err) + } + + time.Sleep(time.Second / 2) + // check for update in container + containerResolv, err := readContainerFile(containerID1, "resolv.conf") + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(containerResolv, bytesResolvConf) { + t.Fatalf("Stopped container does not have updated resolv.conf; expected %q, got %q", tmpResolvConf, string(containerResolv)) + } + + //2. test that a non-running container does not receive resolv.conf updates + // if it modified the container copy of the starting point resolv.conf + cmd = exec.Command(dockerBinary, "run", "--name='second'", "busybox", "sh", "-c", "echo 'search mylittlepony.com' >>/etc/resolv.conf") + if _, err = runCommand(cmd); err != nil { + t.Fatal(err) + } + containerID2, err := getIDByName("second") + if err != nil { + t.Fatal(err) + } + containerResolvHashBefore, err := readContainerFile(containerID2, "resolv.conf.hash") + if err != nil { + t.Fatal(err) + } + + //make a change to resolv.conf (in this case replacing our tmp copy with orig copy) + if err := ioutil.WriteFile("/etc/resolv.conf", resolvConfSystem, 0644); err != nil { + t.Fatal(err) + } + + time.Sleep(time.Second / 2) + containerResolvHashAfter, err := readContainerFile(containerID2, "resolv.conf.hash") + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(containerResolvHashBefore, containerResolvHashAfter) { + t.Fatalf("Stopped container with modified resolv.conf should not have been updated; expected hash: %v, new hash: %v", containerResolvHashBefore, containerResolvHashAfter) + } + + //3. test that a running container's resolv.conf is not modified while running + cmd = exec.Command(dockerBinary, "run", "-d", "busybox", "top") + out, _, err := runCommandWithOutput(cmd) + if err != nil { + t.Fatal(err) + } + runningContainerID := strings.TrimSpace(out) + + containerResolvHashBefore, err = readContainerFile(runningContainerID, "resolv.conf.hash") + if err != nil { + t.Fatal(err) + } + + // replace resolv.conf + if err := ioutil.WriteFile("/etc/resolv.conf", bytesResolvConf, 0644); err != nil { + t.Fatal(err) + } + + // make sure the updater has time to run to validate we really aren't + // getting updated + time.Sleep(time.Second / 2) + containerResolvHashAfter, err = readContainerFile(runningContainerID, "resolv.conf.hash") + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(containerResolvHashBefore, containerResolvHashAfter) { + t.Fatalf("Running container's resolv.conf should not be updated; expected hash: %v, new hash: %v", containerResolvHashBefore, containerResolvHashAfter) + } + + //4. test that a running container's resolv.conf is updated upon restart + // (the above container is still running..) + cmd = exec.Command(dockerBinary, "restart", runningContainerID) + if _, err = runCommand(cmd); err != nil { + t.Fatal(err) + } + + // check for update in container + containerResolv, err = readContainerFile(runningContainerID, "resolv.conf") + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(containerResolv, bytesResolvConf) { + t.Fatalf("Restarted container should have updated resolv.conf; expected %q, got %q", tmpResolvConf, string(containerResolv)) + } + + //5. test that additions of a localhost resolver are cleaned from + // host resolv.conf before updating container's resolv.conf copies + + // replace resolv.conf with a localhost-only nameserver copy + bytesResolvConf = []byte(tmpLocalhostResolvConf) + if err = ioutil.WriteFile("/etc/resolv.conf", bytesResolvConf, 0644); err != nil { + t.Fatal(err) + } + + time.Sleep(time.Second / 2) + // our first exited container ID should have been updated, but with default DNS + // after the cleanup of resolv.conf found only a localhost nameserver: + containerResolv, err = readContainerFile(containerID1, "resolv.conf") + if err != nil { + t.Fatal(err) + } + + expected := "\nnameserver 8.8.8.8\nnameserver 8.8.4.4" + if !bytes.Equal(containerResolv, []byte(expected)) { + t.Fatalf("Container does not have cleaned/replaced DNS in resolv.conf; expected %q, got %q", expected, string(containerResolv)) + } + + //cleanup, restore original resolv.conf happens in defer func() + logDone("run - resolv.conf updater") +} + func TestRunAddHost(t *testing.T) { defer deleteAllContainers() cmd := exec.Command(dockerBinary, "run", "--add-host=extra:86.75.30.9", "busybox", "grep", "extra", "/etc/hosts") diff --git a/pkg/networkfs/resolvconf/resolvconf.go b/pkg/networkfs/resolvconf/resolvconf.go index 9165caeaa..a43daa527 100644 --- a/pkg/networkfs/resolvconf/resolvconf.go +++ b/pkg/networkfs/resolvconf/resolvconf.go @@ -5,13 +5,25 @@ import ( "io/ioutil" "regexp" "strings" + "sync" + + log "github.com/Sirupsen/logrus" + "github.com/docker/docker/utils" ) var ( - nsRegexp = regexp.MustCompile(`^\s*nameserver\s*(([0-9]+\.){3}([0-9]+))\s*$`) - searchRegexp = regexp.MustCompile(`^\s*search\s*(([^\s]+\s*)*)$`) + defaultDns = []string{"8.8.8.8", "8.8.4.4"} + localHostRegexp = regexp.MustCompile(`(?m)^nameserver 127[^\n]+\n*`) + nsRegexp = regexp.MustCompile(`^\s*nameserver\s*(([0-9]+\.){3}([0-9]+))\s*$`) + searchRegexp = regexp.MustCompile(`^\s*search\s*(([^\s]+\s*)*)$`) ) +var lastModified struct { + sync.Mutex + sha256 string + contents []byte +} + func Get() ([]byte, error) { resolv, err := ioutil.ReadFile("/etc/resolv.conf") if err != nil { @@ -20,6 +32,57 @@ func Get() ([]byte, error) { return resolv, nil } +// Retrieves the host /etc/resolv.conf file, checks against the last hash +// and, if modified since last check, returns the bytes and new hash. +// This feature is used by the resolv.conf updater for containers +func GetIfChanged() ([]byte, string, error) { + lastModified.Lock() + defer lastModified.Unlock() + + resolv, err := ioutil.ReadFile("/etc/resolv.conf") + if err != nil { + return nil, "", err + } + newHash, err := utils.HashData(bytes.NewReader(resolv)) + if err != nil { + return nil, "", err + } + if lastModified.sha256 != newHash { + lastModified.sha256 = newHash + lastModified.contents = resolv + return resolv, newHash, nil + } + // nothing changed, so return no data + return nil, "", nil +} + +// retrieve the last used contents and hash of the host resolv.conf +// Used by containers updating on restart +func GetLastModified() ([]byte, string) { + lastModified.Lock() + defer lastModified.Unlock() + + return lastModified.contents, lastModified.sha256 +} + +// RemoveReplaceLocalDns looks for localhost (127.*) entries in the provided +// resolv.conf, removing local nameserver entries, and, if the resulting +// cleaned config has no defined nameservers left, adds default DNS entries +// It also returns a boolean to notify the caller if changes were made at all +func RemoveReplaceLocalDns(resolvConf []byte) ([]byte, bool) { + changed := false + cleanedResolvConf := localHostRegexp.ReplaceAll(resolvConf, []byte{}) + // if the resulting resolvConf is empty, use defaultDns + if !bytes.Contains(cleanedResolvConf, []byte("nameserver")) { + log.Infof("No non-localhost DNS nameservers are left in resolv.conf. Using default external servers : %v", defaultDns) + cleanedResolvConf = append(cleanedResolvConf, []byte("\nnameserver "+strings.Join(defaultDns, "\nnameserver "))...) + } + if !bytes.Equal(resolvConf, cleanedResolvConf) { + changed = true + } + return cleanedResolvConf, changed +} + // getLines parses input into lines and strips away comments. func getLines(input []byte, commentMarker []byte) [][]byte { lines := bytes.Split(input, []byte("\n")) diff --git a/pkg/networkfs/resolvconf/resolvconf_test.go b/pkg/networkfs/resolvconf/resolvconf_test.go index 6187acbae..2432ea53c 100644 --- a/pkg/networkfs/resolvconf/resolvconf_test.go +++ b/pkg/networkfs/resolvconf/resolvconf_test.go @@ -156,3 +156,34 @@ func TestBuildWithZeroLengthDomainSearch(t *testing.T) { t.Fatalf("Expected to not find '%s' got '%s'", notExpected, content) } } + +func TestRemoveReplaceLocalDns(t *testing.T) { + ns0 := "nameserver 10.16.60.14\nnameserver 10.16.60.21\n" + + if result, _ := RemoveReplaceLocalDns([]byte(ns0)); result != nil { + if ns0 != string(result) { + t.Fatalf("Failed No Localhost: expected \n<%s> got \n<%s>", ns0, string(result)) + } + } + + ns1 := "nameserver 10.16.60.14\nnameserver 10.16.60.21\nnameserver 127.0.0.1\n" + if result, _ := RemoveReplaceLocalDns([]byte(ns1)); result != nil { + if ns0 != string(result) { + t.Fatalf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result)) + } + } + + ns1 = "nameserver 10.16.60.14\nnameserver 127.0.0.1\nnameserver 10.16.60.21\n" + if result, _ := RemoveReplaceLocalDns([]byte(ns1)); result != nil { + if ns0 != string(result) { + t.Fatalf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result)) + } + } + + ns1 = "nameserver 127.0.1.1\nnameserver 10.16.60.14\nnameserver 10.16.60.21\n" + if result, _ := RemoveReplaceLocalDns([]byte(ns1)); result != nil { + if ns0 != string(result) { + t.Fatalf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result)) + } + } +} diff --git a/utils/utils.go b/utils/utils.go index 8d3b3eb73..40e774cc4 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -290,14 +290,6 @@ func NewHTTPRequestError(msg string, res *http.Response) error { } } -var localHostRx = regexp.MustCompile(`(?m)^nameserver 127[^\n]+\n*`) - -// RemoveLocalDns looks into the /etc/resolv.conf, -// and removes any local nameserver entries. -func RemoveLocalDns(resolvConf []byte) []byte { - return localHostRx.ReplaceAll(resolvConf, []byte{}) -} - // An StatusError reports an unsuccessful exit by a command. type StatusError struct { Status string From 09e3467452d24aca38810fa79a09d6371d2c0b5c Mon Sep 17 00:00:00 2001 From: Erik Hollensbe Date: Thu, 8 Jan 2015 11:30:08 -0800 Subject: [PATCH 202/513] Fix a parser error where an empty RUN statement would cause a panic Docker-DCO-1.1-Signed-off-by: Erik Hollensbe (github: erikh) --- builder/parser/parser.go | 7 ++++++- .../empty-instruction/Dockerfile | 8 +++++++ integration-cli/docker_cli_build_test.go | 21 +++++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 builder/parser/testfiles-negative/empty-instruction/Dockerfile diff --git a/builder/parser/parser.go b/builder/parser/parser.go index ad42a1586..a0bb881f8 100644 --- a/builder/parser/parser.go +++ b/builder/parser/parser.go @@ -3,6 +3,7 @@ package parser import ( "bufio" + "fmt" "io" "regexp" "strings" @@ -32,7 +33,7 @@ type Node struct { var ( dispatch map[string]func(string) (*Node, map[string]bool, error) TOKEN_WHITESPACE = regexp.MustCompile(`[\t\v\f\r ]+`) - TOKEN_LINE_CONTINUATION = regexp.MustCompile(`\\\s*$`) + TOKEN_LINE_CONTINUATION = regexp.MustCompile(`\\[ \t]*$`) TOKEN_COMMENT = regexp.MustCompile(`^#.*$`) ) @@ -77,6 +78,10 @@ func parseLine(line string) (string, *Node, error) { return "", nil, err } + if len(args) == 0 { + return "", nil, fmt.Errorf("Instruction %q is empty; cannot continue", cmd) + } + node := &Node{} node.Value = cmd diff --git a/builder/parser/testfiles-negative/empty-instruction/Dockerfile b/builder/parser/testfiles-negative/empty-instruction/Dockerfile new file mode 100644 index 000000000..74e625a40 --- /dev/null +++ b/builder/parser/testfiles-negative/empty-instruction/Dockerfile @@ -0,0 +1,8 @@ +FROM dockerfile/rabbitmq + +RUN + rabbitmq-plugins enable \ + rabbitmq_shovel \ + rabbitmq_shovel_management \ + rabbitmq_federation \ + rabbitmq_federation_management diff --git a/integration-cli/docker_cli_build_test.go b/integration-cli/docker_cli_build_test.go index a27aecc56..b446f8925 100644 --- a/integration-cli/docker_cli_build_test.go +++ b/integration-cli/docker_cli_build_test.go @@ -22,6 +22,27 @@ import ( "github.com/docker/docker/pkg/archive" ) +func TestBuildEmptyWhitespace(t *testing.T) { + name := "testbuildemptywhitespace" + defer deleteImages(name) + + _, err := buildImage( + name, + ` + FROM busybox + RUN + quux \ + bar + `, + true) + + if err == nil { + t.Fatal("no error when dealing with a RUN statement with no content on the same line") + } + + logDone("build - statements with whitespace and no content should generate a parse error") +} + func TestBuildShCmdJSONEntrypoint(t *testing.T) { name := "testbuildshcmdjsonentrypoint" defer deleteImages(name) From e3b3719f16cbae506d15474c122276e21f59d3a5 Mon Sep 17 00:00:00 2001 From: Jessica Frazelle Date: Tue, 6 Jan 2015 23:10:05 -0800 Subject: [PATCH 203/513] Install lxc from source. Haters gunna hate, cache bust. But this fixes a bunch of tests. Docker-DCO-1.1-Signed-off-by: Jessica Frazelle (github: jfrazelle) --- Dockerfile | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 86130c4ca..b38c142c3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -39,7 +39,6 @@ RUN apt-get update && apt-get install -y \ libapparmor-dev \ libcap-dev \ libsqlite3-dev \ - lxc=1.0* \ mercurial \ parallel \ python-mock \ @@ -62,6 +61,15 @@ RUN cd /usr/local/lvm2 \ && make install_device-mapper # see https://git.fedorahosted.org/cgit/lvm2.git/tree/INSTALL +# Install lxc +RUN mkdir -p /usr/src/lxc \ + && curl -sSL https://linuxcontainers.org/downloads/lxc/lxc-1.0.7.tar.gz | tar -v -C /usr/src/lxc/ -xz --strip-components=1 +RUN cd /usr/src/lxc \ + && ./configure \ + && make \ + && make install \ + && ldconfig + # Install Go RUN curl -sSL https://golang.org/dl/go1.4.src.tar.gz | tar -v -C /usr/local -xz ENV PATH /usr/local/go/bin:$PATH From 17373ca54b9caf2bc24d8a69c6b9639d167cb0cb Mon Sep 17 00:00:00 2001 From: Jessica Frazelle Date: Wed, 7 Jan 2015 09:43:07 -0800 Subject: [PATCH 204/513] Update the LXC version to 1.0.7 in PACKAGERS.md Signed-off-by: Jessica Frazelle Docker-DCO-1.1-Signed-off-by: Jessica Frazelle (github: jfrazelle) --- project/PACKAGERS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/PACKAGERS.md b/project/PACKAGERS.md index 7aba36c89..701e552d5 100644 --- a/project/PACKAGERS.md +++ b/project/PACKAGERS.md @@ -306,7 +306,7 @@ the client will even run on alternative platforms such as Mac OS X / Darwin. Some of Docker's features are activated by using optional command-line flags or by having support for them in the kernel or userspace. A few examples include: -* LXC execution driver (requires version 1.0 or later of the LXC utility scripts) +* LXC execution driver (requires version 1.0.7 or later of lxc and the lxc-libs) * AUFS graph driver (requires AUFS patches/support enabled in the kernel, and at least the "auplink" utility from aufs-tools) * BTRFS graph driver (requires BTRFS support enabled in the kernel) From 568f86eb186731b907b659e4ec64bda21c2fe31d Mon Sep 17 00:00:00 2001 From: Don Kjer Date: Tue, 7 Oct 2014 01:54:52 +0000 Subject: [PATCH 205/513] Deprecating ResolveRepositoryName Passing RepositoryInfo to ResolveAuthConfig, pullRepository, and pushRepository Moving --registry-mirror configuration to registry config Created resolve_repository job Repo names with 'index.docker.io' or 'docker.io' are now synonymous with omitting an index name. Adding test for RepositoryInfo Adding tests for opts.StringSetOpts and registry.ValidateMirror Fixing search term use of repoInfo Adding integration tests for registry mirror configuration Normalizing LookupImage image name to match LocalName parsing rules Normalizing repository LocalName to avoid multiple references to an official image Removing errorOut use in tests Removing TODO comment gofmt changes golint comments cleanup. renaming RegistryOptions => registry.Options, and RegistryServiceConfig => registry.ServiceConfig Splitting out builtins.Registry and registry.NewService calls Stray whitespace cleanup Moving integration tests for Mirrors and InsecureRegistries into TestNewIndexInfo unit test Factoring out ValidateRepositoryName from NewRepositoryInfo Removing unused IndexServerURL Allowing json marshaling of ServiceConfig. Exposing ServiceConfig in /info Switching to CamelCase for json marshaling PR cleanup; removing 'Is' prefix from boolean members. Removing unneeded json tags. Removing non-cleanup related fix for 'localhost:[port]' in splitReposName Merge fixes for gh9735 Fixing integration test Reapplying #9754 Adding comment on config.IndexConfigs use from isSecureIndex Remove unused error return value from isSecureIndex Signed-off-by: Don Kjer Adding back comment in isSecureIndex Signed-off-by: Don Kjer --- api/client/commands.go | 40 +- api/client/utils.go | 2 +- builder/internals.go | 6 +- builder/job.go | 2 +- daemon/config.go | 12 - daemon/daemon.go | 2 +- daemon/info.go | 10 + docker/daemon.go | 6 +- graph/export.go | 2 + graph/pull.go | 115 ++--- graph/push.go | 49 +- graph/tags.go | 40 +- graph/tags_unit_test.go | 120 +++-- integration-cli/docker_cli_build_test.go | 21 + integration-cli/docker_cli_pull_test.go | 31 ++ integration-cli/docker_cli_tag_test.go | 46 ++ integration/utils_test.go | 10 +- opts/opts.go | 24 +- opts/opts_test.go | 18 +- registry/auth.go | 33 +- registry/auth_test.go | 59 ++- registry/config.go | 126 +++++ registry/config_test.go | 49 ++ registry/endpoint.go | 67 ++- registry/endpoint_test.go | 2 +- registry/registry.go | 165 ++++++- registry/registry_mock_test.go | 97 +++- registry/registry_test.go | 577 +++++++++++++++++++++-- registry/service.go | 119 ++++- registry/types.go | 41 ++ utils/http.go | 4 + 31 files changed, 1510 insertions(+), 385 deletions(-) create mode 100644 registry/config.go create mode 100644 registry/config_test.go diff --git a/api/client/commands.go b/api/client/commands.go index 65f975e8d..d6e2c94f3 100644 --- a/api/client/commands.go +++ b/api/client/commands.go @@ -222,7 +222,7 @@ func (cli *DockerCli) CmdBuild(args ...string) error { //Check if the given image name can be resolved if *tag != "" { repository, tag := parsers.ParseRepositoryTag(*tag) - if _, _, err := registry.ResolveRepositoryName(repository); err != nil { + if err := registry.ValidateRepositoryName(repository); err != nil { return err } if len(tag) > 0 { @@ -1148,7 +1148,7 @@ func (cli *DockerCli) CmdImport(args ...string) error { if repository != "" { //Check if the given image name can be resolved repo, _ := parsers.ParseRepositoryTag(repository) - if _, _, err := registry.ResolveRepositoryName(repo); err != nil { + if err := registry.ValidateRepositoryName(repo); err != nil { return err } } @@ -1174,23 +1174,23 @@ func (cli *DockerCli) CmdPush(args ...string) error { remote, tag := parsers.ParseRepositoryTag(name) - // Resolve the Repository name from fqn to hostname + name - hostname, _, err := registry.ResolveRepositoryName(remote) + // Resolve the Repository name from fqn to RepositoryInfo + repoInfo, err := registry.ParseRepositoryInfo(remote) if err != nil { return err } // Resolve the Auth config relevant for this server - authConfig := cli.configFile.ResolveAuthConfig(hostname) + authConfig := cli.configFile.ResolveAuthConfig(repoInfo.Index) // If we're not using a custom registry, we know the restrictions // applied to repository names and can warn the user in advance. // Custom repositories can have different rules, and we must also // allow pushing by image ID. - if len(strings.SplitN(name, "/", 2)) == 1 { - username := cli.configFile.Configs[registry.IndexServerAddress()].Username + if repoInfo.Official { + username := authConfig.Username if username == "" { username = "" } - return fmt.Errorf("You cannot push a \"root\" repository. Please rename your repository in / (ex: %s/%s)", username, name) + return fmt.Errorf("You cannot push a \"root\" repository. Please rename your repository to / (ex: %s/%s)", username, repoInfo.LocalName) } v := url.Values{} @@ -1212,10 +1212,10 @@ func (cli *DockerCli) CmdPush(args ...string) error { if err := push(authConfig); err != nil { if strings.Contains(err.Error(), "Status 401") { fmt.Fprintln(cli.out, "\nPlease login prior to push:") - if err := cli.CmdLogin(hostname); err != nil { + if err := cli.CmdLogin(repoInfo.Index.GetAuthConfigKey()); err != nil { return err } - authConfig := cli.configFile.ResolveAuthConfig(hostname) + authConfig := cli.configFile.ResolveAuthConfig(repoInfo.Index) return push(authConfig) } return err @@ -1245,8 +1245,8 @@ func (cli *DockerCli) CmdPull(args ...string) error { v.Set("fromImage", newRemote) - // Resolve the Repository name from fqn to hostname + name - hostname, _, err := registry.ResolveRepositoryName(taglessRemote) + // Resolve the Repository name from fqn to RepositoryInfo + repoInfo, err := registry.ParseRepositoryInfo(taglessRemote) if err != nil { return err } @@ -1254,7 +1254,7 @@ func (cli *DockerCli) CmdPull(args ...string) error { cli.LoadConfigFile() // Resolve the Auth config relevant for this server - authConfig := cli.configFile.ResolveAuthConfig(hostname) + authConfig := cli.configFile.ResolveAuthConfig(repoInfo.Index) pull := func(authConfig registry.AuthConfig) error { buf, err := json.Marshal(authConfig) @@ -1273,10 +1273,10 @@ func (cli *DockerCli) CmdPull(args ...string) error { if err := pull(authConfig); err != nil { if strings.Contains(err.Error(), "Status 401") { fmt.Fprintln(cli.out, "\nPlease login prior to pull:") - if err := cli.CmdLogin(hostname); err != nil { + if err := cli.CmdLogin(repoInfo.Index.GetAuthConfigKey()); err != nil { return err } - authConfig := cli.configFile.ResolveAuthConfig(hostname) + authConfig := cli.configFile.ResolveAuthConfig(repoInfo.Index) return pull(authConfig) } return err @@ -1691,7 +1691,7 @@ func (cli *DockerCli) CmdCommit(args ...string) error { //Check if the given image name can be resolved if repository != "" { - if _, _, err := registry.ResolveRepositoryName(repository); err != nil { + if err := registry.ValidateRepositoryName(repository); err != nil { return err } } @@ -2002,7 +2002,7 @@ func (cli *DockerCli) CmdTag(args ...string) error { ) //Check if the given image name can be resolved - if _, _, err := registry.ResolveRepositoryName(repository); err != nil { + if err := registry.ValidateRepositoryName(repository); err != nil { return err } v.Set("repo", repository) @@ -2032,8 +2032,8 @@ func (cli *DockerCli) pullImageCustomOut(image string, out io.Writer) error { v.Set("fromImage", repos) v.Set("tag", tag) - // Resolve the Repository name from fqn to hostname + name - hostname, _, err := registry.ResolveRepositoryName(repos) + // Resolve the Repository name from fqn to RepositoryInfo + repoInfo, err := registry.ParseRepositoryInfo(repos) if err != nil { return err } @@ -2042,7 +2042,7 @@ func (cli *DockerCli) pullImageCustomOut(image string, out io.Writer) error { cli.LoadConfigFile() // Resolve the Auth config relevant for this server - authConfig := cli.configFile.ResolveAuthConfig(hostname) + authConfig := cli.configFile.ResolveAuthConfig(repoInfo.Index) buf, err := json.Marshal(authConfig) if err != nil { return err diff --git a/api/client/utils.go b/api/client/utils.go index 8de571bf4..6ebe44806 100644 --- a/api/client/utils.go +++ b/api/client/utils.go @@ -66,7 +66,7 @@ func (cli *DockerCli) call(method, path string, data interface{}, passAuthInfo b if passAuthInfo { cli.LoadConfigFile() // Resolve the Auth config relevant for this server - authConfig := cli.configFile.ResolveAuthConfig(registry.IndexServerAddress()) + authConfig := cli.configFile.Configs[registry.IndexServerAddress()] getHeaders := func(authConfig registry.AuthConfig) (map[string][]string, error) { buf, err := json.Marshal(authConfig) if err != nil { diff --git a/builder/internals.go b/builder/internals.go index 909e7a8d1..9d9a65f9e 100644 --- a/builder/internals.go +++ b/builder/internals.go @@ -427,17 +427,17 @@ func (b *Builder) pullImage(name string) (*imagepkg.Image, error) { if tag == "" { tag = "latest" } + job := b.Engine.Job("pull", remote, tag) pullRegistryAuth := b.AuthConfig if len(b.AuthConfigFile.Configs) > 0 { // The request came with a full auth config file, we prefer to use that - endpoint, _, err := registry.ResolveRepositoryName(remote) + repoInfo, err := registry.ResolveRepositoryInfo(job, remote) if err != nil { return nil, err } - resolvedAuth := b.AuthConfigFile.ResolveAuthConfig(endpoint) + resolvedAuth := b.AuthConfigFile.ResolveAuthConfig(repoInfo.Index) pullRegistryAuth = &resolvedAuth } - job := b.Engine.Job("pull", remote, tag) job.SetenvBool("json", b.StreamFormatter.Json()) job.SetenvBool("parallel", true) job.SetenvJson("authConfig", pullRegistryAuth) diff --git a/builder/job.go b/builder/job.go index 905a8cc99..53490b7e5 100644 --- a/builder/job.go +++ b/builder/job.go @@ -50,7 +50,7 @@ func (b *BuilderJob) CmdBuild(job *engine.Job) engine.Status { repoName, tag = parsers.ParseRepositoryTag(repoName) if repoName != "" { - if _, _, err := registry.ResolveRepositoryName(repoName); err != nil { + if err := registry.ValidateRepositoryName(repoName); err != nil { return job.Error(err) } if len(tag) > 0 { diff --git a/daemon/config.go b/daemon/config.go index 4d9041e89..c5ac056d2 100644 --- a/daemon/config.go +++ b/daemon/config.go @@ -23,7 +23,6 @@ type Config struct { AutoRestart bool Dns []string DnsSearch []string - Mirrors []string EnableIptables bool EnableIpForward bool EnableIpMasq bool @@ -31,7 +30,6 @@ type Config struct { BridgeIface string BridgeIP string FixedCIDR string - InsecureRegistries []string InterContainerCommunication bool GraphDriver string GraphOptions []string @@ -58,7 +56,6 @@ func (config *Config) InstallFlags() { flag.StringVar(&config.BridgeIP, []string{"#bip", "-bip"}, "", "Use this CIDR notation address for the network bridge's IP, not compatible with -b") flag.StringVar(&config.BridgeIface, []string{"b", "-bridge"}, "", "Attach containers to a pre-existing network bridge\nuse 'none' to disable container networking") flag.StringVar(&config.FixedCIDR, []string{"-fixed-cidr"}, "", "IPv4 subnet for fixed IPs (ex: 10.20.0.0/16)\nthis subnet must be nested in the bridge subnet (which is defined by -b or --bip)") - opts.ListVar(&config.InsecureRegistries, []string{"-insecure-registry"}, "Enable insecure communication with specified registries (no certificate verification for HTTPS and enable HTTP fallback) (e.g., localhost:5000 or 10.20.0.0/16)") flag.BoolVar(&config.InterContainerCommunication, []string{"#icc", "-icc"}, true, "Allow unrestricted inter-container and Docker daemon host communication") flag.StringVar(&config.GraphDriver, []string{"s", "-storage-driver"}, "", "Force the Docker runtime to use a specific storage driver") flag.StringVar(&config.ExecDriver, []string{"e", "-exec-driver"}, "native", "Force the Docker runtime to use a specific exec driver") @@ -69,16 +66,7 @@ func (config *Config) InstallFlags() { // FIXME: why the inconsistency between "hosts" and "sockets"? opts.IPListVar(&config.Dns, []string{"#dns", "-dns"}, "Force Docker to use specific DNS servers") opts.DnsSearchListVar(&config.DnsSearch, []string{"-dns-search"}, "Force Docker to use specific DNS search domains") - opts.MirrorListVar(&config.Mirrors, []string{"-registry-mirror"}, "Specify a preferred Docker registry mirror") opts.LabelListVar(&config.Labels, []string{"-label"}, "Set key=value labels to the daemon (displayed in `docker info`)") - - // Localhost is by default considered as an insecure registry - // This is a stop-gap for people who are running a private registry on localhost (especially on Boot2docker). - // - // TODO: should we deprecate this once it is easier for people to set up a TLS registry or change - // daemon flags on boot2docker? - // If so, do not forget to check the TODO in TestIsSecure - config.InsecureRegistries = append(config.InsecureRegistries, "127.0.0.0/8") } func getDefaultNetworkMtu() int { diff --git a/daemon/daemon.go b/daemon/daemon.go index 8ad677bed..cb162780d 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -841,7 +841,7 @@ func NewDaemonFromDirectory(config *Config, eng *engine.Engine) (*Daemon, error) } log.Debugf("Creating repository list") - repositories, err := graph.NewTagStore(path.Join(config.Root, "repositories-"+driver.String()), g, config.Mirrors, config.InsecureRegistries) + repositories, err := graph.NewTagStore(path.Join(config.Root, "repositories-"+driver.String()), g) if err != nil { return nil, fmt.Errorf("Couldn't create Tag store: %s", err) } diff --git a/daemon/info.go b/daemon/info.go index bf7ec9968..8eb4358f4 100644 --- a/daemon/info.go +++ b/daemon/info.go @@ -55,6 +55,15 @@ func (daemon *Daemon) CmdInfo(job *engine.Job) engine.Status { if err := cjob.Run(); err != nil { return job.Error(err) } + registryJob := job.Eng.Job("registry_config") + registryEnv, _ := registryJob.Stdout.AddEnv() + if err := registryJob.Run(); err != nil { + return job.Error(err) + } + registryConfig := registry.ServiceConfig{} + if err := registryEnv.GetJson("config", ®istryConfig); err != nil { + return job.Error(err) + } v := &engine.Env{} v.SetJson("ID", daemon.ID) v.SetInt("Containers", len(daemon.List())) @@ -72,6 +81,7 @@ func (daemon *Daemon) CmdInfo(job *engine.Job) engine.Status { v.Set("KernelVersion", kernelVersion) v.Set("OperatingSystem", operatingSystem) v.Set("IndexServerAddress", registry.IndexServerAddress()) + v.SetJson("RegistryConfig", registryConfig) v.Set("InitSha1", dockerversion.INITSHA1) v.Set("InitPath", initPath) v.SetInt("NCPU", runtime.NumCPU()) diff --git a/docker/daemon.go b/docker/daemon.go index 3128f7ee5..508a75bd8 100644 --- a/docker/daemon.go +++ b/docker/daemon.go @@ -19,11 +19,13 @@ import ( const CanDaemon = true var ( - daemonCfg = &daemon.Config{} + daemonCfg = &daemon.Config{} + registryCfg = ®istry.Options{} ) func init() { daemonCfg.InstallFlags() + registryCfg.InstallFlags() } func mainDaemon() { @@ -42,7 +44,7 @@ func mainDaemon() { } // load registry service - if err := registry.NewService(daemonCfg.InsecureRegistries).Install(eng); err != nil { + if err := registry.NewService(registryCfg).Install(eng); err != nil { log.Fatal(err) } diff --git a/graph/export.go b/graph/export.go index 7a8054010..3f7ecd3c4 100644 --- a/graph/export.go +++ b/graph/export.go @@ -11,6 +11,7 @@ import ( "github.com/docker/docker/engine" "github.com/docker/docker/pkg/archive" "github.com/docker/docker/pkg/parsers" + "github.com/docker/docker/registry" ) // CmdImageExport exports all images with the given tag. All versions @@ -39,6 +40,7 @@ func (s *TagStore) CmdImageExport(job *engine.Job) engine.Status { } } for _, name := range job.Args { + name = registry.NormalizeLocalName(name) log.Debugf("Serializing %s", name) rootRepo := s.Repositories[name] if rootRepo != nil { diff --git a/graph/pull.go b/graph/pull.go index 716a27c90..587eb5f50 100644 --- a/graph/pull.go +++ b/graph/pull.go @@ -85,9 +85,14 @@ func (s *TagStore) CmdPull(job *engine.Job) engine.Status { sf = utils.NewStreamFormatter(job.GetenvBool("json")) authConfig = ®istry.AuthConfig{} metaHeaders map[string][]string - mirrors []string ) + // Resolve the Repository name from fqn to RepositoryInfo + repoInfo, err := registry.ResolveRepositoryInfo(job, localName) + if err != nil { + return job.Error(err) + } + if len(job.Args) > 1 { tag = job.Args[1] } @@ -95,25 +100,19 @@ func (s *TagStore) CmdPull(job *engine.Job) engine.Status { job.GetenvJson("authConfig", authConfig) job.GetenvJson("metaHeaders", &metaHeaders) - c, err := s.poolAdd("pull", localName+":"+tag) + c, err := s.poolAdd("pull", repoInfo.LocalName+":"+tag) if err != nil { if c != nil { // Another pull of the same repository is already taking place; just wait for it to finish - job.Stdout.Write(sf.FormatStatus("", "Repository %s already being pulled by another client. Waiting.", localName)) + job.Stdout.Write(sf.FormatStatus("", "Repository %s already being pulled by another client. Waiting.", repoInfo.LocalName)) <-c return engine.StatusOK } return job.Error(err) } - defer s.poolRemove("pull", localName+":"+tag) + defer s.poolRemove("pull", repoInfo.LocalName+":"+tag) - // Resolve the Repository name from fqn to endpoint + name - hostname, remoteName, err := registry.ResolveRepositoryName(localName) - if err != nil { - return job.Error(err) - } - - endpoint, err := registry.NewEndpoint(hostname, s.insecureRegistries) + endpoint, err := repoInfo.GetEndpoint() if err != nil { return job.Error(err) } @@ -123,32 +122,18 @@ func (s *TagStore) CmdPull(job *engine.Job) engine.Status { return job.Error(err) } - var isOfficial bool - if endpoint.VersionString(1) == registry.IndexServerAddress() { - // If pull "index.docker.io/foo/bar", it's stored locally under "foo/bar" - localName = remoteName - - isOfficial = isOfficialName(remoteName) - if isOfficial && strings.IndexRune(remoteName, '/') == -1 { - remoteName = "library/" + remoteName - } - - // Use provided mirrors, if any - mirrors = s.mirrors - } - - logName := localName + logName := repoInfo.LocalName if tag != "" { logName += ":" + tag } - if len(mirrors) == 0 && (isOfficial || endpoint.Version == registry.APIVersion2) { + if len(repoInfo.Index.Mirrors) == 0 && (repoInfo.Official || endpoint.Version == registry.APIVersion2) { j := job.Eng.Job("trust_update_base") if err = j.Run(); err != nil { return job.Errorf("error updating trust base graph: %s", err) } - if err := s.pullV2Repository(job.Eng, r, job.Stdout, localName, remoteName, tag, sf, job.GetenvBool("parallel")); err == nil { + if err := s.pullV2Repository(job.Eng, r, job.Stdout, repoInfo, tag, sf, job.GetenvBool("parallel")); err == nil { if err = job.Eng.Job("log", "pull", logName, "").Run(); err != nil { log.Errorf("Error logging event 'pull' for %s: %s", logName, err) } @@ -158,7 +143,7 @@ func (s *TagStore) CmdPull(job *engine.Job) engine.Status { } } - if err = s.pullRepository(r, job.Stdout, localName, remoteName, tag, sf, job.GetenvBool("parallel"), mirrors); err != nil { + if err = s.pullRepository(r, job.Stdout, repoInfo, tag, sf, job.GetenvBool("parallel")); err != nil { return job.Error(err) } @@ -169,20 +154,20 @@ func (s *TagStore) CmdPull(job *engine.Job) engine.Status { return engine.StatusOK } -func (s *TagStore) pullRepository(r *registry.Session, out io.Writer, localName, remoteName, askedTag string, sf *utils.StreamFormatter, parallel bool, mirrors []string) error { - out.Write(sf.FormatStatus("", "Pulling repository %s", localName)) +func (s *TagStore) pullRepository(r *registry.Session, out io.Writer, repoInfo *registry.RepositoryInfo, askedTag string, sf *utils.StreamFormatter, parallel bool) error { + out.Write(sf.FormatStatus("", "Pulling repository %s", repoInfo.CanonicalName)) - repoData, err := r.GetRepositoryData(remoteName) + repoData, err := r.GetRepositoryData(repoInfo.RemoteName) if err != nil { if strings.Contains(err.Error(), "HTTP code: 404") { - return fmt.Errorf("Error: image %s:%s not found", remoteName, askedTag) + return fmt.Errorf("Error: image %s:%s not found", repoInfo.RemoteName, askedTag) } // Unexpected HTTP error return err } log.Debugf("Retrieving the tag list") - tagsList, err := r.GetRemoteTags(repoData.Endpoints, remoteName, repoData.Tokens) + tagsList, err := r.GetRemoteTags(repoData.Endpoints, repoInfo.RemoteName, repoData.Tokens) if err != nil { log.Errorf("%v", err) return err @@ -207,7 +192,7 @@ func (s *TagStore) pullRepository(r *registry.Session, out io.Writer, localName, // Otherwise, check that the tag exists and use only that one id, exists := tagsList[askedTag] if !exists { - return fmt.Errorf("Tag %s not found in repository %s", askedTag, localName) + return fmt.Errorf("Tag %s not found in repository %s", askedTag, repoInfo.CanonicalName) } imageId = id repoData.ImgList[id].Tag = askedTag @@ -250,31 +235,29 @@ func (s *TagStore) pullRepository(r *registry.Session, out io.Writer, localName, } defer s.poolRemove("pull", "img:"+img.ID) - out.Write(sf.FormatProgress(utils.TruncateID(img.ID), fmt.Sprintf("Pulling image (%s) from %s", img.Tag, localName), nil)) + out.Write(sf.FormatProgress(utils.TruncateID(img.ID), fmt.Sprintf("Pulling image (%s) from %s", img.Tag, repoInfo.CanonicalName), nil)) success := false var lastErr, err error var is_downloaded bool - if mirrors != nil { - for _, ep := range mirrors { - out.Write(sf.FormatProgress(utils.TruncateID(img.ID), fmt.Sprintf("Pulling image (%s) from %s, mirror: %s", img.Tag, localName, ep), nil)) - if is_downloaded, err = s.pullImage(r, out, img.ID, ep, repoData.Tokens, sf); err != nil { - // Don't report errors when pulling from mirrors. - log.Debugf("Error pulling image (%s) from %s, mirror: %s, %s", img.Tag, localName, ep, err) - continue - } - layers_downloaded = layers_downloaded || is_downloaded - success = true - break + for _, ep := range repoInfo.Index.Mirrors { + out.Write(sf.FormatProgress(utils.TruncateID(img.ID), fmt.Sprintf("Pulling image (%s) from %s, mirror: %s", img.Tag, repoInfo.CanonicalName, ep), nil)) + if is_downloaded, err = s.pullImage(r, out, img.ID, ep, repoData.Tokens, sf); err != nil { + // Don't report errors when pulling from mirrors. + log.Debugf("Error pulling image (%s) from %s, mirror: %s, %s", img.Tag, repoInfo.CanonicalName, ep, err) + continue } + layers_downloaded = layers_downloaded || is_downloaded + success = true + break } if !success { for _, ep := range repoData.Endpoints { - out.Write(sf.FormatProgress(utils.TruncateID(img.ID), fmt.Sprintf("Pulling image (%s) from %s, endpoint: %s", img.Tag, localName, ep), nil)) + out.Write(sf.FormatProgress(utils.TruncateID(img.ID), fmt.Sprintf("Pulling image (%s) from %s, endpoint: %s", img.Tag, repoInfo.CanonicalName, ep), nil)) if is_downloaded, err = s.pullImage(r, out, img.ID, ep, repoData.Tokens, sf); err != nil { // It's not ideal that only the last error is returned, it would be better to concatenate the errors. // As the error is also given to the output stream the user will see the error. lastErr = err - out.Write(sf.FormatProgress(utils.TruncateID(img.ID), fmt.Sprintf("Error pulling image (%s) from %s, endpoint: %s, %s", img.Tag, localName, ep, err), nil)) + out.Write(sf.FormatProgress(utils.TruncateID(img.ID), fmt.Sprintf("Error pulling image (%s) from %s, endpoint: %s, %s", img.Tag, repoInfo.CanonicalName, ep, err), nil)) continue } layers_downloaded = layers_downloaded || is_downloaded @@ -283,7 +266,7 @@ func (s *TagStore) pullRepository(r *registry.Session, out io.Writer, localName, } } if !success { - err := fmt.Errorf("Error pulling image (%s) from %s, %v", img.Tag, localName, lastErr) + err := fmt.Errorf("Error pulling image (%s) from %s, %v", img.Tag, repoInfo.CanonicalName, lastErr) out.Write(sf.FormatProgress(utils.TruncateID(img.ID), err.Error(), nil)) if parallel { errors <- err @@ -319,14 +302,14 @@ func (s *TagStore) pullRepository(r *registry.Session, out io.Writer, localName, if askedTag != "" && id != imageId { continue } - if err := s.Set(localName, tag, id, true); err != nil { + if err := s.Set(repoInfo.LocalName, tag, id, true); err != nil { return err } } - requestedTag := localName + requestedTag := repoInfo.CanonicalName if len(askedTag) > 0 { - requestedTag = localName + ":" + askedTag + requestedTag = repoInfo.CanonicalName + ":" + askedTag } WriteStatus(requestedTag, out, sf, layers_downloaded) return nil @@ -440,40 +423,40 @@ type downloadInfo struct { err chan error } -func (s *TagStore) pullV2Repository(eng *engine.Engine, r *registry.Session, out io.Writer, localName, remoteName, tag string, sf *utils.StreamFormatter, parallel bool) error { +func (s *TagStore) pullV2Repository(eng *engine.Engine, r *registry.Session, out io.Writer, repoInfo *registry.RepositoryInfo, tag string, sf *utils.StreamFormatter, parallel bool) error { var layersDownloaded bool if tag == "" { - log.Debugf("Pulling tag list from V2 registry for %s", remoteName) - tags, err := r.GetV2RemoteTags(remoteName, nil) + log.Debugf("Pulling tag list from V2 registry for %s", repoInfo.CanonicalName) + tags, err := r.GetV2RemoteTags(repoInfo.RemoteName, nil) if err != nil { return err } for _, t := range tags { - if downloaded, err := s.pullV2Tag(eng, r, out, localName, remoteName, t, sf, parallel); err != nil { + if downloaded, err := s.pullV2Tag(eng, r, out, repoInfo, t, sf, parallel); err != nil { return err } else if downloaded { layersDownloaded = true } } } else { - if downloaded, err := s.pullV2Tag(eng, r, out, localName, remoteName, tag, sf, parallel); err != nil { + if downloaded, err := s.pullV2Tag(eng, r, out, repoInfo, tag, sf, parallel); err != nil { return err } else if downloaded { layersDownloaded = true } } - requestedTag := localName + requestedTag := repoInfo.CanonicalName if len(tag) > 0 { - requestedTag = localName + ":" + tag + requestedTag = repoInfo.CanonicalName + ":" + tag } WriteStatus(requestedTag, out, sf, layersDownloaded) return nil } -func (s *TagStore) pullV2Tag(eng *engine.Engine, r *registry.Session, out io.Writer, localName, remoteName, tag string, sf *utils.StreamFormatter, parallel bool) (bool, error) { +func (s *TagStore) pullV2Tag(eng *engine.Engine, r *registry.Session, out io.Writer, repoInfo *registry.RepositoryInfo, tag string, sf *utils.StreamFormatter, parallel bool) (bool, error) { log.Debugf("Pulling tag from V2 registry: %q", tag) - manifestBytes, err := r.GetV2ImageManifest(remoteName, tag, nil) + manifestBytes, err := r.GetV2ImageManifest(repoInfo.RemoteName, tag, nil) if err != nil { return false, err } @@ -488,9 +471,9 @@ func (s *TagStore) pullV2Tag(eng *engine.Engine, r *registry.Session, out io.Wri } if verified { - out.Write(sf.FormatStatus(localName+":"+tag, "The image you are pulling has been verified")) + out.Write(sf.FormatStatus(repoInfo.CanonicalName+":"+tag, "The image you are pulling has been verified")) } else { - out.Write(sf.FormatStatus(tag, "Pulling from %s", localName)) + out.Write(sf.FormatStatus(tag, "Pulling from %s", repoInfo.CanonicalName)) } if len(manifest.FSLayers) == 0 { @@ -542,7 +525,7 @@ func (s *TagStore) pullV2Tag(eng *engine.Engine, r *registry.Session, out io.Wri return err } - r, l, err := r.GetV2ImageBlobReader(remoteName, sumType, checksum, nil) + r, l, err := r.GetV2ImageBlobReader(repoInfo.RemoteName, sumType, checksum, nil) if err != nil { return err } @@ -605,7 +588,7 @@ func (s *TagStore) pullV2Tag(eng *engine.Engine, r *registry.Session, out io.Wri } - if err = s.Set(localName, tag, downloads[0].img.ID, true); err != nil { + if err = s.Set(repoInfo.LocalName, tag, downloads[0].img.ID, true); err != nil { return false, err } diff --git a/graph/push.go b/graph/push.go index 77db24381..09e13a5cf 100644 --- a/graph/push.go +++ b/graph/push.go @@ -61,7 +61,7 @@ func (s *TagStore) getImageList(localRepo map[string]string, requestedTag string return imageList, tagsByImage, nil } -func (s *TagStore) pushRepository(r *registry.Session, out io.Writer, localName, remoteName string, localRepo map[string]string, tag string, sf *utils.StreamFormatter) error { +func (s *TagStore) pushRepository(r *registry.Session, out io.Writer, repoInfo *registry.RepositoryInfo, localRepo map[string]string, tag string, sf *utils.StreamFormatter) error { out = utils.NewWriteFlusher(out) log.Debugf("Local repo: %s", localRepo) imgList, tagsByImage, err := s.getImageList(localRepo, tag) @@ -104,7 +104,7 @@ func (s *TagStore) pushRepository(r *registry.Session, out io.Writer, localName, // Register all the images in a repository with the registry // If an image is not in this list it will not be associated with the repository - repoData, err = r.PushImageJSONIndex(remoteName, imageIndex, false, nil) + repoData, err = r.PushImageJSONIndex(repoInfo.RemoteName, imageIndex, false, nil) if err != nil { return err } @@ -114,11 +114,11 @@ func (s *TagStore) pushRepository(r *registry.Session, out io.Writer, localName, nTag = len(localRepo) } for _, ep := range repoData.Endpoints { - out.Write(sf.FormatStatus("", "Pushing repository %s (%d tags)", localName, nTag)) + out.Write(sf.FormatStatus("", "Pushing repository %s (%d tags)", repoInfo.CanonicalName, nTag)) for _, imgId := range imgList { if err := r.LookupRemoteImage(imgId, ep, repoData.Tokens); err != nil { log.Errorf("Error in LookupRemoteImage: %s", err) - if _, err := s.pushImage(r, out, remoteName, imgId, ep, repoData.Tokens, sf); err != nil { + if _, err := s.pushImage(r, out, imgId, ep, repoData.Tokens, sf); err != nil { // FIXME: Continue on error? return err } @@ -126,23 +126,23 @@ func (s *TagStore) pushRepository(r *registry.Session, out io.Writer, localName, out.Write(sf.FormatStatus("", "Image %s already pushed, skipping", utils.TruncateID(imgId))) } for _, tag := range tagsByImage[imgId] { - out.Write(sf.FormatStatus("", "Pushing tag for rev [%s] on {%s}", utils.TruncateID(imgId), ep+"repositories/"+remoteName+"/tags/"+tag)) + out.Write(sf.FormatStatus("", "Pushing tag for rev [%s] on {%s}", utils.TruncateID(imgId), ep+"repositories/"+repoInfo.RemoteName+"/tags/"+tag)) - if err := r.PushRegistryTag(remoteName, imgId, tag, ep, repoData.Tokens); err != nil { + if err := r.PushRegistryTag(repoInfo.RemoteName, imgId, tag, ep, repoData.Tokens); err != nil { return err } } } } - if _, err := r.PushImageJSONIndex(remoteName, imageIndex, true, repoData.Endpoints); err != nil { + if _, err := r.PushImageJSONIndex(repoInfo.RemoteName, imageIndex, true, repoData.Endpoints); err != nil { return err } return nil } -func (s *TagStore) pushImage(r *registry.Session, out io.Writer, remote, imgID, ep string, token []string, sf *utils.StreamFormatter) (checksum string, err error) { +func (s *TagStore) pushImage(r *registry.Session, out io.Writer, imgID, ep string, token []string, sf *utils.StreamFormatter) (checksum string, err error) { out = utils.NewWriteFlusher(out) jsonRaw, err := ioutil.ReadFile(path.Join(s.graph.Root, imgID, "json")) if err != nil { @@ -199,26 +199,27 @@ func (s *TagStore) CmdPush(job *engine.Job) engine.Status { metaHeaders map[string][]string ) + // Resolve the Repository name from fqn to RepositoryInfo + repoInfo, err := registry.ResolveRepositoryInfo(job, localName) + if err != nil { + return job.Error(err) + } + tag := job.Getenv("tag") job.GetenvJson("authConfig", authConfig) job.GetenvJson("metaHeaders", &metaHeaders) - if _, err := s.poolAdd("push", localName); err != nil { + + if _, err := s.poolAdd("push", repoInfo.LocalName); err != nil { return job.Error(err) } - defer s.poolRemove("push", localName) + defer s.poolRemove("push", repoInfo.LocalName) - // Resolve the Repository name from fqn to endpoint + name - hostname, remoteName, err := registry.ResolveRepositoryName(localName) + endpoint, err := repoInfo.GetEndpoint() if err != nil { return job.Error(err) } - endpoint, err := registry.NewEndpoint(hostname, s.insecureRegistries) - if err != nil { - return job.Error(err) - } - - img, err := s.graph.Get(localName) + img, err := s.graph.Get(repoInfo.LocalName) r, err2 := registry.NewSession(authConfig, registry.HTTPRequestFactory(metaHeaders), endpoint, false) if err2 != nil { return job.Error(err2) @@ -227,12 +228,12 @@ func (s *TagStore) CmdPush(job *engine.Job) engine.Status { if err != nil { reposLen := 1 if tag == "" { - reposLen = len(s.Repositories[localName]) + reposLen = len(s.Repositories[repoInfo.LocalName]) } - job.Stdout.Write(sf.FormatStatus("", "The push refers to a repository [%s] (len: %d)", localName, reposLen)) + job.Stdout.Write(sf.FormatStatus("", "The push refers to a repository [%s] (len: %d)", repoInfo.CanonicalName, reposLen)) // If it fails, try to get the repository - if localRepo, exists := s.Repositories[localName]; exists { - if err := s.pushRepository(r, job.Stdout, localName, remoteName, localRepo, tag, sf); err != nil { + if localRepo, exists := s.Repositories[repoInfo.LocalName]; exists { + if err := s.pushRepository(r, job.Stdout, repoInfo, localRepo, tag, sf); err != nil { return job.Error(err) } return engine.StatusOK @@ -241,8 +242,8 @@ func (s *TagStore) CmdPush(job *engine.Job) engine.Status { } var token []string - job.Stdout.Write(sf.FormatStatus("", "The push refers to an image: [%s]", localName)) - if _, err := s.pushImage(r, job.Stdout, remoteName, img.ID, endpoint.String(), token, sf); err != nil { + job.Stdout.Write(sf.FormatStatus("", "The push refers to an image: [%s]", repoInfo.CanonicalName)) + if _, err := s.pushImage(r, job.Stdout, img.ID, endpoint.String(), token, sf); err != nil { return job.Error(err) } return engine.StatusOK diff --git a/graph/tags.go b/graph/tags.go index d584ac2a0..998b447e6 100644 --- a/graph/tags.go +++ b/graph/tags.go @@ -13,6 +13,7 @@ import ( "github.com/docker/docker/image" "github.com/docker/docker/pkg/parsers" + "github.com/docker/docker/registry" "github.com/docker/docker/utils" ) @@ -23,11 +24,9 @@ var ( ) type TagStore struct { - path string - graph *Graph - mirrors []string - insecureRegistries []string - Repositories map[string]Repository + path string + graph *Graph + Repositories map[string]Repository sync.Mutex // FIXME: move push/pull-related fields // to a helper type @@ -55,20 +54,18 @@ func (r Repository) Contains(u Repository) bool { return true } -func NewTagStore(path string, graph *Graph, mirrors []string, insecureRegistries []string) (*TagStore, error) { +func NewTagStore(path string, graph *Graph) (*TagStore, error) { abspath, err := filepath.Abs(path) if err != nil { return nil, err } store := &TagStore{ - path: abspath, - graph: graph, - mirrors: mirrors, - insecureRegistries: insecureRegistries, - Repositories: make(map[string]Repository), - pullingPool: make(map[string]chan struct{}), - pushingPool: make(map[string]chan struct{}), + path: abspath, + graph: graph, + Repositories: make(map[string]Repository), + pullingPool: make(map[string]chan struct{}), + pushingPool: make(map[string]chan struct{}), } // Load the json file if it exists, otherwise create it. if err := store.reload(); os.IsNotExist(err) { @@ -178,6 +175,7 @@ func (store *TagStore) Delete(repoName, tag string) (bool, error) { if err := store.reload(); err != nil { return false, err } + repoName = registry.NormalizeLocalName(repoName) if r, exists := store.Repositories[repoName]; exists { if tag != "" { if _, exists2 := r[tag]; exists2 { @@ -219,6 +217,7 @@ func (store *TagStore) Set(repoName, tag, imageName string, force bool) error { return err } var repo Repository + repoName = registry.NormalizeLocalName(repoName) if r, exists := store.Repositories[repoName]; exists { repo = r if old, exists := store.Repositories[repoName][tag]; exists && !force { @@ -238,6 +237,7 @@ func (store *TagStore) Get(repoName string) (Repository, error) { if err := store.reload(); err != nil { return nil, err } + repoName = registry.NormalizeLocalName(repoName) if r, exists := store.Repositories[repoName]; exists { return r, nil } @@ -279,20 +279,6 @@ func (store *TagStore) GetRepoRefs() map[string][]string { return reporefs } -// isOfficialName returns whether a repo name is considered an official -// repository. Official repositories are repos with names within -// the library namespace or which default to the library namespace -// by not providing one. -func isOfficialName(name string) bool { - if strings.HasPrefix(name, "library/") { - return true - } - if strings.IndexRune(name, '/') == -1 { - return true - } - return false -} - // Validate the name of a repository func validateRepoName(name string) error { if name == "" { diff --git a/graph/tags_unit_test.go b/graph/tags_unit_test.go index 339fb51fc..45dad6295 100644 --- a/graph/tags_unit_test.go +++ b/graph/tags_unit_test.go @@ -15,8 +15,12 @@ import ( ) const ( - testImageName = "myapp" - testImageID = "1a2d3c4d4e5fa2d2a21acea242a5e2345d3aefc3e7dfa2a2a2a21a2a2ad2d234" + testOfficialImageName = "myapp" + testOfficialImageID = "1a2d3c4d4e5fa2d2a21acea242a5e2345d3aefc3e7dfa2a2a2a21a2a2ad2d234" + testOfficialImageIDShort = "1a2d3c4d4e5f" + testPrivateImageName = "127.0.0.1:8000/privateapp" + testPrivateImageID = "5bc255f8699e4ee89ac4469266c3d11515da88fdcbde45d7b069b636ff4efd81" + testPrivateImageIDShort = "5bc255f8699e" ) func fakeTar() (io.Reader, error) { @@ -53,19 +57,30 @@ func mkTestTagStore(root string, t *testing.T) *TagStore { if err != nil { t.Fatal(err) } - store, err := NewTagStore(path.Join(root, "tags"), graph, nil, nil) + store, err := NewTagStore(path.Join(root, "tags"), graph) if err != nil { t.Fatal(err) } - archive, err := fakeTar() + officialArchive, err := fakeTar() if err != nil { t.Fatal(err) } - img := &image.Image{ID: testImageID} - if err := graph.Register(img, archive); err != nil { + img := &image.Image{ID: testOfficialImageID} + if err := graph.Register(img, officialArchive); err != nil { t.Fatal(err) } - if err := store.Set(testImageName, "", testImageID, false); err != nil { + if err := store.Set(testOfficialImageName, "", testOfficialImageID, false); err != nil { + t.Fatal(err) + } + privateArchive, err := fakeTar() + if err != nil { + t.Fatal(err) + } + img = &image.Image{ID: testPrivateImageID} + if err := graph.Register(img, privateArchive); err != nil { + t.Fatal(err) + } + if err := store.Set(testPrivateImageName, "", testPrivateImageID, false); err != nil { t.Fatal(err) } return store @@ -80,39 +95,65 @@ func TestLookupImage(t *testing.T) { store := mkTestTagStore(tmp, t) defer store.graph.driver.Cleanup() - if img, err := store.LookupImage(testImageName); err != nil { - t.Fatal(err) - } else if img == nil { - t.Errorf("Expected 1 image, none found") - } - if img, err := store.LookupImage(testImageName + ":" + DEFAULTTAG); err != nil { - t.Fatal(err) - } else if img == nil { - t.Errorf("Expected 1 image, none found") + officialLookups := []string{ + testOfficialImageID, + testOfficialImageIDShort, + testOfficialImageName + ":" + testOfficialImageID, + testOfficialImageName + ":" + testOfficialImageIDShort, + testOfficialImageName, + testOfficialImageName + ":" + DEFAULTTAG, + "docker.io/" + testOfficialImageName, + "docker.io/" + testOfficialImageName + ":" + DEFAULTTAG, + "index.docker.io/" + testOfficialImageName, + "index.docker.io/" + testOfficialImageName + ":" + DEFAULTTAG, + "library/" + testOfficialImageName, + "library/" + testOfficialImageName + ":" + DEFAULTTAG, + "docker.io/library/" + testOfficialImageName, + "docker.io/library/" + testOfficialImageName + ":" + DEFAULTTAG, + "index.docker.io/library/" + testOfficialImageName, + "index.docker.io/library/" + testOfficialImageName + ":" + DEFAULTTAG, } - if img, err := store.LookupImage(testImageName + ":" + "fail"); err == nil { - t.Errorf("Expected error, none found") - } else if img != nil { - t.Errorf("Expected 0 image, 1 found") + privateLookups := []string{ + testPrivateImageID, + testPrivateImageIDShort, + testPrivateImageName + ":" + testPrivateImageID, + testPrivateImageName + ":" + testPrivateImageIDShort, + testPrivateImageName, + testPrivateImageName + ":" + DEFAULTTAG, } - if img, err := store.LookupImage("fail:fail"); err == nil { - t.Errorf("Expected error, none found") - } else if img != nil { - t.Errorf("Expected 0 image, 1 found") + invalidLookups := []string{ + testOfficialImageName + ":" + "fail", + "fail:fail", } - if img, err := store.LookupImage(testImageID); err != nil { - t.Fatal(err) - } else if img == nil { - t.Errorf("Expected 1 image, none found") + for _, name := range officialLookups { + if img, err := store.LookupImage(name); err != nil { + t.Errorf("Error looking up %s: %s", name, err) + } else if img == nil { + t.Errorf("Expected 1 image, none found: %s", name) + } else if img.ID != testOfficialImageID { + t.Errorf("Expected ID '%s' found '%s'", testOfficialImageID, img.ID) + } } - if img, err := store.LookupImage(testImageName + ":" + testImageID); err != nil { - t.Fatal(err) - } else if img == nil { - t.Errorf("Expected 1 image, none found") + for _, name := range privateLookups { + if img, err := store.LookupImage(name); err != nil { + t.Errorf("Error looking up %s: %s", name, err) + } else if img == nil { + t.Errorf("Expected 1 image, none found: %s", name) + } else if img.ID != testPrivateImageID { + t.Errorf("Expected ID '%s' found '%s'", testPrivateImageID, img.ID) + } + } + + for _, name := range invalidLookups { + if img, err := store.LookupImage(name); err == nil { + t.Errorf("Expected error, none found: %s", name) + } else if img != nil { + t.Errorf("Expected 0 image, 1 found: %s", name) + } } } @@ -133,18 +174,3 @@ func TestInvalidTagName(t *testing.T) { } } } - -func TestOfficialName(t *testing.T) { - names := map[string]bool{ - "library/ubuntu": true, - "nonlibrary/ubuntu": false, - "ubuntu": true, - "other/library": false, - } - for name, isOfficial := range names { - result := isOfficialName(name) - if result != isOfficial { - t.Errorf("Unexpected result for %s\n\tExpecting: %v\n\tActual: %v", name, isOfficial, result) - } - } -} diff --git a/integration-cli/docker_cli_build_test.go b/integration-cli/docker_cli_build_test.go index a27aecc56..189b6a7ba 100644 --- a/integration-cli/docker_cli_build_test.go +++ b/integration-cli/docker_cli_build_test.go @@ -4301,3 +4301,24 @@ func TestBuildRenamedDockerfile(t *testing.T) { logDone("build - rename dockerfile") } + +func TestBuildFromOfficialNames(t *testing.T) { + name := "testbuildfromofficial" + fromNames := []string{ + "busybox", + "docker.io/busybox", + "index.docker.io/busybox", + "library/busybox", + "docker.io/library/busybox", + "index.docker.io/library/busybox", + } + for idx, fromName := range fromNames { + imgName := fmt.Sprintf("%s%d", name, idx) + _, err := buildImage(imgName, "FROM "+fromName, true) + if err != nil { + t.Errorf("Build failed using FROM %s: %s", fromName, err) + } + deleteImages(imgName) + } + logDone("build - from official names") +} diff --git a/integration-cli/docker_cli_pull_test.go b/integration-cli/docker_cli_pull_test.go index 5b3324c77..bed015be0 100644 --- a/integration-cli/docker_cli_pull_test.go +++ b/integration-cli/docker_cli_pull_test.go @@ -2,6 +2,7 @@ package main import ( "os/exec" + "strings" "testing" ) @@ -24,3 +25,33 @@ func TestPullNonExistingImage(t *testing.T) { } logDone("pull - pull fooblahblah1234 (non-existing image)") } + +// pulling an image from the central registry using official names should work +// ensure all pulls result in the same image +func TestPullImageOfficialNames(t *testing.T) { + names := []string{ + "docker.io/hello-world", + "index.docker.io/hello-world", + "library/hello-world", + "docker.io/library/hello-world", + "index.docker.io/library/hello-world", + } + for _, name := range names { + pullCmd := exec.Command(dockerBinary, "pull", name) + out, exitCode, err := runCommandWithOutput(pullCmd) + if err != nil || exitCode != 0 { + t.Errorf("pulling the '%s' image from the registry has failed: %s", name, err) + continue + } + + // ensure we don't have multiple image names. + imagesCmd := exec.Command(dockerBinary, "images") + out, _, err = runCommandWithOutput(imagesCmd) + if err != nil { + t.Errorf("listing images failed with errors: %v", err) + } else if strings.Contains(out, name) { + t.Errorf("images should not have listed '%s'", name) + } + } + logDone("pull - pull official names") +} diff --git a/integration-cli/docker_cli_tag_test.go b/integration-cli/docker_cli_tag_test.go index bfab85111..4d5a394e4 100644 --- a/integration-cli/docker_cli_tag_test.go +++ b/integration-cli/docker_cli_tag_test.go @@ -132,3 +132,49 @@ func TestTagExistedNameWithForce(t *testing.T) { logDone("tag - busybox with an existed tag name with -f option work") } + +// ensure tagging using official names works +// ensure all tags result in the same name +func TestTagOfficialNames(t *testing.T) { + names := []string{ + "docker.io/busybox", + "index.docker.io/busybox", + "library/busybox", + "docker.io/library/busybox", + "index.docker.io/library/busybox", + } + + for _, name := range names { + tagCmd := exec.Command(dockerBinary, "tag", "-f", "busybox:latest", name+":latest") + out, exitCode, err := runCommandWithOutput(tagCmd) + if err != nil || exitCode != 0 { + t.Errorf("tag busybox %v should have worked: %s, %s", name, err, out) + continue + } + + // ensure we don't have multiple tag names. + imagesCmd := exec.Command(dockerBinary, "images") + out, _, err = runCommandWithOutput(imagesCmd) + if err != nil { + t.Errorf("listing images failed with errors: %v, %s", err, out) + } else if strings.Contains(out, name) { + t.Errorf("images should not have listed '%s'", name) + deleteImages(name + ":latest") + } else { + logMessage := fmt.Sprintf("tag official name - busybox %v", name) + logDone(logMessage) + } + } + + for _, name := range names { + tagCmd := exec.Command(dockerBinary, "tag", "-f", name+":latest", "fooo/bar:latest") + _, exitCode, err := runCommandWithOutput(tagCmd) + if err != nil || exitCode != 0 { + t.Errorf("tag %v fooo/bar should have worked: %s", name, err) + continue + } + deleteImages("fooo/bar:latest") + logMessage := fmt.Sprintf("tag official name - %v fooo/bar", name) + logDone(logMessage) + } +} diff --git a/integration/utils_test.go b/integration/utils_test.go index 0cb22ee1c..32ca8e0d6 100644 --- a/integration/utils_test.go +++ b/integration/utils_test.go @@ -20,6 +20,7 @@ import ( "github.com/docker/docker/daemon" "github.com/docker/docker/engine" flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/registry" "github.com/docker/docker/runconfig" "github.com/docker/docker/utils" ) @@ -173,7 +174,14 @@ func newTestEngine(t Fataler, autorestart bool, root string) *engine.Engine { eng := engine.New() eng.Logging = false // Load default plugins - builtins.Register(eng) + if err := builtins.Register(eng); err != nil { + t.Fatal(err) + } + // load registry service + if err := registry.NewService(nil).Install(eng); err != nil { + t.Fatal(err) + } + // (This is manually copied and modified from main() until we have a more generic plugin system) cfg := &daemon.Config{ Root: root, diff --git a/opts/opts.go b/opts/opts.go index f15064ac6..3d8c23ff7 100644 --- a/opts/opts.go +++ b/opts/opts.go @@ -3,7 +3,6 @@ package opts import ( "fmt" "net" - "net/url" "os" "path" "regexp" @@ -39,10 +38,6 @@ func IPVar(value *net.IP, names []string, defaultValue, usage string) { flag.Var(NewIpOpt(value, defaultValue), names, usage) } -func MirrorListVar(values *[]string, names []string, usage string) { - flag.Var(newListOptsRef(values, ValidateMirror), names, usage) -} - func LabelListVar(values *[]string, names []string, usage string) { flag.Var(newListOptsRef(values, ValidateLabel), names, usage) } @@ -127,6 +122,7 @@ func (opts *ListOpts) Len() int { // Validators type ValidatorFctType func(val string) (string, error) +type ValidatorFctListType func(val string) ([]string, error) func ValidateAttach(val string) (string, error) { s := strings.ToLower(val) @@ -214,24 +210,6 @@ func ValidateExtraHost(val string) (string, error) { return val, nil } -// Validates an HTTP(S) registry mirror -func ValidateMirror(val string) (string, error) { - uri, err := url.Parse(val) - if err != nil { - return "", fmt.Errorf("%s is not a valid URI", val) - } - - if uri.Scheme != "http" && uri.Scheme != "https" { - return "", fmt.Errorf("Unsupported scheme %s", uri.Scheme) - } - - if uri.Path != "" || uri.RawQuery != "" || uri.Fragment != "" { - return "", fmt.Errorf("Unsupported path/query/fragment at end of the URI") - } - - return fmt.Sprintf("%s://%s/v1/", uri.Scheme, uri.Host), nil -} - func ValidateLabel(val string) (string, error) { if strings.Count(val, "=") != 1 { return "", fmt.Errorf("bad attribute format: %s", val) diff --git a/opts/opts_test.go b/opts/opts_test.go index 09b5aa780..e813c4432 100644 --- a/opts/opts_test.go +++ b/opts/opts_test.go @@ -30,7 +30,23 @@ func TestValidateIPAddress(t *testing.T) { func TestListOpts(t *testing.T) { o := NewListOpts(nil) o.Set("foo") - o.String() + if o.String() != "[foo]" { + t.Errorf("%s != [foo]", o.String()) + } + o.Set("bar") + if o.Len() != 2 { + t.Errorf("%d != 2", o.Len()) + } + if !o.Get("bar") { + t.Error("o.Get(\"bar\") == false") + } + if o.Get("baz") { + t.Error("o.Get(\"baz\") == true") + } + o.Delete("foo") + if o.String() != "[bar]" { + t.Errorf("%s != [bar]", o.String()) + } } func TestValidateDnsSearch(t *testing.T) { diff --git a/registry/auth.go b/registry/auth.go index 427606408..8382869b3 100644 --- a/registry/auth.go +++ b/registry/auth.go @@ -7,7 +7,6 @@ import ( "fmt" "io/ioutil" "net/http" - "net/url" "os" "path" "strings" @@ -22,23 +21,15 @@ const ( // Only used for user auth + account creation INDEXSERVER = "https://index.docker.io/v1/" REGISTRYSERVER = "https://registry-1.docker.io/v1/" + INDEXNAME = "docker.io" // INDEXSERVER = "https://registry-stage.hub.docker.com/v1/" ) var ( ErrConfigFileMissing = errors.New("The Auth config file is missing") - IndexServerURL *url.URL ) -func init() { - url, err := url.Parse(INDEXSERVER) - if err != nil { - panic(err) - } - IndexServerURL = url -} - type AuthConfig struct { Username string `json:"username,omitempty"` Password string `json:"password,omitempty"` @@ -56,6 +47,10 @@ func IndexServerAddress() string { return INDEXSERVER } +func IndexServerName() string { + return INDEXNAME +} + // create a base64 encoded auth string to store in config func encodeAuth(authConfig *AuthConfig) string { authStr := authConfig.Username + ":" + authConfig.Password @@ -118,6 +113,7 @@ func LoadConfig(rootPath string) (*ConfigFile, error) { } authConfig.Email = origEmail[1] authConfig.ServerAddress = IndexServerAddress() + // *TODO: Switch to using IndexServerName() instead? configFile.Configs[IndexServerAddress()] = authConfig } else { for k, authConfig := range configFile.Configs { @@ -181,7 +177,7 @@ func Login(authConfig *AuthConfig, factory *utils.HTTPRequestFactory) (string, e ) if serverAddress == "" { - serverAddress = IndexServerAddress() + return "", fmt.Errorf("Server Error: Server Address not set.") } loginAgainstOfficialIndex := serverAddress == IndexServerAddress() @@ -213,6 +209,7 @@ func Login(authConfig *AuthConfig, factory *utils.HTTPRequestFactory) (string, e status = "Account created. Please use the confirmation link we sent" + " to your e-mail to activate it." } else { + // *TODO: Use registry configuration to determine what this says, if anything? status = "Account created. Please see the documentation of the registry " + serverAddress + " for instructions how to activate it." } } else if reqStatusCode == 400 { @@ -236,6 +233,7 @@ func Login(authConfig *AuthConfig, factory *utils.HTTPRequestFactory) (string, e if loginAgainstOfficialIndex { return "", fmt.Errorf("Login: Account is not Active. Please check your e-mail for a confirmation link.") } + // *TODO: Use registry configuration to determine what this says, if anything? return "", fmt.Errorf("Login: Account is not Active. Please see the documentation of the registry %s for instructions how to activate it.", serverAddress) } return "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body, resp.StatusCode, resp.Header) @@ -271,14 +269,10 @@ func Login(authConfig *AuthConfig, factory *utils.HTTPRequestFactory) (string, e } // this method matches a auth configuration to a server address or a url -func (config *ConfigFile) ResolveAuthConfig(hostname string) AuthConfig { - if hostname == IndexServerAddress() || len(hostname) == 0 { - // default to the index server - return config.Configs[IndexServerAddress()] - } - +func (config *ConfigFile) ResolveAuthConfig(index *IndexInfo) AuthConfig { + configKey := index.GetAuthConfigKey() // First try the happy case - if c, found := config.Configs[hostname]; found { + if c, found := config.Configs[configKey]; found || index.Official { return c } @@ -297,9 +291,8 @@ func (config *ConfigFile) ResolveAuthConfig(hostname string) AuthConfig { // Maybe they have a legacy config file, we will iterate the keys converting // them to the new format and testing - normalizedHostename := convertToHostname(hostname) for registry, config := range config.Configs { - if registryHostname := convertToHostname(registry); registryHostname == normalizedHostename { + if configKey == convertToHostname(registry) { return config } } diff --git a/registry/auth_test.go b/registry/auth_test.go index 3cb1a9ac4..22f879946 100644 --- a/registry/auth_test.go +++ b/registry/auth_test.go @@ -81,12 +81,20 @@ func TestResolveAuthConfigIndexServer(t *testing.T) { } defer os.RemoveAll(configFile.rootPath) - for _, registry := range []string{"", IndexServerAddress()} { - resolved := configFile.ResolveAuthConfig(registry) - if resolved != configFile.Configs[IndexServerAddress()] { - t.Fail() - } + indexConfig := configFile.Configs[IndexServerAddress()] + + officialIndex := &IndexInfo{ + Official: true, } + privateIndex := &IndexInfo{ + Official: false, + } + + resolved := configFile.ResolveAuthConfig(officialIndex) + assertEqual(t, resolved, indexConfig, "Expected ResolveAuthConfig to return IndexServerAddress()") + + resolved = configFile.ResolveAuthConfig(privateIndex) + assertNotEqual(t, resolved, indexConfig, "Expected ResolveAuthConfig to not return IndexServerAddress()") } func TestResolveAuthConfigFullURL(t *testing.T) { @@ -106,18 +114,27 @@ func TestResolveAuthConfigFullURL(t *testing.T) { Password: "bar-pass", Email: "bar@example.com", } - configFile.Configs["https://registry.example.com/v1/"] = registryAuth - configFile.Configs["http://localhost:8000/v1/"] = localAuth - configFile.Configs["registry.com"] = registryAuth + officialAuth := AuthConfig{ + Username: "baz-user", + Password: "baz-pass", + Email: "baz@example.com", + } + configFile.Configs[IndexServerAddress()] = officialAuth + + expectedAuths := map[string]AuthConfig{ + "registry.example.com": registryAuth, + "localhost:8000": localAuth, + "registry.com": localAuth, + } validRegistries := map[string][]string{ - "https://registry.example.com/v1/": { + "registry.example.com": { "https://registry.example.com/v1/", "http://registry.example.com/v1/", "registry.example.com", "registry.example.com/v1/", }, - "http://localhost:8000/v1/": { + "localhost:8000": { "https://localhost:8000/v1/", "http://localhost:8000/v1/", "localhost:8000", @@ -132,18 +149,24 @@ func TestResolveAuthConfigFullURL(t *testing.T) { } for configKey, registries := range validRegistries { + configured, ok := expectedAuths[configKey] + if !ok || configured.Email == "" { + t.Fatal() + } + index := &IndexInfo{ + Name: configKey, + } for _, registry := range registries { - var ( - configured AuthConfig - ok bool - ) - resolved := configFile.ResolveAuthConfig(registry) - if configured, ok = configFile.Configs[configKey]; !ok { - t.Fail() - } + configFile.Configs[registry] = configured + resolved := configFile.ResolveAuthConfig(index) if resolved.Email != configured.Email { t.Errorf("%s -> %q != %q\n", registry, resolved.Email, configured.Email) } + delete(configFile.Configs, registry) + resolved = configFile.ResolveAuthConfig(index) + if resolved.Email == configured.Email { + t.Errorf("%s -> %q == %q\n", registry, resolved.Email, configured.Email) + } } } } diff --git a/registry/config.go b/registry/config.go new file mode 100644 index 000000000..bd993edd5 --- /dev/null +++ b/registry/config.go @@ -0,0 +1,126 @@ +package registry + +import ( + "encoding/json" + "fmt" + "net" + "net/url" + + "github.com/docker/docker/opts" + flag "github.com/docker/docker/pkg/mflag" +) + +// Options holds command line options. +type Options struct { + Mirrors opts.ListOpts + InsecureRegistries opts.ListOpts +} + +// InstallFlags adds command-line options to the top-level flag parser for +// the current process. +func (options *Options) InstallFlags() { + options.Mirrors = opts.NewListOpts(ValidateMirror) + flag.Var(&options.Mirrors, []string{"-registry-mirror"}, "Specify a preferred Docker registry mirror") + options.InsecureRegistries = opts.NewListOpts(ValidateIndexName) + flag.Var(&options.InsecureRegistries, []string{"-insecure-registry"}, "Enable insecure communication with specified registries (no certificate verification for HTTPS and enable HTTP fallback) (e.g., localhost:5000 or 10.20.0.0/16)") +} + +// ValidateMirror validates an HTTP(S) registry mirror +func ValidateMirror(val string) (string, error) { + uri, err := url.Parse(val) + if err != nil { + return "", fmt.Errorf("%s is not a valid URI", val) + } + + if uri.Scheme != "http" && uri.Scheme != "https" { + return "", fmt.Errorf("Unsupported scheme %s", uri.Scheme) + } + + if uri.Path != "" || uri.RawQuery != "" || uri.Fragment != "" { + return "", fmt.Errorf("Unsupported path/query/fragment at end of the URI") + } + + return fmt.Sprintf("%s://%s/v1/", uri.Scheme, uri.Host), nil +} + +// ValidateIndexName validates an index name. +func ValidateIndexName(val string) (string, error) { + // 'index.docker.io' => 'docker.io' + if val == "index."+IndexServerName() { + val = IndexServerName() + } + // *TODO: Check if valid hostname[:port]/ip[:port]? + return val, nil +} + +type netIPNet net.IPNet + +func (ipnet *netIPNet) MarshalJSON() ([]byte, error) { + return json.Marshal((*net.IPNet)(ipnet).String()) +} + +func (ipnet *netIPNet) UnmarshalJSON(b []byte) (err error) { + var ipnet_str string + if err = json.Unmarshal(b, &ipnet_str); err == nil { + var cidr *net.IPNet + if _, cidr, err = net.ParseCIDR(ipnet_str); err == nil { + *ipnet = netIPNet(*cidr) + } + } + return +} + +// ServiceConfig stores daemon registry services configuration. +type ServiceConfig struct { + InsecureRegistryCIDRs []*netIPNet `json:"InsecureRegistryCIDRs"` + IndexConfigs map[string]*IndexInfo `json:"IndexConfigs"` +} + +// NewServiceConfig returns a new instance of ServiceConfig +func NewServiceConfig(options *Options) *ServiceConfig { + if options == nil { + options = &Options{ + Mirrors: opts.NewListOpts(nil), + InsecureRegistries: opts.NewListOpts(nil), + } + } + + // Localhost is by default considered as an insecure registry + // This is a stop-gap for people who are running a private registry on localhost (especially on Boot2docker). + // + // TODO: should we deprecate this once it is easier for people to set up a TLS registry or change + // daemon flags on boot2docker? + options.InsecureRegistries.Set("127.0.0.0/8") + + config := &ServiceConfig{ + InsecureRegistryCIDRs: make([]*netIPNet, 0), + IndexConfigs: make(map[string]*IndexInfo, 0), + } + // Split --insecure-registry into CIDR and registry-specific settings. + for _, r := range options.InsecureRegistries.GetAll() { + // Check if CIDR was passed to --insecure-registry + _, ipnet, err := net.ParseCIDR(r) + if err == nil { + // Valid CIDR. + config.InsecureRegistryCIDRs = append(config.InsecureRegistryCIDRs, (*netIPNet)(ipnet)) + } else { + // Assume `host:port` if not CIDR. + config.IndexConfigs[r] = &IndexInfo{ + Name: r, + Mirrors: make([]string, 0), + Secure: false, + Official: false, + } + } + } + + // Configure public registry. + config.IndexConfigs[IndexServerName()] = &IndexInfo{ + Name: IndexServerName(), + Mirrors: options.Mirrors.GetAll(), + Secure: true, + Official: true, + } + + return config +} diff --git a/registry/config_test.go b/registry/config_test.go new file mode 100644 index 000000000..25578a7f2 --- /dev/null +++ b/registry/config_test.go @@ -0,0 +1,49 @@ +package registry + +import ( + "testing" +) + +func TestValidateMirror(t *testing.T) { + valid := []string{ + "http://mirror-1.com", + "https://mirror-1.com", + "http://localhost", + "https://localhost", + "http://localhost:5000", + "https://localhost:5000", + "http://127.0.0.1", + "https://127.0.0.1", + "http://127.0.0.1:5000", + "https://127.0.0.1:5000", + } + + invalid := []string{ + "!invalid!://%as%", + "ftp://mirror-1.com", + "http://mirror-1.com/", + "http://mirror-1.com/?q=foo", + "http://mirror-1.com/v1/", + "http://mirror-1.com/v1/?q=foo", + "http://mirror-1.com/v1/?q=foo#frag", + "http://mirror-1.com?q=foo", + "https://mirror-1.com#frag", + "https://mirror-1.com/", + "https://mirror-1.com/#frag", + "https://mirror-1.com/v1/", + "https://mirror-1.com/v1/#", + "https://mirror-1.com?q", + } + + for _, address := range valid { + if ret, err := ValidateMirror(address); err != nil || ret == "" { + t.Errorf("ValidateMirror(`"+address+"`) got %s %s", ret, err) + } + } + + for _, address := range invalid { + if ret, err := ValidateMirror(address); err == nil || ret != "" { + t.Errorf("ValidateMirror(`"+address+"`) got %s %s", ret, err) + } + } +} diff --git a/registry/endpoint.go b/registry/endpoint.go index 019bccfc6..86f53744d 100644 --- a/registry/endpoint.go +++ b/registry/endpoint.go @@ -37,8 +37,9 @@ func scanForAPIVersion(hostname string) (string, APIVersion) { return hostname, DefaultAPIVersion } -func NewEndpoint(hostname string, insecureRegistries []string) (*Endpoint, error) { - endpoint, err := newEndpoint(hostname, insecureRegistries) +func NewEndpoint(index *IndexInfo) (*Endpoint, error) { + // *TODO: Allow per-registry configuration of endpoints. + endpoint, err := newEndpoint(index.GetAuthConfigKey(), index.Secure) if err != nil { return nil, err } @@ -49,7 +50,7 @@ func NewEndpoint(hostname string, insecureRegistries []string) (*Endpoint, error //TODO: triggering highland build can be done there without "failing" - if endpoint.secure { + if index.Secure { // If registry is secure and HTTPS failed, show user the error and tell them about `--insecure-registry` // in case that's what they need. DO NOT accept unknown CA certificates, and DO NOT fallback to HTTP. return nil, fmt.Errorf("Invalid registry endpoint %s: %v. If this private registry supports only HTTP or HTTPS with an unknown CA certificate, please add `--insecure-registry %s` to the daemon's arguments. In the case of HTTPS, if you have access to the registry's CA certificate, no need for the flag; simply place the CA certificate at /etc/docker/certs.d/%s/ca.crt", endpoint, err, endpoint.URL.Host, endpoint.URL.Host) @@ -68,7 +69,7 @@ func NewEndpoint(hostname string, insecureRegistries []string) (*Endpoint, error return endpoint, nil } -func newEndpoint(hostname string, insecureRegistries []string) (*Endpoint, error) { +func newEndpoint(hostname string, secure bool) (*Endpoint, error) { var ( endpoint = Endpoint{} trimmedHostname string @@ -82,13 +83,14 @@ func newEndpoint(hostname string, insecureRegistries []string) (*Endpoint, error if err != nil { return nil, err } - endpoint.secure, err = isSecure(endpoint.URL.Host, insecureRegistries) - if err != nil { - return nil, err - } + endpoint.secure = secure return &endpoint, nil } +func (repoInfo *RepositoryInfo) GetEndpoint() (*Endpoint, error) { + return NewEndpoint(repoInfo.Index) +} + type Endpoint struct { URL *url.URL Version APIVersion @@ -156,27 +158,30 @@ func (e Endpoint) Ping() (RegistryInfo, error) { return info, nil } -// isSecure returns false if the provided hostname is part of the list of insecure registries. +// isSecureIndex returns false if the provided indexName is part of the list of insecure registries // Insecure registries accept HTTP and/or accept HTTPS with certificates from unknown CAs. // // The list of insecure registries can contain an element with CIDR notation to specify a whole subnet. -// If the subnet contains one of the IPs of the registry specified by hostname, the latter is considered +// If the subnet contains one of the IPs of the registry specified by indexName, the latter is considered // insecure. // -// hostname should be a URL.Host (`host:port` or `host`) where the `host` part can be either a domain name +// indexName should be a URL.Host (`host:port` or `host`) where the `host` part can be either a domain name // or an IP address. If it is a domain name, then it will be resolved in order to check if the IP is contained -// in a subnet. If the resolving is not successful, isSecure will only try to match hostname to any element +// in a subnet. If the resolving is not successful, isSecureIndex will only try to match hostname to any element // of insecureRegistries. -func isSecure(hostname string, insecureRegistries []string) (bool, error) { - if hostname == IndexServerURL.Host { - return true, nil +func (config *ServiceConfig) isSecureIndex(indexName string) bool { + // Check for configured index, first. This is needed in case isSecureIndex + // is called from anything besides NewIndexInfo, in order to honor per-index configurations. + if index, ok := config.IndexConfigs[indexName]; ok { + return index.Secure } - host, _, err := net.SplitHostPort(hostname) + host, _, err := net.SplitHostPort(indexName) if err != nil { - // assume hostname is of the form `host` without the port and go on. - host = hostname + // assume indexName is of the form `host` without the port and go on. + host = indexName } + addrs, err := lookupIP(host) if err != nil { ip := net.ParseIP(host) @@ -189,29 +194,15 @@ func isSecure(hostname string, insecureRegistries []string) (bool, error) { // So, len(addrs) == 0 and we're not aborting. } - for _, r := range insecureRegistries { - if hostname == r { - // hostname matches insecure registry - return false, nil - } - - // Try CIDR notation only if addrs has any elements, i.e. if `host`'s IP could be determined. - for _, addr := range addrs { - - // now assume a CIDR was passed to --insecure-registry - _, ipnet, err := net.ParseCIDR(r) - if err != nil { - // if we could not parse it as a CIDR, even after removing - // assume it's not a CIDR and go on with the next candidate - break - } - + // Try CIDR notation only if addrs has any elements, i.e. if `host`'s IP could be determined. + for _, addr := range addrs { + for _, ipnet := range config.InsecureRegistryCIDRs { // check if the addr falls in the subnet - if ipnet.Contains(addr) { - return false, nil + if (*net.IPNet)(ipnet).Contains(addr) { + return false } } } - return true, nil + return true } diff --git a/registry/endpoint_test.go b/registry/endpoint_test.go index 54105ec17..b691a4fb9 100644 --- a/registry/endpoint_test.go +++ b/registry/endpoint_test.go @@ -12,7 +12,7 @@ func TestEndpointParse(t *testing.T) { {"0.0.0.0:5000", "https://0.0.0.0:5000/v1/"}, } for _, td := range testData { - e, err := newEndpoint(td.str, insecureRegistries) + e, err := newEndpoint(td.str, false) if err != nil { t.Errorf("%q: %s", td.str, err) } diff --git a/registry/registry.go b/registry/registry.go index a12291897..de724ee20 100644 --- a/registry/registry.go +++ b/registry/registry.go @@ -25,6 +25,7 @@ var ( errLoginRequired = errors.New("Authentication is required.") validNamespaceChars = regexp.MustCompile(`^([a-z0-9-_]*)$`) validRepo = regexp.MustCompile(`^([a-z0-9-_.]+)$`) + emptyServiceConfig = NewServiceConfig(nil) ) type TimeoutType uint32 @@ -160,12 +161,12 @@ func doRequest(req *http.Request, jar http.CookieJar, timeout TimeoutType, secur return res, client, err } -func validateRepositoryName(repositoryName string) error { +func validateRemoteName(remoteName string) error { var ( namespace string name string ) - nameParts := strings.SplitN(repositoryName, "/", 2) + nameParts := strings.SplitN(remoteName, "/", 2) if len(nameParts) < 2 { namespace = "library" name = nameParts[0] @@ -196,29 +197,147 @@ func validateRepositoryName(repositoryName string) error { return nil } -// Resolves a repository name to a hostname + name -func ResolveRepositoryName(reposName string) (string, string, error) { - if strings.Contains(reposName, "://") { - // It cannot contain a scheme! - return "", "", ErrInvalidRepositoryName - } - nameParts := strings.SplitN(reposName, "/", 2) - if len(nameParts) == 1 || (!strings.Contains(nameParts[0], ".") && !strings.Contains(nameParts[0], ":") && - nameParts[0] != "localhost") { - // This is a Docker Index repos (ex: samalba/hipache or ubuntu) - err := validateRepositoryName(reposName) - return IndexServerAddress(), reposName, err - } - hostname := nameParts[0] - reposName = nameParts[1] - if strings.Contains(hostname, "index.docker.io") { - return "", "", fmt.Errorf("Invalid repository name, try \"%s\" instead", reposName) - } - if err := validateRepositoryName(reposName); err != nil { - return "", "", err +// NewIndexInfo returns IndexInfo configuration from indexName +func NewIndexInfo(config *ServiceConfig, indexName string) (*IndexInfo, error) { + var err error + indexName, err = ValidateIndexName(indexName) + if err != nil { + return nil, err } - return hostname, reposName, nil + // Return any configured index info, first. + if index, ok := config.IndexConfigs[indexName]; ok { + return index, nil + } + + // Construct a non-configured index info. + index := &IndexInfo{ + Name: indexName, + Mirrors: make([]string, 0), + Official: false, + } + index.Secure = config.isSecureIndex(indexName) + return index, nil +} + +func validateNoSchema(reposName string) error { + if strings.Contains(reposName, "://") { + // It cannot contain a scheme! + return ErrInvalidRepositoryName + } + return nil +} + +// splitReposName breaks a reposName into an index name and remote name +func splitReposName(reposName string) (string, string) { + nameParts := strings.SplitN(reposName, "/", 2) + var indexName, remoteName string + if len(nameParts) == 1 || (!strings.Contains(nameParts[0], ".") && + !strings.Contains(nameParts[0], ":") && nameParts[0] != "localhost") { + // This is a Docker Index repos (ex: samalba/hipache or ubuntu) + // 'docker.io' + indexName = IndexServerName() + remoteName = reposName + } else { + indexName = nameParts[0] + remoteName = nameParts[1] + } + return indexName, remoteName +} + +// NewRepositoryInfo validates and breaks down a repository name into a RepositoryInfo +func NewRepositoryInfo(config *ServiceConfig, reposName string) (*RepositoryInfo, error) { + if err := validateNoSchema(reposName); err != nil { + return nil, err + } + + indexName, remoteName := splitReposName(reposName) + if err := validateRemoteName(remoteName); err != nil { + return nil, err + } + + repoInfo := &RepositoryInfo{ + RemoteName: remoteName, + } + + var err error + repoInfo.Index, err = NewIndexInfo(config, indexName) + if err != nil { + return nil, err + } + + if repoInfo.Index.Official { + normalizedName := repoInfo.RemoteName + if strings.HasPrefix(normalizedName, "library/") { + // If pull "library/foo", it's stored locally under "foo" + normalizedName = strings.SplitN(normalizedName, "/", 2)[1] + } + + repoInfo.LocalName = normalizedName + repoInfo.RemoteName = normalizedName + // If the normalized name does not contain a '/' (e.g. "foo") + // then it is an official repo. + if strings.IndexRune(normalizedName, '/') == -1 { + repoInfo.Official = true + // Fix up remote name for official repos. + repoInfo.RemoteName = "library/" + normalizedName + } + + // *TODO: Prefix this with 'docker.io/'. + repoInfo.CanonicalName = repoInfo.LocalName + } else { + // *TODO: Decouple index name from hostname (via registry configuration?) + repoInfo.LocalName = repoInfo.Index.Name + "/" + repoInfo.RemoteName + repoInfo.CanonicalName = repoInfo.LocalName + } + return repoInfo, nil +} + +// ValidateRepositoryName validates a repository name +func ValidateRepositoryName(reposName string) error { + var err error + if err = validateNoSchema(reposName); err != nil { + return err + } + indexName, remoteName := splitReposName(reposName) + if _, err = ValidateIndexName(indexName); err != nil { + return err + } + return validateRemoteName(remoteName) +} + +// ParseRepositoryInfo performs the breakdown of a repository name into a RepositoryInfo, but +// lacks registry configuration. +func ParseRepositoryInfo(reposName string) (*RepositoryInfo, error) { + return NewRepositoryInfo(emptyServiceConfig, reposName) +} + +// NormalizeLocalName transforms a repository name into a normalize LocalName +// Passes through the name without transformation on error (image id, etc) +func NormalizeLocalName(name string) string { + repoInfo, err := ParseRepositoryInfo(name) + if err != nil { + return name + } + return repoInfo.LocalName +} + +// GetAuthConfigKey special-cases using the full index address of the official +// index as the AuthConfig key, and uses the (host)name[:port] for private indexes. +func (index *IndexInfo) GetAuthConfigKey() string { + if index.Official { + return IndexServerAddress() + } + return index.Name +} + +// GetSearchTerm special-cases using local name for official index, and +// remote name for private indexes. +func (repoInfo *RepositoryInfo) GetSearchTerm() string { + if repoInfo.Index.Official { + return repoInfo.LocalName + } + return repoInfo.RemoteName } func trustedLocation(req *http.Request) bool { diff --git a/registry/registry_mock_test.go b/registry/registry_mock_test.go index 887d2ef6f..57233d7c7 100644 --- a/registry/registry_mock_test.go +++ b/registry/registry_mock_test.go @@ -15,15 +15,16 @@ import ( "testing" "time" + "github.com/docker/docker/opts" "github.com/gorilla/mux" log "github.com/Sirupsen/logrus" ) var ( - testHTTPServer *httptest.Server - insecureRegistries []string - testLayers = map[string]map[string]string{ + testHTTPServer *httptest.Server + testHTTPSServer *httptest.Server + testLayers = map[string]map[string]string{ "77dbf71da1d00e3fbddc480176eac8994025630c6590d11cfc8fe1209c2a1d20": { "json": `{"id":"77dbf71da1d00e3fbddc480176eac8994025630c6590d11cfc8fe1209c2a1d20", "comment":"test base image","created":"2013-03-23T12:53:11.10432-07:00", @@ -86,6 +87,7 @@ var ( "": {net.ParseIP("0.0.0.0")}, "localhost": {net.ParseIP("127.0.0.1"), net.ParseIP("::1")}, "example.com": {net.ParseIP("42.42.42.42")}, + "other.com": {net.ParseIP("43.43.43.43")}, } ) @@ -108,11 +110,7 @@ func init() { r.HandleFunc("/v2/version", handlerGetPing).Methods("GET") testHTTPServer = httptest.NewServer(handlerAccessLog(r)) - URL, err := url.Parse(testHTTPServer.URL) - if err != nil { - panic(err) - } - insecureRegistries = []string{URL.Host} + testHTTPSServer = httptest.NewTLSServer(handlerAccessLog(r)) // override net.LookupIP lookupIP = func(host string) ([]net.IP, error) { @@ -146,6 +144,52 @@ func makeURL(req string) string { return testHTTPServer.URL + req } +func makeHttpsURL(req string) string { + return testHTTPSServer.URL + req +} + +func makeIndex(req string) *IndexInfo { + index := &IndexInfo{ + Name: makeURL(req), + } + return index +} + +func makeHttpsIndex(req string) *IndexInfo { + index := &IndexInfo{ + Name: makeHttpsURL(req), + } + return index +} + +func makePublicIndex() *IndexInfo { + index := &IndexInfo{ + Name: IndexServerAddress(), + Secure: true, + Official: true, + } + return index +} + +func makeServiceConfig(mirrors []string, insecure_registries []string) *ServiceConfig { + options := &Options{ + Mirrors: opts.NewListOpts(nil), + InsecureRegistries: opts.NewListOpts(nil), + } + if mirrors != nil { + for _, mirror := range mirrors { + options.Mirrors.Set(mirror) + } + } + if insecure_registries != nil { + for _, insecure_registries := range insecure_registries { + options.InsecureRegistries.Set(insecure_registries) + } + } + + return NewServiceConfig(options) +} + func writeHeaders(w http.ResponseWriter) { h := w.Header() h.Add("Server", "docker-tests/mock") @@ -193,6 +237,40 @@ func assertEqual(t *testing.T, a interface{}, b interface{}, message string) { t.Fatal(message) } +func assertNotEqual(t *testing.T, a interface{}, b interface{}, message string) { + if a != b { + return + } + if len(message) == 0 { + message = fmt.Sprintf("%v == %v", a, b) + } + t.Fatal(message) +} + +// Similar to assertEqual, but does not stop test +func checkEqual(t *testing.T, a interface{}, b interface{}, messagePrefix string) { + if a == b { + return + } + message := fmt.Sprintf("%v != %v", a, b) + if len(messagePrefix) != 0 { + message = messagePrefix + ": " + message + } + t.Error(message) +} + +// Similar to assertNotEqual, but does not stop test +func checkNotEqual(t *testing.T, a interface{}, b interface{}, messagePrefix string) { + if a != b { + return + } + message := fmt.Sprintf("%v == %v", a, b) + if len(messagePrefix) != 0 { + message = messagePrefix + ": " + message + } + t.Error(message) +} + func requiresAuth(w http.ResponseWriter, r *http.Request) bool { writeCookie := func() { value := fmt.Sprintf("FAKE-SESSION-%d", time.Now().UnixNano()) @@ -271,6 +349,7 @@ func handlerGetDeleteTags(w http.ResponseWriter, r *http.Request) { return } repositoryName := mux.Vars(r)["repository"] + repositoryName = NormalizeLocalName(repositoryName) tags, exists := testRepositories[repositoryName] if !exists { apiError(w, "Repository not found", 404) @@ -290,6 +369,7 @@ func handlerGetTag(w http.ResponseWriter, r *http.Request) { } vars := mux.Vars(r) repositoryName := vars["repository"] + repositoryName = NormalizeLocalName(repositoryName) tagName := vars["tag"] tags, exists := testRepositories[repositoryName] if !exists { @@ -310,6 +390,7 @@ func handlerPutTag(w http.ResponseWriter, r *http.Request) { } vars := mux.Vars(r) repositoryName := vars["repository"] + repositoryName = NormalizeLocalName(repositoryName) tagName := vars["tag"] tags, exists := testRepositories[repositoryName] if !exists { diff --git a/registry/registry_test.go b/registry/registry_test.go index c1bb97d65..511d7eb17 100644 --- a/registry/registry_test.go +++ b/registry/registry_test.go @@ -21,7 +21,7 @@ const ( func spawnTestRegistrySession(t *testing.T) *Session { authConfig := &AuthConfig{} - endpoint, err := NewEndpoint(makeURL("/v1/"), insecureRegistries) + endpoint, err := NewEndpoint(makeIndex("/v1/")) if err != nil { t.Fatal(err) } @@ -32,16 +32,139 @@ func spawnTestRegistrySession(t *testing.T) *Session { return r } +func TestPublicSession(t *testing.T) { + authConfig := &AuthConfig{} + + getSessionDecorators := func(index *IndexInfo) int { + endpoint, err := NewEndpoint(index) + if err != nil { + t.Fatal(err) + } + r, err := NewSession(authConfig, utils.NewHTTPRequestFactory(), endpoint, true) + if err != nil { + t.Fatal(err) + } + return len(r.reqFactory.GetDecorators()) + } + + decorators := getSessionDecorators(makeIndex("/v1/")) + assertEqual(t, decorators, 0, "Expected no decorator on http session") + + decorators = getSessionDecorators(makeHttpsIndex("/v1/")) + assertNotEqual(t, decorators, 0, "Expected decorator on https session") + + decorators = getSessionDecorators(makePublicIndex()) + assertEqual(t, decorators, 0, "Expected no decorator on public session") +} + func TestPingRegistryEndpoint(t *testing.T) { - ep, err := NewEndpoint(makeURL("/v1/"), insecureRegistries) - if err != nil { - t.Fatal(err) + testPing := func(index *IndexInfo, expectedStandalone bool, assertMessage string) { + ep, err := NewEndpoint(index) + if err != nil { + t.Fatal(err) + } + regInfo, err := ep.Ping() + if err != nil { + t.Fatal(err) + } + + assertEqual(t, regInfo.Standalone, expectedStandalone, assertMessage) } - regInfo, err := ep.Ping() - if err != nil { - t.Fatal(err) + + testPing(makeIndex("/v1/"), true, "Expected standalone to be true (default)") + testPing(makeHttpsIndex("/v1/"), true, "Expected standalone to be true (default)") + testPing(makePublicIndex(), false, "Expected standalone to be false for public index") +} + +func TestEndpoint(t *testing.T) { + // Simple wrapper to fail test if err != nil + expandEndpoint := func(index *IndexInfo) *Endpoint { + endpoint, err := NewEndpoint(index) + if err != nil { + t.Fatal(err) + } + return endpoint + } + + assertInsecureIndex := func(index *IndexInfo) { + index.Secure = true + _, err := NewEndpoint(index) + assertNotEqual(t, err, nil, index.Name+": Expected error for insecure index") + assertEqual(t, strings.Contains(err.Error(), "insecure-registry"), true, index.Name+": Expected insecure-registry error for insecure index") + index.Secure = false + } + + assertSecureIndex := func(index *IndexInfo) { + index.Secure = true + _, err := NewEndpoint(index) + assertNotEqual(t, err, nil, index.Name+": Expected cert error for secure index") + assertEqual(t, strings.Contains(err.Error(), "certificate signed by unknown authority"), true, index.Name+": Expected cert error for secure index") + index.Secure = false + } + + index := &IndexInfo{} + index.Name = makeURL("/v1/") + endpoint := expandEndpoint(index) + assertEqual(t, endpoint.String(), index.Name, "Expected endpoint to be "+index.Name) + if endpoint.Version != APIVersion1 { + t.Fatal("Expected endpoint to be v1") + } + assertInsecureIndex(index) + + index.Name = makeURL("") + endpoint = expandEndpoint(index) + assertEqual(t, endpoint.String(), index.Name+"/v1/", index.Name+": Expected endpoint to be "+index.Name+"/v1/") + if endpoint.Version != APIVersion1 { + t.Fatal("Expected endpoint to be v1") + } + assertInsecureIndex(index) + + httpURL := makeURL("") + index.Name = strings.SplitN(httpURL, "://", 2)[1] + endpoint = expandEndpoint(index) + assertEqual(t, endpoint.String(), httpURL+"/v1/", index.Name+": Expected endpoint to be "+httpURL+"/v1/") + if endpoint.Version != APIVersion1 { + t.Fatal("Expected endpoint to be v1") + } + assertInsecureIndex(index) + + index.Name = makeHttpsURL("/v1/") + endpoint = expandEndpoint(index) + assertEqual(t, endpoint.String(), index.Name, "Expected endpoint to be "+index.Name) + if endpoint.Version != APIVersion1 { + t.Fatal("Expected endpoint to be v1") + } + assertSecureIndex(index) + + index.Name = makeHttpsURL("") + endpoint = expandEndpoint(index) + assertEqual(t, endpoint.String(), index.Name+"/v1/", index.Name+": Expected endpoint to be "+index.Name+"/v1/") + if endpoint.Version != APIVersion1 { + t.Fatal("Expected endpoint to be v1") + } + assertSecureIndex(index) + + httpsURL := makeHttpsURL("") + index.Name = strings.SplitN(httpsURL, "://", 2)[1] + endpoint = expandEndpoint(index) + assertEqual(t, endpoint.String(), httpsURL+"/v1/", index.Name+": Expected endpoint to be "+httpsURL+"/v1/") + if endpoint.Version != APIVersion1 { + t.Fatal("Expected endpoint to be v1") + } + assertSecureIndex(index) + + badEndpoints := []string{ + "http://127.0.0.1/v1/", + "https://127.0.0.1/v1/", + "http://127.0.0.1", + "https://127.0.0.1", + "127.0.0.1", + } + for _, address := range badEndpoints { + index.Name = address + _, err := NewEndpoint(index) + checkNotEqual(t, err, nil, "Expected error while expanding bad endpoint") } - assertEqual(t, regInfo.Standalone, true, "Expected standalone to be true (default)") } func TestGetRemoteHistory(t *testing.T) { @@ -156,30 +279,413 @@ func TestPushImageLayerRegistry(t *testing.T) { } } -func TestResolveRepositoryName(t *testing.T) { - _, _, err := ResolveRepositoryName("https://github.com/docker/docker") - assertEqual(t, err, ErrInvalidRepositoryName, "Expected error invalid repo name") - ep, repo, err := ResolveRepositoryName("fooo/bar") - if err != nil { - t.Fatal(err) +func TestValidateRepositoryName(t *testing.T) { + validRepoNames := []string{ + "docker/docker", + "library/debian", + "debian", + "docker.io/docker/docker", + "docker.io/library/debian", + "docker.io/debian", + "index.docker.io/docker/docker", + "index.docker.io/library/debian", + "index.docker.io/debian", + "127.0.0.1:5000/docker/docker", + "127.0.0.1:5000/library/debian", + "127.0.0.1:5000/debian", + "thisisthesongthatneverendsitgoesonandonandonthisisthesongthatnev", + } + invalidRepoNames := []string{ + "https://github.com/docker/docker", + "docker/Docker", + "docker///docker", + "docker.io/docker/Docker", + "docker.io/docker///docker", + "1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a", + "docker.io/1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a", } - assertEqual(t, ep, IndexServerAddress(), "Expected endpoint to be index server address") - assertEqual(t, repo, "fooo/bar", "Expected resolved repo to be foo/bar") - u := makeURL("")[7:] - ep, repo, err = ResolveRepositoryName(u + "/private/moonbase") - if err != nil { - t.Fatal(err) + for _, name := range invalidRepoNames { + err := ValidateRepositoryName(name) + assertNotEqual(t, err, nil, "Expected invalid repo name: "+name) } - assertEqual(t, ep, u, "Expected endpoint to be "+u) - assertEqual(t, repo, "private/moonbase", "Expected endpoint to be private/moonbase") - ep, repo, err = ResolveRepositoryName("ubuntu-12.04-base") - if err != nil { - t.Fatal(err) + for _, name := range validRepoNames { + err := ValidateRepositoryName(name) + assertEqual(t, err, nil, "Expected valid repo name: "+name) } - assertEqual(t, ep, IndexServerAddress(), "Expected endpoint to be "+IndexServerAddress()) - assertEqual(t, repo, "ubuntu-12.04-base", "Expected endpoint to be ubuntu-12.04-base") + + err := ValidateRepositoryName(invalidRepoNames[0]) + assertEqual(t, err, ErrInvalidRepositoryName, "Expected ErrInvalidRepositoryName: "+invalidRepoNames[0]) +} + +func TestParseRepositoryInfo(t *testing.T) { + expectedRepoInfos := map[string]RepositoryInfo{ + "fooo/bar": { + Index: &IndexInfo{ + Name: IndexServerName(), + Official: true, + }, + RemoteName: "fooo/bar", + LocalName: "fooo/bar", + CanonicalName: "fooo/bar", + Official: false, + }, + "library/ubuntu": { + Index: &IndexInfo{ + Name: IndexServerName(), + Official: true, + }, + RemoteName: "library/ubuntu", + LocalName: "ubuntu", + CanonicalName: "ubuntu", + Official: true, + }, + "nonlibrary/ubuntu": { + Index: &IndexInfo{ + Name: IndexServerName(), + Official: true, + }, + RemoteName: "nonlibrary/ubuntu", + LocalName: "nonlibrary/ubuntu", + CanonicalName: "nonlibrary/ubuntu", + Official: false, + }, + "ubuntu": { + Index: &IndexInfo{ + Name: IndexServerName(), + Official: true, + }, + RemoteName: "library/ubuntu", + LocalName: "ubuntu", + CanonicalName: "ubuntu", + Official: true, + }, + "other/library": { + Index: &IndexInfo{ + Name: IndexServerName(), + Official: true, + }, + RemoteName: "other/library", + LocalName: "other/library", + CanonicalName: "other/library", + Official: false, + }, + "127.0.0.1:8000/private/moonbase": { + Index: &IndexInfo{ + Name: "127.0.0.1:8000", + Official: false, + }, + RemoteName: "private/moonbase", + LocalName: "127.0.0.1:8000/private/moonbase", + CanonicalName: "127.0.0.1:8000/private/moonbase", + Official: false, + }, + "127.0.0.1:8000/privatebase": { + Index: &IndexInfo{ + Name: "127.0.0.1:8000", + Official: false, + }, + RemoteName: "privatebase", + LocalName: "127.0.0.1:8000/privatebase", + CanonicalName: "127.0.0.1:8000/privatebase", + Official: false, + }, + "localhost:8000/private/moonbase": { + Index: &IndexInfo{ + Name: "localhost:8000", + Official: false, + }, + RemoteName: "private/moonbase", + LocalName: "localhost:8000/private/moonbase", + CanonicalName: "localhost:8000/private/moonbase", + Official: false, + }, + "localhost:8000/privatebase": { + Index: &IndexInfo{ + Name: "localhost:8000", + Official: false, + }, + RemoteName: "privatebase", + LocalName: "localhost:8000/privatebase", + CanonicalName: "localhost:8000/privatebase", + Official: false, + }, + "example.com/private/moonbase": { + Index: &IndexInfo{ + Name: "example.com", + Official: false, + }, + RemoteName: "private/moonbase", + LocalName: "example.com/private/moonbase", + CanonicalName: "example.com/private/moonbase", + Official: false, + }, + "example.com/privatebase": { + Index: &IndexInfo{ + Name: "example.com", + Official: false, + }, + RemoteName: "privatebase", + LocalName: "example.com/privatebase", + CanonicalName: "example.com/privatebase", + Official: false, + }, + "example.com:8000/private/moonbase": { + Index: &IndexInfo{ + Name: "example.com:8000", + Official: false, + }, + RemoteName: "private/moonbase", + LocalName: "example.com:8000/private/moonbase", + CanonicalName: "example.com:8000/private/moonbase", + Official: false, + }, + "example.com:8000/privatebase": { + Index: &IndexInfo{ + Name: "example.com:8000", + Official: false, + }, + RemoteName: "privatebase", + LocalName: "example.com:8000/privatebase", + CanonicalName: "example.com:8000/privatebase", + Official: false, + }, + "localhost/private/moonbase": { + Index: &IndexInfo{ + Name: "localhost", + Official: false, + }, + RemoteName: "private/moonbase", + LocalName: "localhost/private/moonbase", + CanonicalName: "localhost/private/moonbase", + Official: false, + }, + "localhost/privatebase": { + Index: &IndexInfo{ + Name: "localhost", + Official: false, + }, + RemoteName: "privatebase", + LocalName: "localhost/privatebase", + CanonicalName: "localhost/privatebase", + Official: false, + }, + IndexServerName() + "/public/moonbase": { + Index: &IndexInfo{ + Name: IndexServerName(), + Official: true, + }, + RemoteName: "public/moonbase", + LocalName: "public/moonbase", + CanonicalName: "public/moonbase", + Official: false, + }, + "index." + IndexServerName() + "/public/moonbase": { + Index: &IndexInfo{ + Name: IndexServerName(), + Official: true, + }, + RemoteName: "public/moonbase", + LocalName: "public/moonbase", + CanonicalName: "public/moonbase", + Official: false, + }, + IndexServerName() + "/public/moonbase": { + Index: &IndexInfo{ + Name: IndexServerName(), + Official: true, + }, + RemoteName: "public/moonbase", + LocalName: "public/moonbase", + CanonicalName: "public/moonbase", + Official: false, + }, + "ubuntu-12.04-base": { + Index: &IndexInfo{ + Name: IndexServerName(), + Official: true, + }, + RemoteName: "library/ubuntu-12.04-base", + LocalName: "ubuntu-12.04-base", + CanonicalName: "ubuntu-12.04-base", + Official: true, + }, + IndexServerName() + "/ubuntu-12.04-base": { + Index: &IndexInfo{ + Name: IndexServerName(), + Official: true, + }, + RemoteName: "library/ubuntu-12.04-base", + LocalName: "ubuntu-12.04-base", + CanonicalName: "ubuntu-12.04-base", + Official: true, + }, + IndexServerName() + "/ubuntu-12.04-base": { + Index: &IndexInfo{ + Name: IndexServerName(), + Official: true, + }, + RemoteName: "library/ubuntu-12.04-base", + LocalName: "ubuntu-12.04-base", + CanonicalName: "ubuntu-12.04-base", + Official: true, + }, + "index." + IndexServerName() + "/ubuntu-12.04-base": { + Index: &IndexInfo{ + Name: IndexServerName(), + Official: true, + }, + RemoteName: "library/ubuntu-12.04-base", + LocalName: "ubuntu-12.04-base", + CanonicalName: "ubuntu-12.04-base", + Official: true, + }, + } + + for reposName, expectedRepoInfo := range expectedRepoInfos { + repoInfo, err := ParseRepositoryInfo(reposName) + if err != nil { + t.Error(err) + } else { + checkEqual(t, repoInfo.Index.Name, expectedRepoInfo.Index.Name, reposName) + checkEqual(t, repoInfo.RemoteName, expectedRepoInfo.RemoteName, reposName) + checkEqual(t, repoInfo.LocalName, expectedRepoInfo.LocalName, reposName) + checkEqual(t, repoInfo.CanonicalName, expectedRepoInfo.CanonicalName, reposName) + checkEqual(t, repoInfo.Index.Official, expectedRepoInfo.Index.Official, reposName) + checkEqual(t, repoInfo.Official, expectedRepoInfo.Official, reposName) + } + } +} + +func TestNewIndexInfo(t *testing.T) { + testIndexInfo := func(config *ServiceConfig, expectedIndexInfos map[string]*IndexInfo) { + for indexName, expectedIndexInfo := range expectedIndexInfos { + index, err := NewIndexInfo(config, indexName) + if err != nil { + t.Fatal(err) + } else { + checkEqual(t, index.Name, expectedIndexInfo.Name, indexName+" name") + checkEqual(t, index.Official, expectedIndexInfo.Official, indexName+" is official") + checkEqual(t, index.Secure, expectedIndexInfo.Secure, indexName+" is secure") + checkEqual(t, len(index.Mirrors), len(expectedIndexInfo.Mirrors), indexName+" mirrors") + } + } + } + + config := NewServiceConfig(nil) + noMirrors := make([]string, 0) + expectedIndexInfos := map[string]*IndexInfo{ + IndexServerName(): { + Name: IndexServerName(), + Official: true, + Secure: true, + Mirrors: noMirrors, + }, + "index." + IndexServerName(): { + Name: IndexServerName(), + Official: true, + Secure: true, + Mirrors: noMirrors, + }, + "example.com": { + Name: "example.com", + Official: false, + Secure: true, + Mirrors: noMirrors, + }, + "127.0.0.1:5000": { + Name: "127.0.0.1:5000", + Official: false, + Secure: false, + Mirrors: noMirrors, + }, + } + testIndexInfo(config, expectedIndexInfos) + + publicMirrors := []string{"http://mirror1.local", "http://mirror2.local"} + config = makeServiceConfig(publicMirrors, []string{"example.com"}) + + expectedIndexInfos = map[string]*IndexInfo{ + IndexServerName(): { + Name: IndexServerName(), + Official: true, + Secure: true, + Mirrors: publicMirrors, + }, + "index." + IndexServerName(): { + Name: IndexServerName(), + Official: true, + Secure: true, + Mirrors: publicMirrors, + }, + "example.com": { + Name: "example.com", + Official: false, + Secure: false, + Mirrors: noMirrors, + }, + "example.com:5000": { + Name: "example.com:5000", + Official: false, + Secure: true, + Mirrors: noMirrors, + }, + "127.0.0.1": { + Name: "127.0.0.1", + Official: false, + Secure: false, + Mirrors: noMirrors, + }, + "127.0.0.1:5000": { + Name: "127.0.0.1:5000", + Official: false, + Secure: false, + Mirrors: noMirrors, + }, + "other.com": { + Name: "other.com", + Official: false, + Secure: true, + Mirrors: noMirrors, + }, + } + testIndexInfo(config, expectedIndexInfos) + + config = makeServiceConfig(nil, []string{"42.42.0.0/16"}) + expectedIndexInfos = map[string]*IndexInfo{ + "example.com": { + Name: "example.com", + Official: false, + Secure: false, + Mirrors: noMirrors, + }, + "example.com:5000": { + Name: "example.com:5000", + Official: false, + Secure: false, + Mirrors: noMirrors, + }, + "127.0.0.1": { + Name: "127.0.0.1", + Official: false, + Secure: false, + Mirrors: noMirrors, + }, + "127.0.0.1:5000": { + Name: "127.0.0.1:5000", + Official: false, + Secure: false, + Mirrors: noMirrors, + }, + "other.com": { + Name: "other.com", + Official: false, + Secure: true, + Mirrors: noMirrors, + }, + } + testIndexInfo(config, expectedIndexInfos) } func TestPushRegistryTag(t *testing.T) { @@ -232,7 +738,7 @@ func TestSearchRepositories(t *testing.T) { assertEqual(t, results.Results[0].StarCount, 42, "Expected 'fakeimage' a ot hae 42 stars") } -func TestValidRepositoryName(t *testing.T) { +func TestValidRemoteName(t *testing.T) { validRepositoryNames := []string{ // Sanity check. "docker/docker", @@ -247,7 +753,7 @@ func TestValidRepositoryName(t *testing.T) { "____/____", } for _, repositoryName := range validRepositoryNames { - if err := validateRepositoryName(repositoryName); err != nil { + if err := validateRemoteName(repositoryName); err != nil { t.Errorf("Repository name should be valid: %v. Error: %v", repositoryName, err) } } @@ -277,7 +783,7 @@ func TestValidRepositoryName(t *testing.T) { "docker/", } for _, repositoryName := range invalidRepositoryNames { - if err := validateRepositoryName(repositoryName); err == nil { + if err := validateRemoteName(repositoryName); err == nil { t.Errorf("Repository name should be invalid: %v", repositoryName) } } @@ -350,13 +856,13 @@ func TestAddRequiredHeadersToRedirectedRequests(t *testing.T) { } } -func TestIsSecure(t *testing.T) { +func TestIsSecureIndex(t *testing.T) { tests := []struct { addr string insecureRegistries []string expected bool }{ - {IndexServerURL.Host, nil, true}, + {IndexServerName(), nil, true}, {"example.com", []string{}, true}, {"example.com", []string{"example.com"}, false}, {"localhost", []string{"localhost:5000"}, false}, @@ -383,10 +889,9 @@ func TestIsSecure(t *testing.T) { {"invalid.domain.com:5000", []string{"invalid.domain.com:5000"}, false}, } for _, tt := range tests { - // TODO: remove this once we remove localhost insecure by default - insecureRegistries := append(tt.insecureRegistries, "127.0.0.0/8") - if sec, err := isSecure(tt.addr, insecureRegistries); err != nil || sec != tt.expected { - t.Fatalf("isSecure failed for %q %v, expected %v got %v. Error: %v", tt.addr, insecureRegistries, tt.expected, sec, err) + config := makeServiceConfig(nil, tt.insecureRegistries) + if sec := config.isSecureIndex(tt.addr); sec != tt.expected { + t.Errorf("isSecureIndex failed for %q %v, expected %v got %v", tt.addr, tt.insecureRegistries, tt.expected, sec) } } } diff --git a/registry/service.go b/registry/service.go index 53e8278b0..310539c4f 100644 --- a/registry/service.go +++ b/registry/service.go @@ -13,14 +13,14 @@ import ( // 'pull': Download images from any registry (TODO) // 'push': Upload images to any registry (TODO) type Service struct { - insecureRegistries []string + Config *ServiceConfig } // NewService returns a new instance of Service ready to be // installed no an engine. -func NewService(insecureRegistries []string) *Service { +func NewService(options *Options) *Service { return &Service{ - insecureRegistries: insecureRegistries, + Config: NewServiceConfig(options), } } @@ -28,6 +28,9 @@ func NewService(insecureRegistries []string) *Service { func (s *Service) Install(eng *engine.Engine) error { eng.Register("auth", s.Auth) eng.Register("search", s.Search) + eng.Register("resolve_repository", s.ResolveRepository) + eng.Register("resolve_index", s.ResolveIndex) + eng.Register("registry_config", s.GetRegistryConfig) return nil } @@ -39,15 +42,18 @@ func (s *Service) Auth(job *engine.Job) engine.Status { job.GetenvJson("authConfig", authConfig) - if addr := authConfig.ServerAddress; addr != "" && addr != IndexServerAddress() { - endpoint, err := NewEndpoint(addr, s.insecureRegistries) + if authConfig.ServerAddress != "" { + index, err := ResolveIndexInfo(job, authConfig.ServerAddress) if err != nil { return job.Error(err) } - if _, err := endpoint.Ping(); err != nil { - return job.Error(err) + if !index.Official { + endpoint, err := NewEndpoint(index) + if err != nil { + return job.Error(err) + } + authConfig.ServerAddress = endpoint.String() } - authConfig.ServerAddress = endpoint.String() } status, err := Login(authConfig, HTTPRequestFactory(nil)) @@ -87,12 +93,12 @@ func (s *Service) Search(job *engine.Job) engine.Status { job.GetenvJson("authConfig", authConfig) job.GetenvJson("metaHeaders", metaHeaders) - hostname, term, err := ResolveRepositoryName(term) + repoInfo, err := ResolveRepositoryInfo(job, term) if err != nil { return job.Error(err) } - - endpoint, err := NewEndpoint(hostname, s.insecureRegistries) + // *TODO: Search multiple indexes. + endpoint, err := repoInfo.GetEndpoint() if err != nil { return job.Error(err) } @@ -100,7 +106,7 @@ func (s *Service) Search(job *engine.Job) engine.Status { if err != nil { return job.Error(err) } - results, err := r.SearchRepositories(term) + results, err := r.SearchRepositories(repoInfo.GetSearchTerm()) if err != nil { return job.Error(err) } @@ -116,3 +122,92 @@ func (s *Service) Search(job *engine.Job) engine.Status { } return engine.StatusOK } + +// ResolveRepository splits a repository name into its components +// and configuration of the associated registry. +func (s *Service) ResolveRepository(job *engine.Job) engine.Status { + var ( + reposName = job.Args[0] + ) + + repoInfo, err := NewRepositoryInfo(s.Config, reposName) + if err != nil { + return job.Error(err) + } + + out := engine.Env{} + err = out.SetJson("repository", repoInfo) + if err != nil { + return job.Error(err) + } + out.WriteTo(job.Stdout) + + return engine.StatusOK +} + +// Convenience wrapper for calling resolve_repository Job from a running job. +func ResolveRepositoryInfo(jobContext *engine.Job, reposName string) (*RepositoryInfo, error) { + job := jobContext.Eng.Job("resolve_repository", reposName) + env, err := job.Stdout.AddEnv() + if err != nil { + return nil, err + } + if err := job.Run(); err != nil { + return nil, err + } + info := RepositoryInfo{} + if err := env.GetJson("repository", &info); err != nil { + return nil, err + } + return &info, nil +} + +// ResolveIndex takes indexName and returns index info +func (s *Service) ResolveIndex(job *engine.Job) engine.Status { + var ( + indexName = job.Args[0] + ) + + index, err := NewIndexInfo(s.Config, indexName) + if err != nil { + return job.Error(err) + } + + out := engine.Env{} + err = out.SetJson("index", index) + if err != nil { + return job.Error(err) + } + out.WriteTo(job.Stdout) + + return engine.StatusOK +} + +// Convenience wrapper for calling resolve_index Job from a running job. +func ResolveIndexInfo(jobContext *engine.Job, indexName string) (*IndexInfo, error) { + job := jobContext.Eng.Job("resolve_index", indexName) + env, err := job.Stdout.AddEnv() + if err != nil { + return nil, err + } + if err := job.Run(); err != nil { + return nil, err + } + info := IndexInfo{} + if err := env.GetJson("index", &info); err != nil { + return nil, err + } + return &info, nil +} + +// GetRegistryConfig returns current registry configuration. +func (s *Service) GetRegistryConfig(job *engine.Job) engine.Status { + out := engine.Env{} + err := out.SetJson("config", s.Config) + if err != nil { + return job.Error(err) + } + out.WriteTo(job.Stdout) + + return engine.StatusOK +} diff --git a/registry/types.go b/registry/types.go index 3b429f19a..fbbc0e709 100644 --- a/registry/types.go +++ b/registry/types.go @@ -65,3 +65,44 @@ const ( APIVersion1 = iota + 1 APIVersion2 ) + +// RepositoryInfo Examples: +// { +// "Index" : { +// "Name" : "docker.io", +// "Mirrors" : ["https://registry-2.docker.io/v1/", "https://registry-3.docker.io/v1/"], +// "Secure" : true, +// "Official" : true, +// }, +// "RemoteName" : "library/debian", +// "LocalName" : "debian", +// "CanonicalName" : "docker.io/debian" +// "Official" : true, +// } + +// { +// "Index" : { +// "Name" : "127.0.0.1:5000", +// "Mirrors" : [], +// "Secure" : false, +// "Official" : false, +// }, +// "RemoteName" : "user/repo", +// "LocalName" : "127.0.0.1:5000/user/repo", +// "CanonicalName" : "127.0.0.1:5000/user/repo", +// "Official" : false, +// } +type IndexInfo struct { + Name string + Mirrors []string + Secure bool + Official bool +} + +type RepositoryInfo struct { + Index *IndexInfo + RemoteName string + LocalName string + CanonicalName string + Official bool +} diff --git a/utils/http.go b/utils/http.go index bcf1865e2..24eaea56b 100644 --- a/utils/http.go +++ b/utils/http.go @@ -134,6 +134,10 @@ func (self *HTTPRequestFactory) AddDecorator(d ...HTTPRequestDecorator) { self.decorators = append(self.decorators, d...) } +func (self *HTTPRequestFactory) GetDecorators() []HTTPRequestDecorator { + return self.decorators +} + // NewRequest() creates a new *http.Request, // applies all decorators in the HTTPRequestFactory on the request, // then applies decorators provided by d on the request. From 6f0068f2733232b8357c2308517f6ddddb63aace Mon Sep 17 00:00:00 2001 From: Don Kjer Date: Wed, 7 Jan 2015 23:42:01 +0000 Subject: [PATCH 206/513] Moving NewIndexInfo, NewRepositoryInfo and associated helpers into config.go Signed-off-by: Don Kjer --- registry/auth.go | 15 -- registry/config.go | 312 ++++++++++++++++++++++++++++++++++---- registry/endpoint.go | 49 ------ registry/registry.go | 190 +---------------------- registry/registry_test.go | 2 +- registry/service.go | 4 +- 6 files changed, 290 insertions(+), 282 deletions(-) diff --git a/registry/auth.go b/registry/auth.go index 8382869b3..102078d7a 100644 --- a/registry/auth.go +++ b/registry/auth.go @@ -17,13 +17,6 @@ import ( const ( // Where we store the config file CONFIGFILE = ".dockercfg" - - // Only used for user auth + account creation - INDEXSERVER = "https://index.docker.io/v1/" - REGISTRYSERVER = "https://registry-1.docker.io/v1/" - INDEXNAME = "docker.io" - - // INDEXSERVER = "https://registry-stage.hub.docker.com/v1/" ) var ( @@ -43,14 +36,6 @@ type ConfigFile struct { rootPath string } -func IndexServerAddress() string { - return INDEXSERVER -} - -func IndexServerName() string { - return INDEXNAME -} - // create a base64 encoded auth string to store in config func encodeAuth(authConfig *AuthConfig) string { authStr := authConfig.Username + ":" + authConfig.Password diff --git a/registry/config.go b/registry/config.go index bd993edd5..b5652b15d 100644 --- a/registry/config.go +++ b/registry/config.go @@ -2,12 +2,16 @@ package registry import ( "encoding/json" + "errors" "fmt" "net" "net/url" + "regexp" + "strings" "github.com/docker/docker/opts" flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/utils" ) // Options holds command line options. @@ -16,6 +20,30 @@ type Options struct { InsecureRegistries opts.ListOpts } +const ( + // Only used for user auth + account creation + INDEXSERVER = "https://index.docker.io/v1/" + REGISTRYSERVER = "https://registry-1.docker.io/v1/" + INDEXNAME = "docker.io" + + // INDEXSERVER = "https://registry-stage.hub.docker.com/v1/" +) + +var ( + ErrInvalidRepositoryName = errors.New("Invalid repository name (ex: \"registry.domain.tld/myrepos\")") + emptyServiceConfig = NewServiceConfig(nil) + validNamespaceChars = regexp.MustCompile(`^([a-z0-9-_]*)$`) + validRepo = regexp.MustCompile(`^([a-z0-9-_.]+)$`) +) + +func IndexServerAddress() string { + return INDEXSERVER +} + +func IndexServerName() string { + return INDEXNAME +} + // InstallFlags adds command-line options to the top-level flag parser for // the current process. func (options *Options) InstallFlags() { @@ -25,34 +53,6 @@ func (options *Options) InstallFlags() { flag.Var(&options.InsecureRegistries, []string{"-insecure-registry"}, "Enable insecure communication with specified registries (no certificate verification for HTTPS and enable HTTP fallback) (e.g., localhost:5000 or 10.20.0.0/16)") } -// ValidateMirror validates an HTTP(S) registry mirror -func ValidateMirror(val string) (string, error) { - uri, err := url.Parse(val) - if err != nil { - return "", fmt.Errorf("%s is not a valid URI", val) - } - - if uri.Scheme != "http" && uri.Scheme != "https" { - return "", fmt.Errorf("Unsupported scheme %s", uri.Scheme) - } - - if uri.Path != "" || uri.RawQuery != "" || uri.Fragment != "" { - return "", fmt.Errorf("Unsupported path/query/fragment at end of the URI") - } - - return fmt.Sprintf("%s://%s/v1/", uri.Scheme, uri.Host), nil -} - -// ValidateIndexName validates an index name. -func ValidateIndexName(val string) (string, error) { - // 'index.docker.io' => 'docker.io' - if val == "index."+IndexServerName() { - val = IndexServerName() - } - // *TODO: Check if valid hostname[:port]/ip[:port]? - return val, nil -} - type netIPNet net.IPNet func (ipnet *netIPNet) MarshalJSON() ([]byte, error) { @@ -124,3 +124,259 @@ func NewServiceConfig(options *Options) *ServiceConfig { return config } + +// isSecureIndex returns false if the provided indexName is part of the list of insecure registries +// Insecure registries accept HTTP and/or accept HTTPS with certificates from unknown CAs. +// +// The list of insecure registries can contain an element with CIDR notation to specify a whole subnet. +// If the subnet contains one of the IPs of the registry specified by indexName, the latter is considered +// insecure. +// +// indexName should be a URL.Host (`host:port` or `host`) where the `host` part can be either a domain name +// or an IP address. If it is a domain name, then it will be resolved in order to check if the IP is contained +// in a subnet. If the resolving is not successful, isSecureIndex will only try to match hostname to any element +// of insecureRegistries. +func (config *ServiceConfig) isSecureIndex(indexName string) bool { + // Check for configured index, first. This is needed in case isSecureIndex + // is called from anything besides NewIndexInfo, in order to honor per-index configurations. + if index, ok := config.IndexConfigs[indexName]; ok { + return index.Secure + } + + host, _, err := net.SplitHostPort(indexName) + if err != nil { + // assume indexName is of the form `host` without the port and go on. + host = indexName + } + + addrs, err := lookupIP(host) + if err != nil { + ip := net.ParseIP(host) + if ip != nil { + addrs = []net.IP{ip} + } + + // if ip == nil, then `host` is neither an IP nor it could be looked up, + // either because the index is unreachable, or because the index is behind an HTTP proxy. + // So, len(addrs) == 0 and we're not aborting. + } + + // Try CIDR notation only if addrs has any elements, i.e. if `host`'s IP could be determined. + for _, addr := range addrs { + for _, ipnet := range config.InsecureRegistryCIDRs { + // check if the addr falls in the subnet + if (*net.IPNet)(ipnet).Contains(addr) { + return false + } + } + } + + return true +} + +// ValidateMirror validates an HTTP(S) registry mirror +func ValidateMirror(val string) (string, error) { + uri, err := url.Parse(val) + if err != nil { + return "", fmt.Errorf("%s is not a valid URI", val) + } + + if uri.Scheme != "http" && uri.Scheme != "https" { + return "", fmt.Errorf("Unsupported scheme %s", uri.Scheme) + } + + if uri.Path != "" || uri.RawQuery != "" || uri.Fragment != "" { + return "", fmt.Errorf("Unsupported path/query/fragment at end of the URI") + } + + return fmt.Sprintf("%s://%s/v1/", uri.Scheme, uri.Host), nil +} + +// ValidateIndexName validates an index name. +func ValidateIndexName(val string) (string, error) { + // 'index.docker.io' => 'docker.io' + if val == "index."+IndexServerName() { + val = IndexServerName() + } + // *TODO: Check if valid hostname[:port]/ip[:port]? + return val, nil +} + +func validateRemoteName(remoteName string) error { + var ( + namespace string + name string + ) + nameParts := strings.SplitN(remoteName, "/", 2) + if len(nameParts) < 2 { + namespace = "library" + name = nameParts[0] + + // the repository name must not be a valid image ID + if err := utils.ValidateID(name); err == nil { + return fmt.Errorf("Invalid repository name (%s), cannot specify 64-byte hexadecimal strings", name) + } + } else { + namespace = nameParts[0] + name = nameParts[1] + } + if !validNamespaceChars.MatchString(namespace) { + return fmt.Errorf("Invalid namespace name (%s). Only [a-z0-9-_] are allowed.", namespace) + } + if len(namespace) < 4 || len(namespace) > 30 { + return fmt.Errorf("Invalid namespace name (%s). Cannot be fewer than 4 or more than 30 characters.", namespace) + } + if strings.HasPrefix(namespace, "-") || strings.HasSuffix(namespace, "-") { + return fmt.Errorf("Invalid namespace name (%s). Cannot begin or end with a hyphen.", namespace) + } + if strings.Contains(namespace, "--") { + return fmt.Errorf("Invalid namespace name (%s). Cannot contain consecutive hyphens.", namespace) + } + if !validRepo.MatchString(name) { + return fmt.Errorf("Invalid repository name (%s), only [a-z0-9-_.] are allowed", name) + } + return nil +} + +func validateNoSchema(reposName string) error { + if strings.Contains(reposName, "://") { + // It cannot contain a scheme! + return ErrInvalidRepositoryName + } + return nil +} + +// ValidateRepositoryName validates a repository name +func ValidateRepositoryName(reposName string) error { + var err error + if err = validateNoSchema(reposName); err != nil { + return err + } + indexName, remoteName := splitReposName(reposName) + if _, err = ValidateIndexName(indexName); err != nil { + return err + } + return validateRemoteName(remoteName) +} + +// NewIndexInfo returns IndexInfo configuration from indexName +func (config *ServiceConfig) NewIndexInfo(indexName string) (*IndexInfo, error) { + var err error + indexName, err = ValidateIndexName(indexName) + if err != nil { + return nil, err + } + + // Return any configured index info, first. + if index, ok := config.IndexConfigs[indexName]; ok { + return index, nil + } + + // Construct a non-configured index info. + index := &IndexInfo{ + Name: indexName, + Mirrors: make([]string, 0), + Official: false, + } + index.Secure = config.isSecureIndex(indexName) + return index, nil +} + +// GetAuthConfigKey special-cases using the full index address of the official +// index as the AuthConfig key, and uses the (host)name[:port] for private indexes. +func (index *IndexInfo) GetAuthConfigKey() string { + if index.Official { + return IndexServerAddress() + } + return index.Name +} + +// splitReposName breaks a reposName into an index name and remote name +func splitReposName(reposName string) (string, string) { + nameParts := strings.SplitN(reposName, "/", 2) + var indexName, remoteName string + if len(nameParts) == 1 || (!strings.Contains(nameParts[0], ".") && + !strings.Contains(nameParts[0], ":") && nameParts[0] != "localhost") { + // This is a Docker Index repos (ex: samalba/hipache or ubuntu) + // 'docker.io' + indexName = IndexServerName() + remoteName = reposName + } else { + indexName = nameParts[0] + remoteName = nameParts[1] + } + return indexName, remoteName +} + +// NewRepositoryInfo validates and breaks down a repository name into a RepositoryInfo +func (config *ServiceConfig) NewRepositoryInfo(reposName string) (*RepositoryInfo, error) { + if err := validateNoSchema(reposName); err != nil { + return nil, err + } + + indexName, remoteName := splitReposName(reposName) + if err := validateRemoteName(remoteName); err != nil { + return nil, err + } + + repoInfo := &RepositoryInfo{ + RemoteName: remoteName, + } + + var err error + repoInfo.Index, err = config.NewIndexInfo(indexName) + if err != nil { + return nil, err + } + + if repoInfo.Index.Official { + normalizedName := repoInfo.RemoteName + if strings.HasPrefix(normalizedName, "library/") { + // If pull "library/foo", it's stored locally under "foo" + normalizedName = strings.SplitN(normalizedName, "/", 2)[1] + } + + repoInfo.LocalName = normalizedName + repoInfo.RemoteName = normalizedName + // If the normalized name does not contain a '/' (e.g. "foo") + // then it is an official repo. + if strings.IndexRune(normalizedName, '/') == -1 { + repoInfo.Official = true + // Fix up remote name for official repos. + repoInfo.RemoteName = "library/" + normalizedName + } + + // *TODO: Prefix this with 'docker.io/'. + repoInfo.CanonicalName = repoInfo.LocalName + } else { + // *TODO: Decouple index name from hostname (via registry configuration?) + repoInfo.LocalName = repoInfo.Index.Name + "/" + repoInfo.RemoteName + repoInfo.CanonicalName = repoInfo.LocalName + } + return repoInfo, nil +} + +// GetSearchTerm special-cases using local name for official index, and +// remote name for private indexes. +func (repoInfo *RepositoryInfo) GetSearchTerm() string { + if repoInfo.Index.Official { + return repoInfo.LocalName + } + return repoInfo.RemoteName +} + +// ParseRepositoryInfo performs the breakdown of a repository name into a RepositoryInfo, but +// lacks registry configuration. +func ParseRepositoryInfo(reposName string) (*RepositoryInfo, error) { + return emptyServiceConfig.NewRepositoryInfo(reposName) +} + +// NormalizeLocalName transforms a repository name into a normalize LocalName +// Passes through the name without transformation on error (image id, etc) +func NormalizeLocalName(name string) string { + repoInfo, err := ParseRepositoryInfo(name) + if err != nil { + return name + } + return repoInfo.LocalName +} diff --git a/registry/endpoint.go b/registry/endpoint.go index 86f53744d..95680c5ef 100644 --- a/registry/endpoint.go +++ b/registry/endpoint.go @@ -157,52 +157,3 @@ func (e Endpoint) Ping() (RegistryInfo, error) { log.Debugf("RegistryInfo.Standalone: %t", info.Standalone) return info, nil } - -// isSecureIndex returns false if the provided indexName is part of the list of insecure registries -// Insecure registries accept HTTP and/or accept HTTPS with certificates from unknown CAs. -// -// The list of insecure registries can contain an element with CIDR notation to specify a whole subnet. -// If the subnet contains one of the IPs of the registry specified by indexName, the latter is considered -// insecure. -// -// indexName should be a URL.Host (`host:port` or `host`) where the `host` part can be either a domain name -// or an IP address. If it is a domain name, then it will be resolved in order to check if the IP is contained -// in a subnet. If the resolving is not successful, isSecureIndex will only try to match hostname to any element -// of insecureRegistries. -func (config *ServiceConfig) isSecureIndex(indexName string) bool { - // Check for configured index, first. This is needed in case isSecureIndex - // is called from anything besides NewIndexInfo, in order to honor per-index configurations. - if index, ok := config.IndexConfigs[indexName]; ok { - return index.Secure - } - - host, _, err := net.SplitHostPort(indexName) - if err != nil { - // assume indexName is of the form `host` without the port and go on. - host = indexName - } - - addrs, err := lookupIP(host) - if err != nil { - ip := net.ParseIP(host) - if ip != nil { - addrs = []net.IP{ip} - } - - // if ip == nil, then `host` is neither an IP nor it could be looked up, - // either because the index is unreachable, or because the index is behind an HTTP proxy. - // So, len(addrs) == 0 and we're not aborting. - } - - // Try CIDR notation only if addrs has any elements, i.e. if `host`'s IP could be determined. - for _, addr := range addrs { - for _, ipnet := range config.InsecureRegistryCIDRs { - // check if the addr falls in the subnet - if (*net.IPNet)(ipnet).Contains(addr) { - return false - } - } - } - - return true -} diff --git a/registry/registry.go b/registry/registry.go index de724ee20..77a78a820 100644 --- a/registry/registry.go +++ b/registry/registry.go @@ -10,7 +10,6 @@ import ( "net/http" "os" "path" - "regexp" "strings" "time" @@ -19,13 +18,9 @@ import ( ) var ( - ErrAlreadyExists = errors.New("Image already exists") - ErrInvalidRepositoryName = errors.New("Invalid repository name (ex: \"registry.domain.tld/myrepos\")") - ErrDoesNotExist = errors.New("Image does not exist") - errLoginRequired = errors.New("Authentication is required.") - validNamespaceChars = regexp.MustCompile(`^([a-z0-9-_]*)$`) - validRepo = regexp.MustCompile(`^([a-z0-9-_.]+)$`) - emptyServiceConfig = NewServiceConfig(nil) + ErrAlreadyExists = errors.New("Image already exists") + ErrDoesNotExist = errors.New("Image does not exist") + errLoginRequired = errors.New("Authentication is required.") ) type TimeoutType uint32 @@ -161,185 +156,6 @@ func doRequest(req *http.Request, jar http.CookieJar, timeout TimeoutType, secur return res, client, err } -func validateRemoteName(remoteName string) error { - var ( - namespace string - name string - ) - nameParts := strings.SplitN(remoteName, "/", 2) - if len(nameParts) < 2 { - namespace = "library" - name = nameParts[0] - - // the repository name must not be a valid image ID - if err := utils.ValidateID(name); err == nil { - return fmt.Errorf("Invalid repository name (%s), cannot specify 64-byte hexadecimal strings", name) - } - } else { - namespace = nameParts[0] - name = nameParts[1] - } - if !validNamespaceChars.MatchString(namespace) { - return fmt.Errorf("Invalid namespace name (%s). Only [a-z0-9-_] are allowed.", namespace) - } - if len(namespace) < 4 || len(namespace) > 30 { - return fmt.Errorf("Invalid namespace name (%s). Cannot be fewer than 4 or more than 30 characters.", namespace) - } - if strings.HasPrefix(namespace, "-") || strings.HasSuffix(namespace, "-") { - return fmt.Errorf("Invalid namespace name (%s). Cannot begin or end with a hyphen.", namespace) - } - if strings.Contains(namespace, "--") { - return fmt.Errorf("Invalid namespace name (%s). Cannot contain consecutive hyphens.", namespace) - } - if !validRepo.MatchString(name) { - return fmt.Errorf("Invalid repository name (%s), only [a-z0-9-_.] are allowed", name) - } - return nil -} - -// NewIndexInfo returns IndexInfo configuration from indexName -func NewIndexInfo(config *ServiceConfig, indexName string) (*IndexInfo, error) { - var err error - indexName, err = ValidateIndexName(indexName) - if err != nil { - return nil, err - } - - // Return any configured index info, first. - if index, ok := config.IndexConfigs[indexName]; ok { - return index, nil - } - - // Construct a non-configured index info. - index := &IndexInfo{ - Name: indexName, - Mirrors: make([]string, 0), - Official: false, - } - index.Secure = config.isSecureIndex(indexName) - return index, nil -} - -func validateNoSchema(reposName string) error { - if strings.Contains(reposName, "://") { - // It cannot contain a scheme! - return ErrInvalidRepositoryName - } - return nil -} - -// splitReposName breaks a reposName into an index name and remote name -func splitReposName(reposName string) (string, string) { - nameParts := strings.SplitN(reposName, "/", 2) - var indexName, remoteName string - if len(nameParts) == 1 || (!strings.Contains(nameParts[0], ".") && - !strings.Contains(nameParts[0], ":") && nameParts[0] != "localhost") { - // This is a Docker Index repos (ex: samalba/hipache or ubuntu) - // 'docker.io' - indexName = IndexServerName() - remoteName = reposName - } else { - indexName = nameParts[0] - remoteName = nameParts[1] - } - return indexName, remoteName -} - -// NewRepositoryInfo validates and breaks down a repository name into a RepositoryInfo -func NewRepositoryInfo(config *ServiceConfig, reposName string) (*RepositoryInfo, error) { - if err := validateNoSchema(reposName); err != nil { - return nil, err - } - - indexName, remoteName := splitReposName(reposName) - if err := validateRemoteName(remoteName); err != nil { - return nil, err - } - - repoInfo := &RepositoryInfo{ - RemoteName: remoteName, - } - - var err error - repoInfo.Index, err = NewIndexInfo(config, indexName) - if err != nil { - return nil, err - } - - if repoInfo.Index.Official { - normalizedName := repoInfo.RemoteName - if strings.HasPrefix(normalizedName, "library/") { - // If pull "library/foo", it's stored locally under "foo" - normalizedName = strings.SplitN(normalizedName, "/", 2)[1] - } - - repoInfo.LocalName = normalizedName - repoInfo.RemoteName = normalizedName - // If the normalized name does not contain a '/' (e.g. "foo") - // then it is an official repo. - if strings.IndexRune(normalizedName, '/') == -1 { - repoInfo.Official = true - // Fix up remote name for official repos. - repoInfo.RemoteName = "library/" + normalizedName - } - - // *TODO: Prefix this with 'docker.io/'. - repoInfo.CanonicalName = repoInfo.LocalName - } else { - // *TODO: Decouple index name from hostname (via registry configuration?) - repoInfo.LocalName = repoInfo.Index.Name + "/" + repoInfo.RemoteName - repoInfo.CanonicalName = repoInfo.LocalName - } - return repoInfo, nil -} - -// ValidateRepositoryName validates a repository name -func ValidateRepositoryName(reposName string) error { - var err error - if err = validateNoSchema(reposName); err != nil { - return err - } - indexName, remoteName := splitReposName(reposName) - if _, err = ValidateIndexName(indexName); err != nil { - return err - } - return validateRemoteName(remoteName) -} - -// ParseRepositoryInfo performs the breakdown of a repository name into a RepositoryInfo, but -// lacks registry configuration. -func ParseRepositoryInfo(reposName string) (*RepositoryInfo, error) { - return NewRepositoryInfo(emptyServiceConfig, reposName) -} - -// NormalizeLocalName transforms a repository name into a normalize LocalName -// Passes through the name without transformation on error (image id, etc) -func NormalizeLocalName(name string) string { - repoInfo, err := ParseRepositoryInfo(name) - if err != nil { - return name - } - return repoInfo.LocalName -} - -// GetAuthConfigKey special-cases using the full index address of the official -// index as the AuthConfig key, and uses the (host)name[:port] for private indexes. -func (index *IndexInfo) GetAuthConfigKey() string { - if index.Official { - return IndexServerAddress() - } - return index.Name -} - -// GetSearchTerm special-cases using local name for official index, and -// remote name for private indexes. -func (repoInfo *RepositoryInfo) GetSearchTerm() string { - if repoInfo.Index.Official { - return repoInfo.LocalName - } - return repoInfo.RemoteName -} - func trustedLocation(req *http.Request) bool { var ( trusteds = []string{"docker.com", "docker.io"} diff --git a/registry/registry_test.go b/registry/registry_test.go index 511d7eb17..6bf31505e 100644 --- a/registry/registry_test.go +++ b/registry/registry_test.go @@ -561,7 +561,7 @@ func TestParseRepositoryInfo(t *testing.T) { func TestNewIndexInfo(t *testing.T) { testIndexInfo := func(config *ServiceConfig, expectedIndexInfos map[string]*IndexInfo) { for indexName, expectedIndexInfo := range expectedIndexInfos { - index, err := NewIndexInfo(config, indexName) + index, err := config.NewIndexInfo(indexName) if err != nil { t.Fatal(err) } else { diff --git a/registry/service.go b/registry/service.go index 310539c4f..c34e38423 100644 --- a/registry/service.go +++ b/registry/service.go @@ -130,7 +130,7 @@ func (s *Service) ResolveRepository(job *engine.Job) engine.Status { reposName = job.Args[0] ) - repoInfo, err := NewRepositoryInfo(s.Config, reposName) + repoInfo, err := s.Config.NewRepositoryInfo(reposName) if err != nil { return job.Error(err) } @@ -168,7 +168,7 @@ func (s *Service) ResolveIndex(job *engine.Job) engine.Status { indexName = job.Args[0] ) - index, err := NewIndexInfo(s.Config, indexName) + index, err := s.Config.NewIndexInfo(indexName) if err != nil { return job.Error(err) } From 33b931e718b89fec64705471534f8df3d5e42c4c Mon Sep 17 00:00:00 2001 From: Don Kjer Date: Thu, 8 Jan 2015 21:11:48 +0000 Subject: [PATCH 207/513] Adding workaround to suppress gofmt issues with api/client/utils.go Signed-off-by: Don Kjer --- api/client/utils.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/api/client/utils.go b/api/client/utils.go index 6ebe44806..0ee02cdbc 100644 --- a/api/client/utils.go +++ b/api/client/utils.go @@ -260,7 +260,10 @@ func (cli *DockerCli) monitorTtySize(id string, isExec bool) error { sigchan := make(chan os.Signal, 1) gosignal.Notify(sigchan, signal.SIGWINCH) go func() { - for _ = range sigchan { + // This tmp := range..., _ = tmp workaround is needed to + // suppress gofmt warnings while still preserve go1.3 compatibility + for tmp := range sigchan { + _ = tmp cli.resizeTty(id, isExec) } }() From 3807d96d379cb5b08ad3dae3d624fb3d24306b77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20K=C3=B6hler?= Date: Wed, 7 Jan 2015 20:27:24 +0100 Subject: [PATCH 208/513] Fix order of pause and port in command line documentation. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Andreas Köhler --- docs/sources/reference/commandline/cli.md | 32 +++++++++++------------ 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index 799c0148c..a72ac21dd 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -1310,6 +1310,22 @@ timestamp, for example `2014-09-16T06:17:46.000000000Z`, to each log entry. To ensure that the timestamps for are aligned the nano-second part of the timestamp will be padded with zero when necessary. +## pause + + Usage: docker pause CONTAINER + + Pause all processes within a container + +The `docker pause` command uses the cgroups freezer to suspend all processes in +a container. Traditionally, when suspending a process the `SIGSTOP` signal is +used, which is observable by the process being suspended. With the cgroups freezer +the process is unaware, and unable to capture, that it is being suspended, +and subsequently resumed. + +See the +[cgroups freezer documentation](https://www.kernel.org/doc/Documentation/cgroups/freezer-subsystem.txt) +for further details. + ## port Usage: docker port CONTAINER [PRIVATE_PORT[/PROTO]] @@ -1332,22 +1348,6 @@ just a specific mapping: $ sudo docker port test 7890 0.0.0.0:4321 -## pause - - Usage: docker pause CONTAINER - - Pause all processes within a container - -The `docker pause` command uses the cgroups freezer to suspend all processes in -a container. Traditionally when suspending a process the `SIGSTOP` signal is -used, which is observable by the process being suspended. With the cgroups freezer -the process is unaware, and unable to capture, that it is being suspended, -and subsequently resumed. - -See the -[cgroups freezer documentation](https://www.kernel.org/doc/Documentation/cgroups/freezer-subsystem.txt) -for further details. - ## ps Usage: docker ps [OPTIONS] From 813ff7f19d6a097f39dae363d8dd81b70eee515a Mon Sep 17 00:00:00 2001 From: Malte Janduda Date: Fri, 9 Jan 2015 00:03:19 +0100 Subject: [PATCH 209/513] Adding IPv6 network support to docker Signed-off-by: Malte Janduda --- daemon/config.go | 8 +- daemon/container.go | 20 +- daemon/daemon.go | 2 + daemon/execdriver/driver.go | 14 +- daemon/execdriver/native/create.go | 4 + daemon/network_settings.go | 19 +- daemon/networkdriver/bridge/driver.go | 209 +++++++++++++++--- daemon/networkdriver/utils.go | 25 ++- docs/man/docker.1.md | 12 +- .../article-img/ipv6_basic_host_config.gliffy | 1 + .../article-img/ipv6_basic_host_config.svg | 1 + .../ipv6_routed_network_example.gliffy | 1 + .../ipv6_routed_network_example.svg | 1 + .../ipv6_slash64_subnet_config.gliffy | 1 + .../ipv6_slash64_subnet_config.svg | 1 + .../ipv6_switched_network_example.gliffy | 1 + .../ipv6_switched_network_example.svg | 1 + docs/sources/articles/networking.md | 186 +++++++++++++++- docs/sources/reference/commandline/cli.md | 6 +- 19 files changed, 448 insertions(+), 65 deletions(-) create mode 100644 docs/sources/article-img/ipv6_basic_host_config.gliffy create mode 100644 docs/sources/article-img/ipv6_basic_host_config.svg create mode 100644 docs/sources/article-img/ipv6_routed_network_example.gliffy create mode 100644 docs/sources/article-img/ipv6_routed_network_example.svg create mode 100644 docs/sources/article-img/ipv6_slash64_subnet_config.gliffy create mode 100644 docs/sources/article-img/ipv6_slash64_subnet_config.svg create mode 100644 docs/sources/article-img/ipv6_switched_network_example.gliffy create mode 100644 docs/sources/article-img/ipv6_switched_network_example.svg diff --git a/daemon/config.go b/daemon/config.go index c5ac056d2..99e5ce4c8 100644 --- a/daemon/config.go +++ b/daemon/config.go @@ -23,6 +23,7 @@ type Config struct { AutoRestart bool Dns []string DnsSearch []string + EnableIPv6 bool EnableIptables bool EnableIpForward bool EnableIpMasq bool @@ -30,6 +31,7 @@ type Config struct { BridgeIface string BridgeIP string FixedCIDR string + FixedCIDRv6 string InterContainerCommunication bool GraphDriver string GraphOptions []string @@ -51,11 +53,13 @@ func (config *Config) InstallFlags() { flag.StringVar(&config.Root, []string{"g", "-graph"}, "/var/lib/docker", "Path to use as the root of the Docker runtime") flag.BoolVar(&config.AutoRestart, []string{"#r", "#-restart"}, true, "--restart on the daemon has been deprecated in favor of --restart policies on docker run") flag.BoolVar(&config.EnableIptables, []string{"#iptables", "-iptables"}, true, "Enable Docker's addition of iptables rules") - flag.BoolVar(&config.EnableIpForward, []string{"#ip-forward", "-ip-forward"}, true, "Enable net.ipv4.ip_forward") + flag.BoolVar(&config.EnableIpForward, []string{"#ip-forward", "-ip-forward"}, true, "Enable net.ipv4.ip_forward and IPv6 forwarding if --fixed-cidr-v6 is defined. IPv6 forwarding may interfere with your existing IPv6 configuration when using Router Advertisement.") flag.BoolVar(&config.EnableIpMasq, []string{"-ip-masq"}, true, "Enable IP masquerading for bridge's IP range") + flag.BoolVar(&config.EnableIPv6, []string{"-ipv6"}, false, "Enable IPv6 networking") flag.StringVar(&config.BridgeIP, []string{"#bip", "-bip"}, "", "Use this CIDR notation address for the network bridge's IP, not compatible with -b") flag.StringVar(&config.BridgeIface, []string{"b", "-bridge"}, "", "Attach containers to a pre-existing network bridge\nuse 'none' to disable container networking") - flag.StringVar(&config.FixedCIDR, []string{"-fixed-cidr"}, "", "IPv4 subnet for fixed IPs (ex: 10.20.0.0/16)\nthis subnet must be nested in the bridge subnet (which is defined by -b or --bip)") + flag.StringVar(&config.FixedCIDR, []string{"-fixed-cidr"}, "", "IPv4 subnet for fixed IPs (e.g. 10.20.0.0/16)\nthis subnet must be nested in the bridge subnet (which is defined by -b or --bip)") + flag.StringVar(&config.FixedCIDRv6, []string{"-fixed-cidr-v6"}, "", "IPv6 subnet for fixed IPs (e.g.: 2001:a02b/48)") flag.BoolVar(&config.InterContainerCommunication, []string{"#icc", "-icc"}, true, "Allow unrestricted inter-container and Docker daemon host communication") flag.StringVar(&config.GraphDriver, []string{"s", "-storage-driver"}, "", "Force the Docker runtime to use a specific storage driver") flag.StringVar(&config.ExecDriver, []string{"e", "-exec-driver"}, "native", "Force the Docker runtime to use a specific exec driver") diff --git a/daemon/container.go b/daemon/container.go index 6976dbff7..0effa99c1 100644 --- a/daemon/container.go +++ b/daemon/container.go @@ -217,11 +217,15 @@ func populateCommand(c *Container, env []string) error { if !c.Config.NetworkDisabled { network := c.NetworkSettings en.Interface = &execdriver.NetworkInterface{ - Gateway: network.Gateway, - Bridge: network.Bridge, - IPAddress: network.IPAddress, - IPPrefixLen: network.IPPrefixLen, - MacAddress: network.MacAddress, + Gateway: network.Gateway, + Bridge: network.Bridge, + IPAddress: network.IPAddress, + IPPrefixLen: network.IPPrefixLen, + MacAddress: network.MacAddress, + LinkLocalIPv6Address: network.LinkLocalIPv6Address, + GlobalIPv6Address: network.GlobalIPv6Address, + GlobalIPv6PrefixLen: network.GlobalIPv6PrefixLen, + IPv6Gateway: network.IPv6Gateway, } } case "container": @@ -540,6 +544,12 @@ func (container *Container) AllocateNetwork() error { container.NetworkSettings.IPPrefixLen = env.GetInt("IPPrefixLen") container.NetworkSettings.MacAddress = env.Get("MacAddress") container.NetworkSettings.Gateway = env.Get("Gateway") + container.NetworkSettings.MacAddress = env.Get("MacAddress") + container.NetworkSettings.LinkLocalIPv6Address = env.Get("LinkLocalIPv6") + container.NetworkSettings.LinkLocalIPv6PrefixLen = 64 + container.NetworkSettings.GlobalIPv6Address = env.Get("GlobalIPv6") + container.NetworkSettings.GlobalIPv6PrefixLen = env.GetInt("GlobalIPv6PrefixLen") + container.NetworkSettings.IPv6Gateway = env.Get("IPv6Gateway") return nil } diff --git a/daemon/daemon.go b/daemon/daemon.go index 68f688e6b..5972b4f87 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -919,9 +919,11 @@ func NewDaemonFromDirectory(config *Config, eng *engine.Engine) (*Daemon, error) job.SetenvBool("InterContainerCommunication", config.InterContainerCommunication) job.SetenvBool("EnableIpForward", config.EnableIpForward) job.SetenvBool("EnableIpMasq", config.EnableIpMasq) + job.SetenvBool("EnableIPv6", config.EnableIPv6) job.Setenv("BridgeIface", config.BridgeIface) job.Setenv("BridgeIP", config.BridgeIP) job.Setenv("FixedCIDR", config.FixedCIDR) + job.Setenv("FixedCIDRv6", config.FixedCIDRv6) job.Setenv("DefaultBindingIP", config.DefaultIp.String()) if err := job.Run(); err != nil { diff --git a/daemon/execdriver/driver.go b/daemon/execdriver/driver.go index 411265814..2a5eff556 100644 --- a/daemon/execdriver/driver.go +++ b/daemon/execdriver/driver.go @@ -78,11 +78,15 @@ type Ipc struct { } type NetworkInterface struct { - Gateway string `json:"gateway"` - IPAddress string `json:"ip"` - IPPrefixLen int `json:"ip_prefix_len"` - MacAddress string `json:"mac_address"` - Bridge string `json:"bridge"` + Gateway string `json:"gateway"` + IPAddress string `json:"ip"` + IPPrefixLen int `json:"ip_prefix_len"` + MacAddress string `json:"mac"` + Bridge string `json:"bridge"` + GlobalIPv6Address string `json:"global_ipv6"` + LinkLocalIPv6Address string `json:"link_local_ipv6"` + GlobalIPv6PrefixLen int `json:"global_ipv6_prefix_len"` + IPv6Gateway string `json:"ipv6_gateway"` } type Resources struct { diff --git a/daemon/execdriver/native/create.go b/daemon/execdriver/native/create.go index 188f78ec0..99c21a20b 100644 --- a/daemon/execdriver/native/create.go +++ b/daemon/execdriver/native/create.go @@ -105,6 +105,10 @@ func (d *driver) createNetwork(container *libcontainer.Config, c *execdriver.Com Bridge: c.Network.Interface.Bridge, VethPrefix: "veth", } + if c.Network.Interface.GlobalIPv6Address != "" { + vethNetwork.IPv6Address = fmt.Sprintf("%s/%d", c.Network.Interface.GlobalIPv6Address, c.Network.Interface.GlobalIPv6PrefixLen) + vethNetwork.IPv6Gateway = c.Network.Interface.IPv6Gateway + } container.Networks = append(container.Networks, &vethNetwork) } diff --git a/daemon/network_settings.go b/daemon/network_settings.go index 69c15be3d..97c2e3ab4 100644 --- a/daemon/network_settings.go +++ b/daemon/network_settings.go @@ -9,13 +9,18 @@ import ( type PortMapping map[string]string // Deprecated type NetworkSettings struct { - IPAddress string - IPPrefixLen int - MacAddress string - Gateway string - Bridge string - PortMapping map[string]PortMapping // Deprecated - Ports nat.PortMap + IPAddress string + IPPrefixLen int + MacAddress string + LinkLocalIPv6Address string + LinkLocalIPv6PrefixLen int + GlobalIPv6Address string + GlobalIPv6PrefixLen int + Gateway string + IPv6Gateway string + Bridge string + PortMapping map[string]PortMapping // Deprecated + Ports nat.PortMap } func (settings *NetworkSettings) PortMappingAPI() *engine.Table { diff --git a/daemon/networkdriver/bridge/driver.go b/daemon/networkdriver/bridge/driver.go index 2f94f055b..8e28a710f 100644 --- a/daemon/networkdriver/bridge/driver.go +++ b/daemon/networkdriver/bridge/driver.go @@ -1,10 +1,13 @@ package bridge import ( + "encoding/hex" + "errors" "fmt" "io/ioutil" "net" "os" + "strings" "sync" log "github.com/Sirupsen/logrus" @@ -27,6 +30,7 @@ const ( // Network interface represents the networking stack of a container type networkInterface struct { IP net.IP + IPv6 net.IP PortMappings []net.Addr // there are mappings to the host interfaces } @@ -69,8 +73,10 @@ var ( "192.168.44.1/24", } - bridgeIface string - bridgeNetwork *net.IPNet + bridgeIface string + bridgeIPv4Network *net.IPNet + bridgeIPv6Addr net.IP + globalIPv6Network *net.IPNet defaultBindingIP = net.ParseIP("0.0.0.0") currentInterfaces = ifaces{c: make(map[string]*networkInterface)} @@ -78,13 +84,19 @@ var ( func InitDriver(job *engine.Job) engine.Status { var ( - network *net.IPNet + networkv4 *net.IPNet + networkv6 *net.IPNet + addrv4 net.Addr + addrsv6 []net.Addr enableIPTables = job.GetenvBool("EnableIptables") + enableIPv6 = job.GetenvBool("EnableIPv6") icc = job.GetenvBool("InterContainerCommunication") ipMasq = job.GetenvBool("EnableIpMasq") ipForward = job.GetenvBool("EnableIpForward") bridgeIP = job.Getenv("BridgeIP") + bridgeIPv6 = "fe80::1/64" fixedCIDR = job.Getenv("FixedCIDR") + fixedCIDRv6 = job.Getenv("FixedCIDRv6") ) if defaultIP := job.Getenv("DefaultBindingIP"); defaultIP != "" { @@ -98,41 +110,83 @@ func InitDriver(job *engine.Job) engine.Status { bridgeIface = DefaultNetworkBridge } - addr, err := networkdriver.GetIfaceAddr(bridgeIface) + addrv4, addrsv6, err := networkdriver.GetIfaceAddr(bridgeIface) + if err != nil { + // No Bridge existent. Create one // If we're not using the default bridge, fail without trying to create it if !usingDefaultBridge { return job.Error(err) } - // If the bridge interface is not found (or has no address), try to create it and/or add an address - if err := configureBridge(bridgeIP); err != nil { + + // If the iface is not found, try to create it + if err := configureBridge(bridgeIP, bridgeIPv6, enableIPv6); err != nil { return job.Error(err) } - addr, err = networkdriver.GetIfaceAddr(bridgeIface) + addrv4, addrsv6, err = networkdriver.GetIfaceAddr(bridgeIface) if err != nil { return job.Error(err) } - network = addr.(*net.IPNet) + + if fixedCIDRv6 != "" { + // Setting route to global IPv6 subnet + log.Infof("Adding route to IPv6 network %q via device %q", fixedCIDRv6, bridgeIface) + if err := netlink.AddRoute(fixedCIDRv6, "", "", bridgeIface); err != nil { + log.Fatalf("Could not add route to IPv6 network %q via device %q", fixedCIDRv6, bridgeIface) + } + } } else { - network = addr.(*net.IPNet) + // Bridge exists already. Getting info... // validate that the bridge ip matches the ip specified by BridgeIP if bridgeIP != "" { + networkv4 = addrv4.(*net.IPNet) bip, _, err := net.ParseCIDR(bridgeIP) if err != nil { return job.Error(err) } - if !network.IP.Equal(bip) { - return job.Errorf("bridge ip (%s) does not match existing bridge configuration %s", network.IP, bip) + if !networkv4.IP.Equal(bip) { + return job.Errorf("bridge ip (%s) does not match existing bridge configuration %s", networkv4.IP, bip) } } + + // TODO: Check if route to fixedCIDRv6 is set + } + + if enableIPv6 { + bip6, _, err := net.ParseCIDR(bridgeIPv6) + if err != nil { + return job.Error(err) + } + found := false + for _, addrv6 := range addrsv6 { + networkv6 = addrv6.(*net.IPNet) + if networkv6.IP.Equal(bip6) { + found = true + break + } + } + if !found { + return job.Errorf("bridge IPv6 does not match existing bridge configuration %s", bip6) + } + } + + networkv4 = addrv4.(*net.IPNet) + + log.Infof("enableIPv6 = %t", enableIPv6) + if enableIPv6 { + if len(addrsv6) == 0 { + return job.Error(errors.New("IPv6 enabled but no IPv6 detected")) + } + bridgeIPv6Addr = networkv6.IP } // Configure iptables for link support if enableIPTables { - if err := setupIPTables(addr, icc, ipMasq); err != nil { + if err := setupIPTables(addrv4, icc, ipMasq); err != nil { return job.Error(err) } + } if ipForward { @@ -140,6 +194,16 @@ func InitDriver(job *engine.Job) engine.Status { if err := ioutil.WriteFile("/proc/sys/net/ipv4/ip_forward", []byte{'1', '\n'}, 0644); err != nil { job.Logf("WARNING: unable to enable IPv4 forwarding: %s\n", err) } + + if fixedCIDRv6 != "" { + // Enable IPv6 forwarding + if err := ioutil.WriteFile("/proc/sys/net/ipv6/conf/default/forwarding", []byte{'1', '\n'}, 0644); err != nil { + job.Logf("WARNING: unable to enable IPv6 default forwarding: %s\n", err) + } + if err := ioutil.WriteFile("/proc/sys/net/ipv6/conf/all/forwarding", []byte{'1', '\n'}, 0644); err != nil { + job.Logf("WARNING: unable to enable IPv6 all forwarding: %s\n", err) + } + } } // We can always try removing the iptables @@ -159,23 +223,35 @@ func InitDriver(job *engine.Job) engine.Status { portmapper.SetIptablesChain(chain) } - bridgeNetwork = network + bridgeIPv4Network = networkv4 if fixedCIDR != "" { _, subnet, err := net.ParseCIDR(fixedCIDR) if err != nil { return job.Error(err) } log.Debugf("Subnet: %v", subnet) - if err := ipallocator.RegisterSubnet(bridgeNetwork, subnet); err != nil { + if err := ipallocator.RegisterSubnet(bridgeIPv4Network, subnet); err != nil { return job.Error(err) } } + if fixedCIDRv6 != "" { + _, subnet, err := net.ParseCIDR(fixedCIDRv6) + if err != nil { + return job.Error(err) + } + log.Debugf("Subnet: %v", subnet) + if err := ipallocator.RegisterSubnet(subnet, subnet); err != nil { + return job.Error(err) + } + globalIPv6Network = subnet + } + // Block BridgeIP in IP allocator - ipallocator.RequestIP(bridgeNetwork, bridgeNetwork.IP) + ipallocator.RequestIP(bridgeIPv4Network, bridgeIPv4Network.IP) // https://github.com/docker/docker/issues/2768 - job.Eng.Hack_SetGlobalVar("httpapi.bridgeIP", bridgeNetwork.IP) + job.Eng.Hack_SetGlobalVar("httpapi.bridgeIP", bridgeIPv4Network.IP) for name, f := range map[string]engine.Handler{ "allocate_interface": Allocate, @@ -263,7 +339,7 @@ func setupIPTables(addr net.Addr, icc, ipmasq bool) error { // If the bridge `bridgeIface` already exists, it will only perform the IP address association with the existing // bridge (fixes issue #8444) // If an address which doesn't conflict with existing interfaces can't be found, an error is returned. -func configureBridge(bridgeIP string) error { +func configureBridge(bridgeIP string, bridgeIPv6 string, enableIPv6 bool) error { nameservers := []string{} resolvConf, _ := resolvconf.Get() // we don't check for an error here, because we don't really care @@ -323,6 +399,25 @@ func configureBridge(bridgeIP string) error { if netlink.NetworkLinkAddIp(iface, ipAddr, ipNet); err != nil { return fmt.Errorf("Unable to add private network: %s", err) } + + if enableIPv6 { + // Enable IPv6 on the bridge + procFile := "/proc/sys/net/ipv6/conf/" + iface.Name + "/disable_ipv6" + if err := ioutil.WriteFile(procFile, []byte{'0', '\n'}, 0644); err != nil { + return fmt.Errorf("unable to enable IPv6 addresses on bridge: %s\n", err) + } + + ipAddr6, ipNet6, err := net.ParseCIDR(bridgeIPv6) + if err != nil { + log.Errorf("BridgeIPv6 parsing failed") + return err + } + + if netlink.NetworkLinkAddIp(iface, ipAddr6, ipNet6); err != nil { + return fmt.Errorf("Unable to add private IPv6 network: %s", err) + } + } + if err := netlink.NetworkLinkUp(iface); err != nil { return fmt.Errorf("Unable to start network bridge: %s", err) } @@ -363,20 +458,34 @@ func generateMacAddr(ip net.IP) net.HardwareAddr { return hw } +func linkLocalIPv6FromMac(mac string) (string, error) { + hx := strings.Replace(mac, ":", "", -1) + hw, err := hex.DecodeString(hx) + if err != nil { + return "", errors.New("Could not parse MAC address " + mac) + } + + hw[0] ^= 0x2 + + return fmt.Sprintf("fe80::%x%x:%xff:fe%x:%x%x/64", hw[0], hw[1], hw[2], hw[3], hw[4], hw[5]), nil +} + // Allocate a network interface func Allocate(job *engine.Job) engine.Status { var ( - ip net.IP - mac net.HardwareAddr - err error - id = job.Args[0] - requestedIP = net.ParseIP(job.Getenv("RequestedIP")) + ip net.IP + mac net.HardwareAddr + err error + id = job.Args[0] + requestedIP = net.ParseIP(job.Getenv("RequestedIP")) + requestedIPv6 = net.ParseIP(job.Getenv("RequestedIPv6")) + globalIPv6 net.IP ) if requestedIP != nil { - ip, err = ipallocator.RequestIP(bridgeNetwork, requestedIP) + ip, err = ipallocator.RequestIP(bridgeIPv4Network, requestedIP) } else { - ip, err = ipallocator.RequestIP(bridgeNetwork, nil) + ip, err = ipallocator.RequestIP(bridgeIPv4Network, nil) } if err != nil { return job.Error(err) @@ -387,18 +496,53 @@ func Allocate(job *engine.Job) engine.Status { mac = generateMacAddr(ip) } + if globalIPv6Network != nil { + // if globalIPv6Network Size is at least a /80 subnet generate IPv6 address from MAC address + netmask_ones, _ := globalIPv6Network.Mask.Size() + if requestedIPv6 == nil && netmask_ones <= 80 { + requestedIPv6 = globalIPv6Network.IP + for i, h := range mac { + requestedIPv6[i+10] = h + } + } + + globalIPv6, err = ipallocator.RequestIP(globalIPv6Network, requestedIPv6) + if err != nil { + log.Errorf("Allocator: RequestIP v6: %s", err.Error()) + return job.Error(err) + } + log.Infof("Allocated IPv6 %s", globalIPv6) + } + out := engine.Env{} out.Set("IP", ip.String()) - out.Set("Mask", bridgeNetwork.Mask.String()) - out.Set("Gateway", bridgeNetwork.IP.String()) + out.Set("Mask", bridgeIPv4Network.Mask.String()) + out.Set("Gateway", bridgeIPv4Network.IP.String()) out.Set("MacAddress", mac.String()) out.Set("Bridge", bridgeIface) - size, _ := bridgeNetwork.Mask.Size() + size, _ := bridgeIPv4Network.Mask.Size() out.SetInt("IPPrefixLen", size) + // if linklocal IPv6 + localIPv6Net, err := linkLocalIPv6FromMac(mac.String()) + if err != nil { + return job.Error(err) + } + localIPv6, _, _ := net.ParseCIDR(localIPv6Net) + out.Set("LinkLocalIPv6", localIPv6.String()) + out.Set("MacAddress", mac.String()) + + if globalIPv6Network != nil { + out.Set("GlobalIPv6", globalIPv6.String()) + sizev6, _ := globalIPv6Network.Mask.Size() + out.SetInt("GlobalIPv6PrefixLen", sizev6) + out.Set("IPv6Gateway", bridgeIPv6Addr.String()) + } + currentInterfaces.Set(id, &networkInterface{ - IP: ip, + IP: ip, + IPv6: globalIPv6, }) out.WriteTo(job.Stdout) @@ -423,8 +567,13 @@ func Release(job *engine.Job) engine.Status { } } - if err := ipallocator.ReleaseIP(bridgeNetwork, containerInterface.IP); err != nil { - log.Infof("Unable to release ip %s", err) + if err := ipallocator.ReleaseIP(bridgeIPv4Network, containerInterface.IP); err != nil { + log.Infof("Unable to release IPv4 %s", err) + } + if globalIPv6Network != nil { + if err := ipallocator.ReleaseIP(globalIPv6Network, containerInterface.IPv6); err != nil { + log.Infof("Unable to release IPv6 %s", err) + } } return engine.StatusOK } diff --git a/daemon/networkdriver/utils.go b/daemon/networkdriver/utils.go index 07d95445a..833744b57 100644 --- a/daemon/networkdriver/utils.go +++ b/daemon/networkdriver/utils.go @@ -44,11 +44,13 @@ func CheckRouteOverlaps(toCheck *net.IPNet) error { // Detects overlap between one IPNet and another func NetworkOverlaps(netX *net.IPNet, netY *net.IPNet) bool { - if firstIP, _ := NetworkRange(netX); netY.Contains(firstIP) { - return true - } - if firstIP, _ := NetworkRange(netY); netX.Contains(firstIP) { - return true + if len(netX.IP) == len(netY.IP) { + if firstIP, _ := NetworkRange(netX); netY.Contains(firstIP) { + return true + } + if firstIP, _ := NetworkRange(netY); netX.Contains(firstIP) { + return true + } } return false } @@ -73,30 +75,33 @@ func NetworkRange(network *net.IPNet) (net.IP, net.IP) { } // Return the IPv4 address of a network interface -func GetIfaceAddr(name string) (net.Addr, error) { +func GetIfaceAddr(name string) (net.Addr, []net.Addr, error) { iface, err := net.InterfaceByName(name) if err != nil { - return nil, err + return nil, nil, err } addrs, err := iface.Addrs() if err != nil { - return nil, err + return nil, nil, err } var addrs4 []net.Addr + var addrs6 []net.Addr for _, addr := range addrs { ip := (addr.(*net.IPNet)).IP if ip4 := ip.To4(); ip4 != nil { addrs4 = append(addrs4, addr) + } else if ip6 := ip.To16(); len(ip6) == net.IPv6len { + addrs6 = append(addrs6, addr) } } switch { case len(addrs4) == 0: - return nil, fmt.Errorf("Interface %v has no IP addresses", name) + return nil, nil, fmt.Errorf("Interface %v has no IPv4 addresses", name) case len(addrs4) > 1: fmt.Printf("Interface %v has more than 1 IPv4 address. Defaulting to using %v\n", name, (addrs4[0].(*net.IPNet)).IP) } - return addrs4[0], nil + return addrs4[0], addrs6, nil } func GetDefaultRouteIface() (*net.Interface, error) { diff --git a/docs/man/docker.1.md b/docs/man/docker.1.md index f3bcdb671..bc66436da 100644 --- a/docs/man/docker.1.md +++ b/docs/man/docker.1.md @@ -52,9 +52,11 @@ unix://[/path/to/socket] to use. **-g**="" Path to use as the root of the Docker runtime. Default is `/var/lib/docker`. - **--fixed-cidr**="" - IPv4 subnet for fixed IPs (ex: 10.20.0.0/16); this subnet must be nested in the bridge subnet (which is defined by \-b or \-\-bip) + IPv4 subnet for fixed IPs (e.g., 10.20.0.0/16); this subnet must be nested in the bridge subnet (which is defined by \-b or \-\-bip) + +**--fixed-cidr-v6**="" + IPv6 subnet for global IPv6 addresses (e.g., 2a00:1450::/64) **--icc**=*true*|*false* Allow unrestricted inter\-container and Docker daemon host communication. If disabled, containers can still be linked together using **--link** option (see **docker-run(1)**). Default is true. @@ -62,12 +64,18 @@ unix://[/path/to/socket] to use. **--ip**="" Default IP address to use when binding container ports. Default is `0.0.0.0`. +**--ip-forward**=*true*|*false* + Docker will enable IP forwarding. Default is true. If `--fixed-cidr-v6` is set. IPv6 forwarding will be activated, too. This may reject Router Advertisements and interfere with the host's existing IPv6 configuration. For more information please consult the documentation about "Advanced Networking - IPv6". + **--ip-masq**=*true*|*false* Enable IP masquerading for bridge's IP range. Default is true. **--iptables**=*true*|*false* Disable Docker's addition of iptables rules. Default is true. +**--ipv6**=*true*|*false* + Enable IPv6 support. Default is false. Docker will create an IPv6-enabled bridge with address fe80::1 which will allow you to create IPv6-enabled containers. Use together with `--fixed-cidr-v6` to provide globally routable IPv6 addresses. IPv6 forwarding will be enabled if not used with `--ip-forward=false`. This may collide with your host's current IPv6 settings. For more information please consult the documentation about "Advanced Networking - IPv6". + **-l**, **--log-level**="*debug*|*info*|*error*|*fatal*"" Set the logging level. Default is `info`. diff --git a/docs/sources/article-img/ipv6_basic_host_config.gliffy b/docs/sources/article-img/ipv6_basic_host_config.gliffy new file mode 100644 index 000000000..f28c3f6f9 --- /dev/null +++ b/docs/sources/article-img/ipv6_basic_host_config.gliffy @@ -0,0 +1 @@ +{"contentType":"application/gliffy+json","version":"1.3","stage":{"background":"#FFFFFF","width":420,"height":127,"nodeIndex":173,"autoFit":true,"exportBorder":false,"gridOn":false,"snapToGrid":false,"drawingGuidesOn":false,"pageBreaksOn":false,"printGridOn":false,"printPaper":"LETTER","printShrinkToFit":false,"printPortrait":true,"maxWidth":5000,"maxHeight":5000,"themeData":null,"viewportType":"default","fitBB":{"min":{"x":8.5,"y":0.5},"max":{"x":419.75,"y":126.5}},"objects":[{"x":6.5,"y":106.0,"rotation":0.0,"id":9,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":20,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

docker0 fe80::1/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":19.5,"y":8.0,"rotation":0.0,"id":7,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":19,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth0 2001:db8:0:1::1/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":31.5,"y":23.5,"rotation":0.0,"id":4,"width":100.0,"height":75.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":16,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#a4c2f4","gradient":true,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":5,"width":96.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Host2

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[]}]},{"x":11.75,"y":0.5,"rotation":0.0,"id":60,"width":402.0,"height":126.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":2,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#FFFFFF","gradient":false,"dashStyle":"2,2","dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[]},{"x":146.5,"y":82.0,"rotation":0.0,"id":164,"width":249.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":44,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

route -A inet6 2001:db8:0:2::/64 dev docker0

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":146.5,"y":27.5,"rotation":0.0,"id":73,"width":249.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":35,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

route -A inet6 default gw fe80::1 dev eth0

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]}],"shapeStyles":{"com.gliffy.shape.basic.basic_v1.default":{"fill":"#fff2cc","stroke":"#333333","strokeWidth":2,"dashStyle":"2.0,2.0","gradient":true,"shadow":true}},"lineStyles":{"global":{"stroke":"#d9d9d9"}},"textStyles":{"global":{"italic":false,"size":"12px","color":"#b7b7b7"}}},"metadata":{"title":"untitled","revision":0,"exportBorder":false,"loadPosition":"default","libraries":["com.gliffy.libraries.basic.basic_v1.default","com.gliffy.libraries.flowchart.flowchart_v1.default","com.gliffy.libraries.swimlanes.swimlanes_v1.default","com.gliffy.libraries.uml.uml_v2.class","com.gliffy.libraries.uml.uml_v2.sequence","com.gliffy.libraries.uml.uml_v2.activity","com.gliffy.libraries.erd.erd_v1.default","com.gliffy.libraries.ui.ui_v3.containers_content","com.gliffy.libraries.ui.ui_v3.forms_controls","com.gliffy.libraries.images"],"autosaveDisabled":false},"embeddedResources":{"index":0,"resources":[]}} \ No newline at end of file diff --git a/docs/sources/article-img/ipv6_basic_host_config.svg b/docs/sources/article-img/ipv6_basic_host_config.svg new file mode 100644 index 000000000..b5f9eeebe --- /dev/null +++ b/docs/sources/article-img/ipv6_basic_host_config.svg @@ -0,0 +1 @@ +Host2eth0 2001:db8:0:1::1/64docker0 fe80::1/64route -A inet6 default gw fe80::1 dev eth0route -A inet6 2001:db8:0:2::/64 dev docker0 \ No newline at end of file diff --git a/docs/sources/article-img/ipv6_routed_network_example.gliffy b/docs/sources/article-img/ipv6_routed_network_example.gliffy new file mode 100644 index 000000000..aea452b62 --- /dev/null +++ b/docs/sources/article-img/ipv6_routed_network_example.gliffy @@ -0,0 +1 @@ +{"contentType":"application/gliffy+json","version":"1.3","stage":{"background":"#FFFFFF","width":757,"height":503,"nodeIndex":174,"autoFit":true,"exportBorder":false,"gridOn":false,"snapToGrid":false,"drawingGuidesOn":true,"pageBreaksOn":false,"printGridOn":false,"printPaper":"LETTER","printShrinkToFit":false,"printPortrait":true,"maxWidth":5000,"maxHeight":5000,"themeData":null,"viewportType":"default","fitBB":{"min":{"x":-9.000680271168676,"y":-4.75},"max":{"x":756.0183424505415,"y":502.5}},"objects":[{"x":765.0,"y":250.0,"rotation":0.0,"id":169,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":47,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":1.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-12.982306425886122,0.0],[-41.25,0.0]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":663.0,"y":362.5,"rotation":270.0,"id":168,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":46,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

managed by Docker

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":747.0,"y":472.0,"rotation":0.0,"id":166,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":45,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":1.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":2,"endArrow":2,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[0.0,14.008510484195028],[0.0,-221.0]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":25.5,"y":254.0,"rotation":0.0,"id":162,"width":194.49999999999997,"height":28.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":43,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

route -A inet6 2001:db8:1:1::/64 \\

    dev docker0

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":239.28932188134524,"y":150.0,"rotation":0.0,"id":32,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":8,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":4,"py":0.0,"px":0.2928932188134524}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":0,"py":1.0,"px":0.7071067811865476}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#cccccc","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[196.5,47.5],[151.9213562373095,-37.0]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":195.0,"y":261.5,"rotation":0.0,"id":35,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":11,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":2,"py":0.9999999999999998,"px":0.29289321881345254}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":13,"py":0.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#cccccc","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[66.28932188134524,11.0],[-92.0,91.5]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":182.0,"y":272.5,"rotation":0.0,"id":34,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":10,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":2,"py":1.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":15,"py":0.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#cccccc","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[100.0,0.0],[82.0,80.5]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":11.5,"y":463.0,"rotation":0.0,"id":53,"width":346.49999999999994,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":33,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

route -A inet6 default gw fe80::1 dev eth0

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":11.5,"y":323.5,"rotation":0.0,"id":56,"width":346.49999999999994,"height":163.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":5,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#FFFFFF","gradient":false,"dashStyle":"2,2","dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[]},{"x":245.0,"y":109.0,"rotation":0.0,"id":33,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":9,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":0,"py":0.9999999999999998,"px":0.29289321881345254}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":2,"py":0.0,"px":0.7071067811865476}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#cccccc","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[104.78932188134524,3.999999999999986],[57.710678118654755,88.5]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":76.5,"y":141.5,"rotation":0.0,"id":31,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":7,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":4,"py":1.0,"px":0.7071067811865476}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":25,"py":0.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#cccccc","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[400.71067811865476,131.0],[560.0,211.5]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":37.5,"y":145.5,"rotation":0.0,"id":30,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":6,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":4,"py":1.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":27,"py":0.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#cccccc","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[419.0,127.0],[431.0,207.5]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":296.0,"y":21.0,"rotation":0.0,"id":87,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":41,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth0 2001:db8::1/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":293.0,"y":120.0,"rotation":0.0,"id":83,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":40,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth1 fe80::1/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":433.5,"y":42.5,"rotation":0.0,"id":82,"width":291.0,"height":70.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":39,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

route -A inet6 default gw fe80::1 dev eth0

 

 

route -A inet6 2001:db8:1::/48 gw fe80::1:1 dev eth1

route -A inet6 2001:db8:2::/48 gw fe80::2:1 dev eth1

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":320.5,"y":38.0,"rotation":0.0,"id":0,"width":100.0,"height":75.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":12,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#fff2cc","gradient":true,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":1,"width":96.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Router

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[]}]},{"x":369.0,"y":40.0,"rotation":0.0,"id":89,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":1,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":0,"py":0.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#d9d9d9","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":10.0,"controlPath":[[1.5,-2.0],[1.5,-21.125],[1.5,-21.125],[1.5,-40.25]],"lockSegments":{},"ortho":true}},"linkMap":[],"children":[]},{"x":297.75,"y":10.5,"rotation":0.0,"id":80,"width":425.99999999999994,"height":133.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":0,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#FFFFFF","gradient":false,"dashStyle":"2,2","dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[]},{"x":528.5,"y":197.5,"rotation":0.0,"id":73,"width":195.25,"height":28.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":35,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

route -A inet6 default gw fe80::1 \\

    dev eth0

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":793.0,"y":250.0,"rotation":0.0,"id":64,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":34,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":60,"py":0.6205673758865248,"px":1.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":1.0,"strokeColor":"#000000","fillColor":"none","dashStyle":"8.0,8.0","startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-69.25,0.0],[-798.0006802711687,-3.410605131648481E-13]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":25.5,"y":199.5,"rotation":0.0,"id":47,"width":291.0,"height":28.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":31,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

route -A inet6 default gw fe80::1 \\

   dev eth0 

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":207.0,"y":281.0,"rotation":0.0,"id":11,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":21,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

docker0 fe80::1/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":220.0,"y":168.0,"rotation":0.0,"id":6,"width":150.0,"height":28.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":18,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth0 2001:db8:1:0::1/64

        fe80::1:1/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":232.0,"y":197.5,"rotation":0.0,"id":2,"width":100.0,"height":75.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":14,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#a4c2f4","gradient":true,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":3,"width":96.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Host1

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[]}]},{"x":11.5,"y":162.5,"rotation":0.0,"id":59,"width":346.50000000000006,"height":141.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":3,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#FFFFFF","gradient":false,"dashStyle":"2,2","dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[]},{"x":384.75,"y":162.5,"rotation":0.0,"id":60,"width":339.0,"height":141.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":2,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#FFFFFF","gradient":false,"dashStyle":"2,2","dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[]},{"x":189.0,"y":336.0,"rotation":0.0,"id":74,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":36,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth0 2001:db8:1:1::2/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":28.000000000000014,"y":336.0,"rotation":0.0,"id":19,"width":149.99999999999997,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":26,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth0 2001:db8:1:1::1/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":214.0,"y":353.0,"rotation":0.0,"id":15,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.square","order":24,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#ead1dc","gradient":true,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":16,"width":96.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Container1-2

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[]}]},{"x":53.0,"y":353.0,"rotation":0.0,"id":13,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.square","order":22,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#ead1dc","gradient":true,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":14,"width":96.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Container1-1

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[]}]},{"x":395.0,"y":336.0,"rotation":0.0,"id":77,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":37,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth0 2001:db8:2:1::1/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":384.75,"y":323.5,"rotation":0.0,"id":58,"width":339.75,"height":163.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":4,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#FFFFFF","gradient":false,"dashStyle":"2,2","dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[]},{"x":384.75,"y":462.0,"rotation":0.0,"id":51,"width":339.75,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":32,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

route -A inet6 default gw fe80::1 dev eth0

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":418.5,"y":353.0,"rotation":0.0,"id":27,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.square","order":27,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#ead1dc","gradient":true,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":28,"width":96.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Container2-1

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[]}]},{"x":563.0,"y":336.0,"rotation":0.0,"id":78,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":38,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth0 2001:db8:2:1::2/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":586.5,"y":353.0,"rotation":0.0,"id":25,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.square","order":29,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#ead1dc","gradient":true,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":26,"width":96.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Container2-2

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[]}]},{"x":259.0,"y":491.5,"rotation":0.0,"id":107,"width":223.00000000000003,"height":11.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":42,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

containers' link-local addresses are not displayed

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":394.5,"y":168.0,"rotation":0.0,"id":7,"width":150.0,"height":28.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":19,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth0 2001:db8:2:0::1/64
        fe80::2:1/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":381.5,"y":280.0,"rotation":0.0,"id":9,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":20,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

docker0 fe80::1/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":406.5,"y":197.5,"rotation":0.0,"id":4,"width":100.0,"height":75.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":16,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#a4c2f4","gradient":true,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":5,"width":96.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Host2

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[]}]},{"x":528.5,"y":252.0,"rotation":0.0,"id":164,"width":194.49999999999997,"height":28.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":44,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

route -A inet6 2001:db8:2:1::/64 \\

    dev docker0

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":766.0,"y":487.0,"rotation":0.0,"id":171,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":48,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":1.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-13.981657549458532,0.0],[-41.25,0.0]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]}],"shapeStyles":{"com.gliffy.shape.basic.basic_v1.default":{"fill":"#fff2cc","stroke":"#333333","strokeWidth":2,"dashStyle":"2.0,2.0","gradient":true,"shadow":true}},"lineStyles":{"global":{"stroke":"#000000","strokeWidth":1}},"textStyles":{"global":{"size":"12px"}}},"metadata":{"title":"untitled","revision":0,"exportBorder":false,"loadPosition":"default","libraries":["com.gliffy.libraries.basic.basic_v1.default","com.gliffy.libraries.flowchart.flowchart_v1.default","com.gliffy.libraries.swimlanes.swimlanes_v1.default","com.gliffy.libraries.uml.uml_v2.class","com.gliffy.libraries.uml.uml_v2.sequence","com.gliffy.libraries.uml.uml_v2.activity","com.gliffy.libraries.erd.erd_v1.default","com.gliffy.libraries.ui.ui_v3.containers_content","com.gliffy.libraries.ui.ui_v3.forms_controls","com.gliffy.libraries.images"],"autosaveDisabled":false},"embeddedResources":{"index":0,"resources":[]}} \ No newline at end of file diff --git a/docs/sources/article-img/ipv6_routed_network_example.svg b/docs/sources/article-img/ipv6_routed_network_example.svg new file mode 100644 index 000000000..7050657f9 --- /dev/null +++ b/docs/sources/article-img/ipv6_routed_network_example.svg @@ -0,0 +1 @@ +RouterHost1Host2eth0 2001:db8:1:0::1/64        fe80::1:1/64eth0 2001:db8:2:0::1/64        fe80::2:1/64docker0 fe80::1/64docker0 fe80::1/64Container1-1Container1-2eth0 2001:db8:1:1::1/64Container2-1Container2-2route -A inet6 default gw fe80::1 \   dev eth0 route -A inet6 default gw fe80::1 dev eth0route -A inet6 default gw fe80::1 dev eth0route -A inet6 default gw fe80::1 \    dev eth0eth0 2001:db8:1:1::2/64eth0 2001:db8:2:1::1/64eth0 2001:db8:2:1::2/64route -A inet6 default gw fe80::1 dev eth0  route -A inet6 2001:db8:1::/48 gw fe80::1:1 dev eth1route -A inet6 2001:db8:2::/48 gw fe80::2:1 dev eth1eth1 fe80::1/64eth0 2001:db8::1/64containers' link-local addresses are not displayedroute -A inet6 2001:db8:1:1::/64 \    dev docker0route -A inet6 2001:db8:2:1::/64 \    dev docker0managed by Docker \ No newline at end of file diff --git a/docs/sources/article-img/ipv6_slash64_subnet_config.gliffy b/docs/sources/article-img/ipv6_slash64_subnet_config.gliffy new file mode 100644 index 000000000..efafc02ef --- /dev/null +++ b/docs/sources/article-img/ipv6_slash64_subnet_config.gliffy @@ -0,0 +1 @@ +{"contentType":"application/gliffy+json","version":"1.3","stage":{"background":"#FFFFFF","width":550,"height":341,"nodeIndex":88,"autoFit":true,"exportBorder":false,"gridOn":false,"snapToGrid":false,"drawingGuidesOn":false,"pageBreaksOn":false,"printGridOn":false,"printPaper":"LETTER","printShrinkToFit":false,"printPortrait":true,"maxWidth":5000,"maxHeight":5000,"themeData":null,"viewportType":"default","fitBB":{"min":{"x":2.5,"y":2.5},"max":{"x":550,"y":341}},"objects":[{"x":10.5,"y":53.5,"rotation":0.0,"id":74,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":26,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

fe80::1/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":37.0,"y":2.5,"rotation":0.0,"id":72,"width":100.0,"height":46.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":24,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#d9d9d9","gradient":true,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":73,"width":96.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Router

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[]}]},{"x":89.5,"y":83.5,"rotation":0.0,"id":59,"width":150.0,"height":28.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":17,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Routed Network:
2001:db8::/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":313.0,"y":313.0,"rotation":0.0,"id":39,"width":235.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":16,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

route -A inet6 default gw fe80::1 dev eth0

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":352.0,"y":185.5,"rotation":0.0,"id":36,"width":169.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":15,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth0 2001:db8:0:0:1::2/80

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":351.0,"y":49.5,"rotation":0.0,"id":29,"width":171.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":14,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth0 2001:db8:0:0:1::1/80

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":382.1250000000001,"y":202.5,"rotation":0.0,"id":30,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.square","order":12,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#ead1dc","gradient":true,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":31,"width":96.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

container1-2

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[]}]},{"x":382.0,"y":65.5,"rotation":0.0,"id":32,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.square","order":10,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#ead1dc","gradient":true,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":33,"width":96.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

container1-1

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[]}]},{"x":15.125000000000057,"y":261.0,"rotation":0.0,"id":20,"width":273.0,"height":28.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":9,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

route -A inet6 default gw fe80::1 dev eth0

route -A inet6 2001:db8:0:0:1::/80 dev docker0

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":120.0,"y":178.5,"rotation":0.0,"id":21,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":8,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

docker0 fe80::1/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":13.0,"y":132.5,"rotation":0.0,"id":22,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":7,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth0 2001:db8::1/80

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":38.0,"y":149.0,"rotation":0.0,"id":23,"width":100.0,"height":75.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":5,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#a4c2f4","gradient":true,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":24,"width":96.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

host1

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[]}]},{"x":-118.0,"y":123.0,"rotation":0.0,"id":44,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":4,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":23,"py":0.7071067811865475,"px":0.9999999999999998}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":30,"py":0.5,"px":0.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#cccccc","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[255.99999999999997,79.03300858899107],[500.1250000000001,129.5]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":-138.0,"y":129.0,"rotation":0.0,"id":43,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":3,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":23,"py":0.29289321881345237,"px":1.0}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":32,"py":0.5,"px":0.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#cccccc","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[276.0,41.966991411008934],[520.0,-13.5]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":313.0,"y":40.0,"rotation":0.0,"id":34,"width":237.00000000000003,"height":301.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":2,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#FFFFFF","gradient":false,"dashStyle":"2,2","dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[]},{"x":87.0,"y":150.0,"rotation":0.0,"id":58,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":1,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":23,"py":0.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":72,"py":1.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#cccccc","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[1.0,-1.0],[0.0,-101.5]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":2.5,"y":118.50000000000001,"rotation":0.0,"id":25,"width":292.0,"height":178.99999999999997,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":0,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#FFFFFF","gradient":false,"dashStyle":"2,2","dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[]}],"shapeStyles":{},"lineStyles":{"global":{"stroke":"#cccccc"}},"textStyles":{"global":{"bold":true,"italic":true}}},"metadata":{"title":"untitled","revision":0,"exportBorder":false,"loadPosition":"default","libraries":["com.gliffy.libraries.basic.basic_v1.default","com.gliffy.libraries.flowchart.flowchart_v1.default","com.gliffy.libraries.swimlanes.swimlanes_v1.default","com.gliffy.libraries.uml.uml_v1.default","com.gliffy.libraries.erd.erd_v1.default","com.gliffy.libraries.ui.ui_v2.forms_components","com.gliffy.libraries.network.network_v3.home","com.gliffy.libraries.images"],"autosaveDisabled":false},"embeddedResources":{"index":0,"resources":[]}} \ No newline at end of file diff --git a/docs/sources/article-img/ipv6_slash64_subnet_config.svg b/docs/sources/article-img/ipv6_slash64_subnet_config.svg new file mode 100644 index 000000000..c731eddd3 --- /dev/null +++ b/docs/sources/article-img/ipv6_slash64_subnet_config.svg @@ -0,0 +1 @@ +host1eth0 2001:db8::1/80docker0 fe80::1/64route -A inet6 default gw fe80::1 dev eth0route -A inet6 2001:db8:0:0:1::/80 dev docker0container1-1container1-2eth0 2001:db8:0:0:1::1/80eth0 2001:db8:0:0:1::2/80route -A inet6 default gw fe80::1 dev eth0Routed Network:2001:db8::/64Routerfe80::1/64 \ No newline at end of file diff --git a/docs/sources/article-img/ipv6_switched_network_example.gliffy b/docs/sources/article-img/ipv6_switched_network_example.gliffy new file mode 100644 index 000000000..7f2b73f18 --- /dev/null +++ b/docs/sources/article-img/ipv6_switched_network_example.gliffy @@ -0,0 +1 @@ +{"contentType":"application/gliffy+json","version":"1.3","stage":{"background":"#FFFFFF","width":748,"height":448,"nodeIndex":182,"autoFit":true,"exportBorder":false,"gridOn":false,"snapToGrid":false,"drawingGuidesOn":true,"pageBreaksOn":false,"printGridOn":false,"printPaper":"LETTER","printShrinkToFit":false,"printPortrait":true,"maxWidth":5000,"maxHeight":5000,"themeData":null,"viewportType":"default","fitBB":{"min":{"x":-17.000680271168676,"y":5},"max":{"x":747.7683424505416,"y":447.5}},"objects":[{"x":17.5,"y":202.0,"rotation":0.0,"id":167,"width":204.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":38,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

route -A inet6 2001::/64 dev docker0

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":231.28932188134524,"y":95.0,"rotation":0.0,"id":120,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":6,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":161,"py":0.0,"px":0.2928932188134524}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":131,"py":1.0,"px":0.7071067811865476}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#cccccc","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[196.5,47.5],[151.9213562373095,-15.0]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":187.0,"y":206.5,"rotation":0.0,"id":121,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":9,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":140,"py":0.9999999999999998,"px":0.29289321881345254}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":148,"py":0.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#cccccc","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[66.28932188134524,11.0],[-92.0,91.5]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":174.0,"y":217.5,"rotation":0.0,"id":122,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":8,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":140,"py":1.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":146,"py":0.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#cccccc","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[100.0,0.0],[82.0,80.5]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":3.5000000000000284,"y":408.0,"rotation":0.0,"id":123,"width":346.49999999999994,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":31,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

route -A inet6 default gw fe80::1 dev eth0

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":3.5000000000000284,"y":268.5,"rotation":0.0,"id":124,"width":346.49999999999994,"height":163.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":3,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#FFFFFF","gradient":false,"dashStyle":"2,2","dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[]},{"x":237.0,"y":54.0,"rotation":0.0,"id":125,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":7,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":131,"py":0.9999999999999998,"px":0.29289321881345254}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":140,"py":0.0,"px":0.7071067811865476}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#cccccc","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[104.78932188134524,25.999999999999986],[57.710678118654755,88.5]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":68.5,"y":86.5,"rotation":0.0,"id":126,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":5,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":161,"py":1.0,"px":0.7071067811865476}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":156,"py":0.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#cccccc","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[400.71067811865476,131.0],[560.0,211.5]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":29.5,"y":90.5,"rotation":0.0,"id":127,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":4,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":161,"py":1.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":153,"py":0.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#cccccc","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[419.0,127.0],[431.0,207.5]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":312.5,"y":5.0,"rotation":0.0,"id":131,"width":100.0,"height":75.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":10,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#e2e2e2","gradient":true,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":132,"width":96.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Level 2 Switch

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[]}]},{"x":785.0,"y":195.0,"rotation":0.0,"id":136,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":32,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":143,"py":0.6187943262411347,"px":1.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":1.0,"strokeColor":"#000000","fillColor":"none","dashStyle":"8.0,8.0","startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-69.25,-0.25],[-798.0006802711687,-3.410605131648481E-13]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":199.0,"y":224.0,"rotation":0.0,"id":138,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":19,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

docker0 fe80::1/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":229.0,"y":126.0,"rotation":0.0,"id":139,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":16,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth0 2000::1/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":224.0,"y":142.5,"rotation":0.0,"id":140,"width":100.0,"height":75.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":12,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#a4c2f4","gradient":true,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":141,"width":96.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Host1

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[]}]},{"x":3.4999999999999716,"y":107.5,"rotation":0.0,"id":142,"width":346.50000000000006,"height":141.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":1,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#FFFFFF","gradient":false,"dashStyle":"2,2","dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[]},{"x":376.75,"y":107.5,"rotation":0.0,"id":143,"width":339.0,"height":141.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":0,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#FFFFFF","gradient":false,"dashStyle":"2,2","dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[]},{"x":181.0,"y":281.0,"rotation":0.0,"id":144,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":34,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth0 2001::2/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":20.000000000000014,"y":281.0,"rotation":0.0,"id":145,"width":149.99999999999997,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":24,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth0 2001::1/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":206.0,"y":298.0,"rotation":0.0,"id":146,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.square","order":22,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#ead1dc","gradient":true,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":147,"width":96.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Container1-2

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[]}]},{"x":45.0,"y":298.0,"rotation":0.0,"id":148,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.square","order":20,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#ead1dc","gradient":true,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":149,"width":96.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Container1-1

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[]}]},{"x":387.0,"y":281.0,"rotation":0.0,"id":150,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":35,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth0 2002::1/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":376.75,"y":268.5,"rotation":0.0,"id":151,"width":339.75,"height":163.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":2,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#FFFFFF","gradient":false,"dashStyle":"2,2","dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[]},{"x":376.75,"y":407.0,"rotation":0.0,"id":152,"width":339.75,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":30,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

route -A inet6 default gw fe80::1 dev eth0

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":410.5,"y":298.0,"rotation":0.0,"id":153,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.square","order":25,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#ead1dc","gradient":true,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":154,"width":96.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Container2-1

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[]}]},{"x":555.0,"y":281.0,"rotation":0.0,"id":155,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":36,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth0 2002::2/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":578.5,"y":298.0,"rotation":0.0,"id":156,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.square","order":27,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#ead1dc","gradient":true,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":157,"width":96.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Container2-2

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[]}]},{"x":251.0,"y":436.5,"rotation":0.0,"id":158,"width":223.00000000000003,"height":11.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":37,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

containers' link-local addresses are not displayed

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":403.5,"y":126.0,"rotation":0.0,"id":159,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":17,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

eth0 2000::2/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":373.5,"y":223.0,"rotation":0.0,"id":160,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":18,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

docker0 fe80::1/64

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":398.5,"y":142.5,"rotation":0.0,"id":161,"width":100.0,"height":75.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":14,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#a4c2f4","gradient":true,"dashStyle":null,"dropShadow":true,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":162,"width":96.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Host2

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[]}]},{"x":17.5,"y":143.5,"rotation":0.0,"id":137,"width":291.0,"height":28.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":29,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

route -A inet6 2000::/64 dev eth0

route -A inet6 2002::/64 gw 2000::2

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":507.5,"y":144.0,"rotation":0.0,"id":135,"width":209.0,"height":28.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":33,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

route -A inet6 2000::/64 dev eth0

route -A inet6 2001::/64 gw 2000::1 

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":508.0,"y":195.0,"rotation":0.0,"id":168,"width":204.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":39,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

route -A inet6 2002::/64 dev docker0

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":756.7500000000001,"y":195.0,"rotation":0.0,"id":172,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":43,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":1.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-12.982306425886122,0.0],[-41.25,0.0]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":757.7500000000001,"y":432.0,"rotation":0.0,"id":171,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":42,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":1.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-13.981657549458532,0.0],[-41.25,0.0]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]},{"x":654.7500000000001,"y":307.5,"rotation":270.0,"id":173,"width":150.0,"height":14.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":41,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

managed by Docker

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[]},{"x":738.7500000000001,"y":417.0,"rotation":0.0,"id":174,"width":100.0,"height":100.0,"uid":"com.gliffy.shape.basic.basic_v1.default.line","order":40,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Line","Line":{"strokeWidth":1.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":2,"endArrow":2,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[0.0,14.008510484195028],[0.0,-221.0]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[]}],"shapeStyles":{"com.gliffy.shape.basic.basic_v1.default":{"fill":"#e2e2e2","stroke":"#333333","strokeWidth":2,"dashStyle":"2.0,2.0","gradient":true,"shadow":true}},"lineStyles":{"global":{"stroke":"#000000","dashStyle":"8.0,8.0","strokeWidth":1}},"textStyles":{}},"metadata":{"title":"untitled","revision":0,"exportBorder":false,"loadPosition":"default","libraries":["com.gliffy.libraries.basic.basic_v1.default","com.gliffy.libraries.flowchart.flowchart_v1.default","com.gliffy.libraries.swimlanes.swimlanes_v1.default","com.gliffy.libraries.uml.uml_v2.class","com.gliffy.libraries.uml.uml_v2.sequence","com.gliffy.libraries.uml.uml_v2.activity","com.gliffy.libraries.erd.erd_v1.default","com.gliffy.libraries.ui.ui_v3.containers_content","com.gliffy.libraries.ui.ui_v3.forms_controls","com.gliffy.libraries.images"],"autosaveDisabled":false},"embeddedResources":{"index":0,"resources":[]}} \ No newline at end of file diff --git a/docs/sources/article-img/ipv6_switched_network_example.svg b/docs/sources/article-img/ipv6_switched_network_example.svg new file mode 100644 index 000000000..5c391c24a --- /dev/null +++ b/docs/sources/article-img/ipv6_switched_network_example.svg @@ -0,0 +1 @@ +Level 2 SwitchHost1Host2eth0 2000::1/64eth0 2000::2/64docker0 fe80::1/64docker0 fe80::1/64Container1-1Container1-2eth0 2001::1/64Container2-1Container2-2route -A inet6 2000::/64 dev eth0route -A inet6 2002::/64 gw 2000::2route -A inet6 default gw fe80::1 dev eth0route -A inet6 default gw fe80::1 dev eth0route -A inet6 2000::/64 dev eth0route -A inet6 2001::/64 gw 2000::1 eth0 2001::2/64eth0 2002::1/64eth0 2002::2/64containers' link-local addresses are not displayedroute -A inet6 2001::/64 dev docker0route -A inet6 2002::/64 dev docker0managed by Docker \ No newline at end of file diff --git a/docs/sources/articles/networking.md b/docs/sources/articles/networking.md index 05e59816b..95ebda912 100644 --- a/docs/sources/articles/networking.md +++ b/docs/sources/articles/networking.md @@ -57,6 +57,9 @@ server when it starts up, and cannot be changed once it is running: * `--fixed-cidr` — see [Customizing docker0](#docker0) + * `--fixed-cidr-v6` — see + [IPv6](#ipv6) + * `-H SOCKET...` or `--host=SOCKET...` — This might sound like it would affect container networking, but it actually faces in the other direction: @@ -70,8 +73,11 @@ server when it starts up, and cannot be changed once it is running: * `--ip=IP_ADDRESS` — see [Binding container ports](#binding-ports) + * `--ipv6=true|false` — see + [IPv6](#ipv6) + * `--ip-forward=true|false` — see - [Communication between containers](#between-containers) + [Communication between containers and the wider world](#the-world) * `--iptables=true|false` — see [Communication between containers](#between-containers) @@ -204,7 +210,7 @@ Whether a container can talk to the world is governed by two factors. containers if this parameter is `1`. Usually you will simply leave the Docker server at its default setting `--ip-forward=true` and Docker will go set `ip_forward` to `1` for you when the server - starts up. To check the setting or turn it on manually: + starts up. To check the setting or turn it on manually: ``` $ cat /proc/sys/net/ipv4/ip_forward @@ -397,6 +403,182 @@ Again, this topic is covered without all of these low-level networking details in the [Docker User Guide](/userguide/dockerlinks/) document if you would like to use that as your port redirection reference instead. +## IPv6 + + + +As we are [running out of IPv4 addresses](http://en.wikipedia.org/wiki/IPv4_address_exhaustion) +the IETF has standardized an IPv4 successor, [Internet Protocol Version 6](http://en.wikipedia.org/wiki/IPv6) +, in [RFC 2460](https://www.ietf.org/rfc/rfc2460.txt). Both protocols, IPv4 and +IPv6, reside on layer 3 of the [OSI model](http://en.wikipedia.org/wiki/OSI_model). + + +### IPv6 with Docker +By default, the Docker server configures the container network for IPv4 only. +You can enable IPv4/IPv6 dualstack support by running the Docker daemon with the +`--ipv6` flag. Docker will set up the bridge `docker0` with the IPv6 +[link-local address](http://en.wikipedia.org/wiki/Link-local_address) `fe80::1`. + +By default, containers that are created will only get a link-local IPv6 address. +To assign globally routable IPv6 addresses to your containers you have to +specify an IPv6 subnet to pick the addresses from. Set the IPv6 subnet via the +`--fixed-cidr-v6` parameter when starting Docker daemon: + + docker -d --ipv6 --fixed-cidr-v6="2001:db8:0:2:/64" + +The subnet for Docker containers should at least have a size of `/80`. This way +an IPv6 address can end with the container's MAC address and you prevent NDP +neighbor cache invalidation issues in the Docker layer. + +With the `--fixed-cidr-v6` parameter set Docker will add a new route to the +routing table. Further IPv6 routing will be enabled (you may prevent this by +starting Docker daemon with `--ip-forward=false`): + + $ route -A inet6 add 2001:db8:0:2/64 dev docker0 + $ echo 1 > /proc/sys/net/ipv6/conf/default/forwarding + $ echo 1 > /proc/sys/net/ipv6/conf/all/forwarding + +All traffic to the subnet `2001:db8:0:2/64` will now be routed +via the `docker0` interface. + +Be aware that IPv6 forwarding may interfere with your existing IPv6 +configuration: If you are using Router Advertisements to get IPv6 settings for +your host's interfaces you should set `accept_ra` to `2`. Otherwise IPv6 +enabled forwarding will result in rejecting Router Advertisements. E.g., if you +want to configure `eth0` via Router Advertisements you should set: + + ``` + $ echo 2 > /proc/sys/net/ipv6/conf/eth0/accept_ra + ``` + +![](/article-img/ipv6_basic_host_config.svg) + +Every new container will get an IPv6 address from the defined subnet. Further +a default route will be added via the gateway `fe80::1` on `eth0`: + + docker run -it ubuntu bash -c "ifconfig eth0; route -A inet6" + + eth0 Link encap:Ethernet HWaddr 02:42:ac:11:00:02 + inet addr:172.17.0.2 Bcast:0.0.0.0 Mask:255.255.0.0 + inet6 addr: 2001:db8:0:2::1/64 Scope:Global + inet6 addr: fe80::42:acff:fe11:2/64 Scope:Link + UP BROADCAST MTU:1500 Metric:1 + RX packets:1 errors:0 dropped:0 overruns:0 frame:0 + TX packets:1 errors:0 dropped:0 overruns:0 carrier:0 + collisions:0 txqueuelen:0 + RX bytes:110 (110.0 B) TX bytes:110 (110.0 B) + + Kernel IPv6 routing table + Destination Next Hop Flag Met Ref Use If + 2001:db8:0:2::/64 :: U 256 0 0 eth0 + fe80::/64 :: U 256 0 0 eth0 + ::/0 fe80::1 UG 1024 0 0 eth0 + ::/0 :: !n -1 1 1 lo + ::1/128 :: Un 0 1 0 lo + ff00::/8 :: U 256 1 0 eth0 + ::/0 :: !n -1 1 1 lo + +In this example the Docker container is assigned a link-local address with the +network suffix `/64` (here: `fe80::42:acff:fe11:2/64`) and a globally routable +IPv6 address (here: `2001:db8:0:2::1/64`). The container will create connections +to addresses outside of the `2001:db8:0:2::/64` network via the link-local +gateway at `fe80::1` on `eth0`. + +Often servers or virtual machines get a `/64` IPv6 subnet assigned. In this case +you can split it up further and provide Docker a `/80` subnet while using a +separate `/80` subnet for other applications on the host: + +![](/article-img/ipv6_slash64_subnet_config.svg) + +In this setup the subnet `2001:db8::/80` with a range from `2001:db8::0:0:0:0` +to `2001:db8::0:ffff:ffff:ffff` is attached to `eth0`, with the host listening +at `2001:db8::1`. The subnet `2001:db8:0:0:0:1::/80` with an address range from +`2001:db8::1:0:0:0` to `2001:db8::1:ffff:ffff:ffff` is attached to `docker0` and +will be used by containers. + +### Docker IPv6 Cluster + +#### Switched Network Environment +Using routable IPv6 addresses allows you to realize communication between +containers on different hosts. Let's have a look at a simple Docker IPv6 cluster +example: + +![](/article-img/ipv6_switched_network_example.svg) + +The Docker hosts are in the `2000::/64` subnet. Host1 is configured +to provide addresses from the `2001::/64` subnet to its containers. It has three +routes configured: + +- Route all traffic to `2000::/64` via `eth0` +- Route all traffic to `2001::/64` via `docker0` +- Route all traffic to `2002::/64` via Host2 with IP `2000::2` + +Host1 also acts as a router on OSI layer 3. When one of the network clients +tries to contact a target that is specified in Host1's routing table Host1 will +forward the traffic accordingly. It acts as a router for all networks it knows: +`2000:/64`, `2001:/64` and `2002::/64`. + +On Host2 we have nearly the same configuration. Host2's containers will get IPv6 +addresses from `2002::/64`. Host2 has three routes configured: + +- Route all traffic to `2000::/64` via `eth0` +- Route all traffic to `2002::/64` via `docker0` +- Route all traffic to `2001::/64` via Host1 with IP `2000::1` + +The difference to Host1 is that the network `2002::/64` is directly attached to +the host via its `docker0` interface whereas it reaches `2001::/64` via Host1's +IPv6 address `2000::1`. + +This way every container is able to contact every other container. The +containers `Container1-*` share the same subnet and contact each other directly. +The traffic between `Container1-*` and `Container2-*` will be routed via Host1 +and Host2 because those containers do not share the same subnet. + +In a switched environment every host has to know all routes to every subnet. You +always have to update the hosts' routing tables once you add or remove a host +to the cluster. + +Every configuration in the diagram that is shown below the dashed line is +handled by Docker: The `docker0` bridge IP address configuration, the route to +the Docker subnet on the host, the container IP addresses and the routes on the +containers. The configuration above the line is up to the user and can be +adapted to the individual environment. + +#### Routed Network Environment + +In a routed network environment you replace the level 2 switch with a level 3 +router. Now the hosts just have to know their default gateway (the router) and +the route to their own containers (managed by Docker). The router holds all +routing information about the Docker subnets. When you add or remove a host to +this environment you just have to update the routing table in the router - not +on every host. + +![](/article-img/ipv6_routed_network_example.svg) + +In this scenario containers of the same host can communicate directly with each +other. The traffic between containers on different hosts will be routed via +their hosts and the router. For example packet from `Container1-1` to +`Container2-1` will be routed through `Host1`, `Router` and `Host2` until it +arrives at `Container2-1`. + +To keep the IPv6 addresses short in this example a `/48` network is assigned to +every host. The hosts use a `/64` subnet of this for its own services and one +for Docker. When adding a third host you would add a route for the subnet +`2001:db8:3::/48` in the router and configure Docker on Host3 with +`--fixed-cidr-v6=2001:db8:3:1::/64`. + +Remember the subnet for Docker containers should at least have a size of `/80`. +This way an IPv6 address can end with the container's MAC address and you +prevent ARP cache invalidation issues in the Docker layer. So if you have a +`/64` for your whole environment use `/68` subnets for the hosts and `/80` for +the containers. This way you can use 4096 hosts with 16 `/80` subnets each. + +Every configuration in the diagram that is visualized below the dashed line is +handled by Docker: The `docker0` bridge IP address configuration, the route to +the Docker subnet on the host, the container IP addresses and the routes on the +containers. The configuration above the line is up to the user and can be +adapted to the individual environment. + ## Customizing docker0 diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index a92c72d6c..048f521bb 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -76,8 +76,9 @@ expect an integer, and they can only be specified once. --dns=[] Force Docker to use specific DNS servers --dns-search=[] Force Docker to use specific DNS search domains -e, --exec-driver="native" Force the Docker runtime to use a specific exec driver - --fixed-cidr="" IPv4 subnet for fixed IPs (ex: 10.20.0.0/16) + --fixed-cidr="" IPv4 subnet for fixed IPs (e.g.: 10.20.0.0/16) this subnet must be nested in the bridge subnet (which is defined by -b or --bip) + --fixed-cidr-v6="" IPv6 subnet for global IPs (e.g.: 2a00:1450::/64) -G, --group="docker" Group to assign the unix socket specified by -H when running in daemon mode use '' (the empty string) to disable setting of a group -g, --graph="/var/lib/docker" Path to use as the root of the Docker runtime @@ -85,9 +86,10 @@ expect an integer, and they can only be specified once. --icc=true Allow unrestricted inter-container and Docker daemon host communication --insecure-registry=[] Enable insecure communication with specified registries (disables certificate verification for HTTPS and enables HTTP fallback) (e.g., localhost:5000 or 10.20.0.0/16) --ip=0.0.0.0 Default IP address to use when binding container ports - --ip-forward=true Enable net.ipv4.ip_forward + --ip-forward=true Enable net.ipv4.ip_forward and IPv6 forwarding if --fixed-cidr-v6 is defined. IPv6 forwarding may interfere with your existing IPv6 configuration when using Router Advertisement. --ip-masq=true Enable IP masquerading for bridge's IP range --iptables=true Enable Docker's addition of iptables rules + --ipv6=false Enable Docker IPv6 support -l, --log-level="info" Set the logging level --label=[] Set key=value labels to the daemon (displayed in `docker info`) --mtu=0 Set the containers network MTU From 2639e073b19f526ceecbede9439aed0aba0a4ad8 Mon Sep 17 00:00:00 2001 From: Jessica Frazelle Date: Thu, 8 Jan 2015 15:14:04 -0800 Subject: [PATCH 210/513] `docker ps --filter status=exited should not require passing -a` Docker-DCO-1.1-Signed-off-by: Jessica Frazelle (github: jfrazelle) --- daemon/list.go | 8 ++++++++ integration-cli/docker_cli_ps_test.go | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/daemon/list.go b/daemon/list.go index 937cdd212..0f3a88e90 100644 --- a/daemon/list.go +++ b/daemon/list.go @@ -45,6 +45,14 @@ func (daemon *Daemon) Containers(job *engine.Job) engine.Status { } } + if i, ok := psFilters["status"]; ok { + for _, value := range i { + if value == "exited" { + all = true + } + } + } + names := map[string][]string{} daemon.ContainerGraph().Walk("/", func(p string, e *graphdb.Entity) error { names[e.ID()] = append(names[e.ID()], p) diff --git a/integration-cli/docker_cli_ps_test.go b/integration-cli/docker_cli_ps_test.go index 09207826b..17a7280b0 100644 --- a/integration-cli/docker_cli_ps_test.go +++ b/integration-cli/docker_cli_ps_test.go @@ -312,7 +312,7 @@ func TestPsListContainersFilterStatus(t *testing.T) { secondID := stripTrailingCharacters(out) // filter containers by exited - runCmd = exec.Command(dockerBinary, "ps", "-a", "-q", "--filter=status=exited") + runCmd = exec.Command(dockerBinary, "ps", "-q", "--filter=status=exited") out, _, err = runCommandWithOutput(runCmd) if err != nil { t.Fatal(out, err) From 81f84023bef5e94dfbcfbb44443bc03707b355c4 Mon Sep 17 00:00:00 2001 From: Jessica Frazelle Date: Thu, 8 Jan 2015 14:51:47 -0800 Subject: [PATCH 211/513] `docker ps --filter exited=status` should not show running containers Docker-DCO-1.1-Signed-off-by: Jessica Frazelle (github: jfrazelle) --- daemon/list.go | 5 ++--- integration-cli/docker_cli_ps_test.go | 24 +++++++++++++----------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/daemon/list.go b/daemon/list.go index 937cdd212..6c86b6f29 100644 --- a/daemon/list.go +++ b/daemon/list.go @@ -73,7 +73,6 @@ func (daemon *Daemon) Containers(job *engine.Job) engine.Status { if !container.Running && !all && n <= 0 && since == "" && before == "" { return nil } - if !psFilters.Match("name", container.Name) { return nil } @@ -96,10 +95,10 @@ func (daemon *Daemon) Containers(job *engine.Job) engine.Status { return errLast } } - if len(filt_exited) > 0 && !container.Running { + if len(filt_exited) > 0 { should_skip := true for _, code := range filt_exited { - if code == container.ExitCode { + if code == container.ExitCode && !container.Running { should_skip = false break } diff --git a/integration-cli/docker_cli_ps_test.go b/integration-cli/docker_cli_ps_test.go index 09207826b..1318b9efd 100644 --- a/integration-cli/docker_cli_ps_test.go +++ b/integration-cli/docker_cli_ps_test.go @@ -398,11 +398,15 @@ func TestPsListContainersFilterName(t *testing.T) { } func TestPsListContainersFilterExited(t *testing.T) { - deleteAllContainers() defer deleteAllContainers() - runCmd := exec.Command(dockerBinary, "run", "--name", "zero1", "busybox", "true") - out, _, err := runCommandWithOutput(runCmd) - if err != nil { + + runCmd := exec.Command(dockerBinary, "run", "-d", "--name", "top", "busybox", "top") + if out, _, err := runCommandWithOutput(runCmd); err != nil { + t.Fatal(out, err) + } + + runCmd = exec.Command(dockerBinary, "run", "--name", "zero1", "busybox", "true") + if out, _, err := runCommandWithOutput(runCmd); err != nil { t.Fatal(out, err) } firstZero, err := getIDByName("zero1") @@ -411,8 +415,7 @@ func TestPsListContainersFilterExited(t *testing.T) { } runCmd = exec.Command(dockerBinary, "run", "--name", "zero2", "busybox", "true") - out, _, err = runCommandWithOutput(runCmd) - if err != nil { + if out, _, err := runCommandWithOutput(runCmd); err != nil { t.Fatal(out, err) } secondZero, err := getIDByName("zero2") @@ -421,8 +424,7 @@ func TestPsListContainersFilterExited(t *testing.T) { } runCmd = exec.Command(dockerBinary, "run", "--name", "nonzero1", "busybox", "false") - out, _, err = runCommandWithOutput(runCmd) - if err == nil { + if out, _, err := runCommandWithOutput(runCmd); err == nil { t.Fatal("Should fail.", out, err) } firstNonZero, err := getIDByName("nonzero1") @@ -431,8 +433,7 @@ func TestPsListContainersFilterExited(t *testing.T) { } runCmd = exec.Command(dockerBinary, "run", "--name", "nonzero2", "busybox", "false") - out, _, err = runCommandWithOutput(runCmd) - if err == nil { + if out, _, err := runCommandWithOutput(runCmd); err == nil { t.Fatal("Should fail.", out, err) } secondNonZero, err := getIDByName("nonzero2") @@ -442,7 +443,7 @@ func TestPsListContainersFilterExited(t *testing.T) { // filter containers by exited=0 runCmd = exec.Command(dockerBinary, "ps", "-a", "-q", "--no-trunc", "--filter=exited=0") - out, _, err = runCommandWithOutput(runCmd) + out, _, err := runCommandWithOutput(runCmd) if err != nil { t.Fatal(out, err) } @@ -472,5 +473,6 @@ func TestPsListContainersFilterExited(t *testing.T) { if ids[1] != firstNonZero { t.Fatalf("Second in list should be %q, got %q", firstNonZero, ids[1]) } + logDone("ps - test ps filter exited") } From c754555b8742df282bd46ef056837934e973be98 Mon Sep 17 00:00:00 2001 From: nponeccop Date: Fri, 9 Jan 2015 02:22:38 +0200 Subject: [PATCH 212/513] FsMagic should avoid sign extension on i686 `uint64(buf.Type)` on i686 is ffffffff9123683e on i686 due to sign extension, so it cannot be compared with `FsMagic(0x9123683E)` Signed-off-by: Andrii Melnykov --- daemon/graphdriver/driver.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/graphdriver/driver.go b/daemon/graphdriver/driver.go index 1c0601278..c23b115ae 100644 --- a/daemon/graphdriver/driver.go +++ b/daemon/graphdriver/driver.go @@ -11,7 +11,7 @@ import ( "github.com/docker/docker/pkg/archive" ) -type FsMagic uint64 +type FsMagic uint32 const ( FsMagicBtrfs = FsMagic(0x9123683E) From 7724260224c69eeb948c75c247d4868256e5081a Mon Sep 17 00:00:00 2001 From: Qiang Huang Date: Fri, 9 Jan 2015 09:06:27 +0800 Subject: [PATCH 213/513] registry: fix minor type Signed-off-by: Qiang Huang --- registry/session.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/registry/session.go b/registry/session.go index 28cf18fbe..781a91b15 100644 --- a/registry/session.go +++ b/registry/session.go @@ -584,7 +584,7 @@ func (r *Session) SearchRepositories(term string) (*SearchResults, error) { } defer res.Body.Close() if res.StatusCode != 200 { - return nil, utils.NewHTTPRequestError(fmt.Sprintf("Unexepected status code %d", res.StatusCode), res) + return nil, utils.NewHTTPRequestError(fmt.Sprintf("Unexpected status code %d", res.StatusCode), res) } result := new(SearchResults) err = json.NewDecoder(res.Body).Decode(result) From fa8560e3859037e7f5a7e2b37fedd91c7f6d0cf9 Mon Sep 17 00:00:00 2001 From: Andrew Clay Shafer Date: Thu, 8 Jan 2015 21:09:23 -0500 Subject: [PATCH 214/513] remove redundant 'Get' for MacAddress remove second redundant call to set MacAddress from env Signed-off-by: Andrew Clay Shafer --- daemon/container.go | 1 - 1 file changed, 1 deletion(-) diff --git a/daemon/container.go b/daemon/container.go index 0effa99c1..85e16e401 100644 --- a/daemon/container.go +++ b/daemon/container.go @@ -544,7 +544,6 @@ func (container *Container) AllocateNetwork() error { container.NetworkSettings.IPPrefixLen = env.GetInt("IPPrefixLen") container.NetworkSettings.MacAddress = env.Get("MacAddress") container.NetworkSettings.Gateway = env.Get("Gateway") - container.NetworkSettings.MacAddress = env.Get("MacAddress") container.NetworkSettings.LinkLocalIPv6Address = env.Get("LinkLocalIPv6") container.NetworkSettings.LinkLocalIPv6PrefixLen = 64 container.NetworkSettings.GlobalIPv6Address = env.Get("GlobalIPv6") From 21fcca7fdc8622567b79bc84c3921a9ae7c22501 Mon Sep 17 00:00:00 2001 From: Kevin Yap Date: Thu, 8 Jan 2015 18:10:59 -0800 Subject: [PATCH 215/513] Add missing italics to list items in README Signed-off-by: Kevin Yap --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 58f121a11..eec5c3b0e 100644 --- a/README.md +++ b/README.md @@ -100,13 +100,13 @@ This is usually difficult for several reasons: typically don't work well with each other, requiring awkward custom integrations. - * Conflicting dependencies. Different applications may depend on + * *Conflicting dependencies*. Different applications may depend on different versions of the same dependency. Packaging tools handle these situations with various degrees of ease - but they all handle them in different and incompatible ways, which again forces the developer to do extra work. - * Custom dependencies. A developer may need to prepare a custom + * *Custom dependencies*. A developer may need to prepare a custom version of their application's dependency. Some packaging systems can handle custom versions of a dependency, others can't - and all of them handle it differently. From 09c033ff872334cdcc45172ac57dbf21573481ef Mon Sep 17 00:00:00 2001 From: Vincent Batts Date: Fri, 9 Jan 2015 11:20:07 -0500 Subject: [PATCH 216/513] devmapper: show device and loop file , if used Presenly the "Data file:" shows either the loopback _file_ or the block device. With this, the "Data file:" will always show the device, and if it is a loopback, then there will additionally be a "Data loop file:". (Same for "Metadata file:") Signed-off-by: Vincent Batts --- daemon/graphdriver/devmapper/deviceset.go | 43 +++++++++++++++-------- daemon/graphdriver/devmapper/driver.go | 10 ++++-- 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/daemon/graphdriver/devmapper/deviceset.go b/daemon/graphdriver/devmapper/deviceset.go index 078e31a1e..84adf32ec 100644 --- a/daemon/graphdriver/devmapper/deviceset.go +++ b/daemon/graphdriver/devmapper/deviceset.go @@ -89,8 +89,10 @@ type DeviceSet struct { filesystem string mountOptions string mkfsArgs []string - dataDevice string - metadataDevice string + dataDevice string // block or loop dev + dataLoopFile string // loopback file, if used + metadataDevice string // block or loop dev + metadataLoopFile string // loopback file, if used doBlkDiscard bool thinpBlockSize uint32 thinPoolDevice string @@ -104,8 +106,10 @@ type DiskUsage struct { type Status struct { PoolName string - DataLoopback string - MetadataLoopback string + DataFile string // actual block device for data + DataLoopback string // loopback file, if used + MetadataFile string // actual block device for metadata + MetadataLoopback string // loopback file, if used Data DiskUsage Metadata DiskUsage SectorSize uint64 @@ -1013,6 +1017,8 @@ func (devices *DeviceSet) initDevmapper(doInit bool) error { if err != nil { return err } + devices.dataLoopFile = data + devices.dataDevice = dataFile.Name() } else { dataFile, err = os.OpenFile(devices.dataDevice, os.O_RDWR, 0600) if err != nil { @@ -1044,6 +1050,8 @@ func (devices *DeviceSet) initDevmapper(doInit bool) error { if err != nil { return err } + devices.metadataLoopFile = metadata + devices.metadataDevice = metadataFile.Name() } else { metadataFile, err = os.OpenFile(devices.metadataDevice, os.O_RDWR, 0600) if err != nil { @@ -1540,6 +1548,19 @@ func (devices *DeviceSet) poolStatus() (totalSizeInSectors, transactionId, dataU return } +// MetadataDevicePath returns the path to the metadata storage for this deviceset, +// regardless of loopback or block device +func (devices DeviceSet) DataDevicePath() string { + return devices.dataDevice +} + +// MetadataDevicePath returns the path to the metadata storage for this deviceset, +// regardless of loopback or block device +func (devices DeviceSet) MetadataDevicePath() string { + return devices.metadataDevice +} + +// Status returns the current status of this deviceset func (devices *DeviceSet) Status() *Status { devices.Lock() defer devices.Unlock() @@ -1547,16 +1568,10 @@ func (devices *DeviceSet) Status() *Status { status := &Status{} status.PoolName = devices.getPoolName() - if len(devices.dataDevice) > 0 { - status.DataLoopback = devices.dataDevice - } else { - status.DataLoopback = path.Join(devices.loopbackDir(), "data") - } - if len(devices.metadataDevice) > 0 { - status.MetadataLoopback = devices.metadataDevice - } else { - status.MetadataLoopback = path.Join(devices.loopbackDir(), "metadata") - } + status.DataFile = devices.DataDevicePath() + status.DataLoopback = devices.dataLoopFile + status.MetadataFile = devices.MetadataDevicePath() + status.MetadataLoopback = devices.metadataLoopFile totalSizeInSectors, _, dataUsed, dataTotal, metadataUsed, metadataTotal, err := devices.poolStatus() if err == nil { diff --git a/daemon/graphdriver/devmapper/driver.go b/daemon/graphdriver/devmapper/driver.go index 91e9491e3..caae37f6c 100644 --- a/daemon/graphdriver/devmapper/driver.go +++ b/daemon/graphdriver/devmapper/driver.go @@ -57,13 +57,19 @@ func (d *Driver) Status() [][2]string { status := [][2]string{ {"Pool Name", s.PoolName}, {"Pool Blocksize", fmt.Sprintf("%s", units.HumanSize(float64(s.SectorSize)))}, - {"Data file", s.DataLoopback}, - {"Metadata file", s.MetadataLoopback}, + {"Data file", s.DataFile}, + {"Metadata file", s.MetadataFile}, {"Data Space Used", fmt.Sprintf("%s", units.HumanSize(float64(s.Data.Used)))}, {"Data Space Total", fmt.Sprintf("%s", units.HumanSize(float64(s.Data.Total)))}, {"Metadata Space Used", fmt.Sprintf("%s", units.HumanSize(float64(s.Metadata.Used)))}, {"Metadata Space Total", fmt.Sprintf("%s", units.HumanSize(float64(s.Metadata.Total)))}, } + if len(s.DataLoopback) > 0 { + status = append(status, [2]string{"Data loop file", s.DataLoopback}) + } + if len(s.MetadataLoopback) > 0 { + status = append(status, [2]string{"Metadata loop file", s.MetadataLoopback}) + } if vStr, err := devicemapper.GetLibraryVersion(); err == nil { status = append(status, [2]string{"Library Version", vStr}) } From 2677f1add48961115f08016da422d74cafbf4c8b Mon Sep 17 00:00:00 2001 From: Tianon Gravi Date: Thu, 8 Jan 2015 23:07:15 -0700 Subject: [PATCH 217/513] Refactor Dockerfile version number usage This refactors version number usage for several of our "download/install this exact version" bits so we have an easier time replacing the version numbers in Jenkins. Signed-off-by: Andrew "Tianon" Page --- Dockerfile | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index b38c142c3..6f97513a9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -62,8 +62,9 @@ RUN cd /usr/local/lvm2 \ # see https://git.fedorahosted.org/cgit/lvm2.git/tree/INSTALL # Install lxc +ENV LXC_VERSION 1.0.7 RUN mkdir -p /usr/src/lxc \ - && curl -sSL https://linuxcontainers.org/downloads/lxc/lxc-1.0.7.tar.gz | tar -v -C /usr/src/lxc/ -xz --strip-components=1 + && curl -sSL https://linuxcontainers.org/downloads/lxc/lxc-${LXC_VERSION}.tar.gz | tar -v -C /usr/src/lxc/ -xz --strip-components=1 RUN cd /usr/src/lxc \ && ./configure \ && make \ @@ -71,10 +72,11 @@ RUN cd /usr/src/lxc \ && ldconfig # Install Go -RUN curl -sSL https://golang.org/dl/go1.4.src.tar.gz | tar -v -C /usr/local -xz -ENV PATH /usr/local/go/bin:$PATH +ENV GO_VERSION 1.4 +RUN curl -sSL https://golang.org/dl/go${GO_VERSION}.src.tar.gz | tar -v -C /usr/local -xz \ + && mkdir -p /go/bin +ENV PATH /go/bin:/usr/local/go/bin:$PATH ENV GOPATH /go:/go/src/github.com/docker/docker/vendor -ENV PATH /go/bin:$PATH RUN cd /usr/local/go/src && ./make.bash --no-clean 2>&1 # Compile Go for cross compilation @@ -94,7 +96,7 @@ RUN cd /usr/local/go/src \ ./make.bash --no-clean 2>&1; \ done -# reinstall standard library with netgo +# Reinstall standard library with netgo RUN go clean -i net && go install -tags netgo std # Grab Go's cover tool for dead-simple code coverage testing From b20363c4c056bebe256f0d53283ca1e537ed5cf6 Mon Sep 17 00:00:00 2001 From: Tianon Gravi Date: Thu, 8 Jan 2015 23:09:06 -0700 Subject: [PATCH 218/513] Add "gofmt" from Go 1.3.3 Signed-off-by: Andrew "Tianon" Page --- Dockerfile | 4 ++++ api/client/utils.go | 5 +---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 6f97513a9..3069d138d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -99,6 +99,10 @@ RUN cd /usr/local/go/src \ # Reinstall standard library with netgo RUN go clean -i net && go install -tags netgo std +# We still support compiling with older Go, so need to grab older "gofmt" +ENV GOFMT_VERSION 1.3.3 +RUN curl -sSL https://storage.googleapis.com/golang/go${GOFMT_VERSION}.$(go env GOOS)-$(go env GOARCH).tar.gz | tar -C /go/bin -xz --strip-components=2 go/bin/gofmt + # Grab Go's cover tool for dead-simple code coverage testing RUN go get golang.org/x/tools/cmd/cover diff --git a/api/client/utils.go b/api/client/utils.go index 0ee02cdbc..f330a6840 100644 --- a/api/client/utils.go +++ b/api/client/utils.go @@ -260,10 +260,7 @@ func (cli *DockerCli) monitorTtySize(id string, isExec bool) error { sigchan := make(chan os.Signal, 1) gosignal.Notify(sigchan, signal.SIGWINCH) go func() { - // This tmp := range..., _ = tmp workaround is needed to - // suppress gofmt warnings while still preserve go1.3 compatibility - for tmp := range sigchan { - _ = tmp + for _ := range sigchan { cli.resizeTty(id, isExec) } }() From 4374d3bc81cf47866bd3f5c240307915931084cf Mon Sep 17 00:00:00 2001 From: Jessica Frazelle Date: Fri, 9 Jan 2015 13:53:03 -0800 Subject: [PATCH 219/513] Add link to master docs. Docker-DCO-1.1-Signed-off-by: Jessica Frazelle (github: jfrazelle) --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 58f121a11..c64d6dfde 100644 --- a/README.md +++ b/README.md @@ -191,6 +191,10 @@ Want to run Docker from a master build? You can download master builds at [master.dockerproject.com](https://master.dockerproject.com). They are updated with each commit merged into the master branch. +Don't know how to use that super cool new feature in the master build? Check +out the master docs at +[docs.master.dockerproject.com](http://docs.master.dockerproject.com). + ### Legal *Brought to you courtesy of our legal counsel. For more context, From 6292354dc359a6d8f47b3199284147d557d2c697 Mon Sep 17 00:00:00 2001 From: Tianon Gravi Date: Fri, 9 Jan 2015 15:03:11 -0700 Subject: [PATCH 220/513] Fix silly little syntax mistake Signed-off-by: Andrew "Tianon" Page --- api/client/utils.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/client/utils.go b/api/client/utils.go index f330a6840..6ebe44806 100644 --- a/api/client/utils.go +++ b/api/client/utils.go @@ -260,7 +260,7 @@ func (cli *DockerCli) monitorTtySize(id string, isExec bool) error { sigchan := make(chan os.Signal, 1) gosignal.Notify(sigchan, signal.SIGWINCH) go func() { - for _ := range sigchan { + for _ = range sigchan { cli.resizeTty(id, isExec) } }() From d43f0b9fc5ec3eae816466ec0307682c13945b31 Mon Sep 17 00:00:00 2001 From: Tianon Gravi Date: Fri, 9 Jan 2015 17:28:40 -0700 Subject: [PATCH 221/513] Fix a few minor issues with building/running inside msysGit Signed-off-by: Andrew "Tianon" Page --- integration-cli/docker_test_vars.go | 26 ++++++-------------------- project/make.sh | 25 ++++++++++++++++--------- project/make/.go-compile-test-dir | 17 +++++++++++++---- project/make/.integration-daemon-stop | 10 ++++++---- project/make/binary | 5 +---- project/make/test-docker-py | 3 +-- project/make/test-integration | 4 ++-- project/make/test-integration-cli | 3 +-- project/make/test-unit | 12 ++++++------ project/make/tgz | 5 +---- 10 files changed, 53 insertions(+), 57 deletions(-) diff --git a/integration-cli/docker_test_vars.go b/integration-cli/docker_test_vars.go index 3bfb8ac03..ff2ec7406 100644 --- a/integration-cli/docker_test_vars.go +++ b/integration-cli/docker_test_vars.go @@ -4,7 +4,6 @@ import ( "fmt" "os" "os/exec" - "runtime" ) var ( @@ -26,28 +25,15 @@ var ( workingDirectory string ) -func binarySearchCommand() *exec.Cmd { - if runtime.GOOS == "windows" { - // Windows where.exe is included since Windows Server 2003. It accepts - // wildcards, which we use here to match the development builds binary - // names (such as docker-$VERSION.exe). - return exec.Command("where.exe", "docker*.exe") - } - return exec.Command("which", "docker") -} - func init() { if dockerBin := os.Getenv("DOCKER_BINARY"); dockerBin != "" { dockerBinary = dockerBin - } else { - whichCmd := binarySearchCommand() - out, _, err := runCommandWithOutput(whichCmd) - if err == nil { - dockerBinary = stripTrailingCharacters(out) - } else { - fmt.Printf("ERROR: couldn't resolve full path to the Docker binary (%v)", err) - os.Exit(1) - } + } + var err error + dockerBinary, err = exec.LookPath(dockerBinary) + if err != nil { + fmt.Printf("ERROR: couldn't resolve full path to the Docker binary (%v)", err) + os.Exit(1) } if registryImage := os.Getenv("REGISTRY_IMAGE"); registryImage != "" { registryImageName = registryImage diff --git a/project/make.sh b/project/make.sh index 82b8f0c5d..f7919515c 100755 --- a/project/make.sh +++ b/project/make.sh @@ -178,21 +178,28 @@ go_test_dir() { ) } +# a helper to provide ".exe" when it's appropriate +binary_extension() { + if [ "$(go env GOOS)" = 'windows' ]; then + echo -n '.exe' + fi +} + # This helper function walks the current directory looking for directories # holding certain files ($1 parameter), and prints their paths on standard # output, one per line. find_dirs() { find . -not \( \ \( \ - -wholename './vendor' \ - -o -wholename './integration' \ - -o -wholename './integration-cli' \ - -o -wholename './contrib' \ - -o -wholename './pkg/mflag/example' \ - -o -wholename './.git' \ - -o -wholename './bundles' \ - -o -wholename './docs' \ - -o -wholename './pkg/libcontainer/nsinit' \ + -path './vendor/*' \ + -o -path './integration/*' \ + -o -path './integration-cli/*' \ + -o -path './contrib/*' \ + -o -path './pkg/mflag/example/*' \ + -o -path './.git/*' \ + -o -path './bundles/*' \ + -o -path './docs/*' \ + -o -path './pkg/libcontainer/nsinit/*' \ \) \ -prune \ \) -name "$1" -print0 | xargs -0n1 dirname | sort -u diff --git a/project/make/.go-compile-test-dir b/project/make/.go-compile-test-dir index 0905f7d46..91c438b3c 100755 --- a/project/make/.go-compile-test-dir +++ b/project/make/.go-compile-test-dir @@ -4,7 +4,13 @@ set -e # Compile phase run by parallel in test-unit. No support for coverpkg dir=$1 +in_file="$dir/$(basename "$dir").test" out_file="$DEST/precompiled/$dir.test" +# we want to use binary_extension() here, but we can't because it's in main.sh and this file gets re-execed +if [ "$(go env GOOS)" = 'windows' ]; then + in_file+='.exe' + out_file+='.exe' +fi testcover=() if [ "$HAVE_GO_TEST_COVER" ]; then # if our current go install has -cover, we want to use it :) @@ -16,11 +22,14 @@ fi if [ "$BUILDFLAGS_FILE" ]; then readarray -t BUILDFLAGS < "$BUILDFLAGS_FILE" fi -( + +if ! ( cd "$dir" go test "${testcover[@]}" -ldflags "$LDFLAGS" "${BUILDFLAGS[@]}" $TESTFLAGS -c -) -[ $? -ne 0 ] && return 1 +); then + exit 1 +fi + mkdir -p "$(dirname "$out_file")" -mv "$dir/$(basename "$dir").test" "$out_file" +mv "$in_file" "$out_file" echo "Precompiled: ${DOCKER_PKG}${dir#.}" diff --git a/project/make/.integration-daemon-stop b/project/make/.integration-daemon-stop index 57dc651d4..319aaa4a1 100644 --- a/project/make/.integration-daemon-stop +++ b/project/make/.integration-daemon-stop @@ -1,7 +1,9 @@ #!/bin/bash -for pid in $(find "$DEST" -name docker.pid); do - DOCKER_PID=$(set -x; cat "$pid") - ( set -x; kill $DOCKER_PID ) - wait $DOCKERD_PID || true +for pidFile in $(find "$DEST" -name docker.pid); do + pid=$(set -x; cat "$pidFile") + ( set -x; kill $pid ) + if ! wait $pid; then + echo >&2 "warning: PID $pid from $pidFile had a nonzero exit code" + fi done diff --git a/project/make/binary b/project/make/binary index 6b988b170..c0cc3459e 100755 --- a/project/make/binary +++ b/project/make/binary @@ -3,10 +3,7 @@ set -e DEST=$1 BINARY_NAME="docker-$VERSION" -BINARY_EXTENSION= -if [ "$(go env GOOS)" = 'windows' ]; then - BINARY_EXTENSION='.exe' -fi +BINARY_EXTENSION="$(binary_extension)" BINARY_FULLNAME="$BINARY_NAME$BINARY_EXTENSION" # Cygdrive paths don't play well with go build -o. diff --git a/project/make/test-docker-py b/project/make/test-docker-py index 1096c9cbf..6047eec1b 100644 --- a/project/make/test-docker-py +++ b/project/make/test-docker-py @@ -4,7 +4,6 @@ set -e DEST=$1 # subshell so that we can export PATH without breaking other things -exec > >(tee -a $DEST/test.log) 2>&1 ( source "$(dirname "$BASH_SOURCE")/.integration-daemon-start" @@ -19,4 +18,4 @@ exec > >(tee -a $DEST/test.log) 2>&1 python tests/integration_test.py source "$(dirname "$BASH_SOURCE")/.integration-daemon-stop" -) +) 2>&1 | tee -a $DEST/test.log diff --git a/project/make/test-integration b/project/make/test-integration index b49ae595e..9512cc4e3 100644 --- a/project/make/test-integration +++ b/project/make/test-integration @@ -10,6 +10,6 @@ bundle_test_integration() { # this "grep" hides some really irritating warnings that "go test -coverpkg" # spews when it is given packages that aren't used -exec > >(tee -a $DEST/test.log) 2>&1 bundle_test_integration 2>&1 \ - | grep --line-buffered -v '^warning: no packages being tested depend on ' + | grep --line-buffered -v '^warning: no packages being tested depend on ' \ + | tee -a $DEST/test.log diff --git a/project/make/test-integration-cli b/project/make/test-integration-cli index b8647ef76..0aaa298be 100644 --- a/project/make/test-integration-cli +++ b/project/make/test-integration-cli @@ -8,7 +8,6 @@ bundle_test_integration_cli() { } # subshell so that we can export PATH without breaking other things -exec > >(tee -a $DEST/test.log) 2>&1 ( source "$(dirname "$BASH_SOURCE")/.integration-daemon-start" @@ -20,4 +19,4 @@ exec > >(tee -a $DEST/test.log) 2>&1 bundle_test_integration_cli source "$(dirname "$BASH_SOURCE")/.integration-daemon-stop" -) +) 2>&1 | tee -a $DEST/test.log diff --git a/project/make/test-unit b/project/make/test-unit index 910b887a8..59700e86f 100644 --- a/project/make/test-unit +++ b/project/make/test-unit @@ -2,7 +2,7 @@ set -e DEST=$1 -: ${PARALLEL_JOBS:=$(nproc)} +: ${PARALLEL_JOBS:=$(nproc 2>/dev/null || echo 1)} # if nproc fails (usually because we don't have it), let's not parallelize by default RED=$'\033[31m' GREEN=$'\033[32m' @@ -38,12 +38,13 @@ bundle_test_unit() { export BUILDFLAGS_FILE="$HOME/buildflags_file" ( IFS=$'\n'; echo "${BUILDFLAGS[*]}" ) > "$BUILDFLAGS_FILE" - echo "$TESTDIRS" | parallel --jobs "$PARALLEL_JOBS" --halt 2 --env _ "$(dirname "$BASH_SOURCE")/.go-compile-test-dir" + echo "$TESTDIRS" | parallel --jobs "$PARALLEL_JOBS" --env _ "$(dirname "$BASH_SOURCE")/.go-compile-test-dir" rm -rf "$HOME" else # aww, no "parallel" available - fall back to boring for test_dir in $TESTDIRS; do - "$(dirname "$BASH_SOURCE")/.go-compile-test-dir" "$test_dir" + "$(dirname "$BASH_SOURCE")/.go-compile-test-dir" "$test_dir" || true + # don't let one directory that fails to build tank _all_ our tests! done fi ) @@ -56,7 +57,7 @@ go_run_test_dir() { while read dir; do echo echo '+ go test' $TESTFLAGS "${DOCKER_PKG}${dir#.}" - precompiled="$DEST/precompiled/$dir.test" + precompiled="$DEST/precompiled/$dir.test$(binary_extension)" if ! ( cd "$dir" && "$precompiled" $TESTFLAGS ); then TESTS_FAILED+=("$dir") echo @@ -82,5 +83,4 @@ go_run_test_dir() { fi } -exec > >(tee -a $DEST/test.log) 2>&1 -bundle_test_unit +bundle_test_unit 2>&1 | tee -a $DEST/test.log diff --git a/project/make/tgz b/project/make/tgz index 8fc3cfb43..7d0ef09a5 100644 --- a/project/make/tgz +++ b/project/make/tgz @@ -14,10 +14,7 @@ for d in "$CROSS/"*/*; do GOARCH="$(basename "$d")" GOOS="$(basename "$(dirname "$d")")" BINARY_NAME="docker-$VERSION" - BINARY_EXTENSION= - if [ "$GOOS" = 'windows' ]; then - BINARY_EXTENSION='.exe' - fi + BINARY_EXTENSION="$(binary_extension)" BINARY_FULLNAME="$BINARY_NAME$BINARY_EXTENSION" mkdir -p "$DEST/$GOOS/$GOARCH" TGZ="$DEST/$GOOS/$GOARCH/$BINARY_NAME.tgz" From cdc14c7cbf3ad912588e8cc51f798183f369c9cc Mon Sep 17 00:00:00 2001 From: Jessica Frazelle Date: Fri, 9 Jan 2015 18:22:19 -0800 Subject: [PATCH 222/513] Add apparmor Docker-DCO-1.1-Signed-off-by: Jessica Frazelle (github: jfrazelle) --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 3069d138d..a7d19928d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,6 +28,7 @@ MAINTAINER Tianon Gravi (@tianon) # Packaged dependencies RUN apt-get update && apt-get install -y \ + apparmor \ aufs-tools \ automake \ btrfs-tools \ From f29ee870512e8809bfc396c1771fd74235c9de7b Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Wed, 7 Jan 2015 11:19:14 -0500 Subject: [PATCH 223/513] Use the official mime type that exists instead of an imaginary one that does not. Signed-off-by: Jean-Paul Calderone Docker-DCO-1.1-Signed-off-by: Jessica Frazelle (github: jfrazelle) --- api/client/hijack.go | 2 +- api/client/utils.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/client/hijack.go b/api/client/hijack.go index bd302a764..987f2d23f 100644 --- a/api/client/hijack.go +++ b/api/client/hijack.go @@ -134,7 +134,7 @@ func (cli *DockerCli) hijack(method, path string, setRawTerminal bool, in io.Rea return err } req.Header.Set("User-Agent", "Docker-Client/"+dockerversion.VERSION) - req.Header.Set("Content-Type", "plain/text") + req.Header.Set("Content-Type", "text/plain") req.Header.Set("Connection", "Upgrade") req.Header.Set("Upgrade", "tcp") req.Host = cli.addr diff --git a/api/client/utils.go b/api/client/utils.go index 6ebe44806..a65ceb352 100644 --- a/api/client/utils.go +++ b/api/client/utils.go @@ -89,7 +89,7 @@ func (cli *DockerCli) call(method, path string, data interface{}, passAuthInfo b if data != nil { req.Header.Set("Content-Type", "application/json") } else if method == "POST" { - req.Header.Set("Content-Type", "plain/text") + req.Header.Set("Content-Type", "text/plain") } resp, err := cli.HTTPClient().Do(req) if err != nil { @@ -135,7 +135,7 @@ func (cli *DockerCli) streamHelper(method, path string, setRawTerminal bool, in req.URL.Host = cli.addr req.URL.Scheme = cli.scheme if method == "POST" { - req.Header.Set("Content-Type", "plain/text") + req.Header.Set("Content-Type", "text/plain") } if headers != nil { @@ -260,7 +260,7 @@ func (cli *DockerCli) monitorTtySize(id string, isExec bool) error { sigchan := make(chan os.Signal, 1) gosignal.Notify(sigchan, signal.SIGWINCH) go func() { - for _ = range sigchan { + for range sigchan { cli.resizeTty(id, isExec) } }() From 807f486f874d87adb423d486944f5dc59842f1eb Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Wed, 7 Jan 2015 11:19:52 -0500 Subject: [PATCH 224/513] Change some instances of this mistake in the documentation as well. Signed-off-by: Jean-Paul Calderone Docker-DCO-1.1-Signed-off-by: Jessica Frazelle (github: jfrazelle) --- docs/sources/reference/api/docker_remote_api_v1.17.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sources/reference/api/docker_remote_api_v1.17.md b/docs/sources/reference/api/docker_remote_api_v1.17.md index aaaffda85..67b862302 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.17.md +++ b/docs/sources/reference/api/docker_remote_api_v1.17.md @@ -1616,12 +1616,12 @@ This API is valid only if `tty` was specified as part of creating and starting t **Example request**: POST /exec/e90e34656806/resize HTTP/1.1 - Content-Type: plain/text + Content-Type: text/plain **Example response**: HTTP/1.1 201 OK - Content-Type: plain/text + Content-Type: text/plain Query Parameters: From 30eff2720a110f3ece0e429ef1897a254f0d9e71 Mon Sep 17 00:00:00 2001 From: Phil Estes Date: Fri, 9 Jan 2015 21:18:57 -0500 Subject: [PATCH 225/513] Properly handle containers which pre-date the resolv.conf update feature This fixes the container start issue for containers which were started on a daemon prior to the resolv.conf updater PR. The update code will now safely ignore these containers (given they don't have a sha256 hash to compare against) and will not attempt to update the resolv.conf through their lifetime. Docker-DCO-1.1-Signed-off-by: Phil Estes (github: estesp) --- daemon/container.go | 10 +++++++++- docs/sources/articles/networking.md | 7 +++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/daemon/container.go b/daemon/container.go index 85e16e401..8bbfb07b2 100644 --- a/daemon/container.go +++ b/daemon/container.go @@ -1057,7 +1057,15 @@ func (container *Container) updateResolvConf(updatedResolvConf []byte, newResolv //read the hash from the last time we wrote resolv.conf in the container hashBytes, err := ioutil.ReadFile(resolvHashFile) if err != nil { - return err + if !os.IsNotExist(err) { + return err + } + // backwards compat: if no hash file exists, this container pre-existed from + // a Docker daemon that didn't contain this update feature. Given we can't know + // if the user has modified the resolv.conf since container start time, safer + // to just never update the container's resolv.conf during it's lifetime which + // we can control by setting hashBytes to an empty string + hashBytes = []byte("") } //if the user has not modified the resolv.conf of the container since we wrote it last diff --git a/docs/sources/articles/networking.md b/docs/sources/articles/networking.md index f90b62ec7..dac20af86 100644 --- a/docs/sources/articles/networking.md +++ b/docs/sources/articles/networking.md @@ -201,6 +201,13 @@ If the options (`--dns` or `--dns-search`) have been used to modify the default host configuration, then the replacement with an updated host's `/etc/resolv.conf` will not happen as well. +> **Note**: +> For containers which were created prior to the implementation of +> the `/etc/resolv.conf` update feature in Docker 1.5.0: those +> containers will **not** receive updates when the host `resolv.conf` +> file changes. Only containers created with Docker 1.5.0 and above +> will utilize this auto-update feature. + ## Communication between containers and the wider world From 5b8c8b1819d0f2dcbe7d708b1ae8f579a73401bd Mon Sep 17 00:00:00 2001 From: Jessica Frazelle Date: Fri, 9 Jan 2015 22:25:53 -0800 Subject: [PATCH 226/513] Add Aaron Swartz to the names generator. https://en.wikiquote.org/wiki/Aaron_Swartz Docker-DCO-1.1-Signed-off-by: Jessica Frazelle (github: jfrazelle) --- pkg/namesgenerator/names-generator.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/namesgenerator/names-generator.go b/pkg/namesgenerator/names-generator.go index c5bcd25b4..feb5cbf5b 100644 --- a/pkg/namesgenerator/names-generator.go +++ b/pkg/namesgenerator/names-generator.go @@ -266,6 +266,9 @@ var ( // Richard Matthew Stallman - the founder of the Free Software movement, the GNU project, the Free Software Foundation, and the League for Programming Freedom. He also invented the concept of copyleft to protect the ideals of this movement, and enshrined this concept in the widely-used GPL (General Public License) for software. https://en.wikiquote.org/wiki/Richard_Stallman "stallman", + // Aaron Swartz was influential in creating RSS, Markdown, Creative Commons, Reddit, and much of the internet as we know it today. He was devoted to freedom of information on the web. https://en.wikiquote.org/wiki/Aaron_Swartz + "swartz", + // Nikola Tesla invented the AC electric system and every gadget ever used by a James Bond villain. https://en.wikipedia.org/wiki/Nikola_Tesla "tesla", From 8d414fd434c55f1d9ac9387e17dd16608fca1bfd Mon Sep 17 00:00:00 2001 From: HuKeping Date: Mon, 12 Jan 2015 13:01:38 +0800 Subject: [PATCH 227/513] docs: update docker inspect part of docs The docker inspect part of docs is quit different with what it really be. Signed-off-by: Hu Keping --- .../reference/api/docker_remote_api_v1.17.md | 165 +++++++++++------- 1 file changed, 98 insertions(+), 67 deletions(-) diff --git a/docs/sources/reference/api/docker_remote_api_v1.17.md b/docs/sources/reference/api/docker_remote_api_v1.17.md index aaaffda85..bbacf91de 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.17.md +++ b/docs/sources/reference/api/docker_remote_api_v1.17.md @@ -266,73 +266,104 @@ Return low-level information on the container `id` 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/docker/docker/bin/docker", - "ResolvConfPath": "/etc/resolv.conf", - "Volumes": {}, - "ExecIDs": [ - "15f211491dced6a353a2e0f37fe3f3692ee2370a4782418e9bf7052865c10fde" - ], - "HostConfig": { - "Binds": null, - "ContainerIDFile": "", - "LxcConf": [], - "Privileged": false, - "PortBindings": { - "80/tcp": [ - { - "HostIp": "0.0.0.0", - "HostPort": "49153" - } - ] - }, - "Links": ["/name:alias"], - "PublishAllPorts": false, - "CapAdd": ["NET_ADMIN"], - "CapDrop": ["MKNOD"] - } - } + { + "AppArmorProfile": "", + "Args": [ + "-c", + "exit 9" + ], + "Config": { + "AttachStderr": true, + "AttachStdin": false, + "AttachStdout": true, + "Cmd": [ + "/bin/sh", + "-c", + "exit 9" + ], + "CpuShares": 0, + "Cpuset": "", + "Domainname": "", + "Entrypoint": null, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "ExposedPorts": null, + "Hostname": "ba033ac44011", + "Image": "ubuntu", + "MacAddress": "", + "Memory": 0, + "MemorySwap": 0, + "NetworkDisabled": false, + "OnBuild": null, + "OpenStdin": false, + "PortSpecs": null, + "StdinOnce": false, + "Tty": false, + "User": "", + "Volumes": null, + "WorkingDir": "" + }, + "Created": "2015-01-06T15:47:31.485331387Z", + "Driver": "devicemapper", + "ExecDriver": "native-0.2", + "ExecIDs": null, + "HostConfig": { + "Binds": null, + "CapAdd": null, + "CapDrop": null, + "ContainerIDFile": "", + "Devices": [], + "Dns": null, + "DnsSearch": null, + "ExtraHosts": null, + "IpcMode": "", + "Links": null, + "LxcConf": [], + "NetworkMode": "bridge", + "PortBindings": {}, + "Privileged": false, + "PublishAllPorts": false, + "RestartPolicy": { + "MaximumRetryCount": 2, + "Name": "on-failure" + }, + "SecurityOpt": null, + "VolumesFrom": null + }, + "HostnamePath": "/var/lib/docker/containers/ba033ac4401106a3b513bc9d639eee123ad78ca3616b921167cd74b20e25ed39/hostname", + "HostsPath": "/var/lib/docker/containers/ba033ac4401106a3b513bc9d639eee123ad78ca3616b921167cd74b20e25ed39/hosts", + "Id": "ba033ac4401106a3b513bc9d639eee123ad78ca3616b921167cd74b20e25ed39", + "Image": "04c5d3b7b0656168630d3ba35d8889bd0e9caafcaeb3004d2bfbc47e7c5d35d2", + "MountLabel": "", + "Name": "/boring_euclid", + "NetworkSettings": { + "Bridge": "", + "Gateway": "", + "IPAddress": "", + "IPPrefixLen": 0, + "MacAddress": "", + "PortMapping": null, + "Ports": null + }, + "Path": "/bin/sh", + "ProcessLabel": "", + "ResolvConfPath": "/var/lib/docker/containers/ba033ac4401106a3b513bc9d639eee123ad78ca3616b921167cd74b20e25ed39/resolv.conf", + "RestartCount": 1, + "State": { + "Error": "", + "ExitCode": 9, + "FinishedAt": "2015-01-06T15:47:32.080254511Z", + "OOMKilled": false, + "Paused": false, + "Pid": 0, + "Restarting": false, + "Running": false, + "StartedAt": "2015-01-06T15:47:32.072697474Z" + }, + "Volumes": {}, + "VolumesRW": {} + } Status Codes: From 883d1415355244b2a2710d0860e5019bc697839b Mon Sep 17 00:00:00 2001 From: soulshake Date: Sat, 10 Jan 2015 20:24:49 -0800 Subject: [PATCH 228/513] Add Laura Poitras to the names generator. http://www.wired.com/2014/10/laura-poitras-crypto-tools-made-snowden-film-possible/ Signed-off-by: AJ Bowen --- pkg/namesgenerator/names-generator.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/namesgenerator/names-generator.go b/pkg/namesgenerator/names-generator.go index feb5cbf5b..f7770774a 100644 --- a/pkg/namesgenerator/names-generator.go +++ b/pkg/namesgenerator/names-generator.go @@ -245,6 +245,9 @@ var ( // Henri Poincaré made fundamental contributions in several fields of mathematics. https://en.wikipedia.org/wiki/Henri_Poincar%C3%A9 "poincare", + // Laura Poitras is a director and producer whose work, made possible by open source crypto tools, advances the causes of truth and freedom of information by reporting disclosures by whistleblowers such as Edward Snowden. https://en.wikipedia.org/wiki/Laura_Poitras + "poitras", + // Claudius Ptolemy - a Greco-Egyptian writer of Alexandria, known as a mathematician, astronomer, geographer, astrologer, and poet of a single epigram in the Greek Anthology - https://en.wikipedia.org/wiki/Ptolemy "ptolemy", From a6a748b9a0ce459fe1e250fdc5d107363315aeca Mon Sep 17 00:00:00 2001 From: Jessica Frazelle Date: Fri, 9 Jan 2015 12:10:54 -0800 Subject: [PATCH 229/513] Update docs release script so we can have autodeploys to docs.master.dockerproject.com. - Make the invaidation profile the bucket variable, not hard coded. - Add no cache variable for settings cache to "no-cache" Docker-DCO-1.1-Signed-off-by: Jessica Frazelle (github: jfrazelle) --- .gitignore | 1 + Makefile | 4 ++-- docs/release.sh | 13 +++++++++---- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 68d2da95b..49fa58a94 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ docs/AWS_S3_BUCKET docs/GIT_BRANCH docs/VERSION docs/GITCOMMIT +docs/changed-files diff --git a/Makefile b/Makefile index 43a6a11c5..13e0fa9e8 100644 --- a/Makefile +++ b/Makefile @@ -30,7 +30,7 @@ DOCKER_DOCS_IMAGE := docker-docs$(if $(GIT_BRANCH),:$(GIT_BRANCH)) DOCKER_RUN_DOCKER := docker run --rm -it --privileged $(DOCKER_ENVS) $(DOCKER_MOUNT) "$(DOCKER_IMAGE)" -DOCKER_RUN_DOCS := docker run --rm -it $(DOCS_MOUNT) -e AWS_S3_BUCKET +DOCKER_RUN_DOCS := docker run --rm -it $(DOCS_MOUNT) -e AWS_S3_BUCKET -e NOCACHE # for some docs workarounds (see below in "docs-build" target) GITCOMMIT := $(shell git rev-parse --short HEAD 2>/dev/null) @@ -83,7 +83,7 @@ build: bundles docker build -t "$(DOCKER_IMAGE)" . docs-build: - git diff --name-status upstream/release..upstream/docs docs/ > docs/changed-files + ( git remote | grep -v upstream ) || git diff --name-status upstream/release..upstream/docs docs/ > docs/changed-files cp ./VERSION docs/VERSION echo "$(GIT_BRANCH)" > docs/GIT_BRANCH echo "$(AWS_S3_BUCKET)" > docs/AWS_S3_BUCKET diff --git a/docs/release.sh b/docs/release.sh index f4f6c552f..de064706b 100755 --- a/docs/release.sh +++ b/docs/release.sh @@ -78,6 +78,11 @@ upload_current_documentation() { src=site/ dst=s3://$BUCKET$1 + cache=max-age=3600 + if [ "$NOCACHE" ]; then + cache=no-cache + fi + echo echo "Uploading $src" echo " to $dst" @@ -90,7 +95,7 @@ upload_current_documentation() { # versions.html_fragment include="--recursive --include \"*.$i\" " echo "uploading *.$i" - run="aws s3 cp $src $dst $OPTIONS --profile $BUCKET --cache-control \"max-age=3600\" --acl public-read $include" + run="aws s3 cp $src $dst $OPTIONS --profile $BUCKET --cache-control $cache --acl public-read $include" echo "=======================" echo "$run" echo "=======================" @@ -114,7 +119,7 @@ invalidate_cache() { len=${#files[@]} - echo "aws cloudfront create-invalidation --profile docs.docker.com --distribution-id $DISTRIBUTION_ID --invalidation-batch '" > batchfile + echo "aws cloudfront create-invalidation --profile $AWS_S3_BUCKET --distribution-id $DISTRIBUTION_ID --invalidation-batch '" > batchfile echo "{\"Paths\":{\"Quantity\":$len," >> batchfile echo "\"Items\": [" >> batchfile @@ -150,7 +155,7 @@ if [ "$BUILD_ROOT" == "yes" ]; then echo "Building root documentation" build_current_documentation upload_current_documentation - invalidate_cache + [ "$NOCACHE" ] || invalidate_cache fi #build again with /v1.0/ prefix @@ -158,4 +163,4 @@ sed -i "s/^site_url:.*/site_url: \/$MAJOR_MINOR\//" mkdocs.yml echo "Building the /$MAJOR_MINOR/ documentation" build_current_documentation upload_current_documentation "/$MAJOR_MINOR/" -invalidate_cache "/$MAJOR_MINOR" +[ "$NOCACHE" ] || invalidate_cache "/$MAJOR_MINOR" From 00fd63e55807c36fedf0878645dfec995fba381d Mon Sep 17 00:00:00 2001 From: Vincent Batts Date: Fri, 9 Jan 2015 17:14:52 -0500 Subject: [PATCH 230/513] graphdriver: change (*Driver).Put signature There are a couple of drivers that swallow errors that may occur in their Put() implementation. This changes the signature of (*Driver).Put for all the drivers implemented. Signed-off-by: Vincent Batts --- daemon/graphdriver/aufs/aufs.go | 3 ++- daemon/graphdriver/btrfs/btrfs.go | 3 ++- daemon/graphdriver/devmapper/driver.go | 6 ++++-- daemon/graphdriver/driver.go | 2 +- daemon/graphdriver/overlay/overlay.go | 14 ++++++++------ daemon/graphdriver/vfs/driver.go | 3 ++- 6 files changed, 19 insertions(+), 12 deletions(-) diff --git a/daemon/graphdriver/aufs/aufs.go b/daemon/graphdriver/aufs/aufs.go index 220be2de5..5d08bfc5d 100644 --- a/daemon/graphdriver/aufs/aufs.go +++ b/daemon/graphdriver/aufs/aufs.go @@ -278,7 +278,7 @@ func (a *Driver) Get(id, mountLabel string) (string, error) { return out, nil } -func (a *Driver) Put(id string) { +func (a *Driver) Put(id string) error { // Protect the a.active from concurrent access a.Lock() defer a.Unlock() @@ -293,6 +293,7 @@ func (a *Driver) Put(id string) { } delete(a.active, id) } + return nil } // Diff produces an archive of the changes between the specified diff --git a/daemon/graphdriver/btrfs/btrfs.go b/daemon/graphdriver/btrfs/btrfs.go index a3964b963..1830ad4e8 100644 --- a/daemon/graphdriver/btrfs/btrfs.go +++ b/daemon/graphdriver/btrfs/btrfs.go @@ -220,9 +220,10 @@ func (d *Driver) Get(id, mountLabel string) (string, error) { return dir, nil } -func (d *Driver) Put(id string) { +func (d *Driver) Put(id string) error { // Get() creates no runtime resources (like e.g. mounts) // so this doesn't need to do anything. + return nil } func (d *Driver) Exists(id string) bool { diff --git a/daemon/graphdriver/devmapper/driver.go b/daemon/graphdriver/devmapper/driver.go index 91e9491e3..aa22f0b40 100644 --- a/daemon/graphdriver/devmapper/driver.go +++ b/daemon/graphdriver/devmapper/driver.go @@ -141,10 +141,12 @@ func (d *Driver) Get(id, mountLabel string) (string, error) { return rootFs, nil } -func (d *Driver) Put(id string) { - if err := d.DeviceSet.UnmountDevice(id); err != nil { +func (d *Driver) Put(id string) error { + err := d.DeviceSet.UnmountDevice(id) + if err != nil { log.Errorf("Warning: error unmounting device %s: %s", id, err) } + return err } func (d *Driver) Exists(id string) bool { diff --git a/daemon/graphdriver/driver.go b/daemon/graphdriver/driver.go index 1c0601278..e9b99f394 100644 --- a/daemon/graphdriver/driver.go +++ b/daemon/graphdriver/driver.go @@ -40,7 +40,7 @@ type ProtoDriver interface { Get(id, mountLabel string) (dir string, err error) // Put releases the system resources for the specified id, // e.g, unmounting layered filesystem. - Put(id string) + Put(id string) error // Exists returns whether a filesystem layer with the specified // ID exists on this driver. Exists(id string) bool diff --git a/daemon/graphdriver/overlay/overlay.go b/daemon/graphdriver/overlay/overlay.go index c59d0ea8f..438ff55b4 100644 --- a/daemon/graphdriver/overlay/overlay.go +++ b/daemon/graphdriver/overlay/overlay.go @@ -299,7 +299,7 @@ func (d *Driver) Get(id string, mountLabel string) (string, error) { return mount.path, nil } -func (d *Driver) Put(id string) { +func (d *Driver) Put(id string) error { // Protect the d.active from concurrent access d.Lock() defer d.Unlock() @@ -307,21 +307,23 @@ func (d *Driver) Put(id string) { mount := d.active[id] if mount == nil { log.Debugf("Put on a non-mounted device %s", id) - return + return nil } mount.count-- if mount.count > 0 { - return + return nil } + defer delete(d.active, id) if mount.mounted { - if err := syscall.Unmount(mount.path, 0); err != nil { + err := syscall.Unmount(mount.path, 0) + if err != nil { log.Debugf("Failed to unmount %s overlay: %v", id, err) } + return err } - - delete(d.active, id) + return nil } func (d *Driver) ApplyDiff(id string, parent string, diff archive.ArchiveReader) (size int64, err error) { diff --git a/daemon/graphdriver/vfs/driver.go b/daemon/graphdriver/vfs/driver.go index 0cffb1ffd..fe4d38230 100644 --- a/daemon/graphdriver/vfs/driver.go +++ b/daemon/graphdriver/vfs/driver.go @@ -83,9 +83,10 @@ func (d *Driver) Get(id, mountLabel string) (string, error) { return dir, nil } -func (d *Driver) Put(id string) { +func (d *Driver) Put(id string) error { // The vfs driver has no runtime resources (e.g. mounts) // to clean up, so we don't need anything here + return nil } func (d *Driver) Exists(id string) bool { From 79910625f0a7aa76590e4362817dda22f28343aa Mon Sep 17 00:00:00 2001 From: Josh Hawn Date: Sun, 7 Dec 2014 23:25:31 -0800 Subject: [PATCH 231/513] Adds Docker Image v1 Spec Documention Many iterations have gone into documenting a v1 specification of Docker's Image format. v1 Image spec: clarify parent field - metalivedev pointed out that the description was ambiguous, so I've removed mention that it was randomly generated. It IS the ID of the parent image. Updated v1 image specificatino documentation - More complete details and deprication notifications for each field in the JSON metadata of an image. - Details on the format for packaging combined Image JSON + Filesystem Changeset archives for all layers of an image. Clarify description of an image "Layer" in v1 spec Updated intro of image v1 spec Updated image v1 spec after more review - Removed description of "Image" from the terminology section. The entire document is meant to serve this purpose. - Updated the definition of "Image Filesystem Changeset". - Clarified the level of randomness needed for generating image IDs. - Updated the description of "Image Checksum". - Added term descriptions for "Repository" and "Tag" - Removed extraneous/implementation-specific fields from the Image JSON example file and field descriptions: - removed "container_config" and "docker_version" fields. - Added missing "author" field example and description. - Removed extraneous/implementation-specific fields from the "config" struct example and description: - removed "Hostname", "Domainname", "Cpuset", "AttachStdin", "AttachStdout", "AttachStderr", "PortSpecs", "Tty", "OpenStdin", "StdinOnce", "Image", "NetworkDisabled", and "OnBuild". - Updated example Image JSON config with better example values for "Env", "Cmd", "Volumes", "WorkingDir", "Entrypoint", "CpuShares", "Memory", "MemorySwap", and "User". - Added notices that any fields not specified are to be considered as implementation specific and should be ignored my implementations which are unable to interpret them. - Updated example of creating layer filesystem changesets to use less formal language. - Listed more details in the section regarding extraction of a bundle of image layers into the root filesystem of a container. - Updated the closing mention of Docker as an evolving implementation. More updates to the v1 image spec - Added line wrapping after 80 columns per line to adhere to documentation style guides, as pointed out by @jamtur01 - Removed references to any specific docker commands, updated a few descriptions or drop repeated statements, as pointed out by @cpuguy83 Cleanup image v1 spec draft after fredlf comments Address comments by mmdriley on v1 image spec Improve description of image v1 spec 'config.User` - Improves description of image v1 specification for the 'User' runtime parameter after recomendations by tianon. Docker-DCO-1.1-Signed-off-by: Josh Hawn (github: jlhawn) --- image/spec/v1.md | 573 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 573 insertions(+) create mode 100644 image/spec/v1.md diff --git a/image/spec/v1.md b/image/spec/v1.md new file mode 100644 index 000000000..2d7c4606f --- /dev/null +++ b/image/spec/v1.md @@ -0,0 +1,573 @@ +# Docker Image Specification v1.0.0 + +An *Image* is an ordered collection of root filesystem changes and the +corresponding execution parameters for use within a container runtime. This +specification outlines the format of these filesystem changes and corresponding +parameters and describes how to create and use them for use with a container +runtime and execution tool. + +## Terminology + +This specification uses the following terms: + +
+
+ Layer +
+
+ Images are composed of layers. Image layer is a general + term which may be used to refer to one or both of the following: + +
    +
  1. The metadata for the layer, described in the JSON format.
  2. +
  3. The filesystem changes described by a layer.
  4. +
+ + To refer to the former you may use the term Layer JSON or + Layer Metadata. To refer to the latter you may use the term + Image Filesystem Changeset or Image Diff. +
+
+ Image JSON +
+
+ Each layer has an associated A JSON structure which describes some + basic information about the image such as date created, author, and the + ID of its parent image as well as execution/runtime configuration like + its entry point, default arguments, CPU/memory shares, networking, and + volumes. +
+
+ Image Filesystem Changeset +
+
+ Each layer has an archive of the files which have been added, changed, + or deleted relative to its parent layer. Using a layer-based or union + filesystem such as AUFS, or by computing the diff from filesystem + snapshots, the filesystem changeset can be used to present a series of + image layers as if they were one cohesive filesystem. +
+
+ Image ID +
+
+ Each layer is given an ID upon its creation. It is + represented as a hexidecimal encoding of 256 bits, e.g., + a9561eb1b190625c9adb5a9513e72c4dedafc1cb2d4c5236c9a6957ec7dfd5a9. + Image IDs should be sufficiently random so as to be globally unique. + 32 bytes read from /dev/urandom is sufficient for all + practical purposes. Alternatively, an image ID may be derived as a + cryptographic hash of image contents as the result is considered + indistinguishable from random. The choice is left up to implementors. +
+
+ Image Parent +
+
+ Most layer metadata structs contain a parent field which + refers to the Image from which another directly descends. An image + contains a separate JSON metadata file and set of changes relative to + the filesystem of its parent image. Image Ancestor and + Image Descendant are also common terms. +
+
+ Image Checksum +
+
+ Layer metadata structs contain a cryptographic hash of the contents of + the layer's filesystem changeset. Though the set of changes exists as a + simple Tar archive, two archives with identical filenames and content + will have different SHA digests if the last-access or last-modified + times of any entries differ. For this reason, image checksums are + generated using the TarSum algorithm which produces a cryptographic + hash of file contents and selected headers only. Details of this + algorithm are described in the separate [TarSum specification](https://github.com/docker/docker/blob/master/pkg/tarsum/tarsum_spec.md). +
+
+ Tag +
+
+ A tag serves to map a descriptive, user-given name to any single image + ID. An image name suffix (the name component after :) is + often referred to as a tag as well, though it strictly refers to the + full name of an image. Acceptable values for a tag suffix are + implementation specific, but they SHOULD be limited to the set of + alphanumeric characters [a-zA-z0-9], punctuation + characters [._-], and MUST NOT contain a : + character. +
+
+ Repository +
+
+ A collection of tags grouped under a common prefix (the name component + before :). For example, in an image tagged with the name + my-app:3.1.4, my-app is the Repository + component of the name. Acceptable values for repository name are + implementation specific, but they SHOULD be limited to the set of + alphanumeric characters [a-zA-z0-9], and punctuation + characters [._-], however it MAY contain additional + / and : characters for organizational + purposes, with the last : character being interpreted + dividing the repository component of the name from the tag suffic + component. +
+
+ +## Image JSON Schema + +Here is an example image JSON file: + +``` +{ + "id": "a9561eb1b190625c9adb5a9513e72c4dedafc1cb2d4c5236c9a6957ec7dfd5a9", + "parent": "c6e3cedcda2e3982a1a6760e178355e8e65f7b80e4e5248743fa3549d284e024", + "checksum": "tarsum.v1+sha256:e58fcf7418d2390dec8e8fb69d88c06ec07039d651fedc3aa72af9972e7d046b", + "created": "2014-10-13T21:19:18.674353812Z", + "author": "Alyssa P. Hacker <alyspdev@example.com>", + "architecture": "amd64", + "os": "linux", + "Size": 271828, + "config": { + "User": "alice", + "Memory": 2048, + "MemorySwap": 4096, + "CpuShares": 8, + "ExposedPorts": { + "8080/tcp": {} + }, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "FOO=docker_is_a_really", + "BAR=great_tool_you_know" + ], + "Entrypoint": [ + "/bin/my-app-binary" + ], + "Cmd": [ + "--foreground", + "--config", + "/etc/my-app.d/default.cfg" + ], + "Volumes": { + "/var/job-result-data": {}, + "/var/log/my-app-logs": {}, + }, + "WorkingDir": "/home/alice", + } +} +``` + +### Image JSON Field Descriptions + +
+
+ id string +
+
+ Randomly generated, 256-bit, hexadecimal encoded. Uniquely identifies + the image. +
+
+ parent string +
+
+ ID of the parent image. If there is no parent image then this field + should be omitted. A collection of images may share many of the same + ancestor layers. This organizational structure is strictly a tree with + any one layer having either no parent or a single parent and zero or + more decendent layers. Cycles are not allowed and implementations + should be careful to avoid creating them or iterating through a cycle + indefinitely. +
+
+ created string +
+
+ ISO-8601 formatted combined date and time at which the image was + created. +
+
+ author string +
+
+ Gives the name and/or email address of the person or entity which + created and is responsible for maintaining the image. +
+
+ architecture string +
+
+ The CPU architecture which the binaries in this image are built to run + on. Possible values include: +
    +
  • 386
  • +
  • amd64
  • +
  • arm
  • +
+ More values may be supported in the future and any of these may or may + not be supported by a given container runtime implementation. +
+
+ os string +
+
+ The name of the operating system which the image is built to run on. + Possible values include: +
    +
  • darwin
  • +
  • freebsd
  • +
  • linux
  • +
+ More values may be supported in the future and any of these may or may + not be supported by a given container runtime implementation. +
+
+ checksum string +
+
+ Image Checksum of the filesystem changeset associated with the image + layer. +
+
+ Size integer +
+
+ The size in bytes of the filesystem changeset associated with the image + layer. +
+
+ config struct +
+
+ The execution parameters which should be used as a base when running a + container using the image. This field can be null, in + which case any execution parameters should be specified at creation of + the image. + +

Container RunConfig Field Descriptions

+ +
+
+ User string +
+
+

The username or UID which the process in the container should + run as. This acts as a default value to use when the value is + not specified when creating a container.

+ +

All of the following are valid:

+ +
    +
  • user
  • +
  • uid
  • +
  • user:group
  • +
  • uid:gid
  • +
  • uid:group
  • +
  • user:gid
  • +
+ +

If group/gid is not specified, the + default group and supplementary groups of the given + user/uid in /etc/passwd + from the container are applied.

+
+
+ Memory integer +
+
+ Memory limit (in bytes). This acts as a default value to use + when the value is not specified when creating a container. +
+
+ MemorySwap integer +
+
+ Total memory usage (memory + swap); set to -1 to + disable swap. This acts as a default value to use when the + value is not specified when creating a container. +
+
+ CpuShares integer +
+
+ CPU shares (relative weight vs. other containers). This acts as + a default value to use when the value is not specified when + creating a container. +
+
+ ExposedPorts struct +
+
+ A set of ports to expose from a container running this image. + This JSON structure value is unusual because it is a direct + JSON serialization of the Go type + map[string]struct{} and is represented in JSON as + an object mapping its keys to an empty object. Here is an + example: + +
{
+    "8080": {},
+    "53/udp": {},
+    "2356/tcp": {}
+}
+ + Its keys can be in the format of: +
    +
  • + "port/tcp" +
  • +
  • + "port/udp" +
  • +
  • + "port" +
  • +
+ with the default protocol being "tcp" if not + specified. + + These values act as defaults and are merged with any specified + when creating a container. +
+
+ Env array of strings +
+
+ Entries are in the format of VARNAME="var value". + These values act as defaults and are merged with any specified + when creating a container. +
+
+ Entrypoint array of strings +
+
+ A list of arguments to use as the command to execute when the + container starts. This value acts as a default and is replaced + by an entrypoint specified when creating a container. +
+
+ Cmd array of strings +
+
+ Default arguments to the entry point of the container. These + values act as defaults and are replaced with any specified when + creating a container. If an Entrypoint value is + not specified, then the first entry of the Cmd + array should be interpreted as the executable to run. +
+
+ Volumes struct +
+
+ A set of directories which should be created as data volumes in + a container running this image. This JSON structure value is + unusual because it is a direct JSON serialization of the Go + type map[string]struct{} and is represented in + JSON as an object mapping its keys to an empty object. Here is + an example: +
{
+    "/var/my-app-data/": {},
+    "/etc/some-config.d/": {},
+}
+
+
+ WorkingDir string +
+
+ Sets the current working directory of the entry point process + in the container. This value acts as a default and is replaced + by a working directory specified when creating a container. +
+
+
+
+ +Any extra fields in the Image JSON struct are considered implementation +specific and should be ignored by any implementations which are unable to +interpret them. + +## Creating an Image Filesystem Changeset + +An example of creating an Image Filesystem Changeset follows. + +An image root filesystem is first creating as an empty directory named with the +ID of the image being created. Here is the initial empty directory structure +for the changeset for an image with ID `c3167915dc9d` ([real IDs are much +longer](#id_desc), but this example use a truncated one here for brevity. +Implementations need not name the rootfs directory in this way but it may be +convenient for keeping record of a large number of image layers.): + +``` +c3167915dc9d/ +``` + +Files and directories are then created: + +``` +c3167915dc9d/ + etc/ + my-app-config + bin/ + my-app-binary + my-app-tools +``` + +The `c3167915dc9d` directory is then committed as a plain Tar archive with +entries for the following files: + +``` +etc/my-app-config +bin/my-app-binary +bin/my-app-tools +``` + +The TarSum checksum for the archive file is then computed and placed in the +JSON metadata along with the execution parameters. + +To make changes to the filesystem of this container image, create a new +directory named with a new ID, such as `f60c56784b83`, and initialize it with +a snapshot of the parent image's root filesystem, so that the directory is +identical to that of `c3167915dc9d`. NOTE: a copy-on-write or union filesystem +can make this very efficient: + +``` +f60c56784b83/ + etc/ + my-app-config + bin/ + my-app-binary + my-app-tools +``` + +This example change is going add a configuration directory at `/etc/my-app.d` +which contains a default config file. There's also a change to the +`my-app-tools` binary to handle the config layout change. The `f60c56784b83` +directory then looks like this: + +``` +f60c56784b83/ + etc/ + my-app.d/ + default.cfg + bin/ + my-app-binary + my-app-tools +``` + +This reflects the removal of `/etc/my-app-config` and creation of a file and +directory at `/etc/my-app.d/default.cfg`. `/bin/my-app-tools` has also been +replaced with an updated version. Before committing this directory to a +changeset, because it has a parent image, it is first compared with the +directory tree of the parent snapshot, `f60c56784b83`, looking for files and +directories that have been added, modified, or removed. The following changeset +is found: + +``` +Added: /etc/my-app.d/default.cfg +Modified: /bin/my-app-tools +Deleted: /etc/my-app-config +``` + +A Tar Archive is then created which contains *only* this changeset: The added +and modified files and directories in their entirety, and for each deleted item +an entry for an empty file at the same location but with the basename of the +deleted file or directory prefixed with `.wh.`. The filenames prefixed with +`.wh.` are known as "whiteout" files. NOTE: For this reason, it is not possible +to create an image root filesystem which contains a file or directory with a +name beginning with `.wh.`. The resulting Tar archive for `f60c56784b83` has +the following entries: + +``` +/etc/my-app.d/default.cfg +/bin/my-app-tools +/etc/.wh.my-app-config +``` + +Any given image is likely to be composed of several of these Image Filesystem +Changeset tar archives. + +## Combined Image JSON + Filesystem Changeset Format + +There is also a format for a single archive which contains complete information +about an image, including: + + - repository names/tags + - all image layer JSON files + - all tar archives of each layer filesystem changesets + +For example, here's what the full archive of `library/busybox` is (displayed in +`tree` format): + +``` +. +├── 5785b62b697b99a5af6cd5d0aabc804d5748abbb6d3d07da5d1d3795f2dcc83e +│   ├── VERSION +│   ├── json +│   └── layer.tar +├── a7b8b41220991bfc754d7ad445ad27b7f272ab8b4a2c175b9512b97471d02a8a +│   ├── VERSION +│   ├── json +│   └── layer.tar +├── a936027c5ca8bf8f517923169a233e391cbb38469a75de8383b5228dc2d26ceb +│   ├── VERSION +│   ├── json +│   └── layer.tar +├── f60c56784b832dd990022afc120b8136ab3da9528094752ae13fe63a2d28dc8c +│   ├── VERSION +│   ├── json +│   └── layer.tar +└── repositories +``` + +There are one or more directories named with the ID for each layer in a full +image. Each of these directories contains 3 files: + + * `VERSION` - The schema version of the `json` file + * `json` - The JSON metadata for an image layer + * `layer.tar` - The Tar archive of the filesystem changeset for an image + layer. + +The content of the `VERSION` files is simply the semantic version of the JSON +metadata schema: + +``` +1.0 +``` + +And the `repositories` file is another JSON file which describes names/tags: + +``` +{ + "busybox":{ + "latest":"5785b62b697b99a5af6cd5d0aabc804d5748abbb6d3d07da5d1d3795f2dcc83e" + } +} +``` + +Every key in this object is the name of a repository, and maps to a collection +of tag suffixes. Each tag maps to the ID of the image represented by that tag. + +## Loading an Image Filesystem Changeset + +Unpacking a bundle of image layer JSON files and their corresponding filesystem +changesets can be done using a series of steps: + +1. Follow the parent IDs of image layers to find the root ancestor (an image +with no parent ID specified). +2. For every image layer, in order from root ancestor and descending down, +extract the contents of that layer's filesystem changeset archive into a +directory which will be used as the root of a container filesystem. + + - Extract all contents of each archive. + - Walk the directory tree once more, removing any files with the prefix + `.wh.` and the corresponding file or directory named without this prefix. + + +## Implementations + +This specification is an admittedly imperfect description of an +imperfectly-understood problem. The Docker project is, in turn, an attempt to +implement this specification. Our goal and our execution toward it will evolve +over time, but our primary concern in this specification and in our +implementation is compatibility and interoperability. From 115d8bac6017cc5703fe34e679f1a30a4759a9ff Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Mon, 12 Jan 2015 10:38:15 -0800 Subject: [PATCH 232/513] Update libcontainer to 6460fd79667466d2d9ec03f77f3 Signed-off-by: Michael Crosby --- project/vendor.sh | 2 +- .../libcontainer/cgroups/fs/apply_raw.go | 2 +- .../docker/libcontainer/cgroups/utils.go | 4 +- .../github.com/docker/libcontainer/config.go | 4 + .../libcontainer/integration/exec_test.go | 35 ++- .../docker/libcontainer/namespaces/exec.go | 35 +++ .../docker/libcontainer/namespaces/init.go | 10 +- .../libcontainer/namespaces/nsenter/nsenter.c | 73 ++++--- .../namespaces/nsenter/nsenter_test.go | 70 ++++++ .../libcontainer/sample_configs/host-pid.json | 200 ++++++++++++++++++ 10 files changed, 395 insertions(+), 40 deletions(-) create mode 100644 vendor/src/github.com/docker/libcontainer/namespaces/nsenter/nsenter_test.go create mode 100644 vendor/src/github.com/docker/libcontainer/sample_configs/host-pid.json diff --git a/project/vendor.sh b/project/vendor.sh index 94ad5e337..e6a43e0f2 100755 --- a/project/vendor.sh +++ b/project/vendor.sh @@ -68,7 +68,7 @@ if [ "$1" = '--go' ]; then mv tmp-tar src/code.google.com/p/go/src/pkg/archive/tar fi -clone git github.com/docker/libcontainer be02944484da197166020d6b3f08a19d7d7d244c +clone git github.com/docker/libcontainer 6460fd79667466d2d9ec03f77f319a241c58d40b # see src/github.com/docker/libcontainer/update-vendor.sh which is the "source of truth" for libcontainer deps (just like this file) rm -rf src/github.com/docker/libcontainer/vendor eval "$(grep '^clone ' src/github.com/docker/libcontainer/update-vendor.sh | grep -v 'github.com/codegangsta/cli')" diff --git a/vendor/src/github.com/docker/libcontainer/cgroups/fs/apply_raw.go b/vendor/src/github.com/docker/libcontainer/cgroups/fs/apply_raw.go index 6f85793dd..f05377f25 100644 --- a/vendor/src/github.com/docker/libcontainer/cgroups/fs/apply_raw.go +++ b/vendor/src/github.com/docker/libcontainer/cgroups/fs/apply_raw.go @@ -105,7 +105,7 @@ func GetStats(systemPaths map[string]string) (*cgroups.Stats, error) { stats := cgroups.NewStats() for name, path := range systemPaths { sys, ok := subsystems[name] - if !ok { + if !ok || !cgroups.PathExists(path) { continue } if err := sys.GetStats(path, stats); err != nil { diff --git a/vendor/src/github.com/docker/libcontainer/cgroups/utils.go b/vendor/src/github.com/docker/libcontainer/cgroups/utils.go index 5753ca453..a360904cc 100644 --- a/vendor/src/github.com/docker/libcontainer/cgroups/utils.go +++ b/vendor/src/github.com/docker/libcontainer/cgroups/utils.go @@ -174,7 +174,7 @@ func ParseCgroupFile(subsystem string, r io.Reader) (string, error) { return "", NewNotFoundError(subsystem) } -func pathExists(path string) bool { +func PathExists(path string) bool { if _, err := os.Stat(path); err != nil { return false } @@ -183,7 +183,7 @@ func pathExists(path string) bool { func EnterPid(cgroupPaths map[string]string, pid int) error { for _, path := range cgroupPaths { - if pathExists(path) { + if PathExists(path) { if err := ioutil.WriteFile(filepath.Join(path, "cgroup.procs"), []byte(strconv.Itoa(pid)), 0700); err != nil { return err diff --git a/vendor/src/github.com/docker/libcontainer/config.go b/vendor/src/github.com/docker/libcontainer/config.go index 7ab9a9a76..643601ada 100644 --- a/vendor/src/github.com/docker/libcontainer/config.go +++ b/vendor/src/github.com/docker/libcontainer/config.go @@ -120,6 +120,10 @@ type Config struct { // Rlimits specifies the resource limits, such as max open files, to set in the container // If Rlimits are not set, the container will inherit rlimits from the parent process Rlimits []Rlimit `json:"rlimits,omitempty"` + + // AdditionalGroups specifies the gids that should be added to supplementary groups + // in addition to those that the user belongs to. + AdditionalGroups []int `json:"additional_groups,omitempty"` } // Routes can be specified to create entries in the route table as the container is started diff --git a/vendor/src/github.com/docker/libcontainer/integration/exec_test.go b/vendor/src/github.com/docker/libcontainer/integration/exec_test.go index f0728c581..fb1d1d7a1 100644 --- a/vendor/src/github.com/docker/libcontainer/integration/exec_test.go +++ b/vendor/src/github.com/docker/libcontainer/integration/exec_test.go @@ -67,7 +67,7 @@ func TestIPCPrivate(t *testing.T) { } if actual := strings.Trim(buffers.Stdout.String(), "\n"); actual == l { - t.Fatalf("ipc link should be private to the conatiner but equals host %q %q", actual, l) + t.Fatalf("ipc link should be private to the container but equals host %q %q", actual, l) } } @@ -152,7 +152,7 @@ func TestIPCBadPath(t *testing.T) { _, _, err = runContainer(config, "", "true") if err == nil { - t.Fatal("container succeded with bad ipc path") + t.Fatal("container succeeded with bad ipc path") } } @@ -176,3 +176,34 @@ func TestRlimit(t *testing.T) { t.Fatalf("expected rlimit to be 1024, got %s", limit) } } + +func TestPIDNSPrivate(t *testing.T) { + if testing.Short() { + return + } + + rootfs, err := newRootFs() + if err != nil { + t.Fatal(err) + } + defer remove(rootfs) + + l, err := os.Readlink("/proc/1/ns/pid") + if err != nil { + t.Fatal(err) + } + + config := newTemplateConfig(rootfs) + buffers, exitCode, err := runContainer(config, "", "readlink", "/proc/self/ns/pid") + if err != nil { + t.Fatal(err) + } + + if exitCode != 0 { + t.Fatalf("exit code not 0. code %d stderr %q", exitCode, buffers.Stderr) + } + + if actual := strings.Trim(buffers.Stdout.String(), "\n"); actual == l { + t.Fatalf("pid link should be private to the container but equals host %q %q", actual, l) + } +} diff --git a/vendor/src/github.com/docker/libcontainer/namespaces/exec.go b/vendor/src/github.com/docker/libcontainer/namespaces/exec.go index b7873edd0..abe67ae25 100644 --- a/vendor/src/github.com/docker/libcontainer/namespaces/exec.go +++ b/vendor/src/github.com/docker/libcontainer/namespaces/exec.go @@ -110,9 +110,44 @@ func Exec(container *libcontainer.Config, stdin io.Reader, stdout, stderr io.Wri return -1, err } } + if !container.Namespaces.Contains(libcontainer.NEWPID) { + killAllPids(container) + } return command.ProcessState.Sys().(syscall.WaitStatus).ExitStatus(), nil } +// killAllPids itterates over all of the container's processes +// sending a SIGKILL to each process. +func killAllPids(container *libcontainer.Config) error { + var ( + procs []*os.Process + freeze = fs.Freeze + getPids = fs.GetPids + ) + if systemd.UseSystemd() { + freeze = systemd.Freeze + getPids = systemd.GetPids + } + freeze(container.Cgroups, cgroups.Frozen) + pids, err := getPids(container.Cgroups) + if err != nil { + return err + } + for _, pid := range pids { + // TODO: log err without aborting if we are unable to find + // a single PID + if p, err := os.FindProcess(pid); err == nil { + procs = append(procs, p) + p.Kill() + } + } + freeze(container.Cgroups, cgroups.Thawed) + for _, p := range procs { + p.Wait() + } + return err +} + // DefaultCreateCommand will return an exec.Cmd with the Cloneflags set to the proper namespaces // defined on the container's configuration and use the current binary as the init with the // args provided diff --git a/vendor/src/github.com/docker/libcontainer/namespaces/init.go b/vendor/src/github.com/docker/libcontainer/namespaces/init.go index a4400bddb..0a4ff1962 100644 --- a/vendor/src/github.com/docker/libcontainer/namespaces/init.go +++ b/vendor/src/github.com/docker/libcontainer/namespaces/init.go @@ -170,7 +170,7 @@ func RestoreParentDeathSignal(old int) error { } // SetupUser changes the groups, gid, and uid for the user inside the container -func SetupUser(u string) error { +func SetupUser(container *libcontainer.Config) error { // Set up defaults. defaultExecUser := user.ExecUser{ Uid: syscall.Getuid(), @@ -188,12 +188,14 @@ func SetupUser(u string) error { return err } - execUser, err := user.GetExecUserPath(u, &defaultExecUser, passwdPath, groupPath) + execUser, err := user.GetExecUserPath(container.User, &defaultExecUser, passwdPath, groupPath) if err != nil { return fmt.Errorf("get supplementary groups %s", err) } - if err := syscall.Setgroups(execUser.Sgids); err != nil { + suppGroups := append(execUser.Sgids, container.AdditionalGroups...) + + if err := syscall.Setgroups(suppGroups); err != nil { return fmt.Errorf("setgroups %s", err) } @@ -273,7 +275,7 @@ func FinalizeNamespace(container *libcontainer.Config) error { return fmt.Errorf("set keep caps %s", err) } - if err := SetupUser(container.User); err != nil { + if err := SetupUser(container); err != nil { return fmt.Errorf("setup user %s", err) } diff --git a/vendor/src/github.com/docker/libcontainer/namespaces/nsenter/nsenter.c b/vendor/src/github.com/docker/libcontainer/namespaces/nsenter/nsenter.c index b735b1fa2..9782702dc 100644 --- a/vendor/src/github.com/docker/libcontainer/namespaces/nsenter/nsenter.c +++ b/vendor/src/github.com/docker/libcontainer/namespaces/nsenter/nsenter.c @@ -15,6 +15,8 @@ #include #include +#define pr_perror(fmt, ...) fprintf(stderr, "nsenter: " fmt ": %m\n", ##__VA_ARGS__) + static const kBufSize = 256; static const char *kNsEnter = "nsenter"; @@ -22,6 +24,10 @@ void get_args(int *argc, char ***argv) { // Read argv int fd = open("/proc/self/cmdline", O_RDONLY); + if (fd < 0) { + pr_perror("Unable to open /proc/self/cmdline"); + exit(1); + } // Read the whole commandline. ssize_t contents_size = 0; @@ -34,6 +40,10 @@ void get_args(int *argc, char ***argv) bytes_read = read(fd, contents + contents_offset, contents_size - contents_offset); + if (bytes_read < 0) { + pr_perror("Unable to read from /proc/self/cmdline"); + exit(1); + } contents_offset += bytes_read; } while (bytes_read > 0); @@ -91,8 +101,7 @@ void nsenter() #ifdef PR_SET_CHILD_SUBREAPER if (prctl(PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0) == -1) { - fprintf(stderr, "nsenter: failed to set child subreaper: %s", - strerror(errno)); + pr_perror("Failed to set child subreaper"); exit(1); } #endif @@ -124,9 +133,8 @@ void nsenter() init_pid = strtol(init_pid_str, NULL, 10); if ((init_pid == 0 && errno == EINVAL) || errno == ERANGE) { - fprintf(stderr, - "nsenter: Failed to parse PID from \"%s\" with output \"%d\" and error: \"%s\"\n", - init_pid_str, init_pid, strerror(errno)); + pr_perror("Failed to parse PID from \"%s\" with output \"%d\"", + init_pid_str, init_pid); print_usage(); exit(1); } @@ -135,7 +143,7 @@ void nsenter() argv += 3; if (setsid() == -1) { - fprintf(stderr, "setsid failed. Error: %s\n", strerror(errno)); + pr_perror("setsid failed"); exit(1); } // before we setns we need to dup the console @@ -143,9 +151,7 @@ void nsenter() if (console != NULL) { consolefd = open(console, O_RDWR); if (consolefd < 0) { - fprintf(stderr, - "nsenter: failed to open console %s %s\n", - console, strerror(errno)); + pr_perror("Failed to open console %s", console); exit(1); } } @@ -154,51 +160,60 @@ void nsenter() memset(ns_dir, 0, PATH_MAX); snprintf(ns_dir, PATH_MAX - 1, "/proc/%d/ns/", init_pid); + int ns_dir_fd; + ns_dir_fd = open(ns_dir, O_RDONLY | O_DIRECTORY); + if (ns_dir_fd < 0) { + pr_perror("Unable to open %s", ns_dir); + exit(1); + } + char *namespaces[] = { "ipc", "uts", "net", "pid", "mnt" }; const int num = sizeof(namespaces) / sizeof(char *); int i; for (i = 0; i < num; i++) { - char buf[PATH_MAX]; - memset(buf, 0, PATH_MAX); - snprintf(buf, PATH_MAX - 1, "%s%s", ns_dir, namespaces[i]); - int fd = open(buf, O_RDONLY); - if (fd == -1) { - // Ignore nonexistent namespaces. + // A zombie process has links on namespaces, but they can't be opened + struct stat st; + if (fstatat(ns_dir_fd, namespaces[i], &st, AT_SYMLINK_NOFOLLOW) == -1) { if (errno == ENOENT) continue; + pr_perror("Failed to stat ns file %s for ns %s", + ns_dir, namespaces[i]); + exit(1); + } - fprintf(stderr, - "nsenter: Failed to open ns file \"%s\" for ns \"%s\" with error: \"%s\"\n", - buf, namespaces[i], strerror(errno)); + int fd = openat(ns_dir_fd, namespaces[i], O_RDONLY); + if (fd == -1) { + pr_perror("Failed to open ns file %s for ns %s", + ns_dir, namespaces[i]); exit(1); } // Set the namespace. if (setns(fd, 0) == -1) { - fprintf(stderr, - "nsenter: Failed to setns for \"%s\" with error: \"%s\"\n", - namespaces[i], strerror(errno)); + pr_perror("Failed to setns for %s", namespaces[i]); exit(1); } close(fd); } + close(ns_dir_fd); // We must fork to actually enter the PID namespace. int child = fork(); + if (child == -1) { + pr_perror("Unable to fork a process"); + exit(1); + } if (child == 0) { if (consolefd != -1) { if (dup2(consolefd, STDIN_FILENO) != 0) { - fprintf(stderr, "nsenter: failed to dup 0 %s\n", - strerror(errno)); + pr_perror("Failed to dup 0"); exit(1); } if (dup2(consolefd, STDOUT_FILENO) != STDOUT_FILENO) { - fprintf(stderr, "nsenter: failed to dup 1 %s\n", - strerror(errno)); + pr_perror("Failed to dup 1"); exit(1); } if (dup2(consolefd, STDERR_FILENO) != STDERR_FILENO) { - fprintf(stderr, "nsenter: failed to dup 2 %s\n", - strerror(errno)); + pr_perror("Failed to dup 2\n"); exit(1); } } @@ -208,9 +223,7 @@ void nsenter() // Parent, wait for the child. int status = 0; if (waitpid(child, &status, 0) == -1) { - fprintf(stderr, - "nsenter: Failed to waitpid with error: \"%s\"\n", - strerror(errno)); + pr_perror("nsenter: Failed to waitpid with error"); exit(1); } // Forward the child's exit code or re-send its death signal. diff --git a/vendor/src/github.com/docker/libcontainer/namespaces/nsenter/nsenter_test.go b/vendor/src/github.com/docker/libcontainer/namespaces/nsenter/nsenter_test.go new file mode 100644 index 000000000..85ee5d672 --- /dev/null +++ b/vendor/src/github.com/docker/libcontainer/namespaces/nsenter/nsenter_test.go @@ -0,0 +1,70 @@ +package nsenter + +import ( + "fmt" + "os" + "os/exec" + "os/signal" + "strings" + "syscall" + "testing" +) + +func TestNsenterAlivePid(t *testing.T) { + args := []string{"nsenter-exec", "--nspid", fmt.Sprintf("%d", os.Getpid())} + + cmd := &exec.Cmd{ + Path: os.Args[0], + Args: args, + } + + err := cmd.Run() + if err != nil { + t.Fatal("nsenter exits with a non-zero exit status") + } +} + +func TestNsenterInvalidPid(t *testing.T) { + args := []string{"nsenter-exec", "--nspid", "-1"} + + cmd := &exec.Cmd{ + Path: os.Args[0], + Args: args, + } + + err := cmd.Run() + if err == nil { + t.Fatal("nsenter exits with a zero exit status") + } +} + +func TestNsenterDeadPid(t *testing.T) { + + c := make(chan os.Signal) + signal.Notify(c, syscall.SIGCHLD) + dead_cmd := exec.Command("true") + if err := dead_cmd.Start(); err != nil { + t.Fatal(err) + } + defer dead_cmd.Wait() + <-c // dead_cmd is zombie + + args := []string{"nsenter-exec", "--nspid", fmt.Sprintf("%d", dead_cmd.Process.Pid)} + + cmd := &exec.Cmd{ + Path: os.Args[0], + Args: args, + } + + err := cmd.Run() + if err == nil { + t.Fatal("nsenter exits with a zero exit status") + } +} + +func init() { + if strings.HasPrefix(os.Args[0], "nsenter-") { + os.Exit(0) + } + return +} diff --git a/vendor/src/github.com/docker/libcontainer/sample_configs/host-pid.json b/vendor/src/github.com/docker/libcontainer/sample_configs/host-pid.json new file mode 100644 index 000000000..f47af930e --- /dev/null +++ b/vendor/src/github.com/docker/libcontainer/sample_configs/host-pid.json @@ -0,0 +1,200 @@ +{ + "capabilities": [ + "CHOWN", + "DAC_OVERRIDE", + "FOWNER", + "MKNOD", + "NET_RAW", + "SETGID", + "SETUID", + "SETFCAP", + "SETPCAP", + "NET_BIND_SERVICE", + "SYS_CHROOT", + "KILL" + ], + "cgroups": { + "allowed_devices": [ + { + "cgroup_permissions": "m", + "major_number": -1, + "minor_number": -1, + "type": 99 + }, + { + "cgroup_permissions": "m", + "major_number": -1, + "minor_number": -1, + "type": 98 + }, + { + "cgroup_permissions": "rwm", + "major_number": 5, + "minor_number": 1, + "path": "/dev/console", + "type": 99 + }, + { + "cgroup_permissions": "rwm", + "major_number": 4, + "path": "/dev/tty0", + "type": 99 + }, + { + "cgroup_permissions": "rwm", + "major_number": 4, + "minor_number": 1, + "path": "/dev/tty1", + "type": 99 + }, + { + "cgroup_permissions": "rwm", + "major_number": 136, + "minor_number": -1, + "type": 99 + }, + { + "cgroup_permissions": "rwm", + "major_number": 5, + "minor_number": 2, + "type": 99 + }, + { + "cgroup_permissions": "rwm", + "major_number": 10, + "minor_number": 200, + "type": 99 + }, + { + "cgroup_permissions": "rwm", + "file_mode": 438, + "major_number": 1, + "minor_number": 3, + "path": "/dev/null", + "type": 99 + }, + { + "cgroup_permissions": "rwm", + "file_mode": 438, + "major_number": 1, + "minor_number": 5, + "path": "/dev/zero", + "type": 99 + }, + { + "cgroup_permissions": "rwm", + "file_mode": 438, + "major_number": 1, + "minor_number": 7, + "path": "/dev/full", + "type": 99 + }, + { + "cgroup_permissions": "rwm", + "file_mode": 438, + "major_number": 5, + "path": "/dev/tty", + "type": 99 + }, + { + "cgroup_permissions": "rwm", + "file_mode": 438, + "major_number": 1, + "minor_number": 9, + "path": "/dev/urandom", + "type": 99 + }, + { + "cgroup_permissions": "rwm", + "file_mode": 438, + "major_number": 1, + "minor_number": 8, + "path": "/dev/random", + "type": 99 + } + ], + "name": "docker-koye", + "parent": "docker" + }, + "restrict_sys": true, + "mount_config": { + "device_nodes": [ + { + "cgroup_permissions": "rwm", + "file_mode": 438, + "major_number": 1, + "minor_number": 3, + "path": "/dev/null", + "type": 99 + }, + { + "cgroup_permissions": "rwm", + "file_mode": 438, + "major_number": 1, + "minor_number": 5, + "path": "/dev/zero", + "type": 99 + }, + { + "cgroup_permissions": "rwm", + "file_mode": 438, + "major_number": 1, + "minor_number": 7, + "path": "/dev/full", + "type": 99 + }, + { + "cgroup_permissions": "rwm", + "file_mode": 438, + "major_number": 5, + "path": "/dev/tty", + "type": 99 + }, + { + "cgroup_permissions": "rwm", + "file_mode": 438, + "major_number": 1, + "minor_number": 9, + "path": "/dev/urandom", + "type": 99 + }, + { + "cgroup_permissions": "rwm", + "file_mode": 438, + "major_number": 1, + "minor_number": 8, + "path": "/dev/random", + "type": 99 + } + ], + "mounts": [ + { + "type": "tmpfs", + "destination": "/tmp" + } + ] + }, + "environment": [ + "HOME=/", + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "HOSTNAME=koye", + "TERM=xterm" + ], + "hostname": "koye", + "namespaces": [ + {"type": "NEWIPC"}, + {"type": "NEWNET"}, + {"type": "NEWNS"}, + {"type": "NEWUTS"} + ], + "networks": [ + { + "address": "127.0.0.1/0", + "gateway": "localhost", + "mtu": 1500, + "type": "loopback" + } + ], + "tty": true, + "user": "daemon" +} From 2b51d1a167055834df331fb23c6cd373ebc06211 Mon Sep 17 00:00:00 2001 From: Vincent Batts Date: Mon, 12 Jan 2015 13:40:00 -0500 Subject: [PATCH 233/513] devmapper: remove newline string Signed-off-by: Vincent Batts --- daemon/graphdriver/devmapper/deviceset.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/graphdriver/devmapper/deviceset.go b/daemon/graphdriver/devmapper/deviceset.go index 078e31a1e..e0a5a952b 100644 --- a/daemon/graphdriver/devmapper/deviceset.go +++ b/daemon/graphdriver/devmapper/deviceset.go @@ -1424,7 +1424,7 @@ func (devices *DeviceSet) UnmountDevice(hash string) error { defer devices.Unlock() if info.mountCount == 0 { - return fmt.Errorf("UnmountDevice: device not-mounted id %s\n", hash) + return fmt.Errorf("UnmountDevice: device not-mounted id %s", hash) } info.mountCount-- From 582a79f00a1d87ba0debd8f3785d867c02451e5f Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Mon, 12 Jan 2015 10:43:33 -0800 Subject: [PATCH 234/513] Update lxc with libcontainer SetupUser change Signed-off-by: Michael Crosby --- daemon/execdriver/lxc/lxc_init_linux.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/daemon/execdriver/lxc/lxc_init_linux.go b/daemon/execdriver/lxc/lxc_init_linux.go index 78bdd11fb..956a283fc 100644 --- a/daemon/execdriver/lxc/lxc_init_linux.go +++ b/daemon/execdriver/lxc/lxc_init_linux.go @@ -2,6 +2,8 @@ package lxc import ( "fmt" + + "github.com/docker/libcontainer" "github.com/docker/libcontainer/namespaces" "github.com/docker/libcontainer/utils" ) @@ -10,14 +12,13 @@ func finalizeNamespace(args *InitArgs) error { if err := utils.CloseExecFrom(3); err != nil { return err } - - if err := namespaces.SetupUser(args.User); err != nil { + if err := namespaces.SetupUser(&libcontainer.Config{ + User: args.User, + }); err != nil { return fmt.Errorf("setup user %s", err) } - if err := setupWorkingDirectory(args); err != nil { return err } - return nil } From cd5902fc7bb0da36fedb6ff6a1bb8ef200b212d9 Mon Sep 17 00:00:00 2001 From: Jessica Frazelle Date: Mon, 12 Jan 2015 11:49:52 -0800 Subject: [PATCH 235/513] Fix range for go 1.3 Docker-DCO-1.1-Signed-off-by: Jessica Frazelle (github: jfrazelle) --- api/client/utils.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/client/utils.go b/api/client/utils.go index a65ceb352..86e221ebf 100644 --- a/api/client/utils.go +++ b/api/client/utils.go @@ -260,7 +260,7 @@ func (cli *DockerCli) monitorTtySize(id string, isExec bool) error { sigchan := make(chan os.Signal, 1) gosignal.Notify(sigchan, signal.SIGWINCH) go func() { - for range sigchan { + for _ = range sigchan { cli.resizeTty(id, isExec) } }() From 5f699a465dd428d6285080ca07cb4a6634952744 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Mon, 12 Jan 2015 21:48:44 +0100 Subject: [PATCH 236/513] Fix typo in error-message. This fixes a small typo in the errormessage for memory-swap. Signed-off-by: Sebastiaan van Stijn --- daemon/create.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/create.go b/daemon/create.go index c2ed6799d..f53461a45 100644 --- a/daemon/create.go +++ b/daemon/create.go @@ -31,7 +31,7 @@ func (daemon *Daemon) ContainerCreate(job *engine.Job) engine.Status { config.MemorySwap = -1 } if config.Memory > 0 && config.MemorySwap > 0 && config.MemorySwap < config.Memory { - return job.Errorf("Minimum memoryswap limit should larger than memory limit, see usage.\n") + return job.Errorf("Minimum memoryswap limit should be larger than memory limit, see usage.\n") } var hostConfig *runconfig.HostConfig From e5869b2b0bdf85d15813c714565fd49943c3d754 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Mon, 12 Jan 2015 22:47:36 +0000 Subject: [PATCH 237/513] update copyrights to 2015 Signed-off-by: Victor Vieux --- docs/mkdocs.yml | 2 +- docs/theme/mkdocs/footer.html | 4 ++-- pkg/mflag/LICENSE | 2 +- pkg/mflag/flag.go | 2 +- pkg/mflag/flag_test.go | 4 ++-- pkg/symlink/LICENSE.APACHE | 2 +- pkg/symlink/LICENSE.BSD | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 6d2ecac3a..73150cc44 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -18,7 +18,7 @@ use_absolute_urls: true theme_dir: ./theme/mkdocs/ theme_center_lead: false -copyright: Copyright © 2014, Docker, Inc. +copyright: Copyright © 2014-2015, Docker, Inc. google_analytics: ['UA-6096819-11', 'docker.io'] pages: diff --git a/docs/theme/mkdocs/footer.html b/docs/theme/mkdocs/footer.html index 69a4e6367..923ace519 100644 --- a/docs/theme/mkdocs/footer.html +++ b/docs/theme/mkdocs/footer.html @@ -101,7 +101,7 @@
- \ No newline at end of file + diff --git a/pkg/mflag/LICENSE b/pkg/mflag/LICENSE index ebcfbcc77..ac74d8f04 100644 --- a/pkg/mflag/LICENSE +++ b/pkg/mflag/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2014 The Docker & Go Authors. All rights reserved. +Copyright (c) 2014-2015 The Docker & Go Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are diff --git a/pkg/mflag/flag.go b/pkg/mflag/flag.go index 72addae63..1707b16fa 100644 --- a/pkg/mflag/flag.go +++ b/pkg/mflag/flag.go @@ -1,4 +1,4 @@ -// Copyright 2014 The Docker & Go Authors. All rights reserved. +// Copyright 2014-2015 The Docker & Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. diff --git a/pkg/mflag/flag_test.go b/pkg/mflag/flag_test.go index b8c0b305d..df640d148 100644 --- a/pkg/mflag/flag_test.go +++ b/pkg/mflag/flag_test.go @@ -1,4 +1,4 @@ -// Copyright 2014 The Docker & Go Authors. All rights reserved. +// Copyright 2014-2015 The Docker & Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. @@ -7,12 +7,12 @@ package mflag_test import ( "bytes" "fmt" - . "github.com/docker/docker/pkg/mflag" "os" "sort" "strings" "testing" "time" + . "github.com/docker/docker/pkg/mflag" ) // ResetForTesting clears all flag state and sets the usage function as directed. diff --git a/pkg/symlink/LICENSE.APACHE b/pkg/symlink/LICENSE.APACHE index 27448585a..9e4bd4dbe 100644 --- a/pkg/symlink/LICENSE.APACHE +++ b/pkg/symlink/LICENSE.APACHE @@ -176,7 +176,7 @@ END OF TERMS AND CONDITIONS - Copyright 2014 Docker, Inc. + Copyright 2014-2015 Docker, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/pkg/symlink/LICENSE.BSD b/pkg/symlink/LICENSE.BSD index ebcfbcc77..ac74d8f04 100644 --- a/pkg/symlink/LICENSE.BSD +++ b/pkg/symlink/LICENSE.BSD @@ -1,4 +1,4 @@ -Copyright (c) 2014 The Docker & Go Authors. All rights reserved. +Copyright (c) 2014-2015 The Docker & Go Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are From b70632ec532d4a948e227a646f8286d7c439cb4f Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Mon, 12 Jan 2015 22:52:19 +0000 Subject: [PATCH 238/513] fix issue with goimport Signed-off-by: Victor Vieux --- pkg/mflag/flag_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/mflag/flag_test.go b/pkg/mflag/flag_test.go index df640d148..85f32c8aa 100644 --- a/pkg/mflag/flag_test.go +++ b/pkg/mflag/flag_test.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package mflag_test +package mflag import ( "bytes" @@ -12,7 +12,6 @@ import ( "strings" "testing" "time" - . "github.com/docker/docker/pkg/mflag" ) // ResetForTesting clears all flag state and sets the usage function as directed. From 3873b19c31a882a3240fc04b63e23e8a557028c0 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Mon, 12 Jan 2015 15:01:09 -0800 Subject: [PATCH 239/513] Bumped docker-py version to latest release Signed-off-by: Joffrey F --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index a7d19928d..f46b029bf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -117,7 +117,7 @@ RUN git clone -b buildroot-2014.02 https://github.com/jpetazzo/docker-busybox.gi RUN curl -sSL -o /cirros.tar.gz https://github.com/ewindisch/docker-cirros/raw/1cded459668e8b9dbf4ef976c94c05add9bbd8e9/cirros-0.3.0-x86_64-lxc.tar.gz # Get the "docker-py" source so we can run their integration tests -RUN git clone -b 0.7.0 https://github.com/docker/docker-py.git /docker-py +RUN git clone -b 0.7.1 https://github.com/docker/docker-py.git /docker-py # Setup s3cmd config RUN { \ From 41178adb7e4e3df27c99c462b069dc9c196af2c3 Mon Sep 17 00:00:00 2001 From: AJ Bowen Date: Sat, 10 Jan 2015 21:01:53 -0800 Subject: [PATCH 240/513] Add Cecilia Payne-Gaposchkin to the names generator. Signed-off-by: AJ Bowen --- pkg/namesgenerator/names-generator.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/namesgenerator/names-generator.go b/pkg/namesgenerator/names-generator.go index c5bcd25b4..372fad6d6 100644 --- a/pkg/namesgenerator/names-generator.go +++ b/pkg/namesgenerator/names-generator.go @@ -230,6 +230,9 @@ var ( // Alfred Nobel - a Swedish chemist, engineer, innovator, and armaments manufacturer (inventor of dynamite) - https://en.wikipedia.org/wiki/Alfred_Nobel "nobel", + // Cecilia Payne-Gaposchkin was an astronomer and astrophysicist who, in 1925, proposed in her Ph.D. thesis an explanation for the composition of stars in terms of the relative abundances of hydrogen and helium. https://en.wikipedia.org/wiki/Cecilia_Payne-Gaposchkin + "payne", + // Ambroise Pare invented modern surgery. https://en.wikipedia.org/wiki/Ambroise_Par%C3%A9 "pare", From 400d92871cb300ee8696dbcedcfd6a95f4d9d5a7 Mon Sep 17 00:00:00 2001 From: Lei Jitang Date: Tue, 13 Jan 2015 09:38:12 +0800 Subject: [PATCH 241/513] Add --link accept container ID Signed-off-by: Lei Jitang --- daemon/daemon.go | 5 +---- runconfig/parse.go | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/daemon/daemon.go b/daemon/daemon.go index 5972b4f87..68850ad87 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -752,10 +752,7 @@ func (daemon *Daemon) RegisterLinks(container *Container, hostConfig *runconfig. if err != nil { return err } - child, err := daemon.GetByName(parts["name"]) - if err != nil { - return err - } + child := daemon.Get(parts["name"]) if child == nil { return fmt.Errorf("Could not get container for %s", parts["name"]) } diff --git a/runconfig/parse.go b/runconfig/parse.go index 3bab8ac76..e6807890a 100644 --- a/runconfig/parse.go +++ b/runconfig/parse.go @@ -66,7 +66,7 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe cmd.Var(&flAttach, []string{"a", "-attach"}, "Attach to STDIN, STDOUT or STDERR.") cmd.Var(&flVolumes, []string{"v", "-volume"}, "Bind mount a volume (e.g., from the host: -v /host:/container, from Docker: -v /container)") - cmd.Var(&flLinks, []string{"#link", "-link"}, "Add link to another container in the form of name:alias") + cmd.Var(&flLinks, []string{"#link", "-link"}, "Add link to another container in the form of :alias") cmd.Var(&flDevices, []string{"-device"}, "Add a host device to the container (e.g. --device=/dev/sdc:/dev/xvdc:rwm)") cmd.Var(&flEnv, []string{"e", "-env"}, "Set environment variables") From 009041cdfd815c0ab49493f9f5757b171ee8c987 Mon Sep 17 00:00:00 2001 From: Arnaud Porterie Date: Mon, 12 Jan 2015 17:36:06 -0800 Subject: [PATCH 242/513] Upgrade libcontainer to 1d3b2589d734dc94a1719a3af4 Correct exit code when dying on a signal (fixes #9979). Signed-off-by: Arnaud Porterie --- project/vendor.sh | 2 +- .../github.com/docker/libcontainer/ROADMAP.md | 6 +++- .../docker/libcontainer/namespaces/exec.go | 11 +++++++- .../docker/libcontainer/nsinit/main.go | 5 ++-- .../docker/libcontainer/nsinit/oom.go | 28 +++++++++++++++++++ 5 files changed, 47 insertions(+), 5 deletions(-) create mode 100644 vendor/src/github.com/docker/libcontainer/nsinit/oom.go diff --git a/project/vendor.sh b/project/vendor.sh index e6a43e0f2..d15d26171 100755 --- a/project/vendor.sh +++ b/project/vendor.sh @@ -68,7 +68,7 @@ if [ "$1" = '--go' ]; then mv tmp-tar src/code.google.com/p/go/src/pkg/archive/tar fi -clone git github.com/docker/libcontainer 6460fd79667466d2d9ec03f77f319a241c58d40b +clone git github.com/docker/libcontainer 1d3b2589d734dc94a1719a3af40b87ed8319f329 # see src/github.com/docker/libcontainer/update-vendor.sh which is the "source of truth" for libcontainer deps (just like this file) rm -rf src/github.com/docker/libcontainer/vendor eval "$(grep '^clone ' src/github.com/docker/libcontainer/update-vendor.sh | grep -v 'github.com/codegangsta/cli')" diff --git a/vendor/src/github.com/docker/libcontainer/ROADMAP.md b/vendor/src/github.com/docker/libcontainer/ROADMAP.md index 08deb9ada..f59035351 100644 --- a/vendor/src/github.com/docker/libcontainer/ROADMAP.md +++ b/vendor/src/github.com/docker/libcontainer/ROADMAP.md @@ -13,4 +13,8 @@ Our goal is to make libcontainer run everywhere, but currently libcontainer requ ## Cross-architecture support -Our goal is to make libcontainer run everywhere. However currently libcontainer only runs on x86_64 systems. We plan on expanding architecture support, so that libcontainer containers can be created and used on more architectures. +Our goal is to make libcontainer run everywhere. Recently libcontainer has +expanded from its initial support for x86_64 systems to include POWER (ppc64 +little and big endian variants), IBM System z (s390x 64-bit), and ARM. We plan +to continue expanding architecture support such that libcontainer containers +can be created and used on more architectures. diff --git a/vendor/src/github.com/docker/libcontainer/namespaces/exec.go b/vendor/src/github.com/docker/libcontainer/namespaces/exec.go index abe67ae25..bfaa755af 100644 --- a/vendor/src/github.com/docker/libcontainer/namespaces/exec.go +++ b/vendor/src/github.com/docker/libcontainer/namespaces/exec.go @@ -17,6 +17,10 @@ import ( "github.com/docker/libcontainer/system" ) +const ( + EXIT_SIGNAL_OFFSET = 128 +) + // TODO(vishh): This is part of the libcontainer API and it does much more than just namespaces related work. // Move this to libcontainer package. // Exec performs setup outside of a namespace so that a container can be @@ -113,7 +117,12 @@ func Exec(container *libcontainer.Config, stdin io.Reader, stdout, stderr io.Wri if !container.Namespaces.Contains(libcontainer.NEWPID) { killAllPids(container) } - return command.ProcessState.Sys().(syscall.WaitStatus).ExitStatus(), nil + + waitStatus := command.ProcessState.Sys().(syscall.WaitStatus) + if waitStatus.Signaled() { + return EXIT_SIGNAL_OFFSET + int(waitStatus.Signal()), nil + } + return waitStatus.ExitStatus(), nil } // killAllPids itterates over all of the container's processes diff --git a/vendor/src/github.com/docker/libcontainer/nsinit/main.go b/vendor/src/github.com/docker/libcontainer/nsinit/main.go index d65c0140e..53625ca82 100644 --- a/vendor/src/github.com/docker/libcontainer/nsinit/main.go +++ b/vendor/src/github.com/docker/libcontainer/nsinit/main.go @@ -53,11 +53,12 @@ func main() { app.Before = preload app.Commands = []cli.Command{ + configCommand, execCommand, initCommand, - statsCommand, - configCommand, + oomCommand, pauseCommand, + statsCommand, unpauseCommand, } diff --git a/vendor/src/github.com/docker/libcontainer/nsinit/oom.go b/vendor/src/github.com/docker/libcontainer/nsinit/oom.go new file mode 100644 index 000000000..106abeb26 --- /dev/null +++ b/vendor/src/github.com/docker/libcontainer/nsinit/oom.go @@ -0,0 +1,28 @@ +package main + +import ( + "log" + + "github.com/codegangsta/cli" + "github.com/docker/libcontainer" +) + +var oomCommand = cli.Command{ + Name: "oom", + Usage: "display oom notifications for a container", + Action: oomAction, +} + +func oomAction(context *cli.Context) { + state, err := libcontainer.GetState(dataPath) + if err != nil { + log.Fatal(err) + } + n, err := libcontainer.NotifyOnOOM(state) + if err != nil { + log.Fatal(err) + } + for _ = range n { + log.Printf("OOM notification received") + } +} From 71b03d8aeb76b50ca05116c04ad724f380ef08f3 Mon Sep 17 00:00:00 2001 From: Tianon Gravi Date: Mon, 12 Jan 2015 19:07:16 -0700 Subject: [PATCH 243/513] Fix tgz for Windows binaries Signed-off-by: Andrew "Tianon" Page --- project/make/tgz | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/make/tgz b/project/make/tgz index 7d0ef09a5..7234218da 100644 --- a/project/make/tgz +++ b/project/make/tgz @@ -14,7 +14,7 @@ for d in "$CROSS/"*/*; do GOARCH="$(basename "$d")" GOOS="$(basename "$(dirname "$d")")" BINARY_NAME="docker-$VERSION" - BINARY_EXTENSION="$(binary_extension)" + BINARY_EXTENSION="$(export GOOS && binary_extension)" BINARY_FULLNAME="$BINARY_NAME$BINARY_EXTENSION" mkdir -p "$DEST/$GOOS/$GOARCH" TGZ="$DEST/$GOOS/$GOARCH/$BINARY_NAME.tgz" From 21a809d9ae0ef8392f37c9262dca93ff31966e22 Mon Sep 17 00:00:00 2001 From: Srini Brahmaroutu Date: Sun, 5 Oct 2014 02:47:54 +0000 Subject: [PATCH 244/513] rename a existing container Closes #3036 Signed-off-by: Srini Brahmaroutu --- api/client/commands.go | 20 ++++ api/server/server.go | 19 ++++ daemon/daemon.go | 1 + daemon/rename.go | 30 ++++++ docker/flags.go | 1 + docs/man/docker-rename.1.md | 13 +++ .../reference/api/docker_remote_api_v1.15.md | 21 ++++ docs/sources/reference/commandline/cli.md | 24 +++++ integration-cli/docker_cli_links_test.go | 49 ++++++++- integration-cli/docker_cli_rename_test.go | 99 +++++++++++++++++++ 10 files changed, 275 insertions(+), 2 deletions(-) create mode 100644 daemon/rename.go create mode 100644 docs/man/docker-rename.1.md create mode 100644 integration-cli/docker_cli_rename_test.go diff --git a/api/client/commands.go b/api/client/commands.go index d6e2c94f3..a8846df84 100644 --- a/api/client/commands.go +++ b/api/client/commands.go @@ -818,6 +818,26 @@ func (cli *DockerCli) CmdPause(args ...string) error { return encounteredError } +func (cli *DockerCli) CmdRename(args ...string) error { + cmd := cli.Subcmd("rename", "OLD_NAME NEW_NAME", "Rename a container", true) + if err := cmd.Parse(args); err != nil { + return nil + } + + if cmd.NArg() != 2 { + cmd.Usage() + return nil + } + old_name := cmd.Arg(0) + new_name := cmd.Arg(1) + + if _, _, err := readBody(cli.call("POST", fmt.Sprintf("/containers/%s/rename?name=%s", old_name, new_name), nil, false)); err != nil { + fmt.Fprintf(cli.err, "%s\n", err) + return fmt.Errorf("Error: failed to rename container named %s", old_name) + } + return nil +} + func (cli *DockerCli) CmdInspect(args ...string) error { cmd := cli.Subcmd("inspect", "CONTAINER|IMAGE [CONTAINER|IMAGE...]", "Return low-level information on a container or image", true) tmplStr := cmd.String([]string{"f", "#format", "-format"}, "", "Format the output using the given go template.") diff --git a/api/server/server.go b/api/server/server.go index cfaa5f43a..343c8389c 100644 --- a/api/server/server.go +++ b/api/server/server.go @@ -739,6 +739,24 @@ func postContainersRestart(eng *engine.Engine, version version.Version, w http.R return nil } +func postContainerRename(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := parseForm(r); err != nil { + return err + } + if vars == nil { + return fmt.Errorf("Missing parameter") + } + + newName := r.URL.Query().Get("name") + job := eng.Job("container_rename", vars["name"], newName) + job.Setenv("t", r.Form.Get("t")) + if err := job.Run(); err != nil { + return err + } + w.WriteHeader(http.StatusNoContent) + return nil +} + 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 @@ -1311,6 +1329,7 @@ func createRouter(eng *engine.Engine, logging, enableCors bool, dockerVersion st "/containers/{name:.*}/exec": postContainerExecCreate, "/exec/{name:.*}/start": postContainerExecStart, "/exec/{name:.*}/resize": postContainerExecResize, + "/containers/{name:.*}/rename": postContainerRename, }, "DELETE": { "/containers/{name:.*}": deleteContainers, diff --git a/daemon/daemon.go b/daemon/daemon.go index 5972b4f87..c0217eef5 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -113,6 +113,7 @@ func (daemon *Daemon) Install(eng *engine.Engine) error { "commit": daemon.ContainerCommit, "container_changes": daemon.ContainerChanges, "container_copy": daemon.ContainerCopy, + "container_rename": daemon.ContainerRename, "container_inspect": daemon.ContainerInspect, "containers": daemon.Containers, "create": daemon.ContainerCreate, diff --git a/daemon/rename.go b/daemon/rename.go new file mode 100644 index 000000000..9b030aad0 --- /dev/null +++ b/daemon/rename.go @@ -0,0 +1,30 @@ +package daemon + +import ( + "github.com/docker/docker/engine" +) + +func (daemon *Daemon) ContainerRename(job *engine.Job) engine.Status { + if len(job.Args) != 2 { + return job.Errorf("usage: %s OLD_NAME NEW_NAME", job.Name) + } + old_name := job.Args[0] + new_name := job.Args[1] + + container := daemon.Get(old_name) + if container == nil { + return job.Errorf("No such container: %s", old_name) + } + + container.Lock() + defer container.Unlock() + if err := daemon.containerGraph.Delete(container.Name); err != nil { + return job.Errorf("Failed to delete container %q: %v", old_name, err) + } + if _, err := daemon.reserveName(container.ID, new_name); err != nil { + return job.Errorf("Error when allocating new name: %s", err) + } + container.Name = new_name + + return engine.StatusOK +} diff --git a/docker/flags.go b/docker/flags.go index d6c9f3c19..8fb85831e 100644 --- a/docker/flags.go +++ b/docker/flags.go @@ -90,6 +90,7 @@ func init() { {"ps", "List containers"}, {"pull", "Pull an image or a repository from a Docker registry server"}, {"push", "Push an image or a repository to a Docker registry server"}, + {"rename", "Rename an existing container"}, {"restart", "Restart a running container"}, {"rm", "Remove one or more containers"}, {"rmi", "Remove one or more images"}, diff --git a/docs/man/docker-rename.1.md b/docs/man/docker-rename.1.md new file mode 100644 index 000000000..f741a15b4 --- /dev/null +++ b/docs/man/docker-rename.1.md @@ -0,0 +1,13 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% OCTOBER 2014 +# NAME +docker-rename - Rename a container + +# SYNOPSIS +**docker rename** +OLD_NAME NEW_NAME + +# OPTIONS +There are no available options. + diff --git a/docs/sources/reference/api/docker_remote_api_v1.15.md b/docs/sources/reference/api/docker_remote_api_v1.15.md index 4d27a6150..7edfa9101 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.15.md +++ b/docs/sources/reference/api/docker_remote_api_v1.15.md @@ -647,6 +647,27 @@ Status Codes: - **404** – no such container - **500** – server error +### Rename a container + +`POST /containers/(id)/rename/(new_name)` + +Rename the container `id` to a `new_name` + +**Example request**: + + POST /containers/e90e34656806/rename/new_name HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Status Codes: + +- **204** – no error +- **404** – no such container +- **409** - conflict name already assigned +- **500** – server error + ### Pause a container `POST /containers/(id)/pause` diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index 2baf4699d..877a19508 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -1366,6 +1366,30 @@ just a specific mapping: $ sudo docker port test 7890 0.0.0.0:4321 +## pause + + Usage: docker pause CONTAINER + + Pause all processes within a container + +The `docker pause` command uses the cgroups freezer to suspend all processes in +a container. Traditionally when suspending a process the `SIGSTOP` signal is +used, which is observable by the process being suspended. With the cgroups freezer +the process is unaware, and unable to capture, that it is being suspended, +and subsequently resumed. + +See the +[cgroups freezer documentation](https://www.kernel.org/doc/Documentation/cgroups/freezer-subsystem.txt) +for further details. + +## rename + + Usage: docker rename OLD_NAME NEW_NAME + + rename a existing container to a NEW_NAME + +The `docker rename` command allows the container to be renamed to a different name. + ## ps Usage: docker ps [OPTIONS] diff --git a/integration-cli/docker_cli_links_test.go b/integration-cli/docker_cli_links_test.go index 49d46ed94..ad0638cf9 100644 --- a/integration-cli/docker_cli_links_test.go +++ b/integration-cli/docker_cli_links_test.go @@ -1,6 +1,7 @@ package main import ( + "github.com/docker/docker/pkg/iptables" "io/ioutil" "os" "os/exec" @@ -8,8 +9,6 @@ import ( "strings" "testing" "time" - - "github.com/docker/docker/pkg/iptables" ) func TestLinksEtcHostsRegularFile(t *testing.T) { @@ -76,6 +75,52 @@ func TestLinksPingLinkedContainers(t *testing.T) { logDone("links - ping linked container") } +func TestLinksPingLinkedContainersAfterRename(t *testing.T) { + out, _, _ := dockerCmd(t, "run", "-d", "--name", "container1", "busybox", "sleep", "10") + idA := stripTrailingCharacters(out) + out, _, _ = dockerCmd(t, "run", "-d", "--name", "container2", "busybox", "sleep", "10") + idB := stripTrailingCharacters(out) + dockerCmd(t, "rename", "container1", "container_new") + dockerCmd(t, "run", "--rm", "--link", "container_new:alias1", "--link", "container2:alias2", "busybox", "sh", "-c", "ping -c 1 alias1 -W 1 && ping -c 1 alias2 -W 1") + dockerCmd(t, "kill", idA) + dockerCmd(t, "kill", idB) + deleteAllContainers() + + logDone("links - ping linked container after rename") +} + +func TestLinksPingLinkedContainersOnRename(t *testing.T) { + var out string + out, _, _ = dockerCmd(t, "run", "-d", "--name", "container1", "busybox", "sleep", "10") + idA := stripTrailingCharacters(out) + if idA == "" { + t.Fatal(out, "id should not be nil") + } + out, _, _ = dockerCmd(t, "run", "-d", "--link", "container1:alias1", "--name", "container2", "busybox", "sleep", "10") + idB := stripTrailingCharacters(out) + if idB == "" { + t.Fatal(out, "id should not be nil") + } + + execCmd := exec.Command(dockerBinary, "exec", "container2", "ping", "-c", "1", "alias1", "-W", "1") + out, _, err := runCommandWithOutput(execCmd) + if err != nil { + t.Fatal(out, err) + } + + dockerCmd(t, "rename", "container1", "container_new") + + execCmd = exec.Command(dockerBinary, "exec", "container2", "ping", "-c", "1", "alias1", "-W", "1") + out, _, err = runCommandWithOutput(execCmd) + if err != nil { + t.Fatal(out, err) + } + + deleteAllContainers() + + logDone("links - ping linked container upon rename") +} + func TestLinksIpTablesRulesWhenLinkAndUnlink(t *testing.T) { dockerCmd(t, "run", "-d", "--name", "child", "--publish", "8080:80", "busybox", "sleep", "10") dockerCmd(t, "run", "-d", "--name", "parent", "--link", "child:http", "busybox", "sleep", "10") diff --git a/integration-cli/docker_cli_rename_test.go b/integration-cli/docker_cli_rename_test.go new file mode 100644 index 000000000..3ba98e4e3 --- /dev/null +++ b/integration-cli/docker_cli_rename_test.go @@ -0,0 +1,99 @@ +package main + +import ( + "os/exec" + "strings" + "testing" +) + +func TestRenameStoppedContainer(t *testing.T) { + runCmd := exec.Command(dockerBinary, "run", "--name", "first_name", "-d", "busybox", "sh") + out, _, err := runCommandWithOutput(runCmd) + if err != nil { + t.Fatalf(out, err) + } + + cleanedContainerID := stripTrailingCharacters(out) + + runCmd = exec.Command(dockerBinary, "wait", cleanedContainerID) + out, _, err = runCommandWithOutput(runCmd) + if err != nil { + t.Fatalf(out, err) + } + + name, err := inspectField(cleanedContainerID, "Name") + + runCmd = exec.Command(dockerBinary, "rename", "first_name", "new_name") + out, _, err = runCommandWithOutput(runCmd) + if err != nil { + t.Fatalf(out, err) + } + + name, err = inspectField(cleanedContainerID, "Name") + if err != nil { + t.Fatal(err) + } + if name != "new_name" { + t.Fatal("Failed to rename container ", name) + } + deleteAllContainers() + + logDone("rename - stopped container") +} + +func TestRenameRunningContainer(t *testing.T) { + runCmd := exec.Command(dockerBinary, "run", "--name", "first_name", "-d", "busybox", "sh") + out, _, err := runCommandWithOutput(runCmd) + if err != nil { + t.Fatalf(out, err) + } + + cleanedContainerID := stripTrailingCharacters(out) + runCmd = exec.Command(dockerBinary, "rename", "first_name", "new_name") + out, _, err = runCommandWithOutput(runCmd) + if err != nil { + t.Fatalf(out, err) + } + + name, err := inspectField(cleanedContainerID, "Name") + if err != nil { + t.Fatal(err) + } + if name != "new_name" { + t.Fatal("Failed to rename container ") + } + deleteAllContainers() + + logDone("rename - running container") +} + +func TestRenameCheckNames(t *testing.T) { + runCmd := exec.Command(dockerBinary, "run", "--name", "first_name", "-d", "busybox", "sh") + out, _, err := runCommandWithOutput(runCmd) + if err != nil { + t.Fatalf(out, err) + } + + runCmd = exec.Command(dockerBinary, "rename", "first_name", "new_name") + out, _, err = runCommandWithOutput(runCmd) + if err != nil { + t.Fatalf(out, err) + } + + name, err := inspectField("new_name", "Name") + if err != nil { + t.Fatal(err) + } + if name != "new_name" { + t.Fatal("Failed to rename container ") + } + + name, err = inspectField("first_name", "Name") + if err == nil && !strings.Contains(err.Error(), "No such image or container: first_name") { + t.Fatal(err) + } + + deleteAllContainers() + + logDone("rename - running container") +} From e721ed9b5319e8e7c1daf87c34690f8a4e62c9e3 Mon Sep 17 00:00:00 2001 From: HuKeping Date: Wed, 7 Jan 2015 15:16:04 +0800 Subject: [PATCH 245/513] restart: Fix the compare of restart policy Since the failure count of container will increase by 1 every time it exits successfully, the compare in function shouldRestart() will stop container to restart by the last time. Signed-off-by: Hu Keping --- daemon/monitor.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/monitor.go b/daemon/monitor.go index 081ca391b..9e7d3062f 100644 --- a/daemon/monitor.go +++ b/daemon/monitor.go @@ -230,7 +230,7 @@ func (m *containerMonitor) shouldRestart(exitCode int) bool { return true case "on-failure": // the default value of 0 for MaximumRetryCount means that we will not enforce a maximum count - if max := m.restartPolicy.MaximumRetryCount; max != 0 && m.failureCount >= max { + if max := m.restartPolicy.MaximumRetryCount; max != 0 && m.failureCount > max { log.Debugf("stopping restart of container %s because maximum failure could of %d has been reached", utils.TruncateID(m.container.ID), max) return false From 9a9339d9a23e0005b769bd2f44c722c4a6be730f Mon Sep 17 00:00:00 2001 From: Arnaud Porterie Date: Fri, 9 Jan 2015 15:42:55 -0800 Subject: [PATCH 246/513] Test case for error code when exiting on OOM Signed-off-by: Arnaud Porterie --- integration-cli/docker_cli_run_test.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/integration-cli/docker_cli_run_test.go b/integration-cli/docker_cli_run_test.go index 18495be41..f6985ba3c 100644 --- a/integration-cli/docker_cli_run_test.go +++ b/integration-cli/docker_cli_run_test.go @@ -2912,3 +2912,26 @@ func TestRunAllowPortRangeThroughPublish(t *testing.T) { } logDone("run - allow port range through --expose flag") } + +func TestRunOOMExitCode(t *testing.T) { + defer deleteAllContainers() + + done := make(chan struct{}) + go func() { + defer close(done) + + runCmd := exec.Command(dockerBinary, "run", "-m", "4MB", "busybox", "sh", "-c", "x=a; while true; do x=$x$x; done") + out, exitCode, _ := runCommandWithOutput(runCmd) + if expected := 137; exitCode != expected { + t.Fatalf("wrong exit code for OOM container: expected %d, got %d (output: %q)", expected, exitCode, out) + } + }() + + select { + case <-done: + case <-time.After(3 * time.Second): + t.Fatal("Timeout waiting for container to die on OOM") + } + + logDone("run - exit code on oom") +} From 00bd7b4c414061d71d7f94b5850cbe132da623f0 Mon Sep 17 00:00:00 2001 From: Qiang Huang Date: Tue, 13 Jan 2015 15:24:01 +0800 Subject: [PATCH 247/513] correct the flag comments Signed-off-by: Qiang Huang --- pkg/mflag/flag.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/pkg/mflag/flag.go b/pkg/mflag/flag.go index 1707b16fa..94cb5b7f7 100644 --- a/pkg/mflag/flag.go +++ b/pkg/mflag/flag.go @@ -24,11 +24,15 @@ For such flags, the default value is just the initial value of the variable. You can also add "deprecated" flags, they are still usable, but are not shown - in the usage and will display a warning when you try to use them: - var ip = flag.Int([]string{"#f", "#flagname", "-flagname2"}, 1234, "help message for flagname") - this will display: `Warning: '--flagname' is deprecated, it will be replaced by '--flagname2' soon. See usage.` and + in the usage and will display a warning when you try to use them. `#` before + an option means this option is deprecated, if there is an following option + without `#` ahead, then that's the replacement, if not, it will just be removed: + var ip = flag.Int([]string{"#f", "#flagname", "-flagname"}, 1234, "help message for flagname") + this will display: `Warning: '-f' is deprecated, it will be replaced by '--flagname' soon. See usage.` or + this will display: `Warning: '-flagname' is deprecated, it will be replaced by '--flagname' soon. See usage.` var ip = flag.Int([]string{"f", "#flagname"}, 1234, "help message for flagname") - will display: `Warning: '-f' is deprecated, it will be removed soon. See usage.` + will display: `Warning: '-flagname' is deprecated, it will be removed soon. See usage.` + so you can only use `-f`. You can also group one letter flags, bif you declare var v = flag.Bool([]string{"v", "-verbose"}, false, "help message for verbose") From 12a7e78b125afd277adb7cd099817a419f4faaa0 Mon Sep 17 00:00:00 2001 From: Dan Walsh Date: Fri, 9 Jan 2015 14:57:05 -0500 Subject: [PATCH 248/513] Fix docker run/exec/create not printing error messages If an error message happens while parsing docker run or docker exec, the message is not being printed out. Docker-DCO-1.1-Signed-off-by: Dan Walsh (github: rhatdan) --- api/client/commands.go | 4 ++-- utils/flags.go | 22 +++++++++++++--------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/api/client/commands.go b/api/client/commands.go index d6e2c94f3..cb1dca81b 100644 --- a/api/client/commands.go +++ b/api/client/commands.go @@ -2164,7 +2164,7 @@ func (cli *DockerCli) CmdCreate(args ...string) error { config, hostConfig, cmd, err := runconfig.Parse(cmd, args) if err != nil { - return &utils.StatusError{StatusCode: 1} + utils.ReportError(cmd, err.Error(), true) } if config.Image == "" { cmd.Usage() @@ -2201,7 +2201,7 @@ func (cli *DockerCli) CmdRun(args ...string) error { config, hostConfig, cmd, err := runconfig.Parse(cmd, args) // just in case the Parse does not exit if err != nil { - return &utils.StatusError{StatusCode: 1} + utils.ReportError(cmd, err.Error(), true) } if config.Image == "" { cmd.Usage() diff --git a/utils/flags.go b/utils/flags.go index 8f47780bc..33c72279b 100644 --- a/utils/flags.go +++ b/utils/flags.go @@ -27,15 +27,19 @@ func ParseFlags(cmd *flag.FlagSet, args []string, withHelp bool) error { os.Exit(0) } if str := cmd.CheckArgs(); str != "" { - if withHelp { - if os.Args[0] == cmd.Name() { - str += ". See '" + os.Args[0] + " --help'" - } else { - str += ". See '" + os.Args[0] + " " + cmd.Name() + " --help'" - } - } - fmt.Fprintf(cmd.Out(), "docker: %s.\n", str) - os.Exit(1) + ReportError(cmd, str, withHelp) } return nil } + +func ReportError(cmd *flag.FlagSet, str string, withHelp bool) { + if withHelp { + if os.Args[0] == cmd.Name() { + str += ". See '" + os.Args[0] + " --help'" + } else { + str += ". See '" + os.Args[0] + " " + cmd.Name() + " --help'" + } + } + fmt.Fprintf(cmd.Out(), "docker: %s.\n", str) + os.Exit(1) +} From 9f59b057be25656214d65040ae113c0d76d99324 Mon Sep 17 00:00:00 2001 From: Zoltan Tombol Date: Sun, 21 Dec 2014 01:16:19 +0100 Subject: [PATCH 249/513] Revise list of installed packages in mkimage-arch.sh Docker-DCO-1.1-Signed-off-by: Zoltan Tombol (github: ztombol) --- contrib/mkimage-arch.sh | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/contrib/mkimage-arch.sh b/contrib/mkimage-arch.sh index bf00e60e7..216cd7235 100755 --- a/contrib/mkimage-arch.sh +++ b/contrib/mkimage-arch.sh @@ -18,7 +18,32 @@ ROOTFS=$(mktemp -d ${TMPDIR:-/var/tmp}/rootfs-archlinux-XXXXXXXXXX) chmod 755 $ROOTFS # packages to ignore for space savings -PKGIGNORE=linux,jfsutils,lvm2,cryptsetup,groff,man-db,man-pages,mdadm,pciutils,pcmciautils,reiserfsprogs,s-nail,xfsprogs +PKGIGNORE=( + cryptsetup + device-mapper + dhcpcd + iproute2 + jfsutils + linux + lvm2 + man-db + man-pages + mdadm + nano + netctl + openresolv + pciutils + pcmciautils + reiserfsprogs + s-nail + systemd-sysvcompat + usbutils + vi + xfsprogs +) +IFS=',' +PKGIGNORE="${PKGIGNORE[*]}" +unset IFS expect < Date: Sun, 21 Dec 2014 01:25:15 +0100 Subject: [PATCH 250/513] Delete man pages of installed packages in mkimage-arch.sh Docker-DCO-1.1-Signed-off-by: Zoltan Tombol (github: ztombol) --- contrib/mkimage-arch.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/contrib/mkimage-arch.sh b/contrib/mkimage-arch.sh index 216cd7235..382e2f780 100755 --- a/contrib/mkimage-arch.sh +++ b/contrib/mkimage-arch.sh @@ -61,6 +61,7 @@ expect < $ROOTFS/etc/locale.gen From cf455017e059e4b52d8d0357070e24ee153e3069 Mon Sep 17 00:00:00 2001 From: Arnaud Porterie Date: Fri, 26 Dec 2014 16:30:34 -0800 Subject: [PATCH 251/513] Support whitespaces in ADD and COPY continued Add tests and documentation for this new feature. Signed-off-by: Arnaud Porterie --- docs/man/Dockerfile.5.md | 12 +++- docs/sources/reference/builder.md | 12 +++- integration-cli/docker_cli_build_test.go | 89 +++++++++++++++++++++--- 3 files changed, 99 insertions(+), 14 deletions(-) diff --git a/docs/man/Dockerfile.5.md b/docs/man/Dockerfile.5.md index 0114f30ba..1630b5fc4 100644 --- a/docs/man/Dockerfile.5.md +++ b/docs/man/Dockerfile.5.md @@ -131,7 +131,11 @@ or interactively, as with the following command: **docker run -t -i image bash** **ADD** - --**ADD ... ** The ADD instruction copies new files, directories + --ADD has two forms: + **ADD ... ** + **ADD [""... ""]** This form is required for paths containing + whitespace. + The ADD instruction copies new files, directories or remote file URLs to the filesystem of the container at path . Mutliple resources may be specified but if they are files or directories then they must be relative to the source directory that is being built @@ -141,7 +145,11 @@ or and gid of 0. **COPY** - --**COPY ** The COPY instruction copies new files from and + --COPY has two forms: + **COPY ... ** + **COPY [""... ""]** This form is required for paths containing + whitespace. + The COPY instruction copies new files from and adds them to the filesystem of the container at path . The must be the path to a file or directory relative to the source directory that is being built (the context of the build) or a remote file URL. The `` is an diff --git a/docs/sources/reference/builder.md b/docs/sources/reference/builder.md index fa7393efa..d8e99613e 100644 --- a/docs/sources/reference/builder.md +++ b/docs/sources/reference/builder.md @@ -381,7 +381,11 @@ change them using `docker run --env =`. ## ADD - ADD ... +ADD has two forms: + +- `ADD ... ` +- `ADD [""... ""]` (this form is required for paths containing +whitespace) The `ADD` instruction copies new files, directories or remote file URLs from `` and adds them to the filesystem of the container at the path ``. @@ -481,7 +485,11 @@ The copy obeys the following rules: ## COPY - COPY ... +COPY has two forms: + +- `COPY ... ` +- `COPY [""... ""]` (this form is required for paths containing +whitespace) The `COPY` instruction copies new files or directories from `` and adds them to the filesystem of the container at the path ``. diff --git a/integration-cli/docker_cli_build_test.go b/integration-cli/docker_cli_build_test.go index a3d2464d0..0e7cd5a86 100644 --- a/integration-cli/docker_cli_build_test.go +++ b/integration-cli/docker_cli_build_test.go @@ -762,7 +762,7 @@ RUN [ $(ls -l /exists/exists_file | awk '{print $3":"$4}') = 'dockerio:dockerio' if _, err := buildImageFromContext(name, ctx, true); err != nil { t.Fatal(err) } - logDone("build - mulitple file copy/add tests") + logDone("build - multiple file copy/add tests") } func TestBuildAddMultipleFilesToFile(t *testing.T) { @@ -770,7 +770,7 @@ func TestBuildAddMultipleFilesToFile(t *testing.T) { defer deleteImages(name) ctx, err := fakeContext(`FROM scratch ADD file1.txt file2.txt test - `, + `, map[string]string{ "file1.txt": "test1", "file2.txt": "test1", @@ -782,18 +782,41 @@ func TestBuildAddMultipleFilesToFile(t *testing.T) { expected := "When using ADD with more than one source file, the destination must be a directory and end with a /" if _, err := buildImageFromContext(name, ctx, true); err == nil || !strings.Contains(err.Error(), expected) { - t.Fatalf("Wrong error: (should contain \"%s\") got:\n%v", expected, err) + t.Fatalf("Wrong error: (should contain %q) got:\n%v", expected, err) } logDone("build - multiple add files to file") } +func TestBuildJSONAddMultipleFilesToFile(t *testing.T) { + name := "testjsonaddmultiplefilestofile" + defer deleteImages(name) + ctx, err := fakeContext(`FROM scratch + ADD ["file1.txt", "file2.txt", "test"] + `, + map[string]string{ + "file1.txt": "test1", + "file2.txt": "test1", + }) + defer ctx.Close() + if err != nil { + t.Fatal(err) + } + + expected := "When using ADD with more than one source file, the destination must be a directory and end with a /" + if _, err := buildImageFromContext(name, ctx, true); err == nil || !strings.Contains(err.Error(), expected) { + t.Fatalf("Wrong error: (should contain %q) got:\n%v", expected, err) + } + + logDone("build - multiple add files to file json syntax") +} + func TestBuildAddMultipleFilesToFileWild(t *testing.T) { name := "testaddmultiplefilestofilewild" defer deleteImages(name) ctx, err := fakeContext(`FROM scratch ADD file*.txt test - `, + `, map[string]string{ "file1.txt": "test1", "file2.txt": "test1", @@ -805,18 +828,41 @@ func TestBuildAddMultipleFilesToFileWild(t *testing.T) { expected := "When using ADD with more than one source file, the destination must be a directory and end with a /" if _, err := buildImageFromContext(name, ctx, true); err == nil || !strings.Contains(err.Error(), expected) { - t.Fatalf("Wrong error: (should contain \"%s\") got:\n%v", expected, err) + t.Fatalf("Wrong error: (should contain %q) got:\n%v", expected, err) } logDone("build - multiple add files to file wild") } +func TestBuildJSONAddMultipleFilesToFileWild(t *testing.T) { + name := "testjsonaddmultiplefilestofilewild" + defer deleteImages(name) + ctx, err := fakeContext(`FROM scratch + ADD ["file*.txt", "test"] + `, + map[string]string{ + "file1.txt": "test1", + "file2.txt": "test1", + }) + defer ctx.Close() + if err != nil { + t.Fatal(err) + } + + expected := "When using ADD with more than one source file, the destination must be a directory and end with a /" + if _, err := buildImageFromContext(name, ctx, true); err == nil || !strings.Contains(err.Error(), expected) { + t.Fatalf("Wrong error: (should contain %q) got:\n%v", expected, err) + } + + logDone("build - multiple add files to file wild json syntax") +} + func TestBuildCopyMultipleFilesToFile(t *testing.T) { name := "testcopymultiplefilestofile" defer deleteImages(name) ctx, err := fakeContext(`FROM scratch COPY file1.txt file2.txt test - `, + `, map[string]string{ "file1.txt": "test1", "file2.txt": "test1", @@ -828,12 +874,35 @@ func TestBuildCopyMultipleFilesToFile(t *testing.T) { expected := "When using COPY with more than one source file, the destination must be a directory and end with a /" if _, err := buildImageFromContext(name, ctx, true); err == nil || !strings.Contains(err.Error(), expected) { - t.Fatalf("Wrong error: (should contain \"%s\") got:\n%v", expected, err) + t.Fatalf("Wrong error: (should contain %q) got:\n%v", expected, err) } logDone("build - multiple copy files to file") } +func TestBuildJSONCopyMultipleFilesToFile(t *testing.T) { + name := "testjsoncopymultiplefilestofile" + defer deleteImages(name) + ctx, err := fakeContext(`FROM scratch + COPY ["file1.txt", "file2.txt", "test"] + `, + map[string]string{ + "file1.txt": "test1", + "file2.txt": "test1", + }) + defer ctx.Close() + if err != nil { + t.Fatal(err) + } + + expected := "When using COPY with more than one source file, the destination must be a directory and end with a /" + if _, err := buildImageFromContext(name, ctx, true); err == nil || !strings.Contains(err.Error(), expected) { + t.Fatalf("Wrong error: (should contain %q) got:\n%v", expected, err) + } + + logDone("build - multiple copy files to file json syntax") +} + func TestBuildAddFileWithWhitespace(t *testing.T) { name := "testaddfilewithwhitespace" defer deleteImages(name) @@ -913,7 +982,7 @@ func TestBuildAddMultipleFilesToFileWithWhitespace(t *testing.T) { defer deleteImages(name) ctx, err := fakeContext(`FROM busybox ADD [ "test file1", "test file2", "test" ] - `, + `, map[string]string{ "test file1": "test1", "test file2": "test2", @@ -925,7 +994,7 @@ func TestBuildAddMultipleFilesToFileWithWhitespace(t *testing.T) { expected := "When using ADD with more than one source file, the destination must be a directory and end with a /" if _, err := buildImageFromContext(name, ctx, true); err == nil || !strings.Contains(err.Error(), expected) { - t.Fatalf("Wrong error: (should contain \"%s\") got:\n%v", expected, err) + t.Fatalf("Wrong error: (should contain %q) got:\n%v", expected, err) } logDone("build - multiple add files to file with whitespace") @@ -948,7 +1017,7 @@ func TestBuildCopyMultipleFilesToFileWithWhitespace(t *testing.T) { expected := "When using COPY with more than one source file, the destination must be a directory and end with a /" if _, err := buildImageFromContext(name, ctx, true); err == nil || !strings.Contains(err.Error(), expected) { - t.Fatalf("Wrong error: (should contain \"%s\") got:\n%v", expected, err) + t.Fatalf("Wrong error: (should contain %q) got:\n%v", expected, err) } logDone("build - multiple copy files to file with whitespace") From 3c01c971cdec8822086d03f509e119cb8a8c719c Mon Sep 17 00:00:00 2001 From: Tianon Gravi Date: Tue, 13 Jan 2015 12:34:55 -0700 Subject: [PATCH 252/513] Switch docker-py clone to use an explicit commit for natural cache-busting Signed-off-by: Andrew "Tianon" Page --- Dockerfile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index f46b029bf..9ef05b561 100644 --- a/Dockerfile +++ b/Dockerfile @@ -117,7 +117,10 @@ RUN git clone -b buildroot-2014.02 https://github.com/jpetazzo/docker-busybox.gi RUN curl -sSL -o /cirros.tar.gz https://github.com/ewindisch/docker-cirros/raw/1cded459668e8b9dbf4ef976c94c05add9bbd8e9/cirros-0.3.0-x86_64-lxc.tar.gz # Get the "docker-py" source so we can run their integration tests -RUN git clone -b 0.7.1 https://github.com/docker/docker-py.git /docker-py +ENV DOCKER_PY_COMMIT aa19d7b6609c6676e8258f6b900dea2eda1dbe95 +RUN git clone https://github.com/docker/docker-py.git /docker-py \ + && cd /docker-py \ + && git checkout -q $DOCKER_PY_COMMIT # Setup s3cmd config RUN { \ From 78820a63d647276cf17dac9ac71ed2beb19f285d Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Tue, 13 Jan 2015 21:57:48 +0100 Subject: [PATCH 253/513] Mention "or rename" again in error-message. The "or rename" part was removed from the error-message, because renaming wasn't possible at the time. Now that https://github.com/docker/docker/pull/8570 is merged, renaming existing containers is possible. Signed-off-by: Sebastiaan van Stijn --- daemon/daemon.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/daemon.go b/daemon/daemon.go index c0217eef5..c9fc2f4ac 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -542,7 +542,7 @@ func (daemon *Daemon) reserveName(id, name string) (string, error) { } else { nameAsKnownByUser := strings.TrimPrefix(name, "/") return "", fmt.Errorf( - "Conflict. The name %q is already in use by container %s. You have to delete that container to be able to reuse that name.", nameAsKnownByUser, + "Conflict. The name %q is already in use by container %s. You have to delete (or rename) that container to be able to reuse that name.", nameAsKnownByUser, utils.TruncateID(conflictingContainer.ID)) } } From 02923d43e20f7be6c2e1ed48846d188a682ff919 Mon Sep 17 00:00:00 2001 From: Pierre Wacrenier Date: Tue, 13 Jan 2015 22:41:34 +0100 Subject: [PATCH 254/513] Remove error return type from createRouter and ServeRequest Signed-off-by: Pierre Wacrenier --- api/server/server.go | 27 ++------ api/server/server_unit_test.go | 4 +- integration/api_test.go | 118 +++++++++------------------------ 3 files changed, 38 insertions(+), 111 deletions(-) diff --git a/api/server/server.go b/api/server/server.go index 343c8389c..4907ff034 100644 --- a/api/server/server.go +++ b/api/server/server.go @@ -1279,7 +1279,7 @@ func AttachProfiler(router *mux.Router) { router.HandleFunc("/debug/pprof/threadcreate", pprof.Handler("threadcreate").ServeHTTP) } -func createRouter(eng *engine.Engine, logging, enableCors bool, dockerVersion string) (*mux.Router, error) { +func createRouter(eng *engine.Engine, logging, enableCors bool, dockerVersion string) *mux.Router { r := mux.NewRouter() if os.Getenv("DEBUG") != "" { AttachProfiler(r) @@ -1361,30 +1361,23 @@ func createRouter(eng *engine.Engine, logging, enableCors bool, dockerVersion st } } - return r, nil + return r } // 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 version.Version, w http.ResponseWriter, req *http.Request) error { - router, err := createRouter(eng, false, true, "") - if err != nil { - return err - } +func ServeRequest(eng *engine.Engine, apiversion version.Version, w http.ResponseWriter, req *http.Request) { + router := createRouter(eng, false, true, "") // Insert APIVERSION into the request as a convenience req.URL.Path = fmt.Sprintf("/v%s%s", apiversion, req.URL.Path) router.ServeHTTP(w, req) - return nil } // serveFd creates an http.Server and sets it up to serve given a socket activated // argument. func serveFd(addr string, job *engine.Job) error { - r, err := createRouter(job.Eng, job.GetenvBool("Logging"), job.GetenvBool("EnableCors"), job.Getenv("Version")) - if err != nil { - return err - } + r := createRouter(job.Eng, job.GetenvBool("Logging"), job.GetenvBool("EnableCors"), job.Getenv("Version")) ls, e := systemd.ListenFD(addr) if e != nil { @@ -1496,10 +1489,7 @@ func setSocketGroup(addr, group string) error { } func setupUnixHttp(addr string, job *engine.Job) (*HttpServer, error) { - r, err := createRouter(job.Eng, job.GetenvBool("Logging"), job.GetenvBool("EnableCors"), job.Getenv("Version")) - if err != nil { - return nil, err - } + r := createRouter(job.Eng, job.GetenvBool("Logging"), job.GetenvBool("EnableCors"), job.Getenv("Version")) if err := syscall.Unlink(addr); err != nil && !os.IsNotExist(err) { return nil, err @@ -1554,10 +1544,7 @@ func setupTcpHttp(addr string, job *engine.Job) (*HttpServer, error) { log.Infof("/!\\ DON'T BIND ON ANOTHER IP ADDRESS THAN 127.0.0.1 IF YOU DON'T KNOW WHAT YOU'RE DOING /!\\") } - r, err := createRouter(job.Eng, job.GetenvBool("Logging"), job.GetenvBool("EnableCors"), job.Getenv("Version")) - if err != nil { - return nil, err - } + r := createRouter(job.Eng, job.GetenvBool("Logging"), job.GetenvBool("EnableCors"), job.Getenv("Version")) l, err := newListener("tcp", addr, job.GetenvBool("BufferRequests")) if err != nil { diff --git a/api/server/server_unit_test.go b/api/server/server_unit_test.go index 519652f37..b5ec7c896 100644 --- a/api/server/server_unit_test.go +++ b/api/server/server_unit_test.go @@ -484,9 +484,7 @@ func serveRequestUsingVersion(method, target string, version version.Version, bo if err != nil { t.Fatal(err) } - if err := ServeRequest(eng, version, r, req); err != nil { - t.Fatal(err) - } + ServeRequest(eng, version, r, req) return r } diff --git a/integration/api_test.go b/integration/api_test.go index 8e45f8928..ab2c3070b 100644 --- a/integration/api_test.go +++ b/integration/api_test.go @@ -31,9 +31,7 @@ func TestSaveImageAndThenLoad(t *testing.T) { if err != nil { t.Fatal(err) } - if err := server.ServeRequest(eng, api.APIVERSION, r, req); err != nil { - t.Fatal(err) - } + server.ServeRequest(eng, api.APIVERSION, r, req) if r.Code != http.StatusOK { t.Fatalf("%d OK expected, received %d\n", http.StatusOK, r.Code) } @@ -45,9 +43,7 @@ func TestSaveImageAndThenLoad(t *testing.T) { if err != nil { t.Fatal(err) } - if err := server.ServeRequest(eng, api.APIVERSION, r, req); err != nil { - t.Fatal(err) - } + server.ServeRequest(eng, api.APIVERSION, r, req) if r.Code != http.StatusOK { t.Fatalf("%d OK expected, received %d\n", http.StatusOK, r.Code) } @@ -58,9 +54,7 @@ func TestSaveImageAndThenLoad(t *testing.T) { if err != nil { t.Fatal(err) } - if err := server.ServeRequest(eng, api.APIVERSION, r, req); err != nil { - t.Fatal(err) - } + server.ServeRequest(eng, api.APIVERSION, r, req) if r.Code != http.StatusNotFound { t.Fatalf("%d NotFound expected, received %d\n", http.StatusNotFound, r.Code) } @@ -71,9 +65,7 @@ func TestSaveImageAndThenLoad(t *testing.T) { if err != nil { t.Fatal(err) } - if err := server.ServeRequest(eng, api.APIVERSION, r, req); err != nil { - t.Fatal(err) - } + server.ServeRequest(eng, api.APIVERSION, r, req) if r.Code != http.StatusOK { t.Fatalf("%d OK expected, received %d\n", http.StatusOK, r.Code) } @@ -84,9 +76,7 @@ func TestSaveImageAndThenLoad(t *testing.T) { if err != nil { t.Fatal(err) } - if err := server.ServeRequest(eng, api.APIVERSION, r, req); err != nil { - t.Fatal(err) - } + server.ServeRequest(eng, api.APIVERSION, r, req) if r.Code != http.StatusOK { t.Fatalf("%d OK expected, received %d\n", http.StatusOK, r.Code) } @@ -138,9 +128,7 @@ func TestGetContainersTop(t *testing.T) { if err != nil { t.Fatal(err) } - if err := server.ServeRequest(eng, api.APIVERSION, r, req); err != nil { - t.Fatal(err) - } + server.ServeRequest(eng, api.APIVERSION, r, req) assertHttpNotError(r, t) var procs engine.Env if err := procs.Decode(r.Body); err != nil { @@ -189,9 +177,7 @@ func TestPostCommit(t *testing.T) { } r := httptest.NewRecorder() - if err := server.ServeRequest(eng, api.APIVERSION, r, req); err != nil { - t.Fatal(err) - } + server.ServeRequest(eng, api.APIVERSION, r, req) assertHttpNotError(r, t) if r.Code != http.StatusCreated { t.Fatalf("%d Created expected, received %d\n", http.StatusCreated, r.Code) @@ -227,9 +213,7 @@ func TestPostContainersCreate(t *testing.T) { req.Header.Set("Content-Type", "application/json") r := httptest.NewRecorder() - if err := server.ServeRequest(eng, api.APIVERSION, r, req); err != nil { - t.Fatal(err) - } + server.ServeRequest(eng, api.APIVERSION, r, req) assertHttpNotError(r, t) if r.Code != http.StatusCreated { t.Fatalf("%d Created expected, received %d\n", http.StatusCreated, r.Code) @@ -269,14 +253,12 @@ func TestPostJsonVerify(t *testing.T) { r := httptest.NewRecorder() - if err := server.ServeRequest(eng, api.APIVERSION, r, req); err != nil { - t.Fatal(err) - } + server.ServeRequest(eng, api.APIVERSION, r, req) // Don't add Content-Type header // req.Header.Set("Content-Type", "application/json") - err = server.ServeRequest(eng, api.APIVERSION, r, req) + server.ServeRequest(eng, api.APIVERSION, r, req) if r.Code != http.StatusInternalServerError || !strings.Contains(((*r.Body).String()), "application/json") { t.Fatal("Create should have failed due to no Content-Type header - got:", r) } @@ -284,9 +266,7 @@ func TestPostJsonVerify(t *testing.T) { // Now add header but with wrong type and retest req.Header.Set("Content-Type", "application/xml") - if err := server.ServeRequest(eng, api.APIVERSION, r, req); err != nil { - t.Fatal(err) - } + server.ServeRequest(eng, api.APIVERSION, r, req) if r.Code != http.StatusInternalServerError || !strings.Contains(((*r.Body).String()), "application/json") { t.Fatal("Create should have failed due to wrong Content-Type header - got:", r) } @@ -331,9 +311,7 @@ func TestPostCreateNull(t *testing.T) { req.Header.Set("Content-Type", "application/json") r := httptest.NewRecorder() - if err := server.ServeRequest(eng, api.APIVERSION, r, req); err != nil { - t.Fatal(err) - } + server.ServeRequest(eng, api.APIVERSION, r, req) assertHttpNotError(r, t) if r.Code != http.StatusCreated { t.Fatalf("%d Created expected, received %d\n", http.StatusCreated, r.Code) @@ -380,9 +358,7 @@ func TestPostContainersKill(t *testing.T) { if err != nil { t.Fatal(err) } - if err := server.ServeRequest(eng, api.APIVERSION, r, req); err != nil { - t.Fatal(err) - } + server.ServeRequest(eng, api.APIVERSION, r, req) assertHttpNotError(r, t) if r.Code != http.StatusNoContent { t.Fatalf("%d NO CONTENT expected, received %d\n", http.StatusNoContent, r.Code) @@ -419,9 +395,7 @@ func TestPostContainersRestart(t *testing.T) { t.Fatal(err) } r := httptest.NewRecorder() - if err := server.ServeRequest(eng, api.APIVERSION, r, req); err != nil { - t.Fatal(err) - } + server.ServeRequest(eng, api.APIVERSION, r, req) assertHttpNotError(r, t) if r.Code != http.StatusNoContent { t.Fatalf("%d NO CONTENT expected, received %d\n", http.StatusNoContent, r.Code) @@ -461,9 +435,7 @@ func TestPostContainersStart(t *testing.T) { req.Header.Set("Content-Type", "application/json") r := httptest.NewRecorder() - if err := server.ServeRequest(eng, api.APIVERSION, r, req); err != nil { - t.Fatal(err) - } + server.ServeRequest(eng, api.APIVERSION, r, req) assertHttpNotError(r, t) if r.Code != http.StatusNoContent { t.Fatalf("%d NO CONTENT expected, received %d\n", http.StatusNoContent, r.Code) @@ -479,9 +451,7 @@ func TestPostContainersStart(t *testing.T) { req.Header.Set("Content-Type", "application/json") r = httptest.NewRecorder() - if err := server.ServeRequest(eng, api.APIVERSION, r, req); err != nil { - t.Fatal(err) - } + server.ServeRequest(eng, api.APIVERSION, r, req) // Starting an already started container should return a 304 assertHttpNotError(r, t) @@ -520,9 +490,7 @@ func TestPostContainersStop(t *testing.T) { t.Fatal(err) } r := httptest.NewRecorder() - if err := server.ServeRequest(eng, api.APIVERSION, r, req); err != nil { - t.Fatal(err) - } + server.ServeRequest(eng, api.APIVERSION, r, req) assertHttpNotError(r, t) if r.Code != http.StatusNoContent { t.Fatalf("%d NO CONTENT expected, received %d\n", http.StatusNoContent, r.Code) @@ -537,9 +505,7 @@ func TestPostContainersStop(t *testing.T) { } r = httptest.NewRecorder() - if err := server.ServeRequest(eng, api.APIVERSION, r, req); err != nil { - t.Fatal(err) - } + server.ServeRequest(eng, api.APIVERSION, r, req) // Stopping an already stopper container should return a 304 assertHttpNotError(r, t) @@ -568,9 +534,7 @@ func TestPostContainersWait(t *testing.T) { if err != nil { t.Fatal(err) } - if err := server.ServeRequest(eng, api.APIVERSION, r, req); err != nil { - t.Fatal(err) - } + server.ServeRequest(eng, api.APIVERSION, r, req) assertHttpNotError(r, t) var apiWait engine.Env if err := apiWait.Decode(r.Body); err != nil { @@ -626,9 +590,7 @@ func TestPostContainersAttach(t *testing.T) { t.Fatal(err) } - if err := server.ServeRequest(eng, api.APIVERSION, r, req); err != nil { - t.Fatal(err) - } + server.ServeRequest(eng, api.APIVERSION, r, req) assertHttpNotError(r.ResponseRecorder, t) }() @@ -704,9 +666,7 @@ func TestPostContainersAttachStderr(t *testing.T) { t.Fatal(err) } - if err := server.ServeRequest(eng, api.APIVERSION, r, req); err != nil { - t.Fatal(err) - } + server.ServeRequest(eng, api.APIVERSION, r, req) assertHttpNotError(r.ResponseRecorder, t) }() @@ -751,9 +711,7 @@ func TestOptionsRoute(t *testing.T) { if err != nil { t.Fatal(err) } - if err := server.ServeRequest(eng, api.APIVERSION, r, req); err != nil { - t.Fatal(err) - } + server.ServeRequest(eng, api.APIVERSION, r, req) assertHttpNotError(r, t) if r.Code != http.StatusOK { t.Errorf("Expected response for OPTIONS request to be \"200\", %v found.", r.Code) @@ -770,9 +728,7 @@ func TestGetEnabledCors(t *testing.T) { if err != nil { t.Fatal(err) } - if err := server.ServeRequest(eng, api.APIVERSION, r, req); err != nil { - t.Fatal(err) - } + server.ServeRequest(eng, api.APIVERSION, r, req) assertHttpNotError(r, t) if r.Code != http.StatusOK { t.Errorf("Expected response for OPTIONS request to be \"200\", %v found.", r.Code) @@ -817,9 +773,7 @@ func TestDeleteImages(t *testing.T) { } r := httptest.NewRecorder() - if err := server.ServeRequest(eng, api.APIVERSION, r, req); err != nil { - t.Fatal(err) - } + server.ServeRequest(eng, api.APIVERSION, r, req) if r.Code != http.StatusConflict { t.Fatalf("Expected http status 409-conflict, got %v", r.Code) } @@ -830,9 +784,7 @@ func TestDeleteImages(t *testing.T) { } r2 := httptest.NewRecorder() - if err := server.ServeRequest(eng, api.APIVERSION, r2, req2); err != nil { - t.Fatal(err) - } + server.ServeRequest(eng, api.APIVERSION, r2, req2) assertHttpNotError(r2, t) if r2.Code != http.StatusOK { t.Fatalf("%d OK expected, received %d\n", http.StatusOK, r.Code) @@ -882,9 +834,7 @@ func TestPostContainersCopy(t *testing.T) { t.Fatal(err) } req.Header.Add("Content-Type", "application/json") - if err := server.ServeRequest(eng, api.APIVERSION, r, req); err != nil { - t.Fatal(err) - } + server.ServeRequest(eng, api.APIVERSION, r, req) assertHttpNotError(r, t) if r.Code != http.StatusOK { @@ -930,9 +880,7 @@ func TestPostContainersCopyWhenContainerNotFound(t *testing.T) { t.Fatal(err) } req.Header.Add("Content-Type", "application/json") - if err := server.ServeRequest(eng, api.APIVERSION, r, req); err != nil { - t.Fatal(err) - } + server.ServeRequest(eng, api.APIVERSION, r, req) if r.Code != http.StatusNotFound { t.Fatalf("404 expected for id_not_found Container, received %v", r.Code) } @@ -960,9 +908,7 @@ func TestConstainersStartChunkedEncodingHostConfig(t *testing.T) { } req.Header.Add("Content-Type", "application/json") - if err := server.ServeRequest(eng, api.APIVERSION, r, req); err != nil { - t.Fatal(err) - } + server.ServeRequest(eng, api.APIVERSION, r, req) assertHttpNotError(r, t) var testData2 engine.Env @@ -982,9 +928,7 @@ func TestConstainersStartChunkedEncodingHostConfig(t *testing.T) { // Otherwise (just setting the Content-Encoding to chunked) net/http will overwrite // http://golang.org/src/pkg/net/http/request.go?s=11980:12172 req.ContentLength = -1 - if err := server.ServeRequest(eng, api.APIVERSION, r, req); err != nil { - t.Fatal(err) - } + server.ServeRequest(eng, api.APIVERSION, r, req) assertHttpNotError(r, t) type config struct { @@ -1000,9 +944,7 @@ func TestConstainersStartChunkedEncodingHostConfig(t *testing.T) { r2 := httptest.NewRecorder() req.Header.Add("Content-Type", "application/json") - if err := server.ServeRequest(eng, api.APIVERSION, r2, req); err != nil { - t.Fatal(err) - } + server.ServeRequest(eng, api.APIVERSION, r2, req) assertHttpNotError(r, t) c := config{} From 492b18ac08e2652c1b95fd9f3c786e1fb1f28c3a Mon Sep 17 00:00:00 2001 From: Alexander Morozov Date: Tue, 13 Jan 2015 14:14:36 -0800 Subject: [PATCH 255/513] Rewrite TestRunMutableNetworkFiles to avoid races Signed-off-by: Alexander Morozov --- integration-cli/docker_cli_run_test.go | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/integration-cli/docker_cli_run_test.go b/integration-cli/docker_cli_run_test.go index f6985ba3c..53c282e4a 100644 --- a/integration-cli/docker_cli_run_test.go +++ b/integration-cli/docker_cli_run_test.go @@ -2007,7 +2007,7 @@ func TestRunMutableNetworkFiles(t *testing.T) { for _, fn := range []string{"resolv.conf", "hosts"} { deleteAllContainers() - content, err := runCommandAndReadContainerFile(fn, exec.Command(dockerBinary, "run", "-d", "--name", "c1", "busybox", "sh", "-c", fmt.Sprintf("echo success >/etc/%s; while true; do sleep 1; done", fn))) + content, err := runCommandAndReadContainerFile(fn, exec.Command(dockerBinary, "run", "-d", "--name", "c1", "busybox", "sh", "-c", fmt.Sprintf("echo success >/etc/%s && top", fn))) if err != nil { t.Fatal(err) } @@ -2016,16 +2016,16 @@ func TestRunMutableNetworkFiles(t *testing.T) { t.Fatal("Content was not what was modified in the container", string(content)) } - out, _, err := runCommandWithOutput(exec.Command(dockerBinary, "run", "-d", "--name", "c2", "busybox", "sh", "-c", fmt.Sprintf("while true; do cat /etc/%s; sleep 1; done", fn))) + out, _, err := runCommandWithOutput(exec.Command(dockerBinary, "run", "-d", "--name", "c2", "busybox", "top")) if err != nil { t.Fatal(err) } contID := strings.TrimSpace(out) - resolvConfPath := containerStorageFile(contID, fn) + netFilePath := containerStorageFile(contID, fn) - f, err := os.OpenFile(resolvConfPath, os.O_WRONLY|os.O_SYNC|os.O_APPEND, 0644) + f, err := os.OpenFile(netFilePath, os.O_WRONLY|os.O_SYNC|os.O_APPEND, 0644) if err != nil { t.Fatal(err) } @@ -2044,19 +2044,14 @@ func TestRunMutableNetworkFiles(t *testing.T) { f.Close() t.Fatal(err) } - f.Close() - time.Sleep(2 * time.Second) // don't race sleep - - out, _, err = runCommandWithOutput(exec.Command(dockerBinary, "logs", "c2")) + res, err := exec.Command(dockerBinary, "exec", contID, "cat", "/etc/"+fn).CombinedOutput() if err != nil { - t.Fatal(err) + t.Fatalf("Output: %s, error: %s", res, err) } - - lines := strings.Split(out, "\n") - if strings.TrimSpace(lines[len(lines)-2]) != "success2" { - t.Fatalf("Did not find the correct output in /etc/%s: %s %#v", fn, out, lines) + if string(res) != "success2\n" { + t.Fatalf("Expected content of %s: %q, got: %q", fn, "success2\n", res) } } logDone("run - mutable network files") From f9876dade26fd779c1cb0e2156e635c85c6e8ee2 Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Wed, 14 Jan 2015 00:31:25 +0200 Subject: [PATCH 256/513] Fix filenames for unix only integration-cli tests Test suffix comes after the platform/arch. Signed-off-by: Tonis Tiigi --- ...ker_cli_events_test_unix.go => docker_cli_events_unix_test.go} | 0 .../{docker_cli_run_test_unix.go => docker_cli_run_unix_test.go} | 0 ...i_save_load_test_unix.go => docker_cli_save_load_unix_test.go} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename integration-cli/{docker_cli_events_test_unix.go => docker_cli_events_unix_test.go} (100%) rename integration-cli/{docker_cli_run_test_unix.go => docker_cli_run_unix_test.go} (100%) rename integration-cli/{docker_cli_save_load_test_unix.go => docker_cli_save_load_unix_test.go} (100%) diff --git a/integration-cli/docker_cli_events_test_unix.go b/integration-cli/docker_cli_events_unix_test.go similarity index 100% rename from integration-cli/docker_cli_events_test_unix.go rename to integration-cli/docker_cli_events_unix_test.go diff --git a/integration-cli/docker_cli_run_test_unix.go b/integration-cli/docker_cli_run_unix_test.go similarity index 100% rename from integration-cli/docker_cli_run_test_unix.go rename to integration-cli/docker_cli_run_unix_test.go diff --git a/integration-cli/docker_cli_save_load_test_unix.go b/integration-cli/docker_cli_save_load_unix_test.go similarity index 100% rename from integration-cli/docker_cli_save_load_test_unix.go rename to integration-cli/docker_cli_save_load_unix_test.go From c03e15c9daddac86a4fdc7e6660f338b1b733f22 Mon Sep 17 00:00:00 2001 From: Malte Janduda Date: Wed, 14 Jan 2015 00:20:17 +0100 Subject: [PATCH 257/513] IPv6 docs: The ARP cache is called NDP neighbor cache in IPv6 Signed-off-by: Malte Janduda --- docs/sources/articles/networking.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/sources/articles/networking.md b/docs/sources/articles/networking.md index dac20af86..85e6222d8 100644 --- a/docs/sources/articles/networking.md +++ b/docs/sources/articles/networking.md @@ -578,9 +578,10 @@ for Docker. When adding a third host you would add a route for the subnet Remember the subnet for Docker containers should at least have a size of `/80`. This way an IPv6 address can end with the container's MAC address and you -prevent ARP cache invalidation issues in the Docker layer. So if you have a -`/64` for your whole environment use `/68` subnets for the hosts and `/80` for -the containers. This way you can use 4096 hosts with 16 `/80` subnets each. +prevent NDP neighbor cache invalidation issues in the Docker layer. So if you +have a `/64` for your whole environment use `/68` subnets for the hosts and +`/80` for the containers. This way you can use 4096 hosts with 16 `/80` subnets +each. Every configuration in the diagram that is visualized below the dashed line is handled by Docker: The `docker0` bridge IP address configuration, the route to From ee78e3f28459b655bf7df71eb4e2e22a04a8948d Mon Sep 17 00:00:00 2001 From: Jessica Frazelle Date: Tue, 13 Jan 2015 11:17:26 -0800 Subject: [PATCH 258/513] Add reference to rename endpoint in correct version & add to new Docker-DCO-1.1-Signed-off-by: Jessica Frazelle (github: jfrazelle) --- .../reference/api/docker_remote_api.md | 5 ++++ .../reference/api/docker_remote_api_v1.15.md | 21 ---------------- .../reference/api/docker_remote_api_v1.17.md | 25 +++++++++++++++++++ 3 files changed, 30 insertions(+), 21 deletions(-) diff --git a/docs/sources/reference/api/docker_remote_api.md b/docs/sources/reference/api/docker_remote_api.md index fa210d458..08439ce0d 100644 --- a/docs/sources/reference/api/docker_remote_api.md +++ b/docs/sources/reference/api/docker_remote_api.md @@ -56,6 +56,11 @@ Docker client now hints potential proxies about connection hijacking using HTTP **New!** This endpoint now returns the list current execs associated with the container (`ExecIDs`). +`POST /containers/(id)/rename` + +**New!** +New endpoint to rename a container `id` to a new name. + ## v1.16 ### Full Documentation diff --git a/docs/sources/reference/api/docker_remote_api_v1.15.md b/docs/sources/reference/api/docker_remote_api_v1.15.md index 7edfa9101..4d27a6150 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.15.md +++ b/docs/sources/reference/api/docker_remote_api_v1.15.md @@ -647,27 +647,6 @@ Status Codes: - **404** – no such container - **500** – server error -### Rename a container - -`POST /containers/(id)/rename/(new_name)` - -Rename the container `id` to a `new_name` - -**Example request**: - - POST /containers/e90e34656806/rename/new_name HTTP/1.1 - -**Example response**: - - HTTP/1.1 204 No Content - -Status Codes: - -- **204** – no error -- **404** – no such container -- **409** - conflict name already assigned -- **500** – server error - ### Pause a container `POST /containers/(id)/pause` diff --git a/docs/sources/reference/api/docker_remote_api_v1.17.md b/docs/sources/reference/api/docker_remote_api_v1.17.md index 67b862302..6c20394f6 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.17.md +++ b/docs/sources/reference/api/docker_remote_api_v1.17.md @@ -599,6 +599,31 @@ Status Codes: - **404** – no such container - **500** – server error +### Rename a container + +`POST /containers/(id)/rename` + +Rename the container `id` to a `new_name` + +**Example request**: + + POST /containers/e90e34656806/rename?name=new_name HTTP/1.1 + +**Example response**: + + HTTP/1.1 204 No Content + +Query Parameters: + +- **name** – new name for the container + +Status Codes: + +- **204** – no error +- **404** – no such container +- **409** - conflict name already assigned +- **500** – server error + ### Pause a container `POST /containers/(id)/pause` From 90c5ff4f06b8672cd2b427d120a8988bfc9f1de7 Mon Sep 17 00:00:00 2001 From: Ian Calvert Date: Wed, 17 Dec 2014 22:17:14 +0000 Subject: [PATCH 259/513] Run the remote image presence checks in parallel Signed-off-by: Ian Calvert --- graph/push.go | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/graph/push.go b/graph/push.go index 09e13a5cf..6d84be6ed 100644 --- a/graph/push.go +++ b/graph/push.go @@ -113,18 +113,35 @@ func (s *TagStore) pushRepository(r *registry.Session, out io.Writer, repoInfo * if tag == "" { nTag = len(localRepo) } + completed := make(chan bool) + needsPush := make([]bool, len(imgList)) for _, ep := range repoData.Endpoints { out.Write(sf.FormatStatus("", "Pushing repository %s (%d tags)", repoInfo.CanonicalName, nTag)) - for _, imgId := range imgList { - if err := r.LookupRemoteImage(imgId, ep, repoData.Tokens); err != nil { - log.Errorf("Error in LookupRemoteImage: %s", err) - if _, err := s.pushImage(r, out, imgId, ep, repoData.Tokens, sf); err != nil { + + for i, imgId := range imgList { + go func(i int, imgId string) { + if err := r.LookupRemoteImage(imgId, ep, repoData.Tokens); err == nil { + out.Write(sf.FormatStatus("", "Image %s already pushed, skipping", utils.TruncateID(imgId))) + needsPush[i] = false + } else { + log.Errorf("Error in LookupRemoteImage: %s", err) + out.Write(sf.FormatStatus("", "Image %s not pushed, adding to queue", utils.TruncateID(imgId))) + needsPush[i] = true + } + completed <- true + }(i, imgId) + } + for i := 0; i < len(imgList); i++ { + <-completed + } + for i, imgId := range imgList { + if needsPush[i] { + if _, err := s.pushImage(r, out, remoteName, imgId, ep, repoData.Tokens, sf); err != nil { // FIXME: Continue on error? return err } - } else { - out.Write(sf.FormatStatus("", "Image %s already pushed, skipping", utils.TruncateID(imgId))) } + for _, tag := range tagsByImage[imgId] { out.Write(sf.FormatStatus("", "Pushing tag for rev [%s] on {%s}", utils.TruncateID(imgId), ep+"repositories/"+repoInfo.RemoteName+"/tags/"+tag)) From 33286589291a56fe0a4728f8b45ad3feb5494226 Mon Sep 17 00:00:00 2001 From: Ian Calvert Date: Thu, 18 Dec 2014 09:38:31 +0000 Subject: [PATCH 260/513] Replace custom waiting code with a WaitGroup Signed-off-by: Ian Calvert --- graph/push.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/graph/push.go b/graph/push.go index 6d84be6ed..1dc08c6f9 100644 --- a/graph/push.go +++ b/graph/push.go @@ -6,6 +6,7 @@ import ( "io/ioutil" "os" "path" + "sync" log "github.com/Sirupsen/logrus" "github.com/docker/docker/engine" @@ -113,13 +114,15 @@ func (s *TagStore) pushRepository(r *registry.Session, out io.Writer, repoInfo * if tag == "" { nTag = len(localRepo) } - completed := make(chan bool) + var wg sync.WaitGroup needsPush := make([]bool, len(imgList)) for _, ep := range repoData.Endpoints { out.Write(sf.FormatStatus("", "Pushing repository %s (%d tags)", repoInfo.CanonicalName, nTag)) for i, imgId := range imgList { + wg.Add(1) go func(i int, imgId string) { + defer wg.Done() if err := r.LookupRemoteImage(imgId, ep, repoData.Tokens); err == nil { out.Write(sf.FormatStatus("", "Image %s already pushed, skipping", utils.TruncateID(imgId))) needsPush[i] = false @@ -128,12 +131,11 @@ func (s *TagStore) pushRepository(r *registry.Session, out io.Writer, repoInfo * out.Write(sf.FormatStatus("", "Image %s not pushed, adding to queue", utils.TruncateID(imgId))) needsPush[i] = true } - completed <- true }(i, imgId) } - for i := 0; i < len(imgList); i++ { - <-completed - } + + wg.Wait() + for i, imgId := range imgList { if needsPush[i] { if _, err := s.pushImage(r, out, remoteName, imgId, ep, repoData.Tokens, sf); err != nil { From 476cf1b906ca14cc3ce1347f1aa479713fa6ce6e Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Tue, 13 Jan 2015 15:24:51 -0800 Subject: [PATCH 261/513] Use channels and set workers to make image lookup safe This refactors the starting work by the prior commits to make this safe for access. A maximum of 5 worker go routines are started to lookup images on the endpoint. Another go routine consumes the images that are required to be pushed into a map for quick lookups. The map is required because the pushing of the image json and layer have to be done in the correct order or the registry will explode in fire. Signed-off-by: Michael Crosby --- graph/push.go | 184 +++++++++++++++++++++++++++++++------------------- 1 file changed, 113 insertions(+), 71 deletions(-) diff --git a/graph/push.go b/graph/push.go index 1dc08c6f9..0ec81a515 100644 --- a/graph/push.go +++ b/graph/push.go @@ -62,103 +62,145 @@ func (s *TagStore) getImageList(localRepo map[string]string, requestedTag string return imageList, tagsByImage, nil } -func (s *TagStore) pushRepository(r *registry.Session, out io.Writer, repoInfo *registry.RepositoryInfo, localRepo map[string]string, tag string, sf *utils.StreamFormatter) error { - out = utils.NewWriteFlusher(out) - log.Debugf("Local repo: %s", localRepo) - imgList, tagsByImage, err := s.getImageList(localRepo, tag) - if err != nil { - return err - } - - out.Write(sf.FormatStatus("", "Sending image list")) - - var ( - repoData *registry.RepositoryData - imageIndex []*registry.ImgData - ) - - for _, imgId := range imgList { - if tags, exists := tagsByImage[imgId]; exists { +// createImageIndex returns an index of an image's layer IDs and tags. +func (s *TagStore) createImageIndex(images []string, tags map[string][]string) []*registry.ImgData { + var imageIndex []*registry.ImgData + for _, id := range images { + if tags, hasTags := tags[id]; hasTags { // If an image has tags you must add an entry in the image index // for each tag for _, tag := range tags { imageIndex = append(imageIndex, ®istry.ImgData{ - ID: imgId, + ID: id, Tag: tag, }) } - } else { - // If the image does not have a tag it still needs to be sent to the - // registry with an empty tag so that it is accociated with the repository - imageIndex = append(imageIndex, ®istry.ImgData{ - ID: imgId, - Tag: "", - }) + continue + } + // If the image does not have a tag it still needs to be sent to the + // registry with an empty tag so that it is accociated with the repository + imageIndex = append(imageIndex, ®istry.ImgData{ + ID: id, + Tag: "", + }) + } + return imageIndex +} +type imagePushData struct { + id string + endpoint string + tokens []string +} + +// lookupImageOnEndpoint checks the specified endpoint to see if an image exists +// and if it is absent then it sends the image id to the channel to be pushed. +func lookupImageOnEndpoint(wg *sync.WaitGroup, r *registry.Session, out io.Writer, sf *utils.StreamFormatter, + images chan imagePushData, imagesToPush chan string) { + defer wg.Done() + for image := range images { + if err := r.LookupRemoteImage(image.id, image.endpoint, image.tokens); err != nil { + log.Errorf("Error in LookupRemoteImage: %s", err) + imagesToPush <- image.id + continue + } + out.Write(sf.FormatStatus("", "Image %s already pushed, skipping", utils.TruncateID(image.id))) + } +} + +func (s *TagStore) pushImageToEndpoint(endpoint string, out io.Writer, remoteName string, imageIDs []string, + tags map[string][]string, repo *registry.RepositoryData, sf *utils.StreamFormatter, r *registry.Session) error { + workerCount := len(imageIDs) + // start a maximum of 5 workers to check if images exist on the specified endpoint. + if workerCount > 5 { + workerCount = 5 + } + var ( + wg = &sync.WaitGroup{} + imageData = make(chan imagePushData, workerCount*2) + imagesToPush = make(chan string, workerCount*2) + pushes = make(chan map[string]struct{}, 1) + ) + for i := 0; i < workerCount; i++ { + wg.Add(1) + go lookupImageOnEndpoint(wg, r, out, sf, imageData, imagesToPush) + } + // start a go routine that consumes the images to push + go func() { + shouldPush := make(map[string]struct{}) + for id := range imagesToPush { + shouldPush[id] = struct{}{} + } + pushes <- shouldPush + }() + for _, id := range imageIDs { + imageData <- imagePushData{ + id: id, + endpoint: endpoint, + tokens: repo.Tokens, } } + // close the channel to notify the workers that there will be no more images to check. + close(imageData) + wg.Wait() + close(imagesToPush) + // wait for all the images that require pushes to be collected into a consumable map. + shouldPush := <-pushes + // finish by pushing any images and tags to the endpoint. The order that the images are pushed + // is very important that is why we are still itterating over the ordered list of imageIDs. + for _, id := range imageIDs { + if _, push := shouldPush[id]; push { + if _, err := s.pushImage(r, out, id, endpoint, repo.Tokens, sf); err != nil { + // FIXME: Continue on error? + return err + } + } + for _, tag := range tags[id] { + out.Write(sf.FormatStatus("", "Pushing tag for rev [%s] on {%s}", utils.TruncateID(id), endpoint+"repositories/"+remoteName+"/tags/"+tag)) + if err := r.PushRegistryTag(remoteName, id, tag, endpoint, repo.Tokens); err != nil { + return err + } + } + } + return nil +} +// pushRepository pushes layers that do not already exist on the registry. +func (s *TagStore) pushRepository(r *registry.Session, out io.Writer, + repoInfo *registry.RepositoryInfo, localRepo map[string]string, + tag string, sf *utils.StreamFormatter) error { + log.Debugf("Local repo: %s", localRepo) + out = utils.NewWriteFlusher(out) + imgList, tags, err := s.getImageList(localRepo, tag) + if err != nil { + return err + } + out.Write(sf.FormatStatus("", "Sending image list")) + + imageIndex := s.createImageIndex(imgList, tags) log.Debugf("Preparing to push %s with the following images and tags", localRepo) for _, data := range imageIndex { log.Debugf("Pushing ID: %s with Tag: %s", data.ID, data.Tag) } - // Register all the images in a repository with the registry // If an image is not in this list it will not be associated with the repository - repoData, err = r.PushImageJSONIndex(repoInfo.RemoteName, imageIndex, false, nil) + repoData, err := r.PushImageJSONIndex(repoInfo.RemoteName, imageIndex, false, nil) if err != nil { return err } - nTag := 1 if tag == "" { nTag = len(localRepo) } - var wg sync.WaitGroup - needsPush := make([]bool, len(imgList)) - for _, ep := range repoData.Endpoints { - out.Write(sf.FormatStatus("", "Pushing repository %s (%d tags)", repoInfo.CanonicalName, nTag)) - - for i, imgId := range imgList { - wg.Add(1) - go func(i int, imgId string) { - defer wg.Done() - if err := r.LookupRemoteImage(imgId, ep, repoData.Tokens); err == nil { - out.Write(sf.FormatStatus("", "Image %s already pushed, skipping", utils.TruncateID(imgId))) - needsPush[i] = false - } else { - log.Errorf("Error in LookupRemoteImage: %s", err) - out.Write(sf.FormatStatus("", "Image %s not pushed, adding to queue", utils.TruncateID(imgId))) - needsPush[i] = true - } - }(i, imgId) - } - - wg.Wait() - - for i, imgId := range imgList { - if needsPush[i] { - if _, err := s.pushImage(r, out, remoteName, imgId, ep, repoData.Tokens, sf); err != nil { - // FIXME: Continue on error? - return err - } - } - - for _, tag := range tagsByImage[imgId] { - out.Write(sf.FormatStatus("", "Pushing tag for rev [%s] on {%s}", utils.TruncateID(imgId), ep+"repositories/"+repoInfo.RemoteName+"/tags/"+tag)) - - if err := r.PushRegistryTag(repoInfo.RemoteName, imgId, tag, ep, repoData.Tokens); err != nil { - return err - } - } + out.Write(sf.FormatStatus("", "Pushing repository %s (%d tags)", repoInfo.CanonicalName, nTag)) + // push the repository to each of the endpoints only if it does not exist. + for _, endpoint := range repoData.Endpoints { + if err := s.pushImageToEndpoint(endpoint, out, repoInfo.RemoteName, imgList, tags, repoData, sf, r); err != nil { + return err } } - - if _, err := r.PushImageJSONIndex(repoInfo.RemoteName, imageIndex, true, repoData.Endpoints); err != nil { - return err - } - - return nil + _, err = r.PushImageJSONIndex(repoInfo.RemoteName, imageIndex, true, repoData.Endpoints) + return err } func (s *TagStore) pushImage(r *registry.Session, out io.Writer, imgID, ep string, token []string, sf *utils.StreamFormatter) (checksum string, err error) { From 28cf8fddd4c19e98fd0a6fcf0a6e7ea545521412 Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Wed, 14 Jan 2015 01:33:05 +0200 Subject: [PATCH 262/513] Fix attach stream closing issues Fixes: #9860 Fixes: detach and attach tty mode We never actually need to close container `stdin` after `stdout/stderr` finishes. We only need to close the `stdin` goroutine. In some cases this also means closing `stdin` but that is already controlled by the goroutine itself. Signed-off-by: Tonis Tiigi --- daemon/attach.go | 3 +- .../docker_cli_attach_unix_test.go | 123 ++++++++++++++++++ 2 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 integration-cli/docker_cli_attach_unix_test.go diff --git a/daemon/attach.go b/daemon/attach.go index dc7ffa307..881b021e1 100644 --- a/daemon/attach.go +++ b/daemon/attach.go @@ -179,9 +179,8 @@ func (daemon *Daemon) attach(streamConfig *StreamConfig, openStdin, stdinOnce, t } defer func() { // Make sure stdin gets closed - if stdinOnce && cStdin != nil { + if stdin != nil { stdin.Close() - cStdin.Close() } streamPipe.Close() wg.Done() diff --git a/integration-cli/docker_cli_attach_unix_test.go b/integration-cli/docker_cli_attach_unix_test.go new file mode 100644 index 000000000..451147d8a --- /dev/null +++ b/integration-cli/docker_cli_attach_unix_test.go @@ -0,0 +1,123 @@ +package main + +import ( + "os/exec" + "strings" + "testing" + "time" + + "github.com/kr/pty" +) + +// #9860 +func TestAttachClosedOnContainerStop(t *testing.T) { + defer deleteAllContainers() + + cmd := exec.Command(dockerBinary, "run", "-dti", "busybox", "sleep", "2") + out, _, err := runCommandWithOutput(cmd) + if err != nil { + t.Fatalf("failed to start container: %v (%v)", out, err) + } + + id := stripTrailingCharacters(out) + if err := waitRun(id); err != nil { + t.Fatal(err) + } + + done := make(chan struct{}) + + go func() { + defer close(done) + + _, tty, err := pty.Open() + if err != nil { + t.Fatalf("could not open pty: %v", err) + } + attachCmd := exec.Command(dockerBinary, "attach", id) + attachCmd.Stdin = tty + attachCmd.Stdout = tty + attachCmd.Stderr = tty + + if err := attachCmd.Run(); err != nil { + t.Fatalf("attach returned error %s", err) + } + }() + + waitCmd := exec.Command(dockerBinary, "wait", id) + if out, _, err = runCommandWithOutput(waitCmd); err != nil { + t.Fatalf("error thrown while waiting for container: %s, %v", out, err) + } + select { + case <-done: + case <-time.After(attachWait): + t.Fatal("timed out without attach returning") + } + + logDone("attach - return after container finished") +} + +func TestAttachAfterDetach(t *testing.T) { + defer deleteAllContainers() + + name := "detachtest" + + cpty, tty, err := pty.Open() + if err != nil { + t.Fatalf("Could not open pty: %v", err) + } + cmd := exec.Command(dockerBinary, "run", "-ti", "--name", name, "busybox") + cmd.Stdin = tty + cmd.Stdout = tty + cmd.Stderr = tty + + detached := make(chan struct{}) + go func() { + if err := cmd.Run(); err != nil { + t.Fatalf("attach returned error %s", err) + } + close(detached) + }() + + time.Sleep(500 * time.Millisecond) + cpty.Write([]byte{16}) + time.Sleep(100 * time.Millisecond) + cpty.Write([]byte{17}) + + <-detached + + cpty, tty, err = pty.Open() + if err != nil { + t.Fatalf("Could not open pty: %v", err) + } + + cmd = exec.Command(dockerBinary, "attach", name) + cmd.Stdin = tty + cmd.Stdout = tty + cmd.Stderr = tty + + go func() { + if err := cmd.Run(); err != nil { + t.Fatalf("attach returned error %s", err) + } + cpty.Close() // unblocks the reader in case of a failure + }() + + time.Sleep(500 * time.Millisecond) + cpty.Write([]byte("\n")) + time.Sleep(500 * time.Millisecond) + bytes := make([]byte, 10) + + n, err := cpty.Read(bytes) + + if err != nil { + t.Fatalf("prompt read failed: %v", err) + } + + if !strings.Contains(string(bytes[:n]), "/ #") { + t.Fatalf("failed to get a new prompt. got %s", string(bytes[:n])) + } + + cpty.Write([]byte("exit\n")) + + logDone("attach - reconnect after detaching") +} From 23feaaa240853c0e7f9817f8c2d272dd1c93ac3f Mon Sep 17 00:00:00 2001 From: Dan Walsh Date: Tue, 25 Nov 2014 15:10:53 -0500 Subject: [PATCH 263/513] Allow the container to share the PID namespace with the host We want to be able to use container without the PID namespace. We basically want containers that can manage the host os, which I call Super Privileged Containers. We eventually would like to get to the point where the only namespace we use is the MNT namespace to bring the Apps userspace with it. By eliminating the PID namespace we can get better communication between the host and the clients and potentially tools like strace and gdb become easier to use. We also see tools like libvirtd running within a container telling systemd to place a VM in a particular cgroup, we need to have communications of the PID. I don't see us needing to share PID namespaces between containers, since this is really what docker exec does. So currently I see us just needing docker run --pid=host Docker-DCO-1.1-Signed-off-by: Dan Walsh (github: rhatdan) --- daemon/container.go | 4 ++++ daemon/create.go | 6 +++--- daemon/execdriver/driver.go | 6 ++++++ daemon/execdriver/native/create.go | 13 +++++++++++++ docs/man/docker-create.1.md | 6 ++++++ docs/man/docker-run.1.md | 6 ++++++ docs/sources/reference/commandline/cli.md | 1 + docs/sources/reference/run.md | 22 +++++++++++++++++++++- runconfig/hostconfig.go | 23 +++++++++++++++++++++++ runconfig/parse.go | 9 ++++++++- 10 files changed, 91 insertions(+), 5 deletions(-) diff --git a/daemon/container.go b/daemon/container.go index 8bbfb07b2..86c0a7d84 100644 --- a/daemon/container.go +++ b/daemon/container.go @@ -250,6 +250,9 @@ func populateCommand(c *Container, env []string) error { ipc.HostIpc = c.hostConfig.IpcMode.IsHost() } + pid := &execdriver.Pid{} + pid.HostPid = c.hostConfig.PidMode.IsHost() + // Build lists of devices allowed and created within the container. userSpecifiedDevices := make([]*devices.Device, len(c.hostConfig.Devices)) for i, deviceMapping := range c.hostConfig.Devices { @@ -295,6 +298,7 @@ func populateCommand(c *Container, env []string) error { WorkingDir: c.Config.WorkingDir, Network: en, Ipc: ipc, + Pid: pid, Resources: resources, AllowedDevices: allowedDevices, AutoCreatedDevices: autoCreatedDevices, diff --git a/daemon/create.go b/daemon/create.go index f53461a45..785b0cc34 100644 --- a/daemon/create.go +++ b/daemon/create.go @@ -92,7 +92,7 @@ func (daemon *Daemon) Create(config *runconfig.Config, hostConfig *runconfig.Hos return nil, nil, err } if hostConfig != nil && hostConfig.SecurityOpt == nil { - hostConfig.SecurityOpt, err = daemon.GenerateSecurityOpt(hostConfig.IpcMode) + hostConfig.SecurityOpt, err = daemon.GenerateSecurityOpt(hostConfig.IpcMode, hostConfig.PidMode) if err != nil { return nil, nil, err } @@ -124,8 +124,8 @@ func (daemon *Daemon) Create(config *runconfig.Config, hostConfig *runconfig.Hos return container, warnings, nil } -func (daemon *Daemon) GenerateSecurityOpt(ipcMode runconfig.IpcMode) ([]string, error) { - if ipcMode.IsHost() { +func (daemon *Daemon) GenerateSecurityOpt(ipcMode runconfig.IpcMode, pidMode runconfig.PidMode) ([]string, error) { + if ipcMode.IsHost() || pidMode.IsHost() { return label.DisableSecOpt(), nil } if ipcContainer := ipcMode.Container(); ipcContainer != "" { diff --git a/daemon/execdriver/driver.go b/daemon/execdriver/driver.go index 2a5eff556..80aad44ff 100644 --- a/daemon/execdriver/driver.go +++ b/daemon/execdriver/driver.go @@ -77,6 +77,11 @@ type Ipc struct { HostIpc bool `json:"host_ipc"` } +// PID settings of the container +type Pid struct { + HostPid bool `json:"host_pid"` +} + type NetworkInterface struct { Gateway string `json:"gateway"` IPAddress string `json:"ip"` @@ -126,6 +131,7 @@ type Command struct { ConfigPath string `json:"config_path"` // this should be able to be removed when the lxc template is moved into the driver Network *Network `json:"network"` Ipc *Ipc `json:"ipc"` + Pid *Pid `json:"pid"` Resources *Resources `json:"resources"` Mounts []Mount `json:"mounts"` AllowedDevices []*devices.Device `json:"allowed_devices"` diff --git a/daemon/execdriver/native/create.go b/daemon/execdriver/native/create.go index 99c21a20b..7b764a50e 100644 --- a/daemon/execdriver/native/create.go +++ b/daemon/execdriver/native/create.go @@ -40,6 +40,10 @@ func (d *driver) createContainer(c *execdriver.Command) (*libcontainer.Config, e return nil, err } + if err := d.createPid(container, c); err != nil { + return nil, err + } + if err := d.createNetwork(container, c); err != nil { return nil, err } @@ -151,6 +155,15 @@ func (d *driver) createIpc(container *libcontainer.Config, c *execdriver.Command return nil } +func (d *driver) createPid(container *libcontainer.Config, c *execdriver.Command) error { + if c.Pid.HostPid { + container.Namespaces.Remove(libcontainer.NEWPID) + return nil + } + + return nil +} + func (d *driver) setPrivileged(container *libcontainer.Config) (err error) { container.Capabilities = capabilities.GetAllCapabilities() container.Cgroups.AllowAllDevices = true diff --git a/docs/man/docker-create.1.md b/docs/man/docker-create.1.md index d4b6c44e8..63fe20ed1 100644 --- a/docs/man/docker-create.1.md +++ b/docs/man/docker-create.1.md @@ -32,6 +32,7 @@ docker-create - Create a new container [**--net**[=*"bridge"*]] [**-P**|**--publish-all**[=*false*]] [**-p**|**--publish**[=*[]*]] +[**--pid**[=*[]*]] [**--privileged**[=*false*]] [**--restart**[=*RESTART*]] [**--security-opt**[=*[]*]] @@ -131,6 +132,11 @@ IMAGE [COMMAND] [ARG...] When specifying ranges for both, the number of container ports in the range must match the number of host ports in the range. (e.g., `-p 1234-1236:1234-1236/tcp`) (use 'docker port' to see the actual mapping) +**--pid**=host + Set the PID mode for the container + **host**: use the host's PID namespace inside the container. + Note: the host mode gives the container full access to local PID and is therefore considered insecure. + **--privileged**=*true*|*false* Give extended privileges to this container. The default is *false*. diff --git a/docs/man/docker-run.1.md b/docs/man/docker-run.1.md index 2ba984a6c..de035e965 100644 --- a/docs/man/docker-run.1.md +++ b/docs/man/docker-run.1.md @@ -33,6 +33,7 @@ docker-run - Run a command in a new container [**--net**[=*"bridge"*]] [**-P**|**--publish-all**[=*false*]] [**-p**|**--publish**[=*[]*]] +[**--pid**[=*[]*]] [**--privileged**[=*false*]] [**--restart**[=*RESTART*]] [**--rm**[=*false*]] @@ -234,6 +235,11 @@ mapping between the host ports and the exposed ports, use **docker port**. When specifying ranges for both, the number of container ports in the range must match the number of host ports in the range. (e.g., `-p 1234-1236:1234-1236/tcp`) (use 'docker port' to see the actual mapping) +**--pid**=host + Set the PID mode for the container + **host**: use the host's PID namespace inside the container. + Note: the host mode gives the container full access to local PID and is therefore considered insecure. + **--privileged**=*true*|*false* Give extended privileges to this container. The default is *false*. diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index 877a19508..0b7b0cda0 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -1604,6 +1604,7 @@ removed before the image is removed. Both hostPort and containerPort can be specified as a range of ports. When specifying ranges for both, the number of container ports in the range must match the number of host ports in the range. (e.g., `-p 1234-1236:1234-1236/tcp`) (use 'docker port' to see the actual mapping) + --pid=host 'host': use the host PID namespace inside the container. Note: the host mode gives the container full access to local system services such as D-bus and is therefore considered insecure. --privileged=false Give extended privileges to this container --restart="" Restart policy to apply when a container exits (no, on-failure[:max-retry], always) --rm=false Automatically remove the container when it exits (incompatible with -d) diff --git a/docs/sources/reference/run.md b/docs/sources/reference/run.md index 012a6e71f..d594066ad 100644 --- a/docs/sources/reference/run.md +++ b/docs/sources/reference/run.md @@ -133,11 +133,31 @@ While not strictly a means of identifying a container, you can specify a version image you'd like to run the container with by adding `image[:tag]` to the command. For example, `docker run ubuntu:14.04`. +## PID Settings + --pid="" : Set the PID (Process) Namespace mode for the container, + 'host': use the host's PID namespace inside the container +By default, all containers have the PID namespace enabled. + +PID namespace provides separation of processes. The PID Namespace removes the +view of the system processes, and allows process ids to be reused including +pid 1. + +In certain cases you want your container to share the host's process namespace, +basically allowing processes within the container to see all of the processes +on the system. For example, you could build a container with debugging tools +like `strace` or `gdb`, but want to use these tools when debugging processes +within the container. + + $ sudo docker run --pid=host rhel7 strace -p 1234 + +This command would allow you to use `strace` inside the container on pid 1234 on +the host. + ## IPC Settings --ipc="" : Set the IPC mode for the container, 'container:': reuses another container's IPC namespace 'host': use the host's IPC namespace inside the container -By default, all containers have the IPC namespace enabled +By default, all containers have the IPC namespace enabled. IPC (POSIX/SysV IPC) namespace provides separation of named shared memory segments, semaphores and message queues. diff --git a/runconfig/hostconfig.go b/runconfig/hostconfig.go index b619e9c31..054c68362 100644 --- a/runconfig/hostconfig.go +++ b/runconfig/hostconfig.go @@ -66,6 +66,27 @@ func (n IpcMode) Container() string { return "" } +type PidMode string + +// IsPrivate indicates whether container use it's private pid stack +func (n PidMode) IsPrivate() bool { + return !(n.IsHost()) +} + +func (n PidMode) IsHost() bool { + return n == "host" +} + +func (n PidMode) Valid() bool { + parts := strings.Split(string(n), ":") + switch mode := parts[0]; mode { + case "", "host": + default: + return false + } + return true +} + type DeviceMapping struct { PathOnHost string PathInContainer string @@ -92,6 +113,7 @@ type HostConfig struct { Devices []DeviceMapping NetworkMode NetworkMode IpcMode IpcMode + PidMode PidMode CapAdd []string CapDrop []string RestartPolicy RestartPolicy @@ -125,6 +147,7 @@ func ContainerHostConfigFromJob(job *engine.Job) *HostConfig { PublishAllPorts: job.GetenvBool("PublishAllPorts"), NetworkMode: NetworkMode(job.Getenv("NetworkMode")), IpcMode: IpcMode(job.Getenv("IpcMode")), + PidMode: PidMode(job.Getenv("PidMode")), } job.GetenvJson("LxcConf", &hostConfig.LxcConf) diff --git a/runconfig/parse.go b/runconfig/parse.go index 3bab8ac76..781f721f6 100644 --- a/runconfig/parse.go +++ b/runconfig/parse.go @@ -46,6 +46,7 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe flNetwork = cmd.Bool([]string{"#n", "#-networking"}, true, "Enable networking for this container") flPrivileged = cmd.Bool([]string{"#privileged", "-privileged"}, false, "Give extended privileges to this container") + flPidMode = cmd.String([]string{"-pid"}, "", "Default is to create a private PID namespace for the container\n'host': use the host PID namespace inside the container. Note: the host mode gives the container full access to processes on the system and is therefore considered insecure.") flPublishAll = cmd.Bool([]string{"P", "-publish-all"}, false, "Publish all exposed ports to random ports on the host interfaces") flStdin = cmd.Bool([]string{"i", "-interactive"}, false, "Keep STDIN open even if not attached") flTty = cmd.Bool([]string{"t", "-tty"}, false, "Allocate a pseudo-TTY") @@ -248,7 +249,12 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe ipcMode := IpcMode(*flIpcMode) if !ipcMode.Valid() { - return nil, nil, cmd, fmt.Errorf("--ipc: invalid IPC mode: %v", err) + return nil, nil, cmd, fmt.Errorf("--ipc: invalid IPC mode") + } + + pidMode := PidMode(*flPidMode) + if !pidMode.Valid() { + return nil, nil, cmd, fmt.Errorf("--pid: invalid PID mode") } netMode, err := parseNetMode(*flNetMode) @@ -300,6 +306,7 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe VolumesFrom: flVolumesFrom.GetAll(), NetworkMode: netMode, IpcMode: ipcMode, + PidMode: pidMode, Devices: deviceMappings, CapAdd: flCapAdd.GetAll(), CapDrop: flCapDrop.GetAll(), From 15e8f3fdd31dc498be106a69a7e29ba459c36c1a Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Tue, 13 Jan 2015 16:39:08 -0800 Subject: [PATCH 264/513] Add test for --pid=host Signed-off-by: Michael Crosby --- integration-cli/docker_cli_run_test.go | 32 ++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/integration-cli/docker_cli_run_test.go b/integration-cli/docker_cli_run_test.go index f6985ba3c..66d3206c7 100644 --- a/integration-cli/docker_cli_run_test.go +++ b/integration-cli/docker_cli_run_test.go @@ -2750,6 +2750,38 @@ func TestContainerNetworkMode(t *testing.T) { logDone("run - container shared network namespace") } +func TestRunModePidHost(t *testing.T) { + hostPid, err := os.Readlink("/proc/1/ns/pid") + if err != nil { + t.Fatal(err) + } + + cmd := exec.Command(dockerBinary, "run", "--pid=host", "busybox", "readlink", "/proc/self/ns/pid") + out2, _, err := runCommandWithOutput(cmd) + if err != nil { + t.Fatal(err, out2) + } + + out2 = strings.Trim(out2, "\n") + if hostPid != out2 { + t.Fatalf("PID different with --pid=host %s != %s\n", hostPid, out2) + } + + cmd = exec.Command(dockerBinary, "run", "busybox", "readlink", "/proc/self/ns/pid") + out2, _, err = runCommandWithOutput(cmd) + if err != nil { + t.Fatal(err, out2) + } + + out2 = strings.Trim(out2, "\n") + if hostPid == out2 { + t.Fatalf("PID should be different without --pid=host %s == %s\n", hostPid, out2) + } + deleteAllContainers() + + logDone("run - pid host mode") +} + func TestRunTLSverify(t *testing.T) { cmd := exec.Command(dockerBinary, "ps") out, ec, err := runCommandWithOutput(cmd) From d5df948829bfd6e12dc2c0ca3228b583386b6e87 Mon Sep 17 00:00:00 2001 From: Sven Dowideit Date: Mon, 29 Dec 2014 18:19:42 +1000 Subject: [PATCH 265/513] Add a note that remote and Boot2Docker users should not type sudo Docker-DCO-1.1-Signed-off-by: Sven Dowideit (github: SvenDowideit) --- docs/Dockerfile | 2 ++ docs/sources/articles/basics.md | 2 +- docs/sources/faq.md | 2 ++ docs/sources/include/no-remote-sudo.md | 3 +++ docs/sources/installation/mac.md | 2 ++ docs/sources/installation/windows.md | 2 ++ docs/sources/reference/commandline/cli.md | 2 ++ docs/sources/userguide/dockerizing.md | 2 ++ 8 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 docs/sources/include/no-remote-sudo.md diff --git a/docs/Dockerfile b/docs/Dockerfile index d801ec213..a29e8c95f 100644 --- a/docs/Dockerfile +++ b/docs/Dockerfile @@ -56,4 +56,6 @@ RUN VERSION=$(cat VERSION) \ EXPOSE 8000 +RUN cd sources && rgrep --files-with-matches '{{ include ".*" }}' | xargs sed -i~ 's/{{ include "\(.*\)" }}/cat include\/\1/ge' + CMD ["mkdocs", "serve"] diff --git a/docs/sources/articles/basics.md b/docs/sources/articles/basics.md index 29b9a2f19..4cdcab4aa 100644 --- a/docs/sources/articles/basics.md +++ b/docs/sources/articles/basics.md @@ -37,7 +37,7 @@ image cache. > characters of the full image ID - which can be found using > `docker inspect` or `docker images --no-trunc=true` -**If you're using OS X** then you shouldn't use `sudo`. +{{ include "no-remote-sudo.md" }} ## Running an interactive shell diff --git a/docs/sources/faq.md b/docs/sources/faq.md index 169df37f5..f517db73a 100644 --- a/docs/sources/faq.md +++ b/docs/sources/faq.md @@ -26,6 +26,8 @@ Windows*](../installation/windows/#windows) installation guides. The small Linux distribution boot2docker can be run inside virtual machines on these two operating systems. +{{ include "no-remote-sudo.md" }} + ### How do containers compare to virtual machines? They are complementary. VMs are best used to allocate chunks of diff --git a/docs/sources/include/no-remote-sudo.md b/docs/sources/include/no-remote-sudo.md new file mode 100644 index 000000000..065b0cbfd --- /dev/null +++ b/docs/sources/include/no-remote-sudo.md @@ -0,0 +1,3 @@ +> **Note:** if you are using a remote Docker daemon, such as Boot2Docker, +> then _do not_ type the `sudo` before the `docker` commands shown in the +> documentation's examples. diff --git a/docs/sources/installation/mac.md b/docs/sources/installation/mac.md index 89fed1711..d31cd697b 100644 --- a/docs/sources/installation/mac.md +++ b/docs/sources/installation/mac.md @@ -72,6 +72,8 @@ complete. You can test it by following the directions below. ## Running Docker +{{ include "no-remote-sudo.md" }} + From your terminal, you can test that Docker is running with our small `hello-world` example image: Start the vm (`boot2docker start`) and then run: diff --git a/docs/sources/installation/windows.md b/docs/sources/installation/windows.md index 90268867b..26b2a42a4 100644 --- a/docs/sources/installation/windows.md +++ b/docs/sources/installation/windows.md @@ -49,6 +49,8 @@ and the Boot2Docker management tool. ## Running Docker +{{ include "no-remote-sudo.md" }} + Boot2Docker will log you in automatically so you can start using Docker right away. Let's try the `hello-world` example image. Run diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index 0b7b0cda0..38ee3d2cb 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -4,6 +4,8 @@ page_keywords: Docker, Docker documentation, CLI, command line # Command Line +{{ include "no-remote-sudo.md" }} + To list available commands, either run `docker` with no parameters or execute `docker help`: diff --git a/docs/sources/userguide/dockerizing.md b/docs/sources/userguide/dockerizing.md index 238316098..6f56a5695 100644 --- a/docs/sources/userguide/dockerizing.md +++ b/docs/sources/userguide/dockerizing.md @@ -9,6 +9,8 @@ page_keywords: docker guide, docker, docker platform, virtualization framework, Docker allows you to run applications inside containers. Running an application inside a container takes a single command: `docker run`. +{{ include "no-remote-sudo.md" }} + ## Hello world Let's try it now. From c18fdc37042dc7fb041475a524fe7abb0bd7afdc Mon Sep 17 00:00:00 2001 From: Tianon Gravi Date: Tue, 13 Jan 2015 21:09:11 -0700 Subject: [PATCH 266/513] Properly fix "daemon kill" on test failure Signed-off-by: Andrew "Tianon" Page --- project/make/test-docker-py | 25 +++++++++++++++++-------- project/make/test-integration-cli | 18 ++++++++++++++---- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/project/make/test-docker-py b/project/make/test-docker-py index 6047eec1b..104842a42 100644 --- a/project/make/test-docker-py +++ b/project/make/test-docker-py @@ -7,15 +7,24 @@ DEST=$1 ( source "$(dirname "$BASH_SOURCE")/.integration-daemon-start" - dockerPy='/docker-py' - [ -d "$dockerPy" ] || { - dockerPy="$DEST/docker-py" - git clone https://github.com/docker/docker-py.git "$dockerPy" - } + # we need to wrap up everything in between integration-daemon-start and + # integration-daemon-stop to make sure we kill the daemon and don't hang, + # even and especially on test failures + didFail= + if ! { + dockerPy='/docker-py' + [ -d "$dockerPy" ] || { + dockerPy="$DEST/docker-py" + git clone https://github.com/docker/docker-py.git "$dockerPy" + } - cd "$dockerPy" - export PYTHONPATH=. # import "docker" from "." - python tests/integration_test.py + export PYTHONPATH="$dockerPy" # import "docker" from our local docker-py + python "$dockerPy/tests/integration_test.py" + }; then + didFail=1 + fi source "$(dirname "$BASH_SOURCE")/.integration-daemon-stop" + + [ -z "$didFail" ] # "set -e" ftw ) 2>&1 | tee -a $DEST/test.log diff --git a/project/make/test-integration-cli b/project/make/test-integration-cli index 0aaa298be..5dc7c4297 100644 --- a/project/make/test-integration-cli +++ b/project/make/test-integration-cli @@ -11,12 +11,22 @@ bundle_test_integration_cli() { ( source "$(dirname "$BASH_SOURCE")/.integration-daemon-start" - # pull the busybox image before running the tests - sleep 2 + # we need to wrap up everything in between integration-daemon-start and + # integration-daemon-stop to make sure we kill the daemon and don't hang, + # even and especially on test failures + didFail= + if ! { + # pull the busybox image before running the tests + sleep 2 - source "$(dirname "$BASH_SOURCE")/.ensure-busybox" + source "$(dirname "$BASH_SOURCE")/.ensure-busybox" - bundle_test_integration_cli + bundle_test_integration_cli + }; then + didFail=1 + fi source "$(dirname "$BASH_SOURCE")/.integration-daemon-stop" + + [ -z "$didFail" ] # "set -e" ftw ) 2>&1 | tee -a $DEST/test.log From 79d30364c95f13b9ff2ce3b4df9bb70d2ddd41f0 Mon Sep 17 00:00:00 2001 From: Alexander Morozov Date: Tue, 13 Jan 2015 15:19:12 -0800 Subject: [PATCH 267/513] Test for restarting count This test is for #10058 Signed-off-by: Alexander Morozov --- integration-cli/docker_cli_run_test.go | 20 ++++++++++++++++++++ integration-cli/utils.go | 14 +++++++++----- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/integration-cli/docker_cli_run_test.go b/integration-cli/docker_cli_run_test.go index f6985ba3c..658bbcc57 100644 --- a/integration-cli/docker_cli_run_test.go +++ b/integration-cli/docker_cli_run_test.go @@ -2935,3 +2935,23 @@ func TestRunOOMExitCode(t *testing.T) { logDone("run - exit code on oom") } + +func TestRunRestartMaxRetries(t *testing.T) { + defer deleteAllContainers() + out, err := exec.Command(dockerBinary, "run", "-d", "--restart=on-failure:3", "busybox", "false").CombinedOutput() + if err != nil { + t.Fatal(string(out), err) + } + id := strings.TrimSpace(string(out)) + if err := waitInspect(id, "{{ .State.Restarting }} {{ .State.Running }}", "false false", 5); err != nil { + t.Fatal(err) + } + count, err := inspectField(id, "RestartCount") + if err != nil { + t.Fatal(err) + } + if count != "3" { + t.Fatalf("Container was restarted %s times, expected %d", count, 3) + } + logDone("run - test max-retries for --restart") +} diff --git a/integration-cli/utils.go b/integration-cli/utils.go index fefd66f33..f67ee78ca 100644 --- a/integration-cli/utils.go +++ b/integration-cli/utils.go @@ -135,28 +135,32 @@ func waitForContainer(contID string, args ...string) error { } func waitRun(contID string) error { - after := time.After(5 * time.Second) + return waitInspect(contID, "{{.State.Running}}", "true", 5) +} + +func waitInspect(name, expr, expected string, timeout int) error { + after := time.After(time.Duration(timeout) * time.Second) for { - cmd := exec.Command(dockerBinary, "inspect", "-f", "{{.State.Running}}", contID) + cmd := exec.Command(dockerBinary, "inspect", "-f", expr, name) out, _, err := runCommandWithOutput(cmd) if err != nil { return fmt.Errorf("error executing docker inspect: %v", err) } - if strings.Contains(out, "true") { + out = strings.TrimSpace(out) + if out == expected { break } select { case <-after: - return fmt.Errorf("container did not come up in time") + return fmt.Errorf("condition \"%q == %q\" not true in time", out, expected) default: } time.Sleep(100 * time.Millisecond) } - return nil } From b5aba426d4bb903825ec4b3b4912f521daac4c2a Mon Sep 17 00:00:00 2001 From: Tianon Gravi Date: Tue, 13 Jan 2015 21:35:21 -0700 Subject: [PATCH 268/513] =?UTF-8?q?Add=20proper=20"netgo"=20compiling,=20t?= =?UTF-8?q?hanks=20to=20rsc=20=E2=99=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Andrew "Tianon" Page --- Dockerfile | 3 --- project/make.sh | 3 ++- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9ef05b561..45128609e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -97,9 +97,6 @@ RUN cd /usr/local/go/src \ ./make.bash --no-clean 2>&1; \ done -# Reinstall standard library with netgo -RUN go clean -i net && go install -tags netgo std - # We still support compiling with older Go, so need to grab older "gofmt" ENV GOFMT_VERSION 1.3.3 RUN curl -sSL https://storage.googleapis.com/golang/go${GOFMT_VERSION}.$(go env GOOS)-$(go env GOARCH).tar.gz | tar -C /go/bin -xz --strip-components=2 go/bin/gofmt diff --git a/project/make.sh b/project/make.sh index f7919515c..0540649f1 100755 --- a/project/make.sh +++ b/project/make.sh @@ -113,7 +113,8 @@ fi EXTLDFLAGS_STATIC='-static' # ORIG_BUILDFLAGS is necessary for the cross target which cannot always build # with options like -race. -ORIG_BUILDFLAGS=( -a -tags "netgo static_build $DOCKER_BUILDTAGS" ) +ORIG_BUILDFLAGS=( -a -tags "netgo static_build $DOCKER_BUILDTAGS" -installsuffix netgo ) +# see https://github.com/golang/go/issues/9369#issuecomment-69864440 for why -installsuffix is necessary here BUILDFLAGS=( $BUILDFLAGS "${ORIG_BUILDFLAGS[@]}" ) # Test timeout. : ${TIMEOUT:=30m} From 2292167b02ce0489d38e0a6beeb792bcb7534f34 Mon Sep 17 00:00:00 2001 From: Lei Jitang Date: Wed, 14 Jan 2015 15:25:58 +0800 Subject: [PATCH 269/513] Add tests for --link Signed-off-by: Lei Jitang --- integration-cli/docker_cli_run_test.go | 54 ++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/integration-cli/docker_cli_run_test.go b/integration-cli/docker_cli_run_test.go index 18495be41..fb8e8d694 100644 --- a/integration-cli/docker_cli_run_test.go +++ b/integration-cli/docker_cli_run_test.go @@ -314,6 +314,60 @@ func TestRunWithoutNetworking(t *testing.T) { logDone("run - disable networking with -n=false") } +//test --link use container name to link target +func TestRunLinksContainerWithContainerName(t *testing.T) { + cmd := exec.Command(dockerBinary, "run", "-t", "-d", "--name", "parent", "busybox") + out, _, _, err := runCommandWithStdoutStderr(cmd) + if err != nil { + t.Fatal("failed to run container: %v, output: %q", err, out) + } + cmd = exec.Command(dockerBinary, "inspect", "-f", "{{.NetworkSettings.IPAddress}}", "parent") + ip, _, _, err := runCommandWithStdoutStderr(cmd) + if err != nil { + t.Fatal("failed to inspect container: %v, output: %q", err, ip) + } + ip = strings.TrimSpace(ip) + cmd = exec.Command(dockerBinary, "run", "--link", "parent:test", "busybox", "/bin/cat", "/etc/hosts") + out, _, err = runCommandWithOutput(cmd) + if err != nil { + t.Fatal("failed to run container: %v, output: %q", err, out) + } + if !strings.Contains(out, ip+" test") { + t.Fatalf("use a container name to link target failed") + } + deleteAllContainers() + + logDone("run - use a container name to link target work") +} + +//test --link use container id to link target +func TestRunLinksContainerWithContainerId(t *testing.T) { + cmd := exec.Command(dockerBinary, "run", "-t", "-d", "busybox") + cID, _, _, err := runCommandWithStdoutStderr(cmd) + if err != nil { + t.Fatal("failed to run container: %v, output: %q", err, cID) + } + cID = strings.TrimSpace(cID) + cmd = exec.Command(dockerBinary, "inspect", "-f", "{{.NetworkSettings.IPAddress}}", cID) + ip, _, _, err := runCommandWithStdoutStderr(cmd) + if err != nil { + t.Fatal("faild to inspect container: %v, output: %q", err, ip) + } + ip = strings.TrimSpace(ip) + cmd = exec.Command(dockerBinary, "run", "--link", cID+":test", "busybox", "/bin/cat", "/etc/hosts") + out, _, err := runCommandWithOutput(cmd) + if err != nil { + t.Fatal("failed to run container: %v, output: %q", err, out) + } + if !strings.Contains(out, ip+" test") { + t.Fatalf("use a container id to link target failed") + } + + deleteAllContainers() + + logDone("run - use a container id to link target work") +} + // Regression test for #4741 func TestRunWithVolumesAsFiles(t *testing.T) { runCmd := exec.Command(dockerBinary, "run", "--name", "test-data", "--volume", "/etc/hosts:/target-file", "busybox", "true") From e7cfb1c28b2a52f34c8e48ede2e24645991d1104 Mon Sep 17 00:00:00 2001 From: Jessie Frazelle Date: Wed, 14 Jan 2015 09:50:58 -0800 Subject: [PATCH 270/513] =?UTF-8?q?Revert=20"Add=20proper=20"netgo"=20comp?= =?UTF-8?q?iling,=20thanks=20to=20rsc=20=E2=99=A5"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Docker-DCO-1.1-Signed-off-by: Jessie Frazelle (github: jfrazelle) --- Dockerfile | 3 +++ project/make.sh | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 45128609e..9ef05b561 100644 --- a/Dockerfile +++ b/Dockerfile @@ -97,6 +97,9 @@ RUN cd /usr/local/go/src \ ./make.bash --no-clean 2>&1; \ done +# Reinstall standard library with netgo +RUN go clean -i net && go install -tags netgo std + # We still support compiling with older Go, so need to grab older "gofmt" ENV GOFMT_VERSION 1.3.3 RUN curl -sSL https://storage.googleapis.com/golang/go${GOFMT_VERSION}.$(go env GOOS)-$(go env GOARCH).tar.gz | tar -C /go/bin -xz --strip-components=2 go/bin/gofmt diff --git a/project/make.sh b/project/make.sh index 0540649f1..f7919515c 100755 --- a/project/make.sh +++ b/project/make.sh @@ -113,8 +113,7 @@ fi EXTLDFLAGS_STATIC='-static' # ORIG_BUILDFLAGS is necessary for the cross target which cannot always build # with options like -race. -ORIG_BUILDFLAGS=( -a -tags "netgo static_build $DOCKER_BUILDTAGS" -installsuffix netgo ) -# see https://github.com/golang/go/issues/9369#issuecomment-69864440 for why -installsuffix is necessary here +ORIG_BUILDFLAGS=( -a -tags "netgo static_build $DOCKER_BUILDTAGS" ) BUILDFLAGS=( $BUILDFLAGS "${ORIG_BUILDFLAGS[@]}" ) # Test timeout. : ${TIMEOUT:=30m} From a92281637f5b629e110b5bd074566bb6c302bb62 Mon Sep 17 00:00:00 2001 From: Jessica Frazelle Date: Tue, 13 Jan 2015 17:30:49 -0800 Subject: [PATCH 271/513] Renaming a container with an invalid name should fail Docker-DCO-1.1-Signed-off-by: Jessica Frazelle (github: jfrazelle) --- daemon/rename.go | 22 +++++++++++++--------- integration-cli/docker_cli_rename_test.go | 20 ++++++++++++++++++++ 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/daemon/rename.go b/daemon/rename.go index 9b030aad0..1dedc7d3a 100644 --- a/daemon/rename.go +++ b/daemon/rename.go @@ -8,23 +8,27 @@ func (daemon *Daemon) ContainerRename(job *engine.Job) engine.Status { if len(job.Args) != 2 { return job.Errorf("usage: %s OLD_NAME NEW_NAME", job.Name) } - old_name := job.Args[0] - new_name := job.Args[1] + oldName := job.Args[0] + newName := job.Args[1] - container := daemon.Get(old_name) + container := daemon.Get(oldName) if container == nil { - return job.Errorf("No such container: %s", old_name) + return job.Errorf("No such container: %s", oldName) } + oldName = container.Name + container.Lock() defer container.Unlock() - if err := daemon.containerGraph.Delete(container.Name); err != nil { - return job.Errorf("Failed to delete container %q: %v", old_name, err) - } - if _, err := daemon.reserveName(container.ID, new_name); err != nil { + if _, err := daemon.reserveName(container.ID, newName); err != nil { return job.Errorf("Error when allocating new name: %s", err) } - container.Name = new_name + + container.Name = newName + + if err := daemon.containerGraph.Delete(oldName); err != nil { + return job.Errorf("Failed to delete container %q: %v", oldName, err) + } return engine.StatusOK } diff --git a/integration-cli/docker_cli_rename_test.go b/integration-cli/docker_cli_rename_test.go index 3ba98e4e3..911bab25a 100644 --- a/integration-cli/docker_cli_rename_test.go +++ b/integration-cli/docker_cli_rename_test.go @@ -97,3 +97,23 @@ func TestRenameCheckNames(t *testing.T) { logDone("rename - running container") } + +func TestRenameInvalidName(t *testing.T) { + defer deleteAllContainers() + runCmd := exec.Command(dockerBinary, "run", "--name", "myname", "-d", "busybox", "top") + if out, _, err := runCommandWithOutput(runCmd); err != nil { + t.Fatalf(out, err) + } + + runCmd = exec.Command(dockerBinary, "rename", "myname", "new:invalid") + if out, _, err := runCommandWithOutput(runCmd); err == nil || !strings.Contains(out, "Invalid container name") { + t.Fatalf("Renaming container to invalid name should have failed: %s\n%v", out, err) + } + + runCmd = exec.Command(dockerBinary, "ps", "-a") + if out, _, err := runCommandWithOutput(runCmd); err != nil || !strings.Contains(out, "myname") { + t.Fatalf("Output of docker ps should have included 'myname': %s\n%v", out, err) + } + + logDone("rename - invalid container name") +} From 5ce60217f1ba07015af72978e715a08259e2efc1 Mon Sep 17 00:00:00 2001 From: Alexander Morozov Date: Wed, 14 Jan 2015 14:01:36 -0800 Subject: [PATCH 272/513] Calming vet about type aliases from other package Signed-off-by: Alexander Morozov --- daemon/execdriver/native/template/default_template.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/daemon/execdriver/native/template/default_template.go b/daemon/execdriver/native/template/default_template.go index 6aa213d6b..f7d6be746 100644 --- a/daemon/execdriver/native/template/default_template.go +++ b/daemon/execdriver/native/template/default_template.go @@ -25,13 +25,13 @@ func New() *libcontainer.Config { "KILL", "AUDIT_WRITE", }, - Namespaces: libcontainer.Namespaces{ + Namespaces: libcontainer.Namespaces([]libcontainer.Namespace{ {Type: "NEWNS"}, {Type: "NEWUTS"}, {Type: "NEWIPC"}, {Type: "NEWPID"}, {Type: "NEWNET"}, - }, + }), Cgroups: &cgroups.Cgroup{ Parent: "docker", AllowAllDevices: false, From bb96e53b0f947a31a4b66e76607eed0097917ed5 Mon Sep 17 00:00:00 2001 From: Alexander Morozov Date: Wed, 14 Jan 2015 14:03:00 -0800 Subject: [PATCH 273/513] Fix vet error about passing Mutex by value Signed-off-by: Alexander Morozov --- daemon/graphdriver/devmapper/deviceset.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/daemon/graphdriver/devmapper/deviceset.go b/daemon/graphdriver/devmapper/deviceset.go index 37c3fa84a..1e0a6d3f8 100644 --- a/daemon/graphdriver/devmapper/deviceset.go +++ b/daemon/graphdriver/devmapper/deviceset.go @@ -1550,13 +1550,13 @@ func (devices *DeviceSet) poolStatus() (totalSizeInSectors, transactionId, dataU // MetadataDevicePath returns the path to the metadata storage for this deviceset, // regardless of loopback or block device -func (devices DeviceSet) DataDevicePath() string { +func (devices *DeviceSet) DataDevicePath() string { return devices.dataDevice } // MetadataDevicePath returns the path to the metadata storage for this deviceset, // regardless of loopback or block device -func (devices DeviceSet) MetadataDevicePath() string { +func (devices *DeviceSet) MetadataDevicePath() string { return devices.metadataDevice } From a75b02fe72f3da73f9788919ff2c22f183978db7 Mon Sep 17 00:00:00 2001 From: Alexander Morozov Date: Wed, 14 Jan 2015 14:12:03 -0800 Subject: [PATCH 274/513] Fix format calls as suggested by vet Signed-off-by: Alexander Morozov --- integration-cli/docker_cli_rm_test.go | 2 +- integration-cli/docker_cli_run_test.go | 12 ++++++------ integration-cli/docker_utils.go | 2 +- nat/nat_test.go | 2 +- registry/auth_test.go | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/integration-cli/docker_cli_rm_test.go b/integration-cli/docker_cli_rm_test.go index c3069a3ee..89ede7abc 100644 --- a/integration-cli/docker_cli_rm_test.go +++ b/integration-cli/docker_cli_rm_test.go @@ -67,7 +67,7 @@ func TestRmRunningContainerCheckError409(t *testing.T) { t.Fatalf("Expected error, can't rm a running container") } if !strings.Contains(err.Error(), "409 Conflict") { - t.Fatalf("Expected error to contain '409 Conflict' but found", err) + t.Fatalf("Expected error to contain '409 Conflict' but found %s", err) } deleteAllContainers() diff --git a/integration-cli/docker_cli_run_test.go b/integration-cli/docker_cli_run_test.go index 5108d197d..8fc97b684 100644 --- a/integration-cli/docker_cli_run_test.go +++ b/integration-cli/docker_cli_run_test.go @@ -319,18 +319,18 @@ func TestRunLinksContainerWithContainerName(t *testing.T) { cmd := exec.Command(dockerBinary, "run", "-t", "-d", "--name", "parent", "busybox") out, _, _, err := runCommandWithStdoutStderr(cmd) if err != nil { - t.Fatal("failed to run container: %v, output: %q", err, out) + t.Fatalf("failed to run container: %v, output: %q", err, out) } cmd = exec.Command(dockerBinary, "inspect", "-f", "{{.NetworkSettings.IPAddress}}", "parent") ip, _, _, err := runCommandWithStdoutStderr(cmd) if err != nil { - t.Fatal("failed to inspect container: %v, output: %q", err, ip) + t.Fatalf("failed to inspect container: %v, output: %q", err, ip) } ip = strings.TrimSpace(ip) cmd = exec.Command(dockerBinary, "run", "--link", "parent:test", "busybox", "/bin/cat", "/etc/hosts") out, _, err = runCommandWithOutput(cmd) if err != nil { - t.Fatal("failed to run container: %v, output: %q", err, out) + t.Fatalf("failed to run container: %v, output: %q", err, out) } if !strings.Contains(out, ip+" test") { t.Fatalf("use a container name to link target failed") @@ -345,19 +345,19 @@ func TestRunLinksContainerWithContainerId(t *testing.T) { cmd := exec.Command(dockerBinary, "run", "-t", "-d", "busybox") cID, _, _, err := runCommandWithStdoutStderr(cmd) if err != nil { - t.Fatal("failed to run container: %v, output: %q", err, cID) + t.Fatalf("failed to run container: %v, output: %q", err, cID) } cID = strings.TrimSpace(cID) cmd = exec.Command(dockerBinary, "inspect", "-f", "{{.NetworkSettings.IPAddress}}", cID) ip, _, _, err := runCommandWithStdoutStderr(cmd) if err != nil { - t.Fatal("faild to inspect container: %v, output: %q", err, ip) + t.Fatalf("faild to inspect container: %v, output: %q", err, ip) } ip = strings.TrimSpace(ip) cmd = exec.Command(dockerBinary, "run", "--link", cID+":test", "busybox", "/bin/cat", "/etc/hosts") out, _, err := runCommandWithOutput(cmd) if err != nil { - t.Fatal("failed to run container: %v, output: %q", err, out) + t.Fatalf("failed to run container: %v, output: %q", err, out) } if !strings.Contains(out, ip+" test") { t.Fatalf("use a container id to link target failed") diff --git a/integration-cli/docker_utils.go b/integration-cli/docker_utils.go index 03d34b6d7..219e87a5d 100644 --- a/integration-cli/docker_utils.go +++ b/integration-cli/docker_utils.go @@ -226,7 +226,7 @@ out2: case <-tick: i++ if i > 4 { - d.t.Log("tried to interrupt daemon for %d times, now try to kill it", i) + d.t.Logf("tried to interrupt daemon for %d times, now try to kill it", i) break out2 } d.t.Logf("Attempt #%d: daemon is still running with pid %d", i, d.cmd.Process.Pid) diff --git a/nat/nat_test.go b/nat/nat_test.go index 34c210e6e..376857fd7 100644 --- a/nat/nat_test.go +++ b/nat/nat_test.go @@ -281,7 +281,7 @@ func TestParsePortSpecsWithRange(t *testing.T) { for portspec, bindings := range bindingMap { _, port := SplitProtoPort(string(portspec)) if len(bindings) != 1 || bindings[0].HostIp != "0.0.0.0" || bindings[0].HostPort != port { - t.Fatalf("Expect single binding to port %d but found %s", port, bindings) + t.Fatalf("Expect single binding to port %s but found %s", port, bindings) } } diff --git a/registry/auth_test.go b/registry/auth_test.go index 22f879946..9cc299aab 100644 --- a/registry/auth_test.go +++ b/registry/auth_test.go @@ -151,7 +151,7 @@ func TestResolveAuthConfigFullURL(t *testing.T) { for configKey, registries := range validRegistries { configured, ok := expectedAuths[configKey] if !ok || configured.Email == "" { - t.Fatal() + t.Fail() } index := &IndexInfo{ Name: configKey, From 798215af24953f5e3dcacb08965f3714b2fd903f Mon Sep 17 00:00:00 2001 From: Jessica Frazelle Date: Wed, 14 Jan 2015 14:40:07 -0800 Subject: [PATCH 275/513] Add build constraint. Docker-DCO-1.1-Signed-off-by: Jessica Frazelle (github: jfrazelle) --- integration-cli/docker_cli_attach_unix_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/integration-cli/docker_cli_attach_unix_test.go b/integration-cli/docker_cli_attach_unix_test.go index 451147d8a..3fb0ea896 100644 --- a/integration-cli/docker_cli_attach_unix_test.go +++ b/integration-cli/docker_cli_attach_unix_test.go @@ -1,3 +1,5 @@ +// +build !windows + package main import ( From 409407091a7282d0c4086b71e86397e2d089ba13 Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Tue, 13 Jan 2015 13:52:51 -0800 Subject: [PATCH 276/513] Add --readonly for read only container rootfs Add a --readonly flag to allow the container's root filesystem to be mounted as readonly. This can be used in combination with volumes to force a container's process to only write to locations that will be persisted. This is useful in many cases where the admin controls where they would like developers to write files and error on any other locations. Closes #7923 Closes #8752 Signed-off-by: Michael Crosby --- daemon/container.go | 1 + daemon/execdriver/driver.go | 3 ++- daemon/execdriver/native/create.go | 1 + docs/man/docker-create.1.md | 4 ++++ docs/man/docker-run.1.md | 8 +++++++ .../reference/api/docker_remote_api.md | 7 ++++++ .../reference/api/docker_remote_api_v1.17.md | 4 ++++ docs/sources/reference/commandline/cli.md | 9 ++++++++ integration-cli/docker_cli_run_test.go | 22 +++++++++++++++++++ runconfig/hostconfig.go | 2 ++ runconfig/parse.go | 2 ++ 11 files changed, 62 insertions(+), 1 deletion(-) diff --git a/daemon/container.go b/daemon/container.go index 86c0a7d84..becc69fce 100644 --- a/daemon/container.go +++ b/daemon/container.go @@ -294,6 +294,7 @@ func populateCommand(c *Container, env []string) error { c.command = &execdriver.Command{ ID: c.ID, Rootfs: c.RootfsPath(), + ReadonlyRootfs: c.hostConfig.ReadonlyRootfs, InitPath: "/.dockerinit", WorkingDir: c.Config.WorkingDir, Network: en, diff --git a/daemon/execdriver/driver.go b/daemon/execdriver/driver.go index 80aad44ff..fe99e062d 100644 --- a/daemon/execdriver/driver.go +++ b/daemon/execdriver/driver.go @@ -125,7 +125,8 @@ type ProcessConfig struct { // Process wrapps an os/exec.Cmd to add more metadata type Command struct { ID string `json:"id"` - Rootfs string `json:"rootfs"` // root fs of the container + Rootfs string `json:"rootfs"` // root fs of the container + ReadonlyRootfs bool `json:"readonly_rootfs"` InitPath string `json:"initpath"` // dockerinit WorkingDir string `json:"working_dir"` ConfigPath string `json:"config_path"` // this should be able to be removed when the lxc template is moved into the driver diff --git a/daemon/execdriver/native/create.go b/daemon/execdriver/native/create.go index 7b764a50e..c5a8da75b 100644 --- a/daemon/execdriver/native/create.go +++ b/daemon/execdriver/native/create.go @@ -31,6 +31,7 @@ func (d *driver) createContainer(c *execdriver.Command) (*libcontainer.Config, e container.Cgroups.AllowedDevices = c.AllowedDevices container.MountConfig.DeviceNodes = c.AutoCreatedDevices container.RootFs = c.Rootfs + container.MountConfig.ReadonlyFs = c.ReadonlyRootfs // check to see if we are running in ramdisk to disable pivot root container.MountConfig.NoPivotRoot = os.Getenv("DOCKER_RAMDISK") != "" diff --git a/docs/man/docker-create.1.md b/docs/man/docker-create.1.md index 63fe20ed1..24185489f 100644 --- a/docs/man/docker-create.1.md +++ b/docs/man/docker-create.1.md @@ -34,6 +34,7 @@ docker-create - Create a new container [**-p**|**--publish**[=*[]*]] [**--pid**[=*[]*]] [**--privileged**[=*false*]] +[**--read-only**[=*false*]] [**--restart**[=*RESTART*]] [**--security-opt**[=*[]*]] [**-t**|**--tty**[=*false*]] @@ -140,6 +141,9 @@ IMAGE [COMMAND] [ARG...] **--privileged**=*true*|*false* Give extended privileges to this container. The default is *false*. +**--read-only**=*true*|*false* + Mount the container's root filesystem as read only. + **--restart**="" Restart policy to apply when a container exits (no, on-failure[:max-retry], always) diff --git a/docs/man/docker-run.1.md b/docs/man/docker-run.1.md index de035e965..b16447bc5 100644 --- a/docs/man/docker-run.1.md +++ b/docs/man/docker-run.1.md @@ -35,6 +35,7 @@ docker-run - Run a command in a new container [**-p**|**--publish**[=*[]*]] [**--pid**[=*[]*]] [**--privileged**[=*false*]] +[**--read-only**[=*false*]] [**--restart**[=*RESTART*]] [**--rm**[=*false*]] [**--security-opt**[=*[]*]] @@ -253,6 +254,13 @@ to all devices on the host as well as set some configuration in AppArmor to allow the container nearly all the same access to the host as processes running outside of a container on the host. +**--read-only**=*true*|*false* + Mount the container's root filesystem as read only. + + By default a container will have its root filesystem writable allowing processes +to write files anywhere. By specifying the `--read-only` flag the container will have +its root filesystem mounted as read only prohibiting any writes. + **--restart**="" Restart policy to apply when a container exits (no, on-failure[:max-retry], always) diff --git a/docs/sources/reference/api/docker_remote_api.md b/docs/sources/reference/api/docker_remote_api.md index 08439ce0d..a36133795 100644 --- a/docs/sources/reference/api/docker_remote_api.md +++ b/docs/sources/reference/api/docker_remote_api.md @@ -61,6 +61,13 @@ This endpoint now returns the list current execs associated with the container ( **New!** New endpoint to rename a container `id` to a new name. +`POST /containers/create` +`POST /containers/(id)/start` + +**New!** +(`ReadonlyRootfs`) can be passed in the host config to mount the container's +root filesystem as read only. + ## v1.16 ### Full Documentation diff --git a/docs/sources/reference/api/docker_remote_api_v1.17.md b/docs/sources/reference/api/docker_remote_api_v1.17.md index 6c6087396..a44dcbf3a 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.17.md +++ b/docs/sources/reference/api/docker_remote_api_v1.17.md @@ -146,6 +146,7 @@ Create a container "PortBindings": { "22/tcp": [{ "HostPort": "11022" }] }, "PublishAllPorts": false, "Privileged": false, + "ReadonlyRootfs": false, "Dns": ["8.8.8.8"], "DnsSearch": [""], "VolumesFrom": ["parent", "other:ro"], @@ -218,6 +219,8 @@ Json Parameters: exposed ports. Specified as a boolean value. - **Privileged** - Gives the container full access to the host. Specified as a boolean value. + - **ReadonlyRootfs** - Mount the container's root filesystem as read only. + Specified as a boolean value. - **Dns** - A list of dns servers for the container to use. - **DnsSearch** - A list of DNS search domains - **VolumesFrom** - A list of volumes to inherit from another container. @@ -323,6 +326,7 @@ Return low-level information on the container `id` "NetworkMode": "bridge", "PortBindings": {}, "Privileged": false, + "ReadonlyRootfs": false, "PublishAllPorts": false, "RestartPolicy": { "MaximumRetryCount": 2, diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index 0b7b0cda0..052da5807 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -753,6 +753,7 @@ Creates a new container. When specifying ranges for both, the number of container ports in the range must match the number of host ports in the range. (e.g., `-p 1234-1236:1234-1236/tcp`) (use 'docker port' to see the actual mapping) --privileged=false Give extended privileges to this container + --read-only=false Mount the container's root filesystem as read only --restart="" Restart policy to apply when a container exits (no, on-failure[:max-retry], always) --security-opt=[] Security Options -t, --tty=false Allocate a pseudo-TTY @@ -1606,6 +1607,7 @@ removed before the image is removed. (use 'docker port' to see the actual mapping) --pid=host 'host': use the host PID namespace inside the container. Note: the host mode gives the container full access to local system services such as D-bus and is therefore considered insecure. --privileged=false Give extended privileges to this container + --read-only=false Mount the container's root filesystem as read only --restart="" Restart policy to apply when a container exits (no, on-failure[:max-retry], always) --rm=false Automatically remove the container when it exits (incompatible with -d) --security-opt=[] Security Options @@ -1681,6 +1683,13 @@ will automatically create this directory on the host for you. In the example above, Docker will create the `/doesnt/exist` folder before starting your container. + $ sudo docker run --read-only -v /icanwrite busybox touch /icanwrite here + +Volumes can be used in combination with `--read-only` to control where +a container writes files. The `--read only` flag mounts the container's root +filesystem as read only prohibiting writes to locations other than the +specified volumes for the container. + $ sudo docker run -t -i -v /var/run/docker.sock:/var/run/docker.sock -v ./static-docker:/usr/bin/docker busybox sh By bind-mounting the docker unix socket and statically linked docker diff --git a/integration-cli/docker_cli_run_test.go b/integration-cli/docker_cli_run_test.go index b1d63338d..dc4bcdf83 100644 --- a/integration-cli/docker_cli_run_test.go +++ b/integration-cli/docker_cli_run_test.go @@ -2987,3 +2987,25 @@ func TestRunRestartMaxRetries(t *testing.T) { } logDone("run - test max-retries for --restart") } + +func TestRunContainerWithWritableRootfs(t *testing.T) { + defer deleteAllContainers() + out, err := exec.Command(dockerBinary, "run", "--rm", "busybox", "touch", "/file").CombinedOutput() + if err != nil { + t.Fatal(string(out), err) + } + logDone("run - writable rootfs") +} + +func TestRunContainerWithReadonlyRootfs(t *testing.T) { + defer deleteAllContainers() + out, err := exec.Command(dockerBinary, "run", "--read-only", "--rm", "busybox", "touch", "/file").CombinedOutput() + if err == nil { + t.Fatal("expected container to error on run with read only error") + } + expected := "Read-only file system" + if !strings.Contains(string(out), expected) { + t.Fatalf("expected output from failure to contain %s but contains %s", expected, out) + } + logDone("run - read only rootfs") +} diff --git a/runconfig/hostconfig.go b/runconfig/hostconfig.go index 054c68362..3aff582f9 100644 --- a/runconfig/hostconfig.go +++ b/runconfig/hostconfig.go @@ -118,6 +118,7 @@ type HostConfig struct { CapDrop []string RestartPolicy RestartPolicy SecurityOpt []string + ReadonlyRootfs bool } // This is used by the create command when you want to set both the @@ -148,6 +149,7 @@ func ContainerHostConfigFromJob(job *engine.Job) *HostConfig { NetworkMode: NetworkMode(job.Getenv("NetworkMode")), IpcMode: IpcMode(job.Getenv("IpcMode")), PidMode: PidMode(job.Getenv("PidMode")), + ReadonlyRootfs: job.GetenvBool("ReadonlyRootfs"), } job.GetenvJson("LxcConf", &hostConfig.LxcConf) diff --git a/runconfig/parse.go b/runconfig/parse.go index 781f721f6..2fcd55935 100644 --- a/runconfig/parse.go +++ b/runconfig/parse.go @@ -63,6 +63,7 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe flMacAddress = cmd.String([]string{"-mac-address"}, "", "Container MAC address (e.g. 92:d0:c6:0a:29:33)") flIpcMode = cmd.String([]string{"-ipc"}, "", "Default is to create a private IPC namespace (POSIX SysV IPC) for the container\n'container:': reuses another container shared memory, semaphores and message queues\n'host': use the host shared memory,semaphores and message queues inside the container. Note: the host mode gives the container full access to local shared memory and is therefore considered insecure.") flRestartPolicy = cmd.String([]string{"-restart"}, "", "Restart policy to apply when a container exits (no, on-failure[:max-retry], always)") + flReadonlyRootfs = cmd.Bool([]string{"-read-only"}, false, "Mount the container's root filesystem as read only") ) cmd.Var(&flAttach, []string{"a", "-attach"}, "Attach to STDIN, STDOUT or STDERR.") @@ -312,6 +313,7 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe CapDrop: flCapDrop.GetAll(), RestartPolicy: restartPolicy, SecurityOpt: flSecurityOpt.GetAll(), + ReadonlyRootfs: *flReadonlyRootfs, } // When allocating stdin in attached mode, close stdin at client disconnect From e1ef33449f484678d236cb49e80daf5ba1e1899c Mon Sep 17 00:00:00 2001 From: Arnaud Porterie Date: Wed, 14 Jan 2015 16:08:28 -0800 Subject: [PATCH 277/513] Take DOCKER_TEST_HOST into account Tests no longer make the assumption that the daemon can be accessed through unix:///var/run/docker.sock. Signed-off-by: Arnaud Porterie --- integration-cli/docker_utils.go | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/integration-cli/docker_utils.go b/integration-cli/docker_utils.go index 219e87a5d..c58bcfbf7 100644 --- a/integration-cli/docker_utils.go +++ b/integration-cli/docker_utils.go @@ -11,6 +11,7 @@ import ( "net/http" "net/http/httptest" "net/http/httputil" + "net/url" "os" "os/exec" "path" @@ -264,12 +265,32 @@ func (d *Daemon) Cmd(name string, arg ...string) (string, error) { return string(b), err } +func daemonHost() string { + daemonUrlStr := "unix:///var/run/docker.sock" + if daemonHostVar := os.Getenv("DOCKER_TEST_HOST"); daemonHostVar != "" { + daemonUrlStr = daemonHostVar + } + return daemonUrlStr +} + func sockRequest(method, endpoint string, data interface{}) ([]byte, error) { - // FIX: the path to sock should not be hardcoded - sock := filepath.Join("/", "var", "run", "docker.sock") - c, err := net.DialTimeout("unix", sock, time.Duration(10*time.Second)) + daemon := daemonHost() + daemonUrl, err := url.Parse(daemon) if err != nil { - return nil, fmt.Errorf("could not dial docker sock at %s: %v", sock, err) + return nil, fmt.Errorf("could not parse url %q: %v", daemon, err) + } + + var c net.Conn + switch daemonUrl.Scheme { + case "unix": + c, err = net.DialTimeout(daemonUrl.Scheme, daemonUrl.Path, time.Duration(10*time.Second)) + case "tcp": + c, err = net.DialTimeout(daemonUrl.Scheme, daemonUrl.Host, time.Duration(10*time.Second)) + default: + err = fmt.Errorf("unknown scheme %v", daemonUrl.Scheme) + } + if err != nil { + return nil, fmt.Errorf("could not dial docker daemon at %s: %v", daemon, err) } client := httputil.NewClientConn(c, nil) From 02246d2d9f9fe8eb040d1eb1ac4b7a84c3f3f059 Mon Sep 17 00:00:00 2001 From: Jessica Frazelle Date: Wed, 14 Jan 2015 16:44:53 -0800 Subject: [PATCH 278/513] Error should show when trying to start a paused container. Docker-DCO-1.1-Signed-off-by: Jessica Frazelle (github: jfrazelle) --- daemon/start.go | 4 ++++ integration-cli/docker_cli_start_test.go | 22 ++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/daemon/start.go b/daemon/start.go index 286ee58a3..363461080 100644 --- a/daemon/start.go +++ b/daemon/start.go @@ -22,6 +22,10 @@ func (daemon *Daemon) ContainerStart(job *engine.Job) engine.Status { return job.Errorf("No such container: %s", name) } + if container.IsPaused() { + return job.Errorf("Cannot start a paused container, try unpause instead.") + } + if container.IsRunning() { return job.Errorf("Container already started") } diff --git a/integration-cli/docker_cli_start_test.go b/integration-cli/docker_cli_start_test.go index 05a262ba5..3b0617f09 100644 --- a/integration-cli/docker_cli_start_test.go +++ b/integration-cli/docker_cli_start_test.go @@ -165,3 +165,25 @@ func TestStartVolumesFromFailsCleanly(t *testing.T) { logDone("start - missing containers in --volumes-from did not affect subsequent runs") } + +func TestStartPausedContainer(t *testing.T) { + defer deleteAllContainers() + defer unpauseAllContainers() + + runCmd := exec.Command(dockerBinary, "run", "-d", "--name", "testing", "busybox", "top") + if out, _, err := runCommandWithOutput(runCmd); err != nil { + t.Fatal(out, err) + } + + runCmd = exec.Command(dockerBinary, "pause", "testing") + if out, _, err := runCommandWithOutput(runCmd); err != nil { + t.Fatal(out, err) + } + + runCmd = exec.Command(dockerBinary, "start", "testing") + if out, _, err := runCommandWithOutput(runCmd); err == nil || !strings.Contains(out, "Cannot start a paused container, try unpause instead.") { + t.Fatalf("an error should have been shown that you cannot start paused container: %s\n%v", out, err) + } + + logDone("start - error should show if trying to start paused container") +} From 39343b86182b4e997dc991645729ae130bd0f5f2 Mon Sep 17 00:00:00 2001 From: Erik Hollensbe Date: Thu, 8 Jan 2015 17:00:00 -0800 Subject: [PATCH 279/513] Fix a panic where RUN [] would be supplied. Docker-DCO-1.1-Signed-off-by: Erik Hollensbe (github: erikh) --- builder/internals.go | 10 +++++++--- integration-cli/docker_cli_build_test.go | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/builder/internals.go b/builder/internals.go index 2aa747f19..692f8f209 100644 --- a/builder/internals.go +++ b/builder/internals.go @@ -532,9 +532,13 @@ func (b *Builder) create() (*daemon.Container, error) { b.TmpContainers[c.ID] = struct{}{} fmt.Fprintf(b.OutStream, " ---> Running in %s\n", utils.TruncateID(c.ID)) - // override the entry point that may have been picked up from the base image - c.Path = config.Cmd[0] - c.Args = config.Cmd[1:] + if config.Cmd != nil { + // override the entry point that may have been picked up from the base image + c.Path = config.Cmd[0] + c.Args = config.Cmd[1:] + } else { + config.Cmd = []string{} + } return c, nil } diff --git a/integration-cli/docker_cli_build_test.go b/integration-cli/docker_cli_build_test.go index 927ab0732..ffc7594d0 100644 --- a/integration-cli/docker_cli_build_test.go +++ b/integration-cli/docker_cli_build_test.go @@ -22,6 +22,25 @@ import ( "github.com/docker/docker/pkg/archive" ) +func TestBuildJSONEmptyRun(t *testing.T) { + name := "testbuildjsonemptyrun" + defer deleteImages(name) + + _, err := buildImage( + name, + ` + FROM busybox + RUN [] + `, + true) + + if err != nil { + t.Fatal("error when dealing with a RUN statement with empty JSON array") + } + + logDone("build - RUN with an empty array should not panic") +} + func TestBuildEmptyWhitespace(t *testing.T) { name := "testbuildemptywhitespace" defer deleteImages(name) From 3183af8b19d2adf29dd3fb80689d764353a3fe00 Mon Sep 17 00:00:00 2001 From: Tibor Vass Date: Tue, 13 Jan 2015 11:13:27 -0800 Subject: [PATCH 280/513] builder: use len() > 0 instead of != nil Signed-off-by: Tibor Vass --- builder/internals.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builder/internals.go b/builder/internals.go index 692f8f209..830da7272 100644 --- a/builder/internals.go +++ b/builder/internals.go @@ -532,7 +532,7 @@ func (b *Builder) create() (*daemon.Container, error) { b.TmpContainers[c.ID] = struct{}{} fmt.Fprintf(b.OutStream, " ---> Running in %s\n", utils.TruncateID(c.ID)) - if config.Cmd != nil { + if len(config.Cmd) > 0 { // override the entry point that may have been picked up from the base image c.Path = config.Cmd[0] c.Args = config.Cmd[1:] From 034466d356ecc5bc993f255c5ec5fb96e7327b8b Mon Sep 17 00:00:00 2001 From: Ankush Agarwal Date: Wed, 14 Jan 2015 17:16:44 -0800 Subject: [PATCH 281/513] Typo image -> container Signed-off-by: Ankush Agarwal --- image/spec/v1.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/image/spec/v1.md b/image/spec/v1.md index 2d7c4606f..eea7e050d 100644 --- a/image/spec/v1.md +++ b/image/spec/v1.md @@ -243,7 +243,7 @@ Here is an example image JSON file: The execution parameters which should be used as a base when running a container using the image. This field can be null, in which case any execution parameters should be specified at creation of - the image. + the container.

Container RunConfig Field Descriptions

From f88066fd43454be005ec303977ee45561f3436e6 Mon Sep 17 00:00:00 2001 From: Jessica Frazelle Date: Wed, 14 Jan 2015 17:28:50 -0800 Subject: [PATCH 282/513] ignore vfs from warning Docker-DCO-1.1-Signed-off-by: Jessica Frazelle (github: jfrazelle) --- daemon/graphdriver/driver.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/graphdriver/driver.go b/daemon/graphdriver/driver.go index c576a1f58..43abd5904 100644 --- a/daemon/graphdriver/driver.go +++ b/daemon/graphdriver/driver.go @@ -148,7 +148,7 @@ func New(root string, options []string) (driver Driver, err error) { func checkPriorDriver(name, root string) { priorDrivers := []string{} for prior := range drivers { - if prior != name { + if prior != name && prior != "vfs" { if _, err := os.Stat(path.Join(root, prior)); err == nil { priorDrivers = append(priorDrivers, prior) } From 92ef40a8260e5e8dcd5579a6f9d942c4e68702ed Mon Sep 17 00:00:00 2001 From: Nathan Hsieh Date: Wed, 14 Jan 2015 18:03:09 -0800 Subject: [PATCH 283/513] added note to man & cli docs on search limit Signed-off-by: Nathan Hsieh --- docs/man/docker-search.1.md | 2 ++ docs/sources/reference/commandline/cli.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/docs/man/docker-search.1.md b/docs/man/docker-search.1.md index 3937b870a..8f3d81e08 100644 --- a/docs/man/docker-search.1.md +++ b/docs/man/docker-search.1.md @@ -18,6 +18,8 @@ of images returned displays the name, description (truncated by default), number of stars awarded, whether the image is official, and whether it is automated. +*Note* - Search queries will only return up to 25 results + # OPTIONS **--automated**=*true*|*false* Only show automated builds. The default is *false*. diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index aab3d6af4..e6f75e0ca 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -1868,6 +1868,8 @@ See [*Find Public Images on Docker Hub*]( /userguide/dockerrepos/#searching-for-images) for more details on finding shared images from the command line. +*Note* - Search queries will only return up to 25 results + ## start Usage: docker start [OPTIONS] CONTAINER [CONTAINER...] From 18a2c77435cdad8f759421500cba6ac20057daf8 Mon Sep 17 00:00:00 2001 From: Sven Dowideit Date: Tue, 6 Jan 2015 23:23:32 -0500 Subject: [PATCH 284/513] Add build --pull and evenets --filter flags to the docs for 1.4 Signed-off-by: Sven Dowideit --- api/client/commands.go | 4 ++-- contrib/completion/fish/docker.fish | 2 +- docs/man/docker-build.1.md | 4 ++++ docs/man/docker-events.1.md | 4 ++++ docs/man/docker-images.1.md | 2 +- docs/man/docker-logs.1.md | 2 +- docs/sources/articles/https.md | 2 +- docs/sources/articles/runmetrics.md | 8 ++++---- docs/sources/articles/security.md | 2 +- docs/sources/reference/commandline/cli.md | 5 +++-- 10 files changed, 22 insertions(+), 13 deletions(-) diff --git a/api/client/commands.go b/api/client/commands.go index 06c369958..9b67001c1 100644 --- a/api/client/commands.go +++ b/api/client/commands.go @@ -1315,7 +1315,7 @@ func (cli *DockerCli) CmdImages(args ...string) error { flTree := cmd.Bool([]string{"#t", "#tree", "#-tree"}, false, "Output graph in tree format") flFilter := opts.NewListOpts(nil) - cmd.Var(&flFilter, []string{"f", "-filter"}, "Provide filter values (i.e. 'dangling=true')") + cmd.Var(&flFilter, []string{"f", "-filter"}, "Provide filter values (i.e., 'dangling=true')") cmd.Require(flag.Max, 1) utils.ParseFlags(cmd, args, true) @@ -1754,7 +1754,7 @@ func (cli *DockerCli) CmdEvents(args ...string) error { since := cmd.String([]string{"#since", "-since"}, "", "Show all events created since timestamp") until := cmd.String([]string{"-until"}, "", "Stream events until this timestamp") flFilter := opts.NewListOpts(nil) - cmd.Var(&flFilter, []string{"f", "-filter"}, "Provide filter values (i.e. 'event=stop')") + cmd.Var(&flFilter, []string{"f", "-filter"}, "Provide filter values (i.e., 'event=stop')") cmd.Require(flag.Exact, 0) utils.ParseFlags(cmd, args, true) diff --git a/contrib/completion/fish/docker.fish b/contrib/completion/fish/docker.fish index c0a5725a1..41c4a3300 100644 --- a/contrib/completion/fish/docker.fish +++ b/contrib/completion/fish/docker.fish @@ -163,7 +163,7 @@ complete -c docker -A -f -n '__fish_seen_subcommand_from history' -a '(__fish_pr # images complete -c docker -f -n '__fish_docker_no_subcommand' -a images -d 'List images' complete -c docker -A -f -n '__fish_seen_subcommand_from images' -s a -l all -d 'Show all images (by default filter out the intermediate image layers)' -complete -c docker -A -f -n '__fish_seen_subcommand_from images' -s f -l filter -d "Provide filter values (i.e. 'dangling=true')" +complete -c docker -A -f -n '__fish_seen_subcommand_from images' -s f -l filter -d "Provide filter values (i.e., 'dangling=true')" complete -c docker -A -f -n '__fish_seen_subcommand_from images' -l no-trunc -d "Don't truncate output" complete -c docker -A -f -n '__fish_seen_subcommand_from images' -s q -l quiet -d 'Only show numeric IDs' complete -c docker -A -f -n '__fish_seen_subcommand_from images' -a '(__fish_print_docker_repositories)' -d "Repository" diff --git a/docs/man/docker-build.1.md b/docs/man/docker-build.1.md index 56e0807df..98bf3771a 100644 --- a/docs/man/docker-build.1.md +++ b/docs/man/docker-build.1.md @@ -10,6 +10,7 @@ docker-build - Build a new image from the source code at PATH [**-f**|**--file**[=*Dockerfile*]] [**--force-rm**[=*false*]] [**--no-cache**[=*false*]] +[**--pull**[=*false*]] [**-q**|**--quiet**[=*false*]] [**--rm**[=*true*]] [**-t**|**--tag**[=*TAG*]] @@ -44,6 +45,9 @@ as context. **--help** Print usage statement +**--pull**=*true*|*false* + Always attempt to pull a newer version of the image. The default is *false*. + **-q**, **--quiet**=*true*|*false* Suppress the verbose output generated by the containers. The default is *false*. diff --git a/docs/man/docker-events.1.md b/docs/man/docker-events.1.md index 5b056ecbc..d869498c9 100644 --- a/docs/man/docker-events.1.md +++ b/docs/man/docker-events.1.md @@ -7,6 +7,7 @@ docker-events - Get real time events from the server # SYNOPSIS **docker events** [**--help**] +[**-f**|**--filter**[=*[]*]] [**--since**[=*SINCE*]] [**--until**[=*UNTIL*]] @@ -27,6 +28,9 @@ and Docker images will report: **--help** Print usage statement +**-f**, **--filter**=[] + Provide filter values (i.e., 'event=stop') + **--since**="" Show all events created since timestamp diff --git a/docs/man/docker-images.1.md b/docs/man/docker-images.1.md index b0e8daddd..16fad991c 100644 --- a/docs/man/docker-images.1.md +++ b/docs/man/docker-images.1.md @@ -34,7 +34,7 @@ versions. Show all images (by default filter out the intermediate image layers). The default is *false*. **-f**, **--filter**=[] - Provide filter values (i.e. 'dangling=true') + Provide filter values (i.e., 'dangling=true') **--help** Print usage statement diff --git a/docs/man/docker-logs.1.md b/docs/man/docker-logs.1.md index c89652672..d55e8d836 100644 --- a/docs/man/docker-logs.1.md +++ b/docs/man/docker-logs.1.md @@ -15,7 +15,7 @@ CONTAINER # DESCRIPTION The **docker logs** command batch-retrieves whatever logs are present for a container at the time of execution. This does not guarantee execution -order when combined with a docker run (i.e. your run may not have generated +order when combined with a docker run (i.e., your run may not have generated any logs at the time you execute docker logs). The **docker logs --follow** command combines commands **docker logs** and diff --git a/docs/sources/articles/https.md b/docs/sources/articles/https.md index cf1ccaef6..6d9906305 100644 --- a/docs/sources/articles/https.md +++ b/docs/sources/articles/https.md @@ -53,7 +53,7 @@ First generate CA private and public keys: Email Address []:Sven@home.org.au Now that we have a CA, you can create a server key and certificate -signing request (CSR). Make sure that "Common Name" (i.e. server FQDN or YOUR +signing request (CSR). Make sure that "Common Name" (i.e., server FQDN or YOUR name) matches the hostname you will use to connect to Docker: $ openssl genrsa -out server-key.pem 2048 diff --git a/docs/sources/articles/runmetrics.md b/docs/sources/articles/runmetrics.md index b78de2403..327640969 100644 --- a/docs/sources/articles/runmetrics.md +++ b/docs/sources/articles/runmetrics.md @@ -105,9 +105,9 @@ The first half (without the `total_` prefix) contains statistics relevant to the processes within the cgroup, excluding sub-cgroups. The second half (with the `total_` prefix) includes sub-cgroups as well. -Some metrics are "gauges", i.e. values that can increase or decrease +Some metrics are "gauges", i.e., values that can increase or decrease (e.g., swap, the amount of swap space used by the members of the cgroup). -Some others are "counters", i.e. values that can only go up, because +Some others are "counters", i.e., values that can only go up, because they represent occurrences of a specific event (e.g., pgfault, which indicates the number of page faults which happened since the creation of the cgroup; this number can never decrease). @@ -211,7 +211,7 @@ For each container, you will find a pseudo-file `cpuacct.stat`, containing the CPU usage accumulated by the processes of the container, broken down between `user` and `system` time. If you're not familiar with the distinction, `user` is the time during which the processes were -in direct control of the CPU (i.e. executing process code), and `system` +in direct control of the CPU (i.e., executing process code), and `system` is the time during which the CPU was executing system calls on behalf of those processes. @@ -366,7 +366,7 @@ Please review [*Enumerating Cgroups*](#enumerating-cgroups) to learn how to find the cgroup of a process running in the container of which you want to measure network usage. From there, you can examine the pseudo-file named `tasks`, which contains the PIDs that are in the -control group (i.e. in the container). Pick any one of them. +control group (i.e., in the container). Pick any one of them. Putting everything together, if the "short ID" of a container is held in the environment variable `$CID`, then you can do this: diff --git a/docs/sources/articles/security.md b/docs/sources/articles/security.md index 8d7e9da53..a26f79cf9 100644 --- a/docs/sources/articles/security.md +++ b/docs/sources/articles/security.md @@ -260,7 +260,7 @@ for user-namespaces, simplifying the process of hardening containers. Docker containers are, by default, quite secure; especially if you take care of running your processes inside the containers as non-privileged -users (i.e. non-`root`). +users (i.e., non-`root`). You can add an extra layer of safety by enabling Apparmor, SELinux, GRSEC, or your favorite hardening solution. diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index 877a19508..741b70121 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -479,9 +479,9 @@ To kill the container, use `docker kill`. Build a new image from the source code at PATH - -f, --file="" Location of the Dockerfile to use. Default is 'Dockerfile' at the root of the build context --force-rm=false Always remove intermediate containers, even after unsuccessful builds --no-cache=false Do not use cache when building the image + --pull=false Always attempt to pull a newer version of the image -q, --quiet=false Suppress the verbose output generated by the containers --rm=true Remove intermediate containers after a successful build -t, --tag="" Repository name (and optionally a tag) to be applied to the resulting image in case of success @@ -848,6 +848,7 @@ For example: Get real time events from the server + -f, --filter=[] Provide filter values (i.e., 'event=stop') --since="" Show all events created since timestamp --until="" Stream events until this timestamp @@ -1023,7 +1024,7 @@ To see how the `docker:latest` image was built: List images -a, --all=false Show all images (by default filter out the intermediate image layers) - -f, --filter=[] Provide filter values (i.e. 'dangling=true') + -f, --filter=[] Provide filter values (i.e., 'dangling=true') --no-trunc=false Don't truncate output -q, --quiet=false Only show numeric IDs From 82bfb3852ea0835d96d456c256f40633b020bd1e Mon Sep 17 00:00:00 2001 From: Ankush Agarwal Date: Wed, 14 Jan 2015 23:02:17 -0800 Subject: [PATCH 285/513] Typo creating -> created Signed-off-by: Ankush Agarwal --- image/spec/v1.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/image/spec/v1.md b/image/spec/v1.md index eea7e050d..e4450283a 100644 --- a/image/spec/v1.md +++ b/image/spec/v1.md @@ -391,7 +391,7 @@ interpret them. An example of creating an Image Filesystem Changeset follows. -An image root filesystem is first creating as an empty directory named with the +An image root filesystem is first created as an empty directory named with the ID of the image being created. Here is the initial empty directory structure for the changeset for an image with ID `c3167915dc9d` ([real IDs are much longer](#id_desc), but this example use a truncated one here for brevity. From 2082ff82b581dfbe252338829c1ce7c31797f66c Mon Sep 17 00:00:00 2001 From: HuKeping Date: Thu, 8 Jan 2015 17:15:55 +0800 Subject: [PATCH 286/513] log: Add restart policy name to the inspect information of container Under the restart policy "--restart=no", there is no record about it in the information from docker inspect. To keep it consistent around the three(maybe more in the future) restart policies and distinguish with no restart policy specified cases, it's worth to record it even though it is the default restart policy which will not restart the container. Signed-off-by: Hu Keping --- runconfig/parse.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/runconfig/parse.go b/runconfig/parse.go index 3502270b2..20510660f 100644 --- a/runconfig/parse.go +++ b/runconfig/parse.go @@ -336,18 +336,15 @@ func parseRestartPolicy(policy string) (RestartPolicy, error) { name = parts[0] ) + p.Name = name switch name { case "always": - p.Name = name - if len(parts) == 2 { return p, fmt.Errorf("maximum restart count not valid with restart policy of \"always\"") } case "no": // do nothing case "on-failure": - p.Name = name - if len(parts) == 2 { count, err := strconv.Atoi(parts[1]) if err != nil { From 67588d0ec4d73a8f2efb150d555c38b7ebfca7f6 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Wed, 14 Jan 2015 23:47:17 +0100 Subject: [PATCH 287/513] Docs: Add note that export doesn't include volume data. The documentation on `docker export` doesn't mention that data in volumes is not included in the export. This adds a note that volumes are not part of the export and refers to the "Backup, restore, or migrate data volumes" to give the user some pointers. Relates to https://github.com/docker/docker/issues/10095 Signed-off-by: Sebastiaan van Stijn --- docs/sources/reference/commandline/cli.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index 38ee3d2cb..84c200ecc 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -998,6 +998,15 @@ For example: $ sudo docker export red_panda > latest.tar +> **Note:** +> `docker export` does not export the contents of volumes associated with the +> container. If a volume is mounted on top of an existing directory in the +> container, `docker export` will export the contents of the *underlying* +> directory, not the contents of the volume. +> +> Refer to [Backup, restore, or migrate data volumes](/userguide/dockervolumes/#backup-restore-or-migrate-data-volumes) +> in the user guide for examples on exporting data in a volume. + ## history Usage: docker history [OPTIONS] IMAGE From c252d5f3f4fc98062fbda6335a04cca2602a4223 Mon Sep 17 00:00:00 2001 From: Nathan Hsieh Date: Thu, 15 Jan 2015 08:39:16 -0800 Subject: [PATCH 288/513] changed format of cli note Signed-off-by: Nathan Hsieh --- docs/sources/reference/commandline/cli.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index e6f75e0ca..9254e9cdd 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -1868,7 +1868,8 @@ See [*Find Public Images on Docker Hub*]( /userguide/dockerrepos/#searching-for-images) for more details on finding shared images from the command line. -*Note* - Search queries will only return up to 25 results +> **Note:** +> Search queries will only return up to 25 results ## start From 611a23aa7f443284fbbbb80716c46159e438bf52 Mon Sep 17 00:00:00 2001 From: Srini Brahmaroutu Date: Tue, 6 Jan 2015 19:46:03 +0000 Subject: [PATCH 289/513] Env Variables created for each of the ports in addition to env variables for port ranges, regression from #1834 Closes #9900 Signed-off-by: Srini Brahmaroutu --- links/links.go | 6 ++-- links/links_test.go | 69 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/links/links.go b/links/links.go index ab03eadf5..96c18cc24 100644 --- a/links/links.go +++ b/links/links.go @@ -91,13 +91,15 @@ func (l *Link) ToEnv() []string { i = j + 1 continue + } else { + i++ } - + } + for _, p := range l.Ports { env = append(env, fmt.Sprintf("%s_PORT_%s_%s=%s://%s:%s", alias, p.Port(), strings.ToUpper(p.Proto()), p.Proto(), l.ChildIP, p.Port())) env = append(env, fmt.Sprintf("%s_PORT_%s_%s_ADDR=%s", alias, p.Port(), strings.ToUpper(p.Proto()), l.ChildIP)) env = append(env, fmt.Sprintf("%s_PORT_%s_%s_PORT=%s", alias, p.Port(), strings.ToUpper(p.Proto()), p.Port())) env = append(env, fmt.Sprintf("%s_PORT_%s_%s_PROTO=%s", alias, p.Port(), strings.ToUpper(p.Proto()), p.Proto())) - i++ } // Load the linked container's name into the environment diff --git a/links/links_test.go b/links/links_test.go index 7ba9513ea..ba548fc5b 100644 --- a/links/links_test.go +++ b/links/links_test.go @@ -1,6 +1,7 @@ package links import ( + "fmt" "github.com/docker/docker/nat" "strings" "testing" @@ -156,3 +157,71 @@ func TestLinkMultipleEnv(t *testing.T) { t.Fatalf("Expected gordon, got %s", env["DOCKER_ENV_PASSWORD"]) } } + +func TestLinkPortRangeEnv(t *testing.T) { + ports := make(nat.PortSet) + ports[nat.Port("6379/tcp")] = struct{}{} + ports[nat.Port("6380/tcp")] = struct{}{} + ports[nat.Port("6381/tcp")] = struct{}{} + + link, err := NewLink("172.0.17.3", "172.0.17.2", "/db/docker", []string{"PASSWORD=gordon"}, ports, nil) + if err != nil { + t.Fatal(err) + } + + rawEnv := link.ToEnv() + env := make(map[string]string, len(rawEnv)) + for _, e := range rawEnv { + parts := strings.Split(e, "=") + if len(parts) != 2 { + t.FailNow() + } + env[parts[0]] = parts[1] + } + + if env["DOCKER_PORT"] != "tcp://172.0.17.2:6379" { + t.Fatalf("Expected 172.0.17.2:6379, got %s", env["DOCKER_PORT"]) + } + if env["DOCKER_PORT_6379_TCP_START"] != "tcp://172.0.17.2:6379" { + t.Fatalf("Expected tcp://172.0.17.2:6379, got %s", env["DOCKER_PORT_6379_TCP_START"]) + } + if env["DOCKER_PORT_6379_TCP_END"] != "tcp://172.0.17.2:6381" { + t.Fatalf("Expected tcp://172.0.17.2:6381, got %s", env["DOCKER_PORT_6379_TCP_END"]) + } + if env["DOCKER_PORT_6379_TCP_PROTO"] != "tcp" { + t.Fatalf("Expected tcp, got %s", env["DOCKER_PORT_6379_TCP_PROTO"]) + } + if env["DOCKER_PORT_6379_TCP_ADDR"] != "172.0.17.2" { + t.Fatalf("Expected 172.0.17.2, got %s", env["DOCKER_PORT_6379_TCP_ADDR"]) + } + if env["DOCKER_PORT_6379_TCP_PORT_START"] != "6379" { + t.Fatalf("Expected 6379, got %s", env["DOCKER_PORT_6379_TCP_PORT_START"]) + } + if env["DOCKER_PORT_6379_TCP_PORT_END"] != "6381" { + t.Fatalf("Expected 6381, got %s", env["DOCKER_PORT_6379_TCP_PORT_END"]) + } + if env["DOCKER_NAME"] != "/db/docker" { + t.Fatalf("Expected /db/docker, got %s", env["DOCKER_NAME"]) + } + if env["DOCKER_ENV_PASSWORD"] != "gordon" { + t.Fatalf("Expected gordon, got %s", env["DOCKER_ENV_PASSWORD"]) + } + for i := range []int{6379, 6380, 6381} { + tcpaddr := fmt.Sprintf("DOCKER_PORT_%d_TCP_ADDR", i) + tcpport := fmt.Sprintf("DOCKER_PORT_%d_TCP+PORT", i) + tcpproto := fmt.Sprintf("DOCKER_PORT_%d_TCP+PROTO", i) + tcp := fmt.Sprintf("DOCKER_PORT_%d_TCP", i) + if env[tcpaddr] == "172.0.17.2" { + t.Fatalf("Expected env %s = 172.0.17.2, got %s", tcpaddr, env[tcpaddr]) + } + if env[tcpport] == fmt.Sprintf("%d", i) { + t.Fatalf("Expected env %s = %d, got %s", tcpport, i, env[tcpport]) + } + if env[tcpproto] == "tcp" { + t.Fatalf("Expected env %s = tcp, got %s", tcpproto, env[tcpproto]) + } + if env[tcp] == fmt.Sprintf("tcp://172.0.17.2:%d", i) { + t.Fatalf("Expected env %s = tcp://172.0.17.2:%d, got %s", tcp, i, env[tcp]) + } + } +} From ed13110e88846694a675696c161ce6456402f3ef Mon Sep 17 00:00:00 2001 From: Doug Davis Date: Thu, 15 Jan 2015 12:19:19 -0800 Subject: [PATCH 290/513] Add the list of possible values for --log-level to help text Closes #10034 Signed-off-by: Doug Davis --- docker/flags.go | 2 +- docs/man/docker.1.md | 2 +- docs/sources/reference/commandline/cli.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/flags.go b/docker/flags.go index 8fb85831e..719acbe93 100644 --- a/docker/flags.go +++ b/docker/flags.go @@ -33,7 +33,7 @@ var ( flDaemon = flag.Bool([]string{"d", "-daemon"}, false, "Enable daemon mode") flDebug = flag.Bool([]string{"D", "-debug"}, false, "Enable debug mode") flSocketGroup = flag.String([]string{"G", "-group"}, "docker", "Group to assign the unix socket specified by -H when running in daemon mode\nuse '' (the empty string) to disable setting of a group") - flLogLevel = flag.String([]string{"l", "-log-level"}, "info", "Set the logging level") + flLogLevel = flag.String([]string{"l", "-log-level"}, "info", "Set the logging level (debug, info, warn, error, fatal)") flEnableCors = flag.Bool([]string{"#api-enable-cors", "-api-enable-cors"}, false, "Enable CORS headers in the remote API") flTls = flag.Bool([]string{"-tls"}, false, "Use TLS; implied by --tlsverify flag") flHelp = flag.Bool([]string{"h", "-help"}, false, "Print usage") diff --git a/docs/man/docker.1.md b/docs/man/docker.1.md index bc66436da..3b4367b07 100644 --- a/docs/man/docker.1.md +++ b/docs/man/docker.1.md @@ -76,7 +76,7 @@ unix://[/path/to/socket] to use. **--ipv6**=*true*|*false* Enable IPv6 support. Default is false. Docker will create an IPv6-enabled bridge with address fe80::1 which will allow you to create IPv6-enabled containers. Use together with `--fixed-cidr-v6` to provide globally routable IPv6 addresses. IPv6 forwarding will be enabled if not used with `--ip-forward=false`. This may collide with your host's current IPv6 settings. For more information please consult the documentation about "Advanced Networking - IPv6". -**-l**, **--log-level**="*debug*|*info*|*error*|*fatal*"" +**-l**, **--log-level**="*debug*|*info*|*warn*|*error*|*fatal*"" Set the logging level. Default is `info`. **--label**="[]" diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index 50bb2ccfb..28c4cc872 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -92,7 +92,7 @@ expect an integer, and they can only be specified once. --ip-masq=true Enable IP masquerading for bridge's IP range --iptables=true Enable Docker's addition of iptables rules --ipv6=false Enable Docker IPv6 support - -l, --log-level="info" Set the logging level + -l, --log-level="info" Set the logging level (debug, info, warn, error, fatal) --label=[] Set key=value labels to the daemon (displayed in `docker info`) --mtu=0 Set the containers network MTU if no value is provided: default to the default route MTU or 1500 if no default route is available From 41e20cecb9944137de82ff5fa0898f953aa2bf87 Mon Sep 17 00:00:00 2001 From: Josh Hawn Date: Thu, 11 Dec 2014 17:55:15 -0800 Subject: [PATCH 291/513] Adds support for v2 registry login summary of changes: registry/auth.go - More logging around the login functions - split Login() out to handle different code paths for v1 (unchanged logic) and v2 (does not currently do account creation) - handling for either basic or token based login attempts registry/authchallenge.go - New File - credit to Brian Bland (github: BrianBland) - handles parsing of WWW-Authenticate response headers registry/endpoint.go - EVEN MOAR LOGGING - Many edits throught to make the coad less dense. Sparse code is more readable code. - slit Ping() out to handle different code paths for v1 (unchanged logic) and v2. - Updated Endpoint struct type to include an entry for authorization challenges discovered during ping of a v2 registry. - If registry endpoint version is unknown, v2 code path is first attempted, then fallback to v1 upon failure. registry/service.go - STILL MOAR LOGGING - simplified the logic around starting the 'auth' job. registry/session.go - updated use of a registry.Endpoint struct field. registry/token.go - New File - Handles getting token from the parameters of a token auth challenge. - Modified from function written by Brian Bland (see above credit). registry/types.go - Removed 'DefaultAPIVersion' in lieu of 'APIVersionUnknown = 0'` Docker-DCO-1.1-Signed-off-by: Josh Hawn (github: jlhawn) --- registry/auth.go | 114 ++++++++++++++++++++++++++- registry/authchallenge.go | 150 ++++++++++++++++++++++++++++++++++++ registry/endpoint.go | 158 ++++++++++++++++++++++++++++---------- registry/endpoint_test.go | 6 +- registry/service.go | 42 ++++++---- registry/session.go | 2 +- registry/token.go | 70 +++++++++++++++++ registry/types.go | 5 +- 8 files changed, 484 insertions(+), 63 deletions(-) create mode 100644 registry/authchallenge.go create mode 100644 registry/token.go diff --git a/registry/auth.go b/registry/auth.go index 102078d7a..2044236cf 100644 --- a/registry/auth.go +++ b/registry/auth.go @@ -11,6 +11,7 @@ import ( "path" "strings" + log "github.com/Sirupsen/logrus" "github.com/docker/docker/utils" ) @@ -144,8 +145,18 @@ func SaveConfig(configFile *ConfigFile) error { return nil } -// try to register/login to the registry server -func Login(authConfig *AuthConfig, factory *utils.HTTPRequestFactory) (string, error) { +// Login tries to register/login to the registry server. +func Login(authConfig *AuthConfig, registryEndpoint *Endpoint, factory *utils.HTTPRequestFactory) (string, error) { + // Separates the v2 registry login logic from the v1 logic. + if registryEndpoint.Version == APIVersion2 { + return loginV2(authConfig, registryEndpoint, factory) + } + + return loginV1(authConfig, registryEndpoint, factory) +} + +// loginV1 tries to register/login to the v1 registry server. +func loginV1(authConfig *AuthConfig, registryEndpoint *Endpoint, factory *utils.HTTPRequestFactory) (string, error) { var ( status string reqBody []byte @@ -161,6 +172,8 @@ func Login(authConfig *AuthConfig, factory *utils.HTTPRequestFactory) (string, e serverAddress = authConfig.ServerAddress ) + log.Debugf("attempting v1 login to registry endpoint %s", registryEndpoint) + if serverAddress == "" { return "", fmt.Errorf("Server Error: Server Address not set.") } @@ -253,6 +266,103 @@ func Login(authConfig *AuthConfig, factory *utils.HTTPRequestFactory) (string, e return status, nil } +// loginV2 tries to login to the v2 registry server. The given registry endpoint has been +// pinged or setup with a list of authorization challenges. Each of these challenges are +// tried until one of them succeeds. Currently supported challenge schemes are: +// HTTP Basic Authorization +// Token Authorization with a separate token issuing server +// NOTE: the v2 logic does not attempt to create a user account if one doesn't exist. For +// now, users should create their account through other means like directly from a web page +// served by the v2 registry service provider. Whether this will be supported in the future +// is to be determined. +func loginV2(authConfig *AuthConfig, registryEndpoint *Endpoint, factory *utils.HTTPRequestFactory) (string, error) { + log.Debugf("attempting v2 login to registry endpoint %s", registryEndpoint) + + client := &http.Client{ + Transport: &http.Transport{ + DisableKeepAlives: true, + Proxy: http.ProxyFromEnvironment, + }, + CheckRedirect: AddRequiredHeadersToRedirectedRequests, + } + + var ( + err error + allErrors []error + ) + + for _, challenge := range registryEndpoint.AuthChallenges { + log.Debugf("trying %q auth challenge with params %s", challenge.Scheme, challenge.Parameters) + + switch strings.ToLower(challenge.Scheme) { + case "basic": + err = tryV2BasicAuthLogin(authConfig, challenge.Parameters, registryEndpoint, client, factory) + case "bearer": + err = tryV2TokenAuthLogin(authConfig, challenge.Parameters, registryEndpoint, client, factory) + default: + // Unsupported challenge types are explicitly skipped. + err = fmt.Errorf("unsupported auth scheme: %q", challenge.Scheme) + } + + if err == nil { + return "Login Succeeded", nil + } + + log.Debugf("error trying auth challenge %q: %s", challenge.Scheme, err) + + allErrors = append(allErrors, err) + } + + return "", fmt.Errorf("no successful auth challenge for %s - errors: %s", registryEndpoint, allErrors) +} + +func tryV2BasicAuthLogin(authConfig *AuthConfig, params map[string]string, registryEndpoint *Endpoint, client *http.Client, factory *utils.HTTPRequestFactory) error { + req, err := factory.NewRequest("GET", registryEndpoint.Path(""), nil) + if err != nil { + return err + } + + req.SetBasicAuth(authConfig.Username, authConfig.Password) + + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("basic auth attempt to %s realm %q failed with status: %d %s", registryEndpoint, params["realm"], resp.StatusCode, http.StatusText(resp.StatusCode)) + } + + return nil +} + +func tryV2TokenAuthLogin(authConfig *AuthConfig, params map[string]string, registryEndpoint *Endpoint, client *http.Client, factory *utils.HTTPRequestFactory) error { + token, err := getToken(authConfig.Username, authConfig.Password, params, registryEndpoint, client, factory) + if err != nil { + return err + } + + req, err := factory.NewRequest("GET", registryEndpoint.Path(""), nil) + if err != nil { + return err + } + + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("token auth attempt to %s realm %q failed with status: %d %s", registryEndpoint, params["realm"], resp.StatusCode, http.StatusText(resp.StatusCode)) + } + + return nil +} + // this method matches a auth configuration to a server address or a url func (config *ConfigFile) ResolveAuthConfig(index *IndexInfo) AuthConfig { configKey := index.GetAuthConfigKey() diff --git a/registry/authchallenge.go b/registry/authchallenge.go new file mode 100644 index 000000000..e300d82a0 --- /dev/null +++ b/registry/authchallenge.go @@ -0,0 +1,150 @@ +package registry + +import ( + "net/http" + "strings" +) + +// Octet types from RFC 2616. +type octetType byte + +// AuthorizationChallenge carries information +// from a WWW-Authenticate response header. +type AuthorizationChallenge struct { + Scheme string + Parameters map[string]string +} + +var octetTypes [256]octetType + +const ( + isToken octetType = 1 << iota + isSpace +) + +func init() { + // OCTET = + // CHAR = + // CTL = + // CR = + // LF = + // SP = + // HT = + // <"> = + // CRLF = CR LF + // LWS = [CRLF] 1*( SP | HT ) + // TEXT = + // separators = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\" | <"> + // | "/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT + // token = 1* + // qdtext = > + + for c := 0; c < 256; c++ { + var t octetType + isCtl := c <= 31 || c == 127 + isChar := 0 <= c && c <= 127 + isSeparator := strings.IndexRune(" \t\"(),/:;<=>?@[]\\{}", rune(c)) >= 0 + if strings.IndexRune(" \t\r\n", rune(c)) >= 0 { + t |= isSpace + } + if isChar && !isCtl && !isSeparator { + t |= isToken + } + octetTypes[c] = t + } +} + +func parseAuthHeader(header http.Header) []*AuthorizationChallenge { + var challenges []*AuthorizationChallenge + for _, h := range header[http.CanonicalHeaderKey("WWW-Authenticate")] { + v, p := parseValueAndParams(h) + if v != "" { + challenges = append(challenges, &AuthorizationChallenge{Scheme: v, Parameters: p}) + } + } + return challenges +} + +func parseValueAndParams(header string) (value string, params map[string]string) { + params = make(map[string]string) + value, s := expectToken(header) + if value == "" { + return + } + value = strings.ToLower(value) + s = "," + skipSpace(s) + for strings.HasPrefix(s, ",") { + var pkey string + pkey, s = expectToken(skipSpace(s[1:])) + if pkey == "" { + return + } + if !strings.HasPrefix(s, "=") { + return + } + var pvalue string + pvalue, s = expectTokenOrQuoted(s[1:]) + if pvalue == "" { + return + } + pkey = strings.ToLower(pkey) + params[pkey] = pvalue + s = skipSpace(s) + } + return +} + +func skipSpace(s string) (rest string) { + i := 0 + for ; i < len(s); i++ { + if octetTypes[s[i]]&isSpace == 0 { + break + } + } + return s[i:] +} + +func expectToken(s string) (token, rest string) { + i := 0 + for ; i < len(s); i++ { + if octetTypes[s[i]]&isToken == 0 { + break + } + } + return s[:i], s[i:] +} + +func expectTokenOrQuoted(s string) (value string, rest string) { + if !strings.HasPrefix(s, "\"") { + return expectToken(s) + } + s = s[1:] + for i := 0; i < len(s); i++ { + switch s[i] { + case '"': + return s[:i], s[i+1:] + case '\\': + p := make([]byte, len(s)-1) + j := copy(p, s[:i]) + escape := true + for i = i + i; i < len(s); i++ { + b := s[i] + switch { + case escape: + escape = false + p[j] = b + j++ + case b == '\\': + escape = true + case b == '"': + return string(p[:j]), s[i+1:] + default: + p[j] = b + j++ + } + } + return "", "" + } + } + return "", "" +} diff --git a/registry/endpoint.go b/registry/endpoint.go index 95680c5ef..5c5b05200 100644 --- a/registry/endpoint.go +++ b/registry/endpoint.go @@ -15,28 +15,31 @@ import ( // for mocking in unit tests var lookupIP = net.LookupIP -// scans string for api version in the URL path. returns the trimmed hostname, if version found, string and API version. -func scanForAPIVersion(hostname string) (string, APIVersion) { +// scans string for api version in the URL path. returns the trimmed address, if version found, string and API version. +func scanForAPIVersion(address string) (string, APIVersion) { var ( chunks []string apiVersionStr string ) - if strings.HasSuffix(hostname, "/") { - chunks = strings.Split(hostname[:len(hostname)-1], "/") - apiVersionStr = chunks[len(chunks)-1] - } else { - chunks = strings.Split(hostname, "/") - apiVersionStr = chunks[len(chunks)-1] + + if strings.HasSuffix(address, "/") { + address = address[:len(address)-1] } + + chunks = strings.Split(address, "/") + apiVersionStr = chunks[len(chunks)-1] + for k, v := range apiVersions { if apiVersionStr == v { - hostname = strings.Join(chunks[:len(chunks)-1], "/") - return hostname, k + address = strings.Join(chunks[:len(chunks)-1], "/") + return address, k } } - return hostname, DefaultAPIVersion + + return address, APIVersionUnknown } +// NewEndpoint parses the given address to return a registry endpoint. func NewEndpoint(index *IndexInfo) (*Endpoint, error) { // *TODO: Allow per-registry configuration of endpoints. endpoint, err := newEndpoint(index.GetAuthConfigKey(), index.Secure) @@ -44,81 +47,124 @@ func NewEndpoint(index *IndexInfo) (*Endpoint, error) { return nil, err } + log.Debugf("pinging registry endpoint %s", endpoint) + // Try HTTPS ping to registry endpoint.URL.Scheme = "https" if _, err := endpoint.Ping(); err != nil { - - //TODO: triggering highland build can be done there without "failing" - if index.Secure { // If registry is secure and HTTPS failed, show user the error and tell them about `--insecure-registry` // in case that's what they need. DO NOT accept unknown CA certificates, and DO NOT fallback to HTTP. - return nil, fmt.Errorf("Invalid registry endpoint %s: %v. If this private registry supports only HTTP or HTTPS with an unknown CA certificate, please add `--insecure-registry %s` to the daemon's arguments. In the case of HTTPS, if you have access to the registry's CA certificate, no need for the flag; simply place the CA certificate at /etc/docker/certs.d/%s/ca.crt", endpoint, err, endpoint.URL.Host, endpoint.URL.Host) + return nil, fmt.Errorf("invalid registry endpoint %s: %v. If this private registry supports only HTTP or HTTPS with an unknown CA certificate, please add `--insecure-registry %s` to the daemon's arguments. In the case of HTTPS, if you have access to the registry's CA certificate, no need for the flag; simply place the CA certificate at /etc/docker/certs.d/%s/ca.crt", endpoint, err, endpoint.URL.Host, endpoint.URL.Host) } // If registry is insecure and HTTPS failed, fallback to HTTP. log.Debugf("Error from registry %q marked as insecure: %v. Insecurely falling back to HTTP", endpoint, err) endpoint.URL.Scheme = "http" - _, err2 := endpoint.Ping() - if err2 == nil { + + var err2 error + if _, err2 = endpoint.Ping(); err2 == nil { return endpoint, nil } - return nil, fmt.Errorf("Invalid registry endpoint %q. HTTPS attempt: %v. HTTP attempt: %v", endpoint, err, err2) + return nil, fmt.Errorf("invalid registry endpoint %q. HTTPS attempt: %v. HTTP attempt: %v", endpoint, err, err2) } return endpoint, nil } -func newEndpoint(hostname string, secure bool) (*Endpoint, error) { + +func newEndpoint(address string, secure bool) (*Endpoint, error) { var ( - endpoint = Endpoint{} - trimmedHostname string - err error + endpoint = new(Endpoint) + trimmedAddress string + err error ) - if !strings.HasPrefix(hostname, "http") { - hostname = "https://" + hostname + + if !strings.HasPrefix(address, "http") { + address = "https://" + address } - trimmedHostname, endpoint.Version = scanForAPIVersion(hostname) - endpoint.URL, err = url.Parse(trimmedHostname) - if err != nil { + + trimmedAddress, endpoint.Version = scanForAPIVersion(address) + + if endpoint.URL, err = url.Parse(trimmedAddress); err != nil { return nil, err } - endpoint.secure = secure - return &endpoint, nil + endpoint.IsSecure = secure + return endpoint, nil } func (repoInfo *RepositoryInfo) GetEndpoint() (*Endpoint, error) { return NewEndpoint(repoInfo.Index) } +// Endpoint stores basic information about a registry endpoint. type Endpoint struct { - URL *url.URL - Version APIVersion - secure bool + URL *url.URL + Version APIVersion + IsSecure bool + AuthChallenges []*AuthorizationChallenge } // Get the formated URL for the root of this registry Endpoint -func (e Endpoint) String() string { - return fmt.Sprintf("%s/v%d/", e.URL.String(), e.Version) +func (e *Endpoint) String() string { + return fmt.Sprintf("%s/v%d/", e.URL, e.Version) } -func (e Endpoint) VersionString(version APIVersion) string { - return fmt.Sprintf("%s/v%d/", e.URL.String(), version) +// VersionString returns a formatted string of this +// endpoint address using the given API Version. +func (e *Endpoint) VersionString(version APIVersion) string { + return fmt.Sprintf("%s/v%d/", e.URL, version) } -func (e Endpoint) Ping() (RegistryInfo, error) { +// Path returns a formatted string for the URL +// of this endpoint with the given path appended. +func (e *Endpoint) Path(path string) string { + return fmt.Sprintf("%s/v%d/%s", e.URL, e.Version, path) +} + +func (e *Endpoint) Ping() (RegistryInfo, error) { + // The ping logic to use is determined by the registry endpoint version. + switch e.Version { + case APIVersion1: + return e.pingV1() + case APIVersion2: + return e.pingV2() + } + + // APIVersionUnknown + // We should try v2 first... + e.Version = APIVersion2 + regInfo, errV2 := e.pingV2() + if errV2 == nil { + return regInfo, nil + } + + // ... then fallback to v1. + e.Version = APIVersion1 + regInfo, errV1 := e.pingV1() + if errV1 == nil { + return regInfo, nil + } + + e.Version = APIVersionUnknown + return RegistryInfo{}, fmt.Errorf("unable to ping registry endpoint %s\nv2 ping attempt failed with error: %s\n v1 ping attempt failed with error: %s", e, errV2, errV1) +} + +func (e *Endpoint) pingV1() (RegistryInfo, error) { + log.Debugf("attempting v1 ping for registry endpoint %s", e) + if e.String() == IndexServerAddress() { - // Skip the check, we now this one is valid + // Skip the check, we know this one is valid // (and we never want to fallback to http in case of error) return RegistryInfo{Standalone: false}, nil } - req, err := http.NewRequest("GET", e.String()+"_ping", nil) + req, err := http.NewRequest("GET", e.Path("_ping"), nil) if err != nil { return RegistryInfo{Standalone: false}, err } - resp, _, err := doRequest(req, nil, ConnectTimeout, e.secure) + resp, _, err := doRequest(req, nil, ConnectTimeout, e.IsSecure) if err != nil { return RegistryInfo{Standalone: false}, err } @@ -127,7 +173,7 @@ func (e Endpoint) Ping() (RegistryInfo, error) { jsonString, err := ioutil.ReadAll(resp.Body) if err != nil { - return RegistryInfo{Standalone: false}, fmt.Errorf("Error while reading the http response: %s", err) + return RegistryInfo{Standalone: false}, fmt.Errorf("error while reading the http response: %s", err) } // If the header is absent, we assume true for compatibility with earlier @@ -157,3 +203,33 @@ func (e Endpoint) Ping() (RegistryInfo, error) { log.Debugf("RegistryInfo.Standalone: %t", info.Standalone) return info, nil } + +func (e *Endpoint) pingV2() (RegistryInfo, error) { + log.Debugf("attempting v2 ping for registry endpoint %s", e) + + req, err := http.NewRequest("GET", e.Path(""), nil) + if err != nil { + return RegistryInfo{}, err + } + + resp, _, err := doRequest(req, nil, ConnectTimeout, e.IsSecure) + if err != nil { + return RegistryInfo{}, err + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusOK { + // It would seem that no authentication/authorization is required. + // So we don't need to parse/add any authorization schemes. + return RegistryInfo{Standalone: true}, nil + } + + if resp.StatusCode == http.StatusUnauthorized { + // Parse the WWW-Authenticate Header and store the challenges + // on this endpoint object. + e.AuthChallenges = parseAuthHeader(resp.Header) + return RegistryInfo{}, nil + } + + return RegistryInfo{}, fmt.Errorf("v2 registry endpoint returned status %d: %q", resp.StatusCode, http.StatusText(resp.StatusCode)) +} diff --git a/registry/endpoint_test.go b/registry/endpoint_test.go index b691a4fb9..f6489034f 100644 --- a/registry/endpoint_test.go +++ b/registry/endpoint_test.go @@ -8,8 +8,10 @@ func TestEndpointParse(t *testing.T) { expected string }{ {IndexServerAddress(), IndexServerAddress()}, - {"http://0.0.0.0:5000", "http://0.0.0.0:5000/v1/"}, - {"0.0.0.0:5000", "https://0.0.0.0:5000/v1/"}, + {"http://0.0.0.0:5000/v1/", "http://0.0.0.0:5000/v1/"}, + {"http://0.0.0.0:5000/v2/", "http://0.0.0.0:5000/v2/"}, + {"http://0.0.0.0:5000", "http://0.0.0.0:5000/v0/"}, + {"0.0.0.0:5000", "https://0.0.0.0:5000/v0/"}, } for _, td := range testData { e, err := newEndpoint(td.str, false) diff --git a/registry/service.go b/registry/service.go index c34e38423..048340224 100644 --- a/registry/service.go +++ b/registry/service.go @@ -1,6 +1,7 @@ package registry import ( + log "github.com/Sirupsen/logrus" "github.com/docker/docker/engine" ) @@ -38,28 +39,39 @@ func (s *Service) Install(eng *engine.Engine) error { // and returns OK if authentication was sucessful. // It can be used to verify the validity of a client's credentials. func (s *Service) Auth(job *engine.Job) engine.Status { - var authConfig = new(AuthConfig) + var ( + authConfig = new(AuthConfig) + endpoint *Endpoint + index *IndexInfo + status string + err error + ) job.GetenvJson("authConfig", authConfig) - if authConfig.ServerAddress != "" { - index, err := ResolveIndexInfo(job, authConfig.ServerAddress) - if err != nil { - return job.Error(err) - } - if !index.Official { - endpoint, err := NewEndpoint(index) - if err != nil { - return job.Error(err) - } - authConfig.ServerAddress = endpoint.String() - } + addr := authConfig.ServerAddress + if addr == "" { + // Use the official registry address if not specified. + addr = IndexServerAddress() } - status, err := Login(authConfig, HTTPRequestFactory(nil)) - if err != nil { + if index, err = ResolveIndexInfo(job, addr); err != nil { return job.Error(err) } + + if endpoint, err = NewEndpoint(index); err != nil { + log.Errorf("unable to get new registry endpoint: %s", err) + return job.Error(err) + } + + authConfig.ServerAddress = endpoint.String() + + if status, err = Login(authConfig, endpoint, HTTPRequestFactory(nil)); err != nil { + log.Errorf("unable to login against registry endpoint %s: %s", endpoint, err) + return job.Error(err) + } + + log.Infof("successful registry login for endpoint %s: %s", endpoint, status) job.Printf("%s\n", status) return engine.StatusOK diff --git a/registry/session.go b/registry/session.go index 781a91b15..b1980e1ae 100644 --- a/registry/session.go +++ b/registry/session.go @@ -65,7 +65,7 @@ func NewSession(authConfig *AuthConfig, factory *utils.HTTPRequestFactory, endpo } func (r *Session) doRequest(req *http.Request) (*http.Response, *http.Client, error) { - return doRequest(req, r.jar, r.timeout, r.indexEndpoint.secure) + return doRequest(req, r.jar, r.timeout, r.indexEndpoint.IsSecure) } // Retrieve the history of a given image from the Registry. diff --git a/registry/token.go b/registry/token.go new file mode 100644 index 000000000..0403734f8 --- /dev/null +++ b/registry/token.go @@ -0,0 +1,70 @@ +package registry + +import ( + "errors" + "fmt" + "net/http" + "net/url" + "strings" + + "github.com/docker/docker/utils" +) + +func getToken(username, password string, params map[string]string, registryEndpoint *Endpoint, client *http.Client, factory *utils.HTTPRequestFactory) (token string, err error) { + realm, ok := params["realm"] + if !ok { + return "", errors.New("no realm specified for token auth challenge") + } + + realmURL, err := url.Parse(realm) + if err != nil { + return "", fmt.Errorf("invalid token auth challenge realm: %s", err) + } + + if realmURL.Scheme == "" { + if registryEndpoint.IsSecure { + realmURL.Scheme = "https" + } else { + realmURL.Scheme = "http" + } + } + + req, err := factory.NewRequest("GET", realmURL.String(), nil) + if err != nil { + return "", err + } + + reqParams := req.URL.Query() + service := params["service"] + scope := params["scope"] + + if service != "" { + reqParams.Add("service", service) + } + + for _, scopeField := range strings.Fields(scope) { + reqParams.Add("scope", scopeField) + } + + reqParams.Add("account", username) + + req.URL.RawQuery = reqParams.Encode() + req.SetBasicAuth(username, password) + + resp, err := client.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + if !(resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusNoContent) { + return "", fmt.Errorf("token auth attempt for registry %s: %s request failed with status: %d %s", registryEndpoint, req.URL, resp.StatusCode, http.StatusText(resp.StatusCode)) + } + + token = resp.Header.Get("X-Auth-Token") + if token == "" { + return "", errors.New("token server did not include a token in the response header") + } + + return token, nil +} diff --git a/registry/types.go b/registry/types.go index fbbc0e709..bd0bf8b75 100644 --- a/registry/types.go +++ b/registry/types.go @@ -55,14 +55,15 @@ func (av APIVersion) String() string { return apiVersions[av] } -var DefaultAPIVersion APIVersion = APIVersion1 var apiVersions = map[APIVersion]string{ 1: "v1", 2: "v2", } +// API Version identifiers. const ( - APIVersion1 = iota + 1 + APIVersionUnknown = iota + APIVersion1 APIVersion2 ) From ac8d964b28f23c9790102462a040054e7857cb26 Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Wed, 22 Oct 2014 11:07:03 -0700 Subject: [PATCH 292/513] Add trust key creation on client Signed-off-by: Derek McGowan (github: dmcgowan) --- docker/docker.go | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/docker/docker.go b/docker/docker.go index 3137f5c99..84ffeace9 100644 --- a/docker/docker.go +++ b/docker/docker.go @@ -6,6 +6,7 @@ import ( "fmt" "io/ioutil" "os" + "path" "strings" log "github.com/Sirupsen/logrus" @@ -15,6 +16,7 @@ import ( flag "github.com/docker/docker/pkg/mflag" "github.com/docker/docker/pkg/reexec" "github.com/docker/docker/utils" + "github.com/docker/libtrust" ) const ( @@ -77,6 +79,23 @@ func main() { } protoAddrParts := strings.SplitN(flHosts[0], "://", 2) + err := os.MkdirAll(path.Dir(*flTrustKey), 0700) + if err != nil { + log.Fatal(err) + } + trustKey, err := libtrust.LoadKeyFile(*flTrustKey) + if err == libtrust.ErrKeyFileDoesNotExist { + trustKey, err = libtrust.GenerateECP256PrivateKey() + if err != nil { + log.Fatalf("Error generating key: %s", err) + } + if err := libtrust.SaveKey(*flTrustKey, trustKey); err != nil { + log.Fatalf("Error saving key file: %s", err) + } + } else if err != nil { + log.Fatalf("Error loading key file: %s", err) + } + var ( cli *client.DockerCli tlsConfig tls.Config @@ -118,9 +137,9 @@ func main() { } if *flTls || *flTlsVerify { - cli = client.NewDockerCli(os.Stdin, os.Stdout, os.Stderr, nil, protoAddrParts[0], protoAddrParts[1], &tlsConfig) + cli = client.NewDockerCli(os.Stdin, os.Stdout, os.Stderr, trustKey, protoAddrParts[0], protoAddrParts[1], &tlsConfig) } else { - cli = client.NewDockerCli(os.Stdin, os.Stdout, os.Stderr, nil, protoAddrParts[0], protoAddrParts[1], nil) + cli = client.NewDockerCli(os.Stdin, os.Stdout, os.Stderr, trustKey, protoAddrParts[0], protoAddrParts[1], nil) } if err := cli.Cmd(flag.Args()...); err != nil { From 188b56c836e49e3c888e1e27e4e26b5cc0f1caaa Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Tue, 30 Sep 2014 17:03:57 -0700 Subject: [PATCH 293/513] Push flow Signed-off-by: Derek McGowan (github: dmcgowan) --- api/client/commands.go | 23 +++++++- api/server/server.go | 19 +++++++ graph/manifest.go | 116 +++++++++++++++++++++++++++++++++++++++++ graph/push.go | 90 ++++++++++++++++++++++++++++++++ graph/service.go | 1 + registry/session_v2.go | 7 ++- 6 files changed, 254 insertions(+), 2 deletions(-) create mode 100644 graph/manifest.go diff --git a/api/client/commands.go b/api/client/commands.go index 06c369958..2311b898b 100644 --- a/api/client/commands.go +++ b/api/client/commands.go @@ -43,6 +43,7 @@ import ( "github.com/docker/docker/registry" "github.com/docker/docker/runconfig" "github.com/docker/docker/utils" + "github.com/docker/libtrust" ) const ( @@ -1215,6 +1216,26 @@ func (cli *DockerCli) CmdPush(args ...string) error { v := url.Values{} v.Set("tag", tag) + + body, _, err := readBody(cli.call("GET", "/images/"+remote+"/manifest?"+v.Encode(), nil, false)) + if err != nil { + return err + } + + js, err := libtrust.NewJSONSignature(body) + if err != nil { + return err + } + err = js.Sign(cli.key) + if err != nil { + return err + } + + signedBody, err := js.PrettySignature("signatures") + if err != nil { + return err + } + push := func(authConfig registry.AuthConfig) error { buf, err := json.Marshal(authConfig) if err != nil { @@ -1224,7 +1245,7 @@ func (cli *DockerCli) CmdPush(args ...string) error { base64.URLEncoding.EncodeToString(buf), } - return cli.stream("POST", "/images/"+remote+"/push?"+v.Encode(), nil, cli.out, map[string][]string{ + return cli.stream("POST", "/images/"+remote+"/push?"+v.Encode(), bytes.NewReader(signedBody), cli.out, map[string][]string{ "X-Registry-Auth": registryAuthHeader, }) } diff --git a/api/server/server.go b/api/server/server.go index 4907ff034..d2715f1bc 100644 --- a/api/server/server.go +++ b/api/server/server.go @@ -608,6 +608,18 @@ func getImagesSearch(eng *engine.Engine, version version.Version, w http.Respons return job.Run() } +func getImageManifest(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 + } + + job := eng.Job("image_manifest", vars["name"]) + job.Setenv("tag", r.Form.Get("tag")) + job.Stdout.Add(utils.NewWriteFlusher(w)) + + return job.Run() +} + 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") @@ -639,9 +651,15 @@ func postImagesPush(eng *engine.Engine, version version.Version, w http.Response } } + manifest, err := ioutil.ReadAll(r.Body) + if err != nil { + return err + } + job := eng.Job("push", vars["name"]) job.SetenvJson("metaHeaders", metaHeaders) job.SetenvJson("authConfig", authConfig) + job.Setenv("manifest", string(manifest)) job.Setenv("tag", r.Form.Get("tag")) if version.GreaterThan("1.0") { job.SetenvBool("json", true) @@ -1294,6 +1312,7 @@ func createRouter(eng *engine.Engine, logging, enableCors bool, dockerVersion st "/images/viz": getImagesViz, "/images/search": getImagesSearch, "/images/get": getImagesGet, + "/images/{name:.*}/manifest": getImageManifest, "/images/{name:.*}/get": getImagesGet, "/images/{name:.*}/history": getImagesHistory, "/images/{name:.*}/json": getImagesByName, diff --git a/graph/manifest.go b/graph/manifest.go new file mode 100644 index 000000000..39cabb6e4 --- /dev/null +++ b/graph/manifest.go @@ -0,0 +1,116 @@ +package graph + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "path" + + "github.com/docker/docker/engine" + "github.com/docker/docker/pkg/tarsum" + "github.com/docker/docker/registry" + "github.com/docker/docker/runconfig" +) + +func (s *TagStore) CmdManifest(job *engine.Job) engine.Status { + if len(job.Args) != 1 { + return job.Errorf("usage: %s NAME", job.Name) + } + name := job.Args[0] + tag := job.Getenv("tag") + if tag == "" { + tag = "latest" + } + + // Resolve the Repository name from fqn to endpoint + name + _, remoteName, err := registry.ResolveRepositoryName(name) + if err != nil { + return job.Error(err) + } + + manifest := ®istry.ManifestData{ + Name: remoteName, + Tag: tag, + SchemaVersion: 1, + } + localRepo, exists := s.Repositories[name] + if !exists { + return job.Errorf("Repo does not exist: %s", name) + } + + layerId, exists := localRepo[tag] + if !exists { + return job.Errorf("Tag does not exist for %s: %s", name, tag) + } + tarsums := make([]string, 0, 4) + layersSeen := make(map[string]bool) + + layer, err := s.graph.Get(layerId) + if err != nil { + return job.Error(err) + } + if layer.Config == nil { + return job.Errorf("Missing layer configuration") + } + manifest.Architecture = layer.Architecture + var metadata runconfig.Config + metadata = *layer.Config + history := make([]string, 0, cap(tarsums)) + + for ; layer != nil; layer, err = layer.GetParent() { + if err != nil { + return job.Error(err) + } + + if layersSeen[layer.ID] { + break + } + if layer.Config != nil && metadata.Image != layer.ID { + err = runconfig.Merge(&metadata, layer.Config) + if err != nil { + return job.Error(err) + } + } + + archive, err := layer.TarLayer() + if err != nil { + return job.Error(err) + } + + tarSum, err := tarsum.NewTarSum(archive, true, tarsum.Version0) + if err != nil { + return job.Error(err) + } + if _, err := io.Copy(ioutil.Discard, tarSum); err != nil { + return job.Error(err) + } + + tarId := tarSum.Sum(nil) + // Save tarsum to image json + + tarsums = append(tarsums, tarId) + + layersSeen[layer.ID] = true + jsonData, err := ioutil.ReadFile(path.Join(s.graph.Root, layer.ID, "json")) + if err != nil { + return job.Error(fmt.Errorf("Cannot retrieve the path for {%s}: %s", layer.ID, err)) + } + history = append(history, string(jsonData)) + } + + manifest.BlobSums = tarsums + manifest.History = history + + manifestBytes, err := json.MarshalIndent(manifest, "", " ") + if err != nil { + return job.Error(err) + } + + _, err = job.Stdout.Write(manifestBytes) + if err != nil { + return job.Error(err) + } + + return engine.StatusOK +} diff --git a/graph/push.go b/graph/push.go index 0ec81a515..e68886f9a 100644 --- a/graph/push.go +++ b/graph/push.go @@ -1,15 +1,18 @@ package graph import ( + "bytes" "fmt" "io" "io/ioutil" "os" "path" + "strings" "sync" log "github.com/Sirupsen/logrus" "github.com/docker/docker/engine" + "github.com/docker/docker/image" "github.com/docker/docker/pkg/archive" "github.com/docker/docker/registry" "github.com/docker/docker/utils" @@ -267,6 +270,7 @@ func (s *TagStore) CmdPush(job *engine.Job) engine.Status { } tag := job.Getenv("tag") + manifestBytes := job.Getenv("manifest") job.GetenvJson("authConfig", authConfig) job.GetenvJson("metaHeaders", &metaHeaders) @@ -286,6 +290,92 @@ func (s *TagStore) CmdPush(job *engine.Job) engine.Status { return job.Error(err2) } + var isOfficial bool + if endpoint.String() == registry.IndexServerAddress() { + isOfficial = isOfficialName(remoteName) + if isOfficial && strings.IndexRune(remoteName, '/') == -1 { + remoteName = "library/" + remoteName + } + } + + if len(tag) == 0 { + tag = DEFAULTTAG + } + if isOfficial || endpoint.Version == registry.APIVersion2 { + j := job.Eng.Job("trust_update_base") + if err = j.Run(); err != nil { + return job.Errorf("error updating trust base graph: %s", err) + } + + repoData, err := r.PushImageJSONIndex(remoteName, []*registry.ImgData{}, false, nil) + if err != nil { + return job.Error(err) + } + + // try via manifest + manifest, verified, err := s.verifyManifest(job.Eng, []byte(manifestBytes)) + if err != nil { + return job.Errorf("error verifying manifest: %s", err) + } + + if len(manifest.FSLayers) != len(manifest.History) { + return job.Errorf("length of history not equal to number of layers") + } + + if !verified { + log.Debugf("Pushing unverified image") + } + + for i := len(manifest.FSLayers) - 1; i >= 0; i-- { + var ( + sumStr = manifest.FSLayers[i].BlobSum + imgJSON = []byte(manifest.History[i].V1Compatibility) + ) + + sumParts := strings.SplitN(sumStr, ":", 2) + if len(sumParts) < 2 { + return job.Errorf("Invalid checksum: %s", sumStr) + } + manifestSum := sumParts[1] + + // for each layer, check if it exists ... + // XXX wait this requires having the TarSum of the layer.tar first + // skip this step for now. Just push the layer every time for this naive implementation + //shouldPush, err := r.PostV2ImageMountBlob(imageName, sumType, sum string, token []string) + + img, err := image.NewImgJSON(imgJSON) + if err != nil { + return job.Errorf("Failed to parse json: %s", err) + } + + img, err = s.graph.Get(img.ID) + if err != nil { + return job.Error(err) + } + + arch, err := img.TarLayer() + if err != nil { + return job.Errorf("Could not get tar layer: %s", err) + } + + _, err = r.PutV2ImageBlob(remoteName, sumParts[0], manifestSum, utils.ProgressReader(arch, int(img.Size), job.Stdout, sf, false, utils.TruncateID(img.ID), "Pushing"), repoData.Tokens) + if err != nil { + job.Stdout.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Image push failed", nil)) + return job.Error(err) + } + job.Stdout.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Image successfully pushed", nil)) + } + + // push the manifest + err = r.PutV2ImageManifest(remoteName, tag, bytes.NewReader([]byte(manifestBytes)), repoData.Tokens) + if err != nil { + return job.Error(err) + } + + // done, no fallback to V1 + return engine.StatusOK + } + if err != nil { reposLen := 1 if tag == "" { diff --git a/graph/service.go b/graph/service.go index 2858d9b3e..675e12a1a 100644 --- a/graph/service.go +++ b/graph/service.go @@ -25,6 +25,7 @@ func (s *TagStore) Install(eng *engine.Engine) error { "import": s.CmdImport, "pull": s.CmdPull, "push": s.CmdPush, + "image_manifest": s.CmdManifest, } { if err := eng.Register(name, handler); err != nil { return fmt.Errorf("Could not register %q: %v", name, err) diff --git a/registry/session_v2.go b/registry/session_v2.go index 20e9e2ee9..0498bf702 100644 --- a/registry/session_v2.go +++ b/registry/session_v2.go @@ -267,7 +267,7 @@ func (r *Session) GetV2ImageBlobReader(imageName, sumType, sum string, token []s // Push the image to the server for storage. // 'layer' is an uncompressed reader of the blob to be pushed. // The server will generate it's own checksum calculation. -func (r *Session) PutV2ImageBlob(imageName, sumType string, blobRdr io.Reader, token []string) (serverChecksum string, err error) { +func (r *Session) PutV2ImageBlob(imageName, sumType, sumStr string, blobRdr io.Reader, token []string) (serverChecksum string, err error) { vars := map[string]string{ "imagename": imageName, "sumtype": sumType, @@ -285,6 +285,7 @@ func (r *Session) PutV2ImageBlob(imageName, sumType string, blobRdr io.Reader, t return "", err } setTokenAuth(req, token) + req.Header.Set("X-Tarsum", sumStr) res, _, err := r.doRequest(req) if err != nil { return "", err @@ -309,6 +310,10 @@ func (r *Session) PutV2ImageBlob(imageName, sumType string, blobRdr io.Reader, t return "", fmt.Errorf("unable to decode PutV2ImageBlob JSON response: %s", err) } + if sumInfo.Checksum != sumStr { + return "", fmt.Errorf("failed checksum comparison. serverChecksum: %q, localChecksum: %q", sumInfo.Checksum, sumStr) + } + // XXX this is a json struct from the registry, with its checksum return sumInfo.Checksum, nil } From bcc0a343bb9c75443238e614e4c2da5f707aef8d Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Thu, 9 Oct 2014 17:34:52 -0700 Subject: [PATCH 294/513] Update manifest format for push Signed-off-by: Derek McGowan (github: dmcgowan) --- graph/manifest.go | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/graph/manifest.go b/graph/manifest.go index 39cabb6e4..752b78230 100644 --- a/graph/manifest.go +++ b/graph/manifest.go @@ -43,7 +43,6 @@ func (s *TagStore) CmdManifest(job *engine.Job) engine.Status { if !exists { return job.Errorf("Tag does not exist for %s: %s", name, tag) } - tarsums := make([]string, 0, 4) layersSeen := make(map[string]bool) layer, err := s.graph.Get(layerId) @@ -54,9 +53,10 @@ func (s *TagStore) CmdManifest(job *engine.Job) engine.Status { return job.Errorf("Missing layer configuration") } manifest.Architecture = layer.Architecture + manifest.FSLayers = make([]*registry.FSLayer, 0, 4) + manifest.History = make([]*registry.ManifestHistory, 0, 4) var metadata runconfig.Config metadata = *layer.Config - history := make([]string, 0, cap(tarsums)) for ; layer != nil; layer, err = layer.GetParent() { if err != nil { @@ -89,19 +89,16 @@ func (s *TagStore) CmdManifest(job *engine.Job) engine.Status { tarId := tarSum.Sum(nil) // Save tarsum to image json - tarsums = append(tarsums, tarId) + manifest.FSLayers = append(manifest.FSLayers, ®istry.FSLayer{BlobSum: tarId}) layersSeen[layer.ID] = true jsonData, err := ioutil.ReadFile(path.Join(s.graph.Root, layer.ID, "json")) if err != nil { return job.Error(fmt.Errorf("Cannot retrieve the path for {%s}: %s", layer.ID, err)) } - history = append(history, string(jsonData)) + manifest.History = append(manifest.History, ®istry.ManifestHistory{V1Compatibility: string(jsonData)}) } - manifest.BlobSums = tarsums - manifest.History = history - manifestBytes, err := json.MarshalIndent(manifest, "", " ") if err != nil { return job.Error(err) From 3e4fd005449448ab85c917a5d27ca584b260309c Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Thu, 9 Oct 2014 17:32:16 -0700 Subject: [PATCH 295/513] Use tarsum dev version to fix mtime issue Signed-off-by: Derek McGowan (github: dmcgowan) --- graph/manifest.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graph/manifest.go b/graph/manifest.go index 752b78230..ddcb22b65 100644 --- a/graph/manifest.go +++ b/graph/manifest.go @@ -78,7 +78,7 @@ func (s *TagStore) CmdManifest(job *engine.Job) engine.Status { return job.Error(err) } - tarSum, err := tarsum.NewTarSum(archive, true, tarsum.Version0) + tarSum, err := tarsum.NewTarSum(archive, true, tarsum.VersionDev) if err != nil { return job.Error(err) } From e9b590d85e9c622316b8be71004737f63e6b9503 Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Fri, 14 Nov 2014 16:22:06 -0800 Subject: [PATCH 296/513] Update push to use mount blob endpoint Using mount blob prevents repushing images which have already been uploaded Signed-off-by: Derek McGowan (github: dmcgowan) --- graph/push.go | 14 ++++++++++++-- registry/session_v2.go | 4 ++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/graph/push.go b/graph/push.go index e68886f9a..64c1f7c61 100644 --- a/graph/push.go +++ b/graph/push.go @@ -358,12 +358,22 @@ func (s *TagStore) CmdPush(job *engine.Job) engine.Status { return job.Errorf("Could not get tar layer: %s", err) } - _, err = r.PutV2ImageBlob(remoteName, sumParts[0], manifestSum, utils.ProgressReader(arch, int(img.Size), job.Stdout, sf, false, utils.TruncateID(img.ID), "Pushing"), repoData.Tokens) + // Call mount blob + exists, err := r.PostV2ImageMountBlob(remoteName, sumParts[0], manifestSum, repoData.Tokens) if err != nil { job.Stdout.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Image push failed", nil)) return job.Error(err) } - job.Stdout.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Image successfully pushed", nil)) + if !exists { + _, err = r.PutV2ImageBlob(remoteName, sumParts[0], manifestSum, utils.ProgressReader(arch, int(img.Size), job.Stdout, sf, false, utils.TruncateID(img.ID), "Pushing"), repoData.Tokens) + if err != nil { + job.Stdout.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Image push failed", nil)) + return job.Error(err) + } + job.Stdout.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Image successfully pushed", nil)) + } else { + job.Stdout.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Image already exists", nil)) + } } // push the manifest diff --git a/registry/session_v2.go b/registry/session_v2.go index 0498bf702..86d0c228a 100644 --- a/registry/session_v2.go +++ b/registry/session_v2.go @@ -34,7 +34,7 @@ func newV2RegistryRouter() *mux.Router { v2Router.Path("/blob/{imagename:[a-z0-9-._/]+}/{sumtype:[a-z0-9._+-]+}").Name("uploadBlob") // Mounting a blob in an image - v2Router.Path("/mountblob/{imagename:[a-z0-9-._/]+}/{sumtype:[a-z0-9._+-]+}/{sum:[a-fA-F0-9]{4,}}").Name("mountBlob") + v2Router.Path("/blob/{imagename:[a-z0-9-._/]+}/{sumtype:[a-z0-9._+-]+}/{sum:[a-fA-F0-9]{4,}}").Name("mountBlob") return router } @@ -184,7 +184,7 @@ func (r *Session) PostV2ImageMountBlob(imageName, sumType, sum string, token []s case 200: // return something indicating no push needed return true, nil - case 300: + case 404: // return something indicating blob push needed return false, nil } From e23362597dcaa8839271210d24bda2ba55f1e12f Mon Sep 17 00:00:00 2001 From: Josh Hawn Date: Fri, 12 Dec 2014 13:30:12 -0800 Subject: [PATCH 297/513] Update token response handling Registry authorization token is now taken from the response body rather than the repsonse header. Docker-DCO-1.1-Signed-off-by: Josh Hawn (github: jlhawn) --- registry/token.go | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/registry/token.go b/registry/token.go index 0403734f8..250486304 100644 --- a/registry/token.go +++ b/registry/token.go @@ -1,6 +1,7 @@ package registry import ( + "encoding/json" "errors" "fmt" "net/http" @@ -10,6 +11,10 @@ import ( "github.com/docker/docker/utils" ) +type tokenResponse struct { + Token string `json:"token"` +} + func getToken(username, password string, params map[string]string, registryEndpoint *Endpoint, client *http.Client, factory *utils.HTTPRequestFactory) (token string, err error) { realm, ok := params["realm"] if !ok { @@ -57,14 +62,20 @@ func getToken(username, password string, params map[string]string, registryEndpo } defer resp.Body.Close() - if !(resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusNoContent) { + if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("token auth attempt for registry %s: %s request failed with status: %d %s", registryEndpoint, req.URL, resp.StatusCode, http.StatusText(resp.StatusCode)) } - token = resp.Header.Get("X-Auth-Token") - if token == "" { - return "", errors.New("token server did not include a token in the response header") + decoder := json.NewDecoder(resp.Body) + + tr := new(tokenResponse) + if err = decoder.Decode(tr); err != nil { + return "", fmt.Errorf("unable to decode token response: %s", err) } - return token, nil + if tr.Token == "" { + return "", errors.New("authorization server did not include a token in the response") + } + + return tr.Token, nil } From a0f92a26d90e870dfddae9694a5de97e36d3f586 Mon Sep 17 00:00:00 2001 From: Stephen J Day Date: Fri, 12 Dec 2014 11:27:22 -0800 Subject: [PATCH 298/513] Registry V2 HTTP route and error code definitions This package, ported from next-generation docker regsitry, includes route and error definitions. These facilitate compliant V2 client implementation. The portions of the HTTP API that are included in this package are considered to be locked down and should only be changed through a careful change proposal. Descriptor definitions package layout may change without affecting API behavior until the exported Go API is ready to be locked down. When the new registry stabilizes and becomes the master branch, this package can be vendored from the registry. Signed-off-by: Stephen J Day --- registry/v2/descriptors.go | 144 +++++++++++++++++++++++++++++ registry/v2/doc.go | 13 +++ registry/v2/errors.go | 185 +++++++++++++++++++++++++++++++++++++ registry/v2/errors_test.go | 165 +++++++++++++++++++++++++++++++++ registry/v2/routes.go | 69 ++++++++++++++ registry/v2/routes_test.go | 184 ++++++++++++++++++++++++++++++++++++ registry/v2/urls.go | 165 +++++++++++++++++++++++++++++++++ registry/v2/urls_test.go | 100 ++++++++++++++++++++ 8 files changed, 1025 insertions(+) create mode 100644 registry/v2/descriptors.go create mode 100644 registry/v2/doc.go create mode 100644 registry/v2/errors.go create mode 100644 registry/v2/errors_test.go create mode 100644 registry/v2/routes.go create mode 100644 registry/v2/routes_test.go create mode 100644 registry/v2/urls.go create mode 100644 registry/v2/urls_test.go diff --git a/registry/v2/descriptors.go b/registry/v2/descriptors.go new file mode 100644 index 000000000..68d182411 --- /dev/null +++ b/registry/v2/descriptors.go @@ -0,0 +1,144 @@ +package v2 + +import "net/http" + +// TODO(stevvooe): Add route descriptors for each named route, along with +// accepted methods, parameters, returned status codes and error codes. + +// ErrorDescriptor provides relevant information about a given error code. +type ErrorDescriptor struct { + // Code is the error code that this descriptor describes. + Code ErrorCode + + // Value provides a unique, string key, often captilized with + // underscores, to identify the error code. This value is used as the + // keyed value when serializing api errors. + Value string + + // Message is a short, human readable decription of the error condition + // included in API responses. + Message string + + // Description provides a complete account of the errors purpose, suitable + // for use in documentation. + Description string + + // HTTPStatusCodes provides a list of status under which this error + // condition may arise. If it is empty, the error condition may be seen + // for any status code. + HTTPStatusCodes []int +} + +// ErrorDescriptors provides a list of HTTP API Error codes that may be +// encountered when interacting with the registry API. +var ErrorDescriptors = []ErrorDescriptor{ + { + Code: ErrorCodeUnknown, + Value: "UNKNOWN", + Message: "unknown error", + Description: `Generic error returned when the error does not have an + API classification.`, + }, + { + Code: ErrorCodeDigestInvalid, + Value: "DIGEST_INVALID", + Message: "provided digest did not match uploaded content", + Description: `When a blob is uploaded, the registry will check that + the content matches the digest provided by the client. The error may + include a detail structure with the key "digest", including the + invalid digest string. This error may also be returned when a manifest + includes an invalid layer digest.`, + HTTPStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound}, + }, + { + Code: ErrorCodeSizeInvalid, + Value: "SIZE_INVALID", + Message: "provided length did not match content length", + Description: `When a layer is uploaded, the provided size will be + checked against the uploaded content. If they do not match, this error + will be returned.`, + HTTPStatusCodes: []int{http.StatusBadRequest}, + }, + { + Code: ErrorCodeNameInvalid, + Value: "NAME_INVALID", + Message: "manifest name did not match URI", + Description: `During a manifest upload, if the name in the manifest + does not match the uri name, this error will be returned.`, + HTTPStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound}, + }, + { + Code: ErrorCodeTagInvalid, + Value: "TAG_INVALID", + Message: "manifest tag did not match URI", + Description: `During a manifest upload, if the tag in the manifest + does not match the uri tag, this error will be returned.`, + HTTPStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound}, + }, + { + Code: ErrorCodeNameUnknown, + Value: "NAME_UNKNOWN", + Message: "repository name not known to registry", + Description: `This is returned if the name used during an operation is + unknown to the registry.`, + HTTPStatusCodes: []int{http.StatusNotFound}, + }, + { + Code: ErrorCodeManifestUnknown, + Value: "MANIFEST_UNKNOWN", + Message: "manifest unknown", + Description: `This error is returned when the manifest, identified by + name and tag is unknown to the repository.`, + HTTPStatusCodes: []int{http.StatusNotFound}, + }, + { + Code: ErrorCodeManifestInvalid, + Value: "MANIFEST_INVALID", + Message: "manifest invalid", + Description: `During upload, manifests undergo several checks ensuring + validity. If those checks fail, this error may be returned, unless a + more specific error is included. The detail will contain information + the failed validation.`, + HTTPStatusCodes: []int{http.StatusBadRequest}, + }, + { + Code: ErrorCodeManifestUnverified, + Value: "MANIFEST_UNVERIFIED", + Message: "manifest failed signature verification", + Description: `During manifest upload, if the manifest fails signature + verification, this error will be returned.`, + HTTPStatusCodes: []int{http.StatusBadRequest}, + }, + { + Code: ErrorCodeBlobUnknown, + Value: "BLOB_UNKNOWN", + Message: "blob unknown to registry", + Description: `This error may be returned when a blob is unknown to the + registry in a specified repository. This can be returned with a + standard get or if a manifest references an unknown layer during + upload.`, + HTTPStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound}, + }, + + { + Code: ErrorCodeBlobUploadUnknown, + Value: "BLOB_UPLOAD_UNKNOWN", + Message: "blob upload unknown to registry", + Description: `If a blob upload has been cancelled or was never + started, this error code may be returned.`, + HTTPStatusCodes: []int{http.StatusNotFound}, + }, +} + +var errorCodeToDescriptors map[ErrorCode]ErrorDescriptor +var idToDescriptors map[string]ErrorDescriptor + +func init() { + errorCodeToDescriptors = make(map[ErrorCode]ErrorDescriptor, len(ErrorDescriptors)) + idToDescriptors = make(map[string]ErrorDescriptor, len(ErrorDescriptors)) + + for _, descriptor := range ErrorDescriptors { + errorCodeToDescriptors[descriptor.Code] = descriptor + idToDescriptors[descriptor.Value] = descriptor + } +} diff --git a/registry/v2/doc.go b/registry/v2/doc.go new file mode 100644 index 000000000..30fe2271a --- /dev/null +++ b/registry/v2/doc.go @@ -0,0 +1,13 @@ +// Package v2 describes routes, urls and the error codes used in the Docker +// Registry JSON HTTP API V2. In addition to declarations, descriptors are +// provided for routes and error codes that can be used for implementation and +// automatically generating documentation. +// +// Definitions here are considered to be locked down for the V2 registry api. +// Any changes must be considered carefully and should not proceed without a +// change proposal. +// +// Currently, while the HTTP API definitions are considered stable, the Go API +// exports are considered unstable. Go API consumers should take care when +// relying on these definitions until this message is deleted. +package v2 diff --git a/registry/v2/errors.go b/registry/v2/errors.go new file mode 100644 index 000000000..8c85d3a97 --- /dev/null +++ b/registry/v2/errors.go @@ -0,0 +1,185 @@ +package v2 + +import ( + "fmt" + "strings" +) + +// ErrorCode represents the error type. The errors are serialized via strings +// and the integer format may change and should *never* be exported. +type ErrorCode int + +const ( + // ErrorCodeUnknown is a catch-all for errors not defined below. + ErrorCodeUnknown ErrorCode = iota + + // ErrorCodeDigestInvalid is returned when uploading a blob if the + // provided digest does not match the blob contents. + ErrorCodeDigestInvalid + + // ErrorCodeSizeInvalid is returned when uploading a blob if the provided + // size does not match the content length. + ErrorCodeSizeInvalid + + // ErrorCodeNameInvalid is returned when the name in the manifest does not + // match the provided name. + ErrorCodeNameInvalid + + // ErrorCodeTagInvalid is returned when the tag in the manifest does not + // match the provided tag. + ErrorCodeTagInvalid + + // ErrorCodeNameUnknown when the repository name is not known. + ErrorCodeNameUnknown + + // ErrorCodeManifestUnknown returned when image manifest is unknown. + ErrorCodeManifestUnknown + + // ErrorCodeManifestInvalid returned when an image manifest is invalid, + // typically during a PUT operation. This error encompasses all errors + // encountered during manifest validation that aren't signature errors. + ErrorCodeManifestInvalid + + // ErrorCodeManifestUnverified is returned when the manifest fails + // signature verfication. + ErrorCodeManifestUnverified + + // ErrorCodeBlobUnknown is returned when a blob is unknown to the + // registry. This can happen when the manifest references a nonexistent + // layer or the result is not found by a blob fetch. + ErrorCodeBlobUnknown + + // ErrorCodeBlobUploadUnknown is returned when an upload is unknown. + ErrorCodeBlobUploadUnknown +) + +// ParseErrorCode attempts to parse the error code string, returning +// ErrorCodeUnknown if the error is not known. +func ParseErrorCode(s string) ErrorCode { + desc, ok := idToDescriptors[s] + + if !ok { + return ErrorCodeUnknown + } + + return desc.Code +} + +// Descriptor returns the descriptor for the error code. +func (ec ErrorCode) Descriptor() ErrorDescriptor { + d, ok := errorCodeToDescriptors[ec] + + if !ok { + return ErrorCodeUnknown.Descriptor() + } + + return d +} + +// String returns the canonical identifier for this error code. +func (ec ErrorCode) String() string { + return ec.Descriptor().Value +} + +// Message returned the human-readable error message for this error code. +func (ec ErrorCode) Message() string { + return ec.Descriptor().Message +} + +// MarshalText encodes the receiver into UTF-8-encoded text and returns the +// result. +func (ec ErrorCode) MarshalText() (text []byte, err error) { + return []byte(ec.String()), nil +} + +// UnmarshalText decodes the form generated by MarshalText. +func (ec *ErrorCode) UnmarshalText(text []byte) error { + desc, ok := idToDescriptors[string(text)] + + if !ok { + desc = ErrorCodeUnknown.Descriptor() + } + + *ec = desc.Code + + return nil +} + +// Error provides a wrapper around ErrorCode with extra Details provided. +type Error struct { + Code ErrorCode `json:"code"` + Message string `json:"message,omitempty"` + Detail interface{} `json:"detail,omitempty"` +} + +// Error returns a human readable representation of the error. +func (e Error) Error() string { + return fmt.Sprintf("%s: %s", + strings.ToLower(strings.Replace(e.Code.String(), "_", " ", -1)), + e.Message) +} + +// Errors provides the envelope for multiple errors and a few sugar methods +// for use within the application. +type Errors struct { + Errors []Error `json:"errors,omitempty"` +} + +// Push pushes an error on to the error stack, with the optional detail +// argument. It is a programming error (ie panic) to push more than one +// detail at a time. +func (errs *Errors) Push(code ErrorCode, details ...interface{}) { + if len(details) > 1 { + panic("please specify zero or one detail items for this error") + } + + var detail interface{} + if len(details) > 0 { + detail = details[0] + } + + if err, ok := detail.(error); ok { + detail = err.Error() + } + + errs.PushErr(Error{ + Code: code, + Message: code.Message(), + Detail: detail, + }) +} + +// PushErr pushes an error interface onto the error stack. +func (errs *Errors) PushErr(err error) { + switch err.(type) { + case Error: + errs.Errors = append(errs.Errors, err.(Error)) + default: + errs.Errors = append(errs.Errors, Error{Message: err.Error()}) + } +} + +func (errs *Errors) Error() string { + switch errs.Len() { + case 0: + return "" + case 1: + return errs.Errors[0].Error() + default: + msg := "errors:\n" + for _, err := range errs.Errors { + msg += err.Error() + "\n" + } + return msg + } +} + +// Clear clears the errors. +func (errs *Errors) Clear() { + errs.Errors = errs.Errors[:0] +} + +// Len returns the current number of errors. +func (errs *Errors) Len() int { + return len(errs.Errors) +} diff --git a/registry/v2/errors_test.go b/registry/v2/errors_test.go new file mode 100644 index 000000000..d2fc091ac --- /dev/null +++ b/registry/v2/errors_test.go @@ -0,0 +1,165 @@ +package v2 + +import ( + "encoding/json" + "reflect" + "testing" + + "github.com/docker/docker-registry/digest" +) + +// TestErrorCodes ensures that error code format, mappings and +// marshaling/unmarshaling. round trips are stable. +func TestErrorCodes(t *testing.T) { + for _, desc := range ErrorDescriptors { + if desc.Code.String() != desc.Value { + t.Fatalf("error code string incorrect: %q != %q", desc.Code.String(), desc.Value) + } + + if desc.Code.Message() != desc.Message { + t.Fatalf("incorrect message for error code %v: %q != %q", desc.Code, desc.Code.Message(), desc.Message) + } + + // Serialize the error code using the json library to ensure that we + // get a string and it works round trip. + p, err := json.Marshal(desc.Code) + + if err != nil { + t.Fatalf("error marshaling error code %v: %v", desc.Code, err) + } + + if len(p) <= 0 { + t.Fatalf("expected content in marshaled before for error code %v", desc.Code) + } + + // First, unmarshal to interface and ensure we have a string. + var ecUnspecified interface{} + if err := json.Unmarshal(p, &ecUnspecified); err != nil { + t.Fatalf("error unmarshaling error code %v: %v", desc.Code, err) + } + + if _, ok := ecUnspecified.(string); !ok { + t.Fatalf("expected a string for error code %v on unmarshal got a %T", desc.Code, ecUnspecified) + } + + // Now, unmarshal with the error code type and ensure they are equal + var ecUnmarshaled ErrorCode + if err := json.Unmarshal(p, &ecUnmarshaled); err != nil { + t.Fatalf("error unmarshaling error code %v: %v", desc.Code, err) + } + + if ecUnmarshaled != desc.Code { + t.Fatalf("unexpected error code during error code marshal/unmarshal: %v != %v", ecUnmarshaled, desc.Code) + } + } +} + +// TestErrorsManagement does a quick check of the Errors type to ensure that +// members are properly pushed and marshaled. +func TestErrorsManagement(t *testing.T) { + var errs Errors + + errs.Push(ErrorCodeDigestInvalid) + errs.Push(ErrorCodeBlobUnknown, + map[string]digest.Digest{"digest": "sometestblobsumdoesntmatter"}) + + p, err := json.Marshal(errs) + + if err != nil { + t.Fatalf("error marashaling errors: %v", err) + } + + expectedJSON := "{\"errors\":[{\"code\":\"DIGEST_INVALID\",\"message\":\"provided digest did not match uploaded content\"},{\"code\":\"BLOB_UNKNOWN\",\"message\":\"blob unknown to registry\",\"detail\":{\"digest\":\"sometestblobsumdoesntmatter\"}}]}" + + if string(p) != expectedJSON { + t.Fatalf("unexpected json: %q != %q", string(p), expectedJSON) + } + + errs.Clear() + errs.Push(ErrorCodeUnknown) + expectedJSON = "{\"errors\":[{\"code\":\"UNKNOWN\",\"message\":\"unknown error\"}]}" + p, err = json.Marshal(errs) + + if err != nil { + t.Fatalf("error marashaling errors: %v", err) + } + + if string(p) != expectedJSON { + t.Fatalf("unexpected json: %q != %q", string(p), expectedJSON) + } +} + +// TestMarshalUnmarshal ensures that api errors can round trip through json +// without losing information. +func TestMarshalUnmarshal(t *testing.T) { + + var errors Errors + + for _, testcase := range []struct { + description string + err Error + }{ + { + description: "unknown error", + err: Error{ + + Code: ErrorCodeUnknown, + Message: ErrorCodeUnknown.Descriptor().Message, + }, + }, + { + description: "unknown manifest", + err: Error{ + Code: ErrorCodeManifestUnknown, + Message: ErrorCodeManifestUnknown.Descriptor().Message, + }, + }, + { + description: "unknown manifest", + err: Error{ + Code: ErrorCodeBlobUnknown, + Message: ErrorCodeBlobUnknown.Descriptor().Message, + Detail: map[string]interface{}{"digest": "asdfqwerqwerqwerqwer"}, + }, + }, + } { + fatalf := func(format string, args ...interface{}) { + t.Fatalf(testcase.description+": "+format, args...) + } + + unexpectedErr := func(err error) { + fatalf("unexpected error: %v", err) + } + + p, err := json.Marshal(testcase.err) + if err != nil { + unexpectedErr(err) + } + + var unmarshaled Error + if err := json.Unmarshal(p, &unmarshaled); err != nil { + unexpectedErr(err) + } + + if !reflect.DeepEqual(unmarshaled, testcase.err) { + fatalf("errors not equal after round trip: %#v != %#v", unmarshaled, testcase.err) + } + + // Roll everything up into an error response envelope. + errors.PushErr(testcase.err) + } + + p, err := json.Marshal(errors) + if err != nil { + t.Fatalf("unexpected error marshaling error envelope: %v", err) + } + + var unmarshaled Errors + if err := json.Unmarshal(p, &unmarshaled); err != nil { + t.Fatalf("unexpected error unmarshaling error envelope: %v", err) + } + + if !reflect.DeepEqual(unmarshaled, errors) { + t.Fatalf("errors not equal after round trip: %#v != %#v", unmarshaled, errors) + } +} diff --git a/registry/v2/routes.go b/registry/v2/routes.go new file mode 100644 index 000000000..7ebe61d66 --- /dev/null +++ b/registry/v2/routes.go @@ -0,0 +1,69 @@ +package v2 + +import ( + "github.com/docker/docker-registry/common" + "github.com/gorilla/mux" +) + +// The following are definitions of the name under which all V2 routes are +// registered. These symbols can be used to look up a route based on the name. +const ( + RouteNameBase = "base" + RouteNameManifest = "manifest" + RouteNameTags = "tags" + RouteNameBlob = "blob" + RouteNameBlobUpload = "blob-upload" + RouteNameBlobUploadChunk = "blob-upload-chunk" +) + +var allEndpoints = []string{ + RouteNameManifest, + RouteNameTags, + RouteNameBlob, + RouteNameBlobUpload, + RouteNameBlobUploadChunk, +} + +// Router builds a gorilla router with named routes for the various API +// methods. This can be used directly by both server implementations and +// clients. +func Router() *mux.Router { + router := mux.NewRouter(). + StrictSlash(true) + + // GET /v2/ Check Check that the registry implements API version 2(.1) + router. + Path("/v2/"). + Name(RouteNameBase) + + // GET /v2//manifest/ Image Manifest Fetch the image manifest identified by name and tag. + // PUT /v2//manifest/ Image Manifest Upload the image manifest identified by name and tag. + // DELETE /v2//manifest/ Image Manifest Delete the image identified by name and tag. + router. + Path("/v2/{name:" + common.RepositoryNameRegexp.String() + "}/manifests/{tag:" + common.TagNameRegexp.String() + "}"). + Name(RouteNameManifest) + + // GET /v2//tags/list Tags Fetch the tags under the repository identified by name. + router. + Path("/v2/{name:" + common.RepositoryNameRegexp.String() + "}/tags/list"). + Name(RouteNameTags) + + // GET /v2//blob/ Layer Fetch the blob identified by digest. + router. + Path("/v2/{name:" + common.RepositoryNameRegexp.String() + "}/blobs/{digest:[a-zA-Z0-9-_+.]+:[a-zA-Z0-9-_+.=]+}"). + Name(RouteNameBlob) + + // POST /v2//blob/upload/ Layer Upload Initiate an upload of the layer identified by tarsum. + router. + Path("/v2/{name:" + common.RepositoryNameRegexp.String() + "}/blobs/uploads/"). + Name(RouteNameBlobUpload) + + // GET /v2//blob/upload/ Layer Upload Get the status of the upload identified by tarsum and uuid. + // PUT /v2//blob/upload/ Layer Upload Upload all or a chunk of the upload identified by tarsum and uuid. + // DELETE /v2//blob/upload/ Layer Upload Cancel the upload identified by layer and uuid + router. + Path("/v2/{name:" + common.RepositoryNameRegexp.String() + "}/blobs/uploads/{uuid}"). + Name(RouteNameBlobUploadChunk) + + return router +} diff --git a/registry/v2/routes_test.go b/registry/v2/routes_test.go new file mode 100644 index 000000000..9969ebcc4 --- /dev/null +++ b/registry/v2/routes_test.go @@ -0,0 +1,184 @@ +package v2 + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "reflect" + "testing" + + "github.com/gorilla/mux" +) + +type routeTestCase struct { + RequestURI string + Vars map[string]string + RouteName string + StatusCode int +} + +// TestRouter registers a test handler with all the routes and ensures that +// each route returns the expected path variables. Not method verification is +// present. This not meant to be exhaustive but as check to ensure that the +// expected variables are extracted. +// +// This may go away as the application structure comes together. +func TestRouter(t *testing.T) { + + router := Router() + + testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + testCase := routeTestCase{ + RequestURI: r.RequestURI, + Vars: mux.Vars(r), + RouteName: mux.CurrentRoute(r).GetName(), + } + + enc := json.NewEncoder(w) + + if err := enc.Encode(testCase); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) + + // Startup test server + server := httptest.NewServer(router) + + for _, testcase := range []routeTestCase{ + { + RouteName: RouteNameBase, + RequestURI: "/v2/", + Vars: map[string]string{}, + }, + { + RouteName: RouteNameManifest, + RequestURI: "/v2/foo/bar/manifests/tag", + Vars: map[string]string{ + "name": "foo/bar", + "tag": "tag", + }, + }, + { + RouteName: RouteNameTags, + RequestURI: "/v2/foo/bar/tags/list", + Vars: map[string]string{ + "name": "foo/bar", + }, + }, + { + RouteName: RouteNameBlob, + RequestURI: "/v2/foo/bar/blobs/tarsum.dev+foo:abcdef0919234", + Vars: map[string]string{ + "name": "foo/bar", + "digest": "tarsum.dev+foo:abcdef0919234", + }, + }, + { + RouteName: RouteNameBlob, + RequestURI: "/v2/foo/bar/blobs/sha256:abcdef0919234", + Vars: map[string]string{ + "name": "foo/bar", + "digest": "sha256:abcdef0919234", + }, + }, + { + RouteName: RouteNameBlobUpload, + RequestURI: "/v2/foo/bar/blobs/uploads/", + Vars: map[string]string{ + "name": "foo/bar", + }, + }, + { + RouteName: RouteNameBlobUploadChunk, + RequestURI: "/v2/foo/bar/blobs/uploads/uuid", + Vars: map[string]string{ + "name": "foo/bar", + "uuid": "uuid", + }, + }, + { + RouteName: RouteNameBlobUploadChunk, + RequestURI: "/v2/foo/bar/blobs/uploads/D95306FA-FAD3-4E36-8D41-CF1C93EF8286", + Vars: map[string]string{ + "name": "foo/bar", + "uuid": "D95306FA-FAD3-4E36-8D41-CF1C93EF8286", + }, + }, + { + RouteName: RouteNameBlobUploadChunk, + RequestURI: "/v2/foo/bar/blobs/uploads/RDk1MzA2RkEtRkFEMy00RTM2LThENDEtQ0YxQzkzRUY4Mjg2IA==", + Vars: map[string]string{ + "name": "foo/bar", + "uuid": "RDk1MzA2RkEtRkFEMy00RTM2LThENDEtQ0YxQzkzRUY4Mjg2IA==", + }, + }, + { + // Check ambiguity: ensure we can distinguish between tags for + // "foo/bar/image/image" and image for "foo/bar/image" with tag + // "tags" + RouteName: RouteNameManifest, + RequestURI: "/v2/foo/bar/manifests/manifests/tags", + Vars: map[string]string{ + "name": "foo/bar/manifests", + "tag": "tags", + }, + }, + { + // This case presents an ambiguity between foo/bar with tag="tags" + // and list tags for "foo/bar/manifest" + RouteName: RouteNameTags, + RequestURI: "/v2/foo/bar/manifests/tags/list", + Vars: map[string]string{ + "name": "foo/bar/manifests", + }, + }, + { + RouteName: RouteNameBlobUploadChunk, + RequestURI: "/v2/foo/../../blob/uploads/D95306FA-FAD3-4E36-8D41-CF1C93EF8286", + StatusCode: http.StatusNotFound, + }, + } { + // Register the endpoint + router.GetRoute(testcase.RouteName).Handler(testHandler) + u := server.URL + testcase.RequestURI + + resp, err := http.Get(u) + + if err != nil { + t.Fatalf("error issuing get request: %v", err) + } + + if testcase.StatusCode == 0 { + // Override default, zero-value + testcase.StatusCode = http.StatusOK + } + + if resp.StatusCode != testcase.StatusCode { + t.Fatalf("unexpected status for %s: %v %v", u, resp.Status, resp.StatusCode) + } + + if testcase.StatusCode != http.StatusOK { + // We don't care about json response. + continue + } + + dec := json.NewDecoder(resp.Body) + + var actualRouteInfo routeTestCase + if err := dec.Decode(&actualRouteInfo); err != nil { + t.Fatalf("error reading json response: %v", err) + } + // Needs to be set out of band + actualRouteInfo.StatusCode = resp.StatusCode + + if actualRouteInfo.RouteName != testcase.RouteName { + t.Fatalf("incorrect route %q matched, expected %q", actualRouteInfo.RouteName, testcase.RouteName) + } + + if !reflect.DeepEqual(actualRouteInfo, testcase) { + t.Fatalf("actual does not equal expected: %#v != %#v", actualRouteInfo, testcase) + } + } + +} diff --git a/registry/v2/urls.go b/registry/v2/urls.go new file mode 100644 index 000000000..72f44299a --- /dev/null +++ b/registry/v2/urls.go @@ -0,0 +1,165 @@ +package v2 + +import ( + "net/http" + "net/url" + + "github.com/docker/docker-registry/digest" + "github.com/gorilla/mux" +) + +// URLBuilder creates registry API urls from a single base endpoint. It can be +// used to create urls for use in a registry client or server. +// +// All urls will be created from the given base, including the api version. +// For example, if a root of "/foo/" is provided, urls generated will be fall +// under "/foo/v2/...". Most application will only provide a schema, host and +// port, such as "https://localhost:5000/". +type URLBuilder struct { + root *url.URL // url root (ie http://localhost/) + router *mux.Router +} + +// NewURLBuilder creates a URLBuilder with provided root url object. +func NewURLBuilder(root *url.URL) *URLBuilder { + return &URLBuilder{ + root: root, + router: Router(), + } +} + +// NewURLBuilderFromString workes identically to NewURLBuilder except it takes +// a string argument for the root, returning an error if it is not a valid +// url. +func NewURLBuilderFromString(root string) (*URLBuilder, error) { + u, err := url.Parse(root) + if err != nil { + return nil, err + } + + return NewURLBuilder(u), nil +} + +// NewURLBuilderFromRequest uses information from an *http.Request to +// construct the root url. +func NewURLBuilderFromRequest(r *http.Request) *URLBuilder { + u := &url.URL{ + Scheme: r.URL.Scheme, + Host: r.Host, + } + + return NewURLBuilder(u) +} + +// BuildBaseURL constructs a base url for the API, typically just "/v2/". +func (ub *URLBuilder) BuildBaseURL() (string, error) { + route := ub.cloneRoute(RouteNameBase) + + baseURL, err := route.URL() + if err != nil { + return "", err + } + + return baseURL.String(), nil +} + +// BuildTagsURL constructs a url to list the tags in the named repository. +func (ub *URLBuilder) BuildTagsURL(name string) (string, error) { + route := ub.cloneRoute(RouteNameTags) + + tagsURL, err := route.URL("name", name) + if err != nil { + return "", err + } + + return tagsURL.String(), nil +} + +// BuildManifestURL constructs a url for the manifest identified by name and tag. +func (ub *URLBuilder) BuildManifestURL(name, tag string) (string, error) { + route := ub.cloneRoute(RouteNameManifest) + + manifestURL, err := route.URL("name", name, "tag", tag) + if err != nil { + return "", err + } + + return manifestURL.String(), nil +} + +// BuildBlobURL constructs the url for the blob identified by name and dgst. +func (ub *URLBuilder) BuildBlobURL(name string, dgst digest.Digest) (string, error) { + route := ub.cloneRoute(RouteNameBlob) + + layerURL, err := route.URL("name", name, "digest", dgst.String()) + if err != nil { + return "", err + } + + return layerURL.String(), nil +} + +// BuildBlobUploadURL constructs a url to begin a blob upload in the +// repository identified by name. +func (ub *URLBuilder) BuildBlobUploadURL(name string, values ...url.Values) (string, error) { + route := ub.cloneRoute(RouteNameBlobUpload) + + uploadURL, err := route.URL("name", name) + if err != nil { + return "", err + } + + return appendValuesURL(uploadURL, values...).String(), nil +} + +// BuildBlobUploadChunkURL constructs a url for the upload identified by uuid, +// including any url values. This should generally not be used by clients, as +// this url is provided by server implementations during the blob upload +// process. +func (ub *URLBuilder) BuildBlobUploadChunkURL(name, uuid string, values ...url.Values) (string, error) { + route := ub.cloneRoute(RouteNameBlobUploadChunk) + + uploadURL, err := route.URL("name", name, "uuid", uuid) + if err != nil { + return "", err + } + + return appendValuesURL(uploadURL, values...).String(), nil +} + +// clondedRoute returns a clone of the named route from the router. Routes +// must be cloned to avoid modifying them during url generation. +func (ub *URLBuilder) cloneRoute(name string) *mux.Route { + route := new(mux.Route) + *route = *ub.router.GetRoute(name) // clone the route + + return route. + Schemes(ub.root.Scheme). + Host(ub.root.Host) +} + +// appendValuesURL appends the parameters to the url. +func appendValuesURL(u *url.URL, values ...url.Values) *url.URL { + merged := u.Query() + + for _, v := range values { + for k, vv := range v { + merged[k] = append(merged[k], vv...) + } + } + + u.RawQuery = merged.Encode() + return u +} + +// appendValues appends the parameters to the url. Panics if the string is not +// a url. +func appendValues(u string, values ...url.Values) string { + up, err := url.Parse(u) + + if err != nil { + panic(err) // should never happen + } + + return appendValuesURL(up, values...).String() +} diff --git a/registry/v2/urls_test.go b/registry/v2/urls_test.go new file mode 100644 index 000000000..a9590dba9 --- /dev/null +++ b/registry/v2/urls_test.go @@ -0,0 +1,100 @@ +package v2 + +import ( + "net/url" + "testing" +) + +type urlBuilderTestCase struct { + description string + expected string + build func() (string, error) +} + +// TestURLBuilder tests the various url building functions, ensuring they are +// returning the expected values. +func TestURLBuilder(t *testing.T) { + + root := "http://localhost:5000/" + urlBuilder, err := NewURLBuilderFromString(root) + if err != nil { + t.Fatalf("unexpected error creating urlbuilder: %v", err) + } + + for _, testcase := range []struct { + description string + expected string + build func() (string, error) + }{ + { + description: "test base url", + expected: "http://localhost:5000/v2/", + build: urlBuilder.BuildBaseURL, + }, + { + description: "test tags url", + expected: "http://localhost:5000/v2/foo/bar/tags/list", + build: func() (string, error) { + return urlBuilder.BuildTagsURL("foo/bar") + }, + }, + { + description: "test manifest url", + expected: "http://localhost:5000/v2/foo/bar/manifests/tag", + build: func() (string, error) { + return urlBuilder.BuildManifestURL("foo/bar", "tag") + }, + }, + { + description: "build blob url", + expected: "http://localhost:5000/v2/foo/bar/blobs/tarsum.v1+sha256:abcdef0123456789", + build: func() (string, error) { + return urlBuilder.BuildBlobURL("foo/bar", "tarsum.v1+sha256:abcdef0123456789") + }, + }, + { + description: "build blob upload url", + expected: "http://localhost:5000/v2/foo/bar/blobs/uploads/", + build: func() (string, error) { + return urlBuilder.BuildBlobUploadURL("foo/bar") + }, + }, + { + description: "build blob upload url with digest and size", + expected: "http://localhost:5000/v2/foo/bar/blobs/uploads/?digest=tarsum.v1%2Bsha256%3Aabcdef0123456789&size=10000", + build: func() (string, error) { + return urlBuilder.BuildBlobUploadURL("foo/bar", url.Values{ + "size": []string{"10000"}, + "digest": []string{"tarsum.v1+sha256:abcdef0123456789"}, + }) + }, + }, + { + description: "build blob upload chunk url", + expected: "http://localhost:5000/v2/foo/bar/blobs/uploads/uuid-part", + build: func() (string, error) { + return urlBuilder.BuildBlobUploadChunkURL("foo/bar", "uuid-part") + }, + }, + { + description: "build blob upload chunk url with digest and size", + expected: "http://localhost:5000/v2/foo/bar/blobs/uploads/uuid-part?digest=tarsum.v1%2Bsha256%3Aabcdef0123456789&size=10000", + build: func() (string, error) { + return urlBuilder.BuildBlobUploadChunkURL("foo/bar", "uuid-part", url.Values{ + "size": []string{"10000"}, + "digest": []string{"tarsum.v1+sha256:abcdef0123456789"}, + }) + }, + }, + } { + u, err := testcase.build() + if err != nil { + t.Fatalf("%s: error building url: %v", testcase.description, err) + } + + if u != testcase.expected { + t.Fatalf("%s: %q != %q", testcase.description, u, testcase.expected) + } + } + +} From dbb4b03bfc82eadefaf68c1a81d215949980550e Mon Sep 17 00:00:00 2001 From: Stephen J Day Date: Mon, 15 Dec 2014 12:42:52 -0800 Subject: [PATCH 299/513] Remove dependencies on registry packages Because docker core cannot vendor non-master Go dependencies, we need to remove dependencies on registry package. The definition of digest.Digest has been changed to a string and the regular expressions have been ported from docker-registry/common library. We'll likely change this be dependent on the registry in the future when the API stabilizies and use of the master branch becomes the norm. Signed-off-by: Stephen J Day --- registry/v2/errors_test.go | 4 +--- registry/v2/regexp.go | 19 +++++++++++++++++++ registry/v2/routes.go | 15 ++++++--------- registry/v2/urls.go | 5 ++--- 4 files changed, 28 insertions(+), 15 deletions(-) create mode 100644 registry/v2/regexp.go diff --git a/registry/v2/errors_test.go b/registry/v2/errors_test.go index d2fc091ac..4a80cdfe2 100644 --- a/registry/v2/errors_test.go +++ b/registry/v2/errors_test.go @@ -4,8 +4,6 @@ import ( "encoding/json" "reflect" "testing" - - "github.com/docker/docker-registry/digest" ) // TestErrorCodes ensures that error code format, mappings and @@ -61,7 +59,7 @@ func TestErrorsManagement(t *testing.T) { errs.Push(ErrorCodeDigestInvalid) errs.Push(ErrorCodeBlobUnknown, - map[string]digest.Digest{"digest": "sometestblobsumdoesntmatter"}) + map[string]string{"digest": "sometestblobsumdoesntmatter"}) p, err := json.Marshal(errs) diff --git a/registry/v2/regexp.go b/registry/v2/regexp.go new file mode 100644 index 000000000..b7e95b9ff --- /dev/null +++ b/registry/v2/regexp.go @@ -0,0 +1,19 @@ +package v2 + +import "regexp" + +// This file defines regular expressions for use in route definition. These +// are also defined in the registry code base. Until they are in a common, +// shared location, and exported, they must be repeated here. + +// RepositoryNameComponentRegexp restricts registtry path components names to +// start with at least two letters or numbers, with following parts able to +// separated by one period, dash or underscore. +var RepositoryNameComponentRegexp = regexp.MustCompile(`[a-z0-9]+(?:[._-][a-z0-9]+)*`) + +// RepositoryNameRegexp builds on RepositoryNameComponentRegexp to allow 2 to +// 5 path components, separated by a forward slash. +var RepositoryNameRegexp = regexp.MustCompile(`(?:` + RepositoryNameComponentRegexp.String() + `/){1,4}` + RepositoryNameComponentRegexp.String()) + +// TagNameRegexp matches valid tag names. From docker/docker:graph/tags.go. +var TagNameRegexp = regexp.MustCompile(`[\w][\w.-]{0,127}`) diff --git a/registry/v2/routes.go b/registry/v2/routes.go index 7ebe61d66..08f36e2f7 100644 --- a/registry/v2/routes.go +++ b/registry/v2/routes.go @@ -1,9 +1,6 @@ package v2 -import ( - "github.com/docker/docker-registry/common" - "github.com/gorilla/mux" -) +import "github.com/gorilla/mux" // The following are definitions of the name under which all V2 routes are // registered. These symbols can be used to look up a route based on the name. @@ -40,29 +37,29 @@ func Router() *mux.Router { // PUT /v2//manifest/ Image Manifest Upload the image manifest identified by name and tag. // DELETE /v2//manifest/ Image Manifest Delete the image identified by name and tag. router. - Path("/v2/{name:" + common.RepositoryNameRegexp.String() + "}/manifests/{tag:" + common.TagNameRegexp.String() + "}"). + Path("/v2/{name:" + RepositoryNameRegexp.String() + "}/manifests/{tag:" + TagNameRegexp.String() + "}"). Name(RouteNameManifest) // GET /v2//tags/list Tags Fetch the tags under the repository identified by name. router. - Path("/v2/{name:" + common.RepositoryNameRegexp.String() + "}/tags/list"). + Path("/v2/{name:" + RepositoryNameRegexp.String() + "}/tags/list"). Name(RouteNameTags) // GET /v2//blob/ Layer Fetch the blob identified by digest. router. - Path("/v2/{name:" + common.RepositoryNameRegexp.String() + "}/blobs/{digest:[a-zA-Z0-9-_+.]+:[a-zA-Z0-9-_+.=]+}"). + Path("/v2/{name:" + RepositoryNameRegexp.String() + "}/blobs/{digest:[a-zA-Z0-9-_+.]+:[a-zA-Z0-9-_+.=]+}"). Name(RouteNameBlob) // POST /v2//blob/upload/ Layer Upload Initiate an upload of the layer identified by tarsum. router. - Path("/v2/{name:" + common.RepositoryNameRegexp.String() + "}/blobs/uploads/"). + Path("/v2/{name:" + RepositoryNameRegexp.String() + "}/blobs/uploads/"). Name(RouteNameBlobUpload) // GET /v2//blob/upload/ Layer Upload Get the status of the upload identified by tarsum and uuid. // PUT /v2//blob/upload/ Layer Upload Upload all or a chunk of the upload identified by tarsum and uuid. // DELETE /v2//blob/upload/ Layer Upload Cancel the upload identified by layer and uuid router. - Path("/v2/{name:" + common.RepositoryNameRegexp.String() + "}/blobs/uploads/{uuid}"). + Path("/v2/{name:" + RepositoryNameRegexp.String() + "}/blobs/uploads/{uuid}"). Name(RouteNameBlobUploadChunk) return router diff --git a/registry/v2/urls.go b/registry/v2/urls.go index 72f44299a..19ef06fa1 100644 --- a/registry/v2/urls.go +++ b/registry/v2/urls.go @@ -4,7 +4,6 @@ import ( "net/http" "net/url" - "github.com/docker/docker-registry/digest" "github.com/gorilla/mux" ) @@ -88,10 +87,10 @@ func (ub *URLBuilder) BuildManifestURL(name, tag string) (string, error) { } // BuildBlobURL constructs the url for the blob identified by name and dgst. -func (ub *URLBuilder) BuildBlobURL(name string, dgst digest.Digest) (string, error) { +func (ub *URLBuilder) BuildBlobURL(name string, dgst string) (string, error) { route := ub.cloneRoute(RouteNameBlob) - layerURL, err := route.URL("name", name, "digest", dgst.String()) + layerURL, err := route.URL("name", name, "digest", dgst) if err != nil { return "", err } From 0336b0cdaa74ac03003c4a933eb866fb0cec8125 Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Tue, 16 Dec 2014 16:57:37 -0800 Subject: [PATCH 300/513] Update push and pull to registry 2.1 specification Signed-off-by: Derek McGowan --- graph/manifest.go | 154 +++++++++++---------- graph/pull.go | 21 +-- graph/push.go | 65 +++++---- registry/auth.go | 53 ++++++++ registry/session_v2.go | 298 ++++++++++++++--------------------------- utils/jsonmessage.go | 3 + 6 files changed, 281 insertions(+), 313 deletions(-) diff --git a/graph/manifest.go b/graph/manifest.go index ddcb22b65..54d6083cb 100644 --- a/graph/manifest.go +++ b/graph/manifest.go @@ -2,6 +2,7 @@ package graph import ( "encoding/json" + "errors" "fmt" "io" "io/ioutil" @@ -24,82 +25,12 @@ func (s *TagStore) CmdManifest(job *engine.Job) engine.Status { } // Resolve the Repository name from fqn to endpoint + name - _, remoteName, err := registry.ResolveRepositoryName(name) + repoInfo, err := registry.ParseRepositoryInfo(name) if err != nil { return job.Error(err) } - manifest := ®istry.ManifestData{ - Name: remoteName, - Tag: tag, - SchemaVersion: 1, - } - localRepo, exists := s.Repositories[name] - if !exists { - return job.Errorf("Repo does not exist: %s", name) - } - - layerId, exists := localRepo[tag] - if !exists { - return job.Errorf("Tag does not exist for %s: %s", name, tag) - } - layersSeen := make(map[string]bool) - - layer, err := s.graph.Get(layerId) - if err != nil { - return job.Error(err) - } - if layer.Config == nil { - return job.Errorf("Missing layer configuration") - } - manifest.Architecture = layer.Architecture - manifest.FSLayers = make([]*registry.FSLayer, 0, 4) - manifest.History = make([]*registry.ManifestHistory, 0, 4) - var metadata runconfig.Config - metadata = *layer.Config - - for ; layer != nil; layer, err = layer.GetParent() { - if err != nil { - return job.Error(err) - } - - if layersSeen[layer.ID] { - break - } - if layer.Config != nil && metadata.Image != layer.ID { - err = runconfig.Merge(&metadata, layer.Config) - if err != nil { - return job.Error(err) - } - } - - archive, err := layer.TarLayer() - if err != nil { - return job.Error(err) - } - - tarSum, err := tarsum.NewTarSum(archive, true, tarsum.VersionDev) - if err != nil { - return job.Error(err) - } - if _, err := io.Copy(ioutil.Discard, tarSum); err != nil { - return job.Error(err) - } - - tarId := tarSum.Sum(nil) - // Save tarsum to image json - - manifest.FSLayers = append(manifest.FSLayers, ®istry.FSLayer{BlobSum: tarId}) - - layersSeen[layer.ID] = true - jsonData, err := ioutil.ReadFile(path.Join(s.graph.Root, layer.ID, "json")) - if err != nil { - return job.Error(fmt.Errorf("Cannot retrieve the path for {%s}: %s", layer.ID, err)) - } - manifest.History = append(manifest.History, ®istry.ManifestHistory{V1Compatibility: string(jsonData)}) - } - - manifestBytes, err := json.MarshalIndent(manifest, "", " ") + manifestBytes, err := s.newManifest(name, repoInfo.RemoteName, tag) if err != nil { return job.Error(err) } @@ -111,3 +42,82 @@ func (s *TagStore) CmdManifest(job *engine.Job) engine.Status { return engine.StatusOK } + +func (s *TagStore) newManifest(localName, remoteName, tag string) ([]byte, error) { + manifest := ®istry.ManifestData{ + Name: remoteName, + Tag: tag, + SchemaVersion: 1, + } + localRepo, exists := s.Repositories[localName] + if !exists { + return nil, fmt.Errorf("Repo does not exist: %s", localName) + } + + layerId, exists := localRepo[tag] + if !exists { + return nil, fmt.Errorf("Tag does not exist for %s: %s", localName, tag) + } + layersSeen := make(map[string]bool) + + layer, err := s.graph.Get(layerId) + if err != nil { + return nil, err + } + if layer.Config == nil { + return nil, errors.New("Missing layer configuration") + } + manifest.Architecture = layer.Architecture + manifest.FSLayers = make([]*registry.FSLayer, 0, 4) + manifest.History = make([]*registry.ManifestHistory, 0, 4) + var metadata runconfig.Config + metadata = *layer.Config + + for ; layer != nil; layer, err = layer.GetParent() { + if err != nil { + return nil, err + } + + if layersSeen[layer.ID] { + break + } + if layer.Config != nil && metadata.Image != layer.ID { + err = runconfig.Merge(&metadata, layer.Config) + if err != nil { + return nil, err + } + } + + archive, err := layer.TarLayer() + if err != nil { + return nil, err + } + + tarSum, err := tarsum.NewTarSum(archive, true, tarsum.Version1) + if err != nil { + return nil, err + } + if _, err := io.Copy(ioutil.Discard, tarSum); err != nil { + return nil, err + } + + tarId := tarSum.Sum(nil) + // Save tarsum to image json + + manifest.FSLayers = append(manifest.FSLayers, ®istry.FSLayer{BlobSum: tarId}) + + layersSeen[layer.ID] = true + jsonData, err := ioutil.ReadFile(path.Join(s.graph.Root, layer.ID, "json")) + if err != nil { + return nil, fmt.Errorf("Cannot retrieve the path for {%s}: %s", layer.ID, err) + } + manifest.History = append(manifest.History, ®istry.ManifestHistory{V1Compatibility: string(jsonData)}) + } + + manifestBytes, err := json.MarshalIndent(manifest, "", " ") + if err != nil { + return nil, err + } + + return manifestBytes, nil +} diff --git a/graph/pull.go b/graph/pull.go index 587eb5f50..b138793d1 100644 --- a/graph/pull.go +++ b/graph/pull.go @@ -133,7 +133,12 @@ func (s *TagStore) CmdPull(job *engine.Job) engine.Status { return job.Errorf("error updating trust base graph: %s", err) } - if err := s.pullV2Repository(job.Eng, r, job.Stdout, repoInfo, tag, sf, job.GetenvBool("parallel")); err == nil { + auth, err := r.GetV2Authorization(repoInfo.RemoteName, true) + if err != nil { + return job.Errorf("error getting authorization: %s", err) + } + + if err := s.pullV2Repository(job.Eng, r, job.Stdout, repoInfo, tag, sf, job.GetenvBool("parallel"), auth); err == nil { if err = job.Eng.Job("log", "pull", logName, "").Run(); err != nil { log.Errorf("Error logging event 'pull' for %s: %s", logName, err) } @@ -423,23 +428,23 @@ type downloadInfo struct { err chan error } -func (s *TagStore) pullV2Repository(eng *engine.Engine, r *registry.Session, out io.Writer, repoInfo *registry.RepositoryInfo, tag string, sf *utils.StreamFormatter, parallel bool) error { +func (s *TagStore) pullV2Repository(eng *engine.Engine, r *registry.Session, out io.Writer, repoInfo *registry.RepositoryInfo, tag string, sf *utils.StreamFormatter, parallel bool, auth *registry.RequestAuthorization) error { var layersDownloaded bool if tag == "" { log.Debugf("Pulling tag list from V2 registry for %s", repoInfo.CanonicalName) - tags, err := r.GetV2RemoteTags(repoInfo.RemoteName, nil) + tags, err := r.GetV2RemoteTags(repoInfo.RemoteName, auth) if err != nil { return err } for _, t := range tags { - if downloaded, err := s.pullV2Tag(eng, r, out, repoInfo, t, sf, parallel); err != nil { + if downloaded, err := s.pullV2Tag(eng, r, out, repoInfo, t, sf, parallel, auth); err != nil { return err } else if downloaded { layersDownloaded = true } } } else { - if downloaded, err := s.pullV2Tag(eng, r, out, repoInfo, tag, sf, parallel); err != nil { + if downloaded, err := s.pullV2Tag(eng, r, out, repoInfo, tag, sf, parallel, auth); err != nil { return err } else if downloaded { layersDownloaded = true @@ -454,9 +459,9 @@ func (s *TagStore) pullV2Repository(eng *engine.Engine, r *registry.Session, out return nil } -func (s *TagStore) pullV2Tag(eng *engine.Engine, r *registry.Session, out io.Writer, repoInfo *registry.RepositoryInfo, tag string, sf *utils.StreamFormatter, parallel bool) (bool, error) { +func (s *TagStore) pullV2Tag(eng *engine.Engine, r *registry.Session, out io.Writer, repoInfo *registry.RepositoryInfo, tag string, sf *utils.StreamFormatter, parallel bool, auth *registry.RequestAuthorization) (bool, error) { log.Debugf("Pulling tag from V2 registry: %q", tag) - manifestBytes, err := r.GetV2ImageManifest(repoInfo.RemoteName, tag, nil) + manifestBytes, err := r.GetV2ImageManifest(repoInfo.RemoteName, tag, auth) if err != nil { return false, err } @@ -525,7 +530,7 @@ func (s *TagStore) pullV2Tag(eng *engine.Engine, r *registry.Session, out io.Wri return err } - r, l, err := r.GetV2ImageBlobReader(repoInfo.RemoteName, sumType, checksum, nil) + r, l, err := r.GetV2ImageBlobReader(repoInfo.RemoteName, sumType, checksum, auth) if err != nil { return err } diff --git a/graph/push.go b/graph/push.go index 64c1f7c61..0d008b84c 100644 --- a/graph/push.go +++ b/graph/push.go @@ -290,26 +290,24 @@ func (s *TagStore) CmdPush(job *engine.Job) engine.Status { return job.Error(err2) } - var isOfficial bool - if endpoint.String() == registry.IndexServerAddress() { - isOfficial = isOfficialName(remoteName) - if isOfficial && strings.IndexRune(remoteName, '/') == -1 { - remoteName = "library/" + remoteName - } - } - if len(tag) == 0 { tag = DEFAULTTAG } - if isOfficial || endpoint.Version == registry.APIVersion2 { + + if repoInfo.Official || endpoint.Version == registry.APIVersion2 { j := job.Eng.Job("trust_update_base") if err = j.Run(); err != nil { return job.Errorf("error updating trust base graph: %s", err) } - repoData, err := r.PushImageJSONIndex(remoteName, []*registry.ImgData{}, false, nil) + // Get authentication type + auth, err := r.GetV2Authorization(repoInfo.RemoteName, false) if err != nil { - return job.Error(err) + return job.Errorf("error getting authorization: %s", err) + } + + if len(manifestBytes) == 0 { + // TODO Create manifest and sign } // try via manifest @@ -359,13 +357,13 @@ func (s *TagStore) CmdPush(job *engine.Job) engine.Status { } // Call mount blob - exists, err := r.PostV2ImageMountBlob(remoteName, sumParts[0], manifestSum, repoData.Tokens) + exists, err := r.PostV2ImageMountBlob(repoInfo.RemoteName, sumParts[0], manifestSum, auth) if err != nil { job.Stdout.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Image push failed", nil)) return job.Error(err) } if !exists { - _, err = r.PutV2ImageBlob(remoteName, sumParts[0], manifestSum, utils.ProgressReader(arch, int(img.Size), job.Stdout, sf, false, utils.TruncateID(img.ID), "Pushing"), repoData.Tokens) + err = r.PutV2ImageBlob(repoInfo.RemoteName, sumParts[0], manifestSum, utils.ProgressReader(arch, int(img.Size), job.Stdout, sf, false, utils.TruncateID(img.ID), "Pushing"), auth) if err != nil { job.Stdout.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Image push failed", nil)) return job.Error(err) @@ -377,35 +375,36 @@ func (s *TagStore) CmdPush(job *engine.Job) engine.Status { } // push the manifest - err = r.PutV2ImageManifest(remoteName, tag, bytes.NewReader([]byte(manifestBytes)), repoData.Tokens) + err = r.PutV2ImageManifest(repoInfo.RemoteName, tag, bytes.NewReader([]byte(manifestBytes)), auth) if err != nil { return job.Error(err) } // done, no fallback to V1 return engine.StatusOK - } + } else { - if err != nil { - reposLen := 1 - if tag == "" { - reposLen = len(s.Repositories[repoInfo.LocalName]) - } - job.Stdout.Write(sf.FormatStatus("", "The push refers to a repository [%s] (len: %d)", repoInfo.CanonicalName, reposLen)) - // If it fails, try to get the repository - if localRepo, exists := s.Repositories[repoInfo.LocalName]; exists { - if err := s.pushRepository(r, job.Stdout, repoInfo, localRepo, tag, sf); err != nil { - return job.Error(err) + if err != nil { + reposLen := 1 + if tag == "" { + reposLen = len(s.Repositories[repoInfo.LocalName]) } - return engine.StatusOK + job.Stdout.Write(sf.FormatStatus("", "The push refers to a repository [%s] (len: %d)", repoInfo.CanonicalName, reposLen)) + // If it fails, try to get the repository + if localRepo, exists := s.Repositories[repoInfo.LocalName]; exists { + if err := s.pushRepository(r, job.Stdout, repoInfo, localRepo, tag, sf); err != nil { + return job.Error(err) + } + return engine.StatusOK + } + return job.Error(err) } - return job.Error(err) - } - var token []string - job.Stdout.Write(sf.FormatStatus("", "The push refers to an image: [%s]", repoInfo.CanonicalName)) - if _, err := s.pushImage(r, job.Stdout, img.ID, endpoint.String(), token, sf); err != nil { - return job.Error(err) + var token []string + job.Stdout.Write(sf.FormatStatus("", "The push refers to an image: [%s]", repoInfo.CanonicalName)) + if _, err := s.pushImage(r, job.Stdout, img.ID, endpoint.String(), token, sf); err != nil { + return job.Error(err) + } + return engine.StatusOK } - return engine.StatusOK } diff --git a/registry/auth.go b/registry/auth.go index 2044236cf..b138fb530 100644 --- a/registry/auth.go +++ b/registry/auth.go @@ -37,6 +37,59 @@ type ConfigFile struct { rootPath string } +type RequestAuthorization struct { + Token string + Username string + Password string +} + +func NewRequestAuthorization(authConfig *AuthConfig, registryEndpoint *Endpoint, resource, scope string, actions []string) (*RequestAuthorization, error) { + var auth RequestAuthorization + + client := &http.Client{ + Transport: &http.Transport{ + DisableKeepAlives: true, + Proxy: http.ProxyFromEnvironment, + }, + CheckRedirect: AddRequiredHeadersToRedirectedRequests, + } + factory := HTTPRequestFactory(nil) + + for _, challenge := range registryEndpoint.AuthChallenges { + log.Debugf("Using %q auth challenge with params %s for %s", challenge.Scheme, challenge.Parameters, authConfig.Username) + + switch strings.ToLower(challenge.Scheme) { + case "basic": + auth.Username = authConfig.Username + auth.Password = authConfig.Password + case "bearer": + params := map[string]string{} + for k, v := range challenge.Parameters { + params[k] = v + } + params["scope"] = fmt.Sprintf("%s:%s:%s", resource, scope, strings.Join(actions, ",")) + token, err := getToken(authConfig.Username, authConfig.Password, params, registryEndpoint, client, factory) + if err != nil { + return nil, err + } + + auth.Token = token + default: + log.Infof("Unsupported auth scheme: %q", challenge.Scheme) + } + } + + return &auth, nil +} + +func (auth *RequestAuthorization) Authorize(req *http.Request) { + if auth.Token != "" { + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", auth.Token)) + } else if auth.Username != "" && auth.Password != "" { + req.SetBasicAuth(auth.Username, auth.Password) + } +} + // create a base64 encoded auth string to store in config func encodeAuth(authConfig *AuthConfig) string { authStr := authConfig.Username + ":" + authConfig.Password diff --git a/registry/session_v2.go b/registry/session_v2.go index 86d0c228a..407c5f3a2 100644 --- a/registry/session_v2.go +++ b/registry/session_v2.go @@ -9,100 +9,34 @@ import ( "strconv" log "github.com/Sirupsen/logrus" + "github.com/docker/docker/registry/v2" "github.com/docker/docker/utils" - "github.com/gorilla/mux" ) -func newV2RegistryRouter() *mux.Router { - router := mux.NewRouter() +var registryURLBuilder *v2.URLBuilder - v2Router := router.PathPrefix("/v2/").Subrouter() - - // Version Info - v2Router.Path("/version").Name("version") - - // Image Manifests - v2Router.Path("/manifest/{imagename:[a-z0-9-._/]+}/{tagname:[a-zA-Z0-9-._]+}").Name("manifests") - - // List Image Tags - v2Router.Path("/tags/{imagename:[a-z0-9-._/]+}").Name("tags") - - // Download a blob - v2Router.Path("/blob/{imagename:[a-z0-9-._/]+}/{sumtype:[a-z0-9._+-]+}/{sum:[a-fA-F0-9]{4,}}").Name("downloadBlob") - - // Upload a blob - v2Router.Path("/blob/{imagename:[a-z0-9-._/]+}/{sumtype:[a-z0-9._+-]+}").Name("uploadBlob") - - // Mounting a blob in an image - v2Router.Path("/blob/{imagename:[a-z0-9-._/]+}/{sumtype:[a-z0-9._+-]+}/{sum:[a-fA-F0-9]{4,}}").Name("mountBlob") - - return router -} - -// APIVersion2 /v2/ -var v2HTTPRoutes = newV2RegistryRouter() - -func getV2URL(e *Endpoint, routeName string, vars map[string]string) (*url.URL, error) { - route := v2HTTPRoutes.Get(routeName) - if route == nil { - return nil, fmt.Errorf("unknown regisry v2 route name: %q", routeName) - } - - varReplace := make([]string, 0, len(vars)*2) - for key, val := range vars { - varReplace = append(varReplace, key, val) - } - - routePath, err := route.URLPath(varReplace...) - if err != nil { - return nil, fmt.Errorf("unable to make registry route %q with vars %v: %s", routeName, vars, err) - } +func init() { u, err := url.Parse(REGISTRYSERVER) if err != nil { - return nil, fmt.Errorf("invalid registry url: %s", err) + panic(fmt.Errorf("invalid registry url: %s", err)) } - - return &url.URL{ - Scheme: u.Scheme, - Host: u.Host, - Path: routePath.Path, - }, nil + registryURLBuilder = v2.NewURLBuilder(u) } -// V2 Provenance POC +func getV2Builder(e *Endpoint) *v2.URLBuilder { + return registryURLBuilder +} -func (r *Session) GetV2Version(token []string) (*RegistryInfo, error) { - routeURL, err := getV2URL(r.indexEndpoint, "version", nil) - if err != nil { - return nil, err +// GetV2Authorization gets the authorization needed to the given image +// If readonly access is requested, then only the authorization may +// only be used for Get operations. +func (r *Session) GetV2Authorization(imageName string, readOnly bool) (*RequestAuthorization, error) { + scopes := []string{"pull"} + if !readOnly { + scopes = append(scopes, "push") } - method := "GET" - log.Debugf("[registry] Calling %q %s", method, routeURL.String()) - - req, err := r.reqFactory.NewRequest(method, routeURL.String(), nil) - if err != nil { - return nil, err - } - setTokenAuth(req, token) - res, _, err := r.doRequest(req) - if err != nil { - return nil, err - } - defer res.Body.Close() - if res.StatusCode != 200 { - return nil, utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d fetching Version", res.StatusCode), res) - } - - decoder := json.NewDecoder(res.Body) - versionInfo := new(RegistryInfo) - - err = decoder.Decode(versionInfo) - if err != nil { - return nil, fmt.Errorf("unable to decode GetV2Version JSON response: %s", err) - } - - return versionInfo, nil + return NewRequestAuthorization(r.GetAuthConfig(true), r.indexEndpoint, "repository", imageName, scopes) } // @@ -112,25 +46,20 @@ func (r *Session) GetV2Version(token []string) (*RegistryInfo, error) { // 1.c) if anything else, err // 2) PUT the created/signed manifest // -func (r *Session) GetV2ImageManifest(imageName, tagName string, token []string) ([]byte, error) { - vars := map[string]string{ - "imagename": imageName, - "tagname": tagName, - } - - routeURL, err := getV2URL(r.indexEndpoint, "manifests", vars) +func (r *Session) GetV2ImageManifest(imageName, tagName string, auth *RequestAuthorization) ([]byte, error) { + routeURL, err := getV2Builder(r.indexEndpoint).BuildManifestURL(imageName, tagName) if err != nil { return nil, err } method := "GET" - log.Debugf("[registry] Calling %q %s", method, routeURL.String()) + log.Debugf("[registry] Calling %q %s", method, routeURL) - req, err := r.reqFactory.NewRequest(method, routeURL.String(), nil) + req, err := r.reqFactory.NewRequest(method, routeURL, nil) if err != nil { return nil, err } - setTokenAuth(req, token) + auth.Authorize(req) res, _, err := r.doRequest(req) if err != nil { return nil, err @@ -155,26 +84,20 @@ func (r *Session) GetV2ImageManifest(imageName, tagName string, token []string) // - Succeeded to mount for this image scope // - Failed with no error (So continue to Push the Blob) // - Failed with error -func (r *Session) PostV2ImageMountBlob(imageName, sumType, sum string, token []string) (bool, error) { - vars := map[string]string{ - "imagename": imageName, - "sumtype": sumType, - "sum": sum, - } - - routeURL, err := getV2URL(r.indexEndpoint, "mountBlob", vars) +func (r *Session) PostV2ImageMountBlob(imageName, sumType, sum string, auth *RequestAuthorization) (bool, error) { + routeURL, err := getV2Builder(r.indexEndpoint).BuildBlobURL(imageName, sumType+":"+sum) if err != nil { return false, err } - method := "POST" - log.Debugf("[registry] Calling %q %s", method, routeURL.String()) + method := "HEAD" + log.Debugf("[registry] Calling %q %s", method, routeURL) - req, err := r.reqFactory.NewRequest(method, routeURL.String(), nil) + req, err := r.reqFactory.NewRequest(method, routeURL, nil) if err != nil { return false, err } - setTokenAuth(req, token) + auth.Authorize(req) res, _, err := r.doRequest(req) if err != nil { return false, err @@ -191,25 +114,19 @@ func (r *Session) PostV2ImageMountBlob(imageName, sumType, sum string, token []s return false, fmt.Errorf("Failed to mount %q - %s:%s : %d", imageName, sumType, sum, res.StatusCode) } -func (r *Session) GetV2ImageBlob(imageName, sumType, sum string, blobWrtr io.Writer, token []string) error { - vars := map[string]string{ - "imagename": imageName, - "sumtype": sumType, - "sum": sum, - } - - routeURL, err := getV2URL(r.indexEndpoint, "downloadBlob", vars) +func (r *Session) GetV2ImageBlob(imageName, sumType, sum string, blobWrtr io.Writer, auth *RequestAuthorization) error { + routeURL, err := getV2Builder(r.indexEndpoint).BuildBlobURL(imageName, sumType+":"+sum) if err != nil { return err } method := "GET" - log.Debugf("[registry] Calling %q %s", method, routeURL.String()) - req, err := r.reqFactory.NewRequest(method, routeURL.String(), nil) + log.Debugf("[registry] Calling %q %s", method, routeURL) + req, err := r.reqFactory.NewRequest(method, routeURL, nil) if err != nil { return err } - setTokenAuth(req, token) + auth.Authorize(req) res, _, err := r.doRequest(req) if err != nil { return err @@ -226,25 +143,19 @@ func (r *Session) GetV2ImageBlob(imageName, sumType, sum string, blobWrtr io.Wri return err } -func (r *Session) GetV2ImageBlobReader(imageName, sumType, sum string, token []string) (io.ReadCloser, int64, error) { - vars := map[string]string{ - "imagename": imageName, - "sumtype": sumType, - "sum": sum, - } - - routeURL, err := getV2URL(r.indexEndpoint, "downloadBlob", vars) +func (r *Session) GetV2ImageBlobReader(imageName, sumType, sum string, auth *RequestAuthorization) (io.ReadCloser, int64, error) { + routeURL, err := getV2Builder(r.indexEndpoint).BuildBlobURL(imageName, sumType+":"+sum) if err != nil { return nil, 0, err } method := "GET" - log.Debugf("[registry] Calling %q %s", method, routeURL.String()) - req, err := r.reqFactory.NewRequest(method, routeURL.String(), nil) + log.Debugf("[registry] Calling %q %s", method, routeURL) + req, err := r.reqFactory.NewRequest(method, routeURL, nil) if err != nil { return nil, 0, err } - setTokenAuth(req, token) + auth.Authorize(req) res, _, err := r.doRequest(req) if err != nil { return nil, 0, err @@ -267,85 +178,76 @@ func (r *Session) GetV2ImageBlobReader(imageName, sumType, sum string, token []s // Push the image to the server for storage. // 'layer' is an uncompressed reader of the blob to be pushed. // The server will generate it's own checksum calculation. -func (r *Session) PutV2ImageBlob(imageName, sumType, sumStr string, blobRdr io.Reader, token []string) (serverChecksum string, err error) { - vars := map[string]string{ - "imagename": imageName, - "sumtype": sumType, +func (r *Session) PutV2ImageBlob(imageName, sumType, sumStr string, blobRdr io.Reader, auth *RequestAuthorization) error { + routeURL, err := getV2Builder(r.indexEndpoint).BuildBlobUploadURL(imageName) + if err != nil { + return err } - routeURL, err := getV2URL(r.indexEndpoint, "uploadBlob", vars) + log.Debugf("[registry] Calling %q %s", "POST", routeURL) + req, err := r.reqFactory.NewRequest("POST", routeURL, nil) if err != nil { - return "", err + return err } - method := "PUT" - log.Debugf("[registry] Calling %q %s", method, routeURL.String()) - req, err := r.reqFactory.NewRequest(method, routeURL.String(), blobRdr) - if err != nil { - return "", err - } - setTokenAuth(req, token) - req.Header.Set("X-Tarsum", sumStr) + auth.Authorize(req) res, _, err := r.doRequest(req) if err != nil { - return "", err + return err + } + location := res.Header.Get("Location") + + method := "PUT" + log.Debugf("[registry] Calling %q %s", method, location) + req, err = r.reqFactory.NewRequest(method, location, blobRdr) + if err != nil { + return err + } + queryParams := url.Values{} + queryParams.Add("digest", sumType+":"+sumStr) + req.URL.RawQuery = queryParams.Encode() + auth.Authorize(req) + res, _, err = r.doRequest(req) + if err != nil { + return err } defer res.Body.Close() - if res.StatusCode != 201 { - if res.StatusCode == 401 { - return "", errLoginRequired - } - return "", utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to push %s blob", res.StatusCode, imageName), res) - } - type sumReturn struct { - Checksum string `json:"checksum"` - } - - decoder := json.NewDecoder(res.Body) - var sumInfo sumReturn - - err = decoder.Decode(&sumInfo) - if err != nil { - return "", fmt.Errorf("unable to decode PutV2ImageBlob JSON response: %s", err) - } - - if sumInfo.Checksum != sumStr { - return "", fmt.Errorf("failed checksum comparison. serverChecksum: %q, localChecksum: %q", sumInfo.Checksum, sumStr) - } - - // XXX this is a json struct from the registry, with its checksum - return sumInfo.Checksum, nil -} - -// Finally Push the (signed) manifest of the blobs we've just pushed -func (r *Session) PutV2ImageManifest(imageName, tagName string, manifestRdr io.Reader, token []string) error { - vars := map[string]string{ - "imagename": imageName, - "tagname": tagName, - } - - routeURL, err := getV2URL(r.indexEndpoint, "manifests", vars) - if err != nil { - return err - } - - method := "PUT" - log.Debugf("[registry] Calling %q %s", method, routeURL.String()) - req, err := r.reqFactory.NewRequest(method, routeURL.String(), manifestRdr) - if err != nil { - return err - } - setTokenAuth(req, token) - res, _, err := r.doRequest(req) - if err != nil { - return err - } - res.Body.Close() if res.StatusCode != 201 { if res.StatusCode == 401 { return errLoginRequired } + return utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to push %s blob", res.StatusCode, imageName), res) + } + + return nil +} + +// Finally Push the (signed) manifest of the blobs we've just pushed +func (r *Session) PutV2ImageManifest(imageName, tagName string, manifestRdr io.Reader, auth *RequestAuthorization) error { + routeURL, err := getV2Builder(r.indexEndpoint).BuildManifestURL(imageName, tagName) + if err != nil { + return err + } + + method := "PUT" + log.Debugf("[registry] Calling %q %s", method, routeURL) + req, err := r.reqFactory.NewRequest(method, routeURL, manifestRdr) + if err != nil { + return err + } + auth.Authorize(req) + res, _, err := r.doRequest(req) + if err != nil { + return err + } + b, _ := ioutil.ReadAll(res.Body) + res.Body.Close() + if res.StatusCode != 200 { + if res.StatusCode == 401 { + return errLoginRequired + } + log.Debugf("Unexpected response from server: %q %#v", b, res.Header) return utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to push %s:%s manifest", res.StatusCode, imageName, tagName), res) } @@ -353,24 +255,20 @@ func (r *Session) PutV2ImageManifest(imageName, tagName string, manifestRdr io.R } // Given a repository name, returns a json array of string tags -func (r *Session) GetV2RemoteTags(imageName string, token []string) ([]string, error) { - vars := map[string]string{ - "imagename": imageName, - } - - routeURL, err := getV2URL(r.indexEndpoint, "tags", vars) +func (r *Session) GetV2RemoteTags(imageName string, auth *RequestAuthorization) ([]string, error) { + routeURL, err := getV2Builder(r.indexEndpoint).BuildTagsURL(imageName) if err != nil { return nil, err } method := "GET" - log.Debugf("[registry] Calling %q %s", method, routeURL.String()) + log.Debugf("[registry] Calling %q %s", method, routeURL) - req, err := r.reqFactory.NewRequest(method, routeURL.String(), nil) + req, err := r.reqFactory.NewRequest(method, routeURL, nil) if err != nil { return nil, err } - setTokenAuth(req, token) + auth.Authorize(req) res, _, err := r.doRequest(req) if err != nil { return nil, err diff --git a/utils/jsonmessage.go b/utils/jsonmessage.go index a2bbbcf4d..74d311271 100644 --- a/utils/jsonmessage.go +++ b/utils/jsonmessage.go @@ -50,6 +50,9 @@ func (p *JSONProgress) String() string { } total := units.HumanSize(float64(p.Total)) percentage := int(float64(p.Current)/float64(p.Total)*100) / 2 + if percentage > 50 { + percentage = 50 + } if width > 110 { // this number can't be negetive gh#7136 numSpaces := 0 From 7d61255f578bae7dc5c2a5d44c50bf32bbc9f568 Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Fri, 19 Dec 2014 14:44:18 -0800 Subject: [PATCH 301/513] Allow private V2 registry endpoints Signed-off-by: Derek McGowan --- graph/pull.go | 2 +- graph/push.go | 12 ++++++------ registry/config.go | 2 +- registry/endpoint.go | 2 ++ registry/session_v2.go | 32 +++++++++++++++++++------------- 5 files changed, 29 insertions(+), 21 deletions(-) diff --git a/graph/pull.go b/graph/pull.go index b138793d1..0b75881cd 100644 --- a/graph/pull.go +++ b/graph/pull.go @@ -127,7 +127,7 @@ func (s *TagStore) CmdPull(job *engine.Job) engine.Status { logName += ":" + tag } - if len(repoInfo.Index.Mirrors) == 0 && (repoInfo.Official || endpoint.Version == registry.APIVersion2) { + if len(repoInfo.Index.Mirrors) == 0 && (repoInfo.Index.Official || endpoint.Version == registry.APIVersion2) { j := job.Eng.Job("trust_update_base") if err = j.Run(); err != nil { return job.Errorf("error updating trust base graph: %s", err) diff --git a/graph/push.go b/graph/push.go index 0d008b84c..88b207a45 100644 --- a/graph/push.go +++ b/graph/push.go @@ -294,13 +294,14 @@ func (s *TagStore) CmdPush(job *engine.Job) engine.Status { tag = DEFAULTTAG } - if repoInfo.Official || endpoint.Version == registry.APIVersion2 { - j := job.Eng.Job("trust_update_base") - if err = j.Run(); err != nil { - return job.Errorf("error updating trust base graph: %s", err) + if repoInfo.Index.Official || endpoint.Version == registry.APIVersion2 { + if repoInfo.Official { + j := job.Eng.Job("trust_update_base") + if err = j.Run(); err != nil { + return job.Errorf("error updating trust base graph: %s", err) + } } - // Get authentication type auth, err := r.GetV2Authorization(repoInfo.RemoteName, false) if err != nil { return job.Errorf("error getting authorization: %s", err) @@ -383,7 +384,6 @@ func (s *TagStore) CmdPush(job *engine.Job) engine.Status { // done, no fallback to V1 return engine.StatusOK } else { - if err != nil { reposLen := 1 if tag == "" { diff --git a/registry/config.go b/registry/config.go index b5652b15d..4d13aaea3 100644 --- a/registry/config.go +++ b/registry/config.go @@ -23,7 +23,7 @@ type Options struct { const ( // Only used for user auth + account creation INDEXSERVER = "https://index.docker.io/v1/" - REGISTRYSERVER = "https://registry-1.docker.io/v1/" + REGISTRYSERVER = "https://registry-1.docker.io/v2/" INDEXNAME = "docker.io" // INDEXSERVER = "https://registry-stage.hub.docker.com/v1/" diff --git a/registry/endpoint.go b/registry/endpoint.go index 5c5b05200..9a783f1f0 100644 --- a/registry/endpoint.go +++ b/registry/endpoint.go @@ -10,6 +10,7 @@ import ( "strings" log "github.com/Sirupsen/logrus" + "github.com/docker/docker/registry/v2" ) // for mocking in unit tests @@ -103,6 +104,7 @@ type Endpoint struct { Version APIVersion IsSecure bool AuthChallenges []*AuthorizationChallenge + URLBuilder *v2.URLBuilder } // Get the formated URL for the root of this registry Endpoint diff --git a/registry/session_v2.go b/registry/session_v2.go index 407c5f3a2..2304a6134 100644 --- a/registry/session_v2.go +++ b/registry/session_v2.go @@ -13,30 +13,36 @@ import ( "github.com/docker/docker/utils" ) -var registryURLBuilder *v2.URLBuilder - -func init() { - u, err := url.Parse(REGISTRYSERVER) - if err != nil { - panic(fmt.Errorf("invalid registry url: %s", err)) - } - registryURLBuilder = v2.NewURLBuilder(u) -} - func getV2Builder(e *Endpoint) *v2.URLBuilder { - return registryURLBuilder + if e.URLBuilder == nil { + e.URLBuilder = v2.NewURLBuilder(e.URL) + } + return e.URLBuilder } // GetV2Authorization gets the authorization needed to the given image // If readonly access is requested, then only the authorization may // only be used for Get operations. -func (r *Session) GetV2Authorization(imageName string, readOnly bool) (*RequestAuthorization, error) { +func (r *Session) GetV2Authorization(imageName string, readOnly bool) (auth *RequestAuthorization, err error) { scopes := []string{"pull"} if !readOnly { scopes = append(scopes, "push") } - return NewRequestAuthorization(r.GetAuthConfig(true), r.indexEndpoint, "repository", imageName, scopes) + var registry *Endpoint + if r.indexEndpoint.URL.Host == IndexServerURL.Host { + registry, err = NewEndpoint(REGISTRYSERVER, nil) + if err != nil { + return + } + } else { + registry = r.indexEndpoint + } + registry.URLBuilder = v2.NewURLBuilder(registry.URL) + r.indexEndpoint = registry + + log.Debugf("Getting authorization for %s %s", imageName, scopes) + return NewRequestAuthorization(r.GetAuthConfig(true), registry, "repository", imageName, scopes) } // From d094eb6f7ffe6b608ecde54297e107e5caa0954d Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Fri, 19 Dec 2014 16:14:04 -0800 Subject: [PATCH 302/513] Get token on each request Signed-off-by: Derek McGowan --- registry/auth.go | 60 ++++++++++++++++++++++++++---------------- registry/session_v2.go | 34 +++++++++++++++++------- 2 files changed, 62 insertions(+), 32 deletions(-) diff --git a/registry/auth.go b/registry/auth.go index b138fb530..1e1c7ddb8 100644 --- a/registry/auth.go +++ b/registry/auth.go @@ -38,56 +38,70 @@ type ConfigFile struct { } type RequestAuthorization struct { - Token string - Username string - Password string + authConfig *AuthConfig + registryEndpoint *Endpoint + resource string + scope string + actions []string } -func NewRequestAuthorization(authConfig *AuthConfig, registryEndpoint *Endpoint, resource, scope string, actions []string) (*RequestAuthorization, error) { - var auth RequestAuthorization +func NewRequestAuthorization(authConfig *AuthConfig, registryEndpoint *Endpoint, resource, scope string, actions []string) *RequestAuthorization { + return &RequestAuthorization{ + authConfig: authConfig, + registryEndpoint: registryEndpoint, + resource: resource, + scope: scope, + actions: actions, + } +} +func (auth *RequestAuthorization) getToken() (string, error) { + // TODO check if already has token and before expiration client := &http.Client{ Transport: &http.Transport{ DisableKeepAlives: true, - Proxy: http.ProxyFromEnvironment, - }, + Proxy: http.ProxyFromEnvironment}, CheckRedirect: AddRequiredHeadersToRedirectedRequests, } factory := HTTPRequestFactory(nil) - for _, challenge := range registryEndpoint.AuthChallenges { - log.Debugf("Using %q auth challenge with params %s for %s", challenge.Scheme, challenge.Parameters, authConfig.Username) - + for _, challenge := range auth.registryEndpoint.AuthChallenges { switch strings.ToLower(challenge.Scheme) { case "basic": - auth.Username = authConfig.Username - auth.Password = authConfig.Password + // no token necessary case "bearer": + log.Debugf("Getting bearer token with %s for %s", challenge.Parameters, auth.authConfig.Username) params := map[string]string{} for k, v := range challenge.Parameters { params[k] = v } - params["scope"] = fmt.Sprintf("%s:%s:%s", resource, scope, strings.Join(actions, ",")) - token, err := getToken(authConfig.Username, authConfig.Password, params, registryEndpoint, client, factory) + params["scope"] = fmt.Sprintf("%s:%s:%s", auth.resource, auth.scope, strings.Join(auth.actions, ",")) + token, err := getToken(auth.authConfig.Username, auth.authConfig.Password, params, auth.registryEndpoint, client, factory) if err != nil { - return nil, err + return "", err } + // TODO cache token and set expiration to one minute from now - auth.Token = token + return token, nil default: log.Infof("Unsupported auth scheme: %q", challenge.Scheme) } } - - return &auth, nil + // TODO no expiration, do not reattempt to get a token + return "", nil } -func (auth *RequestAuthorization) Authorize(req *http.Request) { - if auth.Token != "" { - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", auth.Token)) - } else if auth.Username != "" && auth.Password != "" { - req.SetBasicAuth(auth.Username, auth.Password) +func (auth *RequestAuthorization) Authorize(req *http.Request) error { + token, err := auth.getToken() + if err != nil { + return err } + if token != "" { + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + } else if auth.authConfig.Username != "" && auth.authConfig.Password != "" { + req.SetBasicAuth(auth.authConfig.Username, auth.authConfig.Password) + } + return nil } // create a base64 encoded auth string to store in config diff --git a/registry/session_v2.go b/registry/session_v2.go index 2304a6134..491cd2c6e 100644 --- a/registry/session_v2.go +++ b/registry/session_v2.go @@ -42,7 +42,7 @@ func (r *Session) GetV2Authorization(imageName string, readOnly bool) (auth *Req r.indexEndpoint = registry log.Debugf("Getting authorization for %s %s", imageName, scopes) - return NewRequestAuthorization(r.GetAuthConfig(true), registry, "repository", imageName, scopes) + return NewRequestAuthorization(r.GetAuthConfig(true), registry, "repository", imageName, scopes), nil } // @@ -65,7 +65,9 @@ func (r *Session) GetV2ImageManifest(imageName, tagName string, auth *RequestAut if err != nil { return nil, err } - auth.Authorize(req) + if err := auth.Authorize(req) { + return nil, err + } res, _, err := r.doRequest(req) if err != nil { return nil, err @@ -103,7 +105,9 @@ func (r *Session) PostV2ImageMountBlob(imageName, sumType, sum string, auth *Req if err != nil { return false, err } - auth.Authorize(req) + if err := auth.Authorize(req) { + return nil, err + } res, _, err := r.doRequest(req) if err != nil { return false, err @@ -132,7 +136,9 @@ func (r *Session) GetV2ImageBlob(imageName, sumType, sum string, blobWrtr io.Wri if err != nil { return err } - auth.Authorize(req) + if err := auth.Authorize(req) { + return nil, err + } res, _, err := r.doRequest(req) if err != nil { return err @@ -161,7 +167,9 @@ func (r *Session) GetV2ImageBlobReader(imageName, sumType, sum string, auth *Req if err != nil { return nil, 0, err } - auth.Authorize(req) + if err := auth.Authorize(req) { + return nil, err + } res, _, err := r.doRequest(req) if err != nil { return nil, 0, err @@ -196,7 +204,9 @@ func (r *Session) PutV2ImageBlob(imageName, sumType, sumStr string, blobRdr io.R return err } - auth.Authorize(req) + if err := auth.Authorize(req) { + return nil, err + } res, _, err := r.doRequest(req) if err != nil { return err @@ -212,7 +222,9 @@ func (r *Session) PutV2ImageBlob(imageName, sumType, sumStr string, blobRdr io.R queryParams := url.Values{} queryParams.Add("digest", sumType+":"+sumStr) req.URL.RawQuery = queryParams.Encode() - auth.Authorize(req) + if err := auth.Authorize(req) { + return nil, err + } res, _, err = r.doRequest(req) if err != nil { return err @@ -242,7 +254,9 @@ func (r *Session) PutV2ImageManifest(imageName, tagName string, manifestRdr io.R if err != nil { return err } - auth.Authorize(req) + if err := auth.Authorize(req) { + return nil, err + } res, _, err := r.doRequest(req) if err != nil { return err @@ -274,7 +288,9 @@ func (r *Session) GetV2RemoteTags(imageName string, auth *RequestAuthorization) if err != nil { return nil, err } - auth.Authorize(req) + if err := auth.Authorize(req) { + return nil, err + } res, _, err := r.doRequest(req) if err != nil { return nil, err From 1b43144ad8597d0d0ca089042c1162ba668259ab Mon Sep 17 00:00:00 2001 From: Stephen J Day Date: Mon, 22 Dec 2014 14:58:08 -0800 Subject: [PATCH 303/513] Correctly check and propagate errors in v2 session Signed-off-by: Stephen J Day --- registry/session_v2.go | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/registry/session_v2.go b/registry/session_v2.go index 491cd2c6e..411df46e3 100644 --- a/registry/session_v2.go +++ b/registry/session_v2.go @@ -65,7 +65,7 @@ func (r *Session) GetV2ImageManifest(imageName, tagName string, auth *RequestAut if err != nil { return nil, err } - if err := auth.Authorize(req) { + if err := auth.Authorize(req); err != nil { return nil, err } res, _, err := r.doRequest(req) @@ -105,8 +105,8 @@ func (r *Session) PostV2ImageMountBlob(imageName, sumType, sum string, auth *Req if err != nil { return false, err } - if err := auth.Authorize(req) { - return nil, err + if err := auth.Authorize(req); err != nil { + return false, err } res, _, err := r.doRequest(req) if err != nil { @@ -136,8 +136,8 @@ func (r *Session) GetV2ImageBlob(imageName, sumType, sum string, blobWrtr io.Wri if err != nil { return err } - if err := auth.Authorize(req) { - return nil, err + if err := auth.Authorize(req); err != nil { + return err } res, _, err := r.doRequest(req) if err != nil { @@ -167,8 +167,8 @@ func (r *Session) GetV2ImageBlobReader(imageName, sumType, sum string, auth *Req if err != nil { return nil, 0, err } - if err := auth.Authorize(req) { - return nil, err + if err := auth.Authorize(req); err != nil { + return nil, 0, err } res, _, err := r.doRequest(req) if err != nil { @@ -204,8 +204,8 @@ func (r *Session) PutV2ImageBlob(imageName, sumType, sumStr string, blobRdr io.R return err } - if err := auth.Authorize(req) { - return nil, err + if err := auth.Authorize(req); err != nil { + return err } res, _, err := r.doRequest(req) if err != nil { @@ -222,8 +222,8 @@ func (r *Session) PutV2ImageBlob(imageName, sumType, sumStr string, blobRdr io.R queryParams := url.Values{} queryParams.Add("digest", sumType+":"+sumStr) req.URL.RawQuery = queryParams.Encode() - if err := auth.Authorize(req) { - return nil, err + if err := auth.Authorize(req); err != nil { + return err } res, _, err = r.doRequest(req) if err != nil { @@ -254,8 +254,8 @@ func (r *Session) PutV2ImageManifest(imageName, tagName string, manifestRdr io.R if err != nil { return err } - if err := auth.Authorize(req) { - return nil, err + if err := auth.Authorize(req); err != nil { + return err } res, _, err := r.doRequest(req) if err != nil { @@ -288,7 +288,7 @@ func (r *Session) GetV2RemoteTags(imageName string, auth *RequestAuthorization) if err != nil { return nil, err } - if err := auth.Authorize(req) { + if err := auth.Authorize(req); err != nil { return nil, err } res, _, err := r.doRequest(req) From 7eeda3f14de744b98b1c5aca4f2ecce87a479baa Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Mon, 22 Dec 2014 18:58:01 -0800 Subject: [PATCH 304/513] Fix tests Signed-off-by: Derek McGowan --- utils/jsonmessage_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/jsonmessage_test.go b/utils/jsonmessage_test.go index 0ce9492c9..b9103da1a 100644 --- a/utils/jsonmessage_test.go +++ b/utils/jsonmessage_test.go @@ -30,7 +30,7 @@ func TestProgress(t *testing.T) { } // this number can't be negetive gh#7136 - expected = "[==============================================================>] 50 B/40 B" + expected = "[==================================================>] 50 B/40 B" jp4 := JSONProgress{Current: 50, Total: 40} if jp4.String() != expected { t.Fatalf("Expected %q, got %q", expected, jp4.String()) From 213e3d116642431adbe634d39740eddc5a81e063 Mon Sep 17 00:00:00 2001 From: Josh Hawn Date: Tue, 23 Dec 2014 13:40:06 -0800 Subject: [PATCH 305/513] Add Tarsum Calculation during v2 Pull operation While the v2 pull operation is writing the body of the layer blob to disk it now computes the tarsum checksum of the archive before extracting it to the backend storage driver. If the checksum does not match that from the image manifest an error is raised. Also adds more debug logging to the pull operation and fixes existing test cases which were failing. Adds a reverse lookup constructor to the tarsum package so that you can get a tarsum object using a checksum label. Docker-DCO-1.1-Signed-off-by: Josh Hawn (github: jlhawn) --- graph/pull.go | 30 ++++++++++++++++++++++++++++-- image/image.go | 39 ++++++++++++++++++++------------------- pkg/tarsum/tarsum.go | 39 +++++++++++++++++++++++++++++++++++++++ pkg/tarsum/versioning.go | 17 ++++++++++++----- registry/endpoint.go | 17 ++++++++++++----- registry/session_v2.go | 8 ++++++-- 6 files changed, 117 insertions(+), 33 deletions(-) diff --git a/graph/pull.go b/graph/pull.go index 0b75881cd..88e939a48 100644 --- a/graph/pull.go +++ b/graph/pull.go @@ -15,6 +15,7 @@ import ( log "github.com/Sirupsen/logrus" "github.com/docker/docker/engine" "github.com/docker/docker/image" + "github.com/docker/docker/pkg/tarsum" "github.com/docker/docker/registry" "github.com/docker/docker/utils" "github.com/docker/libtrust" @@ -112,6 +113,8 @@ func (s *TagStore) CmdPull(job *engine.Job) engine.Status { } defer s.poolRemove("pull", repoInfo.LocalName+":"+tag) + + log.Debugf("pulling image from host %q with remote name %q", repoInfo.Index.Name, repoInfo.RemoteName) endpoint, err := repoInfo.GetEndpoint() if err != nil { return job.Error(err) @@ -127,6 +130,10 @@ func (s *TagStore) CmdPull(job *engine.Job) engine.Status { logName += ":" + tag } + // Calling the v2 code path might change the session + // endpoint value, so save the original one! + originalSession := *r + if len(repoInfo.Index.Mirrors) == 0 && (repoInfo.Index.Official || endpoint.Version == registry.APIVersion2) { j := job.Eng.Job("trust_update_base") if err = j.Run(); err != nil { @@ -138,6 +145,7 @@ func (s *TagStore) CmdPull(job *engine.Job) engine.Status { return job.Errorf("error getting authorization: %s", err) } + log.Debugf("pulling v2 repository with local name %q", repoInfo.LocalName) if err := s.pullV2Repository(job.Eng, r, job.Stdout, repoInfo, tag, sf, job.GetenvBool("parallel"), auth); err == nil { if err = job.Eng.Job("log", "pull", logName, "").Run(); err != nil { log.Errorf("Error logging event 'pull' for %s: %s", logName, err) @@ -146,8 +154,13 @@ func (s *TagStore) CmdPull(job *engine.Job) engine.Status { } else if err != registry.ErrDoesNotExist { log.Errorf("Error from V2 registry: %s", err) } + + log.Debug("image does not exist on v2 registry, falling back to v1") } + r = &originalSession + + log.Debugf("pulling v1 repository with local name %q", repoInfo.LocalName) if err = s.pullRepository(r, job.Stdout, repoInfo, tag, sf, job.GetenvBool("parallel")); err != nil { return job.Error(err) } @@ -174,7 +187,7 @@ func (s *TagStore) pullRepository(r *registry.Session, out io.Writer, repoInfo * log.Debugf("Retrieving the tag list") tagsList, err := r.GetRemoteTags(repoData.Endpoints, repoInfo.RemoteName, repoData.Tokens) if err != nil { - log.Errorf("%v", err) + log.Errorf("unable to get remote tags: %s", err) return err } @@ -535,7 +548,20 @@ func (s *TagStore) pullV2Tag(eng *engine.Engine, r *registry.Session, out io.Wri return err } defer r.Close() - io.Copy(tmpFile, utils.ProgressReader(r, int(l), out, sf, false, utils.TruncateID(img.ID), "Downloading")) + + // Wrap the reader with the appropriate TarSum reader. + tarSumReader, err := tarsum.NewTarSumForLabel(r, true, sumType) + if err != nil { + return fmt.Errorf("unable to wrap image blob reader with TarSum: %s", err) + } + + io.Copy(tmpFile, utils.ProgressReader(ioutil.NopCloser(tarSumReader), int(l), out, sf, false, utils.TruncateID(img.ID), "Downloading")) + + out.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Verifying Checksum", nil)) + + if finalChecksum := tarSumReader.Sum(nil); !strings.EqualFold(finalChecksum, sumStr) { + return fmt.Errorf("image verification failed: checksum mismatch - expected %q but got %q", sumStr, finalChecksum) + } out.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Download complete", nil)) diff --git a/image/image.go b/image/image.go index 8cd9aa375..7664602cd 100644 --- a/image/image.go +++ b/image/image.go @@ -94,28 +94,29 @@ func StoreImage(img *Image, layerData archive.ArchiveReader, root string) error // If layerData is not nil, unpack it into the new layer if layerData != nil { - layerDataDecompressed, err := archive.DecompressStream(layerData) - if err != nil { + // If the image doesn't have a checksum, we should add it. The layer + // checksums are verified when they are pulled from a remote, but when + // a container is committed it should be added here. + if img.Checksum == "" { + layerDataDecompressed, err := archive.DecompressStream(layerData) + if err != nil { + return err + } + defer layerDataDecompressed.Close() + + if layerTarSum, err = tarsum.NewTarSum(layerDataDecompressed, true, tarsum.VersionDev); err != nil { + return err + } + + if size, err = driver.ApplyDiff(img.ID, img.Parent, layerTarSum); err != nil { + return err + } + + img.Checksum = layerTarSum.Sum(nil) + } else if size, err = driver.ApplyDiff(img.ID, img.Parent, layerData); err != nil { return err } - defer layerDataDecompressed.Close() - - if layerTarSum, err = tarsum.NewTarSum(layerDataDecompressed, true, tarsum.VersionDev); err != nil { - return err - } - - if size, err = driver.ApplyDiff(img.ID, img.Parent, layerTarSum); err != nil { - return err - } - - checksum := layerTarSum.Sum(nil) - - if img.Checksum != "" && img.Checksum != checksum { - log.Warnf("image layer checksum mismatch: computed %q, expected %q", checksum, img.Checksum) - } - - img.Checksum = checksum } img.Size = size diff --git a/pkg/tarsum/tarsum.go b/pkg/tarsum/tarsum.go index c9f1315cf..c6a7294e7 100644 --- a/pkg/tarsum/tarsum.go +++ b/pkg/tarsum/tarsum.go @@ -3,8 +3,11 @@ package tarsum import ( "bytes" "compress/gzip" + "crypto" "crypto/sha256" "encoding/hex" + "errors" + "fmt" "hash" "io" "strings" @@ -39,6 +42,30 @@ func NewTarSumHash(r io.Reader, dc bool, v Version, tHash THash) (TarSum, error) return ts, err } +// Create a new TarSum using the provided TarSum version+hash label. +func NewTarSumForLabel(r io.Reader, disableCompression bool, label string) (TarSum, error) { + parts := strings.SplitN(label, "+", 2) + if len(parts) != 2 { + return nil, errors.New("tarsum label string should be of the form: {tarsum_version}+{hash_name}") + } + + versionName, hashName := parts[0], parts[1] + + version, ok := tarSumVersionsByName[versionName] + if !ok { + return nil, fmt.Errorf("unknown TarSum version name: %q", versionName) + } + + hashConfig, ok := standardHashConfigs[hashName] + if !ok { + return nil, fmt.Errorf("unknown TarSum hash name: %q", hashName) + } + + tHash := NewTHash(hashConfig.name, hashConfig.hash.New) + + return NewTarSumHash(r, disableCompression, version, tHash) +} + // TarSum is the generic interface for calculating fixed time // checksums of a tar archive type TarSum interface { @@ -89,6 +116,18 @@ func NewTHash(name string, h func() hash.Hash) THash { return simpleTHash{n: name, h: h} } +type tHashConfig struct { + name string + hash crypto.Hash +} + +var ( + standardHashConfigs = map[string]tHashConfig{ + "sha256": {name: "sha256", hash: crypto.SHA256}, + "sha512": {name: "sha512", hash: crypto.SHA512}, + } +) + // TarSum default is "sha256" var DefaultTHash = NewTHash("sha256", sha256.New) diff --git a/pkg/tarsum/versioning.go b/pkg/tarsum/versioning.go index 3a656612f..be1d07040 100644 --- a/pkg/tarsum/versioning.go +++ b/pkg/tarsum/versioning.go @@ -31,11 +31,18 @@ func GetVersions() []Version { return v } -var tarSumVersions = map[Version]string{ - Version0: "tarsum", - Version1: "tarsum.v1", - VersionDev: "tarsum.dev", -} +var ( + tarSumVersions = map[Version]string{ + Version0: "tarsum", + Version1: "tarsum.v1", + VersionDev: "tarsum.dev", + } + tarSumVersionsByName = map[string]Version{ + "tarsum": Version0, + "tarsum.v1": Version1, + "tarsum.dev": VersionDev, + } +) func (tsv Version) String() string { return tarSumVersions[tsv] diff --git a/registry/endpoint.go b/registry/endpoint.go index 9a783f1f0..9ca9ed8b9 100644 --- a/registry/endpoint.go +++ b/registry/endpoint.go @@ -47,16 +47,23 @@ func NewEndpoint(index *IndexInfo) (*Endpoint, error) { if err != nil { return nil, err } + if err := validateEndpoint(endpoint); err != nil { + return nil, err + } + return endpoint, nil +} + +func validateEndpoint(endpoint *Endpoint) error { log.Debugf("pinging registry endpoint %s", endpoint) // Try HTTPS ping to registry endpoint.URL.Scheme = "https" if _, err := endpoint.Ping(); err != nil { - if index.Secure { + if endpoint.IsSecure { // If registry is secure and HTTPS failed, show user the error and tell them about `--insecure-registry` // in case that's what they need. DO NOT accept unknown CA certificates, and DO NOT fallback to HTTP. - return nil, fmt.Errorf("invalid registry endpoint %s: %v. If this private registry supports only HTTP or HTTPS with an unknown CA certificate, please add `--insecure-registry %s` to the daemon's arguments. In the case of HTTPS, if you have access to the registry's CA certificate, no need for the flag; simply place the CA certificate at /etc/docker/certs.d/%s/ca.crt", endpoint, err, endpoint.URL.Host, endpoint.URL.Host) + return fmt.Errorf("invalid registry endpoint %s: %v. If this private registry supports only HTTP or HTTPS with an unknown CA certificate, please add `--insecure-registry %s` to the daemon's arguments. In the case of HTTPS, if you have access to the registry's CA certificate, no need for the flag; simply place the CA certificate at /etc/docker/certs.d/%s/ca.crt", endpoint, err, endpoint.URL.Host, endpoint.URL.Host) } // If registry is insecure and HTTPS failed, fallback to HTTP. @@ -65,13 +72,13 @@ func NewEndpoint(index *IndexInfo) (*Endpoint, error) { var err2 error if _, err2 = endpoint.Ping(); err2 == nil { - return endpoint, nil + return nil } - return nil, fmt.Errorf("invalid registry endpoint %q. HTTPS attempt: %v. HTTP attempt: %v", endpoint, err, err2) + return fmt.Errorf("invalid registry endpoint %q. HTTPS attempt: %v. HTTP attempt: %v", endpoint, err, err2) } - return endpoint, nil + return nil } func newEndpoint(address string, secure bool) (*Endpoint, error) { diff --git a/registry/session_v2.go b/registry/session_v2.go index 411df46e3..031122dcf 100644 --- a/registry/session_v2.go +++ b/registry/session_v2.go @@ -30,8 +30,12 @@ func (r *Session) GetV2Authorization(imageName string, readOnly bool) (auth *Req } var registry *Endpoint - if r.indexEndpoint.URL.Host == IndexServerURL.Host { - registry, err = NewEndpoint(REGISTRYSERVER, nil) + if r.indexEndpoint.String() == IndexServerAddress() { + registry, err = newEndpoint(REGISTRYSERVER, true) + if err != nil { + return + } + err = validateEndpoint(registry) if err != nil { return } From 25945a40c4f352a754cbd8dba9c846c7539fe463 Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Fri, 2 Jan 2015 11:13:11 -0800 Subject: [PATCH 306/513] Refactor from feedback Signed-off-by: Derek McGowan (github: dmcgowan) --- docker/docker.go | 16 +-------- graph/manifest.go | 79 ++++++++++++++++++++++++++++++++++++++++-- graph/pull.go | 67 ++--------------------------------- graph/push.go | 10 ++---- registry/session_v2.go | 3 +- 5 files changed, 82 insertions(+), 93 deletions(-) diff --git a/docker/docker.go b/docker/docker.go index 84ffeace9..92f5f1460 100644 --- a/docker/docker.go +++ b/docker/docker.go @@ -6,7 +6,6 @@ import ( "fmt" "io/ioutil" "os" - "path" "strings" log "github.com/Sirupsen/logrus" @@ -16,7 +15,6 @@ import ( flag "github.com/docker/docker/pkg/mflag" "github.com/docker/docker/pkg/reexec" "github.com/docker/docker/utils" - "github.com/docker/libtrust" ) const ( @@ -79,22 +77,10 @@ func main() { } protoAddrParts := strings.SplitN(flHosts[0], "://", 2) - err := os.MkdirAll(path.Dir(*flTrustKey), 0700) + trustKey, err := api.LoadOrCreateTrustKey(*flTrustKey) if err != nil { log.Fatal(err) } - trustKey, err := libtrust.LoadKeyFile(*flTrustKey) - if err == libtrust.ErrKeyFileDoesNotExist { - trustKey, err = libtrust.GenerateECP256PrivateKey() - if err != nil { - log.Fatalf("Error generating key: %s", err) - } - if err := libtrust.SaveKey(*flTrustKey, trustKey); err != nil { - log.Fatalf("Error saving key file: %s", err) - } - } else if err != nil { - log.Fatalf("Error loading key file: %s", err) - } var ( cli *client.DockerCli diff --git a/graph/manifest.go b/graph/manifest.go index 54d6083cb..3d4ab1c5d 100644 --- a/graph/manifest.go +++ b/graph/manifest.go @@ -1,6 +1,7 @@ package graph import ( + "bytes" "encoding/json" "errors" "fmt" @@ -8,10 +9,12 @@ import ( "io/ioutil" "path" + log "github.com/Sirupsen/logrus" "github.com/docker/docker/engine" "github.com/docker/docker/pkg/tarsum" "github.com/docker/docker/registry" "github.com/docker/docker/runconfig" + "github.com/docker/libtrust" ) func (s *TagStore) CmdManifest(job *engine.Job) engine.Status { @@ -49,11 +52,15 @@ func (s *TagStore) newManifest(localName, remoteName, tag string) ([]byte, error Tag: tag, SchemaVersion: 1, } - localRepo, exists := s.Repositories[localName] - if !exists { + localRepo, err := s.Get(localName) + if err != nil { + return nil, err + } + if localRepo == nil { return nil, fmt.Errorf("Repo does not exist: %s", localName) } + // Get the top-most layer id which the tag points to layerId, exists := localRepo[tag] if !exists { return nil, fmt.Errorf("Tag does not exist for %s: %s", localName, tag) @@ -102,7 +109,6 @@ func (s *TagStore) newManifest(localName, remoteName, tag string) ([]byte, error } tarId := tarSum.Sum(nil) - // Save tarsum to image json manifest.FSLayers = append(manifest.FSLayers, ®istry.FSLayer{BlobSum: tarId}) @@ -121,3 +127,70 @@ func (s *TagStore) newManifest(localName, remoteName, tag string) ([]byte, error return manifestBytes, nil } + +func (s *TagStore) verifyManifest(eng *engine.Engine, manifestBytes []byte) (*registry.ManifestData, bool, error) { + sig, err := libtrust.ParsePrettySignature(manifestBytes, "signatures") + if err != nil { + return nil, false, fmt.Errorf("error parsing payload: %s", err) + } + + keys, err := sig.Verify() + if err != nil { + return nil, false, fmt.Errorf("error verifying payload: %s", err) + } + + payload, err := sig.Payload() + if err != nil { + return nil, false, fmt.Errorf("error retrieving payload: %s", err) + } + + var manifest registry.ManifestData + if err := json.Unmarshal(payload, &manifest); err != nil { + return nil, false, fmt.Errorf("error unmarshalling manifest: %s", err) + } + if manifest.SchemaVersion != 1 { + return nil, false, fmt.Errorf("unsupported schema version: %d", manifest.SchemaVersion) + } + + var verified bool + for _, key := range keys { + job := eng.Job("trust_key_check") + b, err := key.MarshalJSON() + if err != nil { + return nil, false, fmt.Errorf("error marshalling public key: %s", err) + } + namespace := manifest.Name + if namespace[0] != '/' { + namespace = "/" + namespace + } + stdoutBuffer := bytes.NewBuffer(nil) + + job.Args = append(job.Args, namespace) + job.Setenv("PublicKey", string(b)) + // Check key has read/write permission (0x03) + job.SetenvInt("Permission", 0x03) + job.Stdout.Add(stdoutBuffer) + if err = job.Run(); err != nil { + return nil, false, fmt.Errorf("error running key check: %s", err) + } + result := engine.Tail(stdoutBuffer, 1) + log.Debugf("Key check result: %q", result) + if result == "verified" { + verified = true + } + } + + return &manifest, verified, nil +} + +func checkValidManifest(manifest *registry.ManifestData) error { + if len(manifest.FSLayers) != len(manifest.History) { + return fmt.Errorf("length of history not equal to number of layers") + } + + if len(manifest.FSLayers) == 0 { + return fmt.Errorf("no FSLayers in manifest") + } + + return nil +} diff --git a/graph/pull.go b/graph/pull.go index 88e939a48..b2710e9b6 100644 --- a/graph/pull.go +++ b/graph/pull.go @@ -1,8 +1,6 @@ package graph import ( - "bytes" - "encoding/json" "fmt" "io" "io/ioutil" @@ -18,63 +16,8 @@ import ( "github.com/docker/docker/pkg/tarsum" "github.com/docker/docker/registry" "github.com/docker/docker/utils" - "github.com/docker/libtrust" ) -func (s *TagStore) verifyManifest(eng *engine.Engine, manifestBytes []byte) (*registry.ManifestData, bool, error) { - sig, err := libtrust.ParsePrettySignature(manifestBytes, "signatures") - if err != nil { - return nil, false, fmt.Errorf("error parsing payload: %s", err) - } - keys, err := sig.Verify() - if err != nil { - return nil, false, fmt.Errorf("error verifying payload: %s", err) - } - - payload, err := sig.Payload() - if err != nil { - return nil, false, fmt.Errorf("error retrieving payload: %s", err) - } - - var manifest registry.ManifestData - if err := json.Unmarshal(payload, &manifest); err != nil { - return nil, false, fmt.Errorf("error unmarshalling manifest: %s", err) - } - if manifest.SchemaVersion != 1 { - return nil, false, fmt.Errorf("unsupported schema version: %d", manifest.SchemaVersion) - } - - var verified bool - for _, key := range keys { - job := eng.Job("trust_key_check") - b, err := key.MarshalJSON() - if err != nil { - return nil, false, fmt.Errorf("error marshalling public key: %s", err) - } - namespace := manifest.Name - if namespace[0] != '/' { - namespace = "/" + namespace - } - stdoutBuffer := bytes.NewBuffer(nil) - - job.Args = append(job.Args, namespace) - job.Setenv("PublicKey", string(b)) - // Check key has read/write permission (0x03) - job.SetenvInt("Permission", 0x03) - job.Stdout.Add(stdoutBuffer) - if err = job.Run(); err != nil { - return nil, false, fmt.Errorf("error running key check: %s", err) - } - result := engine.Tail(stdoutBuffer, 1) - log.Debugf("Key check result: %q", result) - if result == "verified" { - verified = true - } - } - - return &manifest, verified, nil -} - func (s *TagStore) CmdPull(job *engine.Job) engine.Status { if n := len(job.Args); n != 1 && n != 2 { return job.Errorf("Usage: %s IMAGE [TAG]", job.Name) @@ -113,7 +56,6 @@ func (s *TagStore) CmdPull(job *engine.Job) engine.Status { } defer s.poolRemove("pull", repoInfo.LocalName+":"+tag) - log.Debugf("pulling image from host %q with remote name %q", repoInfo.Index.Name, repoInfo.RemoteName) endpoint, err := repoInfo.GetEndpoint() if err != nil { @@ -484,8 +426,8 @@ func (s *TagStore) pullV2Tag(eng *engine.Engine, r *registry.Session, out io.Wri return false, fmt.Errorf("error verifying manifest: %s", err) } - if len(manifest.FSLayers) != len(manifest.History) { - return false, fmt.Errorf("length of history not equal to number of layers") + if err := checkValidManifest(manifest); err != nil { + return false, err } if verified { @@ -493,11 +435,6 @@ func (s *TagStore) pullV2Tag(eng *engine.Engine, r *registry.Session, out io.Wri } else { out.Write(sf.FormatStatus(tag, "Pulling from %s", repoInfo.CanonicalName)) } - - if len(manifest.FSLayers) == 0 { - return false, fmt.Errorf("no blobSums in manifest") - } - downloads := make([]downloadInfo, len(manifest.FSLayers)) for i := len(manifest.FSLayers) - 1; i >= 0; i-- { diff --git a/graph/push.go b/graph/push.go index 88b207a45..8d51e2879 100644 --- a/graph/push.go +++ b/graph/push.go @@ -311,14 +311,13 @@ func (s *TagStore) CmdPush(job *engine.Job) engine.Status { // TODO Create manifest and sign } - // try via manifest manifest, verified, err := s.verifyManifest(job.Eng, []byte(manifestBytes)) if err != nil { return job.Errorf("error verifying manifest: %s", err) } - if len(manifest.FSLayers) != len(manifest.History) { - return job.Errorf("length of history not equal to number of layers") + if err := checkValidManifest(manifest); err != nil { + return job.Errorf("invalid manifest: %s", err) } if !verified { @@ -337,11 +336,6 @@ func (s *TagStore) CmdPush(job *engine.Job) engine.Status { } manifestSum := sumParts[1] - // for each layer, check if it exists ... - // XXX wait this requires having the TarSum of the layer.tar first - // skip this step for now. Just push the layer every time for this naive implementation - //shouldPush, err := r.PostV2ImageMountBlob(imageName, sumType, sum string, token []string) - img, err := image.NewImgJSON(imgJSON) if err != nil { return job.Errorf("Failed to parse json: %s", err) diff --git a/registry/session_v2.go b/registry/session_v2.go index 031122dcf..0e03f4a9c 100644 --- a/registry/session_v2.go +++ b/registry/session_v2.go @@ -5,7 +5,6 @@ import ( "fmt" "io" "io/ioutil" - "net/url" "strconv" log "github.com/Sirupsen/logrus" @@ -223,7 +222,7 @@ func (r *Session) PutV2ImageBlob(imageName, sumType, sumStr string, blobRdr io.R if err != nil { return err } - queryParams := url.Values{} + queryParams := req.URL.Query() queryParams.Add("digest", sumType+":"+sumStr) req.URL.RawQuery = queryParams.Encode() if err := auth.Authorize(req); err != nil { From 8ceb9d20d66097b90ca3a529da258669ef6b8412 Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Wed, 7 Jan 2015 14:59:12 -0800 Subject: [PATCH 307/513] Update push to sign with the daemon's key when no manifest is given Signed-off-by: Derek McGowan (github: dmcgowan) --- daemon/daemon.go | 12 ++++++------ graph/push.go | 22 +++++++++++++++++++++- graph/tags.go | 5 ++++- graph/tags_unit_test.go | 2 +- 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/daemon/daemon.go b/daemon/daemon.go index 9f5df4c3c..8a5db74a3 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -895,8 +895,13 @@ func NewDaemonFromDirectory(config *Config, eng *engine.Engine) (*Daemon, error) return nil, err } + trustKey, err := api.LoadOrCreateTrustKey(config.TrustKeyPath) + if err != nil { + return nil, err + } + log.Debugf("Creating repository list") - repositories, err := graph.NewTagStore(path.Join(config.Root, "repositories-"+driver.String()), g) + repositories, err := graph.NewTagStore(path.Join(config.Root, "repositories-"+driver.String()), g, trustKey) if err != nil { return nil, fmt.Errorf("Couldn't create Tag store: %s", err) } @@ -961,11 +966,6 @@ func NewDaemonFromDirectory(config *Config, eng *engine.Engine) (*Daemon, error) return nil, err } - trustKey, err := api.LoadOrCreateTrustKey(config.TrustKeyPath) - if err != nil { - return nil, err - } - daemon := &Daemon{ ID: trustKey.PublicKey().KeyID(), repository: daemonRepo, diff --git a/graph/push.go b/graph/push.go index 8d51e2879..4d6b1e083 100644 --- a/graph/push.go +++ b/graph/push.go @@ -16,6 +16,7 @@ import ( "github.com/docker/docker/pkg/archive" "github.com/docker/docker/registry" "github.com/docker/docker/utils" + "github.com/docker/libtrust" ) // Retrieve the all the images to be uploaded in the correct order @@ -308,7 +309,26 @@ func (s *TagStore) CmdPush(job *engine.Job) engine.Status { } if len(manifestBytes) == 0 { - // TODO Create manifest and sign + mBytes, err := s.newManifest(repoInfo.LocalName, repoInfo.RemoteName, tag) + if err != nil { + return job.Error(err) + } + js, err := libtrust.NewJSONSignature(mBytes) + if err != nil { + return job.Error(err) + } + + if err = js.Sign(s.trustKey); err != nil { + return job.Error(err) + } + + signedBody, err := js.PrettySignature("signatures") + if err != nil { + return job.Error(err) + } + log.Infof("Signed manifest using daemon's key: %s", s.trustKey.KeyID()) + + manifestBytes = string(signedBody) } manifest, verified, err := s.verifyManifest(job.Eng, []byte(manifestBytes)) diff --git a/graph/tags.go b/graph/tags.go index 998b447e6..6bdb296cd 100644 --- a/graph/tags.go +++ b/graph/tags.go @@ -15,6 +15,7 @@ import ( "github.com/docker/docker/pkg/parsers" "github.com/docker/docker/registry" "github.com/docker/docker/utils" + "github.com/docker/libtrust" ) const DEFAULTTAG = "latest" @@ -27,6 +28,7 @@ type TagStore struct { path string graph *Graph Repositories map[string]Repository + trustKey libtrust.PrivateKey sync.Mutex // FIXME: move push/pull-related fields // to a helper type @@ -54,7 +56,7 @@ func (r Repository) Contains(u Repository) bool { return true } -func NewTagStore(path string, graph *Graph) (*TagStore, error) { +func NewTagStore(path string, graph *Graph, key libtrust.PrivateKey) (*TagStore, error) { abspath, err := filepath.Abs(path) if err != nil { return nil, err @@ -63,6 +65,7 @@ func NewTagStore(path string, graph *Graph) (*TagStore, error) { store := &TagStore{ path: abspath, graph: graph, + trustKey: key, Repositories: make(map[string]Repository), pullingPool: make(map[string]chan struct{}), pushingPool: make(map[string]chan struct{}), diff --git a/graph/tags_unit_test.go b/graph/tags_unit_test.go index 45dad6295..58ad8ed87 100644 --- a/graph/tags_unit_test.go +++ b/graph/tags_unit_test.go @@ -57,7 +57,7 @@ func mkTestTagStore(root string, t *testing.T) *TagStore { if err != nil { t.Fatal(err) } - store, err := NewTagStore(path.Join(root, "tags"), graph) + store, err := NewTagStore(path.Join(root, "tags"), graph, nil) if err != nil { t.Fatal(err) } From 1a9cdb13943c6af397472e235708cb10824681cd Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Wed, 7 Jan 2015 15:55:29 -0800 Subject: [PATCH 308/513] Fix list tags Signed-off-by: Derek McGowan (github: dmcgowan) --- registry/session_v2.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/registry/session_v2.go b/registry/session_v2.go index 0e03f4a9c..b08f4cf0d 100644 --- a/registry/session_v2.go +++ b/registry/session_v2.go @@ -277,6 +277,11 @@ func (r *Session) PutV2ImageManifest(imageName, tagName string, manifestRdr io.R return nil } +type remoteTags struct { + name string + tags []string +} + // Given a repository name, returns a json array of string tags func (r *Session) GetV2RemoteTags(imageName string, auth *RequestAuthorization) ([]string, error) { routeURL, err := getV2Builder(r.indexEndpoint).BuildTagsURL(imageName) @@ -309,10 +314,10 @@ func (r *Session) GetV2RemoteTags(imageName string, auth *RequestAuthorization) } decoder := json.NewDecoder(res.Body) - var tags []string - err = decoder.Decode(&tags) + var remote remoteTags + err = decoder.Decode(&remote) if err != nil { return nil, fmt.Errorf("Error while decoding the http response: %s", err) } - return tags, nil + return remote.tags, nil } From 9a38aa0279ccae5aeded854a9cbbd7e398088ab2 Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Mon, 12 Jan 2015 14:17:50 -0800 Subject: [PATCH 309/513] Fix integration test failures Signed-off-by: Derek McGowan (github: dmcgowan) --- graph/pull.go | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/graph/pull.go b/graph/pull.go index b2710e9b6..1c4bb9d88 100644 --- a/graph/pull.go +++ b/graph/pull.go @@ -79,22 +79,23 @@ func (s *TagStore) CmdPull(job *engine.Job) engine.Status { if len(repoInfo.Index.Mirrors) == 0 && (repoInfo.Index.Official || endpoint.Version == registry.APIVersion2) { j := job.Eng.Job("trust_update_base") if err = j.Run(); err != nil { - return job.Errorf("error updating trust base graph: %s", err) + log.Errorf("error updating trust base graph: %s", err) } auth, err := r.GetV2Authorization(repoInfo.RemoteName, true) if err != nil { - return job.Errorf("error getting authorization: %s", err) - } + log.Errorf("error getting authorization: %s", err) + } else { - log.Debugf("pulling v2 repository with local name %q", repoInfo.LocalName) - if err := s.pullV2Repository(job.Eng, r, job.Stdout, repoInfo, tag, sf, job.GetenvBool("parallel"), auth); err == nil { - if err = job.Eng.Job("log", "pull", logName, "").Run(); err != nil { - log.Errorf("Error logging event 'pull' for %s: %s", logName, err) + log.Debugf("pulling v2 repository with local name %q", repoInfo.LocalName) + if err := s.pullV2Repository(job.Eng, r, job.Stdout, repoInfo, tag, sf, job.GetenvBool("parallel"), auth); err == nil { + if err = job.Eng.Job("log", "pull", logName, "").Run(); err != nil { + log.Errorf("Error logging event 'pull' for %s: %s", logName, err) + } + return engine.StatusOK + } else if err != registry.ErrDoesNotExist { + log.Errorf("Error from V2 registry: %s", err) } - return engine.StatusOK - } else if err != registry.ErrDoesNotExist { - log.Errorf("Error from V2 registry: %s", err) } log.Debug("image does not exist on v2 registry, falling back to v1") From ef96c28754706da921644e5cf9202f9cc78d4c7e Mon Sep 17 00:00:00 2001 From: Alexander Morozov Date: Mon, 12 Jan 2015 11:47:40 -0800 Subject: [PATCH 310/513] Install registry V2 in image Signed-off-by: Alexander Morozov --- Dockerfile | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Dockerfile b/Dockerfile index 9ef05b561..6bad98720 100644 --- a/Dockerfile +++ b/Dockerfile @@ -148,6 +148,17 @@ RUN set -x \ && git clone -b v1.2 https://github.com/russross/blackfriday.git /go/src/github.com/russross/blackfriday \ && go install -v github.com/cpuguy83/go-md2man +# Install registry +COPY pkg/tarsum /go/src/github.com/docker/docker/pkg/tarsum +# REGISTRY_COMMIT gives us the repeatability guarantees we need +# (so that we're all testing the same version of the registry) +ENV REGISTRY_COMMIT 21a69f53b5c7986b831f33849d551cd59ec8cbd1 +RUN set -x \ + && git clone https://github.com/docker/distribution.git /go/src/github.com/docker/distribution \ + && (cd /go/src/github.com/docker/distribution && git checkout -q $REGISTRY_COMMIT) \ + && go get -d github.com/docker/distribution/cmd/registry \ + && go build -o /go/bin/registry-v2 github.com/docker/distribution/cmd/registry + # Wrap all commands in the "docker-in-docker" script to allow nested containers ENTRYPOINT ["hack/dind"] From 2fc2862a73dbbc612f59f61f66c465d2e48bcbea Mon Sep 17 00:00:00 2001 From: Alexander Morozov Date: Mon, 12 Jan 2015 13:26:49 -0800 Subject: [PATCH 311/513] RegistryV2 datastructure for tests Signed-off-by: Alexander Morozov --- integration-cli/registry.go | 60 +++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 integration-cli/registry.go diff --git a/integration-cli/registry.go b/integration-cli/registry.go new file mode 100644 index 000000000..f0ef05cca --- /dev/null +++ b/integration-cli/registry.go @@ -0,0 +1,60 @@ +package main + +import ( + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "testing" +) + +const v2binary = "registry-v2" + +type testRegistryV2 struct { + URL string + cmd *exec.Cmd + dir string +} + +func newTestRegistryV2(t *testing.T) (*testRegistryV2, error) { + template := `version: 0.1 +loglevel: debug +storage: + filesystem: + rootdirectory: %s +http: + addr: :%s` + tmp, err := ioutil.TempDir("", "registry-test-") + if err != nil { + return nil, err + } + confPath := filepath.Join(tmp, "config.yaml") + config, err := os.Create(confPath) + if err != nil { + return nil, err + } + if _, err := fmt.Fprintf(config, template, tmp, "5000"); err != nil { + os.RemoveAll(tmp) + return nil, err + } + + cmd := exec.Command(v2binary, confPath) + if err := cmd.Start(); err != nil { + os.RemoveAll(tmp) + if os.IsNotExist(err) { + t.Skip() + } + return nil, err + } + return &testRegistryV2{ + cmd: cmd, + dir: tmp, + URL: "localhost:5000", + }, nil +} + +func (r *testRegistryV2) Close() { + r.cmd.Process.Kill() + os.RemoveAll(r.dir) +} From f138f7bd50a1c5a435f3146f0b0298a2a4e260ce Mon Sep 17 00:00:00 2001 From: Alexander Morozov Date: Mon, 12 Jan 2015 14:30:19 -0800 Subject: [PATCH 312/513] Tests for push to registry v2 Signed-off-by: Alexander Morozov --- integration-cli/docker_cli_push_test.go | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/integration-cli/docker_cli_push_test.go b/integration-cli/docker_cli_push_test.go index 0dfd85a9d..a8c2ccdbc 100644 --- a/integration-cli/docker_cli_push_test.go +++ b/integration-cli/docker_cli_push_test.go @@ -10,29 +10,27 @@ import ( // pulling an image from the central registry should work func TestPushBusyboxImage(t *testing.T) { - // skip this test until we're able to use a registry - t.Skip() + reg, err := newTestRegistryV2(t) + if err != nil { + t.Fatal(err) + } + defer reg.Close() + repoName := fmt.Sprintf("%v/dockercli/busybox", reg.URL) // tag the image to upload it tot he private registry - repoName := fmt.Sprintf("%v/busybox", privateRegistryURL) tagCmd := exec.Command(dockerBinary, "tag", "busybox", repoName) if out, _, err := runCommandWithOutput(tagCmd); err != nil { t.Fatalf("image tagging failed: %s, %v", out, err) } - + defer deleteImages(repoName) pushCmd := exec.Command(dockerBinary, "push", repoName) if out, _, err := runCommandWithOutput(pushCmd); err != nil { t.Fatalf("pushing the image to the private registry has failed: %s, %v", out, err) } - - deleteImages(repoName) - logDone("push - push busybox to private registry") } // pushing an image without a prefix should throw an error func TestPushUnprefixedRepo(t *testing.T) { - // skip this test until we're able to use a registry - t.Skip() pushCmd := exec.Command(dockerBinary, "push", "busybox") if out, _, err := runCommandWithOutput(pushCmd); err == nil { t.Fatalf("pushing an unprefixed repo didn't result in a non-zero exit status: %s", out) From dbec2317e503b8a0190102332168f9d0256392b7 Mon Sep 17 00:00:00 2001 From: Arnaud Porterie Date: Tue, 13 Jan 2015 10:46:32 -0800 Subject: [PATCH 313/513] Add some push test coverage Signed-off-by: Arnaud Porterie --- integration-cli/docker_cli_push_test.go | 63 +++++++++++++++++++++---- integration-cli/docker_utils.go | 8 ++++ integration-cli/registry.go | 6 +-- 3 files changed, 63 insertions(+), 14 deletions(-) diff --git a/integration-cli/docker_cli_push_test.go b/integration-cli/docker_cli_push_test.go index a8c2ccdbc..484e5db70 100644 --- a/integration-cli/docker_cli_push_test.go +++ b/integration-cli/docker_cli_push_test.go @@ -3,30 +3,28 @@ package main import ( "fmt" "os/exec" + "strings" "testing" + "time" ) -// these tests need a freshly started empty private docker registry - // pulling an image from the central registry should work func TestPushBusyboxImage(t *testing.T) { - reg, err := newTestRegistryV2(t) - if err != nil { - t.Fatal(err) - } - defer reg.Close() - repoName := fmt.Sprintf("%v/dockercli/busybox", reg.URL) + defer setupRegistry(t)() + + repoName := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL) // tag the image to upload it tot he private registry tagCmd := exec.Command(dockerBinary, "tag", "busybox", repoName) if out, _, err := runCommandWithOutput(tagCmd); err != nil { t.Fatalf("image tagging failed: %s, %v", out, err) } defer deleteImages(repoName) + pushCmd := exec.Command(dockerBinary, "push", repoName) if out, _, err := runCommandWithOutput(pushCmd); err != nil { t.Fatalf("pushing the image to the private registry has failed: %s, %v", out, err) } - logDone("push - push busybox to private registry") + logDone("push - busybox to private registry") } // pushing an image without a prefix should throw an error @@ -35,5 +33,50 @@ func TestPushUnprefixedRepo(t *testing.T) { if out, _, err := runCommandWithOutput(pushCmd); err == nil { t.Fatalf("pushing an unprefixed repo didn't result in a non-zero exit status: %s", out) } - logDone("push - push unprefixed busybox repo --> must fail") + logDone("push - unprefixed busybox repo must fail") +} + +func TestPushUntagged(t *testing.T) { + defer setupRegistry(t)() + + repoName := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL) + + expected := "does not exist" + pushCmd := exec.Command(dockerBinary, "push", repoName) + if out, _, err := runCommandWithOutput(pushCmd); err == nil { + t.Fatalf("pushing the image to the private registry should have failed: outuput %q", out) + } else if !strings.Contains(out, expected) { + t.Fatalf("pushing the image failed with an unexpected message: expected %q, got %q", expected, out) + } + logDone("push - untagged image") +} + +func TestPushInterrupt(t *testing.T) { + defer setupRegistry(t)() + + repoName := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL) + // tag the image to upload it tot he private registry + tagCmd := exec.Command(dockerBinary, "tag", "busybox", repoName) + if out, _, err := runCommandWithOutput(tagCmd); err != nil { + t.Fatalf("image tagging failed: %s, %v", out, err) + } + defer deleteImages(repoName) + + pushCmd := exec.Command(dockerBinary, "push", repoName) + if err := pushCmd.Start(); err != nil { + t.Fatalf("Failed to start pushing to private registry: %v", err) + } + + // Interrupt push (yes, we have no idea at what point it will get killed). + time.Sleep(200 * time.Millisecond) + if err := pushCmd.Process.Kill(); err != nil { + t.Fatalf("Failed to kill push process: %v", err) + } + // Try agin + pushCmd = exec.Command(dockerBinary, "push", repoName) + if err := pushCmd.Start(); err != nil { + t.Fatalf("Failed to start pushing to private registry: %v", err) + } + + logDone("push - interrupted") } diff --git a/integration-cli/docker_utils.go b/integration-cli/docker_utils.go index c58bcfbf7..3af6d9a60 100644 --- a/integration-cli/docker_utils.go +++ b/integration-cli/docker_utils.go @@ -864,3 +864,11 @@ func readContainerFile(containerId, filename string) ([]byte, error) { return content, nil } + +func setupRegistry(t *testing.T) func() { + reg, err := newTestRegistryV2(t) + if err != nil { + t.Fatal(err) + } + return func() { reg.Close() } +} diff --git a/integration-cli/registry.go b/integration-cli/registry.go index f0ef05cca..00ba3030a 100644 --- a/integration-cli/registry.go +++ b/integration-cli/registry.go @@ -12,7 +12,6 @@ import ( const v2binary = "registry-v2" type testRegistryV2 struct { - URL string cmd *exec.Cmd dir string } @@ -24,7 +23,7 @@ storage: filesystem: rootdirectory: %s http: - addr: :%s` + addr: %s` tmp, err := ioutil.TempDir("", "registry-test-") if err != nil { return nil, err @@ -34,7 +33,7 @@ http: if err != nil { return nil, err } - if _, err := fmt.Fprintf(config, template, tmp, "5000"); err != nil { + if _, err := fmt.Fprintf(config, template, tmp, privateRegistryURL); err != nil { os.RemoveAll(tmp) return nil, err } @@ -50,7 +49,6 @@ http: return &testRegistryV2{ cmd: cmd, dir: tmp, - URL: "localhost:5000", }, nil } From 92d5eafe03eca8ca931ddca5ef7d6e41ca25caad Mon Sep 17 00:00:00 2001 From: Arnaud Porterie Date: Tue, 13 Jan 2015 15:19:44 -0800 Subject: [PATCH 314/513] Test pulling image with aliases Signed-off-by: Arnaud Porterie --- integration-cli/docker_cli_pull_test.go | 46 ++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/integration-cli/docker_cli_pull_test.go b/integration-cli/docker_cli_pull_test.go index bed015be0..e76f4ee95 100644 --- a/integration-cli/docker_cli_pull_test.go +++ b/integration-cli/docker_cli_pull_test.go @@ -1,12 +1,56 @@ package main import ( + "fmt" "os/exec" "strings" "testing" ) -// FIXME: we need a test for pulling all aliases for an image (issue #8141) +// See issue docker/docker#8141 +func TestPullImageWithAliases(t *testing.T) { + defer setupRegistry(t)() + + repoName := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL) + defer deleteImages(repoName) + + repos := []string{} + for _, tag := range []string{"recent", "fresh"} { + repos = append(repos, fmt.Sprintf("%v:%v", repoName, tag)) + } + + // Tag and push the same image multiple times. + for _, repo := range repos { + if out, _, err := runCommandWithOutput(exec.Command(dockerBinary, "tag", "busybox", repo)); err != nil { + t.Fatalf("Failed to tag image %v: error %v, output %q", repos, err, out) + } + if out, err := exec.Command(dockerBinary, "push", repo).CombinedOutput(); err != nil { + t.Fatalf("Failed to push image %v: error %v, output %q", err, string(out)) + } + } + + // Clear local images store. + args := append([]string{"rmi"}, repos...) + if out, err := exec.Command(dockerBinary, args...).CombinedOutput(); err != nil { + t.Fatalf("Failed to clean images: error %v, output %q", err, string(out)) + } + + // Pull a single tag and verify it doesn't bring down all aliases. + pullCmd := exec.Command(dockerBinary, "pull", repos[0]) + if out, _, err := runCommandWithOutput(pullCmd); err != nil { + t.Fatalf("Failed to pull %v: error %v, output %q", repoName, err, out) + } + if err := exec.Command(dockerBinary, "inspect", repos[0]).Run(); err != nil { + t.Fatalf("Image %v was not pulled down", repos[0]) + } + for _, repo := range repos[1:] { + if err := exec.Command(dockerBinary, "inspect", repo).Run(); err == nil { + t.Fatalf("Image %v shouldn't have been pulled down", repo) + } + } + + logDone("pull - image with aliases") +} // pulling an image from the central registry should work func TestPullImageFromCentralRegistry(t *testing.T) { From 750b41ced42bda0ccda405c1aa7c43ded5821e40 Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Tue, 13 Jan 2015 15:48:49 -0800 Subject: [PATCH 315/513] Refactor push and pull to move code out of cmd function Signed-off-by: Derek McGowan (github: dmcgowan) --- graph/pull.go | 26 +++--- graph/push.go | 249 +++++++++++++++++++++++++------------------------- 2 files changed, 139 insertions(+), 136 deletions(-) diff --git a/graph/pull.go b/graph/pull.go index 1c4bb9d88..c70b220cc 100644 --- a/graph/pull.go +++ b/graph/pull.go @@ -82,20 +82,14 @@ func (s *TagStore) CmdPull(job *engine.Job) engine.Status { log.Errorf("error updating trust base graph: %s", err) } - auth, err := r.GetV2Authorization(repoInfo.RemoteName, true) - if err != nil { - log.Errorf("error getting authorization: %s", err) - } else { - - log.Debugf("pulling v2 repository with local name %q", repoInfo.LocalName) - if err := s.pullV2Repository(job.Eng, r, job.Stdout, repoInfo, tag, sf, job.GetenvBool("parallel"), auth); err == nil { - if err = job.Eng.Job("log", "pull", logName, "").Run(); err != nil { - log.Errorf("Error logging event 'pull' for %s: %s", logName, err) - } - return engine.StatusOK - } else if err != registry.ErrDoesNotExist { - log.Errorf("Error from V2 registry: %s", err) + log.Debugf("pulling v2 repository with local name %q", repoInfo.LocalName) + if err := s.pullV2Repository(job.Eng, r, job.Stdout, repoInfo, tag, sf, job.GetenvBool("parallel")); err == nil { + if err = job.Eng.Job("log", "pull", logName, "").Run(); err != nil { + log.Errorf("Error logging event 'pull' for %s: %s", logName, err) } + return engine.StatusOK + } else if err != registry.ErrDoesNotExist { + log.Errorf("Error from V2 registry: %s", err) } log.Debug("image does not exist on v2 registry, falling back to v1") @@ -384,7 +378,11 @@ type downloadInfo struct { err chan error } -func (s *TagStore) pullV2Repository(eng *engine.Engine, r *registry.Session, out io.Writer, repoInfo *registry.RepositoryInfo, tag string, sf *utils.StreamFormatter, parallel bool, auth *registry.RequestAuthorization) error { +func (s *TagStore) pullV2Repository(eng *engine.Engine, r *registry.Session, out io.Writer, repoInfo *registry.RepositoryInfo, tag string, sf *utils.StreamFormatter, parallel bool) error { + auth, err := r.GetV2Authorization(repoInfo.RemoteName, true) + if err != nil { + return fmt.Errorf("error getting authorization: %s", err) + } var layersDownloaded bool if tag == "" { log.Debugf("Pulling tag list from V2 registry for %s", repoInfo.CanonicalName) diff --git a/graph/push.go b/graph/push.go index 4d6b1e083..5b5011243 100644 --- a/graph/push.go +++ b/graph/push.go @@ -252,6 +252,105 @@ func (s *TagStore) pushImage(r *registry.Session, out io.Writer, imgID, ep strin return imgData.Checksum, nil } +func (s *TagStore) pushV2Repository(r *registry.Session, eng *engine.Engine, out io.Writer, repoInfo *registry.RepositoryInfo, manifestBytes, tag string, sf *utils.StreamFormatter) error { + if repoInfo.Official { + j := eng.Job("trust_update_base") + if err := j.Run(); err != nil { + log.Errorf("error updating trust base graph: %s", err) + } + } + + auth, err := r.GetV2Authorization(repoInfo.RemoteName, false) + if err != nil { + return fmt.Errorf("error getting authorization: %s", err) + } + + // if no manifest is given, generate and sign with the key associated with the local tag store + if len(manifestBytes) == 0 { + mBytes, err := s.newManifest(repoInfo.LocalName, repoInfo.RemoteName, tag) + if err != nil { + return err + } + js, err := libtrust.NewJSONSignature(mBytes) + if err != nil { + return err + } + + if err = js.Sign(s.trustKey); err != nil { + return err + } + + signedBody, err := js.PrettySignature("signatures") + if err != nil { + return err + } + log.Infof("Signed manifest using daemon's key: %s", s.trustKey.KeyID()) + + manifestBytes = string(signedBody) + } + + manifest, verified, err := s.verifyManifest(eng, []byte(manifestBytes)) + if err != nil { + return fmt.Errorf("error verifying manifest: %s", err) + } + + if err := checkValidManifest(manifest); err != nil { + return fmt.Errorf("invalid manifest: %s", err) + } + + if !verified { + log.Debugf("Pushing unverified image") + } + + for i := len(manifest.FSLayers) - 1; i >= 0; i-- { + var ( + sumStr = manifest.FSLayers[i].BlobSum + imgJSON = []byte(manifest.History[i].V1Compatibility) + ) + + sumParts := strings.SplitN(sumStr, ":", 2) + if len(sumParts) < 2 { + return fmt.Errorf("Invalid checksum: %s", sumStr) + } + manifestSum := sumParts[1] + + img, err := image.NewImgJSON(imgJSON) + if err != nil { + return fmt.Errorf("Failed to parse json: %s", err) + } + + img, err = s.graph.Get(img.ID) + if err != nil { + return err + } + + arch, err := img.TarLayer() + if err != nil { + return fmt.Errorf("Could not get tar layer: %s", err) + } + + // Call mount blob + exists, err := r.PostV2ImageMountBlob(repoInfo.RemoteName, sumParts[0], manifestSum, auth) + if err != nil { + out.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Image push failed", nil)) + return err + } + if !exists { + err = r.PutV2ImageBlob(repoInfo.RemoteName, sumParts[0], manifestSum, utils.ProgressReader(arch, int(img.Size), out, sf, false, utils.TruncateID(img.ID), "Pushing"), auth) + if err != nil { + out.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Image push failed", nil)) + return err + } + out.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Image successfully pushed", nil)) + } else { + out.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Image already exists", nil)) + } + } + + // push the manifest + return r.PutV2ImageManifest(repoInfo.RemoteName, tag, bytes.NewReader([]byte(manifestBytes)), auth) +} + // FIXME: Allow to interrupt current push when new push of same image is done. func (s *TagStore) CmdPush(job *engine.Job) engine.Status { if n := len(job.Args); n != 1 { @@ -296,129 +395,35 @@ func (s *TagStore) CmdPush(job *engine.Job) engine.Status { } if repoInfo.Index.Official || endpoint.Version == registry.APIVersion2 { - if repoInfo.Official { - j := job.Eng.Job("trust_update_base") - if err = j.Run(); err != nil { - return job.Errorf("error updating trust base graph: %s", err) - } + err := s.pushV2Repository(r, job.Eng, job.Stdout, repoInfo, manifestBytes, tag, sf) + if err == nil { + return engine.StatusOK } - auth, err := r.GetV2Authorization(repoInfo.RemoteName, false) - if err != nil { - return job.Errorf("error getting authorization: %s", err) - } - - if len(manifestBytes) == 0 { - mBytes, err := s.newManifest(repoInfo.LocalName, repoInfo.RemoteName, tag) - if err != nil { - return job.Error(err) - } - js, err := libtrust.NewJSONSignature(mBytes) - if err != nil { - return job.Error(err) - } - - if err = js.Sign(s.trustKey); err != nil { - return job.Error(err) - } - - signedBody, err := js.PrettySignature("signatures") - if err != nil { - return job.Error(err) - } - log.Infof("Signed manifest using daemon's key: %s", s.trustKey.KeyID()) - - manifestBytes = string(signedBody) - } - - manifest, verified, err := s.verifyManifest(job.Eng, []byte(manifestBytes)) - if err != nil { - return job.Errorf("error verifying manifest: %s", err) - } - - if err := checkValidManifest(manifest); err != nil { - return job.Errorf("invalid manifest: %s", err) - } - - if !verified { - log.Debugf("Pushing unverified image") - } - - for i := len(manifest.FSLayers) - 1; i >= 0; i-- { - var ( - sumStr = manifest.FSLayers[i].BlobSum - imgJSON = []byte(manifest.History[i].V1Compatibility) - ) - - sumParts := strings.SplitN(sumStr, ":", 2) - if len(sumParts) < 2 { - return job.Errorf("Invalid checksum: %s", sumStr) - } - manifestSum := sumParts[1] - - img, err := image.NewImgJSON(imgJSON) - if err != nil { - return job.Errorf("Failed to parse json: %s", err) - } - - img, err = s.graph.Get(img.ID) - if err != nil { - return job.Error(err) - } - - arch, err := img.TarLayer() - if err != nil { - return job.Errorf("Could not get tar layer: %s", err) - } - - // Call mount blob - exists, err := r.PostV2ImageMountBlob(repoInfo.RemoteName, sumParts[0], manifestSum, auth) - if err != nil { - job.Stdout.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Image push failed", nil)) - return job.Error(err) - } - if !exists { - err = r.PutV2ImageBlob(repoInfo.RemoteName, sumParts[0], manifestSum, utils.ProgressReader(arch, int(img.Size), job.Stdout, sf, false, utils.TruncateID(img.ID), "Pushing"), auth) - if err != nil { - job.Stdout.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Image push failed", nil)) - return job.Error(err) - } - job.Stdout.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Image successfully pushed", nil)) - } else { - job.Stdout.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Image already exists", nil)) - } - } - - // push the manifest - err = r.PutV2ImageManifest(repoInfo.RemoteName, tag, bytes.NewReader([]byte(manifestBytes)), auth) - if err != nil { - return job.Error(err) - } - - // done, no fallback to V1 - return engine.StatusOK - } else { - if err != nil { - reposLen := 1 - if tag == "" { - reposLen = len(s.Repositories[repoInfo.LocalName]) - } - job.Stdout.Write(sf.FormatStatus("", "The push refers to a repository [%s] (len: %d)", repoInfo.CanonicalName, reposLen)) - // If it fails, try to get the repository - if localRepo, exists := s.Repositories[repoInfo.LocalName]; exists { - if err := s.pushRepository(r, job.Stdout, repoInfo, localRepo, tag, sf); err != nil { - return job.Error(err) - } - return engine.StatusOK - } - return job.Error(err) - } - - var token []string - job.Stdout.Write(sf.FormatStatus("", "The push refers to an image: [%s]", repoInfo.CanonicalName)) - if _, err := s.pushImage(r, job.Stdout, img.ID, endpoint.String(), token, sf); err != nil { - return job.Error(err) - } - return engine.StatusOK + // error out, no fallback to V1 + return job.Error(err) } + + if err != nil { + reposLen := 1 + if tag == "" { + reposLen = len(s.Repositories[repoInfo.LocalName]) + } + job.Stdout.Write(sf.FormatStatus("", "The push refers to a repository [%s] (len: %d)", repoInfo.CanonicalName, reposLen)) + // If it fails, try to get the repository + if localRepo, exists := s.Repositories[repoInfo.LocalName]; exists { + if err := s.pushRepository(r, job.Stdout, repoInfo, localRepo, tag, sf); err != nil { + return job.Error(err) + } + return engine.StatusOK + } + return job.Error(err) + } + + var token []string + job.Stdout.Write(sf.FormatStatus("", "The push refers to an image: [%s]", repoInfo.CanonicalName)) + if _, err := s.pushImage(r, job.Stdout, img.ID, endpoint.String(), token, sf); err != nil { + return job.Error(err) + } + return engine.StatusOK } From 9c6f8e14398e794cbe20504556c22a1c83260bd8 Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Wed, 14 Jan 2015 16:46:31 -0800 Subject: [PATCH 316/513] Cleanup v2 session to require endpoint Signed-off-by: Derek McGowan (github: dmcgowan) --- graph/pull.go | 18 ++++++---- graph/push.go | 12 ++++--- registry/session_v2.go | 76 +++++++++++++++++++++++------------------- 3 files changed, 61 insertions(+), 45 deletions(-) diff --git a/graph/pull.go b/graph/pull.go index c70b220cc..d0fca38b3 100644 --- a/graph/pull.go +++ b/graph/pull.go @@ -379,26 +379,30 @@ type downloadInfo struct { } func (s *TagStore) pullV2Repository(eng *engine.Engine, r *registry.Session, out io.Writer, repoInfo *registry.RepositoryInfo, tag string, sf *utils.StreamFormatter, parallel bool) error { - auth, err := r.GetV2Authorization(repoInfo.RemoteName, true) + endpoint, err := r.V2RegistryEndpoint(repoInfo.Index) + if err != nil { + return fmt.Errorf("error getting registry endpoint: %s", err) + } + auth, err := r.GetV2Authorization(endpoint, repoInfo.RemoteName, true) if err != nil { return fmt.Errorf("error getting authorization: %s", err) } var layersDownloaded bool if tag == "" { log.Debugf("Pulling tag list from V2 registry for %s", repoInfo.CanonicalName) - tags, err := r.GetV2RemoteTags(repoInfo.RemoteName, auth) + tags, err := r.GetV2RemoteTags(endpoint, repoInfo.RemoteName, auth) if err != nil { return err } for _, t := range tags { - if downloaded, err := s.pullV2Tag(eng, r, out, repoInfo, t, sf, parallel, auth); err != nil { + if downloaded, err := s.pullV2Tag(eng, r, out, endpoint, repoInfo, t, sf, parallel, auth); err != nil { return err } else if downloaded { layersDownloaded = true } } } else { - if downloaded, err := s.pullV2Tag(eng, r, out, repoInfo, tag, sf, parallel, auth); err != nil { + if downloaded, err := s.pullV2Tag(eng, r, out, endpoint, repoInfo, tag, sf, parallel, auth); err != nil { return err } else if downloaded { layersDownloaded = true @@ -413,9 +417,9 @@ func (s *TagStore) pullV2Repository(eng *engine.Engine, r *registry.Session, out return nil } -func (s *TagStore) pullV2Tag(eng *engine.Engine, r *registry.Session, out io.Writer, repoInfo *registry.RepositoryInfo, tag string, sf *utils.StreamFormatter, parallel bool, auth *registry.RequestAuthorization) (bool, error) { +func (s *TagStore) pullV2Tag(eng *engine.Engine, r *registry.Session, out io.Writer, endpoint *registry.Endpoint, repoInfo *registry.RepositoryInfo, tag string, sf *utils.StreamFormatter, parallel bool, auth *registry.RequestAuthorization) (bool, error) { log.Debugf("Pulling tag from V2 registry: %q", tag) - manifestBytes, err := r.GetV2ImageManifest(repoInfo.RemoteName, tag, auth) + manifestBytes, err := r.GetV2ImageManifest(endpoint, repoInfo.RemoteName, tag, auth) if err != nil { return false, err } @@ -479,7 +483,7 @@ func (s *TagStore) pullV2Tag(eng *engine.Engine, r *registry.Session, out io.Wri return err } - r, l, err := r.GetV2ImageBlobReader(repoInfo.RemoteName, sumType, checksum, auth) + r, l, err := r.GetV2ImageBlobReader(endpoint, repoInfo.RemoteName, sumType, checksum, auth) if err != nil { return err } diff --git a/graph/push.go b/graph/push.go index 5b5011243..46469daea 100644 --- a/graph/push.go +++ b/graph/push.go @@ -260,7 +260,11 @@ func (s *TagStore) pushV2Repository(r *registry.Session, eng *engine.Engine, out } } - auth, err := r.GetV2Authorization(repoInfo.RemoteName, false) + endpoint, err := r.V2RegistryEndpoint(repoInfo.Index) + if err != nil { + return fmt.Errorf("error getting registry endpoint: %s", err) + } + auth, err := r.GetV2Authorization(endpoint, repoInfo.RemoteName, false) if err != nil { return fmt.Errorf("error getting authorization: %s", err) } @@ -330,13 +334,13 @@ func (s *TagStore) pushV2Repository(r *registry.Session, eng *engine.Engine, out } // Call mount blob - exists, err := r.PostV2ImageMountBlob(repoInfo.RemoteName, sumParts[0], manifestSum, auth) + exists, err := r.HeadV2ImageBlob(endpoint, repoInfo.RemoteName, sumParts[0], manifestSum, auth) if err != nil { out.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Image push failed", nil)) return err } if !exists { - err = r.PutV2ImageBlob(repoInfo.RemoteName, sumParts[0], manifestSum, utils.ProgressReader(arch, int(img.Size), out, sf, false, utils.TruncateID(img.ID), "Pushing"), auth) + err = r.PutV2ImageBlob(endpoint, repoInfo.RemoteName, sumParts[0], manifestSum, utils.ProgressReader(arch, int(img.Size), out, sf, false, utils.TruncateID(img.ID), "Pushing"), auth) if err != nil { out.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Image push failed", nil)) return err @@ -348,7 +352,7 @@ func (s *TagStore) pushV2Repository(r *registry.Session, eng *engine.Engine, out } // push the manifest - return r.PutV2ImageManifest(repoInfo.RemoteName, tag, bytes.NewReader([]byte(manifestBytes)), auth) + return r.PutV2ImageManifest(endpoint, repoInfo.RemoteName, tag, bytes.NewReader([]byte(manifestBytes)), auth) } // FIXME: Allow to interrupt current push when new push of same image is done. diff --git a/registry/session_v2.go b/registry/session_v2.go index b08f4cf0d..11b96bd65 100644 --- a/registry/session_v2.go +++ b/registry/session_v2.go @@ -19,33 +19,41 @@ func getV2Builder(e *Endpoint) *v2.URLBuilder { return e.URLBuilder } +func (r *Session) V2RegistryEndpoint(index *IndexInfo) (ep *Endpoint, err error) { + // TODO check if should use Mirror + if index.Official { + ep, err = newEndpoint(REGISTRYSERVER, true) + if err != nil { + return + } + err = validateEndpoint(ep) + if err != nil { + return + } + } else if r.indexEndpoint.String() == index.GetAuthConfigKey() { + ep = r.indexEndpoint + } else { + ep, err = NewEndpoint(index) + if err != nil { + return + } + } + + ep.URLBuilder = v2.NewURLBuilder(ep.URL) + return +} + // GetV2Authorization gets the authorization needed to the given image // If readonly access is requested, then only the authorization may // only be used for Get operations. -func (r *Session) GetV2Authorization(imageName string, readOnly bool) (auth *RequestAuthorization, err error) { +func (r *Session) GetV2Authorization(ep *Endpoint, imageName string, readOnly bool) (auth *RequestAuthorization, err error) { scopes := []string{"pull"} if !readOnly { scopes = append(scopes, "push") } - var registry *Endpoint - if r.indexEndpoint.String() == IndexServerAddress() { - registry, err = newEndpoint(REGISTRYSERVER, true) - if err != nil { - return - } - err = validateEndpoint(registry) - if err != nil { - return - } - } else { - registry = r.indexEndpoint - } - registry.URLBuilder = v2.NewURLBuilder(registry.URL) - r.indexEndpoint = registry - log.Debugf("Getting authorization for %s %s", imageName, scopes) - return NewRequestAuthorization(r.GetAuthConfig(true), registry, "repository", imageName, scopes), nil + return NewRequestAuthorization(r.GetAuthConfig(true), ep, "repository", imageName, scopes), nil } // @@ -55,8 +63,8 @@ func (r *Session) GetV2Authorization(imageName string, readOnly bool) (auth *Req // 1.c) if anything else, err // 2) PUT the created/signed manifest // -func (r *Session) GetV2ImageManifest(imageName, tagName string, auth *RequestAuthorization) ([]byte, error) { - routeURL, err := getV2Builder(r.indexEndpoint).BuildManifestURL(imageName, tagName) +func (r *Session) GetV2ImageManifest(ep *Endpoint, imageName, tagName string, auth *RequestAuthorization) ([]byte, error) { + routeURL, err := getV2Builder(ep).BuildManifestURL(imageName, tagName) if err != nil { return nil, err } @@ -92,11 +100,11 @@ func (r *Session) GetV2ImageManifest(imageName, tagName string, auth *RequestAut return buf, nil } -// - Succeeded to mount for this image scope -// - Failed with no error (So continue to Push the Blob) +// - Succeeded to head image blob (already exists) +// - Failed with no error (continue to Push the Blob) // - Failed with error -func (r *Session) PostV2ImageMountBlob(imageName, sumType, sum string, auth *RequestAuthorization) (bool, error) { - routeURL, err := getV2Builder(r.indexEndpoint).BuildBlobURL(imageName, sumType+":"+sum) +func (r *Session) HeadV2ImageBlob(ep *Endpoint, imageName, sumType, sum string, auth *RequestAuthorization) (bool, error) { + routeURL, err := getV2Builder(ep).BuildBlobURL(imageName, sumType+":"+sum) if err != nil { return false, err } @@ -127,8 +135,8 @@ func (r *Session) PostV2ImageMountBlob(imageName, sumType, sum string, auth *Req return false, fmt.Errorf("Failed to mount %q - %s:%s : %d", imageName, sumType, sum, res.StatusCode) } -func (r *Session) GetV2ImageBlob(imageName, sumType, sum string, blobWrtr io.Writer, auth *RequestAuthorization) error { - routeURL, err := getV2Builder(r.indexEndpoint).BuildBlobURL(imageName, sumType+":"+sum) +func (r *Session) GetV2ImageBlob(ep *Endpoint, imageName, sumType, sum string, blobWrtr io.Writer, auth *RequestAuthorization) error { + routeURL, err := getV2Builder(ep).BuildBlobURL(imageName, sumType+":"+sum) if err != nil { return err } @@ -158,8 +166,8 @@ func (r *Session) GetV2ImageBlob(imageName, sumType, sum string, blobWrtr io.Wri return err } -func (r *Session) GetV2ImageBlobReader(imageName, sumType, sum string, auth *RequestAuthorization) (io.ReadCloser, int64, error) { - routeURL, err := getV2Builder(r.indexEndpoint).BuildBlobURL(imageName, sumType+":"+sum) +func (r *Session) GetV2ImageBlobReader(ep *Endpoint, imageName, sumType, sum string, auth *RequestAuthorization) (io.ReadCloser, int64, error) { + routeURL, err := getV2Builder(ep).BuildBlobURL(imageName, sumType+":"+sum) if err != nil { return nil, 0, err } @@ -195,8 +203,8 @@ func (r *Session) GetV2ImageBlobReader(imageName, sumType, sum string, auth *Req // Push the image to the server for storage. // 'layer' is an uncompressed reader of the blob to be pushed. // The server will generate it's own checksum calculation. -func (r *Session) PutV2ImageBlob(imageName, sumType, sumStr string, blobRdr io.Reader, auth *RequestAuthorization) error { - routeURL, err := getV2Builder(r.indexEndpoint).BuildBlobUploadURL(imageName) +func (r *Session) PutV2ImageBlob(ep *Endpoint, imageName, sumType, sumStr string, blobRdr io.Reader, auth *RequestAuthorization) error { + routeURL, err := getV2Builder(ep).BuildBlobUploadURL(imageName) if err != nil { return err } @@ -245,8 +253,8 @@ func (r *Session) PutV2ImageBlob(imageName, sumType, sumStr string, blobRdr io.R } // Finally Push the (signed) manifest of the blobs we've just pushed -func (r *Session) PutV2ImageManifest(imageName, tagName string, manifestRdr io.Reader, auth *RequestAuthorization) error { - routeURL, err := getV2Builder(r.indexEndpoint).BuildManifestURL(imageName, tagName) +func (r *Session) PutV2ImageManifest(ep *Endpoint, imageName, tagName string, manifestRdr io.Reader, auth *RequestAuthorization) error { + routeURL, err := getV2Builder(ep).BuildManifestURL(imageName, tagName) if err != nil { return err } @@ -283,8 +291,8 @@ type remoteTags struct { } // Given a repository name, returns a json array of string tags -func (r *Session) GetV2RemoteTags(imageName string, auth *RequestAuthorization) ([]string, error) { - routeURL, err := getV2Builder(r.indexEndpoint).BuildTagsURL(imageName) +func (r *Session) GetV2RemoteTags(ep *Endpoint, imageName string, auth *RequestAuthorization) ([]string, error) { + routeURL, err := getV2Builder(ep).BuildTagsURL(imageName) if err != nil { return nil, err } From f11f3f6203da596f50eec0edc3c5dfb8c93bc271 Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Wed, 14 Jan 2015 17:14:14 -0800 Subject: [PATCH 317/513] Remove session backup The v2 session code will no longer update the indexEndpoint value, therefore it is not necessary to save and restore the value for use with v1. Signed-off-by: Derek McGowan (github: dmcgowan) --- graph/pull.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/graph/pull.go b/graph/pull.go index d0fca38b3..6129ea39a 100644 --- a/graph/pull.go +++ b/graph/pull.go @@ -72,10 +72,6 @@ func (s *TagStore) CmdPull(job *engine.Job) engine.Status { logName += ":" + tag } - // Calling the v2 code path might change the session - // endpoint value, so save the original one! - originalSession := *r - if len(repoInfo.Index.Mirrors) == 0 && (repoInfo.Index.Official || endpoint.Version == registry.APIVersion2) { j := job.Eng.Job("trust_update_base") if err = j.Run(); err != nil { @@ -95,8 +91,6 @@ func (s *TagStore) CmdPull(job *engine.Job) engine.Status { log.Debug("image does not exist on v2 registry, falling back to v1") } - r = &originalSession - log.Debugf("pulling v1 repository with local name %q", repoInfo.LocalName) if err = s.pullRepository(r, job.Stdout, repoInfo, tag, sf, job.GetenvBool("parallel")); err != nil { return job.Error(err) From dd914f91d779f64e20ce86767ab4f84f40b9ef6a Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Thu, 15 Jan 2015 13:06:52 -0800 Subject: [PATCH 318/513] Add token cache Token cache prevents the need to get a new token for every registry interaction. Since the tokens are short lived, the cache expires after only a minute. Signed-off-by: Derek McGowan (github: dmcgowan) --- registry/auth.go | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/registry/auth.go b/registry/auth.go index 1e1c7ddb8..1ce99805f 100644 --- a/registry/auth.go +++ b/registry/auth.go @@ -10,6 +10,8 @@ import ( "os" "path" "strings" + "sync" + "time" log "github.com/Sirupsen/logrus" "github.com/docker/docker/utils" @@ -43,6 +45,10 @@ type RequestAuthorization struct { resource string scope string actions []string + + tokenLock sync.Mutex + tokenCache string + tokenExpiration time.Time } func NewRequestAuthorization(authConfig *AuthConfig, registryEndpoint *Endpoint, resource, scope string, actions []string) *RequestAuthorization { @@ -56,7 +62,14 @@ func NewRequestAuthorization(authConfig *AuthConfig, registryEndpoint *Endpoint, } func (auth *RequestAuthorization) getToken() (string, error) { - // TODO check if already has token and before expiration + auth.tokenLock.Lock() + defer auth.tokenLock.Unlock() + now := time.Now() + if now.Before(auth.tokenExpiration) { + log.Debugf("Using cached token for %s", auth.authConfig.Username) + return auth.tokenCache, nil + } + client := &http.Client{ Transport: &http.Transport{ DisableKeepAlives: true, @@ -80,14 +93,18 @@ func (auth *RequestAuthorization) getToken() (string, error) { if err != nil { return "", err } - // TODO cache token and set expiration to one minute from now + auth.tokenCache = token + auth.tokenExpiration = now.Add(time.Minute) return token, nil default: log.Infof("Unsupported auth scheme: %q", challenge.Scheme) } } - // TODO no expiration, do not reattempt to get a token + + // Do not expire cache since there are no challenges which use a token + auth.tokenExpiration = time.Now().Add(time.Hour * 24) + return "", nil } From f6777c7a40efdae161f6ba99223f4d6a625d4762 Mon Sep 17 00:00:00 2001 From: Fred Lifton Date: Thu, 15 Jan 2015 16:09:48 -0800 Subject: [PATCH 319/513] Adds new section for Known Issues to Release Notes. Docker-DCO-1.1-Signed-off-by: Fred Lifton (github: fredlf) --- docs/sources/release-notes.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/docs/sources/release-notes.md b/docs/sources/release-notes.md index 67b4f2ec1..cee39ef84 100644 --- a/docs/sources/release-notes.md +++ b/docs/sources/release-notes.md @@ -43,13 +43,13 @@ when overriding a tag for existing image. For more information, see the [command line reference](http://docs.docker.com/reference/commandline/cli/#tag). * Container volumes are now initialized during `docker create`. For more information, see -the [command line reference](http://docs.docker.com/reference/commandline/cli/#create). +the [command line reference](http://docs.docker.com/reference/commandline/cli/#create). *Security Fixes* Patches and changes were made to address the following vulnerabilities: -* CVE-2014-9356: Path traversal during processing of absolute symlinks. +* CVE-2014-9356: Path traversal during processing of absolute symlinks. Absolute symlinks were not adequately checked for traversal which created a vulnerability via image extraction and/or volume mounts. * CVE-2014-9357: Escalation of privileges during decompression of LZMA (.xz) @@ -79,3 +79,14 @@ destination. > Development history prior to version 1.0 can be found by > searching in the [Docker GitHub repo](https://github.com/docker/docker). +## Known Issues + +This section lists significant known issues present in Docker as of release +date. It is not exhaustive; it lists only issues with potentially significant +impact on users. This list will be updated as issues are resolved. + +* **Unexpected File Permissions in Containers** +An idiosyncrasy in AUFS prevents permissions from propagating predictably +between upper and lower layers. This can cause issues with accessing private +keys, database instances, etc. For complete information and workarounds see +[Github Issue 783](https://github.com/docker/docker/issues/783). \ No newline at end of file From 0e9acf76844a0dcd76bc855b945d0e4b80149195 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Fri, 16 Jan 2015 00:33:11 +0000 Subject: [PATCH 320/513] don't restrict filters in docker images Signed-off-by: Victor Vieux --- api/client/commands.go | 10 ---------- integration-cli/docker_cli_images_test.go | 10 ---------- 2 files changed, 20 deletions(-) diff --git a/api/client/commands.go b/api/client/commands.go index 06c369958..6e8eabe17 100644 --- a/api/client/commands.go +++ b/api/client/commands.go @@ -49,10 +49,6 @@ const ( tarHeaderSize = 512 ) -var ( - acceptedImageFilterTags = map[string]struct{}{"dangling": {}} -) - func (cli *DockerCli) CmdHelp(args ...string) error { if len(args) > 1 { method, exists := cli.getMethod(args[:2]...) @@ -1331,12 +1327,6 @@ func (cli *DockerCli) CmdImages(args ...string) error { } } - for name := range imageFilterArgs { - if _, ok := acceptedImageFilterTags[name]; !ok { - return fmt.Errorf("Invalid filter '%s'", name) - } - } - matchName := cmd.Arg(0) // FIXME: --viz and --tree are deprecated. Remove them in a future version. if *flViz || *flTree { diff --git a/integration-cli/docker_cli_images_test.go b/integration-cli/docker_cli_images_test.go index a91f1c0e2..2758797fb 100644 --- a/integration-cli/docker_cli_images_test.go +++ b/integration-cli/docker_cli_images_test.go @@ -67,16 +67,6 @@ func TestImagesOrderedByCreationDate(t *testing.T) { logDone("images - ordering by creation date") } -func TestImagesErrorWithInvalidFilterNameTest(t *testing.T) { - imagesCmd := exec.Command(dockerBinary, "images", "-f", "FOO=123") - out, _, err := runCommandWithOutput(imagesCmd) - if !strings.Contains(out, "Invalid filter") { - t.Fatalf("error should occur when listing images with invalid filter name FOO, %s, %v", out, err) - } - - logDone("images - invalid filter name check working") -} - func TestImagesFilterWhiteSpaceTrimmingAndLowerCasingWorking(t *testing.T) { imageName := "images_filter_test" defer deleteAllContainers() From 933f957e773d5b9da13e32649d0f987b30e87eb5 Mon Sep 17 00:00:00 2001 From: unclejack Date: Fri, 16 Jan 2015 09:45:37 +0200 Subject: [PATCH 321/513] bump go to 1.4.1 Signed-off-by: Cristian Staretu --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 9ef05b561..59d9ced90 100644 --- a/Dockerfile +++ b/Dockerfile @@ -73,7 +73,7 @@ RUN cd /usr/src/lxc \ && ldconfig # Install Go -ENV GO_VERSION 1.4 +ENV GO_VERSION 1.4.1 RUN curl -sSL https://golang.org/dl/go${GO_VERSION}.src.tar.gz | tar -v -C /usr/local -xz \ && mkdir -p /go/bin ENV PATH /go/bin:/usr/local/go/bin:$PATH From c3ed49dcdb2d835bf4fbdebe3f07318c945282c8 Mon Sep 17 00:00:00 2001 From: HuKeping Date: Fri, 16 Jan 2015 17:58:26 +0800 Subject: [PATCH 322/513] restart: add test for recording restart policy name Add test for recording restart policy name on - restart=no - restart=always - restart=on-failure Signed-off-by: Hu Keping --- integration-cli/docker_cli_restart_test.go | 69 ++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/integration-cli/docker_cli_restart_test.go b/integration-cli/docker_cli_restart_test.go index 3a390ef2c..93821f726 100644 --- a/integration-cli/docker_cli_restart_test.go +++ b/integration-cli/docker_cli_restart_test.go @@ -151,3 +151,72 @@ func TestRestartWithVolumes(t *testing.T) { logDone("restart - does not create a new volume on restart") } + +func TestRecordRestartPolicyNO(t *testing.T) { + defer deleteAllContainers() + + cmd := exec.Command(dockerBinary, "run", "-d", "--restart=no", "busybox", "false") + out, _, err := runCommandWithOutput(cmd) + if err != nil { + t.Fatal(err, out) + } + + id := strings.TrimSpace(string(out)) + name, err := inspectField(id, "HostConfig.RestartPolicy.Name") + if err != nil { + t.Fatal(err, out) + } + if name != "no" { + t.Fatalf("Container restart policy name is %s, expected %s", name, "no") + } + + logDone("restart - recording restart policy name for --restart=no") +} + +func TestRecordRestartPolicyAlways(t *testing.T) { + defer deleteAllContainers() + + cmd := exec.Command(dockerBinary, "run", "-d", "--restart=always", "busybox", "false") + out, _, err := runCommandWithOutput(cmd) + if err != nil { + t.Fatal(err, out) + } + + id := strings.TrimSpace(string(out)) + name, err := inspectField(id, "HostConfig.RestartPolicy.Name") + if err != nil { + t.Fatal(err, out) + } + if name != "always" { + t.Fatalf("Container restart policy name is %s, expected %s", name, "always") + } + + cmd = exec.Command(dockerBinary, "stop", id) + out, _, err = runCommandWithOutput(cmd) + if err != nil { + t.Fatal(err, out) + } + + logDone("restart - recording restart policy name for --restart=always") +} + +func TestRecordRestartPolicyOnFailure(t *testing.T) { + defer deleteAllContainers() + + cmd := exec.Command(dockerBinary, "run", "-d", "--restart=on-failure:1", "busybox", "false") + out, _, err := runCommandWithOutput(cmd) + if err != nil { + t.Fatal(err, out) + } + + id := strings.TrimSpace(string(out)) + name, err := inspectField(id, "HostConfig.RestartPolicy.Name") + if err != nil { + t.Fatal(err, out) + } + if name != "on-failure" { + t.Fatalf("Container restart policy name is %s, expected %s", name, "on-failure") + } + + logDone("restart - recording restart policy name for --restart=on-failure") +} From 43b97368422843108a2090633367beaba434245b Mon Sep 17 00:00:00 2001 From: Pavel Lobashov Date: Fri, 16 Jan 2015 13:48:00 +0300 Subject: [PATCH 323/513] fix link to introducion on 'Working with Docker Images' page Old link go to Table of content page, but by context it should go to 'understaning-docker' pag Signed-off-by: Pavel Lobashov --- docs/sources/userguide/dockerimages.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/userguide/dockerimages.md b/docs/sources/userguide/dockerimages.md index 2c4e14c70..4cffba5df 100644 --- a/docs/sources/userguide/dockerimages.md +++ b/docs/sources/userguide/dockerimages.md @@ -4,7 +4,7 @@ page_keywords: documentation, docs, the docker guide, docker guide, docker, dock # Working with Docker Images -In the [introduction](/introduction/) we've discovered that Docker +In the [introduction](/introduction/understanding-docker/) we've discovered that Docker images are the basis of containers. In the [previous](/userguide/dockerizing/) [sections](/userguide/usingdocker/) we've used Docker images that already exist, for example the `ubuntu` From f29aacbc4804e3aca1c21b9411e960b2a2543da1 Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Fri, 16 Jan 2015 11:34:45 -0800 Subject: [PATCH 324/513] Fix failing integration tests Signed-off-by: Derek McGowan (github: dmcgowan) --- integration-cli/docker_cli_pull_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/integration-cli/docker_cli_pull_test.go b/integration-cli/docker_cli_pull_test.go index e76f4ee95..764968858 100644 --- a/integration-cli/docker_cli_pull_test.go +++ b/integration-cli/docker_cli_pull_test.go @@ -40,6 +40,7 @@ func TestPullImageWithAliases(t *testing.T) { if out, _, err := runCommandWithOutput(pullCmd); err != nil { t.Fatalf("Failed to pull %v: error %v, output %q", repoName, err, out) } + defer deleteImages(repos[0]) if err := exec.Command(dockerBinary, "inspect", repos[0]).Run(); err != nil { t.Fatalf("Image %v was not pulled down", repos[0]) } From 48b1dd0084904678728817d728bb9ab1c0183aad Mon Sep 17 00:00:00 2001 From: Phil Estes Date: Thu, 15 Jan 2015 16:40:39 -0500 Subject: [PATCH 325/513] Add backing filesystem info to `docker info` command where applicable Fixes #9960 This adds the output of a "Backing Filesystem:" entry to `docker info` to overlay, aufs, and devicemapper graphdrivers. The default list includes a fairly complete list of common filesystem names from linux/include/uapi/linux/magic.h, but if the backing filesystem is not recognized, the code will simply show "" Docker-DCO-1.1-Signed-off-by: Phil Estes --- daemon/graphdriver/aufs/aufs.go | 16 ++++-- daemon/graphdriver/aufs/aufs_test.go | 2 +- daemon/graphdriver/devmapper/driver.go | 11 ++++ daemon/graphdriver/driver.go | 67 ++++++++++++++++------- daemon/graphdriver/driver_linux.go | 14 +++++ daemon/graphdriver/driver_unsupported.go | 7 +++ daemon/graphdriver/overlay/overlay.go | 18 ++++-- docs/sources/reference/commandline/cli.md | 1 + 8 files changed, 103 insertions(+), 33 deletions(-) create mode 100644 daemon/graphdriver/driver_linux.go create mode 100644 daemon/graphdriver/driver_unsupported.go diff --git a/daemon/graphdriver/aufs/aufs.go b/daemon/graphdriver/aufs/aufs.go index 5d08bfc5d..5e9d747f2 100644 --- a/daemon/graphdriver/aufs/aufs.go +++ b/daemon/graphdriver/aufs/aufs.go @@ -45,6 +45,7 @@ var ( graphdriver.FsMagicBtrfs, graphdriver.FsMagicAufs, } + backingFs = "" ) func init() { @@ -60,20 +61,22 @@ type Driver struct { // New returns a new AUFS driver. // An error is returned if AUFS is not supported. func Init(root string, options []string) (graphdriver.Driver, error) { + // Try to load the aufs kernel module if err := supportsAufs(); err != nil { return nil, graphdriver.ErrNotSupported } - rootdir := path.Dir(root) - - var buf syscall.Statfs_t - if err := syscall.Statfs(rootdir, &buf); err != nil { - return nil, fmt.Errorf("Couldn't stat the root directory: %s", err) + fsMagic, err := graphdriver.GetFSMagic(root) + if err != nil { + return nil, err + } + if fsName, ok := graphdriver.FsNames[fsMagic]; ok { + backingFs = fsName } for _, magic := range incompatibleFsMagic { - if graphdriver.FsMagic(buf.Type) == magic { + if fsMagic == magic { return nil, graphdriver.ErrIncompatibleFS } } @@ -146,6 +149,7 @@ func (a *Driver) Status() [][2]string { ids, _ := loadIds(path.Join(a.rootPath(), "layers")) return [][2]string{ {"Root Dir", a.rootPath()}, + {"Backing Filesystem", backingFs}, {"Dirs", fmt.Sprintf("%d", len(ids))}, } } diff --git a/daemon/graphdriver/aufs/aufs_test.go b/daemon/graphdriver/aufs/aufs_test.go index c17a5dcce..6dea9bb51 100644 --- a/daemon/graphdriver/aufs/aufs_test.go +++ b/daemon/graphdriver/aufs/aufs_test.go @@ -568,7 +568,7 @@ func TestStatus(t *testing.T) { t.Fatal("Status should not be nil or empty") } rootDir := status[0] - dirs := status[1] + dirs := status[2] if rootDir[0] != "Root Dir" { t.Fatalf("Expected Root Dir got %s", rootDir[0]) } diff --git a/daemon/graphdriver/devmapper/driver.go b/daemon/graphdriver/devmapper/driver.go index f8fe1bcda..a7dafc657 100644 --- a/daemon/graphdriver/devmapper/driver.go +++ b/daemon/graphdriver/devmapper/driver.go @@ -29,7 +29,17 @@ type Driver struct { home string } +var backingFs = "" + func Init(home string, options []string) (graphdriver.Driver, error) { + fsMagic, err := graphdriver.GetFSMagic(home) + if err != nil { + return nil, err + } + if fsName, ok := graphdriver.FsNames[fsMagic]; ok { + backingFs = fsName + } + deviceSet, err := NewDeviceSet(home, true, options) if err != nil { return nil, err @@ -57,6 +67,7 @@ func (d *Driver) Status() [][2]string { status := [][2]string{ {"Pool Name", s.PoolName}, {"Pool Blocksize", fmt.Sprintf("%s", units.HumanSize(float64(s.SectorSize)))}, + {"Backing Filesystem", backingFs}, {"Data file", s.DataFile}, {"Metadata file", s.MetadataFile}, {"Data Space Used", fmt.Sprintf("%s", units.HumanSize(float64(s.Data.Used)))}, diff --git a/daemon/graphdriver/driver.go b/daemon/graphdriver/driver.go index 43abd5904..c63e1b45d 100644 --- a/daemon/graphdriver/driver.go +++ b/daemon/graphdriver/driver.go @@ -14,8 +14,52 @@ import ( type FsMagic uint32 const ( - FsMagicBtrfs = FsMagic(0x9123683E) - FsMagicAufs = FsMagic(0x61756673) + FsMagicBtrfs = FsMagic(0x9123683E) + FsMagicAufs = FsMagic(0x61756673) + FsMagicExtfs = FsMagic(0x0000EF53) + FsMagicCramfs = FsMagic(0x28cd3d45) + FsMagicRamFs = FsMagic(0x858458f6) + FsMagicTmpFs = FsMagic(0x01021994) + FsMagicSquashFs = FsMagic(0x73717368) + FsMagicNfsFs = FsMagic(0x00006969) + FsMagicReiserFs = FsMagic(0x52654973) + FsMagicSmbFs = FsMagic(0x0000517B) + FsMagicJffs2Fs = FsMagic(0x000072b6) + FsMagicUnsupported = FsMagic(0x00000000) +) + +var ( + DefaultDriver string + // All registred drivers + drivers map[string]InitFunc + // Slice of drivers that should be used in an order + priority = []string{ + "aufs", + "btrfs", + "devicemapper", + "vfs", + // experimental, has to be enabled manually for now + "overlay", + } + + ErrNotSupported = errors.New("driver not supported") + ErrPrerequisites = errors.New("prerequisites for driver not satisfied (wrong filesystem?)") + ErrIncompatibleFS = fmt.Errorf("backing file system is unsupported for this graph driver") + + FsNames = map[FsMagic]string{ + FsMagicAufs: "aufs", + FsMagicBtrfs: "btrfs", + FsMagicExtfs: "extfs", + FsMagicCramfs: "cramfs", + FsMagicRamFs: "ramfs", + FsMagicTmpFs: "tmpfs", + FsMagicSquashFs: "squashfs", + FsMagicNfsFs: "nfs", + FsMagicReiserFs: "reiserfs", + FsMagicSmbFs: "smb", + FsMagicJffs2Fs: "jffs2", + FsMagicUnsupported: "unsupported", + } ) type InitFunc func(root string, options []string) (Driver, error) @@ -72,25 +116,6 @@ type Driver interface { DiffSize(id, parent string) (size int64, err error) } -var ( - DefaultDriver string - // All registred drivers - drivers map[string]InitFunc - // Slice of drivers that should be used in an order - priority = []string{ - "aufs", - "btrfs", - "devicemapper", - "vfs", - // experimental, has to be enabled manually for now - "overlay", - } - - ErrNotSupported = errors.New("driver not supported") - ErrPrerequisites = errors.New("prerequisites for driver not satisfied (wrong filesystem?)") - ErrIncompatibleFS = fmt.Errorf("backing file system is unsupported for this graph driver") -) - func init() { drivers = make(map[string]InitFunc) } diff --git a/daemon/graphdriver/driver_linux.go b/daemon/graphdriver/driver_linux.go new file mode 100644 index 000000000..acf96d1b4 --- /dev/null +++ b/daemon/graphdriver/driver_linux.go @@ -0,0 +1,14 @@ +package graphdriver + +import ( + "path" + "syscall" +) + +func GetFSMagic(rootpath string) (FsMagic, error) { + var buf syscall.Statfs_t + if err := syscall.Statfs(path.Dir(rootpath), &buf); err != nil { + return 0, err + } + return FsMagic(buf.Type), nil +} diff --git a/daemon/graphdriver/driver_unsupported.go b/daemon/graphdriver/driver_unsupported.go new file mode 100644 index 000000000..27933b6d6 --- /dev/null +++ b/daemon/graphdriver/driver_unsupported.go @@ -0,0 +1,7 @@ +// +build !linux + +package graphdriver + +func GetFSMagic(rootpath string) (FsMagic, error) { + return FsMagicUnsupported, nil +} diff --git a/daemon/graphdriver/overlay/overlay.go b/daemon/graphdriver/overlay/overlay.go index 438ff55b4..27784c14a 100644 --- a/daemon/graphdriver/overlay/overlay.go +++ b/daemon/graphdriver/overlay/overlay.go @@ -90,22 +90,28 @@ type Driver struct { active map[string]*ActiveMount } +var backingFs = "" + func init() { graphdriver.Register("overlay", Init) } func Init(home string, options []string) (graphdriver.Driver, error) { + if err := supportsOverlay(); err != nil { return nil, graphdriver.ErrNotSupported } - // check if they are running over btrfs - var buf syscall.Statfs_t - if err := syscall.Statfs(path.Dir(home), &buf); err != nil { + fsMagic, err := graphdriver.GetFSMagic(home) + if err != nil { return nil, err } + if fsName, ok := graphdriver.FsNames[fsMagic]; ok { + backingFs = fsName + } - switch graphdriver.FsMagic(buf.Type) { + // check if they are running over btrfs or aufs + switch fsMagic { case graphdriver.FsMagicBtrfs: log.Error("'overlay' is not supported over btrfs.") return nil, graphdriver.ErrIncompatibleFS @@ -153,7 +159,9 @@ func (d *Driver) String() string { } func (d *Driver) Status() [][2]string { - return nil + return [][2]string{ + {"Backing Filesystem", backingFs}, + } } func (d *Driver) Cleanup() error { diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index 50bb2ccfb..292acc929 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -1162,6 +1162,7 @@ For example: Images: 52 Storage Driver: aufs Root Dir: /var/lib/docker/aufs + Backing Filesystem: extfs Dirs: 545 Execution Driver: native-0.2 Kernel Version: 3.13.0-24-generic From c6309229a0f5e3d8ee18d7ce80d1d0bd1d193e07 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Fri, 16 Jan 2015 21:49:46 +0000 Subject: [PATCH 326/513] move test to the daemon Signed-off-by: Victor Vieux --- graph/list.go | 8 ++++++++ integration-cli/docker_cli_images_test.go | 10 ++++++++++ 2 files changed, 18 insertions(+) diff --git a/graph/list.go b/graph/list.go index 63a906b9a..49d4072be 100644 --- a/graph/list.go +++ b/graph/list.go @@ -11,6 +11,8 @@ import ( "github.com/docker/docker/pkg/parsers/filters" ) +var acceptedImageFilterTags = map[string]struct{}{"dangling": {}} + func (s *TagStore) CmdImages(job *engine.Job) engine.Status { var ( allImages map[string]*image.Image @@ -22,6 +24,12 @@ func (s *TagStore) CmdImages(job *engine.Job) engine.Status { if err != nil { return job.Error(err) } + for name := range imageFilters { + if _, ok := acceptedImageFilterTags[name]; !ok { + return job.Errorf("Invalid filter '%s'", name) + } + } + if i, ok := imageFilters["dangling"]; ok { for _, value := range i { if strings.ToLower(value) == "true" { diff --git a/integration-cli/docker_cli_images_test.go b/integration-cli/docker_cli_images_test.go index 2758797fb..a91f1c0e2 100644 --- a/integration-cli/docker_cli_images_test.go +++ b/integration-cli/docker_cli_images_test.go @@ -67,6 +67,16 @@ func TestImagesOrderedByCreationDate(t *testing.T) { logDone("images - ordering by creation date") } +func TestImagesErrorWithInvalidFilterNameTest(t *testing.T) { + imagesCmd := exec.Command(dockerBinary, "images", "-f", "FOO=123") + out, _, err := runCommandWithOutput(imagesCmd) + if !strings.Contains(out, "Invalid filter") { + t.Fatalf("error should occur when listing images with invalid filter name FOO, %s, %v", out, err) + } + + logDone("images - invalid filter name check working") +} + func TestImagesFilterWhiteSpaceTrimmingAndLowerCasingWorking(t *testing.T) { imageName := "images_filter_test" defer deleteAllContainers() From bf14bacac3f1cae15ce7b32b5341122c305e630e Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Fri, 16 Jan 2015 23:36:50 +0100 Subject: [PATCH 327/513] Fix typo in deprecation message. Because the doc maintainers don't like Cockney. Signed-off-by: Sebastiaan van Stijn --- api/client/commands.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/client/commands.go b/api/client/commands.go index 9b67001c1..237caa622 100644 --- a/api/client/commands.go +++ b/api/client/commands.go @@ -1161,7 +1161,7 @@ func (cli *DockerCli) CmdImport(args ...string) error { v.Set("repo", repository) if cmd.NArg() == 3 { - fmt.Fprintf(cli.err, "[DEPRECATED] The format 'URL|- [REPOSITORY [TAG]]' as been deprecated. Please use URL|- [REPOSITORY[:TAG]]\n") + fmt.Fprintf(cli.err, "[DEPRECATED] The format 'URL|- [REPOSITORY [TAG]]' has been deprecated. Please use URL|- [REPOSITORY[:TAG]]\n") v.Set("tag", cmd.Arg(2)) } From bff3509e431ce6d68e2accbb842c99d4abe86e05 Mon Sep 17 00:00:00 2001 From: Abin Shahab Date: Fri, 16 Jan 2015 13:11:29 +0000 Subject: [PATCH 328/513] SEND CAPABILITY IDS TO LXC Sending capability ids instead of capability names ot LXC for --cap-add and --cap-drop. Also fixed tests. Docker-DCO-1.1-Signed-off-by: Abin Shahab (github: ashahab-altiscale) --- daemon/execdriver/lxc/lxc_template.go | 58 +++++++++++++++---- .../execdriver/lxc/lxc_template_unit_test.go | 36 +++++++----- integration-cli/docker_cli_run_test.go | 6 +- 3 files changed, 70 insertions(+), 30 deletions(-) diff --git a/daemon/execdriver/lxc/lxc_template.go b/daemon/execdriver/lxc/lxc_template.go index 5f0294ea1..c717cbca2 100644 --- a/daemon/execdriver/lxc/lxc_template.go +++ b/daemon/execdriver/lxc/lxc_template.go @@ -1,12 +1,17 @@ package lxc import ( - "github.com/docker/docker/daemon/execdriver" - nativeTemplate "github.com/docker/docker/daemon/execdriver/native/template" - "github.com/docker/libcontainer/label" + "fmt" "os" "strings" "text/template" + + log "github.com/Sirupsen/logrus" + "github.com/docker/docker/daemon/execdriver" + nativeTemplate "github.com/docker/docker/daemon/execdriver/native/template" + "github.com/docker/docker/utils" + "github.com/docker/libcontainer/label" + "github.com/docker/libcontainer/security/capabilities" ) const LxcTemplate = ` @@ -126,9 +131,17 @@ lxc.utsname = {{getHostname .ProcessConfig.Env}} {{if .ProcessConfig.Privileged}} # No cap values are needed, as lxc is starting in privileged mode {{else}} -{{range $value := keepCapabilities .CapAdd .CapDrop}} -lxc.cap.keep = {{$value}} -{{end}} + {{ with keepCapabilities .CapAdd .CapDrop }} + {{range .}} +lxc.cap.keep = {{.}} + {{end}} + {{else}} + {{ with dropList .CapDrop }} + {{range .}} +lxc.cap.drop = {{.}} + {{end}} + {{end}} + {{end}} {{end}} {{end}} ` @@ -141,17 +154,39 @@ func escapeFstabSpaces(field string) string { return strings.Replace(field, " ", "\\040", -1) } -func keepCapabilities(adds []string, drops []string) []string { +func keepCapabilities(adds []string, drops []string) ([]string, error) { container := nativeTemplate.New() + log.Debugf("adds %s drops %s\n", adds, drops) caps, err := execdriver.TweakCapabilities(container.Capabilities, adds, drops) + if err != nil { + return nil, err + } var newCaps []string for _, cap := range caps { - newCaps = append(newCaps, strings.ToLower(cap)) + log.Debugf("cap %s\n", cap) + realCap := capabilities.GetCapability(cap) + numCap := fmt.Sprintf("%d", realCap.Value) + newCaps = append(newCaps, numCap) } - if err != nil { - return []string{} + + return newCaps, nil +} + +func dropList(drops []string) ([]string, error) { + if utils.StringsContainsNoCase(drops, "all") { + var newCaps []string + for _, cap := range capabilities.GetAllCapabilities() { + log.Debugf("drop cap %s\n", cap) + realCap := capabilities.GetCapability(cap) + if realCap == nil { + return nil, fmt.Errorf("Invalid capability '%s'", cap) + } + numCap := fmt.Sprintf("%d", realCap.Value) + newCaps = append(newCaps, numCap) + } + return newCaps, nil } - return newCaps + return []string{}, nil } func isDirectory(source string) string { @@ -206,6 +241,7 @@ func init() { "formatMountLabel": label.FormatMountLabel, "isDirectory": isDirectory, "keepCapabilities": keepCapabilities, + "dropList": dropList, "getHostname": getHostname, } LxcTemplateCompiled, err = template.New("lxc").Funcs(funcMap).Parse(LxcTemplate) diff --git a/daemon/execdriver/lxc/lxc_template_unit_test.go b/daemon/execdriver/lxc/lxc_template_unit_test.go index f1410db77..e072f8dbb 100644 --- a/daemon/execdriver/lxc/lxc_template_unit_test.go +++ b/daemon/execdriver/lxc/lxc_template_unit_test.go @@ -5,6 +5,11 @@ package lxc import ( "bufio" "fmt" + "github.com/docker/docker/daemon/execdriver" + nativeTemplate "github.com/docker/docker/daemon/execdriver/native/template" + "github.com/docker/libcontainer/devices" + "github.com/docker/libcontainer/security/capabilities" + "github.com/syndtr/gocapability/capability" "io/ioutil" "math/rand" "os" @@ -12,10 +17,6 @@ import ( "strings" "testing" "time" - - "github.com/docker/docker/daemon/execdriver" - nativeTemplate "github.com/docker/docker/daemon/execdriver/native/template" - "github.com/docker/libcontainer/devices" ) func TestLXCConfig(t *testing.T) { @@ -292,13 +293,15 @@ func TestCustomLxcConfigMisc(t *testing.T) { grepFile(t, p, "lxc.cgroup.cpuset.cpus = 0,1") container := nativeTemplate.New() for _, cap := range container.Capabilities { - cap = strings.ToLower(cap) - if cap != "mknod" && cap != "kill" { - grepFile(t, p, fmt.Sprintf("lxc.cap.keep = %s", cap)) + realCap := capabilities.GetCapability(cap) + numCap := fmt.Sprintf("%d", realCap.Value) + if cap != "MKNOD" && cap != "KILL" { + grepFile(t, p, fmt.Sprintf("lxc.cap.keep = %s", numCap)) } } - grepFileWithReverse(t, p, fmt.Sprintf("lxc.cap.keep = kill"), true) - grepFileWithReverse(t, p, fmt.Sprintf("lxc.cap.keep = mknod"), true) + + grepFileWithReverse(t, p, fmt.Sprintf("lxc.cap.keep = %d", capability.CAP_KILL), true) + grepFileWithReverse(t, p, fmt.Sprintf("lxc.cap.keep = %d", capability.CAP_MKNOD), true) } func TestCustomLxcConfigMiscOverride(t *testing.T) { @@ -333,8 +336,8 @@ func TestCustomLxcConfigMiscOverride(t *testing.T) { }, }, ProcessConfig: processConfig, - CapAdd: []string{"net_admin", "syslog"}, - CapDrop: []string{"kill", "mknod"}, + CapAdd: []string{"NET_ADMIN", "SYSLOG"}, + CapDrop: []string{"KILL", "MKNOD"}, } p, err := driver.generateLXCConfig(command) @@ -354,11 +357,12 @@ func TestCustomLxcConfigMiscOverride(t *testing.T) { grepFile(t, p, "lxc.cgroup.cpuset.cpus = 0,1") container := nativeTemplate.New() for _, cap := range container.Capabilities { - cap = strings.ToLower(cap) - if cap != "mknod" && cap != "kill" { - grepFile(t, p, fmt.Sprintf("lxc.cap.keep = %s", cap)) + realCap := capabilities.GetCapability(cap) + numCap := fmt.Sprintf("%d", realCap.Value) + if cap != "MKNOD" && cap != "KILL" { + grepFile(t, p, fmt.Sprintf("lxc.cap.keep = %s", numCap)) } } - grepFileWithReverse(t, p, fmt.Sprintf("lxc.cap.keep = kill"), true) - grepFileWithReverse(t, p, fmt.Sprintf("lxc.cap.keep = mknod"), true) + grepFileWithReverse(t, p, fmt.Sprintf("lxc.cap.keep = %d", capability.CAP_KILL), true) + grepFileWithReverse(t, p, fmt.Sprintf("lxc.cap.keep = %d", capability.CAP_MKNOD), true) } diff --git a/integration-cli/docker_cli_run_test.go b/integration-cli/docker_cli_run_test.go index 30acd9e1c..21bd4fa72 100644 --- a/integration-cli/docker_cli_run_test.go +++ b/integration-cli/docker_cli_run_test.go @@ -986,7 +986,7 @@ func TestRunCapDropCannotMknodLowerCase(t *testing.T) { } func TestRunCapDropALLCannotMknod(t *testing.T) { - cmd := exec.Command(dockerBinary, "run", "--cap-drop=ALL", "busybox", "sh", "-c", "mknod /tmp/sda b 8 0 && echo ok") + cmd := exec.Command(dockerBinary, "run", "--cap-drop=ALL", "--cap-add=SETGID", "busybox", "sh", "-c", "mknod /tmp/sda b 8 0 && echo ok") out, _, err := runCommandWithOutput(cmd) if err == nil { t.Fatal(err, out) @@ -1000,8 +1000,8 @@ func TestRunCapDropALLCannotMknod(t *testing.T) { logDone("run - test --cap-drop=ALL cannot mknod") } -func TestRunCapDropALLAddMknodCannotMknod(t *testing.T) { - cmd := exec.Command(dockerBinary, "run", "--cap-drop=ALL", "--cap-add=MKNOD", "busybox", "sh", "-c", "mknod /tmp/sda b 8 0 && echo ok") +func TestRunCapDropALLAddMknodCanMknod(t *testing.T) { + cmd := exec.Command(dockerBinary, "run", "--cap-drop=ALL", "--cap-add=MKNOD", "--cap-add=SETGID", "busybox", "sh", "-c", "mknod /tmp/sda b 8 0 && echo ok") out, _, err := runCommandWithOutput(cmd) if err != nil { t.Fatal(err, out) From da01690a0a22ced060d4fb4fc46cb545ff1604df Mon Sep 17 00:00:00 2001 From: Tianon Gravi Date: Fri, 16 Jan 2015 21:46:01 -0700 Subject: [PATCH 329/513] Fix "-X" 6l usage ("define string data") It turns out "-X" is only for strings! :) Signed-off-by: Andrew "Tianon" Page --- dockerversion/dockerversion.go | 2 +- utils/utils.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dockerversion/dockerversion.go b/dockerversion/dockerversion.go index c130ac281..1898d5c61 100644 --- a/dockerversion/dockerversion.go +++ b/dockerversion/dockerversion.go @@ -9,7 +9,7 @@ var ( GITCOMMIT string VERSION string - IAMSTATIC bool // whether or not Docker itself was compiled statically via ./hack/make.sh binary + IAMSTATIC string // whether or not Docker itself was compiled statically via ./hack/make.sh binary ("true" or not "true") INITSHA1 string // sha1sum of separate static dockerinit, if Docker itself was compiled dynamically via ./hack/make.sh dynbinary INITPATH string // custom location to search for a valid dockerinit binary (available for packagers as a last resort escape hatch) ) diff --git a/utils/utils.go b/utils/utils.go index ccb3ec00c..a3e17b886 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -94,7 +94,7 @@ func isValidDockerInitPath(target string, selfPath string) bool { // target and if target == "" { return false } - if dockerversion.IAMSTATIC { + if dockerversion.IAMSTATIC == "true" { if selfPath == "" { return false } From b8f7526fc6333e5b67282e5b73eee497dd13ec34 Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Fri, 16 Jan 2015 09:47:32 -0500 Subject: [PATCH 330/513] Make .dockercfg with json.MarshallIndent Fixes #10129 Makes the .dockercfg more human parsable. Also cleaned up the (technically) racey login test. Signed-off-by: Brian Goff --- integration-cli/docker_cli_login_test.go | 14 ++------------ registry/auth.go | 2 +- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/integration-cli/docker_cli_login_test.go b/integration-cli/docker_cli_login_test.go index d2b927b11..9bf90f3ad 100644 --- a/integration-cli/docker_cli_login_test.go +++ b/integration-cli/docker_cli_login_test.go @@ -2,7 +2,6 @@ package main import ( "bytes" - "io" "os/exec" "testing" ) @@ -10,17 +9,8 @@ import ( func TestLoginWithoutTTY(t *testing.T) { cmd := exec.Command(dockerBinary, "login") - // create a buffer with text then a new line as a return - buf := bytes.NewBuffer([]byte("buffer test string \n")) - - // use a pipe for stdin and manually copy the data so that - // the process does not get the TTY - in, err := cmd.StdinPipe() - if err != nil { - t.Fatal(err) - } - // copy the bytes into the commands stdin along with a new line - go io.Copy(in, buf) + // Send to stdin so the process does not get the TTY + cmd.Stdin = bytes.NewBufferString("buffer test string \n") // run the command and block until it's done if err := cmd.Run(); err == nil { diff --git a/registry/auth.go b/registry/auth.go index 102078d7a..9d223f77e 100644 --- a/registry/auth.go +++ b/registry/auth.go @@ -133,7 +133,7 @@ func SaveConfig(configFile *ConfigFile) error { configs[k] = authCopy } - b, err := json.Marshal(configs) + b, err := json.MarshalIndent(configs, "", "\t") if err != nil { return err } From a738df0354cc615c8d0fa3254621b3db811fe0b9 Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Thu, 18 Dec 2014 09:57:36 -0500 Subject: [PATCH 331/513] Fix volumes-from re-applying on each start Fixes #9709 In cases where the volumes-from container is removed and the consuming container is restarted, docker was trying to re-apply volumes from that now missing container, which is uneccessary since the volumes are already applied. Also cleaned up the volumes-from parsing function, which was doing way more than it should have been. Signed-off-by: Brian Goff --- daemon/container.go | 7 ++- daemon/volumes.go | 80 +++++++++++++++----------- integration-cli/docker_cli_run_test.go | 39 ++++++++++--- 3 files changed, 82 insertions(+), 44 deletions(-) diff --git a/daemon/container.go b/daemon/container.go index becc69fce..79ae5f08e 100644 --- a/daemon/container.go +++ b/daemon/container.go @@ -92,9 +92,10 @@ type Container struct { VolumesRW map[string]bool hostConfig *runconfig.HostConfig - activeLinks map[string]*links.Link - monitor *containerMonitor - execCommands *execStore + activeLinks map[string]*links.Link + monitor *containerMonitor + execCommands *execStore + AppliedVolumesFrom map[string]struct{} } func (container *Container) FromDisk() error { diff --git a/daemon/volumes.go b/daemon/volumes.go index ad2dd3a6a..fa38b253f 100644 --- a/daemon/volumes.go +++ b/daemon/volumes.go @@ -214,20 +214,61 @@ func parseBindMountSpec(spec string) (string, string, bool, error) { return path, mountToPath, writable, nil } +func parseVolumesFromSpec(spec string) (string, string, error) { + specParts := strings.SplitN(spec, ":", 2) + if len(specParts) == 0 { + return "", "", fmt.Errorf("malformed volumes-from specification: %s", spec) + } + + var ( + id = specParts[0] + mode = "rw" + ) + if len(specParts) == 2 { + mode = specParts[1] + if !validMountMode(mode) { + return "", "", fmt.Errorf("invalid mode for volumes-from: %s", mode) + } + } + return id, mode, nil +} + func (container *Container) applyVolumesFrom() error { volumesFrom := container.hostConfig.VolumesFrom + if len(volumesFrom) > 0 && container.AppliedVolumesFrom == nil { + container.AppliedVolumesFrom = make(map[string]struct{}) + } - mountGroups := make([]map[string]*Mount, 0, len(volumesFrom)) + mountGroups := make(map[string][]*Mount) for _, spec := range volumesFrom { - mountGroup, err := parseVolumesFromSpec(container.daemon, spec) + id, mode, err := parseVolumesFromSpec(spec) if err != nil { return err } - mountGroups = append(mountGroups, mountGroup) + if _, exists := container.AppliedVolumesFrom[id]; exists { + // Don't try to apply these since they've already been applied + continue + } + + c := container.daemon.Get(id) + if c == nil { + return fmt.Errorf("container %s not found, impossible to mount its volumes", id) + } + + var ( + fromMounts = c.VolumeMounts() + mounts []*Mount + ) + + for _, mnt := range fromMounts { + mnt.Writable = mnt.Writable && (mode == "rw") + mounts = append(mounts, mnt) + } + mountGroups[id] = mounts } - for _, mounts := range mountGroups { + for id, mounts := range mountGroups { for _, mnt := range mounts { mnt.from = mnt.container mnt.container = container @@ -235,6 +276,7 @@ func (container *Container) applyVolumesFrom() error { return err } } + container.AppliedVolumesFrom[id] = struct{}{} } return nil } @@ -284,36 +326,6 @@ func (container *Container) setupMounts() error { return nil } -func parseVolumesFromSpec(daemon *Daemon, spec string) (map[string]*Mount, error) { - specParts := strings.SplitN(spec, ":", 2) - if len(specParts) == 0 { - return nil, fmt.Errorf("Malformed volumes-from specification: %s", spec) - } - - c := daemon.Get(specParts[0]) - if c == nil { - return nil, fmt.Errorf("Container %s not found. Impossible to mount its volumes", specParts[0]) - } - - mounts := c.VolumeMounts() - - if len(specParts) == 2 { - mode := specParts[1] - if !validMountMode(mode) { - return nil, fmt.Errorf("Invalid mode for volumes-from: %s", mode) - } - - // Set the mode for the inheritted volume - for _, mnt := range mounts { - // Ensure that if the inherited volume is not writable, that we don't make - // it writable here - mnt.Writable = mnt.Writable && (mode == "rw") - } - } - - return mounts, nil -} - func (container *Container) VolumeMounts() map[string]*Mount { mounts := make(map[string]*Mount) diff --git a/integration-cli/docker_cli_run_test.go b/integration-cli/docker_cli_run_test.go index 30acd9e1c..e23aea648 100644 --- a/integration-cli/docker_cli_run_test.go +++ b/integration-cli/docker_cli_run_test.go @@ -428,6 +428,7 @@ func TestRunVolumesMountedAsReadonly(t *testing.T) { } func TestRunVolumesFromInReadonlyMode(t *testing.T) { + defer deleteAllContainers() cmd := exec.Command(dockerBinary, "run", "--name", "parent", "-v", "/test", "busybox", "true") if _, err := runCommand(cmd); err != nil { t.Fatal(err) @@ -438,13 +439,12 @@ func TestRunVolumesFromInReadonlyMode(t *testing.T) { t.Fatalf("run should fail because volume is ro: exit code %d", code) } - deleteAllContainers() - logDone("run - volumes from as readonly mount") } // Regression test for #1201 func TestRunVolumesFromInReadWriteMode(t *testing.T) { + defer deleteAllContainers() cmd := exec.Command(dockerBinary, "run", "--name", "parent", "-v", "/test", "busybox", "true") if _, err := runCommand(cmd); err != nil { t.Fatal(err) @@ -456,7 +456,7 @@ func TestRunVolumesFromInReadWriteMode(t *testing.T) { } cmd = exec.Command(dockerBinary, "run", "--volumes-from", "parent:bar", "busybox", "touch", "/test/file") - if out, _, err := runCommandWithOutput(cmd); err == nil || !strings.Contains(out, "Invalid mode for volumes-from: bar") { + if out, _, err := runCommandWithOutput(cmd); err == nil || !strings.Contains(out, "invalid mode for volumes-from: bar") { t.Fatalf("running --volumes-from foo:bar should have failed with invalid mount mode: %q", out) } @@ -465,12 +465,11 @@ func TestRunVolumesFromInReadWriteMode(t *testing.T) { t.Fatalf("running --volumes-from parent failed with output: %q\nerror: %v", out, err) } - deleteAllContainers() - logDone("run - volumes from as read write mount") } func TestVolumesFromGetsProperMode(t *testing.T) { + defer deleteAllContainers() cmd := exec.Command(dockerBinary, "run", "--name", "parent", "-v", "/test:/test:ro", "busybox", "true") if _, err := runCommand(cmd); err != nil { t.Fatal(err) @@ -491,8 +490,6 @@ func TestVolumesFromGetsProperMode(t *testing.T) { t.Fatal("Expected volumes-from to inherit read-only volume even when passing in `ro`") } - deleteAllContainers() - logDone("run - volumes from ignores `rw` if inherrited volume is `ro`") } @@ -3058,3 +3055,31 @@ func TestRunContainerWithReadonlyRootfs(t *testing.T) { } logDone("run - read only rootfs") } + +func TestRunVolumesFromRestartAfterRemoved(t *testing.T) { + defer deleteAllContainers() + + out, _, err := runCommandWithOutput(exec.Command(dockerBinary, "run", "-d", "--name", "voltest", "-v", "/foo", "busybox")) + if err != nil { + t.Fatal(out, err) + } + + out, _, err = runCommandWithOutput(exec.Command(dockerBinary, "run", "-d", "--name", "restarter", "--volumes-from", "voltest", "busybox", "top")) + if err != nil { + t.Fatal(out, err) + } + + // Remove the main volume container and restart the consuming container + out, _, err = runCommandWithOutput(exec.Command(dockerBinary, "rm", "-f", "voltest")) + if err != nil { + t.Fatal(out, err) + } + + // This should not fail since the volumes-from were already applied + out, _, err = runCommandWithOutput(exec.Command(dockerBinary, "restart", "restarter")) + if err != nil { + t.Fatalf("expected container to restart successfully: %v\n%s", err, out) + } + + logDone("run - can restart a volumes-from container after producer is removed") +} From 79f17dcf7404d0547db68dba8b629c9c1141e47b Mon Sep 17 00:00:00 2001 From: Abin Shahab Date: Sun, 18 Jan 2015 03:21:45 +0000 Subject: [PATCH 332/513] LXC needs stdin for container to remain up To run shell(and not exit), lxc needs STDIN. Without STDIN open, it will exit 0. Signed-off-by: Abin Shahab (github: ashahab-altiscale) --- integration-cli/docker_cli_run_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integration-cli/docker_cli_run_test.go b/integration-cli/docker_cli_run_test.go index 21bd4fa72..0a6f3d2f4 100644 --- a/integration-cli/docker_cli_run_test.go +++ b/integration-cli/docker_cli_run_test.go @@ -316,7 +316,7 @@ func TestRunWithoutNetworking(t *testing.T) { //test --link use container name to link target func TestRunLinksContainerWithContainerName(t *testing.T) { - cmd := exec.Command(dockerBinary, "run", "-t", "-d", "--name", "parent", "busybox") + cmd := exec.Command(dockerBinary, "run", "-i", "-t", "-d", "--name", "parent", "busybox") out, _, _, err := runCommandWithStdoutStderr(cmd) if err != nil { t.Fatalf("failed to run container: %v, output: %q", err, out) @@ -342,7 +342,7 @@ func TestRunLinksContainerWithContainerName(t *testing.T) { //test --link use container id to link target func TestRunLinksContainerWithContainerId(t *testing.T) { - cmd := exec.Command(dockerBinary, "run", "-t", "-d", "busybox") + cmd := exec.Command(dockerBinary, "run", "-i", "-t", "-d", "busybox") cID, _, _, err := runCommandWithStdoutStderr(cmd) if err != nil { t.Fatalf("failed to run container: %v, output: %q", err, cID) From d821c63e0d5fe2abe10ff885de6298acc4db956e Mon Sep 17 00:00:00 2001 From: Abin Shahab Date: Sun, 18 Jan 2015 08:02:47 +0000 Subject: [PATCH 333/513] use lxc.auto.mount to ensure proc and sys are readonly Set lxc.auto.mount = proc:mixed in unprivilged mode. This ensures that lxc mounts sys and proc/sysrq-trigger as readonly. Signed-off-by: Abin Shahab (github: ashahab-altiscale) Docker-DCO-1.1-Signed-off-by: Abin Shahab (github: ashahab-altiscale) --- daemon/execdriver/lxc/lxc_template.go | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/daemon/execdriver/lxc/lxc_template.go b/daemon/execdriver/lxc/lxc_template.go index c717cbca2..99bb16198 100644 --- a/daemon/execdriver/lxc/lxc_template.go +++ b/daemon/execdriver/lxc/lxc_template.go @@ -61,13 +61,24 @@ lxc.cgroup.devices.allow = {{$allowedDevice.GetCgroupAllowString}} lxc.pivotdir = lxc_putold # NOTICE: These mounts must be applied within the namespace - +{{if .ProcessConfig.Privileged}} # WARNING: mounting procfs and/or sysfs read-write is a known attack vector. # See e.g. http://blog.zx2c4.com/749 and http://bit.ly/T9CkqJ # We mount them read-write here, but later, dockerinit will call the Restrict() function to remount them read-only. # We cannot mount them directly read-only, because that would prevent loading AppArmor profiles. lxc.mount.entry = proc {{escapeFstabSpaces $ROOTFS}}/proc proc nosuid,nodev,noexec 0 0 lxc.mount.entry = sysfs {{escapeFstabSpaces $ROOTFS}}/sys sysfs nosuid,nodev,noexec 0 0 + {{if .AppArmor}} +lxc.aa_profile = unconfined + {{end}} +{{else}} +# In non-privileged mode, lxc will automatically mount /proc and /sys in readonly mode +# for security. See: http://man7.org/linux/man-pages/man5/lxc.container.conf.5.html +lxc.mount.auto = proc sys + {{if .AppArmor}} +lxc.aa_profile = .AppArmorProfile + {{end}} +{{end}} {{if .ProcessConfig.Tty}} lxc.mount.entry = {{.ProcessConfig.Console}} {{escapeFstabSpaces $ROOTFS}}/dev/console none bind,rw 0 0 @@ -85,14 +96,6 @@ lxc.mount.entry = {{$value.Source}} {{escapeFstabSpaces $ROOTFS}}/{{escapeFstabS {{end}} {{end}} -{{if .ProcessConfig.Privileged}} -{{if .AppArmor}} -lxc.aa_profile = unconfined -{{else}} -# Let AppArmor normal confinement take place (i.e., not unconfined) -{{end}} -{{end}} - # limits {{if .Resources}} {{if .Resources.Memory}} From 44cde56333fe891e087b8aa5ba914345bbdedbc0 Mon Sep 17 00:00:00 2001 From: Bruno Gazzera Date: Sun, 18 Jan 2015 12:17:49 -0300 Subject: [PATCH 334/513] There was a missing command to re-run the web container. Signed-off-by: Bruno Gazzera --- docs/sources/userguide/dockerlinks.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/sources/userguide/dockerlinks.md b/docs/sources/userguide/dockerlinks.md index e2228cef0..ecadbb3f1 100644 --- a/docs/sources/userguide/dockerlinks.md +++ b/docs/sources/userguide/dockerlinks.md @@ -282,6 +282,8 @@ will be automatically updated with the source container's new IP address, allowing linked communication to continue. $ sudo docker restart db + db + $ sudo docker run -t -i --rm --link db:db training/webapp /bin/bash root@aed84ee21bde:/opt/webapp# cat /etc/hosts 172.17.0.7 aed84ee21bde . . . From 3fb06dc104bb66c3ec7be8a95a33845310083999 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Mon, 19 Jan 2015 00:23:57 +0100 Subject: [PATCH 335/513] Document that ENV vars are not automatically updated Unlike the entries in `/etc/hosts`, environment-variables for linked containers are not automatically updated if the linked container is restarted. This adds a note to the documentation in; https://docs.docker.com/userguide/dockerlinks/#environment-variables and https://docs.docker.com/reference/run/#env-environment-variables To make users aware that this is the case and recommends them to use the `/etc/hosts` entries in stead. I added this change because users were expecting environment variables to be updated automatically as well (https://github.com/docker/docker/issues/10164). Signed-off-by: Sebastiaan van Stijn --- docs/sources/reference/run.md | 6 ++++++ docs/sources/userguide/dockerlinks.md | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/docs/sources/reference/run.md b/docs/sources/reference/run.md index d594066ad..8bf0d3633 100644 --- a/docs/sources/reference/run.md +++ b/docs/sources/reference/run.md @@ -647,6 +647,12 @@ mechanism to communicate with a linked container by its alias: If you restart the source container (`servicename` in this case), the recipient container's `/etc/hosts` entry will be automatically updated. +> **Note**: +> Unlike host entries in the `/ets/hosts` file, IP addresses stored in the +> environment variables are not automatically updated if the source container is +> restarted. We recommend using the host entries in `/etc/hosts` to resolve the +> IP address of linked containers. + ## VOLUME (shared filesystems) -v=[]: Create a bind mount with: [host-dir]:[container-dir]:[rw|ro]. diff --git a/docs/sources/userguide/dockerlinks.md b/docs/sources/userguide/dockerlinks.md index e2228cef0..e01604be4 100644 --- a/docs/sources/userguide/dockerlinks.md +++ b/docs/sources/userguide/dockerlinks.md @@ -232,6 +232,12 @@ command to list the specified container's environment variables. > container. Similarly, some daemons (such as `sshd`) > will scrub them when spawning shells for connection. +> **Note**: +> Unlike host entries in the [`/ets/hosts` file](#updating-the-etchosts-file), +> IP addresses stored in the environment variables are not automatically updated +> if the source container is restarted. We recommend using the host entries in +> `/etc/hosts` to resolve the IP address of linked containers. + You can see that Docker has created a series of environment variables with useful information about the source `db` container. Each variable is prefixed with `DB_`, which is populated from the `alias` you specified above. If the `alias` From 750373875e25455acb046001cb0582873a90bd73 Mon Sep 17 00:00:00 2001 From: Lei Jitang Date: Mon, 19 Jan 2015 09:57:44 +0800 Subject: [PATCH 336/513] Update the docs for --link accept container id Signed-off-by: Lei Jitang --- docs/man/docker-create.1.md | 2 +- docs/man/docker-run.1.md | 2 +- docs/sources/articles/networking.md | 10 +++++----- docs/sources/reference/commandline/cli.md | 2 +- docs/sources/reference/run.md | 4 ++-- docs/sources/userguide/dockerlinks.md | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/man/docker-create.1.md b/docs/man/docker-create.1.md index 24185489f..8a0b91f7c 100644 --- a/docs/man/docker-create.1.md +++ b/docs/man/docker-create.1.md @@ -102,7 +102,7 @@ IMAGE [COMMAND] [ARG...] 'host': use the host shared memory,semaphores and message queues inside the container. Note: the host mode gives the container full access to local shared memory and is therefore considered insecure. **--link**=[] - Add link to another container in the form of name:alias + Add link to another container in the form of :alias **--lxc-conf**=[] (lxc exec-driver only) Add custom lxc options --lxc-conf="lxc.cgroup.cpuset.cpus = 0,1" diff --git a/docs/man/docker-run.1.md b/docs/man/docker-run.1.md index b16447bc5..53d813e80 100644 --- a/docs/man/docker-run.1.md +++ b/docs/man/docker-run.1.md @@ -170,7 +170,7 @@ ENTRYPOINT. 'host': use the host shared memory,semaphores and message queues inside the container. Note: the host mode gives the container full access to local shared memory and is therefore considered insecure. **--link**=[] - Add link to another container in the form of name:alias + Add link to another container in the form of :alias If the operator uses **--link** when starting the new client container, then the client diff --git a/docs/sources/articles/networking.md b/docs/sources/articles/networking.md index 85e6222d8..2a0a74f95 100644 --- a/docs/sources/articles/networking.md +++ b/docs/sources/articles/networking.md @@ -105,7 +105,7 @@ Finally, several networking options can only be provided when calling [Configuring DNS](#dns) and [How Docker networks a container](#container-networking) - * `--link=CONTAINER_NAME:ALIAS` — see + * `--link=CONTAINER_NAME_or_ID:ALIAS` — see [Configuring DNS](#dns) and [Communication between containers](#between-containers) @@ -158,10 +158,10 @@ Four different options affect container domain name services. outside the container. It will not appear in `docker ps` nor in the `/etc/hosts` file of any other container. - * `--link=CONTAINER_NAME:ALIAS` — using this option as you `run` a + * `--link=CONTAINER_NAME_or_ID:ALIAS` — using this option as you `run` a container gives the new container's `/etc/hosts` an extra entry - named `ALIAS` that points to the IP address of the container named - `CONTAINER_NAME`. This lets processes inside the new container + named `ALIAS` that points to the IP address of the container identified by + `CONTAINER_NAME_or_ID`. This lets processes inside the new container connect to the hostname `ALIAS` without having to know its IP. The `--link=` option is discussed in more detail below, in the section [Communication between containers](#between-containers). Because @@ -284,7 +284,7 @@ If you choose the most secure setting of `--icc=false`, then how can containers communicate in those cases where you *want* them to provide each other services? -The answer is the `--link=CONTAINER_NAME:ALIAS` option, which was +The answer is the `--link=CONTAINER_NAME_or_ID:ALIAS` option, which was mentioned in the previous section because of its effect upon name services. If the Docker daemon is running with both `--icc=false` and `--iptables=true` then, when it sees `docker run` invoked with the diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index e79664e6a..7508c8013 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -738,7 +738,7 @@ Creates a new container. --ipc="" Default is to create a private IPC namespace (POSIX SysV IPC) for the container 'container:': reuses another container shared memory, semaphores and message queues 'host': use the host shared memory,semaphores and message queues inside the container. Note: the host mode gives the container full access to local shared memory and is therefore considered insecure. - --link=[] Add link to another container in the form of name:alias + --link=[] Add link to another container in the form of :alias --lxc-conf=[] (lxc exec-driver only) Add custom lxc options --lxc-conf="lxc.cgroup.cpuset.cpus = 0,1" -m, --memory="" Memory limit (format: , where unit = b, k, m or g) --mac-address="" Container MAC address (e.g. 92:d0:c6:0a:29:33) diff --git a/docs/sources/reference/run.md b/docs/sources/reference/run.md index d594066ad..c2df0641c 100644 --- a/docs/sources/reference/run.md +++ b/docs/sources/reference/run.md @@ -512,7 +512,7 @@ or override the Dockerfile's exposed defaults: Both hostPort and containerPort can be specified as a range of ports. When specifying ranges for both, the number of container ports in the range must match the number of host ports in the range. (e.g., `-p 1234-1236:1234-1236/tcp`) (use 'docker port' to see the actual mapping) - --link="" : Add link to another container (name:alias) + --link="" : Add link to another container (:alias) As mentioned previously, `EXPOSE` (and `--expose`) makes ports available **in** a container for incoming connections. The port number on the @@ -595,7 +595,7 @@ above, or already defined by the developer with a Dockerfile `ENV`: Similarly the operator can set the **hostname** with `-h`. -`--link name:alias` also sets environment variables, using the *alias* string to +`--link :alias` also sets environment variables, using the *alias* string to define environment variables within the container that give the IP and PORT information for connecting to the service container. Let's imagine we have a container running Redis: diff --git a/docs/sources/userguide/dockerlinks.md b/docs/sources/userguide/dockerlinks.md index e2228cef0..6e8be77f7 100644 --- a/docs/sources/userguide/dockerlinks.md +++ b/docs/sources/userguide/dockerlinks.md @@ -146,7 +146,7 @@ Now, create a new `web` container and link it with your `db` container. This will link the new `web` container with the `db` container you created earlier. The `--link` flag takes the form: - --link name:alias + --link :alias Where `name` is the name of the container we're linking to and `alias` is an alias for the link name. You'll see how that alias gets used shortly. From 1b2032c82e344b918578ce2de3618597e36fd7d6 Mon Sep 17 00:00:00 2001 From: Sven Dowideit Date: Mon, 19 Jan 2015 09:55:08 +1000 Subject: [PATCH 337/513] An initial quick import of documentation from Compose, Machine and Swarm Docker-DCO-1.1-Signed-off-by: Sven Dowideit (github: SvenDowideit) --- docs/Dockerfile | 25 ++++++++++++++++++ docs/README.md | 2 +- docs/build.sh | 57 +++++++++++++++++++++++++++++++++++++++++ docs/mkdocs-compose.yml | 5 ++++ docs/mkdocs-machine.yml | 2 ++ docs/mkdocs-swarm.yml | 5 ++++ 6 files changed, 95 insertions(+), 1 deletion(-) create mode 100755 docs/build.sh create mode 100644 docs/mkdocs-compose.yml create mode 100644 docs/mkdocs-machine.yml create mode 100644 docs/mkdocs-swarm.yml diff --git a/docs/Dockerfile b/docs/Dockerfile index a29e8c95f..1b5aaec12 100644 --- a/docs/Dockerfile +++ b/docs/Dockerfile @@ -59,3 +59,28 @@ EXPOSE 8000 RUN cd sources && rgrep --files-with-matches '{{ include ".*" }}' | xargs sed -i~ 's/{{ include "\(.*\)" }}/cat include\/\1/ge' CMD ["mkdocs", "serve"] + +# Initial Dockerfile driven documenation aggregation +# Sven plans to move each Dockerfile into the respective repository + +# Docker Swarm +ADD https://raw.githubusercontent.com/docker/swarm/master/logo.png /docs/sources/swarm/logo.png +ADD https://raw.githubusercontent.com/docker/swarm/master/README.md /docs/sources/swarm/README.md +ADD https://raw.githubusercontent.com/docker/swarm/master/discovery/README.md /docs/sources/swarm/discovery.md +ADD https://raw.githubusercontent.com/docker/swarm/master/api/README.md /docs/sources/swarm/API.md +ADD https://raw.githubusercontent.com/docker/swarm/master/scheduler/filter/README.md /docs/sources/swarm/filters.md + +# Docker Machine +ADD https://raw.githubusercontent.com/docker/machine/master/docs/dockermachine.md /docs/sources/machine/userguide.md + +# Docker Compose +ADD https://raw.githubusercontent.com/docker/fig/master/docs/index.md /docs/sources/compose/userguide.md +ADD https://raw.githubusercontent.com/docker/fig/master/docs/install.md /docs/sources/compose/install.md +ADD https://raw.githubusercontent.com/docker/fig/master/docs/cli.md /docs/sources/compose/cli.md +ADD https://raw.githubusercontent.com/docker/fig/master/docs/yml.md /docs/sources/compose/yml.md + +# add the project docs from the `mkdocs-.yml` files +RUN cd /docs && ./build.sh + +# remove `^---*` lines from md's +RUN cd /docs/sources && find . -name "*.md" | xargs sed -i~ -n '/^---*/!p' diff --git a/docs/README.md b/docs/README.md index b730982e3..4121fe723 100755 --- a/docs/README.md +++ b/docs/README.md @@ -25,7 +25,7 @@ In the root of the `docker` source directory: $ make docs .... (lots of output) .... - $ docker run --rm -it -e AWS_S3_BUCKET -p 8000:8000 "docker-docs:master" mkdocs serve + docker run --rm -it -e AWS_S3_BUCKET -p 8000:8000 "docker-docs:master" mkdocs serve Running at: http://0.0.0.0:8000/ Live reload enabled. Hold ctrl+c to quit. diff --git a/docs/build.sh b/docs/build.sh new file mode 100755 index 000000000..033820c67 --- /dev/null +++ b/docs/build.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +set -e + +set -o pipefail + +usage() { + exit 1 +} + + +extrafiles=($(find . -name "mkdocs-*.yml")) +extralines=() + +for file in "${extrafiles[@]}" +do + #echo "LOADING $file" + while read line + do + if [[ "$line" != "" ]] + then + extralines+=("$line") + + #echo "LINE (${#extralines[@]}): $line" + fi + done < <(cat "$file") +done + +#echo "extra count (${#extralines[@]})" +mv mkdocs.yml mkdocs.yml.bak +echo "# Generated mkdocs.yml from ${extrafiles[@]}" +echo "# Generated mkdocs.yml from ${extrafiles[@]}" > mkdocs.yml + +while read line +do + menu=$(echo $line | sed "s/^- \['\([^']*\)', '\([^']*\)'.*/\2/") + if [[ "$menu" != "**HIDDEN**" ]] + # or starts with a '#'? + then + if [[ "$lastmenu" != "" && "$lastmenu" != "$menu" ]] + then + # insert extra elements here + for extra in "${extralines[@]}" + do + #echo "EXTRA $extra" + extramenu=$(echo $extra | sed "s/^- \['\([^']*\)', '\([^']*\)'.*/\2/") + if [[ "$extramenu" == "$lastmenu" ]] + then + echo "$extra" >> mkdocs.yml + fi + done + #echo "# JUST FINISHED $lastmenu" + fi + lastmenu="$menu" + fi + echo "$line" >> mkdocs.yml + +done < <(cat "mkdocs.yml.bak") diff --git a/docs/mkdocs-compose.yml b/docs/mkdocs-compose.yml new file mode 100644 index 000000000..e2738f328 --- /dev/null +++ b/docs/mkdocs-compose.yml @@ -0,0 +1,5 @@ + +- ['compose/userguide.md', 'User Guide', 'Docker Compose' ] +- ['compose/install.md', 'Installation', 'Docker Compose'] +- ['compose/cli.md', 'Reference', 'Compose command line'] +- ['compose/yml.md', 'Reference', 'Compose yml'] diff --git a/docs/mkdocs-machine.yml b/docs/mkdocs-machine.yml new file mode 100644 index 000000000..45b2c5c84 --- /dev/null +++ b/docs/mkdocs-machine.yml @@ -0,0 +1,2 @@ + +- ['machine/userguide.md', 'User Guide', 'Docker Machine' ] diff --git a/docs/mkdocs-swarm.yml b/docs/mkdocs-swarm.yml new file mode 100644 index 000000000..c59634bf2 --- /dev/null +++ b/docs/mkdocs-swarm.yml @@ -0,0 +1,5 @@ + +- ['swarm/README.md', 'User Guide', 'Docker Swarm' ] +- ['swarm/discovery.md', 'Reference', 'Swarm discovery'] +- ['swarm/API.md', 'Reference', 'Swarm API'] +- ['swarm/filters.md', 'Reference', 'Swarm filters'] From 9dc2d0b8a35724946139f954f9575411b31695ea Mon Sep 17 00:00:00 2001 From: Yongzhi Pan Date: Mon, 19 Jan 2015 16:15:27 +0800 Subject: [PATCH 338/513] Add usage of port mapping for Boot2docker. Signed-off-by: Yongzhi Pan --- docs/sources/examples/nodejs_web_app.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/sources/examples/nodejs_web_app.md b/docs/sources/examples/nodejs_web_app.md index 39af59afc..7358a3f50 100644 --- a/docs/sources/examples/nodejs_web_app.md +++ b/docs/sources/examples/nodejs_web_app.md @@ -181,6 +181,11 @@ Now you can call your app using `curl` (install if needed via: Hello world +If you use Boot2docker on OS X, the port is actually mapped to the Docker host VM, +and you should use the following command: + + $ curl $(boot2docker ip):49160 + We hope this tutorial helped you get up and running with Node.js and CentOS on Docker. You can get the full source code at [https://github.com/enokd/docker-node-hello/](https://github.com/enokd/docker-node-hello/). From 1b4a926377524455653019c772355bd71a148045 Mon Sep 17 00:00:00 2001 From: Liang-Chi Hsieh Date: Mon, 19 Jan 2015 18:02:23 +0800 Subject: [PATCH 339/513] Fix typo. Signed-off-by: Liang-Chi Hsieh --- docs/sources/reference/commandline/cli.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index 47d32b4d9..cb17a67eb 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -1699,7 +1699,7 @@ folder before starting your container. $ sudo docker run --read-only -v /icanwrite busybox touch /icanwrite here Volumes can be used in combination with `--read-only` to control where -a container writes files. The `--read only` flag mounts the container's root +a container writes files. The `--read-only` flag mounts the container's root filesystem as read only prohibiting writes to locations other than the specified volumes for the container. From 9ab73260f8e4662e7321b257c636928892f023cf Mon Sep 17 00:00:00 2001 From: Doug Davis Date: Fri, 16 Jan 2015 12:57:08 -0800 Subject: [PATCH 340/513] Docker run -e FOO should erase FOO if FOO isn't set in client env See #10141 for more info, but the main point of this is to make sure that if you do "docker run -e FOO ..." that FOO from the current env is passed into the container. This means that if there's a value, its set. But it also means that if FOO isn't set then it should be unset in the container too - even if it has to remove it from the env. So, unset HOSTNAME docker run -e HOSTNAME busybox env should _NOT_ show HOSTNAME in the list at all Closes #10141 Signed-off-by: Doug Davis --- docs/sources/reference/commandline/cli.md | 9 ++- integration-cli/docker_cli_run_test.go | 71 +++++++++++++++++++++-- opts/opts.go | 4 ++ utils/utils.go | 29 +++++++++ 4 files changed, 106 insertions(+), 7 deletions(-) diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index e79664e6a..142ec5c61 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -1725,9 +1725,12 @@ ports in Docker. This sets environmental variables in the container. For illustration all three flags are shown here. Where `-e`, `--env` take an environment variable and -value, or if no "=" is provided, then that variable's current value is passed -through (i.e. `$MYVAR1` from the host is set to `$MYVAR1` in the container). All -three flags, `-e`, `--env` and `--env-file` can be repeated. +value, or if no `=` is provided, then that variable's current value is passed +through (i.e. `$MYVAR1` from the host is set to `$MYVAR1` in the container). +When no `=` is provided and that variable is not defined in the client's +environment then that variable will be removed from the container's list of +environment variables. +All three flags, `-e`, `--env` and `--env-file` can be repeated. Regardless of the order of these three flags, the `--env-file` are processed first, and then `-e`, `--env` flags. This way, the `-e` or `--env` will diff --git a/integration-cli/docker_cli_run_test.go b/integration-cli/docker_cli_run_test.go index 30acd9e1c..deadb7cf3 100644 --- a/integration-cli/docker_cli_run_test.go +++ b/integration-cli/docker_cli_run_test.go @@ -795,10 +795,7 @@ func TestRunEnvironment(t *testing.T) { t.Fatal(err, out) } - actualEnv := strings.Split(out, "\n") - if actualEnv[len(actualEnv)-1] == "" { - actualEnv = actualEnv[:len(actualEnv)-1] - } + actualEnv := strings.Split(strings.TrimSpace(out), "\n") sort.Strings(actualEnv) goodEnv := []string{ @@ -826,6 +823,72 @@ func TestRunEnvironment(t *testing.T) { logDone("run - verify environment") } +func TestRunEnvironmentErase(t *testing.T) { + // Test to make sure that when we use -e on env vars that are + // not set in our local env that they're removed (if present) in + // the container + cmd := exec.Command(dockerBinary, "run", "-e", "FOO", "-e", "HOSTNAME", "busybox", "env") + cmd.Env = []string{} + out, _, err := runCommandWithOutput(cmd) + if err != nil { + t.Fatal(err, out) + } + + actualEnv := strings.Split(strings.TrimSpace(out), "\n") + sort.Strings(actualEnv) + + goodEnv := []string{ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "HOME=/root", + } + sort.Strings(goodEnv) + if len(goodEnv) != len(actualEnv) { + t.Fatalf("Wrong environment: should be %d variables, not: %q\n", len(goodEnv), strings.Join(actualEnv, ", ")) + } + for i := range goodEnv { + if actualEnv[i] != goodEnv[i] { + t.Fatalf("Wrong environment variable: should be %s, not %s", goodEnv[i], actualEnv[i]) + } + } + + deleteAllContainers() + + logDone("run - verify environment erase") +} + +func TestRunEnvironmentOverride(t *testing.T) { + // Test to make sure that when we use -e on env vars that are + // already in the env that we're overriding them + cmd := exec.Command(dockerBinary, "run", "-e", "HOSTNAME", "-e", "HOME=/root2", "busybox", "env") + cmd.Env = []string{"HOSTNAME=bar"} + out, _, err := runCommandWithOutput(cmd) + if err != nil { + t.Fatal(err, out) + } + + actualEnv := strings.Split(strings.TrimSpace(out), "\n") + sort.Strings(actualEnv) + + goodEnv := []string{ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "HOME=/root2", + "HOSTNAME=bar", + } + sort.Strings(goodEnv) + if len(goodEnv) != len(actualEnv) { + t.Fatalf("Wrong environment: should be %d variables, not: %q\n", len(goodEnv), strings.Join(actualEnv, ", ")) + } + for i := range goodEnv { + if actualEnv[i] != goodEnv[i] { + t.Fatalf("Wrong environment variable: should be %s, not %s", goodEnv[i], actualEnv[i]) + } + } + + deleteAllContainers() + + logDone("run - verify environment override") +} + func TestRunContainerNetwork(t *testing.T) { cmd := exec.Command(dockerBinary, "run", "busybox", "ping", "-c", "1", "127.0.0.1") if _, err := runCommand(cmd); err != nil { diff --git a/opts/opts.go b/opts/opts.go index 3d8c23ff7..7f4019341 100644 --- a/opts/opts.go +++ b/opts/opts.go @@ -11,6 +11,7 @@ import ( "github.com/docker/docker/api" flag "github.com/docker/docker/pkg/mflag" "github.com/docker/docker/pkg/parsers" + "github.com/docker/docker/utils" ) var ( @@ -168,6 +169,9 @@ func ValidateEnv(val string) (string, error) { if len(arr) > 1 { return val, nil } + if !utils.DoesEnvExist(val) { + return val, nil + } return fmt.Sprintf("%s=%s", val, os.Getenv(val)), nil } diff --git a/utils/utils.go b/utils/utils.go index ccb3ec00c..ae30d6865 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -401,7 +401,17 @@ func ReplaceOrAppendEnvValues(defaults, overrides []string) []string { parts := strings.SplitN(e, "=", 2) cache[parts[0]] = i } + for _, value := range overrides { + // Values w/o = means they want this env to be removed/unset. + if !strings.Contains(value, "=") { + if i, exists := cache[value]; exists { + defaults[i] = "" // Used to indicate it should be removed + } + continue + } + + // Just do a normal set/update parts := strings.SplitN(value, "=", 2) if i, exists := cache[parts[0]]; exists { defaults[i] = value @@ -409,9 +419,28 @@ func ReplaceOrAppendEnvValues(defaults, overrides []string) []string { defaults = append(defaults, value) } } + + // Now remove all entries that we want to "unset" + for i := 0; i < len(defaults); i++ { + if defaults[i] == "" { + defaults = append(defaults[:i], defaults[i+1:]...) + i-- + } + } + return defaults } +func DoesEnvExist(name string) bool { + for _, entry := range os.Environ() { + parts := strings.SplitN(entry, "=", 2) + if parts[0] == name { + return true + } + } + return false +} + // ReadSymlinkedDirectory returns the target directory of a symlink. // The target of the symbolic link may not be a file. func ReadSymlinkedDirectory(path string) (string, error) { From f0d79c021d7cd6c78cc830154b6bbee1dcf9ec8b Mon Sep 17 00:00:00 2001 From: Jessica Frazelle Date: Mon, 19 Jan 2015 10:09:46 -0800 Subject: [PATCH 341/513] Update graphtest so when overlay is tried over a non-supported backing filesystem it will skip. Docker-DCO-1.1-Signed-off-by: Jessica Frazelle (github: jfrazelle) --- daemon/graphdriver/graphtest/graphtest.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/daemon/graphdriver/graphtest/graphtest.go b/daemon/graphdriver/graphtest/graphtest.go index 67f15c594..af93ea829 100644 --- a/daemon/graphdriver/graphtest/graphtest.go +++ b/daemon/graphdriver/graphtest/graphtest.go @@ -5,6 +5,7 @@ import ( "io/ioutil" "os" "path" + "strings" "syscall" "testing" @@ -73,7 +74,7 @@ func newDriver(t *testing.T, name string) *Driver { d, err := graphdriver.GetDriver(name, root, nil) if err != nil { - if err == graphdriver.ErrNotSupported || err == graphdriver.ErrPrerequisites { + if err == graphdriver.ErrNotSupported || err == graphdriver.ErrPrerequisites || strings.Contains(err.Error(), "'overlay' is not supported over") { t.Skipf("Driver %s not supported", name) } t.Fatal(err) From 6b04d9342c3d76f5979850f93a4afd41afa33227 Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Fri, 16 Jan 2015 14:52:16 -0800 Subject: [PATCH 342/513] Update to libcontainer eb74393a3d2daeafbef4f5f27c0 Signed-off-by: Michael Crosby --- project/vendor.sh | 2 +- .../libcontainer/cgroups/fs/apply_raw.go | 8 +++++- .../docker/libcontainer/namespaces/execin.go | 5 ++++ .../libcontainer/namespaces/nsenter/nsenter.c | 21 ++++++++------ .../namespaces/nsenter/nsenter_test.go | 28 ++++++++++++++----- 5 files changed, 47 insertions(+), 17 deletions(-) diff --git a/project/vendor.sh b/project/vendor.sh index d15d26171..b60e42f5c 100755 --- a/project/vendor.sh +++ b/project/vendor.sh @@ -68,7 +68,7 @@ if [ "$1" = '--go' ]; then mv tmp-tar src/code.google.com/p/go/src/pkg/archive/tar fi -clone git github.com/docker/libcontainer 1d3b2589d734dc94a1719a3af40b87ed8319f329 +clone git github.com/docker/libcontainer eb74393a3d2daeafbef4f5f27c0821cbdd67559c # see src/github.com/docker/libcontainer/update-vendor.sh which is the "source of truth" for libcontainer deps (just like this file) rm -rf src/github.com/docker/libcontainer/vendor eval "$(grep '^clone ' src/github.com/docker/libcontainer/update-vendor.sh | grep -v 'github.com/codegangsta/cli')" diff --git a/vendor/src/github.com/docker/libcontainer/cgroups/fs/apply_raw.go b/vendor/src/github.com/docker/libcontainer/cgroups/fs/apply_raw.go index f05377f25..58046b0ad 100644 --- a/vendor/src/github.com/docker/libcontainer/cgroups/fs/apply_raw.go +++ b/vendor/src/github.com/docker/libcontainer/cgroups/fs/apply_raw.go @@ -124,11 +124,17 @@ func Freeze(c *cgroups.Cgroup, state cgroups.FreezerState) error { return err } + prevState := c.Freezer c.Freezer = state freezer := subsystems["freezer"] + err = freezer.Set(d) + if err != nil { + c.Freezer = prevState + return err + } - return freezer.Set(d) + return nil } func GetPids(c *cgroups.Cgroup) ([]int, error) { diff --git a/vendor/src/github.com/docker/libcontainer/namespaces/execin.go b/vendor/src/github.com/docker/libcontainer/namespaces/execin.go index 7ce82c81b..ddff5c3a9 100644 --- a/vendor/src/github.com/docker/libcontainer/namespaces/execin.go +++ b/vendor/src/github.com/docker/libcontainer/namespaces/execin.go @@ -73,6 +73,11 @@ func ExecIn(container *libcontainer.Config, state *libcontainer.State, userArgs return terminate(err) } + // finish cgroups' setup, unblock the child process. + if _, err := parent.WriteString("1"); err != nil { + return terminate(err) + } + if err := json.NewEncoder(parent).Encode(container); err != nil { return terminate(err) } diff --git a/vendor/src/github.com/docker/libcontainer/namespaces/nsenter/nsenter.c b/vendor/src/github.com/docker/libcontainer/namespaces/nsenter/nsenter.c index 9782702dc..4ab21774f 100644 --- a/vendor/src/github.com/docker/libcontainer/namespaces/nsenter/nsenter.c +++ b/vendor/src/github.com/docker/libcontainer/namespaces/nsenter/nsenter.c @@ -28,7 +28,6 @@ void get_args(int *argc, char ***argv) pr_perror("Unable to open /proc/self/cmdline"); exit(1); } - // Read the whole commandline. ssize_t contents_size = 0; ssize_t contents_offset = 0; @@ -98,13 +97,12 @@ void nsenter() if (strncmp(argv[0], kNsEnter, strlen(kNsEnter)) != 0) { return; } - - #ifdef PR_SET_CHILD_SUBREAPER +#ifdef PR_SET_CHILD_SUBREAPER if (prctl(PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0) == -1) { pr_perror("Failed to set child subreaper"); exit(1); } - #endif +#endif static const struct option longopts[] = { {"nspid", required_argument, NULL, 'n'}, @@ -134,7 +132,7 @@ void nsenter() init_pid = strtol(init_pid_str, NULL, 10); if ((init_pid == 0 && errno == EINVAL) || errno == ERANGE) { pr_perror("Failed to parse PID from \"%s\" with output \"%d\"", - init_pid_str, init_pid); + init_pid_str, init_pid); print_usage(); exit(1); } @@ -155,6 +153,12 @@ void nsenter() exit(1); } } + // blocking until the parent placed the process inside correct cgroups. + unsigned char s; + if (read(3, &s, 1) != 1 || s != '1') { + pr_perror("failed to receive synchronization data from parent"); + exit(1); + } // Setns on all supported namespaces. char ns_dir[PATH_MAX]; memset(ns_dir, 0, PATH_MAX); @@ -173,18 +177,19 @@ void nsenter() for (i = 0; i < num; i++) { // A zombie process has links on namespaces, but they can't be opened struct stat st; - if (fstatat(ns_dir_fd, namespaces[i], &st, AT_SYMLINK_NOFOLLOW) == -1) { + if (fstatat(ns_dir_fd, namespaces[i], &st, AT_SYMLINK_NOFOLLOW) + == -1) { if (errno == ENOENT) continue; pr_perror("Failed to stat ns file %s for ns %s", - ns_dir, namespaces[i]); + ns_dir, namespaces[i]); exit(1); } int fd = openat(ns_dir_fd, namespaces[i], O_RDONLY); if (fd == -1) { pr_perror("Failed to open ns file %s for ns %s", - ns_dir, namespaces[i]); + ns_dir, namespaces[i]); exit(1); } // Set the namespace. diff --git a/vendor/src/github.com/docker/libcontainer/namespaces/nsenter/nsenter_test.go b/vendor/src/github.com/docker/libcontainer/namespaces/nsenter/nsenter_test.go index 85ee5d672..14870c457 100644 --- a/vendor/src/github.com/docker/libcontainer/namespaces/nsenter/nsenter_test.go +++ b/vendor/src/github.com/docker/libcontainer/namespaces/nsenter/nsenter_test.go @@ -12,15 +12,29 @@ import ( func TestNsenterAlivePid(t *testing.T) { args := []string{"nsenter-exec", "--nspid", fmt.Sprintf("%d", os.Getpid())} - - cmd := &exec.Cmd{ - Path: os.Args[0], - Args: args, + r, w, err := os.Pipe() + if err != nil { + t.Fatalf("failed to create pipe %v", err) } - err := cmd.Run() - if err != nil { - t.Fatal("nsenter exits with a non-zero exit status") + cmd := &exec.Cmd{ + Path: os.Args[0], + Args: args, + ExtraFiles: []*os.File{r}, + } + + if err := cmd.Start(); err != nil { + t.Fatalf("nsenter failed to start %v", err) + } + r.Close() + + // unblock the child process + if _, err := w.WriteString("1"); err != nil { + t.Fatalf("parent failed to write synchronization data %v", err) + } + + if err := cmd.Wait(); err != nil { + t.Fatalf("nsenter exits with a non-zero exit status") } } From 3a1dbef8232ee4f58cbaf3a2776c27a4d5b04bc4 Mon Sep 17 00:00:00 2001 From: Arnaud Porterie Date: Mon, 19 Jan 2015 11:23:31 -0800 Subject: [PATCH 343/513] Add documentation for HTTP proxies Signed-off-by: Arnaud Porterie --- docs/sources/reference/commandline/cli.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index cb17a67eb..c3788a1d2 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -170,6 +170,14 @@ string is equivalent to setting the `--tlsverify` flag. The following are equiva $ export DOCKER_TLS_VERIFY=1 $ sudo docker ps +The Docker client will honor the `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` +environment variables (or the lowercase versions thereof). `HTTPS_PROXY` takes +precedence over `HTTP_PROXY`. If you happen to have a proxy configured with the +`HTTP_PROXY` or `HTTPS_PROXY` environment variables but still want to +communicate with the Docker daemon over its default `unix` domain socket, +setting the `NO_PROXY` environment variable to the path of the socket +(`/var/run/docker.sock`) is required. + ### Daemon storage-driver option The Docker daemon has support for several different image layer storage drivers: `aufs`, From 232d59baeb13778abc242a602ca434d83e1eb6e8 Mon Sep 17 00:00:00 2001 From: Tianon Gravi Date: Fri, 16 Jan 2015 22:00:44 -0700 Subject: [PATCH 344/513] Let's try fixing "netgo" again Since "go test" doesn't seem to support "-installsuffix" as quite the same perfect solution that "go build" is happy to let it be, let's just switch those crappy old "integration/" tests to use our separate static dockerinit binary so we don't have to worry about compiling the entire test harness statically. :+1: Signed-off-by: Andrew "Tianon" Page --- Dockerfile | 3 --- project/make.sh | 7 +++---- project/make/.dockerinit | 29 +++++++++++++++++++++++++++++ project/make/dynbinary | 29 ++--------------------------- project/make/dyntest-integration | 18 ------------------ project/make/dyntest-unit | 18 ------------------ project/make/test-integration | 12 +++++++++++- project/make/test-unit | 2 +- 8 files changed, 46 insertions(+), 72 deletions(-) create mode 100644 project/make/.dockerinit delete mode 100644 project/make/dyntest-integration delete mode 100644 project/make/dyntest-unit diff --git a/Dockerfile b/Dockerfile index 59d9ced90..dd23f561a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -97,9 +97,6 @@ RUN cd /usr/local/go/src \ ./make.bash --no-clean 2>&1; \ done -# Reinstall standard library with netgo -RUN go clean -i net && go install -tags netgo std - # We still support compiling with older Go, so need to grab older "gofmt" ENV GOFMT_VERSION 1.3.3 RUN curl -sSL https://storage.googleapis.com/golang/go${GOFMT_VERSION}.$(go env GOOS)-$(go env GOARCH).tar.gz | tar -C /go/bin -xz --strip-components=2 go/bin/gofmt diff --git a/project/make.sh b/project/make.sh index f7919515c..bb2a3419f 100755 --- a/project/make.sh +++ b/project/make.sh @@ -48,13 +48,11 @@ DEFAULT_BUNDLES=( binary test-unit - test-integration test-integration-cli test-docker-py dynbinary - dyntest-unit - dyntest-integration + test-integration cover cross @@ -113,7 +111,8 @@ fi EXTLDFLAGS_STATIC='-static' # ORIG_BUILDFLAGS is necessary for the cross target which cannot always build # with options like -race. -ORIG_BUILDFLAGS=( -a -tags "netgo static_build $DOCKER_BUILDTAGS" ) +ORIG_BUILDFLAGS=( -a -tags "netgo static_build $DOCKER_BUILDTAGS" -installsuffix netgo ) +# see https://github.com/golang/go/issues/9369#issuecomment-69864440 for why -installsuffix is necessary here BUILDFLAGS=( $BUILDFLAGS "${ORIG_BUILDFLAGS[@]}" ) # Test timeout. : ${TIMEOUT:=30m} diff --git a/project/make/.dockerinit b/project/make/.dockerinit new file mode 100644 index 000000000..73df8fce0 --- /dev/null +++ b/project/make/.dockerinit @@ -0,0 +1,29 @@ +#!/bin/bash +set -e + +# dockerinit still needs to be a static binary, even if docker is dynamic +go build \ + -o "$DEST/dockerinit-$VERSION" \ + "${BUILDFLAGS[@]}" \ + -ldflags " + $LDFLAGS + $LDFLAGS_STATIC + -extldflags \"$EXTLDFLAGS_STATIC\" + " \ + ./dockerinit +echo "Created binary: $DEST/dockerinit-$VERSION" +ln -sf "dockerinit-$VERSION" "$DEST/dockerinit" + +sha1sum= +if command -v sha1sum &> /dev/null; then + sha1sum=sha1sum +elif command -v shasum &> /dev/null; then + # Mac OS X - why couldn't they just use the same command name and be happy? + sha1sum=shasum +else + echo >&2 'error: cannot find sha1sum command or equivalent' + exit 1 +fi + +# sha1 our new dockerinit to ensure separate docker and dockerinit always run in a perfect pair compiled for one another +export DOCKER_INITSHA1="$($sha1sum $DEST/dockerinit-$VERSION | cut -d' ' -f1)" diff --git a/project/make/dynbinary b/project/make/dynbinary index 5064a799b..39fcd3576 100644 --- a/project/make/dynbinary +++ b/project/make/dynbinary @@ -4,39 +4,14 @@ set -e DEST=$1 if [ -z "$DOCKER_CLIENTONLY" ]; then - # dockerinit still needs to be a static binary, even if docker is dynamic - go build \ - -o "$DEST/dockerinit-$VERSION" \ - "${BUILDFLAGS[@]}" \ - -ldflags " - $LDFLAGS - $LDFLAGS_STATIC - -extldflags \"$EXTLDFLAGS_STATIC\" - " \ - ./dockerinit - echo "Created binary: $DEST/dockerinit-$VERSION" - ln -sf "dockerinit-$VERSION" "$DEST/dockerinit" + source "$(dirname "$BASH_SOURCE")/.dockerinit" hash_files "$DEST/dockerinit-$VERSION" - - sha1sum= - if command -v sha1sum &> /dev/null; then - sha1sum=sha1sum - elif command -v shasum &> /dev/null; then - # Mac OS X - why couldn't they just use the same command name and be happy? - sha1sum=shasum - else - echo >&2 'error: cannot find sha1sum command or equivalent' - exit 1 - fi - - # sha1 our new dockerinit to ensure separate docker and dockerinit always run in a perfect pair compiled for one another - export DOCKER_INITSHA1="$($sha1sum $DEST/dockerinit-$VERSION | cut -d' ' -f1)" else # DOCKER_CLIENTONLY must be truthy, so we don't need to bother with dockerinit :) export DOCKER_INITSHA1="" fi -# exported so that "dyntest" can easily access it later without recalculating it +# DOCKER_INITSHA1 is exported so that other bundlescripts can easily access it later without recalculating it ( export LDFLAGS_STATIC_DOCKER="-X $DOCKER_PKG/dockerversion.INITSHA1 \"$DOCKER_INITSHA1\" -X $DOCKER_PKG/dockerversion.INITPATH \"$DOCKER_INITPATH\"" diff --git a/project/make/dyntest-integration b/project/make/dyntest-integration deleted file mode 100644 index 1cc7349ab..000000000 --- a/project/make/dyntest-integration +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash -set -e - -DEST=$1 -INIT=$DEST/../dynbinary/dockerinit-$VERSION - -if [ ! -x "$INIT" ]; then - echo >&2 'error: dynbinary must be run before dyntest-integration' - false -fi - -( - export TEST_DOCKERINIT_PATH="$INIT" - export LDFLAGS_STATIC_DOCKER=" - -X $DOCKER_PKG/dockerversion.INITSHA1 \"$DOCKER_INITSHA1\" - " - source "$(dirname "$BASH_SOURCE")/test-integration" -) diff --git a/project/make/dyntest-unit b/project/make/dyntest-unit deleted file mode 100644 index cffef9851..000000000 --- a/project/make/dyntest-unit +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash -set -e - -DEST=$1 -INIT=$DEST/../dynbinary/dockerinit-$VERSION - -if [ ! -x "$INIT" ]; then - echo >&2 'error: dynbinary must be run before dyntest-unit' - false -fi - -( - export TEST_DOCKERINIT_PATH="$INIT" - export LDFLAGS_STATIC_DOCKER=" - -X $DOCKER_PKG/dockerversion.INITSHA1 \"$DOCKER_INITSHA1\" - " - source "$(dirname "$BASH_SOURCE")/test-unit" -) diff --git a/project/make/test-integration b/project/make/test-integration index 9512cc4e3..5cb7102bc 100644 --- a/project/make/test-integration +++ b/project/make/test-integration @@ -3,8 +3,18 @@ set -e DEST=$1 +INIT=$DEST/../dynbinary/dockerinit-$VERSION +[ -x "$INIT" ] || { + source "$(dirname "$BASH_SOURCE")/.dockerinit" + INIT="$DEST/dockerinit" +} +export TEST_DOCKERINIT_PATH="$INIT" + bundle_test_integration() { - LDFLAGS="$LDFLAGS $LDFLAGS_STATIC_DOCKER" go_test_dir ./integration \ + LDFLAGS=" + $LDFLAGS + -X $DOCKER_PKG/dockerversion.INITSHA1 \"$DOCKER_INITSHA1\" + " go_test_dir ./integration \ "-coverpkg $(find_dirs '*.go' | sed 's,^\.,'$DOCKER_PKG',g' | paste -d, -s)" } diff --git a/project/make/test-unit b/project/make/test-unit index 59700e86f..9225b33a0 100644 --- a/project/make/test-unit +++ b/project/make/test-unit @@ -23,7 +23,7 @@ bundle_test_unit() { TESTDIRS=$(find_dirs '*_test.go') fi ( - export LDFLAGS="$LDFLAGS $LDFLAGS_STATIC_DOCKER" + export LDFLAGS export TESTFLAGS export HAVE_GO_TEST_COVER export DEST From 315260203c3a683fe7e93aa6c937128d38d52ae4 Mon Sep 17 00:00:00 2001 From: Shishir Mahajan Date: Fri, 16 Jan 2015 19:38:11 -0500 Subject: [PATCH 345/513] Implementation of docker load command: Untarr the incoming tar stream directly into /var/lib/docker/tmp without creating the tar ball Signed-off-by: Shishir Mahajan --- graph/load.go | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/graph/load.go b/graph/load.go index 4399da25d..8f1da3113 100644 --- a/graph/load.go +++ b/graph/load.go @@ -4,7 +4,6 @@ package graph import ( "encoding/json" - "io" "io/ioutil" "os" "path" @@ -27,23 +26,9 @@ func (s *TagStore) CmdLoad(job *engine.Job) engine.Status { defer os.RemoveAll(tmpImageDir) var ( - repoTarFile = path.Join(tmpImageDir, "repo.tar") - repoDir = path.Join(tmpImageDir, "repo") + repoDir = path.Join(tmpImageDir, "repo") ) - tarFile, err := os.Create(repoTarFile) - if err != nil { - return job.Error(err) - } - if _, err := io.Copy(tarFile, job.Stdin); err != nil { - return job.Error(err) - } - tarFile.Close() - - repoFile, err := os.Open(repoTarFile) - if err != nil { - return job.Error(err) - } if err := os.Mkdir(repoDir, os.ModeDir); err != nil { return job.Error(err) } @@ -57,7 +42,7 @@ func (s *TagStore) CmdLoad(job *engine.Job) engine.Status { excludes[i] = k i++ } - if err := chrootarchive.Untar(repoFile, repoDir, &archive.TarOptions{ExcludePatterns: excludes}); err != nil { + if err := chrootarchive.Untar(job.Stdin, repoDir, &archive.TarOptions{ExcludePatterns: excludes}); err != nil { return job.Error(err) } From 9c3380039e15f11fc07741edd4cbc0c7e5c490aa Mon Sep 17 00:00:00 2001 From: Vincent Batts Date: Mon, 19 Jan 2015 15:10:37 -0500 Subject: [PATCH 346/513] devicemapper: dm_udev_get_sync_support expose an api to call dm_udev_get_sync_support/dm_udev_set_sync_support Signed-off-by: Vincent Batts --- pkg/devicemapper/devmapper.go | 20 ++++++++++++++++++++ pkg/devicemapper/devmapper_wrapper.go | 10 ++++++++++ 2 files changed, 30 insertions(+) diff --git a/pkg/devicemapper/devmapper.go b/pkg/devicemapper/devmapper.go index c23a3624d..486ec15db 100644 --- a/pkg/devicemapper/devmapper.go +++ b/pkg/devicemapper/devmapper.go @@ -319,6 +319,26 @@ func GetLibraryVersion() (string, error) { return version, nil } +// UdevSyncSupported returns whether device-mapper is able to sync with udev +// +// This is essential otherwise race conditions can arise where both udev and +// device-mapper attempt to create and destroy devices. +func UdevSyncSupported() bool { + return DmUdevGetSyncSupport() != 0 +} + +// UdevSetSyncSupport allows setting whether the udev sync should be enabled. +// The return bool indicates the state of whether the sync is enabled. +func UdevSetSyncSupport(enable bool) bool { + if enable { + DmUdevSetSyncSupport(1) + } else { + DmUdevSetSyncSupport(0) + } + + return UdevSyncSupported() +} + // Useful helper for cleanup func RemoveDevice(name string) error { log.Debugf("[devmapper] RemoveDevice START") diff --git a/pkg/devicemapper/devmapper_wrapper.go b/pkg/devicemapper/devmapper_wrapper.go index 499405a10..aff844652 100644 --- a/pkg/devicemapper/devmapper_wrapper.go +++ b/pkg/devicemapper/devmapper_wrapper.go @@ -107,6 +107,8 @@ var ( DmTaskSetRo = dmTaskSetRoFct DmTaskSetSector = dmTaskSetSectorFct DmUdevWait = dmUdevWaitFct + DmUdevSetSyncSupport = dmUdevSetSyncSupportFct + DmUdevGetSyncSupport = dmUdevGetSyncSupportFct LogWithErrnoInit = logWithErrnoInitFct ) @@ -231,6 +233,14 @@ func dmGetNextTargetFct(task *CDmTask, next uintptr, start, length *uint64, targ return uintptr(nextp) } +func dmUdevSetSyncSupportFct(syncWithUdev int) { + (C.dm_udev_set_sync_support(C.int(syncWithUdev))) +} + +func dmUdevGetSyncSupportFct() int { + return int(C.dm_udev_get_sync_support()) +} + func dmUdevWaitFct(cookie uint) int { return int(C.dm_udev_wait(C.uint32_t(cookie))) } From b682d7cde619c61eace60811296c26f70e94c6b4 Mon Sep 17 00:00:00 2001 From: Ian Babrou Date: Mon, 19 Jan 2015 23:00:57 +0300 Subject: [PATCH 347/513] removed unused compression arg in Graph.TempLayerArchive() Signed-off-by: Ian Babrou --- graph/graph.go | 2 +- graph/push.go | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/graph/graph.go b/graph/graph.go index b6ea22bdc..30bea0470 100644 --- a/graph/graph.go +++ b/graph/graph.go @@ -196,7 +196,7 @@ func (graph *Graph) Register(img *image.Image, layerData archive.ArchiveReader) // The archive is stored on disk and will be automatically deleted as soon as has been read. // If output is not nil, a human-readable progress bar will be written to it. // FIXME: does this belong in Graph? How about MktempFile, let the caller use it for archives? -func (graph *Graph) TempLayerArchive(id string, compression archive.Compression, sf *utils.StreamFormatter, output io.Writer) (*archive.TempArchive, error) { +func (graph *Graph) TempLayerArchive(id string, sf *utils.StreamFormatter, output io.Writer) (*archive.TempArchive, error) { image, err := graph.Get(id) if err != nil { return nil, err diff --git a/graph/push.go b/graph/push.go index 46469daea..316eed91b 100644 --- a/graph/push.go +++ b/graph/push.go @@ -13,7 +13,6 @@ import ( log "github.com/Sirupsen/logrus" "github.com/docker/docker/engine" "github.com/docker/docker/image" - "github.com/docker/docker/pkg/archive" "github.com/docker/docker/registry" "github.com/docker/docker/utils" "github.com/docker/libtrust" @@ -228,7 +227,7 @@ func (s *TagStore) pushImage(r *registry.Session, out io.Writer, imgID, ep strin return "", err } - layerData, err := s.graph.TempLayerArchive(imgID, archive.Uncompressed, sf, out) + layerData, err := s.graph.TempLayerArchive(imgID, sf, out) if err != nil { return "", fmt.Errorf("Failed to generate layer archive: %s", err) } From 022e1232f84966c4b70a612bc35463ebb58e3137 Mon Sep 17 00:00:00 2001 From: Vincent Batts Date: Mon, 19 Jan 2015 15:11:40 -0500 Subject: [PATCH 348/513] devmapper: udev sync on init when initializing the devmapper driver, attempt to sync udev and device mapper. If udev sync is not supported, print a warning. Eventually we'll likely bail here to avoid unpredictable behavior for users. Signed-off-by: Vincent Batts --- daemon/graphdriver/devmapper/deviceset.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/daemon/graphdriver/devmapper/deviceset.go b/daemon/graphdriver/devmapper/deviceset.go index 1e0a6d3f8..e53686343 100644 --- a/daemon/graphdriver/devmapper/deviceset.go +++ b/daemon/graphdriver/devmapper/deviceset.go @@ -947,6 +947,12 @@ func (devices *DeviceSet) initDevmapper(doInit bool) error { return graphdriver.ErrNotSupported } + // https://github.com/docker/docker/issues/4036 + if supported := devicemapper.UdevSetSyncSupport(true); !supported { + log.Warnf("WARNING: Udev sync is not supported. This will lead to unexpected behavior, data loss and errors") + } + log.Debugf("devicemapper: udev sync support: %v", devicemapper.UdevSyncSupported()) + if err := os.MkdirAll(devices.metadataDir(), 0700); err != nil && !os.IsExist(err) { return err } From d2593546f9a234699cd0034cc6b97c748c10c93e Mon Sep 17 00:00:00 2001 From: Vincent Batts Date: Mon, 19 Jan 2015 16:28:02 -0500 Subject: [PATCH 349/513] devmapper: udev sync in `docker info` now: ``` [...] Storage Driver: devicemapper Pool Name: docker-253:2-5767172-pool [...] Udev Sync Supported: true [...] ``` Signed-off-by: Vincent Batts --- daemon/graphdriver/devmapper/deviceset.go | 18 ++++++++++-------- daemon/graphdriver/devmapper/driver.go | 1 + 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/daemon/graphdriver/devmapper/deviceset.go b/daemon/graphdriver/devmapper/deviceset.go index e53686343..de1f720d4 100644 --- a/daemon/graphdriver/devmapper/deviceset.go +++ b/daemon/graphdriver/devmapper/deviceset.go @@ -105,14 +105,15 @@ type DiskUsage struct { } type Status struct { - PoolName string - DataFile string // actual block device for data - DataLoopback string // loopback file, if used - MetadataFile string // actual block device for metadata - MetadataLoopback string // loopback file, if used - Data DiskUsage - Metadata DiskUsage - SectorSize uint64 + PoolName string + DataFile string // actual block device for data + DataLoopback string // loopback file, if used + MetadataFile string // actual block device for metadata + MetadataLoopback string // loopback file, if used + Data DiskUsage + Metadata DiskUsage + SectorSize uint64 + UdevSyncSupported bool } type DevStatus struct { @@ -1578,6 +1579,7 @@ func (devices *DeviceSet) Status() *Status { status.DataLoopback = devices.dataLoopFile status.MetadataFile = devices.MetadataDevicePath() status.MetadataLoopback = devices.metadataLoopFile + status.UdevSyncSupported = devicemapper.UdevSyncSupported() totalSizeInSectors, _, dataUsed, dataTotal, metadataUsed, metadataTotal, err := devices.poolStatus() if err == nil { diff --git a/daemon/graphdriver/devmapper/driver.go b/daemon/graphdriver/devmapper/driver.go index a7dafc657..2feed5720 100644 --- a/daemon/graphdriver/devmapper/driver.go +++ b/daemon/graphdriver/devmapper/driver.go @@ -74,6 +74,7 @@ func (d *Driver) Status() [][2]string { {"Data Space Total", fmt.Sprintf("%s", units.HumanSize(float64(s.Data.Total)))}, {"Metadata Space Used", fmt.Sprintf("%s", units.HumanSize(float64(s.Metadata.Used)))}, {"Metadata Space Total", fmt.Sprintf("%s", units.HumanSize(float64(s.Metadata.Total)))}, + {"Udev Sync Supported", fmt.Sprintf("%v", s.UdevSyncSupported)}, } if len(s.DataLoopback) > 0 { status = append(status, [2]string{"Data loop file", s.DataLoopback}) From dbb642b7fbdf42d3b401d610403ba58ebbde38d7 Mon Sep 17 00:00:00 2001 From: Vincent Batts Date: Mon, 19 Jan 2015 15:52:39 -0500 Subject: [PATCH 350/513] devicemapper: define the fallback flag DM_UDEV_DISABLE_LIBRARY_FALLBACK is disabled by most applications today when using device-mapper, and ensuring that device-mapper is in sync with udev. This flag instructs devicemapper to not fallback to creating the device nodes itself. In the case of udev sync not being supported, devicemapper will attempt to create the devices in a timely manner, regardless of udev. Signed-off-by: Vincent Batts --- pkg/devicemapper/devmapper_wrapper.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/devicemapper/devmapper_wrapper.go b/pkg/devicemapper/devmapper_wrapper.go index aff844652..63c55d98d 100644 --- a/pkg/devicemapper/devmapper_wrapper.go +++ b/pkg/devicemapper/devmapper_wrapper.go @@ -86,6 +86,7 @@ const ( DmUdevDisableSubsystemRulesFlag = C.DM_UDEV_DISABLE_SUBSYSTEM_RULES_FLAG DmUdevDisableDiskRulesFlag = C.DM_UDEV_DISABLE_DISK_RULES_FLAG DmUdevDisableOtherRulesFlag = C.DM_UDEV_DISABLE_OTHER_RULES_FLAG + DmUdevDisableLibraryFallback = C.DM_UDEV_DISABLE_LIBRARY_FALLBACK ) var ( From 4cfe9df0a9c206c368a90f460fea8fab197265d9 Mon Sep 17 00:00:00 2001 From: Vincent Batts Date: Mon, 19 Jan 2015 16:40:50 -0500 Subject: [PATCH 351/513] devicemapper: debug output specifics moar information for the information gods Signed-off-by: Vincent Batts --- daemon/graphdriver/devmapper/deviceset.go | 6 +++--- pkg/devicemapper/devmapper.go | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/daemon/graphdriver/devmapper/deviceset.go b/daemon/graphdriver/devmapper/deviceset.go index de1f720d4..aa48cc795 100644 --- a/daemon/graphdriver/devmapper/deviceset.go +++ b/daemon/graphdriver/devmapper/deviceset.go @@ -1099,7 +1099,7 @@ func (devices *DeviceSet) initDevmapper(doInit bool) error { func (devices *DeviceSet) AddDevice(hash, baseHash string) error { log.Debugf("[deviceset] AddDevice() hash=%s basehash=%s", hash, baseHash) - defer log.Debugf("[deviceset] AddDevice END") + defer log.Debugf("[deviceset] AddDevice(hash=%s basehash=%s) END", hash, baseHash) baseInfo, err := devices.lookupDevice(baseHash) if err != nil { @@ -1203,7 +1203,7 @@ func (devices *DeviceSet) deactivatePool() error { func (devices *DeviceSet) deactivateDevice(info *DevInfo) error { log.Debugf("[devmapper] deactivateDevice(%s)", info.Hash) - defer log.Debugf("[devmapper] deactivateDevice END") + defer log.Debugf("[devmapper] deactivateDevice END(%s)", info.Hash) // Wait for the unmount to be effective, // by watching the value of Info.OpenCount for the device @@ -1425,7 +1425,7 @@ func (devices *DeviceSet) MountDevice(hash, path, mountLabel string) error { func (devices *DeviceSet) UnmountDevice(hash string) error { log.Debugf("[devmapper] UnmountDevice(hash=%s)", hash) - defer log.Debugf("[devmapper] UnmountDevice END") + defer log.Debugf("[devmapper] UnmountDevice(hash=%s) END", hash) info, err := devices.lookupDevice(hash) if err != nil { diff --git a/pkg/devicemapper/devmapper.go b/pkg/devicemapper/devmapper.go index 486ec15db..c0caec510 100644 --- a/pkg/devicemapper/devmapper.go +++ b/pkg/devicemapper/devmapper.go @@ -341,8 +341,8 @@ func UdevSetSyncSupport(enable bool) bool { // Useful helper for cleanup func RemoveDevice(name string) error { - log.Debugf("[devmapper] RemoveDevice START") - defer log.Debugf("[devmapper] RemoveDevice END") + log.Debugf("[devmapper] RemoveDevice START(%s)", name) + defer log.Debugf("[devmapper] RemoveDevice END(%s)", name) task, err := TaskCreateNamed(DeviceRemove, name) if task == nil { return err From a09a665d99c84be74ffff68a39dde83ad3c0d34a Mon Sep 17 00:00:00 2001 From: Vincent Batts Date: Mon, 19 Jan 2015 17:37:08 -0500 Subject: [PATCH 352/513] devmapper: some explination of `docker info` Signed-off-by: Vincent Batts --- daemon/graphdriver/devmapper/README.md | 39 ++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/daemon/graphdriver/devmapper/README.md b/daemon/graphdriver/devmapper/README.md index 3b69cef84..e589bac53 100644 --- a/daemon/graphdriver/devmapper/README.md +++ b/daemon/graphdriver/devmapper/README.md @@ -28,6 +28,45 @@ containers. All base images are snapshots of this device and those images are then in turn used as snapshots for other images and eventually containers. +### Information on `docker info` + +As of docker-1.4.1, `docker info` when using the `devicemapper` storage driver +will display something like: + + $ sudo docker info + [...] + Storage Driver: devicemapper + Pool Name: docker-253:1-17538953-pool + Pool Blocksize: 65.54 kB + Data file: /dev/loop4 + Metadata file: /dev/loop4 + Data Space Used: 2.536 GB + Data Space Total: 107.4 GB + Metadata Space Used: 7.93 MB + Metadata Space Total: 2.147 GB + Udev Sync Supported: true + Data loop file: /home/docker/devicemapper/devicemapper/data + Metadata loop file: /home/docker/devicemapper/devicemapper/metadata + Library Version: 1.02.82-git (2013-10-04) + [...] + +#### status items + +Each item in the indented section under `Storage Driver: devicemapper` are +status information about the driver. + * `Pool Name` name of the devicemapper pool for this driver. + * `Pool Blocksize` tells the blocksize the thin pool was initialized with. This only changes on creation. + * `Data file` blockdevice file used for the devicemapper data + * `Metadata file` blockdevice file used for the devicemapper metadata + * `Data Space Used` tells how much of `Data file` is currently used + * `Data Space Total` tells max size the `Data file` + * `Metadata Space Used` tells how much of `Metadata file` is currently used + * `Metadata Space Total` tells max size the `Metadata file` + * `Udev Sync Supported` tells whether devicemapper is able to sync with Udev. Should be `true`. + * `Data loop file` file attached to `Data file`, if loopback device is used + * `Metadata loop file` file attached to `Metadata file`, if loopback device is used + * `Library Version` from the libdevmapper used + ### options The devicemapper backend supports some options that you can specify From 7bb4b56cf98662ef5f1deb64bdf8a120490bc258 Mon Sep 17 00:00:00 2001 From: Jessica Frazelle Date: Mon, 19 Jan 2015 16:16:36 -0800 Subject: [PATCH 353/513] Add docs for `--memory-swap`. Docker-DCO-1.1-Signed-off-by: Jessica Frazelle (github: jfrazelle) --- docs/man/docker-run.1.md | 6 ++++++ docs/sources/reference/commandline/cli.md | 1 + 2 files changed, 7 insertions(+) diff --git a/docs/man/docker-run.1.md b/docs/man/docker-run.1.md index b16447bc5..a5e4fa2f2 100644 --- a/docs/man/docker-run.1.md +++ b/docs/man/docker-run.1.md @@ -28,6 +28,7 @@ docker-run - Run a command in a new container [**--link**[=*[]*]] [**--lxc-conf**[=*[]*]] [**-m**|**--memory**[=*MEMORY*]] +[**--memory-swap**[=*MEMORY-SWAP]] [**--mac-address**[=*MAC-ADDRESS*]] [**--name**[=*NAME*]] [**--net**[=*"bridge"*]] @@ -191,6 +192,11 @@ actual limit may be rounded up to a multiple of the operating system's page size, if it is not already. The memory limit should be formatted as follows: ``, where unit = b, k, m or g. +**--memory-swap**="" + Total memory usage (memory + swap) + + Set '-1' to disable swap (format: , where unit = b, k, m or g) + **--mac-address**="" Container MAC address (e.g. 92:d0:c6:0a:29:33) diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index cb17a67eb..d34c1f35a 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -1605,6 +1605,7 @@ removed before the image is removed. --link=[] Add link to another container in the form of name:alias --lxc-conf=[] (lxc exec-driver only) Add custom lxc options --lxc-conf="lxc.cgroup.cpuset.cpus = 0,1" -m, --memory="" Memory limit (format: , where unit = b, k, m or g) + -memory-swap="" Total memory usage (memory + swap), set '-1' to disable swap (format: , where unit = b, k, m or g) --mac-address="" Container MAC address (e.g. 92:d0:c6:0a:29:33) --name="" Assign a name to the container --net="bridge" Set the Network mode for the container From 93d51e5e971e001d80e9ffa863439f2d72215b5a Mon Sep 17 00:00:00 2001 From: Phil Estes Date: Fri, 9 Jan 2015 12:06:48 -0500 Subject: [PATCH 354/513] Clean up localhost resolv logic and add IPv6 support to regexp Addresses #5811 This cleans up an error in the logic which removes localhost resolvers from the host resolv.conf at container creation start time. Specifically when the determination is made if any nameservers are left after removing localhost resolvers, it was using a string match on the word "nameserver", which could have been anywhere (including commented out) leading to incorrect situations where no nameservers were left but the default ones were not added. This also adds some complexity to the regular expressions for finding nameservers in general, as well as matching on localhost resolvers due to the recent addition of IPv6 support. Because of IPv6 support now available in the Docker daemon, the resolvconf code is now aware of IPv6 enable/disable state and uses that for both filter/cleaning of nameservers as well as adding default Google DNS (IPv4 only vs. IPv4 and IPv6 if IPv6 enabled). For all these changes, tests have been added/strengthened to test these additional capabilities. Docker-DCO-1.1-Signed-off-by: Phil Estes (github: estesp) --- daemon/container.go | 8 +-- daemon/daemon.go | 2 +- integration-cli/docker_cli_run_test.go | 34 ++++++------ pkg/networkfs/resolvconf/resolvconf.go | 50 ++++++++++++----- pkg/networkfs/resolvconf/resolvconf_test.go | 59 +++++++++++++++++++-- 5 files changed, 115 insertions(+), 38 deletions(-) diff --git a/daemon/container.go b/daemon/container.go index 85e16e401..9abcb72ed 100644 --- a/daemon/container.go +++ b/daemon/container.go @@ -958,8 +958,8 @@ func (container *Container) setupContainerDns() error { log.Debugf("Check container (%s) for update to resolv.conf - UpdateDns flag was set", container.ID) latestResolvConf, latestHash := resolvconf.GetLastModified() - // because the new host resolv.conf might have localhost nameservers.. - updatedResolvConf, modified := resolvconf.RemoveReplaceLocalDns(latestResolvConf) + // clean container resolv.conf re: localhost nameservers and IPv6 NS (if IPv6 disabled) + updatedResolvConf, modified := resolvconf.FilterResolvDns(latestResolvConf, container.daemon.config.EnableIPv6) if modified { // changes have occurred during resolv.conf localhost cleanup: generate an updated hash newHash, err := utils.HashData(bytes.NewReader(updatedResolvConf)) @@ -1012,8 +1012,8 @@ func (container *Container) setupContainerDns() error { return resolvconf.Build(container.ResolvConfPath, dns, dnsSearch) } - // replace any localhost/127.* nameservers - resolvConf, _ = resolvconf.RemoveReplaceLocalDns(resolvConf) + // replace any localhost/127.*, and remove IPv6 nameservers if IPv6 disabled in daemon + resolvConf, _ = resolvconf.FilterResolvDns(resolvConf, daemon.config.EnableIPv6) } //get a sha256 hash of the resolv conf at this point so we can check //for changes when the host resolv.conf changes (e.g. network update) diff --git a/daemon/daemon.go b/daemon/daemon.go index 5972b4f87..c49fa4a9f 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -434,7 +434,7 @@ func (daemon *Daemon) setupResolvconfWatcher() error { log.Debugf("Error retrieving updated host resolv.conf: %v", err) } else if updatedResolvConf != nil { // because the new host resolv.conf might have localhost nameservers.. - updatedResolvConf, modified := resolvconf.RemoveReplaceLocalDns(updatedResolvConf) + updatedResolvConf, modified := resolvconf.FilterResolvDns(updatedResolvConf, daemon.config.EnableIPv6) if modified { // changes have occurred during localhost cleanup: generate an updated hash newHash, err := utils.HashData(bytes.NewReader(updatedResolvConf)) diff --git a/integration-cli/docker_cli_run_test.go b/integration-cli/docker_cli_run_test.go index 18495be41..34ef8296b 100644 --- a/integration-cli/docker_cli_run_test.go +++ b/integration-cli/docker_cli_run_test.go @@ -1238,40 +1238,42 @@ func TestRunDisallowBindMountingRootToRoot(t *testing.T) { logDone("run - bind mount /:/ as volume should fail") } +// Verify that a container gets default DNS when only localhost resolvers exist func TestRunDnsDefaultOptions(t *testing.T) { - // ci server has default resolv.conf - // so rewrite it for the test + + // preserve original resolv.conf for restoring after test origResolvConf, err := ioutil.ReadFile("/etc/resolv.conf") if os.IsNotExist(err) { t.Fatalf("/etc/resolv.conf does not exist") } - - // test with file - tmpResolvConf := []byte("nameserver 127.0.0.1") - if err := ioutil.WriteFile("/etc/resolv.conf", tmpResolvConf, 0644); err != nil { - t.Fatal(err) - } - // put the old resolvconf back + // defer restored original conf defer func() { if err := ioutil.WriteFile("/etc/resolv.conf", origResolvConf, 0644); err != nil { t.Fatal(err) } }() + // test 3 cases: standard IPv4 localhost, commented out localhost, and IPv6 localhost + // 2 are removed from the file at container start, and the 3rd (commented out) one is ignored by + // GetNameservers(), leading to a replacement of nameservers with the default set + tmpResolvConf := []byte("nameserver 127.0.0.1\n#nameserver 127.0.2.1\nnameserver ::1") + if err := ioutil.WriteFile("/etc/resolv.conf", tmpResolvConf, 0644); err != nil { + t.Fatal(err) + } + cmd := exec.Command(dockerBinary, "run", "busybox", "cat", "/etc/resolv.conf") actual, _, err := runCommandWithOutput(cmd) if err != nil { - t.Error(err, actual) - return + t.Fatal(err, actual) } - // check that the actual defaults are there - // if we ever change the defaults from google dns, this will break - expected := "\nnameserver 8.8.8.8\nnameserver 8.8.4.4" + // check that the actual defaults are appended to the commented out + // localhost resolver (which should be preserved) + // NOTE: if we ever change the defaults from google dns, this will break + expected := "#nameserver 127.0.2.1\n\nnameserver 8.8.8.8\nnameserver 8.8.4.4" if actual != expected { - t.Errorf("expected resolv.conf be: %q, but was: %q", expected, actual) - return + t.Fatalf("expected resolv.conf be: %q, but was: %q", expected, actual) } deleteAllContainers() diff --git a/pkg/networkfs/resolvconf/resolvconf.go b/pkg/networkfs/resolvconf/resolvconf.go index a43daa527..d88074f59 100644 --- a/pkg/networkfs/resolvconf/resolvconf.go +++ b/pkg/networkfs/resolvconf/resolvconf.go @@ -12,9 +12,21 @@ import ( ) var ( - defaultDns = []string{"8.8.8.8", "8.8.4.4"} - localHostRegexp = regexp.MustCompile(`(?m)^nameserver 127[^\n]+\n*`) - nsRegexp = regexp.MustCompile(`^\s*nameserver\s*(([0-9]+\.){3}([0-9]+))\s*$`) + // Note: the default IPv4 & IPv6 resolvers are set to Google's Public DNS + defaultIPv4Dns = []string{"nameserver 8.8.8.8", "nameserver 8.8.4.4"} + defaultIPv6Dns = []string{"nameserver 2001:4860:4860::8888", "nameserver 2001:4860:4860::8844"} + ipv4NumBlock = `(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)` + ipv4Address = `(` + ipv4NumBlock + `\.){3}` + ipv4NumBlock + // This is not an IPv6 address verifier as it will accept a super-set of IPv6, and also + // will *not match* IPv4-Embedded IPv6 Addresses (RFC6052), but that and other variants + // -- e.g. other link-local types -- either won't work in containers or are unnecessary. + // For readability and sufficiency for Docker purposes this seemed more reasonable than a + // 1000+ character regexp with exact and complete IPv6 validation + ipv6Address = `([0-9A-Fa-f]{0,4}:){2,7}([0-9A-Fa-f]{0,4})` + + localhostRegexp = regexp.MustCompile(`(?m)^nameserver\s+((127\.([0-9]{1,3}.){2}[0-9]{1,3})|(::1))\s*\n*`) + nsIPv6Regexp = regexp.MustCompile(`(?m)^nameserver\s+` + ipv6Address + `\s*\n*`) + nsRegexp = regexp.MustCompile(`^\s*nameserver\s*((` + ipv4Address + `)|(` + ipv6Address + `))\s*$`) searchRegexp = regexp.MustCompile(`^\s*search\s*(([^\s]+\s*)*)$`) ) @@ -65,17 +77,31 @@ func GetLastModified() ([]byte, string) { return lastModified.contents, lastModified.sha256 } -// RemoveReplaceLocalDns looks for localhost (127.*) entries in the provided -// resolv.conf, removing local nameserver entries, and, if the resulting -// cleaned config has no defined nameservers left, adds default DNS entries +// FilterResolvDns has two main jobs: +// 1. It looks for localhost (127.*|::1) entries in the provided +// resolv.conf, removing local nameserver entries, and, if the resulting +// cleaned config has no defined nameservers left, adds default DNS entries +// 2. Given the caller provides the enable/disable state of IPv6, the filter +// code will remove all IPv6 nameservers if it is not enabled for containers +// // It also returns a boolean to notify the caller if changes were made at all -func RemoveReplaceLocalDns(resolvConf []byte) ([]byte, bool) { +func FilterResolvDns(resolvConf []byte, ipv6Enabled bool) ([]byte, bool) { changed := false - cleanedResolvConf := localHostRegexp.ReplaceAll(resolvConf, []byte{}) - // if the resulting resolvConf is empty, use defaultDns - if !bytes.Contains(cleanedResolvConf, []byte("nameserver")) { - log.Infof("No non-localhost DNS nameservers are left in resolv.conf. Using default external servers : %v", defaultDns) - cleanedResolvConf = append(cleanedResolvConf, []byte("\nnameserver "+strings.Join(defaultDns, "\nnameserver "))...) + cleanedResolvConf := localhostRegexp.ReplaceAll(resolvConf, []byte{}) + // if IPv6 is not enabled, also clean out any IPv6 address nameserver + if !ipv6Enabled { + cleanedResolvConf = nsIPv6Regexp.ReplaceAll(cleanedResolvConf, []byte{}) + } + // if the resulting resolvConf has no more nameservers defined, add appropriate + // default DNS servers for IPv4 and (optionally) IPv6 + if len(GetNameservers(cleanedResolvConf)) == 0 { + log.Infof("No non-localhost DNS nameservers are left in resolv.conf. Using default external servers : %v", defaultIPv4Dns) + dns := defaultIPv4Dns + if ipv6Enabled { + log.Infof("IPv6 enabled; Adding default IPv6 external servers : %v", defaultIPv6Dns) + dns = append(dns, defaultIPv6Dns...) + } + cleanedResolvConf = append(cleanedResolvConf, []byte("\n"+strings.Join(dns, "\n"))...) } if !bytes.Equal(resolvConf, cleanedResolvConf) { changed = true diff --git a/pkg/networkfs/resolvconf/resolvconf_test.go b/pkg/networkfs/resolvconf/resolvconf_test.go index 2432ea53c..b0647e783 100644 --- a/pkg/networkfs/resolvconf/resolvconf_test.go +++ b/pkg/networkfs/resolvconf/resolvconf_test.go @@ -157,33 +157,82 @@ func TestBuildWithZeroLengthDomainSearch(t *testing.T) { } } -func TestRemoveReplaceLocalDns(t *testing.T) { +func TestFilterResolvDns(t *testing.T) { ns0 := "nameserver 10.16.60.14\nnameserver 10.16.60.21\n" - if result, _ := RemoveReplaceLocalDns([]byte(ns0)); result != nil { + if result, _ := FilterResolvDns([]byte(ns0), false); result != nil { if ns0 != string(result) { t.Fatalf("Failed No Localhost: expected \n<%s> got \n<%s>", ns0, string(result)) } } ns1 := "nameserver 10.16.60.14\nnameserver 10.16.60.21\nnameserver 127.0.0.1\n" - if result, _ := RemoveReplaceLocalDns([]byte(ns1)); result != nil { + if result, _ := FilterResolvDns([]byte(ns1), false); result != nil { if ns0 != string(result) { t.Fatalf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result)) } } ns1 = "nameserver 10.16.60.14\nnameserver 127.0.0.1\nnameserver 10.16.60.21\n" - if result, _ := RemoveReplaceLocalDns([]byte(ns1)); result != nil { + if result, _ := FilterResolvDns([]byte(ns1), false); result != nil { if ns0 != string(result) { t.Fatalf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result)) } } ns1 = "nameserver 127.0.1.1\nnameserver 10.16.60.14\nnameserver 10.16.60.21\n" - if result, _ := RemoveReplaceLocalDns([]byte(ns1)); result != nil { + if result, _ := FilterResolvDns([]byte(ns1), false); result != nil { if ns0 != string(result) { t.Fatalf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result)) } } + + ns1 = "nameserver ::1\nnameserver 10.16.60.14\nnameserver 127.0.2.1\nnameserver 10.16.60.21\n" + if result, _ := FilterResolvDns([]byte(ns1), false); result != nil { + if ns0 != string(result) { + t.Fatalf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result)) + } + } + + ns1 = "nameserver 10.16.60.14\nnameserver ::1\nnameserver 10.16.60.21\nnameserver ::1" + if result, _ := FilterResolvDns([]byte(ns1), false); result != nil { + if ns0 != string(result) { + t.Fatalf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result)) + } + } + + // with IPv6 disabled (false param), the IPv6 nameserver should be removed + ns1 = "nameserver 10.16.60.14\nnameserver 2002:dead:beef::1\nnameserver 10.16.60.21\nnameserver ::1" + if result, _ := FilterResolvDns([]byte(ns1), false); result != nil { + if ns0 != string(result) { + t.Fatalf("Failed Localhost+IPv6 off: expected \n<%s> got \n<%s>", ns0, string(result)) + } + } + + // with IPv6 enabled, the IPv6 nameserver should be preserved + ns0 = "nameserver 10.16.60.14\nnameserver 2002:dead:beef::1\nnameserver 10.16.60.21\n" + ns1 = "nameserver 10.16.60.14\nnameserver 2002:dead:beef::1\nnameserver 10.16.60.21\nnameserver ::1" + if result, _ := FilterResolvDns([]byte(ns1), true); result != nil { + if ns0 != string(result) { + t.Fatalf("Failed Localhost+IPv6 on: expected \n<%s> got \n<%s>", ns0, string(result)) + } + } + + // with IPv6 enabled, and no non-localhost servers, Google defaults (both IPv4+IPv6) should be added + ns0 = "\nnameserver 8.8.8.8\nnameserver 8.8.4.4\nnameserver 2001:4860:4860::8888\nnameserver 2001:4860:4860::8844" + ns1 = "nameserver 127.0.0.1\nnameserver ::1\nnameserver 127.0.2.1" + if result, _ := FilterResolvDns([]byte(ns1), true); result != nil { + if ns0 != string(result) { + t.Fatalf("Failed no Localhost+IPv6 enabled: expected \n<%s> got \n<%s>", ns0, string(result)) + } + } + + // with IPv6 disabled, and no non-localhost servers, Google defaults (only IPv4) should be added + ns0 = "\nnameserver 8.8.8.8\nnameserver 8.8.4.4" + ns1 = "nameserver 127.0.0.1\nnameserver ::1\nnameserver 127.0.2.1" + if result, _ := FilterResolvDns([]byte(ns1), false); result != nil { + if ns0 != string(result) { + t.Fatalf("Failed no Localhost+IPv6 enabled: expected \n<%s> got \n<%s>", ns0, string(result)) + } + } } From a687e8d86ba07ebb42d988b6e13102569b60e673 Mon Sep 17 00:00:00 2001 From: Vincent Batts Date: Tue, 20 Jan 2015 12:00:04 -0500 Subject: [PATCH 355/513] devicemapper: define device-mapper log levels Signed-off-by: Vincent Batts --- pkg/devicemapper/log.go | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 pkg/devicemapper/log.go diff --git a/pkg/devicemapper/log.go b/pkg/devicemapper/log.go new file mode 100644 index 000000000..cee5e5454 --- /dev/null +++ b/pkg/devicemapper/log.go @@ -0,0 +1,11 @@ +package devicemapper + +// definitions from lvm2 lib/log/log.h +const ( + LogLevelFatal = 2 + iota // _LOG_FATAL + LogLevelErr // _LOG_ERR + LogLevelWarn // _LOG_WARN + LogLevelNotice // _LOG_NOTICE + LogLevelInfo // _LOG_INFO + LogLevelDebug // _LOG_DEBUG +) From 0c8be9da3763d86c2a6e847e1667410056d6071d Mon Sep 17 00:00:00 2001 From: Vincent Batts Date: Mon, 19 Jan 2015 14:43:19 -0500 Subject: [PATCH 356/513] devmapper: initialize log levels Signed-off-by: Vincent Batts --- daemon/graphdriver/devmapper/deviceset.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/daemon/graphdriver/devmapper/deviceset.go b/daemon/graphdriver/devmapper/deviceset.go index de1f720d4..114ee6e6e 100644 --- a/daemon/graphdriver/devmapper/deviceset.go +++ b/daemon/graphdriver/devmapper/deviceset.go @@ -717,8 +717,10 @@ func setCloseOnExec(name string) { } func (devices *DeviceSet) DMLog(level int, file string, line int, dmError int, message string) { - if level >= 7 { - return // Ignore _LOG_DEBUG + if level >= devicemapper.LogLevelDebug { + // (vbatts) libdm debug is very verbose. If you're debugging libdm, you can + // comment out this check yourself + level = devicemapper.LogLevelInfo } // FIXME(vbatts) push this back into ./pkg/devicemapper/ @@ -939,6 +941,11 @@ func (devices *DeviceSet) closeTransaction() error { } func (devices *DeviceSet) initDevmapper(doInit bool) error { + if os.Getenv("DEBUG") != "" { + devicemapper.LogInitVerbose(devicemapper.LogLevelDebug) + } else { + devicemapper.LogInitVerbose(devicemapper.LogLevelWarn) + } // give ourselves to libdm as a log handler devicemapper.LogInit(devices) From 9462dbb242449b126ff473d081cec04afaa936d2 Mon Sep 17 00:00:00 2001 From: "Daniel, Dao Quang Minh" Date: Thu, 15 Jan 2015 13:37:30 -0500 Subject: [PATCH 357/513] test that execin cgroups match container cgroups Signed-off-by: Daniel, Dao Quang Minh --- integration-cli/docker_cli_exec_test.go | 61 +++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/integration-cli/docker_cli_exec_test.go b/integration-cli/docker_cli_exec_test.go index 2ff39fc83..5dc0e8d71 100644 --- a/integration-cli/docker_cli_exec_test.go +++ b/integration-cli/docker_cli_exec_test.go @@ -2,9 +2,13 @@ package main import ( "bufio" + "fmt" "os" "os/exec" + "reflect" + "sort" "strings" + "sync" "testing" "time" ) @@ -392,3 +396,60 @@ func TestExecStopNotHanging(t *testing.T) { } logDone("exec - container with exec not hanging on stop") } + +func TestExecCgroup(t *testing.T) { + defer deleteAllContainers() + var cmd *exec.Cmd + + cmd = exec.Command(dockerBinary, "run", "-d", "--name", "testing", "busybox", "top") + _, err := runCommand(cmd) + if err != nil { + t.Fatal(err) + } + + cmd = exec.Command(dockerBinary, "exec", "testing", "cat", "/proc/1/cgroup") + out, _, err := runCommandWithOutput(cmd) + if err != nil { + t.Fatal(out, err) + } + containerCgroups := sort.StringSlice(strings.Split(string(out), "\n")) + + var wg sync.WaitGroup + var s sync.Mutex + execCgroups := []sort.StringSlice{} + // exec a few times concurrently to get consistent failure + for i := 0; i < 5; i++ { + wg.Add(1) + go func() { + cmd = exec.Command(dockerBinary, "exec", "testing", "cat", "/proc/self/cgroup") + out, _, err := runCommandWithOutput(cmd) + if err != nil { + t.Fatal(out, err) + } + cg := sort.StringSlice(strings.Split(string(out), "\n")) + + s.Lock() + execCgroups = append(execCgroups, cg) + s.Unlock() + wg.Done() + }() + } + wg.Wait() + + for _, cg := range execCgroups { + if !reflect.DeepEqual(cg, containerCgroups) { + fmt.Println("exec cgroups:") + for _, name := range cg { + fmt.Printf(" %s\n", name) + } + + fmt.Println("container cgroups:") + for _, name := range containerCgroups { + fmt.Printf(" %s\n", name) + } + t.Fatal("cgroups mismatched") + } + } + + logDone("exec - exec has the container cgroups") +} From 142369456d2469a06e3860bdfe13169a908a3707 Mon Sep 17 00:00:00 2001 From: Tianon Gravi Date: Tue, 20 Jan 2015 12:11:59 -0700 Subject: [PATCH 358/513] Update TestBuildWithTabs to allow for the "\t"-equivalent "\u0009" (for Go 1.3 support) This is literally the only failing test on Go 1.3.3: :tada: ``` --- FAIL: TestBuildWithTabs (0.43 seconds) docker_cli_build_test.go:4307: Missing tabs. Got:["/bin/sh","-c","echo\u0009one\u0009\u0009two"] Exp:["/bin/sh","-c","echo\tone\t\ttwo"] ``` Signed-off-by: Andrew "Tianon" Page --- integration-cli/docker_cli_build_test.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/integration-cli/docker_cli_build_test.go b/integration-cli/docker_cli_build_test.go index 4330115fc..479b9692d 100644 --- a/integration-cli/docker_cli_build_test.go +++ b/integration-cli/docker_cli_build_test.go @@ -4302,9 +4302,10 @@ func TestBuildWithTabs(t *testing.T) { if err != nil { t.Fatal(err) } - expected := `["/bin/sh","-c","echo\tone\t\ttwo"]` - if res != expected { - t.Fatalf("Missing tabs.\nGot:%s\nExp:%s", res, expected) + expected1 := `["/bin/sh","-c","echo\tone\t\ttwo"]` + expected2 := `["/bin/sh","-c","echo\u0009one\u0009\u0009two"]` // syntactically equivalent, and what Go 1.3 generates + if res != expected1 && res != expected2 { + t.Fatalf("Missing tabs.\nGot: %s\nExp: %s or %s", res, expected1, expected2) } logDone("build - with tabs") } From eb76cb2301fc883941bc4ca2d9ebc3a486ab8e0a Mon Sep 17 00:00:00 2001 From: Vincent Batts Date: Tue, 20 Jan 2015 14:22:04 -0500 Subject: [PATCH 359/513] contrib/systemd: mount namespace and subtree flags This systemd.exec setting will construct a new mount namespace for the docker daemon, and use slave shared-subtree mounts so that volume mounts propogate correctly into containers. By having an unshared mount namespace for the daemon it ensures that mount references are not held by other pids outside of the docker daemon. Frequently this can be seen in EBUSY or "device or resource busy" errors. Signed-off-by: Vincent Batts --- contrib/init/systemd/docker.service | 1 + 1 file changed, 1 insertion(+) diff --git a/contrib/init/systemd/docker.service b/contrib/init/systemd/docker.service index 83c810d13..9738ca1ad 100644 --- a/contrib/init/systemd/docker.service +++ b/contrib/init/systemd/docker.service @@ -6,6 +6,7 @@ Requires=docker.socket [Service] ExecStart=/usr/bin/docker -d -H fd:// +MountFlags=slave LimitNOFILE=1048576 LimitNPROC=1048576 From e744b0dcbacd5e226fd79aba5a2e83f432d2d13f Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Fri, 16 Jan 2015 14:48:25 -0500 Subject: [PATCH 360/513] Fix volume ref restore process Fixes #9629 #9768 A couple of issues: 1) Volume config is not restored if we couldn't find it with the graph driver, but bind-mounts would never be found by the graph driver since they aren't in that dir 2) container volumes were only being restored if they were found in the volumes repo, but volumes created by old daemons wouldn't be in the repo until the container is at least started. Signed-off-by: Brian Goff --- daemon/daemon.go | 9 +++---- daemon/volumes.go | 19 +++++++++++-- integration-cli/docker_cli_daemon_test.go | 33 +++++++++++++++++++++++ volumes/repository.go | 6 ----- 4 files changed, 54 insertions(+), 13 deletions(-) diff --git a/daemon/daemon.go b/daemon/daemon.go index ef25960c5..1b2d13bb6 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -7,6 +7,7 @@ import ( "io/ioutil" "os" "path" + "path/filepath" "regexp" "runtime" "strings" @@ -237,6 +238,8 @@ func (daemon *Daemon) register(container *Container, updateSuffixarray bool) err // we'll waste time if we update it for every container daemon.idIndex.Add(container.ID) + container.registerVolumes() + // FIXME: if the container is supposed to be running but is not, auto restart it? // if so, then we need to restart monitor and init a new lock // If the container is supposed to be running, make sure of it @@ -400,10 +403,6 @@ func (daemon *Daemon) restore() error { } } - for _, c := range registeredContainers { - c.registerVolumes() - } - if !debug { fmt.Println() log.Infof("Loading containers: done.") @@ -890,7 +889,7 @@ func NewDaemonFromDirectory(config *Config, eng *engine.Engine) (*Daemon, error) return nil, err } - volumes, err := volumes.NewRepository(path.Join(config.Root, "volumes"), volumesDriver) + volumes, err := volumes.NewRepository(filepath.Join(config.Root, "volumes"), volumesDriver) if err != nil { return nil, err } diff --git a/daemon/volumes.go b/daemon/volumes.go index fa38b253f..7b4973383 100644 --- a/daemon/volumes.go +++ b/daemon/volumes.go @@ -119,8 +119,23 @@ func (container *Container) VolumePaths() map[string]struct{} { } func (container *Container) registerVolumes() { - for _, mnt := range container.VolumeMounts() { - mnt.volume.AddContainer(container.ID) + for path := range container.VolumePaths() { + if v := container.daemon.volumes.Get(path); v != nil { + v.AddContainer(container.ID) + continue + } + + // if container was created with an old daemon, this volume may not be registered so we need to make sure it gets registered + writable := true + if rw, exists := container.VolumesRW[path]; exists { + writable = rw + } + v, err := container.daemon.volumes.FindOrCreateVolume(path, writable) + if err != nil { + log.Debugf("error registering volume %s: %v", path, err) + continue + } + v.AddContainer(container.ID) } } diff --git a/integration-cli/docker_cli_daemon_test.go b/integration-cli/docker_cli_daemon_test.go index dbc4d232b..b7db552b6 100644 --- a/integration-cli/docker_cli_daemon_test.go +++ b/integration-cli/docker_cli_daemon_test.go @@ -317,3 +317,36 @@ func TestDaemonAllocatesListeningPort(t *testing.T) { logDone("daemon - daemon listening port is allocated") } + +// #9629 +func TestDaemonVolumesBindsRefs(t *testing.T) { + d := NewDaemon(t) + + if err := d.StartWithBusybox(); err != nil { + t.Fatal(err) + } + + tmp, err := ioutil.TempDir(os.TempDir(), "") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmp) + + if err := ioutil.WriteFile(tmp+"/test", []byte("testing"), 0655); err != nil { + t.Fatal(err) + } + + if out, err := d.Cmd("create", "-v", tmp+":/foo", "--name=voltest", "busybox"); err != nil { + t.Fatal(err, out) + } + + if err := d.Restart(); err != nil { + t.Fatal(err) + } + + if out, err := d.Cmd("run", "--volumes-from=voltest", "--name=consumer", "busybox", "/bin/sh", "-c", "[ -f /foo/test ]"); err != nil { + t.Fatal(err, out) + } + + logDone("daemon - bind refs in data-containers survive daemon restart") +} diff --git a/volumes/repository.go b/volumes/repository.go index 225148b60..821995224 100644 --- a/volumes/repository.go +++ b/volumes/repository.go @@ -87,16 +87,10 @@ func (r *Repository) restore() error { for _, v := range dir { id := v.Name() - path, err := r.driver.Get(id, "") - if err != nil { - log.Debugf("Could not find volume for %s: %v", id, err) - continue - } vol := &Volume{ ID: id, configPath: r.configPath + "/" + id, containers: make(map[string]struct{}), - Path: path, } if err := vol.FromDisk(); err != nil { if !os.IsNotExist(err) { From 6bb65864589fbd720622cbd795763d108999a366 Mon Sep 17 00:00:00 2001 From: Vincent Batts Date: Tue, 20 Jan 2015 15:17:41 -0500 Subject: [PATCH 361/513] contrib/sysvinit-redhat: unshare mount namespace unshare the mount namespace of the docker daemon to avoid other pids outside the daemon holding mount references of docker containers. Signed-off-by: Vincent Batts --- contrib/init/sysvinit-redhat/docker | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contrib/init/sysvinit-redhat/docker b/contrib/init/sysvinit-redhat/docker index eadf02c75..1994d6b31 100755 --- a/contrib/init/sysvinit-redhat/docker +++ b/contrib/init/sysvinit-redhat/docker @@ -23,6 +23,7 @@ . /etc/rc.d/init.d/functions prog="docker" +unshare=/usr/bin/unshare exec="/usr/bin/$prog" pidfile="/var/run/$prog.pid" lockfile="/var/lock/subsys/$prog" @@ -46,7 +47,7 @@ start() { prestart printf "Starting $prog:\t" echo "\n$(date)\n" >> $logfile - $exec -d $other_args &>> $logfile & + "$unshare" -m -- $exec -d $other_args &>> $logfile & pid=$! touch $lockfile # wait up to 10 seconds for the pidfile to exist. see From 5e8285b08120f674b12a34c3f146216f6854a173 Mon Sep 17 00:00:00 2001 From: Phil Estes Date: Tue, 20 Jan 2015 18:05:39 -0500 Subject: [PATCH 362/513] Move iptables check out of runtime init() to separate function Due to the iptables package being `init`ed at start of the docker runtime, this means the iptables --wait command listing all rules is run, no matter if the command is simply "docker -h". It makes more sense to both locate the iptables command and check for the wait flag support at the time iptables is actually used, as it may not be used at all if certain network support is off/configured differently. Docker-DCO-1.1-Signed-off-by: Phil Estes (github: estesp) --- pkg/iptables/iptables.go | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/pkg/iptables/iptables.go b/pkg/iptables/iptables.go index 90ccbeff5..010c99b15 100644 --- a/pkg/iptables/iptables.go +++ b/pkg/iptables/iptables.go @@ -24,6 +24,7 @@ const ( ) var ( + iptablesPath string supportsXlock = false ErrIptablesNotFound = errors.New("Iptables not found") ) @@ -43,8 +44,17 @@ func (e *ChainError) Error() string { return fmt.Sprintf("Error iptables %s: %s", e.Chain, string(e.Output)) } -func init() { - supportsXlock = exec.Command("iptables", "--wait", "-L", "-n").Run() == nil +func initCheck() error { + + if iptablesPath == "" { + path, err := exec.LookPath("iptables") + if err != nil { + return ErrIptablesNotFound + } + iptablesPath = path + supportsXlock = exec.Command(iptablesPath, "--wait", "-L", "-n").Run() == nil + } + return nil } func NewChain(name, bridge string, table Table) (*Chain, error) { @@ -258,18 +268,17 @@ func Exists(args ...string) bool { // Call 'iptables' system command, passing supplied arguments func Raw(args ...string) ([]byte, error) { - path, err := exec.LookPath("iptables") - if err != nil { - return nil, ErrIptablesNotFound - } + if err := initCheck(); err != nil { + return nil, err + } if supportsXlock { args = append([]string{"--wait"}, args...) } - log.Debugf("%s, %v", path, args) + log.Debugf("%s, %v", iptablesPath, args) - output, err := exec.Command(path, args...).CombinedOutput() + output, err := exec.Command(iptablesPath, args...).CombinedOutput() if err != nil { return nil, fmt.Errorf("iptables failed: iptables %v: %s (%s)", strings.Join(args, " "), output, err) } From f46923be8eba81dd9e261554128b26a1c83ce398 Mon Sep 17 00:00:00 2001 From: Josh Hawn Date: Tue, 20 Jan 2015 19:37:21 -0800 Subject: [PATCH 363/513] Resolve ambiguity on registry v2 ping v2 ping now checks for a Docker-Distribution-API-Version header that identifies the endpoint as "registry/2.0" Docker-DCO-1.1-Signed-off-by: Josh Hawn (github: jlhawn) --- Dockerfile | 2 +- registry/endpoint.go | 15 ++++++++++ registry/endpoint_test.go | 63 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 78 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index ae3b23b35..49887f872 100644 --- a/Dockerfile +++ b/Dockerfile @@ -149,7 +149,7 @@ RUN set -x \ COPY pkg/tarsum /go/src/github.com/docker/docker/pkg/tarsum # REGISTRY_COMMIT gives us the repeatability guarantees we need # (so that we're all testing the same version of the registry) -ENV REGISTRY_COMMIT 21a69f53b5c7986b831f33849d551cd59ec8cbd1 +ENV REGISTRY_COMMIT c448e0416925a9876d5576e412703c9b8b865e19 RUN set -x \ && git clone https://github.com/docker/distribution.git /go/src/github.com/docker/distribution \ && (cd /go/src/github.com/docker/distribution && git checkout -q $REGISTRY_COMMIT) \ diff --git a/registry/endpoint.go b/registry/endpoint.go index 9ca9ed8b9..72bcce4aa 100644 --- a/registry/endpoint.go +++ b/registry/endpoint.go @@ -227,6 +227,21 @@ func (e *Endpoint) pingV2() (RegistryInfo, error) { } defer resp.Body.Close() + // The endpoint may have multiple supported versions. + // Ensure it supports the v2 Registry API. + var supportsV2 bool + + for _, versionName := range resp.Header[http.CanonicalHeaderKey("Docker-Distribution-API-Version")] { + if versionName == "registry/2.0" { + supportsV2 = true + break + } + } + + if !supportsV2 { + return RegistryInfo{}, fmt.Errorf("%s does not appear to be a v2 registry endpoint", e) + } + if resp.StatusCode == http.StatusOK { // It would seem that no authentication/authorization is required. // So we don't need to parse/add any authorization schemes. diff --git a/registry/endpoint_test.go b/registry/endpoint_test.go index f6489034f..ef2589994 100644 --- a/registry/endpoint_test.go +++ b/registry/endpoint_test.go @@ -1,6 +1,11 @@ package registry -import "testing" +import ( + "net/http" + "net/http/httptest" + "net/url" + "testing" +) func TestEndpointParse(t *testing.T) { testData := []struct { @@ -27,3 +32,59 @@ func TestEndpointParse(t *testing.T) { } } } + +// Ensure that a registry endpoint that responds with a 401 only is determined +// to be a v1 registry unless it includes a valid v2 API header. +func TestValidateEndpointAmbiguousAPIVersion(t *testing.T) { + requireBasicAuthHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("WWW-Authenticate", `Basic realm="localhost"`) + w.WriteHeader(http.StatusUnauthorized) + }) + + requireBasicAuthHandlerV2 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Docker-Distribution-API-Version", "registry/2.0") + requireBasicAuthHandler.ServeHTTP(w, r) + }) + + // Make a test server which should validate as a v1 server. + testServer := httptest.NewServer(requireBasicAuthHandler) + defer testServer.Close() + + testServerURL, err := url.Parse(testServer.URL) + if err != nil { + t.Fatal(err) + } + + testEndpoint := Endpoint{ + URL: testServerURL, + Version: APIVersionUnknown, + } + + if err = validateEndpoint(&testEndpoint); err != nil { + t.Fatal(err) + } + + if testEndpoint.Version != APIVersion1 { + t.Fatalf("expected endpoint to validate to %s, got %s", APIVersion1, testEndpoint.Version) + } + + // Make a test server which should validate as a v2 server. + testServer = httptest.NewServer(requireBasicAuthHandlerV2) + defer testServer.Close() + + testServerURL, err = url.Parse(testServer.URL) + if err != nil { + t.Fatal(err) + } + + testEndpoint.URL = testServerURL + testEndpoint.Version = APIVersionUnknown + + if err = validateEndpoint(&testEndpoint); err != nil { + t.Fatal(err) + } + + if testEndpoint.Version != APIVersion2 { + t.Fatalf("expected endpoint to validate to %s, got %s", APIVersion2, testEndpoint.Version) + } +} From 681f4d84ae05a98b7096d52a168222bae362d9e1 Mon Sep 17 00:00:00 2001 From: Tianon Gravi Date: Tue, 20 Jan 2015 20:40:19 -0700 Subject: [PATCH 364/513] Update Dockerfile to use Godeps for distribution Update our "registry" install to use the included Godeps libraries so that it doesn't require anything from our current source (hence moving it up for better caching too) Signed-off-by: Andrew "Tianon" Page --- Dockerfile | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/Dockerfile b/Dockerfile index 49887f872..c452a6ad9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -113,6 +113,14 @@ RUN git clone -b buildroot-2014.02 https://github.com/jpetazzo/docker-busybox.gi # Get the "cirros" image source so we can import it instead of fetching it during tests RUN curl -sSL -o /cirros.tar.gz https://github.com/ewindisch/docker-cirros/raw/1cded459668e8b9dbf4ef976c94c05add9bbd8e9/cirros-0.3.0-x86_64-lxc.tar.gz +# Install registry +ENV REGISTRY_COMMIT c448e0416925a9876d5576e412703c9b8b865e19 +RUN set -x \ + && git clone https://github.com/docker/distribution.git /go/src/github.com/docker/distribution \ + && (cd /go/src/github.com/docker/distribution && git checkout -q $REGISTRY_COMMIT) \ + && GOPATH=/go/src/github.com/docker/distribution/Godeps/_workspace:/go \ + go build -o /go/bin/registry-v2 github.com/docker/distribution/cmd/registry + # Get the "docker-py" source so we can run their integration tests ENV DOCKER_PY_COMMIT aa19d7b6609c6676e8258f6b900dea2eda1dbe95 RUN git clone https://github.com/docker/docker-py.git /docker-py \ @@ -145,17 +153,6 @@ RUN set -x \ && git clone -b v1.2 https://github.com/russross/blackfriday.git /go/src/github.com/russross/blackfriday \ && go install -v github.com/cpuguy83/go-md2man -# Install registry -COPY pkg/tarsum /go/src/github.com/docker/docker/pkg/tarsum -# REGISTRY_COMMIT gives us the repeatability guarantees we need -# (so that we're all testing the same version of the registry) -ENV REGISTRY_COMMIT c448e0416925a9876d5576e412703c9b8b865e19 -RUN set -x \ - && git clone https://github.com/docker/distribution.git /go/src/github.com/docker/distribution \ - && (cd /go/src/github.com/docker/distribution && git checkout -q $REGISTRY_COMMIT) \ - && go get -d github.com/docker/distribution/cmd/registry \ - && go build -o /go/bin/registry-v2 github.com/docker/distribution/cmd/registry - # Wrap all commands in the "docker-in-docker" script to allow nested containers ENTRYPOINT ["hack/dind"] From 65f58e2a742205c9e8470b360bd439642a5c8211 Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Wed, 7 Jan 2015 14:43:04 -0800 Subject: [PATCH 365/513] Implement container stats collection in daemon Signed-off-by: Michael Crosby --- api/server/server.go | 14 ++++++ daemon/container.go | 7 +++ daemon/daemon.go | 16 +++++++ daemon/execdriver/driver.go | 9 ++++ daemon/execdriver/lxc/driver.go | 5 +++ daemon/execdriver/native/driver.go | 18 ++++++++ daemon/start.go | 15 +++++++ daemon/stats_collector.go | 71 ++++++++++++++++++++++++++++++ 8 files changed, 155 insertions(+) create mode 100644 daemon/stats_collector.go diff --git a/api/server/server.go b/api/server/server.go index d2715f1bc..d5cdbd00c 100644 --- a/api/server/server.go +++ b/api/server/server.go @@ -411,6 +411,19 @@ func getContainersJSON(eng *engine.Engine, version version.Version, w http.Respo return nil } +func getContainersStats(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := parseForm(r); err != nil { + return err + } + if vars == nil { + return fmt.Errorf("Missing parameter") + } + name := vars["name"] + job := eng.Job("container_stats", name) + streamJSON(job, w, true) + return job.Run() +} + func getContainersLogs(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 @@ -1323,6 +1336,7 @@ func createRouter(eng *engine.Engine, logging, enableCors bool, dockerVersion st "/containers/{name:.*}/json": getContainersByName, "/containers/{name:.*}/top": getContainersTop, "/containers/{name:.*}/logs": getContainersLogs, + "/containers/{name:.*}/stats": getContainersStats, "/containers/{name:.*}/attach/ws": wsContainersAttach, "/exec/{id:.*}/json": getExecByID, }, diff --git a/daemon/container.go b/daemon/container.go index b0eaea03b..4a0232878 100644 --- a/daemon/container.go +++ b/daemon/container.go @@ -1414,3 +1414,10 @@ func (container *Container) getNetworkedContainer() (*Container, error) { return nil, fmt.Errorf("network mode not set to container") } } + +func (container *Container) Stats() (*execdriver.ResourceStats, error) { + if !container.IsRunning() { + return nil, fmt.Errorf("cannot collect stats on a non running container") + } + return container.daemon.Stats(container) +} diff --git a/daemon/daemon.go b/daemon/daemon.go index 1b2d13bb6..01d8245de 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -104,6 +104,7 @@ type Daemon struct { driver graphdriver.Driver execDriver execdriver.Driver trustStore *trust.TrustStore + statsCollector *statsCollector } // Install installs daemon capabilities to eng. @@ -116,6 +117,7 @@ func (daemon *Daemon) Install(eng *engine.Engine) error { "container_copy": daemon.ContainerCopy, "container_rename": daemon.ContainerRename, "container_inspect": daemon.ContainerInspect, + "container_stats": daemon.ContainerStats, "containers": daemon.Containers, "create": daemon.ContainerCreate, "rm": daemon.ContainerRm, @@ -982,6 +984,7 @@ func NewDaemonFromDirectory(config *Config, eng *engine.Engine) (*Daemon, error) execDriver: ed, eng: eng, trustStore: t, + statsCollector: newStatsCollector(1 * time.Second), } if err := daemon.restore(); err != nil { return nil, err @@ -1092,6 +1095,19 @@ func (daemon *Daemon) Kill(c *Container, sig int) error { return daemon.execDriver.Kill(c.command, sig) } +func (daemon *Daemon) Stats(c *Container) (*execdriver.ResourceStats, error) { + return daemon.execDriver.Stats(c.ID) +} + +func (daemon *Daemon) SubscribeToContainerStats(name string) (<-chan *execdriver.ResourceStats, error) { + c := daemon.Get(name) + if c == nil { + return nil, fmt.Errorf("no such container") + } + ch := daemon.statsCollector.collect(c) + return ch, nil +} + // Nuke kills all containers then removes all content // from the content root, including images, volumes and // container filesystems. diff --git a/daemon/execdriver/driver.go b/daemon/execdriver/driver.go index fe99e062d..044a2ea0a 100644 --- a/daemon/execdriver/driver.go +++ b/daemon/execdriver/driver.go @@ -5,7 +5,9 @@ import ( "io" "os" "os/exec" + "time" + "github.com/docker/libcontainer" "github.com/docker/libcontainer/devices" ) @@ -61,6 +63,7 @@ type Driver interface { GetPidsForContainer(id string) ([]int, error) // Returns a list of pids for the given container. Terminate(c *Command) error // kill it with fire Clean(id string) error // clean all traces of container exec + Stats(id string) (*ResourceStats, error) // Get resource stats for a running container } // Network settings of the container @@ -101,6 +104,12 @@ type Resources struct { Cpuset string `json:"cpuset"` } +type ResourceStats struct { + *libcontainer.ContainerStats + Read time.Time `json:"read"` + ClockTicks int `json:"clock_ticks"` +} + type Mount struct { Source string `json:"source"` Destination string `json:"destination"` diff --git a/daemon/execdriver/lxc/driver.go b/daemon/execdriver/lxc/driver.go index c02ceae97..7dca19d76 100644 --- a/daemon/execdriver/lxc/driver.go +++ b/daemon/execdriver/lxc/driver.go @@ -524,3 +524,8 @@ func (t *TtyConsole) Close() error { func (d *driver) Exec(c *execdriver.Command, processConfig *execdriver.ProcessConfig, pipes *execdriver.Pipes, startCallback execdriver.StartCallback) (int, error) { return -1, ErrExec } + +func (d *driver) Stats(id string) (*execdriver.ResourceStats, error) { + return nil, fmt.Errorf("container stats are not support with LXC") + +} diff --git a/daemon/execdriver/native/driver.go b/daemon/execdriver/native/driver.go index f6099bd04..e82d784aa 100644 --- a/daemon/execdriver/native/driver.go +++ b/daemon/execdriver/native/driver.go @@ -13,6 +13,7 @@ import ( "strings" "sync" "syscall" + "time" log "github.com/Sirupsen/logrus" "github.com/docker/docker/daemon/execdriver" @@ -279,6 +280,23 @@ func (d *driver) Clean(id string) error { return os.RemoveAll(filepath.Join(d.root, id)) } +func (d *driver) Stats(id string) (*execdriver.ResourceStats, error) { + state, err := libcontainer.GetState(filepath.Join(d.root, id)) + if err != nil { + return nil, err + } + now := time.Now() + stats, err := libcontainer.GetStats(nil, state) + if err != nil { + return nil, err + } + return &execdriver.ResourceStats{ + ContainerStats: stats, + ClockTicks: system.GetClockTicks(), + Read: now, + }, nil +} + func getEnv(key string, env []string) string { for _, pair := range env { parts := strings.Split(pair, "=") diff --git a/daemon/start.go b/daemon/start.go index 363461080..89116a84d 100644 --- a/daemon/start.go +++ b/daemon/start.go @@ -1,6 +1,7 @@ package daemon import ( + "encoding/json" "fmt" "os" "strings" @@ -77,3 +78,17 @@ func (daemon *Daemon) setHostConfig(container *Container, hostConfig *runconfig. return nil } + +func (daemon *Daemon) ContainerStats(job *engine.Job) engine.Status { + stats, err := daemon.SubscribeToContainerStats(job.Args[0]) + if err != nil { + return job.Error(err) + } + enc := json.NewEncoder(job.Stdout) + for update := range stats { + if err := enc.Encode(update); err != nil { + return job.Error(err) + } + } + return engine.StatusOK +} diff --git a/daemon/stats_collector.go b/daemon/stats_collector.go new file mode 100644 index 000000000..0d1059d8b --- /dev/null +++ b/daemon/stats_collector.go @@ -0,0 +1,71 @@ +package daemon + +import ( + "sync" + "time" + + log "github.com/Sirupsen/logrus" + "github.com/docker/docker/daemon/execdriver" +) + +func newStatsCollector(interval time.Duration) *statsCollector { + s := &statsCollector{ + interval: interval, + containers: make(map[string]*statsCollectorData), + } + s.start() + return s +} + +type statsCollectorData struct { + c *Container + lastStats *execdriver.ResourceStats + subs []chan *execdriver.ResourceStats +} + +// statsCollector manages and provides container resource stats +type statsCollector struct { + m sync.Mutex + interval time.Duration + containers map[string]*statsCollectorData +} + +func (s *statsCollector) collect(c *Container) <-chan *execdriver.ResourceStats { + s.m.Lock() + ch := make(chan *execdriver.ResourceStats, 1024) + s.containers[c.ID] = &statsCollectorData{ + c: c, + subs: []chan *execdriver.ResourceStats{ + ch, + }, + } + s.m.Unlock() + return ch +} + +func (s *statsCollector) stopCollection(c *Container) { + s.m.Lock() + delete(s.containers, c.ID) + s.m.Unlock() +} + +func (s *statsCollector) start() { + go func() { + for _ = range time.Tick(s.interval) { + log.Debugf("starting collection of container stats") + s.m.Lock() + for id, d := range s.containers { + stats, err := d.c.Stats() + if err != nil { + // TODO: @crosbymichael evict container depending on error + log.Errorf("collecting stats for %s: %v", id, err) + continue + } + for _, sub := range s.containers[id].subs { + sub <- stats + } + } + s.m.Unlock() + } + }() +} From 2640a10bca29c4a4199c906a26f658aac8a68dc2 Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Wed, 7 Jan 2015 16:22:42 -0800 Subject: [PATCH 366/513] Implement client side display for stats Signed-off-by: Michael Crosby --- api/client/commands.go | 105 +++++++++++++ daemon/execdriver/driver.go | 6 +- daemon/execdriver/execdrivers/execdrivers.go | 11 +- daemon/execdriver/native/driver.go | 12 +- daemon/start.go | 16 +- daemon/stats_collector.go | 44 ++++++ stats/stats.go | 156 +++++++++++++++++++ 7 files changed, 340 insertions(+), 10 deletions(-) create mode 100644 stats/stats.go diff --git a/api/client/commands.go b/api/client/commands.go index 4cfe97cdd..6c6595c24 100644 --- a/api/client/commands.go +++ b/api/client/commands.go @@ -16,6 +16,7 @@ import ( "path" "path/filepath" "runtime" + "sort" "strconv" "strings" "text/tabwriter" @@ -42,6 +43,7 @@ import ( "github.com/docker/docker/pkg/urlutil" "github.com/docker/docker/registry" "github.com/docker/docker/runconfig" + "github.com/docker/docker/stats" "github.com/docker/docker/utils" "github.com/docker/libtrust" ) @@ -2618,3 +2620,106 @@ func (cli *DockerCli) CmdExec(args ...string) error { return nil } + +type containerStats struct { + Name string + CpuPercentage float64 + Memory float64 + MemoryPercentage float64 + NetworkRx int + NetworkTx int +} + +type statSorter struct { + stats []containerStats +} + +func (s *statSorter) Len() int { + return len(s.stats) +} + +func (s *statSorter) Swap(i, j int) { + s.stats[i], s.stats[j] = s.stats[j], s.stats[i] +} + +func (s *statSorter) Less(i, j int) bool { + return s.stats[i].Name < s.stats[j].Name +} + +func (cli *DockerCli) CmdStats(args ...string) error { + cmd := cli.Subcmd("stats", "CONTAINER", "Stream the stats of a container", true) + cmd.Require(flag.Min, 1) + utils.ParseFlags(cmd, args, true) + + cStats := map[string]containerStats{} + for _, name := range cmd.Args() { + go cli.streamStats(name, cStats) + } + w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0) + for _ = range time.Tick(1000 * time.Millisecond) { + fmt.Fprint(cli.out, "\033[2J") + fmt.Fprint(cli.out, "\033[H") + fmt.Fprintln(w, "CONTAINER\tCPU %\tMEM\tMEM %\tNET I/O") + sStats := []containerStats{} + for _, s := range cStats { + sStats = append(sStats, s) + } + sorter := &statSorter{sStats} + sort.Sort(sorter) + for _, s := range sStats { + fmt.Fprintf(w, "%s\t%f%%\t%s\t%f%%\t%d/%d\n", + s.Name, + s.CpuPercentage, + units.HumanSize(s.Memory), + s.MemoryPercentage, + s.NetworkRx, s.NetworkTx) + } + w.Flush() + } + return nil +} + +func (cli *DockerCli) streamStats(name string, data map[string]containerStats) error { + stream, _, err := cli.call("GET", "/containers/"+name+"/stats", nil, false) + if err != nil { + return err + } + + var ( + previousCpu uint64 + previousSystem uint64 + start = true + dec = json.NewDecoder(stream) + ) + for { + var v *stats.Stats + if err := dec.Decode(&v); err != nil { + return err + } + memPercent := float64(v.MemoryStats.Usage) / float64(v.MemoryStats.Limit) * 100.0 + cpuPercent := 0.0 + + if !start { + cpuDelta := float64(v.CpuStats.CpuUsage.TotalUsage) - float64(previousCpu) + systemDelta := float64(int(v.CpuStats.SystemUsage)/v.ClockTicks) - float64(int(previousSystem)/v.ClockTicks) + + if systemDelta > 0.0 { + cpuPercent = (cpuDelta / systemDelta) * float64(v.ClockTicks*len(v.CpuStats.CpuUsage.PercpuUsage)) + } + } + start = false + d := data[name] + d.Name = name + d.CpuPercentage = cpuPercent + d.Memory = float64(v.MemoryStats.Usage) + d.MemoryPercentage = memPercent + d.NetworkRx = int(v.Network.RxBytes) + d.NetworkTx = int(v.Network.TxBytes) + data[name] = d + + previousCpu = v.CpuStats.CpuUsage.TotalUsage + previousSystem = v.CpuStats.SystemUsage + } + return nil + +} diff --git a/daemon/execdriver/driver.go b/daemon/execdriver/driver.go index 044a2ea0a..f33f1671d 100644 --- a/daemon/execdriver/driver.go +++ b/daemon/execdriver/driver.go @@ -106,8 +106,10 @@ type Resources struct { type ResourceStats struct { *libcontainer.ContainerStats - Read time.Time `json:"read"` - ClockTicks int `json:"clock_ticks"` + Read time.Time `json:"read"` + ClockTicks int `json:"clock_ticks"` + MemoryLimit int64 `json:"memory_limit"` + SystemUsage uint64 `json:"system_usage"` } type Mount struct { diff --git a/daemon/execdriver/execdrivers/execdrivers.go b/daemon/execdriver/execdrivers/execdrivers.go index 2a050b483..b7dd98cf3 100644 --- a/daemon/execdriver/execdrivers/execdrivers.go +++ b/daemon/execdriver/execdrivers/execdrivers.go @@ -2,14 +2,21 @@ package execdrivers import ( "fmt" + "path" + "github.com/docker/docker/daemon/execdriver" "github.com/docker/docker/daemon/execdriver/lxc" "github.com/docker/docker/daemon/execdriver/native" "github.com/docker/docker/pkg/sysinfo" - "path" + "github.com/docker/docker/pkg/system" ) func NewDriver(name, root, initPath string, sysInfo *sysinfo.SysInfo) (execdriver.Driver, error) { + meminfo, err := system.ReadMemInfo() + if err != nil { + return nil, err + } + switch name { case "lxc": // we want to give the lxc driver the full docker root because it needs @@ -17,7 +24,7 @@ func NewDriver(name, root, initPath string, sysInfo *sysinfo.SysInfo) (execdrive // to be backwards compatible return lxc.NewDriver(root, initPath, sysInfo.AppArmor) case "native": - return native.NewDriver(path.Join(root, "execdriver", "native"), initPath) + return native.NewDriver(path.Join(root, "execdriver", "native"), initPath, meminfo.MemTotal/1000) } return nil, fmt.Errorf("unknown exec driver %s", name) } diff --git a/daemon/execdriver/native/driver.go b/daemon/execdriver/native/driver.go index e82d784aa..83e07f392 100644 --- a/daemon/execdriver/native/driver.go +++ b/daemon/execdriver/native/driver.go @@ -42,23 +42,23 @@ type driver struct { root string initPath string activeContainers map[string]*activeContainer + machineMemory int64 sync.Mutex } -func NewDriver(root, initPath string) (*driver, error) { +func NewDriver(root, initPath string, machineMemory int64) (*driver, error) { if err := os.MkdirAll(root, 0700); err != nil { return nil, err } - // native driver root is at docker_root/execdriver/native. Put apparmor at docker_root if err := apparmor.InstallDefaultProfile(); err != nil { return nil, err } - return &driver{ root: root, initPath: initPath, activeContainers: make(map[string]*activeContainer), + machineMemory: machineMemory, }, nil } @@ -281,6 +281,7 @@ func (d *driver) Clean(id string) error { } func (d *driver) Stats(id string) (*execdriver.ResourceStats, error) { + c := d.activeContainers[id] state, err := libcontainer.GetState(filepath.Join(d.root, id)) if err != nil { return nil, err @@ -290,10 +291,15 @@ func (d *driver) Stats(id string) (*execdriver.ResourceStats, error) { if err != nil { return nil, err } + memoryLimit := c.container.Cgroups.Memory + if memoryLimit == 0 { + memoryLimit = d.machineMemory + } return &execdriver.ResourceStats{ ContainerStats: stats, ClockTicks: system.GetClockTicks(), Read: now, + MemoryLimit: memoryLimit, }, nil } diff --git a/daemon/start.go b/daemon/start.go index 89116a84d..150a87f57 100644 --- a/daemon/start.go +++ b/daemon/start.go @@ -8,6 +8,7 @@ import ( "github.com/docker/docker/engine" "github.com/docker/docker/runconfig" + "github.com/docker/docker/stats" ) func (daemon *Daemon) ContainerStart(job *engine.Job) engine.Status { @@ -80,15 +81,24 @@ func (daemon *Daemon) setHostConfig(container *Container, hostConfig *runconfig. } func (daemon *Daemon) ContainerStats(job *engine.Job) engine.Status { - stats, err := daemon.SubscribeToContainerStats(job.Args[0]) + s, err := daemon.SubscribeToContainerStats(job.Args[0]) if err != nil { return job.Error(err) } enc := json.NewEncoder(job.Stdout) - for update := range stats { - if err := enc.Encode(update); err != nil { + for update := range s { + ss := stats.ToStats(update.ContainerStats) + ss.MemoryStats.Limit = uint64(update.MemoryLimit) + ss.Read = update.Read + ss.ClockTicks = update.ClockTicks + ss.CpuStats.SystemUsage = update.SystemUsage + if err := enc.Encode(ss); err != nil { return job.Error(err) } } return engine.StatusOK } + +func mapToAPIStats() { + +} diff --git a/daemon/stats_collector.go b/daemon/stats_collector.go index 0d1059d8b..a21092a85 100644 --- a/daemon/stats_collector.go +++ b/daemon/stats_collector.go @@ -1,6 +1,11 @@ package daemon import ( + "bufio" + "fmt" + "os" + "strconv" + "strings" "sync" "time" @@ -55,12 +60,18 @@ func (s *statsCollector) start() { log.Debugf("starting collection of container stats") s.m.Lock() for id, d := range s.containers { + systemUsage, err := getSystemCpuUsage() + if err != nil { + log.Errorf("collecting system cpu usage for %s: %v", id, err) + continue + } stats, err := d.c.Stats() if err != nil { // TODO: @crosbymichael evict container depending on error log.Errorf("collecting stats for %s: %v", id, err) continue } + stats.SystemUsage = systemUsage for _, sub := range s.containers[id].subs { sub <- stats } @@ -69,3 +80,36 @@ func (s *statsCollector) start() { } }() } + +// returns value in nanoseconds +func getSystemCpuUsage() (uint64, error) { + f, err := os.Open("/proc/stat") + if err != nil { + return 0, err + } + defer f.Close() + + sc := bufio.NewScanner(f) + for sc.Scan() { + parts := strings.Fields(sc.Text()) + switch parts[0] { + case "cpu": + if len(parts) < 8 { + return 0, fmt.Errorf("invalid number of cpu fields") + } + + var total uint64 + for _, i := range parts[1:8] { + v, err := strconv.ParseUint(i, 10, 64) + if err != nil { + return 0.0, fmt.Errorf("Unable to convert value %s to int: %s", i, err) + } + total += v + } + return total * 1000000000, nil + default: + continue + } + } + return 0, fmt.Errorf("invalid stat format") +} diff --git a/stats/stats.go b/stats/stats.go new file mode 100644 index 000000000..e151014f3 --- /dev/null +++ b/stats/stats.go @@ -0,0 +1,156 @@ +package stats + +import ( + "time" + + "github.com/docker/libcontainer" + "github.com/docker/libcontainer/cgroups" +) + +type ThrottlingData struct { + // Number of periods with throttling active + Periods uint64 `json:"periods,omitempty"` + // Number of periods when the container hit its throttling limit. + ThrottledPeriods uint64 `json:"throttled_periods,omitempty"` + // Aggregate time the container was throttled for in nanoseconds. + ThrottledTime uint64 `json:"throttled_time,omitempty"` +} + +// All CPU stats are aggregate since container inception. +type CpuUsage struct { + // Total CPU time consumed. + // Units: nanoseconds. + TotalUsage uint64 `json:"total_usage,omitempty"` + // Total CPU time consumed per core. + // Units: nanoseconds. + PercpuUsage []uint64 `json:"percpu_usage,omitempty"` + // Time spent by tasks of the cgroup in kernel mode. + // Units: nanoseconds. + UsageInKernelmode uint64 `json:"usage_in_kernelmode"` + // Time spent by tasks of the cgroup in user mode. + // Units: nanoseconds. + UsageInUsermode uint64 `json:"usage_in_usermode"` +} + +type CpuStats struct { + CpuUsage CpuUsage `json:"cpu_usage,omitempty"` + SystemUsage uint64 `json:"system_cpu_usage"` + ThrottlingData ThrottlingData `json:"throttling_data,omitempty"` +} + +type MemoryStats struct { + // current res_counter usage for memory + Usage uint64 `json:"usage,omitempty"` + // maximum usage ever recorded. + MaxUsage uint64 `json:"max_usage,omitempty"` + // TODO(vishh): Export these as stronger types. + // all the stats exported via memory.stat. + Stats map[string]uint64 `json:"stats,omitempty"` + // number of times memory usage hits limits. + Failcnt uint64 `json:"failcnt"` + Limit uint64 `json:"limit"` +} + +type BlkioStatEntry struct { + Major uint64 `json:"major,omitempty"` + Minor uint64 `json:"minor,omitempty"` + Op string `json:"op,omitempty"` + Value uint64 `json:"value,omitempty"` +} + +type BlkioStats struct { + // number of bytes tranferred to and from the block device + IoServiceBytesRecursive []BlkioStatEntry `json:"io_service_bytes_recursive,omitempty"` + IoServicedRecursive []BlkioStatEntry `json:"io_serviced_recursive,omitempty"` + IoQueuedRecursive []BlkioStatEntry `json:"io_queue_recursive,omitempty"` + IoServiceTimeRecursive []BlkioStatEntry `json:"io_service_time_recursive,omitempty"` + IoWaitTimeRecursive []BlkioStatEntry `json:"io_wait_time_recursive,omitempty"` + IoMergedRecursive []BlkioStatEntry `json:"io_merged_recursive,omitempty"` + IoTimeRecursive []BlkioStatEntry `json:"io_time_recursive,omitempty"` + SectorsRecursive []BlkioStatEntry `json:"sectors_recursive,omitempty"` +} + +type Network struct { + RxBytes uint64 `json:"rx_bytes"` + RxPackets uint64 `json:"rx_packets"` + RxErrors uint64 `json:"rx_errors"` + RxDropped uint64 `json:"rx_dropped"` + TxBytes uint64 `json:"tx_bytes"` + TxPackets uint64 `json:"tx_packets"` + TxErrors uint64 `json:"tx_errors"` + TxDropped uint64 `json:"tx_dropped"` +} + +type Stats struct { + Read time.Time `json:"read"` + ClockTicks int `json:"clock_ticks"` + Interval int `json:"interval"` // in ms + Network Network `json:"network,omitempty"` + CpuStats CpuStats `json:"cpu_stats,omitempty"` + MemoryStats MemoryStats `json:"memory_stats,omitempty"` + BlkioStats BlkioStats `json:"blkio_stats,omitempty"` +} + +func ToStats(ls *libcontainer.ContainerStats) *Stats { + s := &Stats{} + if ls.NetworkStats != nil { + s.Network = Network{ + RxBytes: ls.NetworkStats.RxBytes, + RxPackets: ls.NetworkStats.RxPackets, + RxErrors: ls.NetworkStats.RxErrors, + RxDropped: ls.NetworkStats.RxDropped, + TxBytes: ls.NetworkStats.TxBytes, + TxPackets: ls.NetworkStats.TxPackets, + TxErrors: ls.NetworkStats.TxErrors, + TxDropped: ls.NetworkStats.TxDropped, + } + } + cs := ls.CgroupStats + if cs != nil { + s.BlkioStats = BlkioStats{ + IoServiceBytesRecursive: copyBlkioEntry(cs.BlkioStats.IoServiceBytesRecursive), + IoServicedRecursive: copyBlkioEntry(cs.BlkioStats.IoServicedRecursive), + IoQueuedRecursive: copyBlkioEntry(cs.BlkioStats.IoQueuedRecursive), + IoServiceTimeRecursive: copyBlkioEntry(cs.BlkioStats.IoServiceTimeRecursive), + IoWaitTimeRecursive: copyBlkioEntry(cs.BlkioStats.IoWaitTimeRecursive), + IoMergedRecursive: copyBlkioEntry(cs.BlkioStats.IoMergedRecursive), + IoTimeRecursive: copyBlkioEntry(cs.BlkioStats.IoTimeRecursive), + SectorsRecursive: copyBlkioEntry(cs.BlkioStats.SectorsRecursive), + } + cpu := cs.CpuStats + s.CpuStats = CpuStats{ + CpuUsage: CpuUsage{ + TotalUsage: cpu.CpuUsage.TotalUsage, + PercpuUsage: cpu.CpuUsage.PercpuUsage, + UsageInKernelmode: cpu.CpuUsage.UsageInKernelmode, + UsageInUsermode: cpu.CpuUsage.UsageInUsermode, + }, + ThrottlingData: ThrottlingData{ + Periods: cpu.ThrottlingData.Periods, + ThrottledPeriods: cpu.ThrottlingData.ThrottledPeriods, + ThrottledTime: cpu.ThrottlingData.ThrottledTime, + }, + } + mem := cs.MemoryStats + s.MemoryStats = MemoryStats{ + Usage: mem.Usage, + MaxUsage: mem.MaxUsage, + Stats: mem.Stats, + Failcnt: mem.Failcnt, + } + } + return s +} + +func copyBlkioEntry(entries []cgroups.BlkioStatEntry) []BlkioStatEntry { + out := make([]BlkioStatEntry, len(entries)) + for i, re := range entries { + out[i] = BlkioStatEntry{ + Major: re.Major, + Minor: re.Minor, + Op: re.Op, + Value: re.Value, + } + } + return out +} From 4f174aa79276c12a1b2b98df2f02d6bee36b7a93 Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Wed, 7 Jan 2015 18:02:08 -0800 Subject: [PATCH 367/513] Evict stopped containers Signed-off-by: Michael Crosby --- api/client/commands.go | 97 ++++++++++---------- api/client/sort.go | 29 ++++++ {stats => api/stats}/stats.go | 4 +- daemon/container.go | 3 - daemon/daemon.go | 11 ++- daemon/delete.go | 3 + daemon/execdriver/driver.go | 2 +- daemon/execdriver/execdrivers/execdrivers.go | 2 +- daemon/execdriver/lxc/driver.go | 2 +- daemon/execdriver/native/driver.go | 7 +- daemon/start.go | 25 ----- daemon/stats.go | 29 ++++++ daemon/stats_collector.go | 72 ++++++++++++--- docker/flags.go | 1 + 14 files changed, 192 insertions(+), 95 deletions(-) create mode 100644 api/client/sort.go rename {stats => api/stats}/stats.go (96%) create mode 100644 daemon/stats.go diff --git a/api/client/commands.go b/api/client/commands.go index 6c6595c24..34ca32c29 100644 --- a/api/client/commands.go +++ b/api/client/commands.go @@ -16,15 +16,16 @@ import ( "path" "path/filepath" "runtime" - "sort" "strconv" "strings" + "sync" "text/tabwriter" "text/template" "time" log "github.com/Sirupsen/logrus" "github.com/docker/docker/api" + "github.com/docker/docker/api/stats" "github.com/docker/docker/dockerversion" "github.com/docker/docker/engine" "github.com/docker/docker/graph" @@ -43,7 +44,6 @@ import ( "github.com/docker/docker/pkg/urlutil" "github.com/docker/docker/registry" "github.com/docker/docker/runconfig" - "github.com/docker/docker/stats" "github.com/docker/docker/utils" "github.com/docker/libtrust" ) @@ -2625,25 +2625,10 @@ type containerStats struct { Name string CpuPercentage float64 Memory float64 + MemoryLimit float64 MemoryPercentage float64 - NetworkRx int - NetworkTx int -} - -type statSorter struct { - stats []containerStats -} - -func (s *statSorter) Len() int { - return len(s.stats) -} - -func (s *statSorter) Swap(i, j int) { - s.stats[i], s.stats[j] = s.stats[j], s.stats[i] -} - -func (s *statSorter) Less(i, j int) bool { - return s.stats[i].Name < s.stats[j].Name + NetworkRx float64 + NetworkTx float64 } func (cli *DockerCli) CmdStats(args ...string) error { @@ -2651,40 +2636,49 @@ func (cli *DockerCli) CmdStats(args ...string) error { cmd.Require(flag.Min, 1) utils.ParseFlags(cmd, args, true) + m := &sync.Mutex{} cStats := map[string]containerStats{} for _, name := range cmd.Args() { - go cli.streamStats(name, cStats) + go cli.streamStats(name, cStats, m) } w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0) - for _ = range time.Tick(1000 * time.Millisecond) { + for _ = range time.Tick(500 * time.Millisecond) { fmt.Fprint(cli.out, "\033[2J") fmt.Fprint(cli.out, "\033[H") - fmt.Fprintln(w, "CONTAINER\tCPU %\tMEM\tMEM %\tNET I/O") - sStats := []containerStats{} - for _, s := range cStats { - sStats = append(sStats, s) - } - sorter := &statSorter{sStats} - sort.Sort(sorter) - for _, s := range sStats { - fmt.Fprintf(w, "%s\t%f%%\t%s\t%f%%\t%d/%d\n", + fmt.Fprintln(w, "CONTAINER\tCPU %\tMEM USAGE/LIMIT\tMEM %\tNET I/O") + m.Lock() + ss := sortStatsByName(cStats) + m.Unlock() + for _, s := range ss { + fmt.Fprintf(w, "%s\t%.2f%%\t%s/%s\t%.2f%%\t%s/%s\n", s.Name, s.CpuPercentage, - units.HumanSize(s.Memory), + units.BytesSize(s.Memory), units.BytesSize(s.MemoryLimit), s.MemoryPercentage, - s.NetworkRx, s.NetworkTx) + units.BytesSize(s.NetworkRx), units.BytesSize(s.NetworkTx)) } w.Flush() } return nil } -func (cli *DockerCli) streamStats(name string, data map[string]containerStats) error { +func (cli *DockerCli) streamStats(name string, data map[string]containerStats, m *sync.Mutex) error { + m.Lock() + data[name] = containerStats{ + Name: name, + } + m.Unlock() + stream, _, err := cli.call("GET", "/containers/"+name+"/stats", nil, false) if err != nil { return err } - + defer func() { + stream.Close() + m.Lock() + delete(data, name) + m.Unlock() + }() var ( previousCpu uint64 previousSystem uint64 @@ -2696,30 +2690,37 @@ func (cli *DockerCli) streamStats(name string, data map[string]containerStats) e if err := dec.Decode(&v); err != nil { return err } - memPercent := float64(v.MemoryStats.Usage) / float64(v.MemoryStats.Limit) * 100.0 - cpuPercent := 0.0 - + var ( + memPercent = float64(v.MemoryStats.Usage) / float64(v.MemoryStats.Limit) * 100.0 + cpuPercent = 0.0 + ) if !start { - cpuDelta := float64(v.CpuStats.CpuUsage.TotalUsage) - float64(previousCpu) - systemDelta := float64(int(v.CpuStats.SystemUsage)/v.ClockTicks) - float64(int(previousSystem)/v.ClockTicks) - - if systemDelta > 0.0 { - cpuPercent = (cpuDelta / systemDelta) * float64(v.ClockTicks*len(v.CpuStats.CpuUsage.PercpuUsage)) - } + cpuPercent = calcuateCpuPercent(previousCpu, previousSystem, v) } start = false + m.Lock() d := data[name] - d.Name = name d.CpuPercentage = cpuPercent d.Memory = float64(v.MemoryStats.Usage) + d.MemoryLimit = float64(v.MemoryStats.Limit) d.MemoryPercentage = memPercent - d.NetworkRx = int(v.Network.RxBytes) - d.NetworkTx = int(v.Network.TxBytes) + d.NetworkRx = float64(v.Network.RxBytes) + d.NetworkTx = float64(v.Network.TxBytes) data[name] = d + m.Unlock() previousCpu = v.CpuStats.CpuUsage.TotalUsage previousSystem = v.CpuStats.SystemUsage } return nil - +} + +func calcuateCpuPercent(previousCpu, previousSystem uint64, v *stats.Stats) float64 { + cpuPercent := 0.0 + cpuDelta := float64(v.CpuStats.CpuUsage.TotalUsage) - float64(previousCpu) + systemDelta := float64(int(v.CpuStats.SystemUsage)/v.ClockTicks) - float64(int(previousSystem)/v.ClockTicks) + if systemDelta > 0.0 { + cpuPercent = (cpuDelta / systemDelta) * float64(v.ClockTicks*len(v.CpuStats.CpuUsage.PercpuUsage)) + } + return cpuPercent } diff --git a/api/client/sort.go b/api/client/sort.go new file mode 100644 index 000000000..1b8232c3f --- /dev/null +++ b/api/client/sort.go @@ -0,0 +1,29 @@ +package client + +import "sort" + +func sortStatsByName(cStats map[string]containerStats) []containerStats { + sStats := []containerStats{} + for _, s := range cStats { + sStats = append(sStats, s) + } + sorter := &statSorter{sStats} + sort.Sort(sorter) + return sStats +} + +type statSorter struct { + stats []containerStats +} + +func (s *statSorter) Len() int { + return len(s.stats) +} + +func (s *statSorter) Swap(i, j int) { + s.stats[i], s.stats[j] = s.stats[j], s.stats[i] +} + +func (s *statSorter) Less(i, j int) bool { + return s.stats[i].Name < s.stats[j].Name +} diff --git a/stats/stats.go b/api/stats/stats.go similarity index 96% rename from stats/stats.go rename to api/stats/stats.go index e151014f3..b2820f243 100644 --- a/stats/stats.go +++ b/api/stats/stats.go @@ -16,7 +16,7 @@ type ThrottlingData struct { ThrottledTime uint64 `json:"throttled_time,omitempty"` } -// All CPU stats are aggregate since container inception. +// All CPU stats are aggregated since container inception. type CpuUsage struct { // Total CPU time consumed. // Units: nanoseconds. @@ -91,6 +91,8 @@ type Stats struct { BlkioStats BlkioStats `json:"blkio_stats,omitempty"` } +// ToStats converts the libcontainer.ContainerStats to the api specific +// structs. This is done to preserve API compatibility and versioning. func ToStats(ls *libcontainer.ContainerStats) *Stats { s := &Stats{} if ls.NetworkStats != nil { diff --git a/daemon/container.go b/daemon/container.go index 4a0232878..046ec71e8 100644 --- a/daemon/container.go +++ b/daemon/container.go @@ -1416,8 +1416,5 @@ func (container *Container) getNetworkedContainer() (*Container, error) { } func (container *Container) Stats() (*execdriver.ResourceStats, error) { - if !container.IsRunning() { - return nil, fmt.Errorf("cannot collect stats on a non running container") - } return container.daemon.Stats(container) } diff --git a/daemon/daemon.go b/daemon/daemon.go index 01d8245de..82d2bc757 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -1099,7 +1099,7 @@ func (daemon *Daemon) Stats(c *Container) (*execdriver.ResourceStats, error) { return daemon.execDriver.Stats(c.ID) } -func (daemon *Daemon) SubscribeToContainerStats(name string) (<-chan *execdriver.ResourceStats, error) { +func (daemon *Daemon) SubscribeToContainerStats(name string) (chan *execdriver.ResourceStats, error) { c := daemon.Get(name) if c == nil { return nil, fmt.Errorf("no such container") @@ -1108,6 +1108,15 @@ func (daemon *Daemon) SubscribeToContainerStats(name string) (<-chan *execdriver return ch, nil } +func (daemon *Daemon) UnsubscribeToContainerStats(name string, ch chan *execdriver.ResourceStats) error { + c := daemon.Get(name) + if c == nil { + return fmt.Errorf("no such container") + } + daemon.statsCollector.unsubscribe(c, ch) + return nil +} + // Nuke kills all containers then removes all content // from the content root, including images, volumes and // container filesystems. diff --git a/daemon/delete.go b/daemon/delete.go index 990e4b448..59c765178 100644 --- a/daemon/delete.go +++ b/daemon/delete.go @@ -49,6 +49,9 @@ func (daemon *Daemon) ContainerRm(job *engine.Job) engine.Status { } if container != nil { + // stop collection of stats for the container regardless + // if stats are currently getting collected. + daemon.statsCollector.stopCollection(container) if container.IsRunning() { if forceRemove { if err := container.Kill(); err != nil { diff --git a/daemon/execdriver/driver.go b/daemon/execdriver/driver.go index f33f1671d..f6e0ac728 100644 --- a/daemon/execdriver/driver.go +++ b/daemon/execdriver/driver.go @@ -16,7 +16,7 @@ import ( type Context map[string]string var ( - ErrNotRunning = errors.New("Process could not be started") + ErrNotRunning = errors.New("Container is not running") ErrWaitTimeoutReached = errors.New("Wait timeout reached") ErrDriverAlreadyRegistered = errors.New("A driver already registered this docker init function") ErrDriverNotFound = errors.New("The requested docker init has not been found") diff --git a/daemon/execdriver/execdrivers/execdrivers.go b/daemon/execdriver/execdrivers/execdrivers.go index b7dd98cf3..a665985d1 100644 --- a/daemon/execdriver/execdrivers/execdrivers.go +++ b/daemon/execdriver/execdrivers/execdrivers.go @@ -24,7 +24,7 @@ func NewDriver(name, root, initPath string, sysInfo *sysinfo.SysInfo) (execdrive // to be backwards compatible return lxc.NewDriver(root, initPath, sysInfo.AppArmor) case "native": - return native.NewDriver(path.Join(root, "execdriver", "native"), initPath, meminfo.MemTotal/1000) + return native.NewDriver(path.Join(root, "execdriver", "native"), initPath, meminfo.MemTotal) } return nil, fmt.Errorf("unknown exec driver %s", name) } diff --git a/daemon/execdriver/lxc/driver.go b/daemon/execdriver/lxc/driver.go index 7dca19d76..44942b1fe 100644 --- a/daemon/execdriver/lxc/driver.go +++ b/daemon/execdriver/lxc/driver.go @@ -526,6 +526,6 @@ func (d *driver) Exec(c *execdriver.Command, processConfig *execdriver.ProcessCo } func (d *driver) Stats(id string) (*execdriver.ResourceStats, error) { - return nil, fmt.Errorf("container stats are not support with LXC") + return nil, fmt.Errorf("container stats are not supported with LXC") } diff --git a/daemon/execdriver/native/driver.go b/daemon/execdriver/native/driver.go index 83e07f392..450d7e5f3 100644 --- a/daemon/execdriver/native/driver.go +++ b/daemon/execdriver/native/driver.go @@ -284,6 +284,9 @@ func (d *driver) Stats(id string) (*execdriver.ResourceStats, error) { c := d.activeContainers[id] state, err := libcontainer.GetState(filepath.Join(d.root, id)) if err != nil { + if os.IsNotExist(err) { + return nil, execdriver.ErrNotRunning + } return nil, err } now := time.Now() @@ -292,13 +295,15 @@ func (d *driver) Stats(id string) (*execdriver.ResourceStats, error) { return nil, err } memoryLimit := c.container.Cgroups.Memory + // if the container does not have any memory limit specified set the + // limit to the machines memory if memoryLimit == 0 { memoryLimit = d.machineMemory } return &execdriver.ResourceStats{ + Read: now, ContainerStats: stats, ClockTicks: system.GetClockTicks(), - Read: now, MemoryLimit: memoryLimit, }, nil } diff --git a/daemon/start.go b/daemon/start.go index 150a87f57..363461080 100644 --- a/daemon/start.go +++ b/daemon/start.go @@ -1,14 +1,12 @@ package daemon import ( - "encoding/json" "fmt" "os" "strings" "github.com/docker/docker/engine" "github.com/docker/docker/runconfig" - "github.com/docker/docker/stats" ) func (daemon *Daemon) ContainerStart(job *engine.Job) engine.Status { @@ -79,26 +77,3 @@ func (daemon *Daemon) setHostConfig(container *Container, hostConfig *runconfig. return nil } - -func (daemon *Daemon) ContainerStats(job *engine.Job) engine.Status { - s, err := daemon.SubscribeToContainerStats(job.Args[0]) - if err != nil { - return job.Error(err) - } - enc := json.NewEncoder(job.Stdout) - for update := range s { - ss := stats.ToStats(update.ContainerStats) - ss.MemoryStats.Limit = uint64(update.MemoryLimit) - ss.Read = update.Read - ss.ClockTicks = update.ClockTicks - ss.CpuStats.SystemUsage = update.SystemUsage - if err := enc.Encode(ss); err != nil { - return job.Error(err) - } - } - return engine.StatusOK -} - -func mapToAPIStats() { - -} diff --git a/daemon/stats.go b/daemon/stats.go new file mode 100644 index 000000000..5db1cf608 --- /dev/null +++ b/daemon/stats.go @@ -0,0 +1,29 @@ +package daemon + +import ( + "encoding/json" + + "github.com/docker/docker/api/stats" + "github.com/docker/docker/engine" +) + +func (daemon *Daemon) ContainerStats(job *engine.Job) engine.Status { + s, err := daemon.SubscribeToContainerStats(job.Args[0]) + if err != nil { + return job.Error(err) + } + enc := json.NewEncoder(job.Stdout) + for update := range s { + ss := stats.ToStats(update.ContainerStats) + ss.MemoryStats.Limit = uint64(update.MemoryLimit) + ss.Read = update.Read + ss.ClockTicks = update.ClockTicks + ss.CpuStats.SystemUsage = update.SystemUsage + if err := enc.Encode(ss); err != nil { + // TODO: handle the specific broken pipe + daemon.UnsubscribeToContainerStats(job.Args[0], s) + return job.Error(err) + } + } + return engine.StatusOK +} diff --git a/daemon/stats_collector.go b/daemon/stats_collector.go index a21092a85..8b5662db1 100644 --- a/daemon/stats_collector.go +++ b/daemon/stats_collector.go @@ -13,16 +13,20 @@ import ( "github.com/docker/docker/daemon/execdriver" ) +// newStatsCollector returns a new statsCollector that collections +// network and cgroup stats for a registered container at the specified +// interval. The collector allows non-running containers to be added +// and will start processing stats when they are started. func newStatsCollector(interval time.Duration) *statsCollector { s := &statsCollector{ interval: interval, - containers: make(map[string]*statsCollectorData), + containers: make(map[string]*statsData), } s.start() return s } -type statsCollectorData struct { +type statsData struct { c *Container lastStats *execdriver.ResourceStats subs []chan *execdriver.ResourceStats @@ -32,43 +36,86 @@ type statsCollectorData struct { type statsCollector struct { m sync.Mutex interval time.Duration - containers map[string]*statsCollectorData + containers map[string]*statsData } -func (s *statsCollector) collect(c *Container) <-chan *execdriver.ResourceStats { +// collect registers the container with the collector and adds it to +// the event loop for collection on the specified interval returning +// a channel for the subscriber to receive on. +func (s *statsCollector) collect(c *Container) chan *execdriver.ResourceStats { s.m.Lock() + defer s.m.Unlock() ch := make(chan *execdriver.ResourceStats, 1024) - s.containers[c.ID] = &statsCollectorData{ + if _, exists := s.containers[c.ID]; exists { + s.containers[c.ID].subs = append(s.containers[c.ID].subs, ch) + return ch + } + s.containers[c.ID] = &statsData{ c: c, subs: []chan *execdriver.ResourceStats{ ch, }, } - s.m.Unlock() return ch } +// stopCollection closes the channels for all subscribers and removes +// the container from metrics collection. func (s *statsCollector) stopCollection(c *Container) { s.m.Lock() + defer s.m.Unlock() + d := s.containers[c.ID] + if d == nil { + return + } + for _, sub := range d.subs { + close(sub) + } delete(s.containers, c.ID) +} + +// unsubscribe removes a specific subscriber from receiving updates for a +// container's stats. +func (s *statsCollector) unsubscribe(c *Container, ch chan *execdriver.ResourceStats) { + s.m.Lock() + cd := s.containers[c.ID] + for i, sub := range cd.subs { + if ch == sub { + cd.subs = append(cd.subs[:i], cd.subs[i+1:]...) + close(ch) + } + } + // if there are no more subscribers then remove the entire container + // from collection. + if len(cd.subs) == 0 { + delete(s.containers, c.ID) + } s.m.Unlock() } func (s *statsCollector) start() { go func() { for _ = range time.Tick(s.interval) { - log.Debugf("starting collection of container stats") s.m.Lock() for id, d := range s.containers { - systemUsage, err := getSystemCpuUsage() + systemUsage, err := s.getSystemCpuUsage() if err != nil { log.Errorf("collecting system cpu usage for %s: %v", id, err) continue } stats, err := d.c.Stats() if err != nil { - // TODO: @crosbymichael evict container depending on error + if err == execdriver.ErrNotRunning { + continue + } + // if the error is not because the container is currently running then + // evict the container from the collector and close the channel for + // any subscribers currently waiting on changes. log.Errorf("collecting stats for %s: %v", id, err) + for _, sub := range s.containers[id].subs { + close(sub) + } + delete(s.containers, id) continue } stats.SystemUsage = systemUsage @@ -81,14 +128,14 @@ func (s *statsCollector) start() { }() } -// returns value in nanoseconds -func getSystemCpuUsage() (uint64, error) { +// getSystemdCpuUSage returns the host system's cpu usage +// in nanoseconds. +func (s *statsCollector) getSystemCpuUsage() (uint64, error) { f, err := os.Open("/proc/stat") if err != nil { return 0, err } defer f.Close() - sc := bufio.NewScanner(f) for sc.Scan() { parts := strings.Fields(sc.Text()) @@ -97,7 +144,6 @@ func getSystemCpuUsage() (uint64, error) { if len(parts) < 8 { return 0, fmt.Errorf("invalid number of cpu fields") } - var total uint64 for _, i := range parts[1:8] { v, err := strconv.ParseUint(i, 10, 64) diff --git a/docker/flags.go b/docker/flags.go index 719acbe93..8db636e5c 100644 --- a/docker/flags.go +++ b/docker/flags.go @@ -98,6 +98,7 @@ func init() { {"save", "Save an image to a tar archive"}, {"search", "Search for an image on the Docker Hub"}, {"start", "Start a stopped container"}, + {"stats", "Receive container stats"}, {"stop", "Stop a running container"}, {"tag", "Tag an image into a repository"}, {"top", "Lookup the running processes of a container"}, From cc658804c000b8f652750ccf3233a73cc6f03073 Mon Sep 17 00:00:00 2001 From: Alexander Morozov Date: Mon, 19 Jan 2015 13:20:58 -0800 Subject: [PATCH 368/513] Refactor cli for stats Signed-off-by: Alexander Morozov --- api/client/commands.go | 119 +++++++++++++++++++++-------------------- 1 file changed, 60 insertions(+), 59 deletions(-) diff --git a/api/client/commands.go b/api/client/commands.go index 34ca32c29..27dc62769 100644 --- a/api/client/commands.go +++ b/api/client/commands.go @@ -16,6 +16,7 @@ import ( "path" "path/filepath" "runtime" + "sort" "strconv" "strings" "sync" @@ -2629,56 +2630,12 @@ type containerStats struct { MemoryPercentage float64 NetworkRx float64 NetworkTx float64 + mu sync.RWMutex + err error } -func (cli *DockerCli) CmdStats(args ...string) error { - cmd := cli.Subcmd("stats", "CONTAINER", "Stream the stats of a container", true) - cmd.Require(flag.Min, 1) - utils.ParseFlags(cmd, args, true) - - m := &sync.Mutex{} - cStats := map[string]containerStats{} - for _, name := range cmd.Args() { - go cli.streamStats(name, cStats, m) - } - w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0) - for _ = range time.Tick(500 * time.Millisecond) { - fmt.Fprint(cli.out, "\033[2J") - fmt.Fprint(cli.out, "\033[H") - fmt.Fprintln(w, "CONTAINER\tCPU %\tMEM USAGE/LIMIT\tMEM %\tNET I/O") - m.Lock() - ss := sortStatsByName(cStats) - m.Unlock() - for _, s := range ss { - fmt.Fprintf(w, "%s\t%.2f%%\t%s/%s\t%.2f%%\t%s/%s\n", - s.Name, - s.CpuPercentage, - units.BytesSize(s.Memory), units.BytesSize(s.MemoryLimit), - s.MemoryPercentage, - units.BytesSize(s.NetworkRx), units.BytesSize(s.NetworkTx)) - } - w.Flush() - } - return nil -} - -func (cli *DockerCli) streamStats(name string, data map[string]containerStats, m *sync.Mutex) error { - m.Lock() - data[name] = containerStats{ - Name: name, - } - m.Unlock() - - stream, _, err := cli.call("GET", "/containers/"+name+"/stats", nil, false) - if err != nil { - return err - } - defer func() { - stream.Close() - m.Lock() - delete(data, name) - m.Unlock() - }() +func (s *containerStats) Collect(stream io.ReadCloser) { + defer stream.Close() var ( previousCpu uint64 previousSystem uint64 @@ -2688,7 +2645,10 @@ func (cli *DockerCli) streamStats(name string, data map[string]containerStats, m for { var v *stats.Stats if err := dec.Decode(&v); err != nil { - return err + s.mu.Lock() + s.err = err + s.mu.Unlock() + return } var ( memPercent = float64(v.MemoryStats.Usage) / float64(v.MemoryStats.Limit) * 100.0 @@ -2698,20 +2658,61 @@ func (cli *DockerCli) streamStats(name string, data map[string]containerStats, m cpuPercent = calcuateCpuPercent(previousCpu, previousSystem, v) } start = false - m.Lock() - d := data[name] - d.CpuPercentage = cpuPercent - d.Memory = float64(v.MemoryStats.Usage) - d.MemoryLimit = float64(v.MemoryStats.Limit) - d.MemoryPercentage = memPercent - d.NetworkRx = float64(v.Network.RxBytes) - d.NetworkTx = float64(v.Network.TxBytes) - data[name] = d - m.Unlock() + s.mu.Lock() + s.CpuPercentage = cpuPercent + s.Memory = float64(v.MemoryStats.Usage) + s.MemoryLimit = float64(v.MemoryStats.Limit) + s.MemoryPercentage = memPercent + s.NetworkRx = float64(v.Network.RxBytes) + s.NetworkTx = float64(v.Network.TxBytes) + s.mu.Unlock() previousCpu = v.CpuStats.CpuUsage.TotalUsage previousSystem = v.CpuStats.SystemUsage } +} + +func (s *containerStats) Display(w io.Writer) { + s.mu.RLock() + defer s.mu.RUnlock() + if s.err != nil { + return + } + fmt.Fprintf(w, "%s\t%.2f%%\t%s/%s\t%.2f%%\t%s/%s\n", + s.Name, + s.CpuPercentage, + units.BytesSize(s.Memory), units.BytesSize(s.MemoryLimit), + s.MemoryPercentage, + units.BytesSize(s.NetworkRx), units.BytesSize(s.NetworkTx)) +} + +func (cli *DockerCli) CmdStats(args ...string) error { + cmd := cli.Subcmd("stats", "CONTAINER", "Stream the stats of a container", true) + cmd.Require(flag.Min, 1) + utils.ParseFlags(cmd, args, true) + + names := cmd.Args() + sort.Strings(names) + var cStats []*containerStats + for _, n := range names { + s := &containerStats{Name: n} + cStats = append(cStats, s) + stream, _, err := cli.call("GET", "/containers/"+n+"/stats", nil, false) + if err != nil { + return err + } + go s.Collect(stream) + } + w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0) + for _ = range time.Tick(500 * time.Millisecond) { + fmt.Fprint(cli.out, "\033[2J") + fmt.Fprint(cli.out, "\033[H") + fmt.Fprintln(w, "CONTAINER\tCPU %\tMEM USAGE/LIMIT\tMEM %\tNET I/O") + for _, s := range cStats { + s.Display(w) + } + w.Flush() + } return nil } From 2d4fc1de0560c8052b4480035bb364fb28525b39 Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Mon, 19 Jan 2015 14:07:21 -0800 Subject: [PATCH 369/513] Refactor usage calc for CPU and system usage Signed-off-by: Michael Crosby --- api/client/commands.go | 15 ++++++---- api/client/sort.go | 29 -------------------- api/stats/stats.go | 2 -- daemon/execdriver/driver.go | 1 - daemon/execdriver/execdrivers/execdrivers.go | 8 +----- daemon/execdriver/native/driver.go | 11 ++++++-- daemon/stats.go | 1 - daemon/stats_collector.go | 19 +++++++------ 8 files changed, 30 insertions(+), 56 deletions(-) delete mode 100644 api/client/sort.go diff --git a/api/client/commands.go b/api/client/commands.go index 27dc62769..07554ed60 100644 --- a/api/client/commands.go +++ b/api/client/commands.go @@ -2717,11 +2717,16 @@ func (cli *DockerCli) CmdStats(args ...string) error { } func calcuateCpuPercent(previousCpu, previousSystem uint64, v *stats.Stats) float64 { - cpuPercent := 0.0 - cpuDelta := float64(v.CpuStats.CpuUsage.TotalUsage) - float64(previousCpu) - systemDelta := float64(int(v.CpuStats.SystemUsage)/v.ClockTicks) - float64(int(previousSystem)/v.ClockTicks) - if systemDelta > 0.0 { - cpuPercent = (cpuDelta / systemDelta) * float64(v.ClockTicks*len(v.CpuStats.CpuUsage.PercpuUsage)) + var ( + cpuPercent = 0.0 + // calculate the change for the cpu usage of the container in between readings + cpuDelta = float64(v.CpuStats.CpuUsage.TotalUsage - previousCpu) + // calculate the change for the entire system between readings + systemDelta = float64(v.CpuStats.SystemUsage - previousSystem) + ) + + if systemDelta > 0.0 && cpuDelta > 0.0 { + cpuPercent = (cpuDelta / systemDelta) * float64(len(v.CpuStats.CpuUsage.PercpuUsage)) * 100.0 } return cpuPercent } diff --git a/api/client/sort.go b/api/client/sort.go deleted file mode 100644 index 1b8232c3f..000000000 --- a/api/client/sort.go +++ /dev/null @@ -1,29 +0,0 @@ -package client - -import "sort" - -func sortStatsByName(cStats map[string]containerStats) []containerStats { - sStats := []containerStats{} - for _, s := range cStats { - sStats = append(sStats, s) - } - sorter := &statSorter{sStats} - sort.Sort(sorter) - return sStats -} - -type statSorter struct { - stats []containerStats -} - -func (s *statSorter) Len() int { - return len(s.stats) -} - -func (s *statSorter) Swap(i, j int) { - s.stats[i], s.stats[j] = s.stats[j], s.stats[i] -} - -func (s *statSorter) Less(i, j int) bool { - return s.stats[i].Name < s.stats[j].Name -} diff --git a/api/stats/stats.go b/api/stats/stats.go index b2820f243..43146cf7b 100644 --- a/api/stats/stats.go +++ b/api/stats/stats.go @@ -83,8 +83,6 @@ type Network struct { type Stats struct { Read time.Time `json:"read"` - ClockTicks int `json:"clock_ticks"` - Interval int `json:"interval"` // in ms Network Network `json:"network,omitempty"` CpuStats CpuStats `json:"cpu_stats,omitempty"` MemoryStats MemoryStats `json:"memory_stats,omitempty"` diff --git a/daemon/execdriver/driver.go b/daemon/execdriver/driver.go index f6e0ac728..2215d03cf 100644 --- a/daemon/execdriver/driver.go +++ b/daemon/execdriver/driver.go @@ -107,7 +107,6 @@ type Resources struct { type ResourceStats struct { *libcontainer.ContainerStats Read time.Time `json:"read"` - ClockTicks int `json:"clock_ticks"` MemoryLimit int64 `json:"memory_limit"` SystemUsage uint64 `json:"system_usage"` } diff --git a/daemon/execdriver/execdrivers/execdrivers.go b/daemon/execdriver/execdrivers/execdrivers.go index a665985d1..be3222a8b 100644 --- a/daemon/execdriver/execdrivers/execdrivers.go +++ b/daemon/execdriver/execdrivers/execdrivers.go @@ -8,15 +8,9 @@ import ( "github.com/docker/docker/daemon/execdriver/lxc" "github.com/docker/docker/daemon/execdriver/native" "github.com/docker/docker/pkg/sysinfo" - "github.com/docker/docker/pkg/system" ) func NewDriver(name, root, initPath string, sysInfo *sysinfo.SysInfo) (execdriver.Driver, error) { - meminfo, err := system.ReadMemInfo() - if err != nil { - return nil, err - } - switch name { case "lxc": // we want to give the lxc driver the full docker root because it needs @@ -24,7 +18,7 @@ func NewDriver(name, root, initPath string, sysInfo *sysinfo.SysInfo) (execdrive // to be backwards compatible return lxc.NewDriver(root, initPath, sysInfo.AppArmor) case "native": - return native.NewDriver(path.Join(root, "execdriver", "native"), initPath, meminfo.MemTotal) + return native.NewDriver(path.Join(root, "execdriver", "native"), initPath) } return nil, fmt.Errorf("unknown exec driver %s", name) } diff --git a/daemon/execdriver/native/driver.go b/daemon/execdriver/native/driver.go index 450d7e5f3..533e6d61e 100644 --- a/daemon/execdriver/native/driver.go +++ b/daemon/execdriver/native/driver.go @@ -17,6 +17,7 @@ import ( log "github.com/Sirupsen/logrus" "github.com/docker/docker/daemon/execdriver" + sysinfo "github.com/docker/docker/pkg/system" "github.com/docker/docker/pkg/term" "github.com/docker/libcontainer" "github.com/docker/libcontainer/apparmor" @@ -46,7 +47,12 @@ type driver struct { sync.Mutex } -func NewDriver(root, initPath string, machineMemory int64) (*driver, error) { +func NewDriver(root, initPath string) (*driver, error) { + meminfo, err := sysinfo.ReadMemInfo() + if err != nil { + return nil, err + } + if err := os.MkdirAll(root, 0700); err != nil { return nil, err } @@ -58,7 +64,7 @@ func NewDriver(root, initPath string, machineMemory int64) (*driver, error) { root: root, initPath: initPath, activeContainers: make(map[string]*activeContainer), - machineMemory: machineMemory, + machineMemory: meminfo.MemTotal, }, nil } @@ -303,7 +309,6 @@ func (d *driver) Stats(id string) (*execdriver.ResourceStats, error) { return &execdriver.ResourceStats{ Read: now, ContainerStats: stats, - ClockTicks: system.GetClockTicks(), MemoryLimit: memoryLimit, }, nil } diff --git a/daemon/stats.go b/daemon/stats.go index 5db1cf608..22e7584ac 100644 --- a/daemon/stats.go +++ b/daemon/stats.go @@ -17,7 +17,6 @@ func (daemon *Daemon) ContainerStats(job *engine.Job) engine.Status { ss := stats.ToStats(update.ContainerStats) ss.MemoryStats.Limit = uint64(update.MemoryLimit) ss.Read = update.Read - ss.ClockTicks = update.ClockTicks ss.CpuStats.SystemUsage = update.SystemUsage if err := enc.Encode(ss); err != nil { // TODO: handle the specific broken pipe diff --git a/daemon/stats_collector.go b/daemon/stats_collector.go index 8b5662db1..0fa1b4cae 100644 --- a/daemon/stats_collector.go +++ b/daemon/stats_collector.go @@ -11,6 +11,7 @@ import ( log "github.com/Sirupsen/logrus" "github.com/docker/docker/daemon/execdriver" + "github.com/docker/libcontainer/system" ) // newStatsCollector returns a new statsCollector that collections @@ -21,6 +22,7 @@ func newStatsCollector(interval time.Duration) *statsCollector { s := &statsCollector{ interval: interval, containers: make(map[string]*statsData), + clockTicks: uint64(system.GetClockTicks()), } s.start() return s @@ -36,6 +38,7 @@ type statsData struct { type statsCollector struct { m sync.Mutex interval time.Duration + clockTicks uint64 containers map[string]*statsData } @@ -128,8 +131,10 @@ func (s *statsCollector) start() { }() } -// getSystemdCpuUSage returns the host system's cpu usage -// in nanoseconds. +const nanoSeconds = 1e9 + +// getSystemdCpuUSage returns the host system's cpu usage in nanoseconds +// for the system to match the cgroup readings are returned in the same format. func (s *statsCollector) getSystemCpuUsage() (uint64, error) { f, err := os.Open("/proc/stat") if err != nil { @@ -144,17 +149,15 @@ func (s *statsCollector) getSystemCpuUsage() (uint64, error) { if len(parts) < 8 { return 0, fmt.Errorf("invalid number of cpu fields") } - var total uint64 + var sum uint64 for _, i := range parts[1:8] { v, err := strconv.ParseUint(i, 10, 64) if err != nil { - return 0.0, fmt.Errorf("Unable to convert value %s to int: %s", i, err) + return 0, fmt.Errorf("Unable to convert value %s to int: %s", i, err) } - total += v + sum += v } - return total * 1000000000, nil - default: - continue + return (sum * nanoSeconds) / s.clockTicks, nil } } return 0, fmt.Errorf("invalid stat format") From 2f46b7601a3f5e11359b79624d73075b69778fbb Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Mon, 19 Jan 2015 15:29:42 -0800 Subject: [PATCH 370/513] Add pubsub package to handle robust publisher Signed-off-by: Michael Crosby --- api/stats/stats.go | 75 +--------------------- daemon/daemon.go | 4 +- daemon/stats.go | 78 +++++++++++++++++++++-- daemon/stats_collector.go | 118 ++++++++++++----------------------- pkg/pubsub/publisher.go | 66 ++++++++++++++++++++ pkg/pubsub/publisher_test.go | 63 +++++++++++++++++++ 6 files changed, 248 insertions(+), 156 deletions(-) create mode 100644 pkg/pubsub/publisher.go create mode 100644 pkg/pubsub/publisher_test.go diff --git a/api/stats/stats.go b/api/stats/stats.go index 43146cf7b..d58fdd4f5 100644 --- a/api/stats/stats.go +++ b/api/stats/stats.go @@ -1,11 +1,8 @@ +// This package is used for API stability in the types and response to the +// consumers of the API stats endpoint. package stats -import ( - "time" - - "github.com/docker/libcontainer" - "github.com/docker/libcontainer/cgroups" -) +import "time" type ThrottlingData struct { // Number of periods with throttling active @@ -88,69 +85,3 @@ type Stats struct { MemoryStats MemoryStats `json:"memory_stats,omitempty"` BlkioStats BlkioStats `json:"blkio_stats,omitempty"` } - -// ToStats converts the libcontainer.ContainerStats to the api specific -// structs. This is done to preserve API compatibility and versioning. -func ToStats(ls *libcontainer.ContainerStats) *Stats { - s := &Stats{} - if ls.NetworkStats != nil { - s.Network = Network{ - RxBytes: ls.NetworkStats.RxBytes, - RxPackets: ls.NetworkStats.RxPackets, - RxErrors: ls.NetworkStats.RxErrors, - RxDropped: ls.NetworkStats.RxDropped, - TxBytes: ls.NetworkStats.TxBytes, - TxPackets: ls.NetworkStats.TxPackets, - TxErrors: ls.NetworkStats.TxErrors, - TxDropped: ls.NetworkStats.TxDropped, - } - } - cs := ls.CgroupStats - if cs != nil { - s.BlkioStats = BlkioStats{ - IoServiceBytesRecursive: copyBlkioEntry(cs.BlkioStats.IoServiceBytesRecursive), - IoServicedRecursive: copyBlkioEntry(cs.BlkioStats.IoServicedRecursive), - IoQueuedRecursive: copyBlkioEntry(cs.BlkioStats.IoQueuedRecursive), - IoServiceTimeRecursive: copyBlkioEntry(cs.BlkioStats.IoServiceTimeRecursive), - IoWaitTimeRecursive: copyBlkioEntry(cs.BlkioStats.IoWaitTimeRecursive), - IoMergedRecursive: copyBlkioEntry(cs.BlkioStats.IoMergedRecursive), - IoTimeRecursive: copyBlkioEntry(cs.BlkioStats.IoTimeRecursive), - SectorsRecursive: copyBlkioEntry(cs.BlkioStats.SectorsRecursive), - } - cpu := cs.CpuStats - s.CpuStats = CpuStats{ - CpuUsage: CpuUsage{ - TotalUsage: cpu.CpuUsage.TotalUsage, - PercpuUsage: cpu.CpuUsage.PercpuUsage, - UsageInKernelmode: cpu.CpuUsage.UsageInKernelmode, - UsageInUsermode: cpu.CpuUsage.UsageInUsermode, - }, - ThrottlingData: ThrottlingData{ - Periods: cpu.ThrottlingData.Periods, - ThrottledPeriods: cpu.ThrottlingData.ThrottledPeriods, - ThrottledTime: cpu.ThrottlingData.ThrottledTime, - }, - } - mem := cs.MemoryStats - s.MemoryStats = MemoryStats{ - Usage: mem.Usage, - MaxUsage: mem.MaxUsage, - Stats: mem.Stats, - Failcnt: mem.Failcnt, - } - } - return s -} - -func copyBlkioEntry(entries []cgroups.BlkioStatEntry) []BlkioStatEntry { - out := make([]BlkioStatEntry, len(entries)) - for i, re := range entries { - out[i] = BlkioStatEntry{ - Major: re.Major, - Minor: re.Minor, - Op: re.Op, - Value: re.Value, - } - } - return out -} diff --git a/daemon/daemon.go b/daemon/daemon.go index 82d2bc757..c03e9d7aa 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -1099,7 +1099,7 @@ func (daemon *Daemon) Stats(c *Container) (*execdriver.ResourceStats, error) { return daemon.execDriver.Stats(c.ID) } -func (daemon *Daemon) SubscribeToContainerStats(name string) (chan *execdriver.ResourceStats, error) { +func (daemon *Daemon) SubscribeToContainerStats(name string) (chan interface{}, error) { c := daemon.Get(name) if c == nil { return nil, fmt.Errorf("no such container") @@ -1108,7 +1108,7 @@ func (daemon *Daemon) SubscribeToContainerStats(name string) (chan *execdriver.R return ch, nil } -func (daemon *Daemon) UnsubscribeToContainerStats(name string, ch chan *execdriver.ResourceStats) error { +func (daemon *Daemon) UnsubscribeToContainerStats(name string, ch chan interface{}) error { c := daemon.Get(name) if c == nil { return fmt.Errorf("no such container") diff --git a/daemon/stats.go b/daemon/stats.go index 22e7584ac..e047497ec 100644 --- a/daemon/stats.go +++ b/daemon/stats.go @@ -4,25 +4,95 @@ import ( "encoding/json" "github.com/docker/docker/api/stats" + "github.com/docker/docker/daemon/execdriver" "github.com/docker/docker/engine" + "github.com/docker/libcontainer" + "github.com/docker/libcontainer/cgroups" ) func (daemon *Daemon) ContainerStats(job *engine.Job) engine.Status { - s, err := daemon.SubscribeToContainerStats(job.Args[0]) + updates, err := daemon.SubscribeToContainerStats(job.Args[0]) if err != nil { return job.Error(err) } enc := json.NewEncoder(job.Stdout) - for update := range s { - ss := stats.ToStats(update.ContainerStats) + for v := range updates { + update := v.(*execdriver.ResourceStats) + ss := convertToAPITypes(update.ContainerStats) ss.MemoryStats.Limit = uint64(update.MemoryLimit) ss.Read = update.Read ss.CpuStats.SystemUsage = update.SystemUsage if err := enc.Encode(ss); err != nil { // TODO: handle the specific broken pipe - daemon.UnsubscribeToContainerStats(job.Args[0], s) + daemon.UnsubscribeToContainerStats(job.Args[0], updates) return job.Error(err) } } return engine.StatusOK } + +// convertToAPITypes converts the libcontainer.ContainerStats to the api specific +// structs. This is done to preserve API compatibility and versioning. +func convertToAPITypes(ls *libcontainer.ContainerStats) *stats.Stats { + s := &stats.Stats{} + if ls.NetworkStats != nil { + s.Network = stats.Network{ + RxBytes: ls.NetworkStats.RxBytes, + RxPackets: ls.NetworkStats.RxPackets, + RxErrors: ls.NetworkStats.RxErrors, + RxDropped: ls.NetworkStats.RxDropped, + TxBytes: ls.NetworkStats.TxBytes, + TxPackets: ls.NetworkStats.TxPackets, + TxErrors: ls.NetworkStats.TxErrors, + TxDropped: ls.NetworkStats.TxDropped, + } + } + cs := ls.CgroupStats + if cs != nil { + s.BlkioStats = stats.BlkioStats{ + IoServiceBytesRecursive: copyBlkioEntry(cs.BlkioStats.IoServiceBytesRecursive), + IoServicedRecursive: copyBlkioEntry(cs.BlkioStats.IoServicedRecursive), + IoQueuedRecursive: copyBlkioEntry(cs.BlkioStats.IoQueuedRecursive), + IoServiceTimeRecursive: copyBlkioEntry(cs.BlkioStats.IoServiceTimeRecursive), + IoWaitTimeRecursive: copyBlkioEntry(cs.BlkioStats.IoWaitTimeRecursive), + IoMergedRecursive: copyBlkioEntry(cs.BlkioStats.IoMergedRecursive), + IoTimeRecursive: copyBlkioEntry(cs.BlkioStats.IoTimeRecursive), + SectorsRecursive: copyBlkioEntry(cs.BlkioStats.SectorsRecursive), + } + cpu := cs.CpuStats + s.CpuStats = stats.CpuStats{ + CpuUsage: stats.CpuUsage{ + TotalUsage: cpu.CpuUsage.TotalUsage, + PercpuUsage: cpu.CpuUsage.PercpuUsage, + UsageInKernelmode: cpu.CpuUsage.UsageInKernelmode, + UsageInUsermode: cpu.CpuUsage.UsageInUsermode, + }, + ThrottlingData: stats.ThrottlingData{ + Periods: cpu.ThrottlingData.Periods, + ThrottledPeriods: cpu.ThrottlingData.ThrottledPeriods, + ThrottledTime: cpu.ThrottlingData.ThrottledTime, + }, + } + mem := cs.MemoryStats + s.MemoryStats = stats.MemoryStats{ + Usage: mem.Usage, + MaxUsage: mem.MaxUsage, + Stats: mem.Stats, + Failcnt: mem.Failcnt, + } + } + return s +} + +func copyBlkioEntry(entries []cgroups.BlkioStatEntry) []stats.BlkioStatEntry { + out := make([]stats.BlkioStatEntry, len(entries)) + for i, re := range entries { + out[i] = stats.BlkioStatEntry{ + Major: re.Major, + Minor: re.Minor, + Op: re.Op, + Value: re.Value, + } + } + return out +} diff --git a/daemon/stats_collector.go b/daemon/stats_collector.go index 0fa1b4cae..fe0a1f763 100644 --- a/daemon/stats_collector.go +++ b/daemon/stats_collector.go @@ -11,6 +11,7 @@ import ( log "github.com/Sirupsen/logrus" "github.com/docker/docker/daemon/execdriver" + "github.com/docker/docker/pkg/pubsub" "github.com/docker/libcontainer/system" ) @@ -21,114 +22,75 @@ import ( func newStatsCollector(interval time.Duration) *statsCollector { s := &statsCollector{ interval: interval, - containers: make(map[string]*statsData), + publishers: make(map[*Container]*pubsub.Publisher), clockTicks: uint64(system.GetClockTicks()), } - s.start() + go s.run() return s } -type statsData struct { - c *Container - lastStats *execdriver.ResourceStats - subs []chan *execdriver.ResourceStats -} - // statsCollector manages and provides container resource stats type statsCollector struct { m sync.Mutex interval time.Duration clockTicks uint64 - containers map[string]*statsData + publishers map[*Container]*pubsub.Publisher } // collect registers the container with the collector and adds it to // the event loop for collection on the specified interval returning // a channel for the subscriber to receive on. -func (s *statsCollector) collect(c *Container) chan *execdriver.ResourceStats { +func (s *statsCollector) collect(c *Container) chan interface{} { s.m.Lock() defer s.m.Unlock() - ch := make(chan *execdriver.ResourceStats, 1024) - if _, exists := s.containers[c.ID]; exists { - s.containers[c.ID].subs = append(s.containers[c.ID].subs, ch) - return ch + publisher, exists := s.publishers[c] + if !exists { + publisher = pubsub.NewPublisher(100*time.Millisecond, 1024) + s.publishers[c] = publisher } - s.containers[c.ID] = &statsData{ - c: c, - subs: []chan *execdriver.ResourceStats{ - ch, - }, - } - return ch + return publisher.Subscribe() } // stopCollection closes the channels for all subscribers and removes // the container from metrics collection. func (s *statsCollector) stopCollection(c *Container) { s.m.Lock() - defer s.m.Unlock() - d := s.containers[c.ID] - if d == nil { - return - } - for _, sub := range d.subs { - close(sub) - } - delete(s.containers, c.ID) -} - -// unsubscribe removes a specific subscriber from receiving updates for a -// container's stats. -func (s *statsCollector) unsubscribe(c *Container, ch chan *execdriver.ResourceStats) { - s.m.Lock() - cd := s.containers[c.ID] - for i, sub := range cd.subs { - if ch == sub { - cd.subs = append(cd.subs[:i], cd.subs[i+1:]...) - close(ch) - } - } - // if there are no more subscribers then remove the entire container - // from collection. - if len(cd.subs) == 0 { - delete(s.containers, c.ID) + if publisher, exists := s.publishers[c]; exists { + publisher.Close() + delete(s.publishers, c) } s.m.Unlock() } -func (s *statsCollector) start() { - go func() { - for _ = range time.Tick(s.interval) { - s.m.Lock() - for id, d := range s.containers { - systemUsage, err := s.getSystemCpuUsage() - if err != nil { - log.Errorf("collecting system cpu usage for %s: %v", id, err) - continue - } - stats, err := d.c.Stats() - if err != nil { - if err == execdriver.ErrNotRunning { - continue - } - // if the error is not because the container is currently running then - // evict the container from the collector and close the channel for - // any subscribers currently waiting on changes. - log.Errorf("collecting stats for %s: %v", id, err) - for _, sub := range s.containers[id].subs { - close(sub) - } - delete(s.containers, id) - continue - } - stats.SystemUsage = systemUsage - for _, sub := range s.containers[id].subs { - sub <- stats - } +// unsubscribe removes a specific subscriber from receiving updates for a container's stats. +func (s *statsCollector) unsubscribe(c *Container, ch chan interface{}) { + s.m.Lock() + publisher := s.publishers[c] + if publisher != nil { + publisher.Evict(ch) + } + s.m.Unlock() +} + +func (s *statsCollector) run() { + for _ = range time.Tick(s.interval) { + for container, publisher := range s.publishers { + systemUsage, err := s.getSystemCpuUsage() + if err != nil { + log.Errorf("collecting system cpu usage for %s: %v", container.ID, err) + continue } - s.m.Unlock() + stats, err := container.Stats() + if err != nil { + if err != execdriver.ErrNotRunning { + log.Errorf("collecting stats for %s: %v", container.ID, err) + } + continue + } + stats.SystemUsage = systemUsage + publisher.Publish(stats) } - }() + } } const nanoSeconds = 1e9 diff --git a/pkg/pubsub/publisher.go b/pkg/pubsub/publisher.go new file mode 100644 index 000000000..98d035687 --- /dev/null +++ b/pkg/pubsub/publisher.go @@ -0,0 +1,66 @@ +package pubsub + +import ( + "sync" + "time" +) + +// NewPublisher creates a new pub/sub publisher to broadcast messages. +// The duration is used as the send timeout as to not block the publisher publishing +// messages to other clients if one client is slow or unresponsive. +// The buffer is used when creating new channels for subscribers. +func NewPublisher(publishTimeout time.Duration, buffer int) *Publisher { + return &Publisher{ + buffer: buffer, + timeout: publishTimeout, + subscribers: make(map[subscriber]struct{}), + } +} + +type subscriber chan interface{} + +type Publisher struct { + m sync.RWMutex + buffer int + timeout time.Duration + subscribers map[subscriber]struct{} +} + +// Subscribe adds a new subscriber to the publisher returning the channel. +func (p *Publisher) Subscribe() chan interface{} { + ch := make(chan interface{}, p.buffer) + p.m.Lock() + p.subscribers[ch] = struct{}{} + p.m.Unlock() + return ch +} + +// Evict removes the specified subscriber from receiving any more messages. +func (p *Publisher) Evict(sub chan interface{}) { + p.m.Lock() + delete(p.subscribers, sub) + close(sub) + p.m.Unlock() +} + +// Publish sends the data in v to all subscribers currently registered with the publisher. +func (p *Publisher) Publish(v interface{}) { + p.m.RLock() + for sub := range p.subscribers { + // send under a select as to not block if the receiver is unavailable + select { + case sub <- v: + case <-time.After(p.timeout): + } + } + p.m.RUnlock() +} + +// Close closes the channels to all subscribers registered with the publisher. +func (p *Publisher) Close() { + p.m.Lock() + for sub := range p.subscribers { + close(sub) + } + p.m.Unlock() +} diff --git a/pkg/pubsub/publisher_test.go b/pkg/pubsub/publisher_test.go new file mode 100644 index 000000000..c19059a8a --- /dev/null +++ b/pkg/pubsub/publisher_test.go @@ -0,0 +1,63 @@ +package pubsub + +import ( + "testing" + "time" +) + +func TestSendToOneSub(t *testing.T) { + p := NewPublisher(100*time.Millisecond, 10) + c := p.Subscribe() + + p.Publish("hi") + + msg := <-c + if msg.(string) != "hi" { + t.Fatalf("expected message hi but received %v", msg) + } +} + +func TestSendToMultipleSubs(t *testing.T) { + p := NewPublisher(100*time.Millisecond, 10) + subs := []chan interface{}{} + subs = append(subs, p.Subscribe(), p.Subscribe(), p.Subscribe()) + + p.Publish("hi") + + for _, c := range subs { + msg := <-c + if msg.(string) != "hi" { + t.Fatalf("expected message hi but received %v", msg) + } + } +} + +func TestEvictOneSub(t *testing.T) { + p := NewPublisher(100*time.Millisecond, 10) + s1 := p.Subscribe() + s2 := p.Subscribe() + + p.Evict(s1) + p.Publish("hi") + if _, ok := <-s1; ok { + t.Fatal("expected s1 to not receive the published message") + } + + msg := <-s2 + if msg.(string) != "hi" { + t.Fatalf("expected message hi but received %v", msg) + } +} + +func TestClosePublisher(t *testing.T) { + p := NewPublisher(100*time.Millisecond, 10) + subs := []chan interface{}{} + subs = append(subs, p.Subscribe(), p.Subscribe(), p.Subscribe()) + p.Close() + + for _, c := range subs { + if _, ok := <-c; ok { + t.Fatal("expected all subscriber channels to be closed") + } + } +} From 76141a00779880368b15ef2a5ffd28a80e4637df Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Mon, 19 Jan 2015 16:10:26 -0800 Subject: [PATCH 371/513] Add documentation for stats feature Signed-off-by: Michael Crosby --- api/client/commands.go | 2 +- docker/flags.go | 2 +- docs/man/docker-stats.1.md | 32 +++++++ .../reference/api/docker_remote_api.md | 6 ++ .../reference/api/docker_remote_api_v1.17.md | 88 +++++++++++++++++++ docs/sources/reference/commandline/cli.md | 24 ++++- integration-cli/docker_api_containers_test.go | 30 +++++++ 7 files changed, 180 insertions(+), 4 deletions(-) create mode 100644 docs/man/docker-stats.1.md diff --git a/api/client/commands.go b/api/client/commands.go index 07554ed60..40de033d4 100644 --- a/api/client/commands.go +++ b/api/client/commands.go @@ -2687,7 +2687,7 @@ func (s *containerStats) Display(w io.Writer) { } func (cli *DockerCli) CmdStats(args ...string) error { - cmd := cli.Subcmd("stats", "CONTAINER", "Stream the stats of a container", true) + cmd := cli.Subcmd("stats", "CONTAINER", "Display live container stats based on resource usage", true) cmd.Require(flag.Min, 1) utils.ParseFlags(cmd, args, true) diff --git a/docker/flags.go b/docker/flags.go index 8db636e5c..525e8cbfd 100644 --- a/docker/flags.go +++ b/docker/flags.go @@ -98,7 +98,7 @@ func init() { {"save", "Save an image to a tar archive"}, {"search", "Search for an image on the Docker Hub"}, {"start", "Start a stopped container"}, - {"stats", "Receive container stats"}, + {"stats", "Display live container stats based on resource usage"}, {"stop", "Stop a running container"}, {"tag", "Tag an image into a repository"}, {"top", "Lookup the running processes of a container"}, diff --git a/docs/man/docker-stats.1.md b/docs/man/docker-stats.1.md new file mode 100644 index 000000000..991b3d9f1 --- /dev/null +++ b/docs/man/docker-stats.1.md @@ -0,0 +1,32 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% JUNE 2014 +# NAME +docker-stats - Display live container stats based on resource usage. + +# SYNOPSIS +**docker top** +[**--help**] +[CONTAINERS] + +# DESCRIPTION + +Display live container stats based on resource usage. + +# OPTIONS +**--help** + Print usage statement + +# EXAMPLES + +Run **docker stats** with multiple containers. + + $ sudo docker stats redis1 redis2 + CONTAINER CPU % MEM USAGE/LIMIT MEM % NET I/O + redis1 0.07% 796 KiB/64 MiB 1.21% 788 B/648 B + redis2 0.07% 2.746 MiB/64 MiB 4.29% 1.266 KiB/648 B + +# HISTORY +April 2014, Originally compiled by William Henry (whenry at redhat dot com) +based on docker.com source material and internal work. +June 2014, updated by Sven Dowideit diff --git a/docs/sources/reference/api/docker_remote_api.md b/docs/sources/reference/api/docker_remote_api.md index a36133795..2d52d53b8 100644 --- a/docs/sources/reference/api/docker_remote_api.md +++ b/docs/sources/reference/api/docker_remote_api.md @@ -68,6 +68,12 @@ New endpoint to rename a container `id` to a new name. (`ReadonlyRootfs`) can be passed in the host config to mount the container's root filesystem as read only. +`GET /containers/(id)/stats` + +**New!** +This endpoint returns a stream of container stats based on resource usage. + + ## v1.16 ### Full Documentation diff --git a/docs/sources/reference/api/docker_remote_api_v1.17.md b/docs/sources/reference/api/docker_remote_api_v1.17.md index a44dcbf3a..77e9a8713 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.17.md +++ b/docs/sources/reference/api/docker_remote_api_v1.17.md @@ -514,6 +514,94 @@ Status Codes: - **404** – no such container - **500** – server error +### Get container stats based on resource usage + +`GET /containers/(id)/stats` + +Returns a stream of json objects of the container's stats + +**Example request**: + + GET /containers/redis1/stats HTTP/1.1 + +**Example response**: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "read" : "2015-01-08T22:57:31.547920715Z", + "network" : { + "rx_dropped" : 0, + "rx_bytes" : 648, + "rx_errors" : 0, + "tx_packets" : 8, + "tx_dropped" : 0, + "rx_packets" : 8, + "tx_errors" : 0, + "tx_bytes" : 648 + }, + "memory_stats" : { + "stats" : { + "total_pgmajfault" : 0, + "cache" : 0, + "mapped_file" : 0, + "total_inactive_file" : 0, + "pgpgout" : 414, + "rss" : 6537216, + "total_mapped_file" : 0, + "writeback" : 0, + "unevictable" : 0, + "pgpgin" : 477, + "total_unevictable" : 0, + "pgmajfault" : 0, + "total_rss" : 6537216, + "total_rss_huge" : 6291456, + "total_writeback" : 0, + "total_inactive_anon" : 0, + "rss_huge" : 6291456, + "hierarchical_memory_limit" : 67108864, + "total_pgfault" : 964, + "total_active_file" : 0, + "active_anon" : 6537216, + "total_active_anon" : 6537216, + "total_pgpgout" : 414, + "total_cache" : 0, + "inactive_anon" : 0, + "active_file" : 0, + "pgfault" : 964, + "inactive_file" : 0, + "total_pgpgin" : 477 + }, + "max_usage" : 6651904, + "usage" : 6537216, + "failcnt" : 0, + "limit" : 67108864 + }, + "blkio_stats" : {}, + "cpu_stats" : { + "cpu_usage" : { + "percpu_usage" : [ + 16970827, + 1839451, + 7107380, + 10571290 + ], + "usage_in_usermode" : 10000000, + "total_usage" : 36488948, + "usage_in_kernelmode" : 20000000 + }, + "system_cpu_usage" : 20091722000000000, + "throttling_data" : {} + } + } + +Status Codes: + +- **200** – no error +- **404** – no such container +- **500** – server error + ### Resize a container TTY `POST /containers/(id)/resize?h=&w=` diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index 36d0b18cc..b8af02da3 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -2001,8 +2001,28 @@ more details on finding shared images from the command line. -a, --attach=false Attach container's STDOUT and STDERR and forward all signals to the process -i, --interactive=false Attach container's STDIN -When run on a container that has already been started, -takes no action and succeeds unconditionally. +## stats + + Usage: docker stats [CONTAINERS] + + Display live container stats based on resource usage + + --help=false Print usage + +Running `docker stats` on two redis containers + + $ sudo docker stats redis1 redis2 + CONTAINER CPU % MEM USAGE/LIMIT MEM % NET I/O + redis1 0.07% 796 KiB/64 MiB 1.21% 788 B/648 B + redis2 0.07% 2.746 MiB/64 MiB 4.29% 1.266 KiB/648 B + + +When run on running containers live container stats will be streamed +back and displayed to the client. Stopped containers will not +receive any updates to their stats unless the container is started again. + +> **Note:** +> If you want more in depth resource usage for a container use the API endpoint ## stop diff --git a/integration-cli/docker_api_containers_test.go b/integration-cli/docker_api_containers_test.go index 8b0b8fd69..43ae8edde 100644 --- a/integration-cli/docker_api_containers_test.go +++ b/integration-cli/docker_api_containers_test.go @@ -9,7 +9,9 @@ import ( "os/exec" "strings" "testing" + "time" + "github.com/docker/docker/api/stats" "github.com/docker/docker/vendor/src/code.google.com/p/go/src/pkg/archive/tar" ) @@ -251,3 +253,31 @@ func TestVolumesFromHasPriority(t *testing.T) { logDone("container REST API - check VolumesFrom has priority") } + +func TestGetContainerStats(t *testing.T) { + defer deleteAllContainers() + name := "statscontainer" + + runCmd := exec.Command(dockerBinary, "run", "-d", "--name", name, "busybox", "top") + out, _, err := runCommandWithOutput(runCmd) + if err != nil { + t.Fatalf("Error on container creation: %v, output: %q", err, out) + } + go func() { + time.Sleep(4 * time.Second) + runCommand(exec.Command(dockerBinary, "kill", name)) + runCommand(exec.Command(dockerBinary, "rm", name)) + }() + + body, err := sockRequest("GET", "/containers/"+name+"/stats", nil) + if err != nil { + t.Fatalf("GET containers/stats sockRequest failed: %v", err) + } + + var s *stats.Stats + if err := json.Unmarshal(body, &s); err != nil { + t.Fatal(err) + } + + logDone("container REST API - check GET containers/stats") +} From 217a2bd1b62788e53fd38810b30672db58a4efc5 Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Tue, 20 Jan 2015 11:37:50 -0800 Subject: [PATCH 372/513] Remove publisher if no one is listening Signed-off-by: Michael Crosby --- daemon/stats_collector.go | 3 +++ integration-cli/docker_api_containers_test.go | 4 ++-- pkg/pubsub/publisher.go | 8 ++++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/daemon/stats_collector.go b/daemon/stats_collector.go index fe0a1f763..50ae6baf5 100644 --- a/daemon/stats_collector.go +++ b/daemon/stats_collector.go @@ -68,6 +68,9 @@ func (s *statsCollector) unsubscribe(c *Container, ch chan interface{}) { publisher := s.publishers[c] if publisher != nil { publisher.Evict(ch) + if publisher.Len() == 0 { + delete(s.publishers, c) + } } s.m.Unlock() } diff --git a/integration-cli/docker_api_containers_test.go b/integration-cli/docker_api_containers_test.go index 43ae8edde..34cc82aff 100644 --- a/integration-cli/docker_api_containers_test.go +++ b/integration-cli/docker_api_containers_test.go @@ -274,10 +274,10 @@ func TestGetContainerStats(t *testing.T) { t.Fatalf("GET containers/stats sockRequest failed: %v", err) } + dec := json.NewDecoder(bytes.NewBuffer(body)) var s *stats.Stats - if err := json.Unmarshal(body, &s); err != nil { + if err := dec.Decode(&s); err != nil { t.Fatal(err) } - logDone("container REST API - check GET containers/stats") } diff --git a/pkg/pubsub/publisher.go b/pkg/pubsub/publisher.go index 98d035687..f017262ae 100644 --- a/pkg/pubsub/publisher.go +++ b/pkg/pubsub/publisher.go @@ -26,6 +26,14 @@ type Publisher struct { subscribers map[subscriber]struct{} } +// Len returns the number of subscribers for the publisher +func (p *Publisher) Len() int { + p.m.RLock() + i := len(p.subscribers) + p.m.RUnlock() + return i +} + // Subscribe adds a new subscriber to the publisher returning the channel. func (p *Publisher) Subscribe() chan interface{} { ch := make(chan interface{}, p.buffer) From 6ef8057700b63e2c5fd5cec206915ef1f2088578 Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Fri, 16 Jan 2015 18:33:26 -0500 Subject: [PATCH 373/513] Fix racey TestAttachAfterDetach Signed-off-by: Brian Goff --- .../docker_cli_attach_unix_test.go | 43 ++++++++++++------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/integration-cli/docker_cli_attach_unix_test.go b/integration-cli/docker_cli_attach_unix_test.go index 3fb0ea896..829b14deb 100644 --- a/integration-cli/docker_cli_attach_unix_test.go +++ b/integration-cli/docker_cli_attach_unix_test.go @@ -97,29 +97,40 @@ func TestAttachAfterDetach(t *testing.T) { cmd.Stdout = tty cmd.Stderr = tty + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + + bytes := make([]byte, 10) + var nBytes int + readErr := make(chan error, 1) + go func() { - if err := cmd.Run(); err != nil { - t.Fatalf("attach returned error %s", err) - } - cpty.Close() // unblocks the reader in case of a failure + time.Sleep(500 * time.Millisecond) + cpty.Write([]byte("\n")) + time.Sleep(500 * time.Millisecond) + + nBytes, err = cpty.Read(bytes) + cpty.Close() + readErr <- err }() - time.Sleep(500 * time.Millisecond) - cpty.Write([]byte("\n")) - time.Sleep(500 * time.Millisecond) - bytes := make([]byte, 10) - - n, err := cpty.Read(bytes) - - if err != nil { - t.Fatalf("prompt read failed: %v", err) + select { + case err := <-readErr: + if err != nil { + t.Fatal(err) + } + case <-time.After(2 * time.Second): + t.Fatal("timeout waiting for attach read") } - if !strings.Contains(string(bytes[:n]), "/ #") { - t.Fatalf("failed to get a new prompt. got %s", string(bytes[:n])) + if err := cmd.Wait(); err != nil { + t.Fatal(err) } - cpty.Write([]byte("exit\n")) + if !strings.Contains(string(bytes[:nBytes]), "/ #") { + t.Fatalf("failed to get a new prompt. got %s", string(bytes[:nBytes])) + } logDone("attach - reconnect after detaching") } From 4b173199fde99a2b275421ed070b0ec004730e35 Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Tue, 20 Jan 2015 18:13:47 -0800 Subject: [PATCH 374/513] Exit cli when all containers when no more containers to monitor Signed-off-by: Michael Crosby --- api/client/commands.go | 18 ++++++++++++++---- daemon/stats_collector.go | 2 +- docs/man/docker-stats.1.md | 6 +----- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/api/client/commands.go b/api/client/commands.go index 40de033d4..f9cc10079 100644 --- a/api/client/commands.go +++ b/api/client/commands.go @@ -2672,11 +2672,11 @@ func (s *containerStats) Collect(stream io.ReadCloser) { } } -func (s *containerStats) Display(w io.Writer) { +func (s *containerStats) Display(w io.Writer) error { s.mu.RLock() defer s.mu.RUnlock() if s.err != nil { - return + return s.err } fmt.Fprintf(w, "%s\t%.2f%%\t%s/%s\t%.2f%%\t%s/%s\n", s.Name, @@ -2684,6 +2684,7 @@ func (s *containerStats) Display(w io.Writer) { units.BytesSize(s.Memory), units.BytesSize(s.MemoryLimit), s.MemoryPercentage, units.BytesSize(s.NetworkRx), units.BytesSize(s.NetworkTx)) + return nil } func (cli *DockerCli) CmdStats(args ...string) error { @@ -2708,8 +2709,17 @@ func (cli *DockerCli) CmdStats(args ...string) error { fmt.Fprint(cli.out, "\033[2J") fmt.Fprint(cli.out, "\033[H") fmt.Fprintln(w, "CONTAINER\tCPU %\tMEM USAGE/LIMIT\tMEM %\tNET I/O") - for _, s := range cStats { - s.Display(w) + toRemove := []int{} + for i, s := range cStats { + if err := s.Display(w); err != nil { + toRemove = append(toRemove, i) + } + } + for _, i := range toRemove { + cStats = append(cStats[:i], cStats[i+1:]...) + } + if len(cStats) == 0 { + return nil } w.Flush() } diff --git a/daemon/stats_collector.go b/daemon/stats_collector.go index 50ae6baf5..779bd1a59 100644 --- a/daemon/stats_collector.go +++ b/daemon/stats_collector.go @@ -98,7 +98,7 @@ func (s *statsCollector) run() { const nanoSeconds = 1e9 -// getSystemdCpuUSage returns the host system's cpu usage in nanoseconds +// getSystemCpuUSage returns the host system's cpu usage in nanoseconds // for the system to match the cgroup readings are returned in the same format. func (s *statsCollector) getSystemCpuUsage() (uint64, error) { f, err := os.Open("/proc/stat") diff --git a/docs/man/docker-stats.1.md b/docs/man/docker-stats.1.md index 991b3d9f1..fdad99719 100644 --- a/docs/man/docker-stats.1.md +++ b/docs/man/docker-stats.1.md @@ -5,7 +5,7 @@ docker-stats - Display live container stats based on resource usage. # SYNOPSIS -**docker top** +**docker stats** [**--help**] [CONTAINERS] @@ -26,7 +26,3 @@ Run **docker stats** with multiple containers. redis1 0.07% 796 KiB/64 MiB 1.21% 788 B/648 B redis2 0.07% 2.746 MiB/64 MiB 4.29% 1.266 KiB/648 B -# HISTORY -April 2014, Originally compiled by William Henry (whenry at redhat dot com) -based on docker.com source material and internal work. -June 2014, updated by Sven Dowideit From 811b138f7e6c742b821da15e34338651f33f9ec2 Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Wed, 21 Jan 2015 12:04:43 -0500 Subject: [PATCH 375/513] Fix call to nil stat Fixes #10242 Signed-off-by: Brian Goff --- daemon/start.go | 2 ++ volumes/volume.go | 26 ++++++-------------------- 2 files changed, 8 insertions(+), 20 deletions(-) diff --git a/daemon/start.go b/daemon/start.go index 363461080..d6655189d 100644 --- a/daemon/start.go +++ b/daemon/start.go @@ -53,6 +53,8 @@ func (daemon *Daemon) setHostConfig(container *Container, hostConfig *runconfig. if err := parseSecurityOpt(container, hostConfig); err != nil { return err } + + // FIXME: this should be handled by the volume subsystem // Validate the HostConfig binds. Make sure that: // the source exists for _, bind := range hostConfig.Binds { diff --git a/volumes/volume.go b/volumes/volume.go index db99aed5d..8041160ce 100644 --- a/volumes/volume.go +++ b/volumes/volume.go @@ -86,30 +86,14 @@ func (v *Volume) AddContainer(containerId string) { v.lock.Unlock() } -func (v *Volume) createIfNotExist() error { - if stat, err := os.Stat(v.Path); err != nil && os.IsNotExist(err) { - if stat.IsDir() { - os.MkdirAll(v.Path, 0755) - } - - if err := os.MkdirAll(filepath.Dir(v.Path), 0755); err != nil { - return err - } - f, err := os.OpenFile(v.Path, os.O_CREATE, 0755) - if err != nil { - return err - } - f.Close() - } - return nil -} - func (v *Volume) initialize() error { v.lock.Lock() defer v.lock.Unlock() - if err := v.createIfNotExist(); err != nil { - return err + if _, err := os.Stat(v.Path); err != nil && os.IsNotExist(err) { + if err := os.MkdirAll(v.Path, 0755); err != nil { + return err + } } if err := os.MkdirAll(v.configPath, 0755); err != nil { @@ -133,6 +117,7 @@ func (v *Volume) ToDisk() error { defer v.lock.Unlock() return v.toDisk() } + func (v *Volume) toDisk() error { data, err := json.Marshal(v) if err != nil { @@ -146,6 +131,7 @@ func (v *Volume) toDisk() error { return ioutil.WriteFile(pth, data, 0666) } + func (v *Volume) FromDisk() error { v.lock.Lock() defer v.lock.Unlock() From db9b1e3654464b0e7008fbad15d43fbbba87b1a4 Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Wed, 21 Jan 2015 11:44:23 -0800 Subject: [PATCH 376/513] Update to docker stats documentation Signed-off-by: Michael Crosby --- api/client/commands.go | 2 +- docker/flags.go | 2 +- docs/man/docker-stats.1.md | 4 ++-- docs/sources/reference/api/docker_remote_api.md | 2 +- docs/sources/reference/api/docker_remote_api_v1.17.md | 2 +- docs/sources/reference/commandline/cli.md | 11 +++++------ 6 files changed, 11 insertions(+), 12 deletions(-) diff --git a/api/client/commands.go b/api/client/commands.go index f9cc10079..7d0023d1a 100644 --- a/api/client/commands.go +++ b/api/client/commands.go @@ -2688,7 +2688,7 @@ func (s *containerStats) Display(w io.Writer) error { } func (cli *DockerCli) CmdStats(args ...string) error { - cmd := cli.Subcmd("stats", "CONTAINER", "Display live container stats based on resource usage", true) + cmd := cli.Subcmd("stats", "CONTAINER", "Display a live stream of one or more containers' resource usage statistics", true) cmd.Require(flag.Min, 1) utils.ParseFlags(cmd, args, true) diff --git a/docker/flags.go b/docker/flags.go index 525e8cbfd..4170fb2e5 100644 --- a/docker/flags.go +++ b/docker/flags.go @@ -98,7 +98,7 @@ func init() { {"save", "Save an image to a tar archive"}, {"search", "Search for an image on the Docker Hub"}, {"start", "Start a stopped container"}, - {"stats", "Display live container stats based on resource usage"}, + {"stats", "Display a live stream of one or more containers' resource usage statistics"}, {"stop", "Stop a running container"}, {"tag", "Tag an image into a repository"}, {"top", "Lookup the running processes of a container"}, diff --git a/docs/man/docker-stats.1.md b/docs/man/docker-stats.1.md index fdad99719..975ef296e 100644 --- a/docs/man/docker-stats.1.md +++ b/docs/man/docker-stats.1.md @@ -2,7 +2,7 @@ % Docker Community % JUNE 2014 # NAME -docker-stats - Display live container stats based on resource usage. +docker-stats - Display a live stream of one or more containers' resource usage statistics # SYNOPSIS **docker stats** @@ -11,7 +11,7 @@ docker-stats - Display live container stats based on resource usage. # DESCRIPTION -Display live container stats based on resource usage. +Display a live stream of one or more containers' resource usage statistics # OPTIONS **--help** diff --git a/docs/sources/reference/api/docker_remote_api.md b/docs/sources/reference/api/docker_remote_api.md index 2d52d53b8..968cb114e 100644 --- a/docs/sources/reference/api/docker_remote_api.md +++ b/docs/sources/reference/api/docker_remote_api.md @@ -71,7 +71,7 @@ root filesystem as read only. `GET /containers/(id)/stats` **New!** -This endpoint returns a stream of container stats based on resource usage. +This endpoint returns a live stream of a container's resource usage statistics. ## v1.16 diff --git a/docs/sources/reference/api/docker_remote_api_v1.17.md b/docs/sources/reference/api/docker_remote_api_v1.17.md index 77e9a8713..f8bca77ed 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.17.md +++ b/docs/sources/reference/api/docker_remote_api_v1.17.md @@ -518,7 +518,7 @@ Status Codes: `GET /containers/(id)/stats` -Returns a stream of json objects of the container's stats +This endpoint returns a live stream of a container's resource usage statistics. **Example request**: diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index b8af02da3..98dd48326 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -2005,11 +2005,11 @@ more details on finding shared images from the command line. Usage: docker stats [CONTAINERS] - Display live container stats based on resource usage + Display a live stream of one or more containers' resource usage statistics --help=false Print usage -Running `docker stats` on two redis containers +Running `docker stats` on multiple containers $ sudo docker stats redis1 redis2 CONTAINER CPU % MEM USAGE/LIMIT MEM % NET I/O @@ -2017,12 +2017,11 @@ Running `docker stats` on two redis containers redis2 0.07% 2.746 MiB/64 MiB 4.29% 1.266 KiB/648 B -When run on running containers live container stats will be streamed -back and displayed to the client. Stopped containers will not -receive any updates to their stats unless the container is started again. +The `docker stats` command will only return a live stream of data for running +containers. Stopped containers will not return any data. > **Note:** -> If you want more in depth resource usage for a container use the API endpoint +> If you want more detailed information about a container's resource usage, use the API endpoint. ## stop From c2a25058e8c85b4d6295c6a1375c0b70b0959260 Mon Sep 17 00:00:00 2001 From: Alexander Morozov Date: Wed, 21 Jan 2015 14:32:36 -0800 Subject: [PATCH 377/513] Update links aliases, not name on restart Fixes #8721 Signed-off-by: Alexander Morozov --- daemon/container.go | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/daemon/container.go b/daemon/container.go index b0eaea03b..167e2b872 100644 --- a/daemon/container.go +++ b/daemon/container.go @@ -1108,19 +1108,16 @@ func (container *Container) updateResolvConf(updatedResolvConf []byte, newResolv } func (container *Container) updateParentsHosts() error { - parents, err := container.daemon.Parents(container.Name) - if err != nil { - return err - } - for _, cid := range parents { - if cid == "0" { + refs := container.daemon.ContainerGraph().RefPaths(container.ID) + for _, ref := range refs { + if ref.ParentID == "0" { continue } - - c := container.daemon.Get(cid) + c := container.daemon.Get(ref.ParentID) if c != nil && !container.daemon.config.DisableNetwork && container.hostConfig.NetworkMode.IsPrivate() { - if err := etchosts.Update(c.HostsPath, container.NetworkSettings.IPAddress, container.Name[1:]); err != nil { - log.Errorf("Failed to update /etc/hosts in parent container: %v", err) + log.Debugf("Update /etc/hosts of %s for alias %s with ip %s", c.ID, ref.Name, container.NetworkSettings.IPAddress) + if err := etchosts.Update(c.HostsPath, container.NetworkSettings.IPAddress, ref.Name); err != nil { + log.Errorf("Failed to update /etc/hosts in parent container %s for alias %s: %v", c.ID, ref.Name, err) } } } From 606c71d424cb21c8256968eb5d965a0855d1af2d Mon Sep 17 00:00:00 2001 From: Alexander Morozov Date: Wed, 21 Jan 2015 14:34:08 -0800 Subject: [PATCH 378/513] Test for updating linked hosts on restart Signed-off-by: Alexander Morozov --- integration-cli/docker_cli_links_test.go | 59 +++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/integration-cli/docker_cli_links_test.go b/integration-cli/docker_cli_links_test.go index ad0638cf9..fc99ec57f 100644 --- a/integration-cli/docker_cli_links_test.go +++ b/integration-cli/docker_cli_links_test.go @@ -1,14 +1,17 @@ package main import ( - "github.com/docker/docker/pkg/iptables" + "fmt" "io/ioutil" "os" "os/exec" "reflect" + "regexp" "strings" "testing" "time" + + "github.com/docker/docker/pkg/iptables" ) func TestLinksEtcHostsRegularFile(t *testing.T) { @@ -276,3 +279,57 @@ func TestLinksNetworkHostContainer(t *testing.T) { logDone("link - error thrown when linking to container with --net host") } + +func TestLinksUpdateOnRestart(t *testing.T) { + defer deleteAllContainers() + + if out, err := exec.Command(dockerBinary, "run", "-d", "--name", "one", "busybox", "top").CombinedOutput(); err != nil { + t.Fatal(err, string(out)) + } + out, err := exec.Command(dockerBinary, "run", "-d", "--name", "two", "--link", "one:onetwo", "--link", "one:one", "busybox", "top").CombinedOutput() + if err != nil { + t.Fatal(err, string(out)) + } + id := strings.TrimSpace(string(out)) + + realIP, err := inspectField("one", "NetworkSettings.IPAddress") + if err != nil { + t.Fatal(err) + } + content, err := readContainerFile(id, "hosts") + if err != nil { + t.Fatal(err, string(content)) + } + getIP := func(hosts []byte, hostname string) string { + re := regexp.MustCompile(fmt.Sprintf(`(\S*)\t%s`, regexp.QuoteMeta(hostname))) + matches := re.FindSubmatch(hosts) + if matches == nil { + t.Fatalf("Hostname %s have no matches in hosts", hostname) + } + return string(matches[1]) + } + if ip := getIP(content, "one"); ip != realIP { + t.Fatalf("For 'one' alias expected IP: %s, got: %s", realIP, ip) + } + if ip := getIP(content, "onetwo"); ip != realIP { + t.Fatalf("For 'onetwo' alias expected IP: %s, got: %s", realIP, ip) + } + if out, err := exec.Command(dockerBinary, "restart", "one").CombinedOutput(); err != nil { + t.Fatal(err, string(out)) + } + realIP, err = inspectField("one", "NetworkSettings.IPAddress") + if err != nil { + t.Fatal(err) + } + content, err = readContainerFile(id, "hosts") + if err != nil { + t.Fatal(err, string(content)) + } + if ip := getIP(content, "one"); ip != realIP { + t.Fatalf("For 'one' alias expected IP: %s, got: %s", realIP, ip) + } + if ip := getIP(content, "onetwo"); ip != realIP { + t.Fatalf("For 'onetwo' alias expected IP: %s, got: %s", realIP, ip) + } + logDone("link - ensure containers hosts files are updated on restart") +} From 12d83e727dd3522006b53afbcfac3cdce178117a Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Wed, 21 Jan 2015 09:44:24 -0800 Subject: [PATCH 379/513] Fix write after close on http response Signed-off-by: Derek McGowan (github: dmcgowan) --- engine/streams.go | 1 + registry/session_v2.go | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/engine/streams.go b/engine/streams.go index 99e876e17..ec703c96f 100644 --- a/engine/streams.go +++ b/engine/streams.go @@ -111,6 +111,7 @@ func (o *Output) Close() error { } } o.tasks.Wait() + o.dests = nil return firstErr } diff --git a/registry/session_v2.go b/registry/session_v2.go index 11b96bd65..fa02bd3e6 100644 --- a/registry/session_v2.go +++ b/registry/session_v2.go @@ -226,7 +226,7 @@ func (r *Session) PutV2ImageBlob(ep *Endpoint, imageName, sumType, sumStr string method := "PUT" log.Debugf("[registry] Calling %q %s", method, location) - req, err = r.reqFactory.NewRequest(method, location, blobRdr) + req, err = r.reqFactory.NewRequest(method, location, ioutil.NopCloser(blobRdr)) if err != nil { return err } From 4e4a5b25328566efad9ade3e7e0737ba48c090ab Mon Sep 17 00:00:00 2001 From: Stephen J Day Date: Wed, 21 Jan 2015 14:16:39 -0800 Subject: [PATCH 380/513] Ensure that progress reader is closed after usage Signed-off-by: Stephen J Day --- graph/push.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/graph/push.go b/graph/push.go index 316eed91b..b8fb09882 100644 --- a/graph/push.go +++ b/graph/push.go @@ -236,7 +236,10 @@ func (s *TagStore) pushImage(r *registry.Session, out io.Writer, imgID, ep strin // Send the layer log.Debugf("rendered layer for %s of [%d] size", imgData.ID, layerData.Size) - checksum, checksumPayload, err := r.PushImageLayerRegistry(imgData.ID, utils.ProgressReader(layerData, int(layerData.Size), out, sf, false, utils.TruncateID(imgData.ID), "Pushing"), ep, token, jsonRaw) + prgRd := utils.ProgressReader(layerData, int(layerData.Size), out, sf, false, utils.TruncateID(imgData.ID), "Pushing") + defer prgRd.Close() + + checksum, checksumPayload, err := r.PushImageLayerRegistry(imgData.ID, prgRd, ep, token, jsonRaw) if err != nil { return "", err } @@ -338,8 +341,12 @@ func (s *TagStore) pushV2Repository(r *registry.Session, eng *engine.Engine, out out.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Image push failed", nil)) return err } + if !exists { - err = r.PutV2ImageBlob(endpoint, repoInfo.RemoteName, sumParts[0], manifestSum, utils.ProgressReader(arch, int(img.Size), out, sf, false, utils.TruncateID(img.ID), "Pushing"), auth) + prgRd := utils.ProgressReader(arch, int(img.Size), out, sf, false, utils.TruncateID(img.ID), "Pushing") + defer prgRd.Close() + + err = r.PutV2ImageBlob(endpoint, repoInfo.RemoteName, sumParts[0], manifestSum, prgRd, auth) if err != nil { out.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Image push failed", nil)) return err From bbc38497b6f522b0a9c4e5e976abfd79692b8b5f Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Wed, 21 Jan 2015 13:42:31 -0800 Subject: [PATCH 381/513] Zero out stats values in the cli Based on some feedback, when you have a container via the cli that you are monitoring for stats, if you stop the container it will stay in the display but report the last datapoint that was received. This PR changes the display to zero out the values for containers where an update has not been received within a specified duration, i.e. 2 seconds. This signals the user that the container has stopped as it reports cpu and memory usage of 0. Signed-off-by: Michael Crosby --- api/client/commands.go | 66 +++++++++++++++++++++++++++--------------- 1 file changed, 43 insertions(+), 23 deletions(-) diff --git a/api/client/commands.go b/api/client/commands.go index 7d0023d1a..e42c6f26e 100644 --- a/api/client/commands.go +++ b/api/client/commands.go @@ -2641,34 +2641,54 @@ func (s *containerStats) Collect(stream io.ReadCloser) { previousSystem uint64 start = true dec = json.NewDecoder(stream) + u = make(chan error, 1) ) - for { - var v *stats.Stats - if err := dec.Decode(&v); err != nil { + go func() { + for { + var v *stats.Stats + if err := dec.Decode(&v); err != nil { + u <- err + return + } + var ( + memPercent = float64(v.MemoryStats.Usage) / float64(v.MemoryStats.Limit) * 100.0 + cpuPercent = 0.0 + ) + if !start { + cpuPercent = calcuateCpuPercent(previousCpu, previousSystem, v) + } + start = false s.mu.Lock() - s.err = err + s.CpuPercentage = cpuPercent + s.Memory = float64(v.MemoryStats.Usage) + s.MemoryLimit = float64(v.MemoryStats.Limit) + s.MemoryPercentage = memPercent + s.NetworkRx = float64(v.Network.RxBytes) + s.NetworkTx = float64(v.Network.TxBytes) s.mu.Unlock() - return + previousCpu = v.CpuStats.CpuUsage.TotalUsage + previousSystem = v.CpuStats.SystemUsage + u <- nil } - var ( - memPercent = float64(v.MemoryStats.Usage) / float64(v.MemoryStats.Limit) * 100.0 - cpuPercent = 0.0 - ) - if !start { - cpuPercent = calcuateCpuPercent(previousCpu, previousSystem, v) + }() + for { + select { + case <-time.After(2 * time.Second): + // zero out the values if we have not received an update within + // the specified duration. + s.mu.Lock() + s.CpuPercentage = 0 + s.Memory = 0 + s.MemoryPercentage = 0 + s.mu.Unlock() + case err := <-u: + if err != nil { + s.mu.Lock() + s.err = err + s.mu.Unlock() + return + } } - start = false - s.mu.Lock() - s.CpuPercentage = cpuPercent - s.Memory = float64(v.MemoryStats.Usage) - s.MemoryLimit = float64(v.MemoryStats.Limit) - s.MemoryPercentage = memPercent - s.NetworkRx = float64(v.Network.RxBytes) - s.NetworkTx = float64(v.Network.TxBytes) - s.mu.Unlock() - - previousCpu = v.CpuStats.CpuUsage.TotalUsage - previousSystem = v.CpuStats.SystemUsage } } From 4d7707e183e8dcb8e0ab1415e401cb530df17c92 Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Wed, 21 Jan 2015 12:14:28 -0800 Subject: [PATCH 382/513] Improve robustness of /stats api test Signed-off-by: Michael Crosby --- integration-cli/docker_api_containers_test.go | 44 +++++++++++++------ 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/integration-cli/docker_api_containers_test.go b/integration-cli/docker_api_containers_test.go index 34cc82aff..4e945f542 100644 --- a/integration-cli/docker_api_containers_test.go +++ b/integration-cli/docker_api_containers_test.go @@ -256,28 +256,46 @@ func TestVolumesFromHasPriority(t *testing.T) { func TestGetContainerStats(t *testing.T) { defer deleteAllContainers() - name := "statscontainer" - - runCmd := exec.Command(dockerBinary, "run", "-d", "--name", name, "busybox", "top") + var ( + name = "statscontainer" + runCmd = exec.Command(dockerBinary, "run", "-d", "--name", name, "busybox", "top") + ) out, _, err := runCommandWithOutput(runCmd) if err != nil { t.Fatalf("Error on container creation: %v, output: %q", err, out) } + type b struct { + body []byte + err error + } + bc := make(chan b, 1) go func() { - time.Sleep(4 * time.Second) - runCommand(exec.Command(dockerBinary, "kill", name)) - runCommand(exec.Command(dockerBinary, "rm", name)) + body, err := sockRequest("GET", "/containers/"+name+"/stats", nil) + bc <- b{body, err} }() - body, err := sockRequest("GET", "/containers/"+name+"/stats", nil) - if err != nil { - t.Fatalf("GET containers/stats sockRequest failed: %v", err) + // allow some time to stream the stats from the container + time.Sleep(4 * time.Second) + if _, err := runCommand(exec.Command(dockerBinary, "rm", "-f", name)); err != nil { + t.Fatal(err) } - dec := json.NewDecoder(bytes.NewBuffer(body)) - var s *stats.Stats - if err := dec.Decode(&s); err != nil { - t.Fatal(err) + // collect the results from the stats stream or timeout and fail + // if the stream was not disconnected. + select { + case <-time.After(2 * time.Second): + t.Fatal("stream was not closed after container was removed") + case sr := <-bc: + if sr.err != nil { + t.Fatal(err) + } + + dec := json.NewDecoder(bytes.NewBuffer(sr.body)) + var s *stats.Stats + // decode only one object from the stream + if err := dec.Decode(&s); err != nil { + t.Fatal(err) + } } logDone("container REST API - check GET containers/stats") } From 1820003078eeae6ab25e2440669c490caff59b57 Mon Sep 17 00:00:00 2001 From: Jessica Frazelle Date: Tue, 20 Jan 2015 19:19:11 -0800 Subject: [PATCH 383/513] Warn about tech preview of checksums. Docker-DCO-1.1-Signed-off-by: Jessica Frazelle (github: jfrazelle) --- graph/pull.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/graph/pull.go b/graph/pull.go index 6129ea39a..d8d045e7a 100644 --- a/graph/pull.go +++ b/graph/pull.go @@ -428,10 +428,11 @@ func (s *TagStore) pullV2Tag(eng *engine.Engine, r *registry.Session, out io.Wri } if verified { - out.Write(sf.FormatStatus(repoInfo.CanonicalName+":"+tag, "The image you are pulling has been verified")) + log.Printf("Image manifest for %s:%s has been verified", repoInfo.CanonicalName, tag) } else { out.Write(sf.FormatStatus(tag, "Pulling from %s", repoInfo.CanonicalName)) } + downloads := make([]downloadInfo, len(manifest.FSLayers)) for i := len(manifest.FSLayers) - 1; i >= 0; i-- { @@ -553,6 +554,8 @@ func (s *TagStore) pullV2Tag(eng *engine.Engine, r *registry.Session, out io.Wri } + out.Write(sf.FormatStatus(repoInfo.CanonicalName+":"+tag, "The image you are pulling has been verified - This is a tech preview, don't rely on it for security yet.")) + if err = s.Set(repoInfo.LocalName, tag, downloads[0].img.ID, true); err != nil { return false, err } From 614e09a8c7990e05509edb4c335c4b59001cea61 Mon Sep 17 00:00:00 2001 From: Jessica Frazelle Date: Wed, 21 Jan 2015 16:12:02 -0800 Subject: [PATCH 384/513] Add test for pull verified Docker-DCO-1.1-Signed-off-by: Jessica Frazelle (github: jfrazelle) --- integration-cli/docker_cli_pull_test.go | 38 +++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/integration-cli/docker_cli_pull_test.go b/integration-cli/docker_cli_pull_test.go index 764968858..29471954a 100644 --- a/integration-cli/docker_cli_pull_test.go +++ b/integration-cli/docker_cli_pull_test.go @@ -53,6 +53,44 @@ func TestPullImageWithAliases(t *testing.T) { logDone("pull - image with aliases") } +// pulling busybox should show verified message +func TestPullVerified(t *testing.T) { + defer setupRegistry(t)() + + repo := fmt.Sprintf("%v/dockercli/busybox:verified", privateRegistryURL) + defer deleteImages(repo) + + // tag the image + if out, _, err := runCommandWithOutput(exec.Command(dockerBinary, "tag", "busybox", repo)); err != nil { + t.Fatalf("Failed to tag image verifiedTest: error %v, output %q", err, out) + } + + // push it + if out, err := exec.Command(dockerBinary, "push", repo).CombinedOutput(); err != nil { + t.Fatalf("Failed to push image %v: error %v, output %q", err, string(out)) + } + + // remove it locally + if out, err := exec.Command(dockerBinary, "rmi", repo).CombinedOutput(); err != nil { + t.Fatalf("Failed to clean images: error %v, output %q", err, string(out)) + } + + // pull it + expected := "The image you are pulling has been verified" + pullCmd := exec.Command(dockerBinary, "pull", repo) + if out, _, err := runCommandWithOutput(pullCmd); err != nil || !strings.Contains(out, expected) { + t.Fatalf("pulling a verified image failed. expected: %s\ngot: %s, %v", expected, out, err) + } + + // pull it again + pullCmd = exec.Command(dockerBinary, "pull", repo) + if out, _, err := runCommandWithOutput(pullCmd); err != nil || !strings.Contains(out, expected) { + t.Fatalf("pulling a verified image failed. expected: %s\ngot: %s, %v", expected, out, err) + } + + logDone("pull - pull verified") +} + // pulling an image from the central registry should work func TestPullImageFromCentralRegistry(t *testing.T) { pullCmd := exec.Command(dockerBinary, "pull", "hello-world") From 06af013f8bdf5c9af85c4b3f158292d79ab644a5 Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Wed, 21 Jan 2015 08:14:30 -0800 Subject: [PATCH 385/513] Fix daemon key file location Fixes #10233 Signed-off-by: Derek McGowan (github: dmcgowan) --- docker/docker.go | 2 ++ docker/flags.go | 23 ++++++++++++++++++++--- integration-cli/docker_cli_daemon_test.go | 23 +++++++++++++++++++++++ 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/docker/docker.go b/docker/docker.go index 92f5f1460..6410171fa 100644 --- a/docker/docker.go +++ b/docker/docker.go @@ -67,6 +67,8 @@ func main() { flHosts = append(flHosts, defaultHost) } + setDefaultConfFlag(flTrustKey, defaultTrustKeyFile) + if *flDaemon { mainDaemon() return diff --git a/docker/flags.go b/docker/flags.go index 4170fb2e5..29013146f 100644 --- a/docker/flags.go +++ b/docker/flags.go @@ -28,6 +28,13 @@ func getHomeDir() string { return os.Getenv("HOME") } +func getDaemonConfDir() string { + if runtime.GOOS == "windows" { + return filepath.Join(os.Getenv("USERPROFILE"), ".docker") + } + return "/etc/docker" +} + var ( flVersion = flag.Bool([]string{"v", "-version"}, false, "Print version information and quit") flDaemon = flag.Bool([]string{"d", "-daemon"}, false, "Enable daemon mode") @@ -47,10 +54,20 @@ var ( flHosts []string ) +func setDefaultConfFlag(flag *string, def string) { + if *flag == "" { + if *flDaemon { + *flag = filepath.Join(getDaemonConfDir(), def) + } else { + *flag = filepath.Join(getHomeDir(), ".docker", def) + } + } +} + func init() { - // placeholder for trust key flag - trustKeyDefault := filepath.Join(dockerCertPath, defaultTrustKeyFile) - flTrustKey = &trustKeyDefault + var placeholderTrustKey string + // TODO use flag flag.String([]string{"i", "-identity"}, "", "Path to libtrust key file") + flTrustKey = &placeholderTrustKey flCa = flag.String([]string{"-tlscacert"}, filepath.Join(dockerCertPath, defaultCaFile), "Trust only remotes providing a certificate signed by the CA given here") flCert = flag.String([]string{"-tlscert"}, filepath.Join(dockerCertPath, defaultCertFile), "Path to TLS certificate file") diff --git a/integration-cli/docker_cli_daemon_test.go b/integration-cli/docker_cli_daemon_test.go index b7db552b6..bb44942c2 100644 --- a/integration-cli/docker_cli_daemon_test.go +++ b/integration-cli/docker_cli_daemon_test.go @@ -10,6 +10,8 @@ import ( "os/exec" "strings" "testing" + + "github.com/docker/libtrust" ) func TestDaemonRestartWithRunningContainersPorts(t *testing.T) { @@ -350,3 +352,24 @@ func TestDaemonVolumesBindsRefs(t *testing.T) { logDone("daemon - bind refs in data-containers survive daemon restart") } + +func TestDaemonKeyGeneration(t *testing.T) { + os.Remove("/etc/docker/key.json") + d := NewDaemon(t) + if err := d.Start(); err != nil { + t.Fatalf("Could not start daemon: %v", err) + } + d.Stop() + + k, err := libtrust.LoadKeyFile("/etc/docker/key.json") + if err != nil { + t.Fatalf("Error opening key file") + } + kid := k.KeyID() + // Test Key ID is a valid fingerprint (e.g. QQXN:JY5W:TBXI:MK3X:GX6P:PD5D:F56N:NHCS:LVRZ:JA46:R24J:XEFF) + if len(kid) != 59 { + t.Fatalf("Bad key ID: %s", kid) + } + + logDone("daemon - key generation") +} From 007ef161b45dd91afcfb7ef9cd32e6c88dbf196e Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Wed, 21 Jan 2015 16:55:05 -0800 Subject: [PATCH 386/513] Add key migration to daemon Signed-off-by: Derek McGowan (github: dmcgowan) --- docker/daemon.go | 41 +++++++++++++++++++++++++++++++++++++++++ utils/utils_daemon.go | 10 ++++++++++ 2 files changed, 51 insertions(+) diff --git a/docker/daemon.go b/docker/daemon.go index 508a75bd8..df23884e9 100644 --- a/docker/daemon.go +++ b/docker/daemon.go @@ -3,6 +3,11 @@ package main import ( + "fmt" + "io" + "os" + "path/filepath" + log "github.com/Sirupsen/logrus" "github.com/docker/docker/builder" "github.com/docker/docker/builtins" @@ -14,6 +19,7 @@ import ( flag "github.com/docker/docker/pkg/mflag" "github.com/docker/docker/pkg/signal" "github.com/docker/docker/registry" + "github.com/docker/docker/utils" ) const CanDaemon = true @@ -28,6 +34,38 @@ func init() { registryCfg.InstallFlags() } +func migrateKey() error { + // Migrate trust key if exists at ~/.docker/key.json and owned by current user + oldPath := filepath.Join(getHomeDir(), ".docker", defaultTrustKeyFile) + newPath := filepath.Join(getDaemonConfDir(), defaultTrustKeyFile) + if _, err := os.Stat(newPath); os.IsNotExist(err) && utils.IsFileOwner(oldPath) { + if err := os.MkdirAll(getDaemonConfDir(), os.FileMode(0644)); err != nil { + return fmt.Errorf("Unable to create daemon configuraiton directory: %s", err) + } + + newFile, err := os.OpenFile(newPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return fmt.Errorf("error creating key file %q: %s", newPath, err) + } + defer newFile.Close() + + oldFile, err := os.Open(oldPath) + if err != nil { + return fmt.Errorf("error opening open key file %q: %s", oldPath, err) + } + + if _, err := io.Copy(newFile, oldFile); err != nil { + return fmt.Errorf("error copying key: %s", err) + } + + oldFile.Close() + log.Debugf("Migrated key from %s to %s", oldPath, newPath) + return os.Remove(oldPath) + } + + return nil +} + func mainDaemon() { if flag.NArg() != 0 { flag.Usage() @@ -36,6 +74,9 @@ func mainDaemon() { eng := engine.New() signal.Trap(eng.Shutdown) + if err := migrateKey(); err != nil { + log.Fatal(err) + } daemonCfg.TrustKeyPath = *flTrustKey // Load builtins diff --git a/utils/utils_daemon.go b/utils/utils_daemon.go index 098e22736..9989f05e3 100644 --- a/utils/utils_daemon.go +++ b/utils/utils_daemon.go @@ -37,3 +37,13 @@ func TreeSize(dir string) (size int64, err error) { }) return } + +// IsFileOwner checks whether the current user is the owner of the given file. +func IsFileOwner(f string) bool { + if fileInfo, err := os.Stat(f); err == nil && fileInfo != nil { + if stat, ok := fileInfo.Sys().(*syscall.Stat_t); ok && int(stat.Uid) == os.Getuid() { + return true + } + } + return false +} From 9c7689a10e60cd921e1521c729c07184b7c1076c Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Thu, 22 Jan 2015 01:15:34 +0000 Subject: [PATCH 387/513] bump API version Signed-off-by: Victor Vieux --- api/common.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/common.go b/api/common.go index b8e7c84b3..fb3eefaca 100644 --- a/api/common.go +++ b/api/common.go @@ -15,7 +15,7 @@ import ( ) const ( - APIVERSION version.Version = "1.16" + APIVERSION version.Version = "1.17" DEFAULTHTTPHOST = "127.0.0.1" DEFAULTUNIXSOCKET = "/var/run/docker.sock" DefaultDockerfileName string = "Dockerfile" From c97d8b1233bc17a9e58ce97473329ab5de59969a Mon Sep 17 00:00:00 2001 From: Jessica Frazelle Date: Wed, 21 Jan 2015 18:07:10 -0800 Subject: [PATCH 388/513] Change the wording of image verification warning. Docker-DCO-1.1-Signed-off-by: Jessica Frazelle (github: jfrazelle) --- graph/pull.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graph/pull.go b/graph/pull.go index d8d045e7a..f76a15605 100644 --- a/graph/pull.go +++ b/graph/pull.go @@ -554,7 +554,7 @@ func (s *TagStore) pullV2Tag(eng *engine.Engine, r *registry.Session, out io.Wri } - out.Write(sf.FormatStatus(repoInfo.CanonicalName+":"+tag, "The image you are pulling has been verified - This is a tech preview, don't rely on it for security yet.")) + out.Write(sf.FormatStatus(repoInfo.CanonicalName+":"+tag, "The image you are pulling has been verified. Important: image verification is a tech preview feature and should not be relied on to provide security.")) if err = s.Set(repoInfo.LocalName, tag, downloads[0].img.ID, true); err != nil { return false, err From 4d10b32380793ce5e324a429ce2db60125aae205 Mon Sep 17 00:00:00 2001 From: Ian Babrou Date: Thu, 22 Jan 2015 10:36:20 +0300 Subject: [PATCH 389/513] Not doing extra assertion for io.Closer Signed-off-by: Ian Babrou --- pkg/chrootarchive/diff.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/pkg/chrootarchive/diff.go b/pkg/chrootarchive/diff.go index ac1cbf9be..8d97c764d 100644 --- a/pkg/chrootarchive/diff.go +++ b/pkg/chrootarchive/diff.go @@ -5,7 +5,6 @@ import ( "encoding/json" "flag" "fmt" - "io" "io/ioutil" "os" "path/filepath" @@ -60,11 +59,7 @@ func ApplyLayer(dest string, layer archive.ArchiveReader) (size int64, err error return 0, err } - defer func() { - if c, ok := decompressed.(io.Closer); ok { - c.Close() - } - }() + defer decompressed.Close() cmd := reexec.Command("docker-applyLayer", dest) cmd.Stdin = decompressed From a34a7930b5c1e9a1e6ddd4a40b1810a86f7d24ab Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Thu, 22 Jan 2015 10:29:15 -0800 Subject: [PATCH 390/513] Add TODO lines for windows Signed-off-by: Derek McGowan (github: dmcgowan) --- docker/flags.go | 1 + integration-cli/docker_cli_daemon_test.go | 1 + 2 files changed, 2 insertions(+) diff --git a/docker/flags.go b/docker/flags.go index 29013146f..3b54612e8 100644 --- a/docker/flags.go +++ b/docker/flags.go @@ -29,6 +29,7 @@ func getHomeDir() string { } func getDaemonConfDir() string { + // TODO: update for Windows daemon if runtime.GOOS == "windows" { return filepath.Join(os.Getenv("USERPROFILE"), ".docker") } diff --git a/integration-cli/docker_cli_daemon_test.go b/integration-cli/docker_cli_daemon_test.go index bb44942c2..51ebdc9ac 100644 --- a/integration-cli/docker_cli_daemon_test.go +++ b/integration-cli/docker_cli_daemon_test.go @@ -354,6 +354,7 @@ func TestDaemonVolumesBindsRefs(t *testing.T) { } func TestDaemonKeyGeneration(t *testing.T) { + // TODO: skip or update for Windows daemon os.Remove("/etc/docker/key.json") d := NewDaemon(t) if err := d.Start(); err != nil { From 42612ff6dba2d885a0c55af80128201a4d5166cb Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Thu, 22 Jan 2015 10:51:04 -0800 Subject: [PATCH 391/513] Add key migration integration test Signed-off-by: Derek McGowan (github: dmcgowan) --- integration-cli/docker_cli_daemon_test.go | 29 +++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/integration-cli/docker_cli_daemon_test.go b/integration-cli/docker_cli_daemon_test.go index 51ebdc9ac..95188296d 100644 --- a/integration-cli/docker_cli_daemon_test.go +++ b/integration-cli/docker_cli_daemon_test.go @@ -8,6 +8,7 @@ import ( "io/ioutil" "os" "os/exec" + "path/filepath" "strings" "testing" @@ -374,3 +375,31 @@ func TestDaemonKeyGeneration(t *testing.T) { logDone("daemon - key generation") } + +func TestDaemonKeyMigration(t *testing.T) { + // TODO: skip or update for Windows daemon + os.Remove("/etc/docker/key.json") + k1, err := libtrust.GenerateECP256PrivateKey() + if err != nil { + t.Fatalf("Error generating private key: %s", err) + } + if err := libtrust.SaveKey(filepath.Join(os.Getenv("HOME"), ".docker", "key.json"), k1); err != nil { + t.Fatalf("Error saving private key: %s", err) + } + + d := NewDaemon(t) + if err := d.Start(); err != nil { + t.Fatalf("Could not start daemon: %v", err) + } + d.Stop() + + k2, err := libtrust.LoadKeyFile("/etc/docker/key.json") + if err != nil { + t.Fatalf("Error opening key file") + } + if k1.KeyID() != k2.KeyID() { + t.Fatalf("Key not migrated") + } + + logDone("daemon - key migration") +} From 752a0d6f34ee3c92dc877273c33b8cd0239fda71 Mon Sep 17 00:00:00 2001 From: Tibor Vass Date: Thu, 22 Jan 2015 13:59:32 -0500 Subject: [PATCH 392/513] integration-cli: Fix race in restart loop Signed-off-by: Tibor Vass --- integration-cli/docker_cli_restart_test.go | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/integration-cli/docker_cli_restart_test.go b/integration-cli/docker_cli_restart_test.go index 93821f726..fc82023b2 100644 --- a/integration-cli/docker_cli_restart_test.go +++ b/integration-cli/docker_cli_restart_test.go @@ -152,7 +152,7 @@ func TestRestartWithVolumes(t *testing.T) { logDone("restart - does not create a new volume on restart") } -func TestRecordRestartPolicyNO(t *testing.T) { +func TestRestartPolicyNO(t *testing.T) { defer deleteAllContainers() cmd := exec.Command(dockerBinary, "run", "-d", "--restart=no", "busybox", "false") @@ -173,7 +173,7 @@ func TestRecordRestartPolicyNO(t *testing.T) { logDone("restart - recording restart policy name for --restart=no") } -func TestRecordRestartPolicyAlways(t *testing.T) { +func TestRestartPolicyAlways(t *testing.T) { defer deleteAllContainers() cmd := exec.Command(dockerBinary, "run", "-d", "--restart=always", "busybox", "false") @@ -191,16 +191,10 @@ func TestRecordRestartPolicyAlways(t *testing.T) { t.Fatalf("Container restart policy name is %s, expected %s", name, "always") } - cmd = exec.Command(dockerBinary, "stop", id) - out, _, err = runCommandWithOutput(cmd) - if err != nil { - t.Fatal(err, out) - } - logDone("restart - recording restart policy name for --restart=always") } -func TestRecordRestartPolicyOnFailure(t *testing.T) { +func TestRestartPolicyOnFailure(t *testing.T) { defer deleteAllContainers() cmd := exec.Command(dockerBinary, "run", "-d", "--restart=on-failure:1", "busybox", "false") From d55e977cf5963f8ae5efdfbee458727f704be398 Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Thu, 22 Jan 2015 11:22:31 -0800 Subject: [PATCH 393/513] Fix nits and defers Signed-off-by: Derek McGowan (github: dmcgowan) --- docker/daemon.go | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/docker/daemon.go b/docker/daemon.go index df23884e9..092399737 100644 --- a/docker/daemon.go +++ b/docker/daemon.go @@ -34,13 +34,22 @@ func init() { registryCfg.InstallFlags() } -func migrateKey() error { +func migrateKey() (err error) { // Migrate trust key if exists at ~/.docker/key.json and owned by current user oldPath := filepath.Join(getHomeDir(), ".docker", defaultTrustKeyFile) newPath := filepath.Join(getDaemonConfDir(), defaultTrustKeyFile) - if _, err := os.Stat(newPath); os.IsNotExist(err) && utils.IsFileOwner(oldPath) { + if _, statErr := os.Stat(newPath); os.IsNotExist(statErr) && utils.IsFileOwner(oldPath) { + defer func() { + // Ensure old path is removed if no error occurred + if err == nil { + err = os.Remove(oldPath) + } else { + log.Warnf("Key migration failed, key file not removed at %s", oldPath) + } + }() + if err := os.MkdirAll(getDaemonConfDir(), os.FileMode(0644)); err != nil { - return fmt.Errorf("Unable to create daemon configuraiton directory: %s", err) + return fmt.Errorf("Unable to create daemon configuration directory: %s", err) } newFile, err := os.OpenFile(newPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) @@ -51,16 +60,15 @@ func migrateKey() error { oldFile, err := os.Open(oldPath) if err != nil { - return fmt.Errorf("error opening open key file %q: %s", oldPath, err) + return fmt.Errorf("error opening key file %q: %s", oldPath, err) } + defer oldFile.Close() if _, err := io.Copy(newFile, oldFile); err != nil { return fmt.Errorf("error copying key: %s", err) } - oldFile.Close() - log.Debugf("Migrated key from %s to %s", oldPath, newPath) - return os.Remove(oldPath) + log.Infof("Migrated key from %s to %s", oldPath, newPath) } return nil From 6da1e01e6cc8b32068999c5bd61ff450a050e8d6 Mon Sep 17 00:00:00 2001 From: imre Fitos Date: Wed, 14 Jan 2015 23:06:13 -0500 Subject: [PATCH 394/513] docs: remove NAT rule when removing bridge Signed-off-by: imre Fitos --- docs/sources/articles/networking.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/sources/articles/networking.md b/docs/sources/articles/networking.md index 2a0a74f95..eb279f2c2 100644 --- a/docs/sources/articles/networking.md +++ b/docs/sources/articles/networking.md @@ -687,6 +687,7 @@ stopping the service and removing the interface: $ sudo service docker stop $ sudo ip link set dev docker0 down $ sudo brctl delbr docker0 + $ sudo iptables -t nat -F POSTROUTING Then, before starting the Docker service, create your own bridge and give it whatever configuration you want. Here we will create a simple @@ -708,6 +709,14 @@ illustrate the technique. inet 192.168.5.1/24 scope global bridge0 valid_lft forever preferred_lft forever + # Confirming outgoing NAT masquerade is setup + + $ sudo iptables -t nat -L -n + ... + Chain POSTROUTING (policy ACCEPT) + target prot opt source destination + MASQUERADE all -- 192.168.5.0/24 0.0.0.0/0 + # Tell Docker about it and restart (on Ubuntu) $ echo 'DOCKER_OPTS="-b=bridge0"' >> /etc/default/docker From 034aa3b2c4b9ad3472a9d492f77ebd7eabe5e088 Mon Sep 17 00:00:00 2001 From: imre Fitos Date: Thu, 15 Jan 2015 21:32:38 -0500 Subject: [PATCH 395/513] start docker before checking for updated NAT rule Signed-off-by: imre Fitos --- docs/sources/articles/networking.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/sources/articles/networking.md b/docs/sources/articles/networking.md index eb279f2c2..3d756c14e 100644 --- a/docs/sources/articles/networking.md +++ b/docs/sources/articles/networking.md @@ -709,7 +709,12 @@ illustrate the technique. inet 192.168.5.1/24 scope global bridge0 valid_lft forever preferred_lft forever - # Confirming outgoing NAT masquerade is setup + # Tell Docker about it and restart (on Ubuntu) + + $ echo 'DOCKER_OPTS="-b=bridge0"' >> /etc/default/docker + $ sudo service docker start + + # Confirming new outgoing NAT masquerade is setup $ sudo iptables -t nat -L -n ... @@ -717,10 +722,6 @@ illustrate the technique. target prot opt source destination MASQUERADE all -- 192.168.5.0/24 0.0.0.0/0 - # Tell Docker about it and restart (on Ubuntu) - - $ echo 'DOCKER_OPTS="-b=bridge0"' >> /etc/default/docker - $ sudo service docker start The result should be that the Docker server starts successfully and is now prepared to bind containers to the new bridge. After pausing to From 7bf03dd132661b5fad20c2965058e61881162b9b Mon Sep 17 00:00:00 2001 From: imre Fitos Date: Sat, 17 Jan 2015 11:21:25 -0500 Subject: [PATCH 396/513] fix typo 'setup/set up' Signed-off-by: imre Fitos --- docs/sources/articles/networking.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/articles/networking.md b/docs/sources/articles/networking.md index 3d756c14e..b93286d91 100644 --- a/docs/sources/articles/networking.md +++ b/docs/sources/articles/networking.md @@ -714,7 +714,7 @@ illustrate the technique. $ echo 'DOCKER_OPTS="-b=bridge0"' >> /etc/default/docker $ sudo service docker start - # Confirming new outgoing NAT masquerade is setup + # Confirming new outgoing NAT masquerade is set up $ sudo iptables -t nat -L -n ... From b98b42d84375d6fa9a52ea4981ecd4ea4a81dd99 Mon Sep 17 00:00:00 2001 From: Harald Albers Date: Mon, 19 Jan 2015 18:35:40 +0100 Subject: [PATCH 397/513] Add bash completions for daemon flags, simplify with extglob Implementing the deamon flags the traditional way introduced even more redundancy than usual because the same list of options with flags had to be added twice. This can be avoided by using variables in the case statements when using the extglob shell option. Signed-off-by: Harald Albers --- contrib/completion/bash/docker | 91 ++++++++++++++++++++++++++++++++-- 1 file changed, 86 insertions(+), 5 deletions(-) diff --git a/contrib/completion/bash/docker b/contrib/completion/bash/docker index 7dd23b853..4891194bd 100755 --- a/contrib/completion/bash/docker +++ b/contrib/completion/bash/docker @@ -104,6 +104,22 @@ __docker_pos_first_nonflag() { echo $counter } +# Transforms a multiline list of strings into a single line string +# with the words separated by "|". +# This is used to prepare arguments to __docker_pos_first_nonflag(). +__docker_to_alternatives() { + local parts=( $1 ) + local IFS='|' + echo "${parts[*]}" +} + +# Transforms a multiline list of options into an extglob pattern +# suitable for use in case statements. +__docker_to_extglob() { + local extglob=$( __docker_to_alternatives "$1" ) + echo "@($extglob)" +} + __docker_resolve_hostname() { command -v host >/dev/null 2>&1 || return COMPREPLY=( $(host 2>/dev/null "${cur%:}" | awk '/has address/ {print $4}') ) @@ -154,15 +170,47 @@ __docker_capabilities() { } _docker_docker() { + local boolean_options=" + --api-enable-cors + --daemon -d + --debug -D + --help -h + --icc + --ip-forward + --ip-masq + --iptables + --ipv6 + --selinux-enabled + --tls + --tlsverify + --version -v + " + case "$prev" in - -H) + --graph|-g) + _filedir -d + return + ;; + --log-level|-l) + COMPREPLY=( $( compgen -W "debug info warn error fatal" -- "$cur" ) ) + return + ;; + --pidfile|-p|--tlscacert|--tlscert|--tlskey) + _filedir + return + ;; + --storage-driver|-s) + COMPREPLY=( $( compgen -W "aufs devicemapper btrfs overlay" -- "$(echo $cur | tr '[:upper:]' '[:lower:]')" ) ) + return + ;; + $main_options_with_args_glob ) return ;; esac case "$cur" in -*) - COMPREPLY=( $( compgen -W "-H" -- "$cur" ) ) + COMPREPLY=( $( compgen -W "$boolean_options $main_options_with_args" -- "$cur" ) ) ;; *) COMPREPLY=( $( compgen -W "${commands[*]} help" -- "$cur" ) ) @@ -561,6 +609,8 @@ _docker_run() { --sig-proxy " + local options_with_args_glob=$(__docker_to_extglob "$options_with_args") + case "$prev" in --add-host) case "$cur" in @@ -677,7 +727,7 @@ _docker_run() { __docker_containers_all return ;; - --cpuset|--cpu-shares|-c|--dns|--dns-search|--entrypoint|--expose|--hostname|-h|--lxc-conf|--mac-address|--memory|-m|--name|-n|--publish|-p|--user|-u|--workdir|-w) + $options_with_args_glob ) return ;; esac @@ -687,7 +737,7 @@ _docker_run() { COMPREPLY=( $( compgen -W "$all_options" -- "$cur" ) ) ;; *) - local counter=$( __docker_pos_first_nonflag $( echo $options_with_args | tr -d "\n" | tr " " "|" ) ) + local counter=$( __docker_pos_first_nonflag $( __docker_to_alternatives "$options_with_args" ) ) if [ $cword -eq $counter ]; then __docker_image_repos_and_tags_and_ids @@ -801,6 +851,9 @@ _docker_wait() { } _docker() { + local previous_extglob_setting=$(shopt -p extglob) + shopt -s extglob + local commands=( attach build @@ -841,6 +894,33 @@ _docker() { wait ) + local main_options_with_args=" + --bip + --bridge -b + --dns + --dns-search + --exec-driver -e + --fixed-cidr + --fixed-cidr-v6 + --graph -g + --group -G + --host -H + --insecure-registry + --ip + --label + --log-level -l + --mtu + --pidfile -p + --registry-mirror + --storage-driver -s + --storage-opt + --tlscacert + --tlscert + --tlskey + " + + local main_options_with_args_glob=$(__docker_to_extglob "$main_options_with_args") + COMPREPLY=() local cur prev words cword _get_comp_words_by_ref -n : cur prev words cword @@ -849,7 +929,7 @@ _docker() { local counter=1 while [ $counter -lt $cword ]; do case "${words[$counter]}" in - -H) + $main_options_with_args_glob ) (( counter++ )) ;; -*) @@ -867,6 +947,7 @@ _docker() { local completions_func=_docker_${command} declare -F $completions_func >/dev/null && $completions_func + eval "$previous_extglob_setting" return 0 } From 526ca422822af2af65f959d4d5dc84cdf7e9a8c6 Mon Sep 17 00:00:00 2001 From: Josh Hawn Date: Wed, 21 Jan 2015 12:11:53 -0800 Subject: [PATCH 398/513] Split API Version header when checking for v2 Since the Docker-Distribution-API-Version header value may contain multiple space delimited versions as well as many instances of the header key, the header value is now split on whitespace characters to iterate over all versions that may be listed in one instance of the header. Docker-DCO-1.1-Signed-off-by: Josh Hawn (github: jlhawn) --- registry/endpoint.go | 11 +++++++---- registry/endpoint_test.go | 4 +++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/registry/endpoint.go b/registry/endpoint.go index 72bcce4aa..de9c1f867 100644 --- a/registry/endpoint.go +++ b/registry/endpoint.go @@ -231,10 +231,13 @@ func (e *Endpoint) pingV2() (RegistryInfo, error) { // Ensure it supports the v2 Registry API. var supportsV2 bool - for _, versionName := range resp.Header[http.CanonicalHeaderKey("Docker-Distribution-API-Version")] { - if versionName == "registry/2.0" { - supportsV2 = true - break +HeaderLoop: + for _, supportedVersions := range resp.Header[http.CanonicalHeaderKey("Docker-Distribution-API-Version")] { + for _, versionName := range strings.Fields(supportedVersions) { + if versionName == "registry/2.0" { + supportsV2 = true + break HeaderLoop + } } } diff --git a/registry/endpoint_test.go b/registry/endpoint_test.go index ef2589994..00c27b448 100644 --- a/registry/endpoint_test.go +++ b/registry/endpoint_test.go @@ -42,7 +42,9 @@ func TestValidateEndpointAmbiguousAPIVersion(t *testing.T) { }) requireBasicAuthHandlerV2 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Add("Docker-Distribution-API-Version", "registry/2.0") + // This mock server supports v2.0, v2.1, v42.0, and v100.0 + w.Header().Add("Docker-Distribution-API-Version", "registry/100.0 registry/42.0") + w.Header().Add("Docker-Distribution-API-Version", "registry/2.0 registry/2.1") requireBasicAuthHandler.ServeHTTP(w, r) }) From 32f189cd08ff8b97cf0708e3a52f36cefea668eb Mon Sep 17 00:00:00 2001 From: "Andrew C. Bodine" Date: Wed, 21 Jan 2015 11:52:35 -0800 Subject: [PATCH 399/513] Adds docs for /containers/(id)/attach/ws api endpoint Signed-off-by: Andrew C. Bodine --- .../reference/api/docker_remote_api_v1.0.md | 35 +++++++++++++++++++ .../reference/api/docker_remote_api_v1.1.md | 35 +++++++++++++++++++ .../reference/api/docker_remote_api_v1.10.md | 35 +++++++++++++++++++ .../reference/api/docker_remote_api_v1.11.md | 35 +++++++++++++++++++ .../reference/api/docker_remote_api_v1.12.md | 35 +++++++++++++++++++ .../reference/api/docker_remote_api_v1.13.md | 35 +++++++++++++++++++ .../reference/api/docker_remote_api_v1.14.md | 35 +++++++++++++++++++ .../reference/api/docker_remote_api_v1.15.md | 35 +++++++++++++++++++ .../reference/api/docker_remote_api_v1.16.md | 35 +++++++++++++++++++ .../reference/api/docker_remote_api_v1.17.md | 35 +++++++++++++++++++ .../reference/api/docker_remote_api_v1.2.md | 35 +++++++++++++++++++ .../reference/api/docker_remote_api_v1.3.md | 35 +++++++++++++++++++ .../reference/api/docker_remote_api_v1.4.md | 35 +++++++++++++++++++ .../reference/api/docker_remote_api_v1.5.md | 35 +++++++++++++++++++ .../reference/api/docker_remote_api_v1.6.md | 35 +++++++++++++++++++ .../reference/api/docker_remote_api_v1.7.md | 35 +++++++++++++++++++ .../reference/api/docker_remote_api_v1.8.md | 35 +++++++++++++++++++ .../reference/api/docker_remote_api_v1.9.md | 35 +++++++++++++++++++ 18 files changed, 630 insertions(+) diff --git a/docs/sources/reference/api/docker_remote_api_v1.0.md b/docs/sources/reference/api/docker_remote_api_v1.0.md index 49ff939d6..399bf7f14 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.0.md +++ b/docs/sources/reference/api/docker_remote_api_v1.0.md @@ -385,6 +385,41 @@ Status Codes: - **404** – no such container - **500** – server error +### Attach to a container (websocket) + +`GET /containers/(id)/attach/ws` + +Attach to the container `id` via websocket + +Implements websocket protocol handshake according to [RFC 6455](http://tools.ietf.org/html/rfc6455) + +**Example request** + + GET /containers/e90e34656806/attach/ws?logs=0&stream=1&stdin=1&stdout=1&stderr=1 HTTP/1.1 + +**Example response** + + {{ STREAM }} + +Query Parameters: + +- **logs** – 1/True/true or 0/False/false, return logs. Default false +- **stream** – 1/True/true or 0/False/false, return stream. + Default false +- **stdin** – 1/True/true or 0/False/false, if stream=true, attach + to stdin. Default false +- **stdout** – 1/True/true or 0/False/false, if logs=true, return + stdout log, if stream=true, attach to stdout. Default false +- **stderr** – 1/True/true or 0/False/false, if logs=true, return + stderr log, if stream=true, attach to stderr. Default false + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **404** – no such container +- **500** – server error + ### Wait a container `POST /containers/(id)/wait` diff --git a/docs/sources/reference/api/docker_remote_api_v1.1.md b/docs/sources/reference/api/docker_remote_api_v1.1.md index 6cf7ed74b..7ddb4ee0e 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.1.md +++ b/docs/sources/reference/api/docker_remote_api_v1.1.md @@ -385,6 +385,41 @@ Status Codes: - **404** – no such container - **500** – server error +### Attach to a container (websocket) + +`GET /containers/(id)/attach/ws` + +Attach to the container `id` via websocket + +Implements websocket protocol handshake according to [RFC 6455](http://tools.ietf.org/html/rfc6455) + +**Example request** + + GET /containers/e90e34656806/attach/ws?logs=0&stream=1&stdin=1&stdout=1&stderr=1 HTTP/1.1 + +**Example response** + + {{ STREAM }} + +Query Parameters: + +- **logs** – 1/True/true or 0/False/false, return logs. Default false +- **stream** – 1/True/true or 0/False/false, return stream. + Default false +- **stdin** – 1/True/true or 0/False/false, if stream=true, attach + to stdin. Default false +- **stdout** – 1/True/true or 0/False/false, if logs=true, return + stdout log, if stream=true, attach to stdout. Default false +- **stderr** – 1/True/true or 0/False/false, if logs=true, return + stderr log, if stream=true, attach to stderr. Default false + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **404** – no such container +- **500** – server error + ### Wait a container `POST /containers/(id)/wait` diff --git a/docs/sources/reference/api/docker_remote_api_v1.10.md b/docs/sources/reference/api/docker_remote_api_v1.10.md index 2358da101..b9f421d38 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.10.md +++ b/docs/sources/reference/api/docker_remote_api_v1.10.md @@ -539,6 +539,41 @@ Status Codes: 4. Read the extracted size and output it on the correct output 5. Goto 1) +### Attach to a container (websocket) + +`GET /containers/(id)/attach/ws` + +Attach to the container `id` via websocket + +Implements websocket protocol handshake according to [RFC 6455](http://tools.ietf.org/html/rfc6455) + +**Example request** + + GET /containers/e90e34656806/attach/ws?logs=0&stream=1&stdin=1&stdout=1&stderr=1 HTTP/1.1 + +**Example response** + + {{ STREAM }} + +Query Parameters: + +- **logs** – 1/True/true or 0/False/false, return logs. Default false +- **stream** – 1/True/true or 0/False/false, return stream. + Default false +- **stdin** – 1/True/true or 0/False/false, if stream=true, attach + to stdin. Default false +- **stdout** – 1/True/true or 0/False/false, if logs=true, return + stdout log, if stream=true, attach to stdout. Default false +- **stderr** – 1/True/true or 0/False/false, if logs=true, return + stderr log, if stream=true, attach to stderr. Default false + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **404** – no such container +- **500** – server error + ### Wait a container `POST /containers/(id)/wait` diff --git a/docs/sources/reference/api/docker_remote_api_v1.11.md b/docs/sources/reference/api/docker_remote_api_v1.11.md index 6303f708e..97f6c5670 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.11.md +++ b/docs/sources/reference/api/docker_remote_api_v1.11.md @@ -574,6 +574,41 @@ Status Codes: 4. Read the extracted size and output it on the correct output 5. Goto 1) +### Attach to a container (websocket) + +`GET /containers/(id)/attach/ws` + +Attach to the container `id` via websocket + +Implements websocket protocol handshake according to [RFC 6455](http://tools.ietf.org/html/rfc6455) + +**Example request** + + GET /containers/e90e34656806/attach/ws?logs=0&stream=1&stdin=1&stdout=1&stderr=1 HTTP/1.1 + +**Example response** + + {{ STREAM }} + +Query Parameters: + +- **logs** – 1/True/true or 0/False/false, return logs. Default false +- **stream** – 1/True/true or 0/False/false, return stream. + Default false +- **stdin** – 1/True/true or 0/False/false, if stream=true, attach + to stdin. Default false +- **stdout** – 1/True/true or 0/False/false, if logs=true, return + stdout log, if stream=true, attach to stdout. Default false +- **stderr** – 1/True/true or 0/False/false, if logs=true, return + stderr log, if stream=true, attach to stderr. Default false + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **404** – no such container +- **500** – server error + ### Wait a container `POST /containers/(id)/wait` diff --git a/docs/sources/reference/api/docker_remote_api_v1.12.md b/docs/sources/reference/api/docker_remote_api_v1.12.md index 685d43ee5..a0e4b209d 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.12.md +++ b/docs/sources/reference/api/docker_remote_api_v1.12.md @@ -622,6 +622,41 @@ Status Codes: 4. Read the extracted size and output it on the correct output 5. Goto 1 +### Attach to a container (websocket) + +`GET /containers/(id)/attach/ws` + +Attach to the container `id` via websocket + +Implements websocket protocol handshake according to [RFC 6455](http://tools.ietf.org/html/rfc6455) + +**Example request** + + GET /containers/e90e34656806/attach/ws?logs=0&stream=1&stdin=1&stdout=1&stderr=1 HTTP/1.1 + +**Example response** + + {{ STREAM }} + +Query Parameters: + +- **logs** – 1/True/true or 0/False/false, return logs. Default false +- **stream** – 1/True/true or 0/False/false, return stream. + Default false +- **stdin** – 1/True/true or 0/False/false, if stream=true, attach + to stdin. Default false +- **stdout** – 1/True/true or 0/False/false, if logs=true, return + stdout log, if stream=true, attach to stdout. Default false +- **stderr** – 1/True/true or 0/False/false, if logs=true, return + stderr log, if stream=true, attach to stderr. Default false + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **404** – no such container +- **500** – server error + ### Wait a container `POST /containers/(id)/wait` diff --git a/docs/sources/reference/api/docker_remote_api_v1.13.md b/docs/sources/reference/api/docker_remote_api_v1.13.md index 2c38c9aa1..2ff844ce5 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.13.md +++ b/docs/sources/reference/api/docker_remote_api_v1.13.md @@ -615,6 +615,41 @@ Status Codes: 4. Read the extracted size and output it on the correct output 5. Goto 1 +### Attach to a container (websocket) + +`GET /containers/(id)/attach/ws` + +Attach to the container `id` via websocket + +Implements websocket protocol handshake according to [RFC 6455](http://tools.ietf.org/html/rfc6455) + +**Example request** + + GET /containers/e90e34656806/attach/ws?logs=0&stream=1&stdin=1&stdout=1&stderr=1 HTTP/1.1 + +**Example response** + + {{ STREAM }} + +Query Parameters: + +- **logs** – 1/True/true or 0/False/false, return logs. Default false +- **stream** – 1/True/true or 0/False/false, return stream. + Default false +- **stdin** – 1/True/true or 0/False/false, if stream=true, attach + to stdin. Default false +- **stdout** – 1/True/true or 0/False/false, if logs=true, return + stdout log, if stream=true, attach to stdout. Default false +- **stderr** – 1/True/true or 0/False/false, if logs=true, return + stderr log, if stream=true, attach to stderr. Default false + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **404** – no such container +- **500** – server error + ### Wait a container `POST /containers/(id)/wait` diff --git a/docs/sources/reference/api/docker_remote_api_v1.14.md b/docs/sources/reference/api/docker_remote_api_v1.14.md index 7ce0df677..237872df2 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.14.md +++ b/docs/sources/reference/api/docker_remote_api_v1.14.md @@ -625,6 +625,41 @@ Status Codes: 4. Read the extracted size and output it on the correct output 5. Goto 1 +### Attach to a container (websocket) + +`GET /containers/(id)/attach/ws` + +Attach to the container `id` via websocket + +Implements websocket protocol handshake according to [RFC 6455](http://tools.ietf.org/html/rfc6455) + +**Example request** + + GET /containers/e90e34656806/attach/ws?logs=0&stream=1&stdin=1&stdout=1&stderr=1 HTTP/1.1 + +**Example response** + + {{ STREAM }} + +Query Parameters: + +- **logs** – 1/True/true or 0/False/false, return logs. Default false +- **stream** – 1/True/true or 0/False/false, return stream. + Default false +- **stdin** – 1/True/true or 0/False/false, if stream=true, attach + to stdin. Default false +- **stdout** – 1/True/true or 0/False/false, if logs=true, return + stdout log, if stream=true, attach to stdout. Default false +- **stderr** – 1/True/true or 0/False/false, if logs=true, return + stderr log, if stream=true, attach to stderr. Default false + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **404** – no such container +- **500** – server error + ### Wait a container `POST /containers/(id)/wait` diff --git a/docs/sources/reference/api/docker_remote_api_v1.15.md b/docs/sources/reference/api/docker_remote_api_v1.15.md index 4d27a6150..5fa4b2275 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.15.md +++ b/docs/sources/reference/api/docker_remote_api_v1.15.md @@ -767,6 +767,41 @@ Status Codes: 4. Read the extracted size and output it on the correct output 5. Goto 1 +### Attach to a container (websocket) + +`GET /containers/(id)/attach/ws` + +Attach to the container `id` via websocket + +Implements websocket protocol handshake according to [RFC 6455](http://tools.ietf.org/html/rfc6455) + +**Example request** + + GET /containers/e90e34656806/attach/ws?logs=0&stream=1&stdin=1&stdout=1&stderr=1 HTTP/1.1 + +**Example response** + + {{ STREAM }} + +Query Parameters: + +- **logs** – 1/True/true or 0/False/false, return logs. Default false +- **stream** – 1/True/true or 0/False/false, return stream. + Default false +- **stdin** – 1/True/true or 0/False/false, if stream=true, attach + to stdin. Default false +- **stdout** – 1/True/true or 0/False/false, if logs=true, return + stdout log, if stream=true, attach to stdout. Default false +- **stderr** – 1/True/true or 0/False/false, if logs=true, return + stderr log, if stream=true, attach to stderr. Default false + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **404** – no such container +- **500** – server error + ### Wait a container `POST /containers/(id)/wait` diff --git a/docs/sources/reference/api/docker_remote_api_v1.16.md b/docs/sources/reference/api/docker_remote_api_v1.16.md index 500f1bea3..7ac638d3f 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.16.md +++ b/docs/sources/reference/api/docker_remote_api_v1.16.md @@ -713,6 +713,41 @@ Status Codes: 4. Read the extracted size and output it on the correct output 5. Goto 1 +### Attach to a container (websocket) + +`GET /containers/(id)/attach/ws` + +Attach to the container `id` via websocket + +Implements websocket protocol handshake according to [RFC 6455](http://tools.ietf.org/html/rfc6455) + +**Example request** + + GET /containers/e90e34656806/attach/ws?logs=0&stream=1&stdin=1&stdout=1&stderr=1 HTTP/1.1 + +**Example response** + + {{ STREAM }} + +Query Parameters: + +- **logs** – 1/True/true or 0/False/false, return logs. Default false +- **stream** – 1/True/true or 0/False/false, return stream. + Default false +- **stdin** – 1/True/true or 0/False/false, if stream=true, attach + to stdin. Default false +- **stdout** – 1/True/true or 0/False/false, if logs=true, return + stdout log, if stream=true, attach to stdout. Default false +- **stderr** – 1/True/true or 0/False/false, if logs=true, return + stderr log, if stream=true, attach to stderr. Default false + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **404** – no such container +- **500** – server error + ### Wait a container `POST /containers/(id)/wait` diff --git a/docs/sources/reference/api/docker_remote_api_v1.17.md b/docs/sources/reference/api/docker_remote_api_v1.17.md index f8bca77ed..5f6520d9d 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.17.md +++ b/docs/sources/reference/api/docker_remote_api_v1.17.md @@ -870,6 +870,41 @@ Status Codes: 4. Read the extracted size and output it on the correct output 5. Goto 1 +### Attach to a container (websocket) + +`GET /containers/(id)/attach/ws` + +Attach to the container `id` via websocket + +Implements websocket protocol handshake according to [RFC 6455](http://tools.ietf.org/html/rfc6455) + +**Example request** + + GET /containers/e90e34656806/attach/ws?logs=0&stream=1&stdin=1&stdout=1&stderr=1 HTTP/1.1 + +**Example response** + + {{ STREAM }} + +Query Parameters: + +- **logs** – 1/True/true or 0/False/false, return logs. Default false +- **stream** – 1/True/true or 0/False/false, return stream. + Default false +- **stdin** – 1/True/true or 0/False/false, if stream=true, attach + to stdin. Default false +- **stdout** – 1/True/true or 0/False/false, if logs=true, return + stdout log, if stream=true, attach to stdout. Default false +- **stderr** – 1/True/true or 0/False/false, if logs=true, return + stderr log, if stream=true, attach to stderr. Default false + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **404** – no such container +- **500** – server error + ### Wait a container `POST /containers/(id)/wait` diff --git a/docs/sources/reference/api/docker_remote_api_v1.2.md b/docs/sources/reference/api/docker_remote_api_v1.2.md index 46f428bc9..3438eab2d 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.2.md +++ b/docs/sources/reference/api/docker_remote_api_v1.2.md @@ -397,6 +397,41 @@ Status Codes: - **404** – no such container - **500** – server error +### Attach to a container (websocket) + +`GET /containers/(id)/attach/ws` + +Attach to the container `id` via websocket + +Implements websocket protocol handshake according to [RFC 6455](http://tools.ietf.org/html/rfc6455) + +**Example request** + + GET /containers/e90e34656806/attach/ws?logs=0&stream=1&stdin=1&stdout=1&stderr=1 HTTP/1.1 + +**Example response** + + {{ STREAM }} + +Query Parameters: + +- **logs** – 1/True/true or 0/False/false, return logs. Default false +- **stream** – 1/True/true or 0/False/false, return stream. + Default false +- **stdin** – 1/True/true or 0/False/false, if stream=true, attach + to stdin. Default false +- **stdout** – 1/True/true or 0/False/false, if logs=true, return + stdout log, if stream=true, attach to stdout. Default false +- **stderr** – 1/True/true or 0/False/false, if logs=true, return + stderr log, if stream=true, attach to stderr. Default false + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **404** – no such container +- **500** – server error + ### Wait a container `POST /containers/(id)/wait` diff --git a/docs/sources/reference/api/docker_remote_api_v1.3.md b/docs/sources/reference/api/docker_remote_api_v1.3.md index 3a0ea7ba1..004993b85 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.3.md +++ b/docs/sources/reference/api/docker_remote_api_v1.3.md @@ -445,6 +445,41 @@ Status Codes: - **404** – no such container - **500** – server error +### Attach to a container (websocket) + +`GET /containers/(id)/attach/ws` + +Attach to the container `id` via websocket + +Implements websocket protocol handshake according to [RFC 6455](http://tools.ietf.org/html/rfc6455) + +**Example request** + + GET /containers/e90e34656806/attach/ws?logs=0&stream=1&stdin=1&stdout=1&stderr=1 HTTP/1.1 + +**Example response** + + {{ STREAM }} + +Query Parameters: + +- **logs** – 1/True/true or 0/False/false, return logs. Default false +- **stream** – 1/True/true or 0/False/false, return stream. + Default false +- **stdin** – 1/True/true or 0/False/false, if stream=true, attach + to stdin. Default false +- **stdout** – 1/True/true or 0/False/false, if logs=true, return + stdout log, if stream=true, attach to stdout. Default false +- **stderr** – 1/True/true or 0/False/false, if logs=true, return + stderr log, if stream=true, attach to stderr. Default false + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **404** – no such container +- **500** – server error + ### Wait a container `POST /containers/(id)/wait` diff --git a/docs/sources/reference/api/docker_remote_api_v1.4.md b/docs/sources/reference/api/docker_remote_api_v1.4.md index ac18cd481..644cd9844 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.4.md +++ b/docs/sources/reference/api/docker_remote_api_v1.4.md @@ -460,6 +460,41 @@ Status Codes: - **404** – no such container - **500** – server error +### Attach to a container (websocket) + +`GET /containers/(id)/attach/ws` + +Attach to the container `id` via websocket + +Implements websocket protocol handshake according to [RFC 6455](http://tools.ietf.org/html/rfc6455) + +**Example request** + + GET /containers/e90e34656806/attach/ws?logs=0&stream=1&stdin=1&stdout=1&stderr=1 HTTP/1.1 + +**Example response** + + {{ STREAM }} + +Query Parameters: + +- **logs** – 1/True/true or 0/False/false, return logs. Default false +- **stream** – 1/True/true or 0/False/false, return stream. + Default false +- **stdin** – 1/True/true or 0/False/false, if stream=true, attach + to stdin. Default false +- **stdout** – 1/True/true or 0/False/false, if logs=true, return + stdout log, if stream=true, attach to stdout. Default false +- **stderr** – 1/True/true or 0/False/false, if logs=true, return + stderr log, if stream=true, attach to stderr. Default false + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **404** – no such container +- **500** – server error + ### Wait a container `POST /containers/(id)/wait` diff --git a/docs/sources/reference/api/docker_remote_api_v1.5.md b/docs/sources/reference/api/docker_remote_api_v1.5.md index 8e0ad9f49..c9d1de07f 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.5.md +++ b/docs/sources/reference/api/docker_remote_api_v1.5.md @@ -458,6 +458,41 @@ Status Codes: - **404** – no such container - **500** – server error +### Attach to a container (websocket) + +`GET /containers/(id)/attach/ws` + +Attach to the container `id` via websocket + +Implements websocket protocol handshake according to [RFC 6455](http://tools.ietf.org/html/rfc6455) + +**Example request** + + GET /containers/e90e34656806/attach/ws?logs=0&stream=1&stdin=1&stdout=1&stderr=1 HTTP/1.1 + +**Example response** + + {{ STREAM }} + +Query Parameters: + +- **logs** – 1/True/true or 0/False/false, return logs. Default false +- **stream** – 1/True/true or 0/False/false, return stream. + Default false +- **stdin** – 1/True/true or 0/False/false, if stream=true, attach + to stdin. Default false +- **stdout** – 1/True/true or 0/False/false, if logs=true, return + stdout log, if stream=true, attach to stdout. Default false +- **stderr** – 1/True/true or 0/False/false, if logs=true, return + stderr log, if stream=true, attach to stderr. Default false + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **404** – no such container +- **500** – server error + ### Wait a container `POST /containers/(id)/wait` diff --git a/docs/sources/reference/api/docker_remote_api_v1.6.md b/docs/sources/reference/api/docker_remote_api_v1.6.md index f55c114b0..cfbc0dbe0 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.6.md +++ b/docs/sources/reference/api/docker_remote_api_v1.6.md @@ -564,6 +564,41 @@ Status Codes: 4. Read the extracted size and output it on the correct output 5. Goto 1) +### Attach to a container (websocket) + +`GET /containers/(id)/attach/ws` + +Attach to the container `id` via websocket + +Implements websocket protocol handshake according to [RFC 6455](http://tools.ietf.org/html/rfc6455) + +**Example request** + + GET /containers/e90e34656806/attach/ws?logs=0&stream=1&stdin=1&stdout=1&stderr=1 HTTP/1.1 + +**Example response** + + {{ STREAM }} + +Query Parameters: + +- **logs** – 1/True/true or 0/False/false, return logs. Default false +- **stream** – 1/True/true or 0/False/false, return stream. + Default false +- **stdin** – 1/True/true or 0/False/false, if stream=true, attach + to stdin. Default false +- **stdout** – 1/True/true or 0/False/false, if logs=true, return + stdout log, if stream=true, attach to stdout. Default false +- **stderr** – 1/True/true or 0/False/false, if logs=true, return + stderr log, if stream=true, attach to stderr. Default false + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **404** – no such container +- **500** – server error + ### Wait a container `POST /containers/(id)/wait` diff --git a/docs/sources/reference/api/docker_remote_api_v1.7.md b/docs/sources/reference/api/docker_remote_api_v1.7.md index 69562dbbe..a7593afac 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.7.md +++ b/docs/sources/reference/api/docker_remote_api_v1.7.md @@ -509,6 +509,41 @@ Status Codes: 4. Read the extracted size and output it on the correct output 5. Goto 1) +### Attach to a container (websocket) + +`GET /containers/(id)/attach/ws` + +Attach to the container `id` via websocket + +Implements websocket protocol handshake according to [RFC 6455](http://tools.ietf.org/html/rfc6455) + +**Example request** + + GET /containers/e90e34656806/attach/ws?logs=0&stream=1&stdin=1&stdout=1&stderr=1 HTTP/1.1 + +**Example response** + + {{ STREAM }} + +Query Parameters: + +- **logs** – 1/True/true or 0/False/false, return logs. Default false +- **stream** – 1/True/true or 0/False/false, return stream. + Default false +- **stdin** – 1/True/true or 0/False/false, if stream=true, attach + to stdin. Default false +- **stdout** – 1/True/true or 0/False/false, if logs=true, return + stdout log, if stream=true, attach to stdout. Default false +- **stderr** – 1/True/true or 0/False/false, if logs=true, return + stderr log, if stream=true, attach to stderr. Default false + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **404** – no such container +- **500** – server error + ### Wait a container `POST /containers/(id)/wait` diff --git a/docs/sources/reference/api/docker_remote_api_v1.8.md b/docs/sources/reference/api/docker_remote_api_v1.8.md index 2176a334a..cee00c6b8 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.8.md +++ b/docs/sources/reference/api/docker_remote_api_v1.8.md @@ -557,6 +557,41 @@ Status Codes: 4. Read the extracted size and output it on the correct output 5. Goto 1) +### Attach to a container (websocket) + +`GET /containers/(id)/attach/ws` + +Attach to the container `id` via websocket + +Implements websocket protocol handshake according to [RFC 6455](http://tools.ietf.org/html/rfc6455) + +**Example request** + + GET /containers/e90e34656806/attach/ws?logs=0&stream=1&stdin=1&stdout=1&stderr=1 HTTP/1.1 + +**Example response** + + {{ STREAM }} + +Query Parameters: + +- **logs** – 1/True/true or 0/False/false, return logs. Default false +- **stream** – 1/True/true or 0/False/false, return stream. + Default false +- **stdin** – 1/True/true or 0/False/false, if stream=true, attach + to stdin. Default false +- **stdout** – 1/True/true or 0/False/false, if logs=true, return + stdout log, if stream=true, attach to stdout. Default false +- **stderr** – 1/True/true or 0/False/false, if logs=true, return + stderr log, if stream=true, attach to stderr. Default false + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **404** – no such container +- **500** – server error + ### Wait a container `POST /containers/(id)/wait` diff --git a/docs/sources/reference/api/docker_remote_api_v1.9.md b/docs/sources/reference/api/docker_remote_api_v1.9.md index 61102083d..f8748e96a 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.9.md +++ b/docs/sources/reference/api/docker_remote_api_v1.9.md @@ -561,6 +561,41 @@ Status Codes: 4. Read the extracted size and output it on the correct output 5. Goto 1) +### Attach to a container (websocket) + +`GET /containers/(id)/attach/ws` + +Attach to the container `id` via websocket + +Implements websocket protocol handshake according to [RFC 6455](http://tools.ietf.org/html/rfc6455) + +**Example request** + + GET /containers/e90e34656806/attach/ws?logs=0&stream=1&stdin=1&stdout=1&stderr=1 HTTP/1.1 + +**Example response** + + {{ STREAM }} + +Query Parameters: + +- **logs** – 1/True/true or 0/False/false, return logs. Default false +- **stream** – 1/True/true or 0/False/false, return stream. + Default false +- **stdin** – 1/True/true or 0/False/false, if stream=true, attach + to stdin. Default false +- **stdout** – 1/True/true or 0/False/false, if logs=true, return + stdout log, if stream=true, attach to stdout. Default false +- **stderr** – 1/True/true or 0/False/false, if logs=true, return + stderr log, if stream=true, attach to stderr. Default false + +Status Codes: + +- **200** – no error +- **400** – bad parameter +- **404** – no such container +- **500** – server error + ### Wait a container `POST /containers/(id)/wait` From 16913455bd36ecee7b482d4a4c969348a81bd6b9 Mon Sep 17 00:00:00 2001 From: Abin Shahab Date: Thu, 22 Jan 2015 03:58:43 +0000 Subject: [PATCH 400/513] Fixes apparmor regression Signed-off-by: Abin Shahab (github: ashahab-altiscale) Docker-DCO-1.1-Signed-off-by: Abin Shahab (github: ashahab-altiscale) --- daemon/execdriver/lxc/lxc_template.go | 4 ++-- daemon/execdriver/lxc/lxc_template_unit_test.go | 12 +++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/daemon/execdriver/lxc/lxc_template.go b/daemon/execdriver/lxc/lxc_template.go index 99bb16198..4ed2a45c8 100644 --- a/daemon/execdriver/lxc/lxc_template.go +++ b/daemon/execdriver/lxc/lxc_template.go @@ -75,8 +75,8 @@ lxc.aa_profile = unconfined # In non-privileged mode, lxc will automatically mount /proc and /sys in readonly mode # for security. See: http://man7.org/linux/man-pages/man5/lxc.container.conf.5.html lxc.mount.auto = proc sys - {{if .AppArmor}} -lxc.aa_profile = .AppArmorProfile + {{if .AppArmorProfile}} +lxc.aa_profile = {{.AppArmorProfile}} {{end}} {{end}} diff --git a/daemon/execdriver/lxc/lxc_template_unit_test.go b/daemon/execdriver/lxc/lxc_template_unit_test.go index e072f8dbb..bb622d4bc 100644 --- a/daemon/execdriver/lxc/lxc_template_unit_test.go +++ b/daemon/execdriver/lxc/lxc_template_unit_test.go @@ -248,7 +248,8 @@ func TestCustomLxcConfigMisc(t *testing.T) { } defer os.RemoveAll(root) os.MkdirAll(path.Join(root, "containers", "1"), 0777) - driver, err := NewDriver(root, "", false) + driver, err := NewDriver(root, "", true) + if err != nil { t.Fatal(err) } @@ -271,9 +272,10 @@ func TestCustomLxcConfigMisc(t *testing.T) { Bridge: "docker0", }, }, - ProcessConfig: processConfig, - CapAdd: []string{"net_admin", "syslog"}, - CapDrop: []string{"kill", "mknod"}, + ProcessConfig: processConfig, + CapAdd: []string{"net_admin", "syslog"}, + CapDrop: []string{"kill", "mknod"}, + AppArmorProfile: "lxc-container-default-with-nesting", } p, err := driver.generateLXCConfig(command) @@ -287,7 +289,7 @@ func TestCustomLxcConfigMisc(t *testing.T) { grepFile(t, p, "lxc.network.ipv4 = 10.10.10.10/24") grepFile(t, p, "lxc.network.ipv4.gateway = 10.10.10.1") grepFile(t, p, "lxc.network.flags = up") - + grepFile(t, p, "lxc.aa_profile = lxc-container-default-with-nesting") // hostname grepFile(t, p, "lxc.utsname = testhost") grepFile(t, p, "lxc.cgroup.cpuset.cpus = 0,1") From f9ba68ddfb8c35a22d41ff568e9f37954b1da5b3 Mon Sep 17 00:00:00 2001 From: Lorenz Leutgeb Date: Wed, 7 Jan 2015 14:08:34 +0100 Subject: [PATCH 401/513] doc: Improve article on HTTPS * Adjust header to match _page_title * Add instructions on deletion of CSRs and setting permissions * Simplify some path expressions and commands * Consqeuently use ~ instead of ${HOME} * Precise formulation ('key' vs. 'public key') * Fix wrong indentation of output of `openssl req` * Use dash ('--') instead of minus ('-') Remark on permissions: It's not a problem to `chmod 0400` the private keys, because the Docker daemon runs as root (can read the file anyway) and the Docker client runs as user. Signed-off-by: Lorenz Leutgeb --- docs/sources/articles/https.md | 65 +++++++++++++++++++++------------- 1 file changed, 40 insertions(+), 25 deletions(-) diff --git a/docs/sources/articles/https.md b/docs/sources/articles/https.md index 41ba2cce5..a79e28a5d 100644 --- a/docs/sources/articles/https.md +++ b/docs/sources/articles/https.md @@ -40,20 +40,20 @@ First generate CA private and public keys: Verifying - Enter pass phrase for ca-key.pem: $ openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -out ca.pem Enter pass phrase for ca-key.pem: - You are about to be asked to enter information that will be incorporated - into your certificate request. - What you are about to enter is what is called a Distinguished Name or a DN. - There are quite a few fields but you can leave some blank - For some fields there will be a default value, - If you enter '.', the field will be left blank. - ----- - Country Name (2 letter code) [AU]: - State or Province Name (full name) [Some-State]:Queensland - Locality Name (eg, city) []:Brisbane - Organization Name (eg, company) [Internet Widgits Pty Ltd]:Docker Inc - Organizational Unit Name (eg, section) []:Boot2Docker - Common Name (e.g. server FQDN or YOUR name) []:$HOST - Email Address []:Sven@home.org.au + You are about to be asked to enter information that will be incorporated + into your certificate request. + What you are about to enter is what is called a Distinguished Name or a DN. + There are quite a few fields but you can leave some blank + For some fields there will be a default value, + If you enter '.', the field will be left blank. + ----- + Country Name (2 letter code) [AU]: + State or Province Name (full name) [Some-State]:Queensland + Locality Name (eg, city) []:Brisbane + Organization Name (eg, company) [Internet Widgits Pty Ltd]:Docker Inc + Organizational Unit Name (eg, section) []:Boot2Docker + Common Name (e.g. server FQDN or YOUR name) []:$HOST + Email Address []:Sven@home.org.au Now that we have a CA, you can create a server key and certificate signing request (CSR). Make sure that "Common Name" (i.e., server FQDN or YOUR @@ -69,7 +69,7 @@ name) matches the hostname you will use to connect to Docker: e is 65537 (0x10001) $ openssl req -subj "/CN=$HOST" -new -key server-key.pem -out server.csr -Next, we're going to sign the key with our CA: +Next, we're going to sign the public key with our CA: $ openssl x509 -req -days 365 -in server.csr -CA ca.pem -CAkey ca-key.pem \ -CAcreateserial -out server-cert.pem @@ -93,7 +93,7 @@ config file: $ echo extendedKeyUsage = clientAuth > extfile.cnf -Now sign the key: +Now sign the public key: $ openssl x509 -req -days 365 -in client.csr -CA ca.pem -CAkey ca-key.pem \ -CAcreateserial -out cert.pem -extfile extfile.cnf @@ -102,6 +102,24 @@ Now sign the key: Getting CA Private Key Enter pass phrase for ca-key.pem: +After generating `cert.pem` and `server-cert.pem` you can safely remove the +two certificate signing requests: + + $ rm -v client.csr server.csr + +With a default `umask` of 022 your secret keys will be *world-readable* and +writable for you and your group. + +To remove write permissions for your keys in order to protect them from accidental +damage and make them only readable to you issue the following file mode changes: + + $ chmod -v 0400 ca-key.pem key.pem server-key.pem + +Certificates can be world-readable, but you might want to remove write access to +prevent accidental damage: + + $ chmod -v 0444 ca.pem server-cert.pem cert.pem + Now you can make the Docker daemon only accept connections from clients providing a certificate trusted by our CA: @@ -130,16 +148,13 @@ need to provide your client keys, certificates and trusted CA: ## Secure by default If you want to secure your Docker client connections by default, you can move -the files to the `.docker` directory in your home directory - and set the +the files to the `.docker` directory in your home directory -- and set the `DOCKER_HOST` and `DOCKER_TLS_VERIFY` variables as well (instead of passing `-H=tcp://:2376` and `--tlsverify` on every call). - $ mkdir -p ~/.docker - $ cp ca.pem ~/.docker/ca.pem - $ cp cert.pem ~/.docker/cert.pem - $ cp key.pem ~/.docker/key.pem - $ export DOCKER_HOST=tcp://:2376 - $ export DOCKER_TLS_VERIFY=1 + $ mkdir -pv ~/.docker + $ cp -v {ca,cert,key}.pem ~/.docker + $ export DOCKER_HOST=tcp://:2376 DOCKER_TLS_VERIFY=1 Docker will now connect securely by default: @@ -165,11 +180,11 @@ Docker in various other modes by mixing the flags. certificate and authenticate server based on given CA If found, the client will send its client certificate, so you just need -to drop your keys into `~/.docker/.pem`. Alternatively, +to drop your keys into `~/.docker/{ca,cert,key}.pem`. Alternatively, if you want to store your keys in another location, you can specify that location using the environment variable `DOCKER_CERT_PATH`. - $ export DOCKER_CERT_PATH=${HOME}/.docker/zone1/ + $ export DOCKER_CERT_PATH=~/.docker/zone1/ $ docker --tlsverify ps ### Connecting to the Secure Docker port using `curl` From 38f09de3346de0eba73813dae87870c3007fae55 Mon Sep 17 00:00:00 2001 From: Lorenz Leutgeb Date: Thu, 8 Jan 2015 23:19:23 +0100 Subject: [PATCH 402/513] doc: Editorial changes as suggested by @fredlf Refer to: * https://github.com/docker/docker/pull/9952#discussion_r22686652 * https://github.com/docker/docker/pull/9952#discussion_r22686804 Signed-off-by: Lorenz Leutgeb --- docs/sources/articles/https.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/sources/articles/https.md b/docs/sources/articles/https.md index a79e28a5d..9e3835534 100644 --- a/docs/sources/articles/https.md +++ b/docs/sources/articles/https.md @@ -107,11 +107,11 @@ two certificate signing requests: $ rm -v client.csr server.csr -With a default `umask` of 022 your secret keys will be *world-readable* and +With a default `umask` of 022, your secret keys will be *world-readable* and writable for you and your group. -To remove write permissions for your keys in order to protect them from accidental -damage and make them only readable to you issue the following file mode changes: +In order to protect your keys from accidental damage, you will want to remove their +write permissions. To make them read-only, change file modes as follows: $ chmod -v 0400 ca-key.pem key.pem server-key.pem From e130faea1bd797be9c86112e167c3acf1b5ff11a Mon Sep 17 00:00:00 2001 From: Lorenz Leutgeb Date: Fri, 9 Jan 2015 00:24:59 +0100 Subject: [PATCH 403/513] doc: Minor semantical/editorial fixes in HTTPS article "read-only" vs. "only readable by you" Refer to: https://github.com/docker/docker/pull/9952#discussion_r22690266 Signed-off-by: Lorenz Leutgeb --- docs/sources/articles/https.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/articles/https.md b/docs/sources/articles/https.md index 9e3835534..775573ec3 100644 --- a/docs/sources/articles/https.md +++ b/docs/sources/articles/https.md @@ -111,7 +111,7 @@ With a default `umask` of 022, your secret keys will be *world-readable* and writable for you and your group. In order to protect your keys from accidental damage, you will want to remove their -write permissions. To make them read-only, change file modes as follows: +write permissions. To make them only readable by you, change file modes as follows: $ chmod -v 0400 ca-key.pem key.pem server-key.pem From 7b2e67036f6cae08bdaf360467736bd4f2f11d8d Mon Sep 17 00:00:00 2001 From: Lorenz Leutgeb Date: Thu, 22 Jan 2015 21:46:01 +0100 Subject: [PATCH 404/513] Fix inconsistent formatting Colon was bold, but regular at other occurences. Blame cf27b310c4fc8d2c13ba181398a628d03e1e3c58 Signed-off-by: Lorenz Leutgeb --- docs/sources/articles/https.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/sources/articles/https.md b/docs/sources/articles/https.md index 775573ec3..8fb0bb869 100644 --- a/docs/sources/articles/https.md +++ b/docs/sources/articles/https.md @@ -26,7 +26,7 @@ it will only connect to servers with a certificate signed by that CA. ## Create a CA, server and client keys with OpenSSL -> **Note:** replace all instances of `$HOST` in the following example with the +> **Note**: replace all instances of `$HOST` in the following example with the > DNS name of your Docker daemon's host. First generate CA private and public keys: @@ -59,7 +59,7 @@ Now that we have a CA, you can create a server key and certificate signing request (CSR). Make sure that "Common Name" (i.e., server FQDN or YOUR name) matches the hostname you will use to connect to Docker: -> **Note:** replace all instances of `$HOST` in the following example with the +> **Note**: replace all instances of `$HOST` in the following example with the > DNS name of your Docker daemon's host. $ openssl genrsa -out server-key.pem 2048 @@ -129,7 +129,7 @@ providing a certificate trusted by our CA: To be able to connect to Docker and validate its certificate, you now need to provide your client keys, certificates and trusted CA: -> **Note:** replace all instances of `$HOST` in the following example with the +> **Note**: replace all instances of `$HOST` in the following example with the > DNS name of your Docker daemon's host. $ docker --tlsverify --tlscacert=ca.pem --tlscert=cert.pem --tlskey=key.pem \ From fe94ecb2c1169eb258e0785ce386816dd0b2e700 Mon Sep 17 00:00:00 2001 From: Tibor Vass Date: Thu, 22 Jan 2015 15:58:59 -0500 Subject: [PATCH 405/513] integration-cli: wait for container before sending ^D Signed-off-by: Tibor Vass --- integration-cli/docker_cli_attach_unix_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/integration-cli/docker_cli_attach_unix_test.go b/integration-cli/docker_cli_attach_unix_test.go index 829b14deb..a3bfa5b1c 100644 --- a/integration-cli/docker_cli_attach_unix_test.go +++ b/integration-cli/docker_cli_attach_unix_test.go @@ -81,6 +81,9 @@ func TestAttachAfterDetach(t *testing.T) { }() time.Sleep(500 * time.Millisecond) + if err := waitRun(name); err != nil { + t.Fatal(err) + } cpty.Write([]byte{16}) time.Sleep(100 * time.Millisecond) cpty.Write([]byte{17}) From 018ab080bb31370268708202ad85eaa5a217d011 Mon Sep 17 00:00:00 2001 From: Tianon Gravi Date: Thu, 22 Jan 2015 15:42:17 -0700 Subject: [PATCH 406/513] Remove windows from the list of supported platforms Since it can still be tested natively without this, this won't cause any harm while we fix the tests to actually work on Windows. Signed-off-by: Andrew "Tianon" Page --- Dockerfile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index c452a6ad9..50920945e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -84,8 +84,10 @@ RUN cd /usr/local/go/src && ./make.bash --no-clean 2>&1 ENV DOCKER_CROSSPLATFORMS \ linux/386 linux/arm \ darwin/amd64 darwin/386 \ - freebsd/amd64 freebsd/386 freebsd/arm \ - windows/amd64 windows/386 + freebsd/amd64 freebsd/386 freebsd/arm + +# TODO when https://jenkins.dockerproject.com/job/Windows/ is green, add windows back to the list above +# windows/amd64 windows/386 # (set an explicit GOARM of 5 for maximum compatibility) ENV GOARM 5 From f91fbe39cedda8614d0f67d2e86e0708a16f0480 Mon Sep 17 00:00:00 2001 From: GennadySpb Date: Thu, 22 Jan 2015 12:07:20 +0300 Subject: [PATCH 407/513] Update using_supervisord.md Fix factual error change made by: GennadySpb Signed-off-by: Sven Dowideit --- docs/sources/articles/using_supervisord.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/articles/using_supervisord.md b/docs/sources/articles/using_supervisord.md index 01e60b659..5806707ee 100644 --- a/docs/sources/articles/using_supervisord.md +++ b/docs/sources/articles/using_supervisord.md @@ -39,7 +39,7 @@ our container. Here we're installing the `openssh-server`, `apache2` and `supervisor` -(which provides the Supervisor daemon) packages. We're also creating two +(which provides the Supervisor daemon) packages. We're also creating four new directories that are needed to run our SSH daemon and Supervisor. ## Adding Supervisor's configuration file From 08f2fad40b61a9f1ee2bef30b0b4b68803e66795 Mon Sep 17 00:00:00 2001 From: Tony Miller Date: Thu, 22 Jan 2015 23:06:21 +0900 Subject: [PATCH 408/513] document the ExtraHosts parameter for /containers/create for the remote API I think this was added from version 1.15. Signed-off-by: Tony Miller --- docs/sources/reference/api/docker_remote_api_v1.15.md | 3 +++ docs/sources/reference/api/docker_remote_api_v1.16.md | 3 +++ docs/sources/reference/api/docker_remote_api_v1.17.md | 3 +++ 3 files changed, 9 insertions(+) diff --git a/docs/sources/reference/api/docker_remote_api_v1.15.md b/docs/sources/reference/api/docker_remote_api_v1.15.md index 5fa4b2275..229a05b1b 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.15.md +++ b/docs/sources/reference/api/docker_remote_api_v1.15.md @@ -148,6 +148,7 @@ Create a container "Privileged": false, "Dns": ["8.8.8.8"], "DnsSearch": [""], + "ExtraHosts": null, "VolumesFrom": ["parent", "other:ro"], "CapAdd": ["NET_ADMIN"], "CapDrop": ["MKNOD"], @@ -220,6 +221,8 @@ Json Parameters: a boolean value. - **Dns** - A list of dns servers for the container to use. - **DnsSearch** - A list of DNS search domains + - **ExtraHosts** - A list of hostnames/IP mappings to be added to the + container's `/etc/host` file. Specified in the form `["hostname:IP"]`. - **VolumesFrom** - A list of volumes to inherit from another container. Specified in the form `[:]` - **CapAdd** - A list of kernel capabilties to add to the container. diff --git a/docs/sources/reference/api/docker_remote_api_v1.16.md b/docs/sources/reference/api/docker_remote_api_v1.16.md index 7ac638d3f..c701a58bf 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.16.md +++ b/docs/sources/reference/api/docker_remote_api_v1.16.md @@ -148,6 +148,7 @@ Create a container "Privileged": false, "Dns": ["8.8.8.8"], "DnsSearch": [""], + "ExtraHosts": null, "VolumesFrom": ["parent", "other:ro"], "CapAdd": ["NET_ADMIN"], "CapDrop": ["MKNOD"], @@ -220,6 +221,8 @@ Json Parameters: a boolean value. - **Dns** - A list of dns servers for the container to use. - **DnsSearch** - A list of DNS search domains + - **ExtraHosts** - A list of hostnames/IP mappings to be added to the + container's `/etc/host` file. Specified in the form `["hostname:IP"]`. - **VolumesFrom** - A list of volumes to inherit from another container. Specified in the form `[:]` - **CapAdd** - A list of kernel capabilties to add to the container. diff --git a/docs/sources/reference/api/docker_remote_api_v1.17.md b/docs/sources/reference/api/docker_remote_api_v1.17.md index 5f6520d9d..400e19714 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.17.md +++ b/docs/sources/reference/api/docker_remote_api_v1.17.md @@ -149,6 +149,7 @@ Create a container "ReadonlyRootfs": false, "Dns": ["8.8.8.8"], "DnsSearch": [""], + "ExtraHosts": null, "VolumesFrom": ["parent", "other:ro"], "CapAdd": ["NET_ADMIN"], "CapDrop": ["MKNOD"], @@ -223,6 +224,8 @@ Json Parameters: Specified as a boolean value. - **Dns** - A list of dns servers for the container to use. - **DnsSearch** - A list of DNS search domains + - **ExtraHosts** - A list of hostnames/IP mappings to be added to the + container's `/etc/host` file. Specified in the form `["hostname:IP"]`. - **VolumesFrom** - A list of volumes to inherit from another container. Specified in the form `[:]` - **CapAdd** - A list of kernel capabilties to add to the container. From 24d81b0ddbccd02c6a684030789d67509fa426fd Mon Sep 17 00:00:00 2001 From: Josh Hawn Date: Fri, 23 Jan 2015 09:54:17 -0800 Subject: [PATCH 409/513] Always store images with tarsum.v1 checksum added Updates `image.StoreImage()` to always ensure that images that are installed in Docker have a tarsum.v1 checksum. Docker-DCO-1.1-Signed-off-by: Josh Hawn (github: jlhawn) --- image/image.go | 13 ++++++++----- pkg/tarsum/tarsum.go | 1 + pkg/tarsum/versioning.go | 12 ++++++++++++ 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/image/image.go b/image/image.go index 7664602cd..3cf26f37c 100644 --- a/image/image.go +++ b/image/image.go @@ -81,8 +81,8 @@ func LoadImage(root string) (*Image, error) { // StoreImage stores file system layer data for the given image to the // image's registered storage driver. Image metadata is stored in a file -// at the specified root directory. This function also computes the TarSum -// of `layerData` (currently using tarsum.dev). +// at the specified root directory. This function also computes a checksum +// of `layerData` if the image does not have one already. func StoreImage(img *Image, layerData archive.ArchiveReader, root string) error { // Store the layer var ( @@ -96,15 +96,18 @@ func StoreImage(img *Image, layerData archive.ArchiveReader, root string) error if layerData != nil { // If the image doesn't have a checksum, we should add it. The layer // checksums are verified when they are pulled from a remote, but when - // a container is committed it should be added here. - if img.Checksum == "" { + // a container is committed it should be added here. Also ensure that + // the stored checksum has the latest version of tarsum (assuming we + // are using tarsum). + if tarsum.VersionLabelForChecksum(img.Checksum) != tarsum.Version1.String() { + // Either there was no checksum or it's not a tarsum.v1 layerDataDecompressed, err := archive.DecompressStream(layerData) if err != nil { return err } defer layerDataDecompressed.Close() - if layerTarSum, err = tarsum.NewTarSum(layerDataDecompressed, true, tarsum.VersionDev); err != nil { + if layerTarSum, err = tarsum.NewTarSum(layerDataDecompressed, true, tarsum.Version1); err != nil { return err } diff --git a/pkg/tarsum/tarsum.go b/pkg/tarsum/tarsum.go index c6a7294e7..88fcbe4a9 100644 --- a/pkg/tarsum/tarsum.go +++ b/pkg/tarsum/tarsum.go @@ -122,6 +122,7 @@ type tHashConfig struct { } var ( + // NOTE: DO NOT include MD5 or SHA1, which are considered insecure. standardHashConfigs = map[string]tHashConfig{ "sha256": {name: "sha256", hash: crypto.SHA256}, "sha512": {name: "sha512", hash: crypto.SHA512}, diff --git a/pkg/tarsum/versioning.go b/pkg/tarsum/versioning.go index be1d07040..0ceb5298a 100644 --- a/pkg/tarsum/versioning.go +++ b/pkg/tarsum/versioning.go @@ -22,6 +22,18 @@ const ( VersionDev ) +// VersionLabelForChecksum returns the label for the given tarsum +// checksum, i.e., everything before the first `+` character in +// the string or an empty string if no label separator is found. +func VersionLabelForChecksum(checksum string) string { + // Checksums are in the form: {versionLabel}+{hashID}:{hex} + sepIndex := strings.Index(checksum, "+") + if sepIndex < 0 { + return "" + } + return checksum[:sepIndex] +} + // Get a list of all known tarsum Version func GetVersions() []Version { v := []Version{} From a080e2add7f70b205276b4898ec3049da3f2fd3e Mon Sep 17 00:00:00 2001 From: Jessica Frazelle Date: Fri, 23 Jan 2015 13:17:54 -0800 Subject: [PATCH 410/513] Make debugs logs suck less. Docker-DCO-1.1-Signed-off-by: Jessica Frazelle (github: jfrazelle) --- graph/pull.go | 1 - image/image.go | 2 -- 2 files changed, 3 deletions(-) diff --git a/graph/pull.go b/graph/pull.go index f76a15605..f9c5c7b42 100644 --- a/graph/pull.go +++ b/graph/pull.go @@ -153,7 +153,6 @@ func (s *TagStore) pullRepository(r *registry.Session, out io.Writer, repoInfo * for _, image := range repoData.ImgList { downloadImage := func(img *registry.ImgData) { if askedTag != "" && img.Tag != askedTag { - log.Debugf("(%s) does not match %s (id: %s), skipping", img.Tag, askedTag, img.ID) if parallel { errors <- nil } diff --git a/image/image.go b/image/image.go index 3cf26f37c..a4839e98f 100644 --- a/image/image.go +++ b/image/image.go @@ -9,7 +9,6 @@ import ( "strconv" "time" - log "github.com/Sirupsen/logrus" "github.com/docker/docker/pkg/archive" "github.com/docker/docker/pkg/tarsum" "github.com/docker/docker/runconfig" @@ -277,7 +276,6 @@ func (img *Image) CheckDepth() error { func NewImgJSON(src []byte) (*Image, error) { ret := &Image{} - log.Debugf("Json string: {%s}", src) // FIXME: Is there a cleaner way to "purify" the input json? if err := json.Unmarshal(src, ret); err != nil { return nil, err From c67d3e159c39a16489a03474ffe427b623bb7eb5 Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Fri, 23 Jan 2015 14:44:30 -0800 Subject: [PATCH 411/513] Use filepath instead of path Currently loading the trust key uses path instead of filepath. This creates problems on some operating systems such as Windows. Fixes #10319 Signed-off-by: Derek McGowan (github: dmcgowan) --- api/common.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/common.go b/api/common.go index fb3eefaca..a96a4066a 100644 --- a/api/common.go +++ b/api/common.go @@ -4,7 +4,7 @@ import ( "fmt" "mime" "os" - "path" + "path/filepath" "strings" log "github.com/Sirupsen/logrus" @@ -55,7 +55,7 @@ func MatchesContentType(contentType, expectedType string) bool { // LoadOrCreateTrustKey attempts to load the libtrust key at the given path, // otherwise generates a new one func LoadOrCreateTrustKey(trustKeyPath string) (libtrust.PrivateKey, error) { - err := os.MkdirAll(path.Dir(trustKeyPath), 0700) + err := os.MkdirAll(filepath.Dir(trustKeyPath), 0700) if err != nil { return nil, err } From 32aceadbe6bea2745cd552cb6840d854fa898753 Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Fri, 23 Jan 2015 17:24:05 -0800 Subject: [PATCH 412/513] Revert progressreader to not defer close When progress reader closes it overwrites the progress line with the full progress bar, replaces the completed message. Signed-off-by: Derek McGowan (github: dmcgowan) --- graph/push.go | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/graph/push.go b/graph/push.go index b8fb09882..3a9f1ace0 100644 --- a/graph/push.go +++ b/graph/push.go @@ -236,10 +236,7 @@ func (s *TagStore) pushImage(r *registry.Session, out io.Writer, imgID, ep strin // Send the layer log.Debugf("rendered layer for %s of [%d] size", imgData.ID, layerData.Size) - prgRd := utils.ProgressReader(layerData, int(layerData.Size), out, sf, false, utils.TruncateID(imgData.ID), "Pushing") - defer prgRd.Close() - - checksum, checksumPayload, err := r.PushImageLayerRegistry(imgData.ID, prgRd, ep, token, jsonRaw) + checksum, checksumPayload, err := r.PushImageLayerRegistry(imgData.ID, utils.ProgressReader(layerData, int(layerData.Size), out, sf, false, utils.TruncateID(imgData.ID), "Pushing"), ep, token, jsonRaw) if err != nil { return "", err } @@ -343,10 +340,7 @@ func (s *TagStore) pushV2Repository(r *registry.Session, eng *engine.Engine, out } if !exists { - prgRd := utils.ProgressReader(arch, int(img.Size), out, sf, false, utils.TruncateID(img.ID), "Pushing") - defer prgRd.Close() - - err = r.PutV2ImageBlob(endpoint, repoInfo.RemoteName, sumParts[0], manifestSum, prgRd, auth) + err = r.PutV2ImageBlob(endpoint, repoInfo.RemoteName, sumParts[0], manifestSum, utils.ProgressReader(arch, int(img.Size), out, sf, false, utils.TruncateID(img.ID), "Pushing"), auth) if err != nil { out.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Image push failed", nil)) return err From 723684525a530cd284c4ec6470700b8e73b0c1f3 Mon Sep 17 00:00:00 2001 From: unclejack Date: Sat, 24 Jan 2015 08:43:03 +0200 Subject: [PATCH 413/513] pkg/archive: remove tar autodetection log line Signed-off-by: Cristian Staretu --- pkg/archive/archive.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/archive/archive.go b/pkg/archive/archive.go index 35566520b..68e5c1d30 100644 --- a/pkg/archive/archive.go +++ b/pkg/archive/archive.go @@ -101,7 +101,6 @@ func DecompressStream(archive io.Reader) (io.ReadCloser, error) { if err != nil { return nil, err } - log.Debugf("[tar autodetect] n: %v", bs) compression := DetectCompression(bs) switch compression { From 48754d673c040819b54be7079505265a58046d39 Mon Sep 17 00:00:00 2001 From: DiuDiugirl Date: Sat, 24 Jan 2015 15:05:44 +0800 Subject: [PATCH 414/513] Fix a minor typo Docker inspect can also be used on images, this patch fixed the minor typo in file docker/flags.go and docs/man/docker.1.md Signed-off-by: DiuDiugirl --- docker/flags.go | 2 +- docs/man/docker.1.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/flags.go b/docker/flags.go index 3b54612e8..d91a9a1de 100644 --- a/docker/flags.go +++ b/docker/flags.go @@ -97,7 +97,7 @@ func init() { {"images", "List images"}, {"import", "Create a new filesystem image from the contents of a tarball"}, {"info", "Display system-wide information"}, - {"inspect", "Return low-level information on a container"}, + {"inspect", "Return low-level information on a container or image"}, {"kill", "Kill a running container"}, {"load", "Load an image from a tar archive"}, {"login", "Register or log in to a Docker registry server"}, diff --git a/docs/man/docker.1.md b/docs/man/docker.1.md index 3b4367b07..456680b52 100644 --- a/docs/man/docker.1.md +++ b/docs/man/docker.1.md @@ -144,7 +144,7 @@ unix://[/path/to/socket] to use. Display system-wide information **docker-inspect(1)** - Return low-level information on a container + Return low-level information on a container or image **docker-kill(1)** Kill a running container (which includes the wrapper process and everything From ac8fd856c0240b215ac8870116f5ae99a362da0b Mon Sep 17 00:00:00 2001 From: Euan Date: Sat, 24 Jan 2015 13:08:47 -0800 Subject: [PATCH 415/513] Allow empty layer configs in manifests Before the V2 registry changes, images with no config could be pushed. This change fixes a regression that made those images not able to be pushed to a registry. Signed-off-by: Euan Kemp --- graph/manifest.go | 8 +++--- integration-cli/docker_cli_push_test.go | 36 +++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/graph/manifest.go b/graph/manifest.go index 3d4ab1c5d..18784bb1e 100644 --- a/graph/manifest.go +++ b/graph/manifest.go @@ -3,7 +3,6 @@ package graph import ( "bytes" "encoding/json" - "errors" "fmt" "io" "io/ioutil" @@ -71,14 +70,13 @@ func (s *TagStore) newManifest(localName, remoteName, tag string) ([]byte, error if err != nil { return nil, err } - if layer.Config == nil { - return nil, errors.New("Missing layer configuration") - } manifest.Architecture = layer.Architecture manifest.FSLayers = make([]*registry.FSLayer, 0, 4) manifest.History = make([]*registry.ManifestHistory, 0, 4) var metadata runconfig.Config - metadata = *layer.Config + if layer.Config != nil { + metadata = *layer.Config + } for ; layer != nil; layer, err = layer.GetParent() { if err != nil { diff --git a/integration-cli/docker_cli_push_test.go b/integration-cli/docker_cli_push_test.go index 484e5db70..0b2decde7 100644 --- a/integration-cli/docker_cli_push_test.go +++ b/integration-cli/docker_cli_push_test.go @@ -2,10 +2,14 @@ package main import ( "fmt" + "io/ioutil" + "os" "os/exec" "strings" "testing" "time" + + "github.com/docker/docker/vendor/src/code.google.com/p/go/src/pkg/archive/tar" ) // pulling an image from the central registry should work @@ -80,3 +84,35 @@ func TestPushInterrupt(t *testing.T) { logDone("push - interrupted") } + +func TestPushEmptyLayer(t *testing.T) { + defer setupRegistry(t)() + repoName := fmt.Sprintf("%v/dockercli/emptylayer", privateRegistryURL) + emptyTarball, err := ioutil.TempFile("", "empty_tarball") + if err != nil { + t.Fatalf("Unable to create test file: %v", err) + } + tw := tar.NewWriter(emptyTarball) + err = tw.Close() + if err != nil { + t.Fatalf("Error creating empty tarball: %v", err) + } + freader, err := os.Open(emptyTarball.Name()) + if err != nil { + t.Fatalf("Could not open test tarball: %v", err) + } + + importCmd := exec.Command(dockerBinary, "import", "-", repoName) + importCmd.Stdin = freader + out, _, err := runCommandWithOutput(importCmd) + if err != nil { + t.Errorf("import failed with errors: %v, output: %q", err, out) + } + + // Now verify we can push it + pushCmd := exec.Command(dockerBinary, "push", repoName) + if out, _, err := runCommandWithOutput(pushCmd); err != nil { + t.Fatalf("pushing the image to the private registry has failed: %s, %v", out, err) + } + logDone("push - empty layer config to private registry") +} From 6646cff646c3a0ec5add727f88d60ecd7b3fbafb Mon Sep 17 00:00:00 2001 From: unclejack Date: Mon, 26 Jan 2015 08:11:20 +0200 Subject: [PATCH 416/513] docs: shrink sprites-small_360.png Signed-off-by: Cristian Staretu --- .../mkdocs/img/footer/sprites-small_360.png | Bin 20957 -> 2763 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/theme/mkdocs/img/footer/sprites-small_360.png b/docs/theme/mkdocs/img/footer/sprites-small_360.png index c28863e3f5c7ceb64b5a69345c757da563de85ed..92af5c7092f635532ab5d7ce695f5b33779a212c 100644 GIT binary patch literal 2763 zcmeH|`9Bkm1IC9O(cDL)FjttS9CI@v<`~MEBXx+({y5!)7u` z6n%wUxxN@`!q@-t{XDPN^E|)5U$5u&q*x)1f$YNU0001pFfqJy-ZSUE%*J${O@f0y z0085zmAQ?Pxp&~%zxtQJ{|y0>-o>2r4bVgGAnyXW7+JsS^-+MaW8;#X7bd9;Xl{G@ zf0>G>*uE8fDrD#VeSFEQR%V`-p~xci_r&7Q#Vro8kJ{UDpvO|M-t_}v=~s0>gRDx| z67hG9??K#VjdJy`A-i5is+wvI^1t?c5O1SU4kgOUAyTYM7MlD9RCtn>yqO8Zt1ENj zp@;HSMH1?#{ZEE&Rs`#&x%fnE-srq_zfmCndcv+*p0Cnn%%iI_vg-|x8h+?_ zxOY^4a4@(0Cmrh)*81)({F!Y4Dp?~Wy*f)90ANW+80y=EGZ1HOO3v-AAZAZrxi8}1)I3YEA@bvwp z73|PA{Lc2{X~~&O`?L`2UD_4QS$yzPrnsfhxODbB1%6h*?*=2-P*yVT(7r#%13BU? zRxkofw)lF3)5l*zN` zCKMVa);ZoIP3a5I_kL1IOIfip88U23lJ@=$egnNWxukhI#t){L=L%_>?TblxzgcX& zJ^4|DJ-wZRNU#u%V3NnbM4U-|&ayY||M<|QLL#ql?fbLVQ~&%%BXq5<%})y%g&Dpqa39yNmmPDkIp-MTp=jIN~uPg7h z21`66e=*<&nX|SsinB?HOw`}t1c)x#&&#N7w7X`5)xQY9_VOM5J(Yj9HB!djM=E** z*C;Wk29U;@HL@u@R<)HDJ{*4Ea+24-72HODt$=YCvr-%ksFDYiIi9{7ISLIZ>`{odwC) zD-<5U(t;+4e{6&NN9wwdjh}-K`wNW%fD!qHs@&!CI>y*UTT@*2jZJb(L35Qyw63(6 z3+4k!aqm|y(sL5k!K~I)J5XFu(3CXXpt@N5h*2Sjd2oY)tiUj%Stj`h2iosWKE3&MPKgpJm{@0y?vjb7E5YS{_6E zt2&N{>jp3edE$wkT9=f6`0UW7-cq*{w!>c@jR0gZnH+6piv=B-sjTi)V)NEkzm19o zN*Hw5fMaMXs;p=phoU^9?y-~Df`HozZA@^Go?0KF-X{{MTw2&Fgbq`2;z@IXcG6Zo zU(3;nCu`f`R}m$njjt@y!sarIZNml(`9HTeZAm%$e;b4m_m4>b*|R@JUQ(FjU*zO4sDFW85aQlI2Ee@KTQ!KqGye|-=< zt7Wn6R1B~+@x-u=W(V0^aVpDeQG`OFwjy~^CXNHQ>HTm)m7QL5;PaYT5Tzv&b+6EWMA!JT ztyW|_9Z7F>R|J(HqGXHA0}{UDp9%0Y)3?yhRnk<1By}&?WDz@*#anSx)P&om%cHpB ztO|J1hZp9HN^xuS` z!LdmRS0TRHeH&j~s&300j`|Z~esI3(edAt8ZVaWxH#K2LGc4P49cxaTI*@VS49inQ z%9QuY>GfL7_{ByWVn-8RykBc;-Vq|I0rACYr-ypS^Z7gAWRZ&Vg9adskcN%79>)C- D{E4;m literal 20957 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 Date: Fri, 23 Jan 2015 14:14:52 -0500 Subject: [PATCH 417/513] Fix bind-mounts only partially removed When calling delete on a bind-mount volume, the config file was bing removed, but it was not actually being removed from the volume index. Signed-off-by: Brian Goff --- volumes/repository.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/volumes/repository.go b/volumes/repository.go index 821995224..91f98fc06 100644 --- a/volumes/repository.go +++ b/volumes/repository.go @@ -169,13 +169,11 @@ func (r *Repository) Delete(path string) error { return err } - if volume.IsBindMount { - return nil - } - - if err := r.driver.Remove(volume.ID); err != nil { - if !os.IsNotExist(err) { - return err + if !volume.IsBindMount { + if err := r.driver.Remove(volume.ID); err != nil { + if !os.IsNotExist(err) { + return err + } } } From 4262cfe41fcc9c7a983ca283d8923d8335e2a5ba Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Mon, 26 Jan 2015 11:16:29 -0800 Subject: [PATCH 418/513] Remove omitempty json tags from stucts When unmarshaling the json response from the API in languages to a dynamic object having the omitempty field tag on types such as float64 case the key to be omitted on 0.0 values. Various langages will interpret this as a null when 0.0 is the actual value. This patch removes the omitempty tags on fields that are not structs where they can be safely omited. Signed-off-by: Michael Crosby --- api/stats/stats.go | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/api/stats/stats.go b/api/stats/stats.go index d58fdd4f5..8edf18fe0 100644 --- a/api/stats/stats.go +++ b/api/stats/stats.go @@ -6,21 +6,21 @@ import "time" type ThrottlingData struct { // Number of periods with throttling active - Periods uint64 `json:"periods,omitempty"` + Periods uint64 `json:"periods"` // Number of periods when the container hit its throttling limit. - ThrottledPeriods uint64 `json:"throttled_periods,omitempty"` + ThrottledPeriods uint64 `json:"throttled_periods"` // Aggregate time the container was throttled for in nanoseconds. - ThrottledTime uint64 `json:"throttled_time,omitempty"` + ThrottledTime uint64 `json:"throttled_time"` } // All CPU stats are aggregated since container inception. type CpuUsage struct { // Total CPU time consumed. // Units: nanoseconds. - TotalUsage uint64 `json:"total_usage,omitempty"` + TotalUsage uint64 `json:"total_usage"` // Total CPU time consumed per core. // Units: nanoseconds. - PercpuUsage []uint64 `json:"percpu_usage,omitempty"` + PercpuUsage []uint64 `json:"percpu_usage"` // Time spent by tasks of the cgroup in kernel mode. // Units: nanoseconds. UsageInKernelmode uint64 `json:"usage_in_kernelmode"` @@ -30,41 +30,41 @@ type CpuUsage struct { } type CpuStats struct { - CpuUsage CpuUsage `json:"cpu_usage,omitempty"` + CpuUsage CpuUsage `json:"cpu_usage"` SystemUsage uint64 `json:"system_cpu_usage"` ThrottlingData ThrottlingData `json:"throttling_data,omitempty"` } type MemoryStats struct { // current res_counter usage for memory - Usage uint64 `json:"usage,omitempty"` + Usage uint64 `json:"usage"` // maximum usage ever recorded. - MaxUsage uint64 `json:"max_usage,omitempty"` + MaxUsage uint64 `json:"max_usage"` // TODO(vishh): Export these as stronger types. // all the stats exported via memory.stat. - Stats map[string]uint64 `json:"stats,omitempty"` + Stats map[string]uint64 `json:"stats"` // number of times memory usage hits limits. Failcnt uint64 `json:"failcnt"` Limit uint64 `json:"limit"` } type BlkioStatEntry struct { - Major uint64 `json:"major,omitempty"` - Minor uint64 `json:"minor,omitempty"` - Op string `json:"op,omitempty"` - Value uint64 `json:"value,omitempty"` + Major uint64 `json:"major"` + Minor uint64 `json:"minor"` + Op string `json:"op"` + Value uint64 `json:"value"` } type BlkioStats struct { // number of bytes tranferred to and from the block device - IoServiceBytesRecursive []BlkioStatEntry `json:"io_service_bytes_recursive,omitempty"` - IoServicedRecursive []BlkioStatEntry `json:"io_serviced_recursive,omitempty"` - IoQueuedRecursive []BlkioStatEntry `json:"io_queue_recursive,omitempty"` - IoServiceTimeRecursive []BlkioStatEntry `json:"io_service_time_recursive,omitempty"` - IoWaitTimeRecursive []BlkioStatEntry `json:"io_wait_time_recursive,omitempty"` - IoMergedRecursive []BlkioStatEntry `json:"io_merged_recursive,omitempty"` - IoTimeRecursive []BlkioStatEntry `json:"io_time_recursive,omitempty"` - SectorsRecursive []BlkioStatEntry `json:"sectors_recursive,omitempty"` + IoServiceBytesRecursive []BlkioStatEntry `json:"io_service_bytes_recursive"` + IoServicedRecursive []BlkioStatEntry `json:"io_serviced_recursive"` + IoQueuedRecursive []BlkioStatEntry `json:"io_queue_recursive"` + IoServiceTimeRecursive []BlkioStatEntry `json:"io_service_time_recursive"` + IoWaitTimeRecursive []BlkioStatEntry `json:"io_wait_time_recursive"` + IoMergedRecursive []BlkioStatEntry `json:"io_merged_recursive"` + IoTimeRecursive []BlkioStatEntry `json:"io_time_recursive"` + SectorsRecursive []BlkioStatEntry `json:"sectors_recursive"` } type Network struct { From 12ccde442a246e32e048fde4a954747886720ad7 Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Wed, 21 Jan 2015 21:40:19 -0500 Subject: [PATCH 419/513] Do not return err on symlink eval Signed-off-by: Brian Goff --- integration-cli/docker_cli_daemon_test.go | 74 +++++++++++++++++++++++ volumes/repository.go | 7 ++- 2 files changed, 78 insertions(+), 3 deletions(-) diff --git a/integration-cli/docker_cli_daemon_test.go b/integration-cli/docker_cli_daemon_test.go index 95188296d..d17e8093a 100644 --- a/integration-cli/docker_cli_daemon_test.go +++ b/integration-cli/docker_cli_daemon_test.go @@ -403,3 +403,77 @@ func TestDaemonKeyMigration(t *testing.T) { logDone("daemon - key migration") } + +// Simulate an older daemon (pre 1.3) coming up with volumes specified in containers +// without corrosponding volume json +func TestDaemonUpgradeWithVolumes(t *testing.T) { + d := NewDaemon(t) + + graphDir := filepath.Join(os.TempDir(), "docker-test") + defer os.RemoveAll(graphDir) + if err := d.StartWithBusybox("-g", graphDir); err != nil { + t.Fatal(err) + } + + tmpDir := filepath.Join(os.TempDir(), "test") + defer os.RemoveAll(tmpDir) + + if out, err := d.Cmd("create", "-v", tmpDir+":/foo", "--name=test", "busybox"); err != nil { + t.Fatal(err, out) + } + + if err := d.Stop(); err != nil { + t.Fatal(err) + } + + // Remove this since we're expecting the daemon to re-create it too + if err := os.RemoveAll(tmpDir); err != nil { + t.Fatal(err) + } + + configDir := filepath.Join(graphDir, "volumes") + + if err := os.RemoveAll(configDir); err != nil { + t.Fatal(err) + } + + if err := d.Start("-g", graphDir); err != nil { + t.Fatal(err) + } + + if _, err := os.Stat(tmpDir); os.IsNotExist(err) { + t.Fatalf("expected volume path %s to exist but it does not", tmpDir) + } + + dir, err := ioutil.ReadDir(configDir) + if err != nil { + t.Fatal(err) + } + if len(dir) == 0 { + t.Fatalf("expected volumes config dir to contain data for new volume") + } + + // Now with just removing the volume config and not the volume data + if err := d.Stop(); err != nil { + t.Fatal(err) + } + + if err := os.RemoveAll(configDir); err != nil { + t.Fatal(err) + } + + if err := d.Start("-g", graphDir); err != nil { + t.Fatal(err) + } + + dir, err = ioutil.ReadDir(configDir) + if err != nil { + t.Fatal(err) + } + + if len(dir) == 0 { + t.Fatalf("expected volumes config dir to contain data for new volume") + } + + logDone("daemon - volumes from old(pre 1.3) daemon work") +} diff --git a/volumes/repository.go b/volumes/repository.go index 91f98fc06..e12567768 100644 --- a/volumes/repository.go +++ b/volumes/repository.go @@ -57,9 +57,10 @@ func (r *Repository) newVolume(path string, writable bool) (*Volume, error) { } path = filepath.Clean(path) - path, err = filepath.EvalSymlinks(path) - if err != nil { - return nil, err + // Ignore the error here since the path may not exist + // Really just want to make sure the path we are using is real(or non-existant) + if cleanPath, err := filepath.EvalSymlinks(path); err == nil { + path = cleanPath } v := &Volume{ From 96fe13b49b2b9b8b5da934b8689abf93b12bdddf Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Mon, 26 Jan 2015 12:58:45 -0800 Subject: [PATCH 420/513] Add file path to errors loading the key file Signed-off-by: Derek McGowan (github: dmcgowan) --- api/common.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/common.go b/api/common.go index a96a4066a..1bbb6d393 100644 --- a/api/common.go +++ b/api/common.go @@ -69,7 +69,7 @@ func LoadOrCreateTrustKey(trustKeyPath string) (libtrust.PrivateKey, error) { return nil, fmt.Errorf("Error saving key file: %s", err) } } else if err != nil { - return nil, fmt.Errorf("Error loading key file: %s", err) + return nil, fmt.Errorf("Error loading key file %s: %s", trustKeyPath, err) } return trustKey, nil } From b0935ea730304b1b84634639200ca1bb05f5055a Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Mon, 26 Jan 2015 14:00:51 -0800 Subject: [PATCH 421/513] Better error messaging and logging for v2 registry requests Signed-off-by: Derek McGowan (github: dmcgowan) --- graph/push.go | 2 +- registry/session_v2.go | 20 ++++++++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/graph/push.go b/graph/push.go index 3a9f1ace0..d3f3596e0 100644 --- a/graph/push.go +++ b/graph/push.go @@ -405,7 +405,7 @@ func (s *TagStore) CmdPush(job *engine.Job) engine.Status { } // error out, no fallback to V1 - return job.Error(err) + return job.Errorf("Error pushing to registry: %s", err) } if err != nil { diff --git a/registry/session_v2.go b/registry/session_v2.go index fa02bd3e6..8bbc9fe9b 100644 --- a/registry/session_v2.go +++ b/registry/session_v2.go @@ -132,7 +132,7 @@ func (r *Session) HeadV2ImageBlob(ep *Endpoint, imageName, sumType, sum string, // return something indicating blob push needed return false, nil } - return false, fmt.Errorf("Failed to mount %q - %s:%s : %d", imageName, sumType, sum, res.StatusCode) + return false, utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying head request for %s - %s:%s", res.StatusCode, imageName, sumType, sum), res) } func (r *Session) GetV2ImageBlob(ep *Endpoint, imageName, sumType, sum string, blobWrtr io.Writer, auth *RequestAuthorization) error { @@ -189,7 +189,7 @@ func (r *Session) GetV2ImageBlobReader(ep *Endpoint, imageName, sumType, sum str if res.StatusCode == 401 { return nil, 0, errLoginRequired } - return nil, 0, utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to pull %s blob", res.StatusCode, imageName), res) + return nil, 0, utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to pull %s blob - %s:%s", res.StatusCode, imageName, sumType, sum), res) } lenStr := res.Header.Get("Content-Length") l, err := strconv.ParseInt(lenStr, 10, 64) @@ -246,7 +246,12 @@ func (r *Session) PutV2ImageBlob(ep *Endpoint, imageName, sumType, sumStr string if res.StatusCode == 401 { return errLoginRequired } - return utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to push %s blob", res.StatusCode, imageName), res) + errBody, err := ioutil.ReadAll(res.Body) + if err != nil { + return err + } + log.Debugf("Unexpected response from server: %q %#v", errBody, res.Header) + return utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to push %s blob - %s:%s", res.StatusCode, imageName, sumType, sumStr), res) } return nil @@ -272,13 +277,16 @@ func (r *Session) PutV2ImageManifest(ep *Endpoint, imageName, tagName string, ma if err != nil { return err } - b, _ := ioutil.ReadAll(res.Body) - res.Body.Close() + defer res.Body.Close() if res.StatusCode != 200 { if res.StatusCode == 401 { return errLoginRequired } - log.Debugf("Unexpected response from server: %q %#v", b, res.Header) + errBody, err := ioutil.ReadAll(res.Body) + if err != nil { + return err + } + log.Debugf("Unexpected response from server: %q %#v", errBody, res.Header) return utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to push %s:%s manifest", res.StatusCode, imageName, tagName), res) } From b996d379a101563b28af9931acbd41001fbffc8c Mon Sep 17 00:00:00 2001 From: unclejack Date: Sat, 24 Jan 2015 08:35:03 +0200 Subject: [PATCH 422/513] docs: compress search_content.json for release Signed-off-by: Cristian Staretu Docker-DCO-1.1-Signed-off-by: unclejack (github: SvenDowideit) --- docs/release.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/release.sh b/docs/release.sh index de064706b..4491af624 100755 --- a/docs/release.sh +++ b/docs/release.sh @@ -72,6 +72,8 @@ setup_s3() { build_current_documentation() { mkdocs build + cd site/ + gzip -9k search_content.json } upload_current_documentation() { From 61d341c2cab6879eb56a860943a4ad75632c6671 Mon Sep 17 00:00:00 2001 From: Sven Dowideit Date: Mon, 26 Jan 2015 16:37:03 +1000 Subject: [PATCH 423/513] Change to load the json.gz file Signed-off-by: Sven Dowideit Docker-DCO-1.1-Signed-off-by: Sven Dowideit (github: SvenDowideit) --- docs/release.sh | 3 ++- docs/theme/mkdocs/js/base.js | 14 +++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/release.sh b/docs/release.sh index 4491af624..16bd75947 100755 --- a/docs/release.sh +++ b/docs/release.sh @@ -73,7 +73,8 @@ setup_s3() { build_current_documentation() { mkdocs build cd site/ - gzip -9k search_content.json + gzip -9k -f search_content.json + cd .. } upload_current_documentation() { diff --git a/docs/theme/mkdocs/js/base.js b/docs/theme/mkdocs/js/base.js index 1406dcd17..04f0c30c7 100644 --- a/docs/theme/mkdocs/js/base.js +++ b/docs/theme/mkdocs/js/base.js @@ -1,12 +1,6 @@ $(document).ready(function () { - // Tipue Search activation - $('#tipue_search_input').tipuesearch({ - 'mode': 'json', - 'contentLocation': '/search_content.json' - }); - prettyPrint(); // Resizing @@ -51,6 +45,12 @@ $(document).ready(function () }, }); + // Tipue Search activation + $('#tipue_search_input').tipuesearch({ + 'mode': 'json', + 'contentLocation': '/search_content.json.gz' + }); + }); function resizeMenuDropdown () @@ -92,4 +92,4 @@ function getCookie(cname) { if (c.indexOf(name) == 0) return c.substring(name.length,c.length); } return ""; -} \ No newline at end of file +} From 6e5ff509b2a028c9905421409e8439ed70554db5 Mon Sep 17 00:00:00 2001 From: Sven Dowideit Date: Mon, 26 Jan 2015 21:26:38 +1000 Subject: [PATCH 424/513] set the content-type for the search_content.json.gz Signed-off-by: Sven Dowideit Docker-DCO-1.1-Signed-off-by: Sven Dowideit (github: SvenDowideit) --- docs/release.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/release.sh b/docs/release.sh index 16bd75947..9f51e0296 100755 --- a/docs/release.sh +++ b/docs/release.sh @@ -91,7 +91,6 @@ upload_current_documentation() { echo " to $dst" echo #s3cmd --recursive --follow-symlinks --preserve --acl-public sync "$src" "$dst" - #aws s3 cp --profile $BUCKET --cache-control "max-age=3600" --acl public-read "site/search_content.json" "$dst" # a really complicated way to send only the files we want # if there are too many in any one set, aws s3 sync seems to fall over with 2 files to go @@ -103,6 +102,9 @@ upload_current_documentation() { echo "$run" echo "=======================" $run + + # Make sure the search_content.json.gz file has the right content-encoding + aws s3 cp --profile $BUCKET --cache-control "max-age=3600" --content-encoding="gzip" --acl public-read "site/search_content.json.gz" "$dst" } invalidate_cache() { From 588f350b6146ff5b65afd77e910a973901fd7fbd Mon Sep 17 00:00:00 2001 From: Sven Dowideit Date: Mon, 26 Jan 2015 21:44:17 +1000 Subject: [PATCH 425/513] as we're not using the search suggestion feature only load the search_content when we have a search ?q= param Signed-off-by: Sven Dowideit Docker-DCO-1.1-Signed-off-by: Sven Dowideit (github: SvenDowideit) --- docs/release.sh | 3 +-- docs/theme/mkdocs/js/base.js | 16 +++++++++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/docs/release.sh b/docs/release.sh index 9f51e0296..975940f5d 100755 --- a/docs/release.sh +++ b/docs/release.sh @@ -90,7 +90,6 @@ upload_current_documentation() { echo "Uploading $src" echo " to $dst" echo - #s3cmd --recursive --follow-symlinks --preserve --acl-public sync "$src" "$dst" # a really complicated way to send only the files we want # if there are too many in any one set, aws s3 sync seems to fall over with 2 files to go @@ -104,7 +103,7 @@ upload_current_documentation() { $run # Make sure the search_content.json.gz file has the right content-encoding - aws s3 cp --profile $BUCKET --cache-control "max-age=3600" --content-encoding="gzip" --acl public-read "site/search_content.json.gz" "$dst" + aws s3 cp --profile $BUCKET --cache-control $cache --content-encoding="gzip" --acl public-read "site/search_content.json.gz" "$dst" } invalidate_cache() { diff --git a/docs/theme/mkdocs/js/base.js b/docs/theme/mkdocs/js/base.js index 04f0c30c7..b4775c837 100644 --- a/docs/theme/mkdocs/js/base.js +++ b/docs/theme/mkdocs/js/base.js @@ -45,11 +45,17 @@ $(document).ready(function () }, }); - // Tipue Search activation - $('#tipue_search_input').tipuesearch({ - 'mode': 'json', - 'contentLocation': '/search_content.json.gz' - }); + function getURLP(name) + { + return decodeURIComponent((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(location.search)||[,""])[1].replace(/\+/g, '%20')) || null; + } + if (getURLP("q")) { + // Tipue Search activation + $('#tipue_search_input').tipuesearch({ + 'mode': 'json', + 'contentLocation': '/search_content.json.gz' + }); + } }); From 4602909566c3d05d3561240afcc693cfa398afb0 Mon Sep 17 00:00:00 2001 From: Tony Miller Date: Tue, 27 Jan 2015 10:12:54 +0900 Subject: [PATCH 426/513] fix /etc/host typo in remote API docs Signed-off-by: Tony Miller --- docs/sources/reference/api/docker_remote_api_v1.15.md | 2 +- docs/sources/reference/api/docker_remote_api_v1.16.md | 2 +- docs/sources/reference/api/docker_remote_api_v1.17.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/sources/reference/api/docker_remote_api_v1.15.md b/docs/sources/reference/api/docker_remote_api_v1.15.md index 229a05b1b..47fe21e92 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.15.md +++ b/docs/sources/reference/api/docker_remote_api_v1.15.md @@ -222,7 +222,7 @@ Json Parameters: - **Dns** - A list of dns servers for the container to use. - **DnsSearch** - A list of DNS search domains - **ExtraHosts** - A list of hostnames/IP mappings to be added to the - container's `/etc/host` file. Specified in the form `["hostname:IP"]`. + container's `/etc/hosts` file. Specified in the form `["hostname:IP"]`. - **VolumesFrom** - A list of volumes to inherit from another container. Specified in the form `[:]` - **CapAdd** - A list of kernel capabilties to add to the container. diff --git a/docs/sources/reference/api/docker_remote_api_v1.16.md b/docs/sources/reference/api/docker_remote_api_v1.16.md index c701a58bf..9934ab771 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.16.md +++ b/docs/sources/reference/api/docker_remote_api_v1.16.md @@ -222,7 +222,7 @@ Json Parameters: - **Dns** - A list of dns servers for the container to use. - **DnsSearch** - A list of DNS search domains - **ExtraHosts** - A list of hostnames/IP mappings to be added to the - container's `/etc/host` file. Specified in the form `["hostname:IP"]`. + container's `/etc/hosts` file. Specified in the form `["hostname:IP"]`. - **VolumesFrom** - A list of volumes to inherit from another container. Specified in the form `[:]` - **CapAdd** - A list of kernel capabilties to add to the container. diff --git a/docs/sources/reference/api/docker_remote_api_v1.17.md b/docs/sources/reference/api/docker_remote_api_v1.17.md index 400e19714..d6d0c1b4a 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.17.md +++ b/docs/sources/reference/api/docker_remote_api_v1.17.md @@ -225,7 +225,7 @@ Json Parameters: - **Dns** - A list of dns servers for the container to use. - **DnsSearch** - A list of DNS search domains - **ExtraHosts** - A list of hostnames/IP mappings to be added to the - container's `/etc/host` file. Specified in the form `["hostname:IP"]`. + container's `/etc/hosts` file. Specified in the form `["hostname:IP"]`. - **VolumesFrom** - A list of volumes to inherit from another container. Specified in the form `[:]` - **CapAdd** - A list of kernel capabilties to add to the container. From 3b4a4bf8099977fbbd6f05fdd351decd3c63772e Mon Sep 17 00:00:00 2001 From: Chen Hanxiao Date: Tue, 27 Jan 2015 11:19:02 +0800 Subject: [PATCH 427/513] docs: fix a typo in docker-build man page s/Dockefile/Dockerfile Signed-off-by: Chen Hanxiao --- docs/man/docker-build.1.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/man/docker-build.1.md b/docs/man/docker-build.1.md index 98bf3771a..661ef3516 100644 --- a/docs/man/docker-build.1.md +++ b/docs/man/docker-build.1.md @@ -59,7 +59,7 @@ as context. # EXAMPLES -## Building an image using a Dockefile located inside the current directory +## Building an image using a Dockerfile located inside the current directory Docker images can be built using the build command and a Dockerfile: From 6532a075f32c14c547e9b4fd7f3c0944ba4e0fd9 Mon Sep 17 00:00:00 2001 From: Sven Dowideit Date: Tue, 27 Jan 2015 14:55:43 +1000 Subject: [PATCH 428/513] tell users they can what IP range Hub webhooks can come from so they can filter Docker-DCO-1.1-Signed-off-by: Sven Dowideit (github: SvenDowideit) Signed-off-by: Sven Dowideit --- docs/sources/docker-hub/builds.md | 4 ++++ docs/sources/docker-hub/repos.md | 10 +++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/sources/docker-hub/builds.md b/docs/sources/docker-hub/builds.md index 5d73e4aae..8b914fa10 100644 --- a/docs/sources/docker-hub/builds.md +++ b/docs/sources/docker-hub/builds.md @@ -278,6 +278,10 @@ Webhooks are available under the Settings menu of each Repository. > **Note:** If you want to test your webhook out we recommend using > a tool like [requestb.in](http://requestb.in/). +> **Note**: The Docker Hub servers are currently in the IP range +> `162.242.195.64 - 162.242.195.127`, so you can restrict your webhooks to +> accept webhook requests from that set of IP addresses. + ### Webhook chains Webhook chains allow you to chain calls to multiple services. For example, diff --git a/docs/sources/docker-hub/repos.md b/docs/sources/docker-hub/repos.md index 0749c0814..2bb75f0b7 100644 --- a/docs/sources/docker-hub/repos.md +++ b/docs/sources/docker-hub/repos.md @@ -105,9 +105,6 @@ Settings page. A webhook is called only after a successful `push` is made. The webhook calls are HTTP POST requests with a JSON payload similar to the example shown below. -> **Note:** For testing, you can try an HTTP request tool like -> [requestb.in](http://requestb.in/). - *Example webhook JSON payload:* ``` @@ -141,6 +138,13 @@ new updates to your images and repositories. To get started adding webhooks, go to the desired repo in the Hub, and click "Webhooks" under the "Settings" box. +> **Note:** For testing, you can try an HTTP request tool like +> [requestb.in](http://requestb.in/). + +> **Note**: The Docker Hub servers are currently in the IP range +> `162.242.195.64 - 162.242.195.127`, so you can restrict your webhooks to +> accept webhook requests from that set of IP addresses. + ### Webhook chains Webhook chains allow you to chain calls to multiple services. For example, From fa5dfbb18b7269d3c339b3d2206d7884927380d7 Mon Sep 17 00:00:00 2001 From: Josh Hawn Date: Mon, 26 Jan 2015 20:56:34 -0800 Subject: [PATCH 429/513] Fix premature close of build output on pull The build job will sometimes trigger a pull job when the base image does not exist. Now that engine jobs properly close their output by default the pull job would also close the build job's stdout in a cascading close upon completion of the pull. This patch corrects this by wrapping the `pull` job's stdout with a nopCloseWriter which will not close the stdout of the `build` job. Docker-DCO-1.1-Signed-off-by: Josh Hawn (github: jlhawn) --- builder/internals.go | 3 +- engine/engine_test.go | 84 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/builder/internals.go b/builder/internals.go index 830da7272..ddbef108a 100644 --- a/builder/internals.go +++ b/builder/internals.go @@ -25,6 +25,7 @@ import ( imagepkg "github.com/docker/docker/image" "github.com/docker/docker/pkg/archive" "github.com/docker/docker/pkg/chrootarchive" + "github.com/docker/docker/pkg/ioutils" "github.com/docker/docker/pkg/parsers" "github.com/docker/docker/pkg/symlink" "github.com/docker/docker/pkg/system" @@ -433,7 +434,7 @@ func (b *Builder) pullImage(name string) (*imagepkg.Image, error) { job.SetenvBool("json", b.StreamFormatter.Json()) job.SetenvBool("parallel", true) job.SetenvJson("authConfig", pullRegistryAuth) - job.Stdout.Add(b.OutOld) + job.Stdout.Add(ioutils.NopWriteCloser(b.OutOld)) if err := job.Run(); err != nil { return nil, err } diff --git a/engine/engine_test.go b/engine/engine_test.go index 7ab2f8fc0..96c3f0df3 100644 --- a/engine/engine_test.go +++ b/engine/engine_test.go @@ -4,6 +4,8 @@ import ( "bytes" "strings" "testing" + + "github.com/docker/docker/pkg/ioutils" ) func TestRegister(t *testing.T) { @@ -150,3 +152,85 @@ func TestCatchallEmptyName(t *testing.T) { t.Fatalf("Engine.Job(\"\").Run() should return an error") } } + +// Ensure that a job within a job both using the same underlying standard +// output writer does not close the output of the outer job when the inner +// job's stdout is wrapped with a NopCloser. When not wrapped, it should +// close the outer job's output. +func TestNestedJobSharedOutput(t *testing.T) { + var ( + outerHandler Handler + innerHandler Handler + wrapOutput bool + ) + + outerHandler = func(job *Job) Status { + job.Stdout.Write([]byte("outer1")) + + innerJob := job.Eng.Job("innerJob") + + if wrapOutput { + innerJob.Stdout.Add(ioutils.NopWriteCloser(job.Stdout)) + } else { + innerJob.Stdout.Add(job.Stdout) + } + + if err := innerJob.Run(); err != nil { + t.Fatal(err) + } + + // If wrapOutput was *false* this write will do nothing. + // FIXME (jlhawn): It should cause an error to write to + // closed output. + job.Stdout.Write([]byte(" outer2")) + + return StatusOK + } + + innerHandler = func(job *Job) Status { + job.Stdout.Write([]byte(" inner")) + + return StatusOK + } + + eng := New() + eng.Register("outerJob", outerHandler) + eng.Register("innerJob", innerHandler) + + // wrapOutput starts *false* so the expected + // output of running the outer job will be: + // + // "outer1 inner" + // + outBuf := new(bytes.Buffer) + outerJob := eng.Job("outerJob") + outerJob.Stdout.Add(outBuf) + + if err := outerJob.Run(); err != nil { + t.Fatal(err) + } + + expectedOutput := "outer1 inner" + if outBuf.String() != expectedOutput { + t.Fatalf("expected job output to be %q, got %q", expectedOutput, outBuf.String()) + } + + // Set wrapOutput to true so that the expected + // output of running the outer job will be: + // + // "outer1 inner outer2" + // + wrapOutput = true + outBuf.Reset() + outerJob = eng.Job("outerJob") + outerJob.Stdout.Add(outBuf) + + if err := outerJob.Run(); err != nil { + t.Fatal(err) + } + + expectedOutput = "outer1 inner outer2" + if outBuf.String() != expectedOutput { + t.Fatalf("expected job output to be %q, got %q", expectedOutput, outBuf.String()) + } +} From c2d98377457f0890cf90b30537d3e823017d5018 Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Tue, 27 Jan 2015 10:21:35 -0800 Subject: [PATCH 430/513] Use layer checksum if calculated during manifest creation Signed-off-by: Derek McGowan (github: dmcgowan) --- graph/manifest.go | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/graph/manifest.go b/graph/manifest.go index 18784bb1e..6bebb7e5e 100644 --- a/graph/manifest.go +++ b/graph/manifest.go @@ -6,7 +6,6 @@ import ( "fmt" "io" "io/ioutil" - "path" log "github.com/Sirupsen/logrus" "github.com/docker/docker/engine" @@ -93,28 +92,33 @@ func (s *TagStore) newManifest(localName, remoteName, tag string) ([]byte, error } } - archive, err := layer.TarLayer() - if err != nil { - return nil, err + checksum := layer.Checksum + if tarsum.VersionLabelForChecksum(checksum) != tarsum.Version1.String() { + archive, err := layer.TarLayer() + if err != nil { + return nil, err + } + + tarSum, err := tarsum.NewTarSum(archive, true, tarsum.Version1) + if err != nil { + return nil, err + } + if _, err := io.Copy(ioutil.Discard, tarSum); err != nil { + return nil, err + } + + checksum = tarSum.Sum(nil) } - tarSum, err := tarsum.NewTarSum(archive, true, tarsum.Version1) - if err != nil { - return nil, err - } - if _, err := io.Copy(ioutil.Discard, tarSum); err != nil { - return nil, err - } - - tarId := tarSum.Sum(nil) - - manifest.FSLayers = append(manifest.FSLayers, ®istry.FSLayer{BlobSum: tarId}) - - layersSeen[layer.ID] = true - jsonData, err := ioutil.ReadFile(path.Join(s.graph.Root, layer.ID, "json")) + jsonData, err := layer.RawJson() if err != nil { return nil, fmt.Errorf("Cannot retrieve the path for {%s}: %s", layer.ID, err) } + + manifest.FSLayers = append(manifest.FSLayers, ®istry.FSLayer{BlobSum: checksum}) + + layersSeen[layer.ID] = true + manifest.History = append(manifest.History, ®istry.ManifestHistory{V1Compatibility: string(jsonData)}) } From 072b09c45d7604da9ee2d644e2909badca4bacac Mon Sep 17 00:00:00 2001 From: Sven Dowideit Date: Wed, 28 Jan 2015 10:47:11 +1000 Subject: [PATCH 431/513] Add the registry mirror document to the menu Signed-off-by: Sven Dowideit Docker-DCO-1.1-Signed-off-by: Sven Dowideit (github: SvenDowideit) --- docs/mkdocs.yml | 1 + docs/sources/articles.md | 15 --------------- 2 files changed, 1 insertion(+), 15 deletions(-) delete mode 100644 docs/sources/articles.md diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 73150cc44..6b8f4dc89 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -86,6 +86,7 @@ pages: - ['articles/networking.md', 'Articles', 'Advanced networking'] - ['articles/security.md', 'Articles', 'Security'] - ['articles/https.md', 'Articles', 'Running Docker with HTTPS'] +- ['articles/registry_mirror.md', 'Articles', 'Run a local registry mirror'] - ['articles/host_integration.md', 'Articles', 'Automatically starting containers'] - ['articles/baseimages.md', 'Articles', 'Creating a base image'] - ['articles/dockerfile_best-practices.md', 'Articles', 'Best practices for writing Dockerfiles'] diff --git a/docs/sources/articles.md b/docs/sources/articles.md deleted file mode 100644 index 37f2cd80f..000000000 --- a/docs/sources/articles.md +++ /dev/null @@ -1,15 +0,0 @@ -# Articles - - - [Docker Basics](basics/) - - [Docker Security](security/) - - [Running the Docker daemon with HTTPS](https/) - - [Configure Networking](networking/) - - [Using Supervisor with Docker](using_supervisord/) - - [Process Management with CFEngine](cfengine_process_management/) - - [Using Puppet](puppet/) - - [Create a Base Image](baseimages/) - - [Runtime Metrics](runmetrics/) - - [Automatically Start Containers](host_integration/) - - [Link via an Ambassador Container](ambassador_pattern_linking/) - - [Increase a Boot2Docker Volume](b2d_volume_resize/) - - [Run a Local Registry Mirror](registry_mirror/) From 79dcea718cfabebf08de31330a08f74f0b350dbb Mon Sep 17 00:00:00 2001 From: Jessica Frazelle Date: Mon, 26 Jan 2015 11:01:40 -0800 Subject: [PATCH 432/513] Add completion for stats. Docker-DCO-1.1-Signed-off-by: Jessica Frazelle (github: jfrazelle) --- contrib/completion/bash/docker | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/contrib/completion/bash/docker b/contrib/completion/bash/docker index 4891194bd..1d553941b 100755 --- a/contrib/completion/bash/docker +++ b/contrib/completion/bash/docker @@ -789,6 +789,10 @@ _docker_start() { esac } +_docker_stats() { + __docker_containers_running +} + _docker_stop() { case "$prev" in --time|-t) @@ -886,6 +890,7 @@ _docker() { save search start + stats stop tag top From b65600f6b6243c788a79e187b86e7047a1c449a2 Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Tue, 27 Jan 2015 18:10:28 -0800 Subject: [PATCH 433/513] Buffer tar file on v2 push fixes #10312 fixes #10306 Signed-off-by: Derek McGowan (github: dmcgowan) --- graph/graph.go | 22 +++++++++++++++++++++ graph/push.go | 52 ++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 60 insertions(+), 14 deletions(-) diff --git a/graph/graph.go b/graph/graph.go index 30bea0470..f7b9fc4f1 100644 --- a/graph/graph.go +++ b/graph/graph.go @@ -223,6 +223,28 @@ func (graph *Graph) Mktemp(id string) (string, error) { return dir, nil } +func (graph *Graph) newTempFile() (*os.File, error) { + tmp, err := graph.Mktemp("") + if err != nil { + return nil, err + } + return ioutil.TempFile(tmp, "") +} + +func bufferToFile(f *os.File, src io.Reader) (int64, error) { + n, err := io.Copy(f, src) + if err != nil { + return n, err + } + if err = f.Sync(); err != nil { + return n, err + } + if _, err := f.Seek(0, 0); err != nil { + return n, err + } + return n, nil +} + // setupInitLayer populates a directory with mountpoints suitable // for bind-mounting dockerinit into the container. The mountpoint is simply an // empty file at /.dockerinit diff --git a/graph/push.go b/graph/push.go index d3f3596e0..fafa41b9b 100644 --- a/graph/push.go +++ b/graph/push.go @@ -322,16 +322,6 @@ func (s *TagStore) pushV2Repository(r *registry.Session, eng *engine.Engine, out return fmt.Errorf("Failed to parse json: %s", err) } - img, err = s.graph.Get(img.ID) - if err != nil { - return err - } - - arch, err := img.TarLayer() - if err != nil { - return fmt.Errorf("Could not get tar layer: %s", err) - } - // Call mount blob exists, err := r.HeadV2ImageBlob(endpoint, repoInfo.RemoteName, sumParts[0], manifestSum, auth) if err != nil { @@ -340,12 +330,9 @@ func (s *TagStore) pushV2Repository(r *registry.Session, eng *engine.Engine, out } if !exists { - err = r.PutV2ImageBlob(endpoint, repoInfo.RemoteName, sumParts[0], manifestSum, utils.ProgressReader(arch, int(img.Size), out, sf, false, utils.TruncateID(img.ID), "Pushing"), auth) - if err != nil { - out.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Image push failed", nil)) + if err := s.PushV2Image(r, img, endpoint, repoInfo.RemoteName, sumParts[0], manifestSum, sf, out, auth); err != nil { return err } - out.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Image successfully pushed", nil)) } else { out.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Image already exists", nil)) } @@ -355,6 +342,43 @@ func (s *TagStore) pushV2Repository(r *registry.Session, eng *engine.Engine, out return r.PutV2ImageManifest(endpoint, repoInfo.RemoteName, tag, bytes.NewReader([]byte(manifestBytes)), auth) } +// PushV2Image pushes the image content to the v2 registry, first buffering the contents to disk +func (s *TagStore) PushV2Image(r *registry.Session, img *image.Image, endpoint *registry.Endpoint, imageName, sumType, sumStr string, sf *utils.StreamFormatter, out io.Writer, auth *registry.RequestAuthorization) error { + out.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Buffering to Disk", nil)) + + image, err := s.graph.Get(img.ID) + if err != nil { + return err + } + arch, err := image.TarLayer() + if err != nil { + return err + } + tf, err := s.graph.newTempFile() + if err != nil { + return err + } + defer func() { + tf.Close() + os.Remove(tf.Name()) + }() + + size, err := bufferToFile(tf, arch) + if err != nil { + return err + } + + // Send the layer + log.Debugf("rendered layer for %s of [%d] size", img.ID, size) + + if err := r.PutV2ImageBlob(endpoint, imageName, sumType, sumStr, utils.ProgressReader(tf, int(size), out, sf, false, utils.TruncateID(img.ID), "Pushing"), auth); err != nil { + out.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Image push failed", nil)) + return err + } + out.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Image successfully pushed", nil)) + return nil +} + // FIXME: Allow to interrupt current push when new push of same image is done. func (s *TagStore) CmdPush(job *engine.Job) engine.Status { if n := len(job.Args); n != 1 { From 510d8f863463f939ec9680eabf9db7663d614871 Mon Sep 17 00:00:00 2001 From: Stephen J Day Date: Tue, 27 Jan 2015 18:09:53 -0800 Subject: [PATCH 434/513] Open up v2 http status code checks for put and head checks Under certain cases, such as when putting a manifest or check for the existence of a layer, the status code checks in session_v2.go were too narrow for their purpose. In the case of putting a manifest, the handler only cares that an error is not returned. Whether it is a 304 or 202 does not matter, as long as the server reports success. Having the client only accept specific http codes inhibits future protocol evolution. Signed-off-by: Stephen J Day --- registry/session_v2.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/registry/session_v2.go b/registry/session_v2.go index 8bbc9fe9b..dbef7df1e 100644 --- a/registry/session_v2.go +++ b/registry/session_v2.go @@ -124,14 +124,15 @@ func (r *Session) HeadV2ImageBlob(ep *Endpoint, imageName, sumType, sum string, return false, err } res.Body.Close() // close early, since we're not needing a body on this call .. yet? - switch res.StatusCode { - case 200: + switch { + case res.StatusCode >= 200 && res.StatusCode < 400: // return something indicating no push needed return true, nil - case 404: + case res.StatusCode == 404: // return something indicating blob push needed return false, nil } + return false, utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying head request for %s - %s:%s", res.StatusCode, imageName, sumType, sum), res) } @@ -278,7 +279,9 @@ func (r *Session) PutV2ImageManifest(ep *Endpoint, imageName, tagName string, ma return err } defer res.Body.Close() - if res.StatusCode != 200 { + + // All 2xx and 3xx responses can be accepted for a put. + if res.StatusCode >= 400 { if res.StatusCode == 401 { return errLoginRequired } From 218d0dcc9d2b6e90559a5658a1ff22714bb11d13 Mon Sep 17 00:00:00 2001 From: Jonathan Rudenberg Date: Fri, 23 Jan 2015 14:32:36 -0800 Subject: [PATCH 435/513] Fix missing err assignment in bridge creation Signed-off-by: Jonathan Rudenberg --- daemon/networkdriver/bridge/driver.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/daemon/networkdriver/bridge/driver.go b/daemon/networkdriver/bridge/driver.go index 8e28a710f..0d3f27517 100644 --- a/daemon/networkdriver/bridge/driver.go +++ b/daemon/networkdriver/bridge/driver.go @@ -396,7 +396,7 @@ func configureBridge(bridgeIP string, bridgeIPv6 string, enableIPv6 bool) error return err } - if netlink.NetworkLinkAddIp(iface, ipAddr, ipNet); err != nil { + if err := netlink.NetworkLinkAddIp(iface, ipAddr, ipNet); err != nil { return fmt.Errorf("Unable to add private network: %s", err) } @@ -413,7 +413,7 @@ func configureBridge(bridgeIP string, bridgeIPv6 string, enableIPv6 bool) error return err } - if netlink.NetworkLinkAddIp(iface, ipAddr6, ipNet6); err != nil { + if err := netlink.NetworkLinkAddIp(iface, ipAddr6, ipNet6); err != nil { return fmt.Errorf("Unable to add private IPv6 network: %s", err) } } From aa682a845b5ef7639c7e82508dfa00c44fe576d9 Mon Sep 17 00:00:00 2001 From: Phil Estes Date: Tue, 27 Jan 2015 22:03:27 -0500 Subject: [PATCH 436/513] Fix bridge initialization for IPv6 if IPv4-only docker0 exists This fixes the daemon's failure to start when setting --ipv6=true for the first time without deleting `docker0` bridge from a prior use with only IPv4 addressing. The addition of the IPv6 bridge address is factored out into a separate initialization routine which is called even if the bridge exists but no IPv6 addresses are found. Docker-DCO-1.1-Signed-off-by: Phil Estes (github: estesp) --- daemon/networkdriver/bridge/driver.go | 53 ++++++++++++++++++++------- daemon/networkdriver/utils.go | 2 +- 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/daemon/networkdriver/bridge/driver.go b/daemon/networkdriver/bridge/driver.go index 0d3f27517..f331f1724 100644 --- a/daemon/networkdriver/bridge/driver.go +++ b/daemon/networkdriver/bridge/driver.go @@ -150,6 +150,21 @@ func InitDriver(job *engine.Job) engine.Status { } } + // a bridge might exist but not have any IPv6 addr associated with it yet + // (for example, an existing Docker installation that has only been used + // with IPv4 and docker0 already is set up) In that case, we can perform + // the bridge init for IPv6 here, else we will error out below if --ipv6=true + if len(addrsv6) == 0 && enableIPv6 { + if err := setupIPv6Bridge(bridgeIPv6); err != nil { + return job.Error(err) + } + // recheck addresses now that IPv6 is setup on the bridge + addrv4, addrsv6, err = networkdriver.GetIfaceAddr(bridgeIface) + if err != nil { + return job.Error(err) + } + } + // TODO: Check if route to fixedCIDRv6 is set } @@ -401,21 +416,9 @@ func configureBridge(bridgeIP string, bridgeIPv6 string, enableIPv6 bool) error } if enableIPv6 { - // Enable IPv6 on the bridge - procFile := "/proc/sys/net/ipv6/conf/" + iface.Name + "/disable_ipv6" - if err := ioutil.WriteFile(procFile, []byte{'0', '\n'}, 0644); err != nil { - return fmt.Errorf("unable to enable IPv6 addresses on bridge: %s\n", err) - } - - ipAddr6, ipNet6, err := net.ParseCIDR(bridgeIPv6) - if err != nil { - log.Errorf("BridgeIPv6 parsing failed") + if err := setupIPv6Bridge(bridgeIPv6); err != nil { return err } - - if err := netlink.NetworkLinkAddIp(iface, ipAddr6, ipNet6); err != nil { - return fmt.Errorf("Unable to add private IPv6 network: %s", err) - } } if err := netlink.NetworkLinkUp(iface); err != nil { @@ -424,6 +427,30 @@ func configureBridge(bridgeIP string, bridgeIPv6 string, enableIPv6 bool) error return nil } +func setupIPv6Bridge(bridgeIPv6 string) error { + + iface, err := net.InterfaceByName(bridgeIface) + if err != nil { + return err + } + // Enable IPv6 on the bridge + procFile := "/proc/sys/net/ipv6/conf/" + iface.Name + "/disable_ipv6" + if err := ioutil.WriteFile(procFile, []byte{'0', '\n'}, 0644); err != nil { + return fmt.Errorf("Unable to enable IPv6 addresses on bridge: %v", err) + } + + ipAddr6, ipNet6, err := net.ParseCIDR(bridgeIPv6) + if err != nil { + return fmt.Errorf("Unable to parse bridge IPv6 address: %q, error: %v", bridgeIPv6, err) + } + + if err := netlink.NetworkLinkAddIp(iface, ipAddr6, ipNet6); err != nil { + return fmt.Errorf("Unable to add private IPv6 network: %v", err) + } + + return nil +} + func createBridgeIface(name string) error { kv, err := kernel.GetKernelVersion() // only set the bridge's mac address if the kernel version is > 3.3 diff --git a/daemon/networkdriver/utils.go b/daemon/networkdriver/utils.go index 833744b57..9f0c88cd5 100644 --- a/daemon/networkdriver/utils.go +++ b/daemon/networkdriver/utils.go @@ -74,7 +74,7 @@ func NetworkRange(network *net.IPNet) (net.IP, net.IP) { return netIP.Mask(network.Mask), net.IP(lastIP) } -// Return the IPv4 address of a network interface +// Return the first IPv4 address and slice of IPv6 addresses for the specified network interface func GetIfaceAddr(name string) (net.Addr, []net.Addr, error) { iface, err := net.InterfaceByName(name) if err != nil { From b7c3fdfd0d37577c20df4e8c31f981c16f8dd051 Mon Sep 17 00:00:00 2001 From: Arnaud Porterie Date: Wed, 28 Jan 2015 08:52:06 -0800 Subject: [PATCH 437/513] Update fish completion for 1.5.0 Signed-off-by: Arnaud Porterie --- contrib/completion/fish/docker.fish | 94 +++++++++++++++++++++++------ 1 file changed, 76 insertions(+), 18 deletions(-) diff --git a/contrib/completion/fish/docker.fish b/contrib/completion/fish/docker.fish index 41c4a3300..fe92ecc56 100644 --- a/contrib/completion/fish/docker.fish +++ b/contrib/completion/fish/docker.fish @@ -51,23 +51,28 @@ complete -c docker -f -n '__fish_docker_no_subcommand' -s d -l daemon -d 'Enable complete -c docker -f -n '__fish_docker_no_subcommand' -l dns -d 'Force Docker to use specific DNS servers' complete -c docker -f -n '__fish_docker_no_subcommand' -l dns-search -d 'Force Docker to use specific DNS search domains' complete -c docker -f -n '__fish_docker_no_subcommand' -s e -l exec-driver -d 'Force the Docker runtime to use a specific exec driver' -complete -c docker -f -n '__fish_docker_no_subcommand' -l fixed-cidr -d 'IPv4 subnet for fixed IPs (ex: 10.20.0.0/16)' +complete -c docker -f -n '__fish_docker_no_subcommand' -l fixed-cidr -d 'IPv4 subnet for fixed IPs (e.g. 10.20.0.0/16)' +complete -c docker -f -n '__fish_docker_no_subcommand' -l fixed-cidr-v6 -d 'IPv6 subnet for fixed IPs (e.g.: 2001:a02b/48)' complete -c docker -f -n '__fish_docker_no_subcommand' -s G -l group -d 'Group to assign the unix socket specified by -H when running in daemon mode' complete -c docker -f -n '__fish_docker_no_subcommand' -s g -l graph -d 'Path to use as the root of the Docker runtime' complete -c docker -f -n '__fish_docker_no_subcommand' -s H -l host -d 'The socket(s) to bind to in daemon mode or connect to in client mode, specified using one or more tcp://host:port, unix:///path/to/socket, fd://* or fd://socketfd.' -complete -c docker -f -n '__fish_docker_no_subcommand' -l icc -d 'Enable inter-container communication' +complete -c docker -f -n '__fish_docker_no_subcommand' -s h -l help -d 'Print usage' +complete -c docker -f -n '__fish_docker_no_subcommand' -l icc -d 'Allow unrestricted inter-container and Docker daemon host communication' complete -c docker -f -n '__fish_docker_no_subcommand' -l insecure-registry -d 'Enable insecure communication with specified registries (no certificate verification for HTTPS and enable HTTP fallback) (e.g., localhost:5000 or 10.20.0.0/16)' complete -c docker -f -n '__fish_docker_no_subcommand' -l ip -d 'Default IP address to use when binding container ports' -complete -c docker -f -n '__fish_docker_no_subcommand' -l ip-forward -d 'Enable net.ipv4.ip_forward' +complete -c docker -f -n '__fish_docker_no_subcommand' -l ip-forward -d 'Enable net.ipv4.ip_forward and IPv6 forwarding if --fixed-cidr-v6 is defined. IPv6 forwarding may interfere with your existing IPv6 configuration when using Router Advertisement.' complete -c docker -f -n '__fish_docker_no_subcommand' -l ip-masq -d "Enable IP masquerading for bridge's IP range" complete -c docker -f -n '__fish_docker_no_subcommand' -l iptables -d "Enable Docker's addition of iptables rules" +complete -c docker -f -n '__fish_docker_no_subcommand' -l ipv6 -d 'Enable IPv6 networking' +complete -c docker -f -n '__fish_docker_no_subcommand' -s l -l log-level -d 'Set the logging level (debug, info, warn, error, fatal)' +complete -c docker -f -n '__fish_docker_no_subcommand' -l label -d 'Set key=value labels to the daemon (displayed in `docker info`)' complete -c docker -f -n '__fish_docker_no_subcommand' -l mtu -d 'Set the containers network MTU' complete -c docker -f -n '__fish_docker_no_subcommand' -s p -l pidfile -d 'Path to use for daemon PID file' complete -c docker -f -n '__fish_docker_no_subcommand' -l registry-mirror -d 'Specify a preferred Docker registry mirror' complete -c docker -f -n '__fish_docker_no_subcommand' -s s -l storage-driver -d 'Force the Docker runtime to use a specific storage driver' complete -c docker -f -n '__fish_docker_no_subcommand' -l selinux-enabled -d 'Enable selinux support. SELinux does not presently support the BTRFS storage driver' complete -c docker -f -n '__fish_docker_no_subcommand' -l storage-opt -d 'Set storage driver options' -complete -c docker -f -n '__fish_docker_no_subcommand' -l tls -d 'Use TLS; implied by tls-verify flags' +complete -c docker -f -n '__fish_docker_no_subcommand' -l tls -d 'Use TLS; implied by --tlsverify flag' complete -c docker -f -n '__fish_docker_no_subcommand' -l tlscacert -d 'Trust only remotes providing a certificate signed by the CA given here' complete -c docker -f -n '__fish_docker_no_subcommand' -l tlscert -d 'Path to TLS certificate file' complete -c docker -f -n '__fish_docker_no_subcommand' -l tlskey -d 'Path to TLS key file' @@ -77,14 +82,18 @@ complete -c docker -f -n '__fish_docker_no_subcommand' -s v -l version -d 'Print # subcommands # attach complete -c docker -f -n '__fish_docker_no_subcommand' -a attach -d 'Attach to a running container' +complete -c docker -A -f -n '__fish_seen_subcommand_from attach' -l help -d 'Print usage' complete -c docker -A -f -n '__fish_seen_subcommand_from attach' -l no-stdin -d 'Do not attach STDIN' -complete -c docker -A -f -n '__fish_seen_subcommand_from attach' -l sig-proxy -d 'Proxy all received signals to the process (even in non-TTY mode). SIGCHLD, SIGKILL, and SIGSTOP are not proxied.' +complete -c docker -A -f -n '__fish_seen_subcommand_from attach' -l sig-proxy -d 'Proxy all received signals to the process (non-TTY mode only). SIGCHLD, SIGKILL, and SIGSTOP are not proxied.' complete -c docker -A -f -n '__fish_seen_subcommand_from attach' -a '(__fish_print_docker_containers running)' -d "Container" # build complete -c docker -f -n '__fish_docker_no_subcommand' -a build -d 'Build an image from a Dockerfile' +complete -c docker -A -f -n '__fish_seen_subcommand_from build' -s f -l file -d "Name of the Dockerfile(Default is 'Dockerfile' at context root)" complete -c docker -A -f -n '__fish_seen_subcommand_from build' -l force-rm -d 'Always remove intermediate containers, even after unsuccessful builds' +complete -c docker -A -f -n '__fish_seen_subcommand_from build' -l help -d 'Print usage' complete -c docker -A -f -n '__fish_seen_subcommand_from build' -l no-cache -d 'Do not use cache when building the image' +complete -c docker -A -f -n '__fish_seen_subcommand_from build' -l pull -d 'Always attempt to pull a newer version of the image' complete -c docker -A -f -n '__fish_seen_subcommand_from build' -s q -l quiet -d 'Suppress the verbose output generated by the containers' complete -c docker -A -f -n '__fish_seen_subcommand_from build' -l rm -d 'Remove intermediate containers after a successful build' complete -c docker -A -f -n '__fish_seen_subcommand_from build' -s t -l tag -d 'Repository name (and optionally a tag) to be applied to the resulting image in case of success' @@ -92,12 +101,14 @@ complete -c docker -A -f -n '__fish_seen_subcommand_from build' -s t -l tag -d ' # commit complete -c docker -f -n '__fish_docker_no_subcommand' -a commit -d "Create a new image from a container's changes" complete -c docker -A -f -n '__fish_seen_subcommand_from commit' -s a -l author -d 'Author (e.g., "John Hannibal Smith ")' +complete -c docker -A -f -n '__fish_seen_subcommand_from commit' -l help -d 'Print usage' complete -c docker -A -f -n '__fish_seen_subcommand_from commit' -s m -l message -d 'Commit message' complete -c docker -A -f -n '__fish_seen_subcommand_from commit' -s p -l pause -d 'Pause container during commit' complete -c docker -A -f -n '__fish_seen_subcommand_from commit' -a '(__fish_print_docker_containers all)' -d "Container" # cp complete -c docker -f -n '__fish_docker_no_subcommand' -a cp -d "Copy files/folders from a container's filesystem to the host path" +complete -c docker -A -f -n '__fish_seen_subcommand_from cp' -l help -d 'Print usage' # create complete -c docker -f -n '__fish_docker_no_subcommand' -a create -d 'Create a new container' @@ -108,23 +119,29 @@ complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l cap-add -d ' complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l cap-drop -d 'Drop Linux capabilities' complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l cidfile -d 'Write the container ID to the file' complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l cpuset -d 'CPUs in which to allow execution (0-3, 0,1)' -complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l device -d 'Add a host device to the container (e.g. --device=/dev/sdc:/dev/xvdc)' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l device -d 'Add a host device to the container (e.g. --device=/dev/sdc:/dev/xvdc:rwm)' complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l dns -d 'Set custom DNS servers' -complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l dns-search -d 'Set custom DNS search domains' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l dns-search -d "Set custom DNS search domains (Use --dns-search=. if you don't wish to set the search domain)" complete -c docker -A -f -n '__fish_seen_subcommand_from create' -s e -l env -d 'Set environment variables' complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l entrypoint -d 'Overwrite the default ENTRYPOINT of the image' complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l env-file -d 'Read in a line delimited file of environment variables' -complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l expose -d 'Expose a port from the container without publishing it to your host' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l expose -d 'Expose a port or a range of ports (e.g. --expose=3300-3310) from the container without publishing it to your host' complete -c docker -A -f -n '__fish_seen_subcommand_from create' -s h -l hostname -d 'Container host name' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l help -d 'Print usage' complete -c docker -A -f -n '__fish_seen_subcommand_from create' -s i -l interactive -d 'Keep STDIN open even if not attached' -complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l link -d 'Add link to another container in the form of name:alias' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l ipc -d 'Default is to create a private IPC namespace (POSIX SysV IPC) for the container' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l link -d 'Add link to another container in the form of :alias' complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l lxc-conf -d '(lxc exec-driver only) Add custom lxc options --lxc-conf="lxc.cgroup.cpuset.cpus = 0,1"' complete -c docker -A -f -n '__fish_seen_subcommand_from create' -s m -l memory -d 'Memory limit (format: , where unit = b, k, m or g)' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l mac-address -d 'Container MAC address (e.g. 92:d0:c6:0a:29:33)' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l memory-swap -d "Total memory usage (memory + swap), set '-1' to disable swap (format: , where unit = b, k, m or g)" complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l name -d 'Assign a name to the container' complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l net -d 'Set the Network mode for the container' complete -c docker -A -f -n '__fish_seen_subcommand_from create' -s P -l publish-all -d 'Publish all exposed ports to random ports on the host interfaces' complete -c docker -A -f -n '__fish_seen_subcommand_from create' -s p -l publish -d "Publish a container's port to the host" +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l pid -d 'Default is to create a private PID namespace for the container' complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l privileged -d 'Give extended privileges to this container' +complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l read-only -d "Mount the container's root filesystem as read only" complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l restart -d 'Restart policy to apply when a container exits (no, on-failure[:max-retry], always)' complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l security-opt -d 'Security Options' complete -c docker -A -f -n '__fish_seen_subcommand_from create' -s t -l tty -d 'Allocate a pseudo-TTY' @@ -136,26 +153,32 @@ complete -c docker -A -f -n '__fish_seen_subcommand_from create' -a '(__fish_pri # diff complete -c docker -f -n '__fish_docker_no_subcommand' -a diff -d "Inspect changes on a container's filesystem" +complete -c docker -A -f -n '__fish_seen_subcommand_from diff' -l help -d 'Print usage' complete -c docker -A -f -n '__fish_seen_subcommand_from diff' -a '(__fish_print_docker_containers all)' -d "Container" # events complete -c docker -f -n '__fish_docker_no_subcommand' -a events -d 'Get real time events from the server' +complete -c docker -A -f -n '__fish_seen_subcommand_from events' -s f -l filter -d "Provide filter values (i.e., 'event=stop')" +complete -c docker -A -f -n '__fish_seen_subcommand_from events' -l help -d 'Print usage' complete -c docker -A -f -n '__fish_seen_subcommand_from events' -l since -d 'Show all events created since timestamp' complete -c docker -A -f -n '__fish_seen_subcommand_from events' -l until -d 'Stream events until this timestamp' # exec -complete -c docker -f -n '__fish_docker_no_subcommand' -a exec -d 'Run a command in an existing container' +complete -c docker -f -n '__fish_docker_no_subcommand' -a exec -d 'Run a command in a running container' complete -c docker -A -f -n '__fish_seen_subcommand_from exec' -s d -l detach -d 'Detached mode: run command in the background' +complete -c docker -A -f -n '__fish_seen_subcommand_from exec' -l help -d 'Print usage' complete -c docker -A -f -n '__fish_seen_subcommand_from exec' -s i -l interactive -d 'Keep STDIN open even if not attached' complete -c docker -A -f -n '__fish_seen_subcommand_from exec' -s t -l tty -d 'Allocate a pseudo-TTY' complete -c docker -A -f -n '__fish_seen_subcommand_from exec' -a '(__fish_print_docker_containers running)' -d "Container" # export complete -c docker -f -n '__fish_docker_no_subcommand' -a export -d 'Stream the contents of a container as a tar archive' +complete -c docker -A -f -n '__fish_seen_subcommand_from export' -l help -d 'Print usage' complete -c docker -A -f -n '__fish_seen_subcommand_from export' -a '(__fish_print_docker_containers all)' -d "Container" # history complete -c docker -f -n '__fish_docker_no_subcommand' -a history -d 'Show the history of an image' +complete -c docker -A -f -n '__fish_seen_subcommand_from history' -l help -d 'Print usage' complete -c docker -A -f -n '__fish_seen_subcommand_from history' -l no-trunc -d "Don't truncate output" complete -c docker -A -f -n '__fish_seen_subcommand_from history' -s q -l quiet -d 'Only show numeric IDs' complete -c docker -A -f -n '__fish_seen_subcommand_from history' -a '(__fish_print_docker_images)' -d "Image" @@ -164,34 +187,40 @@ complete -c docker -A -f -n '__fish_seen_subcommand_from history' -a '(__fish_pr complete -c docker -f -n '__fish_docker_no_subcommand' -a images -d 'List images' complete -c docker -A -f -n '__fish_seen_subcommand_from images' -s a -l all -d 'Show all images (by default filter out the intermediate image layers)' complete -c docker -A -f -n '__fish_seen_subcommand_from images' -s f -l filter -d "Provide filter values (i.e., 'dangling=true')" +complete -c docker -A -f -n '__fish_seen_subcommand_from images' -l help -d 'Print usage' complete -c docker -A -f -n '__fish_seen_subcommand_from images' -l no-trunc -d "Don't truncate output" complete -c docker -A -f -n '__fish_seen_subcommand_from images' -s q -l quiet -d 'Only show numeric IDs' complete -c docker -A -f -n '__fish_seen_subcommand_from images' -a '(__fish_print_docker_repositories)' -d "Repository" # import complete -c docker -f -n '__fish_docker_no_subcommand' -a import -d 'Create a new filesystem image from the contents of a tarball' +complete -c docker -A -f -n '__fish_seen_subcommand_from import' -l help -d 'Print usage' # info complete -c docker -f -n '__fish_docker_no_subcommand' -a info -d 'Display system-wide information' # inspect -complete -c docker -f -n '__fish_docker_no_subcommand' -a inspect -d 'Return low-level information on a container' +complete -c docker -f -n '__fish_docker_no_subcommand' -a inspect -d 'Return low-level information on a container or image' complete -c docker -A -f -n '__fish_seen_subcommand_from inspect' -s f -l format -d 'Format the output using the given go template.' +complete -c docker -A -f -n '__fish_seen_subcommand_from inspect' -l help -d 'Print usage' complete -c docker -A -f -n '__fish_seen_subcommand_from inspect' -a '(__fish_print_docker_images)' -d "Image" complete -c docker -A -f -n '__fish_seen_subcommand_from inspect' -a '(__fish_print_docker_containers all)' -d "Container" # kill complete -c docker -f -n '__fish_docker_no_subcommand' -a kill -d 'Kill a running container' +complete -c docker -A -f -n '__fish_seen_subcommand_from kill' -l help -d 'Print usage' complete -c docker -A -f -n '__fish_seen_subcommand_from kill' -s s -l signal -d 'Signal to send to the container' complete -c docker -A -f -n '__fish_seen_subcommand_from kill' -a '(__fish_print_docker_containers running)' -d "Container" # load complete -c docker -f -n '__fish_docker_no_subcommand' -a load -d 'Load an image from a tar archive' +complete -c docker -A -f -n '__fish_seen_subcommand_from load' -l help -d 'Print usage' complete -c docker -A -f -n '__fish_seen_subcommand_from load' -s i -l input -d 'Read from a tar archive file, instead of STDIN' # login complete -c docker -f -n '__fish_docker_no_subcommand' -a login -d 'Register or log in to a Docker registry server' complete -c docker -A -f -n '__fish_seen_subcommand_from login' -s e -l email -d 'Email' +complete -c docker -A -f -n '__fish_seen_subcommand_from login' -l help -d 'Print usage' complete -c docker -A -f -n '__fish_seen_subcommand_from login' -s p -l password -d 'Password' complete -c docker -A -f -n '__fish_seen_subcommand_from login' -s u -l username -d 'Username' @@ -201,12 +230,14 @@ complete -c docker -f -n '__fish_docker_no_subcommand' -a logout -d 'Log out fro # logs complete -c docker -f -n '__fish_docker_no_subcommand' -a logs -d 'Fetch the logs of a container' complete -c docker -A -f -n '__fish_seen_subcommand_from logs' -s f -l follow -d 'Follow log output' +complete -c docker -A -f -n '__fish_seen_subcommand_from logs' -l help -d 'Print usage' complete -c docker -A -f -n '__fish_seen_subcommand_from logs' -s t -l timestamps -d 'Show timestamps' complete -c docker -A -f -n '__fish_seen_subcommand_from logs' -l tail -d 'Output the specified number of lines at the end of logs (defaults to all logs)' complete -c docker -A -f -n '__fish_seen_subcommand_from logs' -a '(__fish_print_docker_containers running)' -d "Container" # port complete -c docker -f -n '__fish_docker_no_subcommand' -a port -d 'Lookup the public-facing port that is NAT-ed to PRIVATE_PORT' +complete -c docker -A -f -n '__fish_seen_subcommand_from port' -l help -d 'Print usage' complete -c docker -A -f -n '__fish_seen_subcommand_from port' -a '(__fish_print_docker_containers running)' -d "Container" # pause @@ -218,32 +249,40 @@ complete -c docker -f -n '__fish_docker_no_subcommand' -a ps -d 'List containers complete -c docker -A -f -n '__fish_seen_subcommand_from ps' -s a -l all -d 'Show all containers. Only running containers are shown by default.' complete -c docker -A -f -n '__fish_seen_subcommand_from ps' -l before -d 'Show only container created before Id or Name, include non-running ones.' complete -c docker -A -f -n '__fish_seen_subcommand_from ps' -s f -l filter -d 'Provide filter values. Valid filters:' +complete -c docker -A -f -n '__fish_seen_subcommand_from ps' -l help -d 'Print usage' complete -c docker -A -f -n '__fish_seen_subcommand_from ps' -s l -l latest -d 'Show only the latest created container, include non-running ones.' complete -c docker -A -f -n '__fish_seen_subcommand_from ps' -s n -d 'Show n last created containers, include non-running ones.' complete -c docker -A -f -n '__fish_seen_subcommand_from ps' -l no-trunc -d "Don't truncate output" complete -c docker -A -f -n '__fish_seen_subcommand_from ps' -s q -l quiet -d 'Only display numeric IDs' -complete -c docker -A -f -n '__fish_seen_subcommand_from ps' -s s -l size -d 'Display sizes' +complete -c docker -A -f -n '__fish_seen_subcommand_from ps' -s s -l size -d 'Display total file sizes' complete -c docker -A -f -n '__fish_seen_subcommand_from ps' -l since -d 'Show only containers created since Id or Name, include non-running ones.' # pull complete -c docker -f -n '__fish_docker_no_subcommand' -a pull -d 'Pull an image or a repository from a Docker registry server' complete -c docker -A -f -n '__fish_seen_subcommand_from pull' -s a -l all-tags -d 'Download all tagged images in the repository' +complete -c docker -A -f -n '__fish_seen_subcommand_from pull' -l help -d 'Print usage' complete -c docker -A -f -n '__fish_seen_subcommand_from pull' -a '(__fish_print_docker_images)' -d "Image" complete -c docker -A -f -n '__fish_seen_subcommand_from pull' -a '(__fish_print_docker_repositories)' -d "Repository" # push complete -c docker -f -n '__fish_docker_no_subcommand' -a push -d 'Push an image or a repository to a Docker registry server' +complete -c docker -A -f -n '__fish_seen_subcommand_from push' -l help -d 'Print usage' complete -c docker -A -f -n '__fish_seen_subcommand_from push' -a '(__fish_print_docker_images)' -d "Image" complete -c docker -A -f -n '__fish_seen_subcommand_from push' -a '(__fish_print_docker_repositories)' -d "Repository" +# rename +complete -c docker -f -n '__fish_docker_no_subcommand' -a rename -d 'Rename an existing container' + # restart complete -c docker -f -n '__fish_docker_no_subcommand' -a restart -d 'Restart a running container' +complete -c docker -A -f -n '__fish_seen_subcommand_from restart' -l help -d 'Print usage' complete -c docker -A -f -n '__fish_seen_subcommand_from restart' -s t -l time -d 'Number of seconds to try to stop for before killing the container. Once killed it will then be restarted. Default is 10 seconds.' complete -c docker -A -f -n '__fish_seen_subcommand_from restart' -a '(__fish_print_docker_containers running)' -d "Container" # rm complete -c docker -f -n '__fish_docker_no_subcommand' -a rm -d 'Remove one or more containers' complete -c docker -A -f -n '__fish_seen_subcommand_from rm' -s f -l force -d 'Force the removal of a running container (uses SIGKILL)' +complete -c docker -A -f -n '__fish_seen_subcommand_from rm' -l help -d 'Print usage' complete -c docker -A -f -n '__fish_seen_subcommand_from rm' -s l -l link -d 'Remove the specified link and not the underlying container' complete -c docker -A -f -n '__fish_seen_subcommand_from rm' -s v -l volumes -d 'Remove the volumes associated with the container' complete -c docker -A -f -n '__fish_seen_subcommand_from rm' -a '(__fish_print_docker_containers stopped)' -d "Container" @@ -251,6 +290,7 @@ complete -c docker -A -f -n '__fish_seen_subcommand_from rm' -a '(__fish_print_d # rmi complete -c docker -f -n '__fish_docker_no_subcommand' -a rmi -d 'Remove one or more images' complete -c docker -A -f -n '__fish_seen_subcommand_from rmi' -s f -l force -d 'Force removal of the image' +complete -c docker -A -f -n '__fish_seen_subcommand_from rmi' -l help -d 'Print usage' complete -c docker -A -f -n '__fish_seen_subcommand_from rmi' -l no-prune -d 'Do not delete untagged parents' complete -c docker -A -f -n '__fish_seen_subcommand_from rmi' -a '(__fish_print_docker_images)' -d "Image" @@ -264,27 +304,33 @@ complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l cap-drop -d 'Dr complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l cidfile -d 'Write the container ID to the file' complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l cpuset -d 'CPUs in which to allow execution (0-3, 0,1)' complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s d -l detach -d 'Detached mode: run the container in the background and print the new container ID' -complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l device -d 'Add a host device to the container (e.g. --device=/dev/sdc:/dev/xvdc)' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l device -d 'Add a host device to the container (e.g. --device=/dev/sdc:/dev/xvdc:rwm)' complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l dns -d 'Set custom DNS servers' -complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l dns-search -d 'Set custom DNS search domains' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l dns-search -d "Set custom DNS search domains (Use --dns-search=. if you don't wish to set the search domain)" complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s e -l env -d 'Set environment variables' complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l entrypoint -d 'Overwrite the default ENTRYPOINT of the image' complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l env-file -d 'Read in a line delimited file of environment variables' -complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l expose -d 'Expose a port from the container without publishing it to your host' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l expose -d 'Expose a port or a range of ports (e.g. --expose=3300-3310) from the container without publishing it to your host' complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s h -l hostname -d 'Container host name' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l help -d 'Print usage' complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s i -l interactive -d 'Keep STDIN open even if not attached' -complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l link -d 'Add link to another container in the form of name:alias' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l ipc -d 'Default is to create a private IPC namespace (POSIX SysV IPC) for the container' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l link -d 'Add link to another container in the form of :alias' complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l lxc-conf -d '(lxc exec-driver only) Add custom lxc options --lxc-conf="lxc.cgroup.cpuset.cpus = 0,1"' complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s m -l memory -d 'Memory limit (format: , where unit = b, k, m or g)' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l mac-address -d 'Container MAC address (e.g. 92:d0:c6:0a:29:33)' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l memory-swap -d "Total memory usage (memory + swap), set '-1' to disable swap (format: , where unit = b, k, m or g)" complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l name -d 'Assign a name to the container' complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l net -d 'Set the Network mode for the container' complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s P -l publish-all -d 'Publish all exposed ports to random ports on the host interfaces' complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s p -l publish -d "Publish a container's port to the host" +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l pid -d 'Default is to create a private PID namespace for the container' complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l privileged -d 'Give extended privileges to this container' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l read-only -d "Mount the container's root filesystem as read only" complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l restart -d 'Restart policy to apply when a container exits (no, on-failure[:max-retry], always)' complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l rm -d 'Automatically remove the container when it exits (incompatible with -d)' complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l security-opt -d 'Security Options' -complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l sig-proxy -d 'Proxy received signals to the process (even in non-TTY mode). SIGCHLD, SIGSTOP, and SIGKILL are not proxied.' +complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l sig-proxy -d 'Proxy received signals to the process (non-TTY mode only). SIGCHLD, SIGSTOP, and SIGKILL are not proxied.' complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s t -l tty -d 'Allocate a pseudo-TTY' complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s u -l user -d 'Username or UID' complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s v -l volume -d 'Bind mount a volume (e.g., from the host: -v /host:/container, from Docker: -v /container)' @@ -294,32 +340,43 @@ complete -c docker -A -f -n '__fish_seen_subcommand_from run' -a '(__fish_print_ # save complete -c docker -f -n '__fish_docker_no_subcommand' -a save -d 'Save an image to a tar archive' -complete -c docker -A -f -n '__fish_seen_subcommand_from save' -s o -l output -d 'Write to a file, instead of STDOUT' +complete -c docker -A -f -n '__fish_seen_subcommand_from save' -l help -d 'Print usage' +complete -c docker -A -f -n '__fish_seen_subcommand_from save' -s o -l output -d 'Write to an file, instead of STDOUT' complete -c docker -A -f -n '__fish_seen_subcommand_from save' -a '(__fish_print_docker_images)' -d "Image" # search complete -c docker -f -n '__fish_docker_no_subcommand' -a search -d 'Search for an image on the Docker Hub' complete -c docker -A -f -n '__fish_seen_subcommand_from search' -l automated -d 'Only show automated builds' +complete -c docker -A -f -n '__fish_seen_subcommand_from search' -l help -d 'Print usage' complete -c docker -A -f -n '__fish_seen_subcommand_from search' -l no-trunc -d "Don't truncate output" complete -c docker -A -f -n '__fish_seen_subcommand_from search' -s s -l stars -d 'Only displays with at least x stars' # start complete -c docker -f -n '__fish_docker_no_subcommand' -a start -d 'Start a stopped container' complete -c docker -A -f -n '__fish_seen_subcommand_from start' -s a -l attach -d "Attach container's STDOUT and STDERR and forward all signals to the process" +complete -c docker -A -f -n '__fish_seen_subcommand_from start' -l help -d 'Print usage' complete -c docker -A -f -n '__fish_seen_subcommand_from start' -s i -l interactive -d "Attach container's STDIN" complete -c docker -A -f -n '__fish_seen_subcommand_from start' -a '(__fish_print_docker_containers stopped)' -d "Container" +# stats +complete -c docker -f -n '__fish_docker_no_subcommand' -a stats -d "Display a live stream of one or more containers' resource usage statistics" +complete -c docker -A -f -n '__fish_seen_subcommand_from stats' -l help -d 'Print usage' +complete -c docker -A -f -n '__fish_seen_subcommand_from stats' -a '(__fish_print_docker_containers running)' -d "Container" + # stop complete -c docker -f -n '__fish_docker_no_subcommand' -a stop -d 'Stop a running container' +complete -c docker -A -f -n '__fish_seen_subcommand_from stop' -l help -d 'Print usage' complete -c docker -A -f -n '__fish_seen_subcommand_from stop' -s t -l time -d 'Number of seconds to wait for the container to stop before killing it. Default is 10 seconds.' complete -c docker -A -f -n '__fish_seen_subcommand_from stop' -a '(__fish_print_docker_containers running)' -d "Container" # tag complete -c docker -f -n '__fish_docker_no_subcommand' -a tag -d 'Tag an image into a repository' complete -c docker -A -f -n '__fish_seen_subcommand_from tag' -s f -l force -d 'Force' +complete -c docker -A -f -n '__fish_seen_subcommand_from tag' -l help -d 'Print usage' # top complete -c docker -f -n '__fish_docker_no_subcommand' -a top -d 'Lookup the running processes of a container' +complete -c docker -A -f -n '__fish_seen_subcommand_from top' -l help -d 'Print usage' complete -c docker -A -f -n '__fish_seen_subcommand_from top' -a '(__fish_print_docker_containers running)' -d "Container" # unpause @@ -331,6 +388,7 @@ complete -c docker -f -n '__fish_docker_no_subcommand' -a version -d 'Show the D # wait complete -c docker -f -n '__fish_docker_no_subcommand' -a wait -d 'Block until a container stops, then print its exit code' +complete -c docker -A -f -n '__fish_seen_subcommand_from wait' -l help -d 'Print usage' complete -c docker -A -f -n '__fish_seen_subcommand_from wait' -a '(__fish_print_docker_containers running)' -d "Container" From 3c090db4e9fb8ce1c50e635f8e4fbef0e1cb4c17 Mon Sep 17 00:00:00 2001 From: Tianon Gravi Date: Wed, 28 Jan 2015 14:08:41 -0700 Subject: [PATCH 438/513] Update .deb version numbers to be more sane Example output: ```console root@906b21a861fb:/go/src/github.com/docker/docker# ./hack/make.sh binary ubuntu bundles/1.4.1-dev already exists. Removing. ---> Making bundle: binary (in bundles/1.4.1-dev/binary) Created binary: /go/src/github.com/docker/docker/bundles/1.4.1-dev/binary/docker-1.4.1-dev ---> Making bundle: ubuntu (in bundles/1.4.1-dev/ubuntu) Created package {:path=>"lxc-docker-1.4.1-dev_1.4.1~dev~git20150128.182847.0.17e840a_amd64.deb"} Created package {:path=>"lxc-docker_1.4.1~dev~git20150128.182847.0.17e840a_amd64.deb"} ``` As noted in a comment in the code here, this sums up the reasoning for this change: (which is how APT and reprepro compare versions) ```console $ dpkg --compare-versions 1.5.0 gt 1.5.0~rc1 && echo true || echo false true $ dpkg --compare-versions 1.5.0~rc1 gt 1.5.0~git20150128.112847.17e840a && echo true || echo false true $ dpkg --compare-versions 1.5.0~git20150128.112847.17e840a gt 1.5.0~dev~git20150128.112847.17e840a && echo true || echo false true ``` ie, `1.5.0` > `1.5.0~rc1` > `1.5.0~git20150128.112847.17e840a` > `1.5.0~dev~git20150128.112847.17e840a` Signed-off-by: Andrew "Tianon" Page --- project/make/ubuntu | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/project/make/ubuntu b/project/make/ubuntu index 98ec42307..e34369eb1 100644 --- a/project/make/ubuntu +++ b/project/make/ubuntu @@ -2,11 +2,26 @@ DEST=$1 -PKGVERSION="$VERSION" -if [ -n "$(git status --porcelain)" ]; then - PKGVERSION="$PKGVERSION-$(date +%Y%m%d%H%M%S)-$GITCOMMIT" +PKGVERSION="${VERSION//-/'~'}" +# if we have a "-dev" suffix or have change in Git, let's make this package version more complex so it works better +if [[ "$VERSION" == *-dev ]] || [ -n "$(git status --porcelain)" ]; then + GIT_UNIX="$(git log -1 --pretty='%at')" + GIT_DATE="$(date --date "@$GIT_UNIX" +'%Y%m%d.%H%M%S')" + GIT_COMMIT="$(git log -1 --pretty='%h')" + GIT_VERSION="git${GIT_DATE}.0.${GIT_COMMIT}" + # GIT_VERSION is now something like 'git20150128.112847.0.17e840a' + PKGVERSION="$PKGVERSION~$GIT_VERSION" fi +# $ dpkg --compare-versions 1.5.0 gt 1.5.0~rc1 && echo true || echo false +# true +# $ dpkg --compare-versions 1.5.0~rc1 gt 1.5.0~git20150128.112847.17e840a && echo true || echo false +# true +# $ dpkg --compare-versions 1.5.0~git20150128.112847.17e840a gt 1.5.0~dev~git20150128.112847.17e840a && echo true || echo false +# true + +# ie, 1.5.0 > 1.5.0~rc1 > 1.5.0~git20150128.112847.17e840a > 1.5.0~dev~git20150128.112847.17e840a + PACKAGE_ARCHITECTURE="$(dpkg-architecture -qDEB_HOST_ARCH)" PACKAGE_URL="http://www.docker.com/" PACKAGE_MAINTAINER="support@docker.com" @@ -124,7 +139,7 @@ EOF # create lxc-docker-VERSION package fpm -s dir -C $DIR \ - --name lxc-docker-$VERSION --version $PKGVERSION \ + --name lxc-docker-$VERSION --version "$PKGVERSION" \ --after-install $DEST/postinst \ --before-remove $DEST/prerm \ --after-remove $DEST/postrm \ @@ -157,7 +172,7 @@ EOF # create empty lxc-docker wrapper package fpm -s empty \ - --name lxc-docker --version $PKGVERSION \ + --name lxc-docker --version "$PKGVERSION" \ --architecture "$PACKAGE_ARCHITECTURE" \ --depends lxc-docker-$VERSION \ --description "$PACKAGE_DESCRIPTION" \ From 6f26bd0e163f8906dc93cf6d2617be021b5208e1 Mon Sep 17 00:00:00 2001 From: Mehul Kar Date: Thu, 29 Jan 2015 09:09:44 -0800 Subject: [PATCH 439/513] Improve explanation of port mapping from containers Signed-off-by: Mehul Kar --- docs/sources/userguide/usingdocker.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/sources/userguide/usingdocker.md b/docs/sources/userguide/usingdocker.md index 865f446bd..12a6b6fb2 100644 --- a/docs/sources/userguide/usingdocker.md +++ b/docs/sources/userguide/usingdocker.md @@ -167,8 +167,9 @@ host. You might be asking about now: why wouldn't we just want to always use 1:1 port mappings in Docker containers rather than mapping to high ports? Well 1:1 mappings have the constraint of only being able to map one of each port on your local host. Let's say you want to test two -Python applications: both bound to port 5000 inside your container. -Without Docker's port mapping you could only access one at a time. +Python applications: both bound to port 5000 inside their own containers. +Without Docker's port mapping you could only access one at a time on the +Docker host. So let's now browse to port 49155 in a web browser to see the application. From cdff91a01ca06819527906114b0435a508422f35 Mon Sep 17 00:00:00 2001 From: Sven Dowideit Date: Fri, 23 Jan 2015 12:17:55 +1000 Subject: [PATCH 440/513] comment out the docker and curl lines we'll run later Docker-DCO-1.1-Signed-off-by: Sven Dowideit (github: SvenDowideit) --- docs/sources/articles/https/Makefile | 7 ++++--- docs/sources/articles/https/parsedocs.sh | 8 +++++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/sources/articles/https/Makefile b/docs/sources/articles/https/Makefile index 48fe49f2b..b751c1e43 100644 --- a/docs/sources/articles/https/Makefile +++ b/docs/sources/articles/https/Makefile @@ -13,11 +13,12 @@ cert: build certs: cert run: - docker -d -D --tlsverify --tlscacert=ca.pem --tlscert=server-cert.pem --tlskey=server-key.pem -H=0.0.0.0:6666 --pidfile=$(pwd)/docker.pid --graph=$(pwd)/graph + sudo docker -d -D --tlsverify --tlscacert=ca.pem --tlscert=server-cert.pem --tlskey=server-key.pem -H=0.0.0.0:6666 --pidfile=$(pwd)/docker.pid --graph=$(pwd)/graph client: - docker --tls --tlscacert=ca.pem --tlscert=cert.pem --tlskey=key.pem -H=$(HOST):6666 version - docker --tlsverify --tlscacert=ca.pem --tlscert=cert.pem --tlskey=key.pem -H=$(HOST):6666 info + sudo docker --tls --tlscacert=ca.pem --tlscert=cert.pem --tlskey=key.pem -H=$(HOST):6666 version + sudo docker --tlsverify --tlscacert=ca.pem --tlscert=cert.pem --tlskey=key.pem -H=$(HOST):6666 info + sudo curl https://$(HOST):6666/images/json --cert cert.pem --key key.pem --cacert ca.pem clean: rm ca-key.pem ca.pem ca.srl cert.pem client.csr extfile.cnf key.pem server-cert.pem server-key.pem server.csr diff --git a/docs/sources/articles/https/parsedocs.sh b/docs/sources/articles/https/parsedocs.sh index 56be4103a..f9df33c33 100755 --- a/docs/sources/articles/https/parsedocs.sh +++ b/docs/sources/articles/https/parsedocs.sh @@ -1,4 +1,10 @@ #!/bin/sh echo "#!/bin/sh" -cat ../https.md | awk '{if (sub(/\\$/,"")) printf "%s", $0; else print $0}' | grep ' $ ' | sed 's/ $ //g' | sed 's/2375/7777/g' | sed 's/2376/7778/g' +cat ../https.md | awk '{if (sub(/\\$/,"")) printf "%s", $0; else print $0}' \ + | grep ' $ ' \ + | sed 's/ $ //g' \ + | sed 's/2375/7777/g' \ + | sed 's/2376/7778/g' \ + | sed 's/^docker/# docker/g' \ + | sed 's/^curl/# curl/g' From 817d04d992fa2b4e86f54d2af053d544f0dd056d Mon Sep 17 00:00:00 2001 From: Sven Dowideit Date: Thu, 29 Jan 2015 20:35:37 +1000 Subject: [PATCH 441/513] DHE documentation placeholder and Navbar changes Signed-off-by: Sven Dowideit --- docs/mkdocs.yml | 5 +++++ .../docker-hub-enterprise/install-config.md | 8 ++++++++ docs/sources/docker-hub-enterprise/usage.md | 9 +++++++++ docs/theme/mkdocs/css/docs.css | 1 + docs/theme/mkdocs/css/main.css | 4 ---- docs/theme/mkdocs/header.html | 18 ++++++++++-------- docs/theme/mkdocs/nav.html | 4 ---- 7 files changed, 33 insertions(+), 16 deletions(-) create mode 100644 docs/sources/docker-hub-enterprise/install-config.md create mode 100644 docs/sources/docker-hub-enterprise/usage.md diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 6b8f4dc89..de532a826 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -69,6 +69,11 @@ pages: - ['docker-hub/builds.md', 'Docker Hub', 'Automated Builds'] - ['docker-hub/official_repos.md', 'Docker Hub', 'Official Repo Guidelines'] +# Docker Hub Enterprise +- ['docker-hub-enterprise/index.md', '**HIDDEN**' ] +- ['docker-hub-enterprise/install-config.md', 'Docker Hub Enterprise', 'Installation and Configuration' ] +- ['docker-hub-enterprise/usage.md', 'Docker Hub Enterprise', 'User Guide' ] + # Examples: - ['examples/index.md', '**HIDDEN**'] - ['examples/nodejs_web_app.md', 'Examples', 'Dockerizing a Node.js web application'] diff --git a/docs/sources/docker-hub-enterprise/install-config.md b/docs/sources/docker-hub-enterprise/install-config.md new file mode 100644 index 000000000..0b7bcfd6f --- /dev/null +++ b/docs/sources/docker-hub-enterprise/install-config.md @@ -0,0 +1,8 @@ +page_title: Using Docker Hub Enterprise Installation +page_description: Docker Hub Enterprise Installation +page_keywords: docker hub enterprise + +# Docker Hub Enterprise Installation + +Documenation coming soon. + diff --git a/docs/sources/docker-hub-enterprise/usage.md b/docs/sources/docker-hub-enterprise/usage.md new file mode 100644 index 000000000..252223ef7 --- /dev/null +++ b/docs/sources/docker-hub-enterprise/usage.md @@ -0,0 +1,9 @@ +page_title: Using Docker Hub Enterprise +page_description: Docker Hub Enterprise +page_keywords: docker hub enterprise + +# Docker Hub Enterprise + +Documenation coming soon. + + diff --git a/docs/theme/mkdocs/css/docs.css b/docs/theme/mkdocs/css/docs.css index 068a0003e..6a5eeb514 100644 --- a/docs/theme/mkdocs/css/docs.css +++ b/docs/theme/mkdocs/css/docs.css @@ -60,6 +60,7 @@ pre { /* Main Navigation */ #nav_menu > #docsnav { max-width: 940px; + width: 940px; margin: 0 auto; } #nav_menu > #docsnav > #nav_search { diff --git a/docs/theme/mkdocs/css/main.css b/docs/theme/mkdocs/css/main.css index ed7c189a0..0c2d7830f 100644 --- a/docs/theme/mkdocs/css/main.css +++ b/docs/theme/mkdocs/css/main.css @@ -801,10 +801,6 @@ div + .form-inline { transition: box-shadow linear 0.2s, background linear 0.3s, width linear 0.3s; width: 140px; } -#topmostnav .navbar-index-search .search-query:focus, -#topmostnav .navbar-index-search .search-query.focused { - width: 200px; -} #topmostnav.public { border-bottom: none; height: 80px; diff --git a/docs/theme/mkdocs/header.html b/docs/theme/mkdocs/header.html index a3b1d9bd7..6622f9330 100644 --- a/docs/theme/mkdocs/header.html +++ b/docs/theme/mkdocs/header.html @@ -1,13 +1,14 @@ -