From 4c27690cdd6febb1313aa71d85c8c4a97c48d2e2 Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Fri, 4 Oct 2013 19:25:15 -0700 Subject: [PATCH 01/17] Add links for container relationships and introspection --- Dockerfile | 12 +- api.go | 104 +++- api_params.go | 32 +- api_test.go | 36 +- buildfile.go | 9 +- commands.go | 111 +++- config.go | 17 + container.go | 232 ++++++-- container_test.go | 70 +-- docker/docker.go | 50 +- docs/sources/commandline/cli.rst | 97 +++- docs/sources/examples/index.rst | 3 +- docs/sources/examples/linking_into_redis.rst | 146 +++++ gograph/MAINTAINERS | 1 + gograph/gograph.go | 455 ++++++++++++++++ gograph/gograph_test.go | 452 ++++++++++++++++ gograph/sort.go | 27 + gograph/sort_test.go | 29 + gograph/utils.go | 32 ++ hack/make.sh | 3 +- hack/make/binary | 6 +- hack/make/test | 2 +- iptables/MAINTAINERS | 1 + iptables/iptables.go | 105 ++++ iptables/iptables_test.go | 18 + links.go | 141 +++++ links_test.go | 104 ++++ network.go | 280 ++++------ network_test.go | 108 ---- proxy/MAINTAINERS | 1 + .../network_proxy_test.go | 2 +- proxy/proxy.go | 29 + proxy/tcp_proxy.go | 93 ++++ network_proxy.go => proxy/udp_proxy.go | 116 +--- runtime.go | 214 ++++++-- runtime_test.go | 321 ++++++++++- server.go | 129 +++-- server_test.go | 20 +- sorter.go | 69 +++ sorter_test.go | 36 ++ utils.go | 153 +++++- utils/utils.go | 19 + utils/utils_test.go | 20 + utils_test.go | 186 +++++-- .../p/gosqlite/sqlite/sqlite.go | 404 ++++++++++++++ .../p/gosqlite/sqlite3/driver.go | 498 ++++++++++++++++++ 46 files changed, 4252 insertions(+), 741 deletions(-) create mode 100644 config.go create mode 100644 docs/sources/examples/linking_into_redis.rst create mode 100644 gograph/MAINTAINERS create mode 100644 gograph/gograph.go create mode 100644 gograph/gograph_test.go create mode 100644 gograph/sort.go create mode 100644 gograph/sort_test.go create mode 100644 gograph/utils.go create mode 100644 iptables/MAINTAINERS create mode 100644 iptables/iptables.go create mode 100644 iptables/iptables_test.go create mode 100644 links.go create mode 100644 links_test.go create mode 100644 proxy/MAINTAINERS rename network_proxy_test.go => proxy/network_proxy_test.go (99%) create mode 100644 proxy/proxy.go create mode 100644 proxy/tcp_proxy.go rename network_proxy.go => proxy/udp_proxy.go (55%) create mode 100644 vendor/src/code.google.com/p/gosqlite/sqlite/sqlite.go create mode 100644 vendor/src/code.google.com/p/gosqlite/sqlite3/driver.go diff --git a/Dockerfile b/Dockerfile index 67963c836..3c858c898 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,15 +33,13 @@ run apt-get update run apt-get install -y -q curl run apt-get install -y -q git run apt-get install -y -q mercurial -run apt-get install -y -q build-essential +run apt-get install -y -q build-essential libsqlite3-dev -# Install Go from source (for eventual cross-compiling) -env CGO_ENABLED 0 -run curl -s https://go.googlecode.com/files/go1.1.2.src.tar.gz | tar -v -C / -xz && mv /go /goroot -run cd /goroot/src && ./make.bash -env GOROOT /goroot -env PATH $PATH:/goroot/bin +# Install Go +run curl -s https://go.googlecode.com/files/go1.2rc1.src.tar.gz | tar -v -C /usr/local -xz +env PATH /usr/local/go/bin:/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin env GOPATH /go:/go/src/github.com/dotcloud/docker/vendor +run cd /usr/local/go/src && ./make.bash && go install -ldflags '-w -linkmode external -extldflags "-static -Wl,--unresolved-symbols=ignore-in-shared-libs"' -tags netgo -a std # Ubuntu stuff run apt-get install -y -q ruby1.9.3 rubygems libffi-dev diff --git a/api.go b/api.go index ad308335c..3ff74846f 100644 --- a/api.go +++ b/api.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "github.com/dotcloud/docker/auth" + "github.com/dotcloud/docker/gograph" "github.com/dotcloud/docker/utils" "github.com/gorilla/mux" "io" @@ -14,6 +15,7 @@ import ( "mime" "net" "net/http" + "net/url" "os" "os/exec" "regexp" @@ -136,6 +138,7 @@ func postContainersKill(srv *Server, version float64, w http.ResponseWriter, r * return fmt.Errorf("Missing parameter") } name := vars["name"] + name = decodeName(name) if err := srv.ContainerKill(name); err != nil { return err } @@ -148,6 +151,7 @@ func getContainersExport(srv *Server, version float64, w http.ResponseWriter, r return fmt.Errorf("Missing parameter") } name := vars["name"] + name = decodeName(name) if err := srv.ContainerExport(name, w); err != nil { utils.Errorf("%s", err) @@ -515,16 +519,19 @@ func postContainersCreate(srv *Server, version float64, w http.ResponseWriter, r return err } - if !config.NetworkDisabled && len(config.Dns) == 0 && len(srv.runtime.Dns) == 0 && utils.CheckLocalDns(resolvConf) { + if !config.NetworkDisabled && len(config.Dns) == 0 && len(srv.runtime.config.Dns) == 0 && utils.CheckLocalDns(resolvConf) { out.Warnings = append(out.Warnings, fmt.Sprintf("Docker detected local DNS server on resolv.conf. Using default external servers: %v", defaultDns)) config.Dns = defaultDns } - id, err := srv.ContainerCreate(config) + id, warnings, err := srv.ContainerCreate(config) if err != nil { return err } out.ID = id + for _, warning := range warnings { + out.Warnings = append(out.Warnings, warning) + } if config.Memory > 0 && !srv.runtime.capabilities.MemoryLimit { log.Println("WARNING: Your kernel does not support memory limit capabilities. Limitation discarded.") @@ -555,6 +562,7 @@ func postContainersRestart(srv *Server, version float64, w http.ResponseWriter, return fmt.Errorf("Missing parameter") } name := vars["name"] + name = decodeName(name) if err := srv.ContainerRestart(name, t); err != nil { return err } @@ -570,12 +578,18 @@ func deleteContainers(srv *Server, version float64, w http.ResponseWriter, r *ht return fmt.Errorf("Missing parameter") } name := vars["name"] + name = decodeName(name) + removeVolume, err := getBoolParam(r.Form.Get("v")) if err != nil { return err } + removeLink, err := getBoolParam(r.Form.Get("link")) + if err != nil { + return err + } - if err := srv.ContainerDestroy(name, removeVolume); err != nil { + if err := srv.ContainerDestroy(name, removeVolume, removeLink); err != nil { return err } w.WriteHeader(http.StatusNoContent) @@ -621,7 +635,12 @@ func postContainersStart(srv *Server, version float64, w http.ResponseWriter, r if vars == nil { return fmt.Errorf("Missing parameter") } + var err error name := vars["name"] + name = decodeName(name) + if err != nil { + return err + } if err := srv.ContainerStart(name, hostConfig); err != nil { return err } @@ -642,6 +661,7 @@ func postContainersStop(srv *Server, version float64, w http.ResponseWriter, r * return fmt.Errorf("Missing parameter") } name := vars["name"] + name = decodeName(name) if err := srv.ContainerStop(name, t); err != nil { return err @@ -655,6 +675,8 @@ func postContainersWait(srv *Server, version float64, w http.ResponseWriter, r * return fmt.Errorf("Missing parameter") } name := vars["name"] + name = decodeName(name) + status, err := srv.ContainerWait(name) if err != nil { return err @@ -714,6 +736,7 @@ func postContainersAttach(srv *Server, version float64, w http.ResponseWriter, r return fmt.Errorf("Missing parameter") } name := vars["name"] + name = decodeName(name) c, err := srv.ContainerInspect(name) if err != nil { @@ -786,6 +809,7 @@ func wsContainersAttach(srv *Server, version float64, w http.ResponseWriter, r * return fmt.Errorf("Missing parameter") } name := vars["name"] + name = decodeName(name) if _, err := srv.ContainerInspect(name); err != nil { return err @@ -808,6 +832,7 @@ func getContainersByName(srv *Server, version float64, w http.ResponseWriter, r return fmt.Errorf("Missing parameter") } name := vars["name"] + name = decodeName(name) container, err := srv.ContainerInspect(name) if err != nil { @@ -975,7 +1000,7 @@ func makeHttpHandler(srv *Server, logging bool, localMethod string, localRoute s if err != nil { version = APIVERSION } - if srv.enableCors { + if srv.runtime.config.EnableCors { writeCorsHeaders(w, r) } @@ -991,6 +1016,75 @@ func makeHttpHandler(srv *Server, logging bool, localMethod string, localRoute s } } +func getContainersLinks(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := parseForm(r); err != nil { + return err + } + + runtime := srv.runtime + all, err := getBoolParam(r.Form.Get("all")) + if err != nil { + return err + } + + out := []APILink{} + err = runtime.containerGraph.Walk("/", func(p string, e *gograph.Entity) error { + if container := runtime.Get(e.ID()); container != nil { + if !all && strings.Contains(p, container.ID) { + return nil + } + out = append(out, APILink{ + Path: p, + ContainerID: container.ID, + Image: runtime.repositories.ImageName(container.Image), + }) + } + return nil + }, -1) + + if err != nil { + return err + } + return writeJSON(w, http.StatusOK, out) +} + +func postContainerLink(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if vars == nil { + return fmt.Errorf("Missing parameter") + } + values := make(map[string]string) + if matchesContentType(r.Header.Get("Content-Type"), "application/json") && r.Body != nil { + defer r.Body.Close() + + dec := json.NewDecoder(r.Body) + if err := dec.Decode(&values); err != nil { + return err + } + } else { + return fmt.Errorf("Invalid json body") + } + currentName := values["currentName"] + newName := values["newName"] + + if currentName == "" { + return fmt.Errorf("currentName cannot be empty") + } + if newName == "" { + return fmt.Errorf("newName cannot be empty") + } + + if err := srv.runtime.RenameLink(currentName, newName); err != nil { + return err + } + + return nil +} + +func decodeName(name string) string { + s, _ := url.QueryUnescape(name) + return s +} + func createRouter(srv *Server, logging bool) (*mux.Router, error) { r := mux.NewRouter() @@ -1011,6 +1105,7 @@ func createRouter(srv *Server, logging bool) (*mux.Router, error) { "/containers/{name:.*}/json": getContainersByName, "/containers/{name:.*}/top": getContainersTop, "/containers/{name:.*}/attach/ws": wsContainersAttach, + "/containers/links": getContainersLinks, }, "POST": { "/auth": postAuth, @@ -1029,6 +1124,7 @@ func createRouter(srv *Server, logging bool) (*mux.Router, error) { "/containers/{name:.*}/resize": postContainersResize, "/containers/{name:.*}/attach": postContainersAttach, "/containers/{name:.*}/copy": postContainersCopy, + "/containers/link": postContainerLink, }, "DELETE": { "/containers/{name:.*}": deleteContainers, diff --git a/api_params.go b/api_params.go index 6403bc6a2..5242d0222 100644 --- a/api_params.go +++ b/api_params.go @@ -1,7 +1,5 @@ package docker -import "encoding/json" - type APIHistory struct { ID string `json:"Id"` Tags []string `json:",omitempty"` @@ -52,17 +50,18 @@ type APIContainers struct { Ports []APIPort SizeRw int64 SizeRootFs int64 + Names []string } func (self *APIContainers) ToLegacy() APIContainersOld { return APIContainersOld{ - ID: self.ID, - Image: self.Image, - Command: self.Command, - Created: self.Created, - Status: self.Status, - Ports: displayablePorts(self.Ports), - SizeRw: self.SizeRw, + ID: self.ID, + Image: self.Image, + Command: self.Command, + Created: self.Created, + Status: self.Status, + Ports: displayablePorts(self.Ports), + SizeRw: self.SizeRw, SizeRootFs: self.SizeRootFs, } } @@ -96,14 +95,7 @@ type APIPort struct { PrivatePort int64 PublicPort int64 Type string -} - -func (port *APIPort) MarshalJSON() ([]byte, error) { - return json.Marshal(map[string]interface{}{ - "PrivatePort": port.PrivatePort, - "PublicPort": port.PublicPort, - "Type": port.Type, - }) + IP string } type APIVersion struct { @@ -129,3 +121,9 @@ type APICopy struct { Resource string HostPath string } + +type APILink struct { + Path string + ContainerID string + Image string +} diff --git a/api_test.go b/api_test.go index 479b9e1e5..216c31cd6 100644 --- a/api_test.go +++ b/api_test.go @@ -347,7 +347,7 @@ func TestGetContainersJSON(t *testing.T) { srv := &Server{runtime: runtime} - container, err := runtime.Create(&Config{ + container, _, err := runtime.Create(&Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"echo", "test"}, }) @@ -384,7 +384,7 @@ func TestGetContainersExport(t *testing.T) { srv := &Server{runtime: runtime} // Create a container and remove a file - container, err := runtime.Create( + container, _, err := runtime.Create( &Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"touch", "/test"}, @@ -434,7 +434,7 @@ func TestGetContainersChanges(t *testing.T) { srv := &Server{runtime: runtime} // Create a container and remove a file - container, err := runtime.Create( + container, _, err := runtime.Create( &Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"/bin/rm", "/etc/passwd"}, @@ -477,7 +477,7 @@ func TestGetContainersTop(t *testing.T) { srv := &Server{runtime: runtime} - container, err := runtime.Create( + container, _, err := runtime.Create( &Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"/bin/sh", "-c", "cat"}, @@ -559,7 +559,7 @@ func TestGetContainersByName(t *testing.T) { srv := &Server{runtime: runtime} // Create a container and remove a file - container, err := runtime.Create( + container, _, err := runtime.Create( &Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"echo", "test"}, @@ -590,7 +590,7 @@ func TestPostCommit(t *testing.T) { srv := &Server{runtime: runtime} // Create a container and remove a file - container, err := runtime.Create( + container, _, err := runtime.Create( &Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"touch", "/test"}, @@ -684,7 +684,7 @@ func TestPostContainersKill(t *testing.T) { srv := &Server{runtime: runtime} - container, err := runtime.Create( + container, _, err := runtime.Create( &Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"/bin/cat"}, @@ -726,7 +726,7 @@ func TestPostContainersRestart(t *testing.T) { srv := &Server{runtime: runtime} - container, err := runtime.Create( + container, _, err := runtime.Create( &Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"/bin/cat"}, @@ -780,7 +780,7 @@ func TestPostContainersStart(t *testing.T) { srv := &Server{runtime: runtime} - container, err := runtime.Create( + container, _, err := runtime.Create( &Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"/bin/cat"}, @@ -832,7 +832,7 @@ func TestPostContainersStop(t *testing.T) { srv := &Server{runtime: runtime} - container, err := runtime.Create( + container, _, err := runtime.Create( &Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"/bin/cat"}, @@ -879,7 +879,7 @@ func TestPostContainersWait(t *testing.T) { srv := &Server{runtime: runtime} - container, err := runtime.Create( + container, _, err := runtime.Create( &Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"/bin/sleep", "1"}, @@ -921,7 +921,7 @@ func TestPostContainersAttach(t *testing.T) { srv := &Server{runtime: runtime} - container, err := runtime.Create( + container, _, err := runtime.Create( &Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"/bin/cat"}, @@ -1010,7 +1010,7 @@ func TestPostContainersAttachStderr(t *testing.T) { srv := &Server{runtime: runtime} - container, err := runtime.Create( + container, _, err := runtime.Create( &Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"/bin/sh", "-c", "/bin/cat >&2"}, @@ -1102,7 +1102,7 @@ func TestDeleteContainers(t *testing.T) { srv := &Server{runtime: runtime} - container, err := runtime.Create(&Config{ + container, _, err := runtime.Create(&Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"touch", "/test"}, }) @@ -1140,7 +1140,8 @@ func TestOptionsRoute(t *testing.T) { runtime := mkRuntime(t) defer nuke(runtime) - srv := &Server{runtime: runtime, enableCors: true} + runtime.config.EnableCors = true + srv := &Server{runtime: runtime} r := httptest.NewRecorder() router, err := createRouter(srv, false) @@ -1163,7 +1164,8 @@ func TestGetEnabledCors(t *testing.T) { runtime := mkRuntime(t) defer nuke(runtime) - srv := &Server{runtime: runtime, enableCors: true} + runtime.config.EnableCors = true + srv := &Server{runtime: runtime} r := httptest.NewRecorder() @@ -1290,7 +1292,7 @@ func TestPostContainersCopy(t *testing.T) { srv := &Server{runtime: runtime} // Create a container and remove a file - container, err := runtime.Create( + container, _, err := runtime.Create( &Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"touch", "/test.txt"}, diff --git a/buildfile.go b/buildfile.go index 8231287ea..91f707b9a 100644 --- a/buildfile.go +++ b/buildfile.go @@ -187,6 +187,9 @@ func (b *buildFile) CmdCmd(args string) error { } func (b *buildFile) CmdExpose(args string) error { + if strings.Contains(args, ":") { + return fmt.Errorf("EXPOSE cannot be used to bind to a host ip or port") + } ports := strings.Split(args, " ") b.config.PortSpecs = append(ports, b.config.PortSpecs...) return b.commit("", b.config.Cmd, fmt.Sprintf("EXPOSE %v", ports)) @@ -332,7 +335,7 @@ func (b *buildFile) CmdAdd(args string) error { b.config.Image = b.image // Create the container and start it - container, err := b.runtime.Create(b.config) + container, _, err := b.runtime.Create(b.config) if err != nil { return err } @@ -367,7 +370,7 @@ func (b *buildFile) run() (string, error) { b.config.Image = b.image // Create the container and start it - c, err := b.runtime.Create(b.config) + c, _, err := b.runtime.Create(b.config) if err != nil { return "", err } @@ -423,7 +426,7 @@ func (b *buildFile) commit(id string, autoCmd []string, comment string) error { } } - container, err := b.runtime.Create(b.config) + container, _, err := b.runtime.Create(b.config) if err != nil { return err } diff --git a/commands.go b/commands.go index 7d33b81d0..d69ed9319 100644 --- a/commands.go +++ b/commands.go @@ -98,6 +98,7 @@ func (cli *DockerCli) CmdHelp(args ...string) error { {"kill", "Kill a running container"}, {"login", "Register or Login to the docker registry server"}, {"logs", "Fetch the logs of a container"}, + {"ls", "List links for containers"}, {"port", "Lookup the public-facing port which is NAT-ed to PRIVATE_PORT"}, {"top", "Lookup the running processes of a container"}, {"ps", "List containers"}, @@ -510,7 +511,8 @@ func (cli *DockerCli) CmdStop(args ...string) error { v.Set("t", strconv.Itoa(*nSeconds)) for _, name := range cmd.Args() { - _, _, err := cli.call("POST", "/containers/"+name+"/stop?"+v.Encode(), nil) + encName := cleanName(name) + _, _, err := cli.call("POST", "/containers/"+encName+"/stop?"+v.Encode(), nil) if err != nil { fmt.Fprintf(cli.err, "%s\n", err) } else { @@ -535,7 +537,8 @@ func (cli *DockerCli) CmdRestart(args ...string) error { v.Set("t", strconv.Itoa(*nSeconds)) for _, name := range cmd.Args() { - _, _, err := cli.call("POST", "/containers/"+name+"/restart?"+v.Encode(), nil) + encName := cleanName(name) + _, _, err := cli.call("POST", "/containers/"+encName+"/restart?"+v.Encode(), nil) if err != nil { fmt.Fprintf(cli.err, "%s\n", err) } else { @@ -557,7 +560,8 @@ func (cli *DockerCli) CmdStart(args ...string) error { var encounteredError error for _, name := range args { - _, _, err := cli.call("POST", "/containers/"+name+"/start", nil) + encName := cleanName(name) + _, _, err := cli.call("POST", "/containers/"+encName+"/start", nil) if err != nil { fmt.Fprintf(cli.err, "%s\n", err) encounteredError = fmt.Errorf("Error: failed to start one or more containers") @@ -579,10 +583,11 @@ func (cli *DockerCli) CmdInspect(args ...string) error { } fmt.Fprintf(cli.out, "[") for i, name := range args { + encName := cleanName(name) if i > 0 { fmt.Fprintf(cli.out, ",") } - obj, _, err := cli.call("GET", "/containers/"+name+"/json", nil) + obj, _, err := cli.call("GET", "/containers/"+encName+"/json", nil) if err != nil { obj, _, err = cli.call("GET", "/images/"+name+"/json", nil) if err != nil { @@ -740,6 +745,8 @@ func (cli *DockerCli) CmdHistory(args ...string) error { func (cli *DockerCli) CmdRm(args ...string) error { cmd := Subcmd("rm", "[OPTIONS] CONTAINER [CONTAINER...]", "Remove one or more containers") v := cmd.Bool("v", false, "Remove the volumes associated to the container") + link := cmd.Bool("link", false, "Remove the specified link and not the underlying container") + if err := cmd.Parse(args); err != nil { return nil } @@ -751,8 +758,12 @@ func (cli *DockerCli) CmdRm(args ...string) error { if *v { val.Set("v", "1") } + if *link { + val.Set("link", "1") + } for _, name := range cmd.Args() { - _, _, err := cli.call("DELETE", "/containers/"+name+"?"+val.Encode(), nil) + encName := cleanName(name) + _, _, err := cli.call("DELETE", "/containers/"+encName+"?"+val.Encode(), nil) if err != nil { fmt.Fprintf(cli.err, "%s\n", err) } else { @@ -774,7 +785,8 @@ func (cli *DockerCli) CmdKill(args ...string) error { } for _, name := range args { - _, _, err := cli.call("POST", "/containers/"+name+"/kill", nil) + encName := cleanName(name) + _, _, err := cli.call("POST", "/containers/"+encName+"/kill", nil) if err != nil { fmt.Fprintf(cli.err, "%s\n", err) } else { @@ -1017,10 +1029,10 @@ func (cli *DockerCli) CmdImages(args ...string) error { func displayablePorts(ports []APIPort) string { result := []string{} for _, port := range ports { - if port.Type == "tcp" { - result = append(result, fmt.Sprintf("%d->%d", port.PublicPort, port.PrivatePort)) + if port.IP == "" { + result = append(result, fmt.Sprintf("%d/%s", port.PublicPort, port.Type)) } else { - result = append(result, fmt.Sprintf("%d->%d/%s", port.PublicPort, port.PrivatePort, port.Type)) + result = append(result, fmt.Sprintf("%s:%d->%d/%s", port.IP, port.PublicPort, port.PrivatePort, port.Type)) } } sort.Strings(result) @@ -1073,7 +1085,7 @@ func (cli *DockerCli) CmdPs(args ...string) error { } w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0) if !*quiet { - fmt.Fprint(w, "ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS") + fmt.Fprint(w, "ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") if *size { fmt.Fprintln(w, "\tSIZE") } else { @@ -1082,11 +1094,16 @@ func (cli *DockerCli) CmdPs(args ...string) error { } for _, out := range outs { + for i := 0; i < len(out.Names); i++ { + out.Names[i] = utils.Trunc(out.Names[i], 10) + } + + names := strings.Join(out.Names, ",") if !*quiet { if *noTrunc { - fmt.Fprintf(w, "%s\t%s\t%s\t%s ago\t%s\t%s\t", out.ID, out.Image, out.Command, utils.HumanDuration(time.Now().Sub(time.Unix(out.Created, 0))), out.Status, displayablePorts(out.Ports)) + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s ago\t%s\t%s\t", out.ID, out.Image, out.Command, utils.HumanDuration(time.Now().Sub(time.Unix(out.Created, 0))), out.Status, displayablePorts(out.Ports), names) } else { - fmt.Fprintf(w, "%s\t%s\t%s\t%s ago\t%s\t%s\t", utils.TruncateID(out.ID), out.Image, utils.Trunc(out.Command, 20), utils.HumanDuration(time.Now().Sub(time.Unix(out.Created, 0))), out.Status, displayablePorts(out.Ports)) + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s ago\t%s\t%s\t", utils.TruncateID(out.ID), out.Image, utils.Trunc(out.Command, 20), utils.HumanDuration(time.Now().Sub(time.Unix(out.Created, 0))), out.Status, displayablePorts(out.Ports), names) } if *size { if out.SizeRootFs > 0 { @@ -1112,6 +1129,64 @@ func (cli *DockerCli) CmdPs(args ...string) error { return nil } +func (cli *DockerCli) CmdLs(args ...string) error { + cmd := Subcmd("ls", "", "List links for containers") + flAll := cmd.Bool("a", false, "Show all links") + + if err := cmd.Parse(args); err != nil { + return nil + } + v := url.Values{} + if *flAll { + v.Set("all", "1") + } + + body, _, err := cli.call("GET", "/containers/links?"+v.Encode(), nil) + if err != nil { + return err + } + var links []APILink + if err := json.Unmarshal(body, &links); err != nil { + return err + } + + w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0) + fmt.Fprintf(w, "NAME\tID\tIMAGE") + fmt.Fprintf(w, "\n") + + sortLinks(links, func(i, j APILink) bool { + return len(i.Path) < len(j.Path) + }) + for _, link := range links { + fmt.Fprintf(w, "%s\t%s\t%s", link.Path, utils.TruncateID(link.ContainerID), link.Image) + fmt.Fprintf(w, "\n") + } + w.Flush() + + return nil +} + +func (cli *DockerCli) CmdLink(args ...string) error { + cmd := Subcmd("link", "CURRENT_NAME NEW_NAME", "Link the container with a new name") + if err := cmd.Parse(args); err != nil { + return nil + } + if cmd.NArg() != 2 { + cmd.Usage() + return nil + } + body := map[string]string{ + "currentName": cmd.Arg(0), + "newName": cmd.Arg(1), + } + + _, _, err := cli.call("POST", "/containers/link", body) + if err != nil { + return err + } + return nil +} + func (cli *DockerCli) CmdCommit(args ...string) error { cmd := Subcmd("commit", "[OPTIONS] CONTAINER [REPOSITORY [TAG]]", "Create a new image from a container's changes") flComment := cmd.String("m", "", "Commit message") @@ -1229,8 +1304,9 @@ func (cli *DockerCli) CmdLogs(args ...string) error { cmd.Usage() return nil } + name := cleanName(cmd.Arg(0)) - if err := cli.hijack("POST", "/containers/"+cmd.Arg(0)+"/attach?logs=1&stdout=1&stderr=1", false, nil, cli.out, cli.err); err != nil { + if err := cli.hijack("POST", "/containers/"+name+"/attach?logs=1&stdout=1&stderr=1", false, nil, cli.out, cli.err); err != nil { return err } return nil @@ -1245,8 +1321,9 @@ func (cli *DockerCli) CmdAttach(args ...string) error { cmd.Usage() return nil } - - body, _, err := cli.call("GET", "/containers/"+cmd.Arg(0)+"/json", nil) + name := cmd.Arg(0) + name = cleanName(name) + body, _, err := cli.call("GET", "/containers/"+name+"/json", nil) if err != nil { return err } @@ -1920,6 +1997,10 @@ func getExitCode(cli *DockerCli, containerId string) (int, error) { return c.State.ExitCode, nil } +func cleanName(name string) string { + return strings.Replace(name, "/", "%252F", -1) +} + func NewDockerCli(in io.ReadCloser, out, err io.Writer, proto, addr string) *DockerCli { var ( isTerminal = false diff --git a/config.go b/config.go new file mode 100644 index 000000000..d32179056 --- /dev/null +++ b/config.go @@ -0,0 +1,17 @@ +package docker + +import ( + "net" +) + +type DaemonConfig struct { + Pidfile string + GraphPath string + ProtoAddresses []string + AutoRestart bool + EnableCors bool + Dns []string + EnableIptables bool + BridgeIface string + DefaultIp net.IP +} diff --git a/container.go b/container.go index 7d85aef0a..3f9a01519 100644 --- a/container.go +++ b/container.go @@ -16,7 +16,6 @@ import ( "os/exec" "path" "path/filepath" - "strconv" "strings" "syscall" "time" @@ -58,6 +57,8 @@ type Container struct { // Store rw/ro in a separate structure to preserve reverse-compatibility on-disk. // Easier than migrating older container configs :) VolumesRW map[string]bool + + activeLinks map[string]*Link } type Config struct { @@ -70,7 +71,8 @@ type Config struct { AttachStdin bool AttachStdout bool AttachStderr bool - PortSpecs []string + PortSpecs []string // Deprecated - Can be in the format of 8080/tcp + ExposedPorts map[Port]struct{} Tty bool // Attach standard streams to a tty, including stdin if it is not closed. OpenStdin bool // Open stdin StdinOnce bool // If true, close stdin after the 1 attached client disconnects. @@ -90,6 +92,8 @@ type HostConfig struct { Binds []string ContainerIDFile string LxcConf []KeyValuePair + PortBindings map[Port][]PortBinding + Links []string } type BindMap struct { @@ -107,6 +111,34 @@ type KeyValuePair struct { Value string } +type PortBinding struct { + HostIp string + HostPort string +} + +// 80/tcp +type Port string + +func (p Port) Proto() string { + return strings.Split(string(p), "/")[1] +} + +func (p Port) Port() string { + return strings.Split(string(p), "/")[0] +} + +func (p Port) Int() int { + i, err := parsePort(p.Port()) + if err != nil { + panic(err) + } + return i +} + +func NewPort(proto, port string) Port { + return Port(fmt.Sprintf("%s/%s", port, proto)) +} + func ParseRun(args []string, capabilities *Capabilities) (*Config, *HostConfig, *flag.FlagSet, error) { cmd := Subcmd("run", "[OPTIONS] IMAGE [COMMAND] [ARG...]", "Run a command in a new container") if os.Getenv("TEST") != "" { @@ -135,8 +167,11 @@ func ParseRun(args []string, capabilities *Capabilities) (*Config, *HostConfig, flCpuShares := cmd.Int64("c", 0, "CPU shares (relative weight)") - var flPorts ListOpts - cmd.Var(&flPorts, "p", "Expose a container's port to the host (use 'docker port' to see the actual mapping)") + var flPublish ListOpts + cmd.Var(&flPublish, "p", "Publish a container's port to the host (use 'docker port' to see the actual mapping)") + + var flExpose ListOpts + cmd.Var(&flExpose, "expose", "Expose a port from the container without publishing it to your host") var flEnv ListOpts cmd.Var(&flEnv, "e", "Set environment variables") @@ -155,6 +190,9 @@ func ParseRun(args []string, capabilities *Capabilities) (*Config, *HostConfig, var flLxcOpts ListOpts cmd.Var(&flLxcOpts, "lxc-conf", "Add custom lxc options -lxc-conf=\"lxc.cgroup.cpuset.cpus = 0,1\"") + var flLinks ListOpts + cmd.Var(&flLinks, "link", "Add link to another container (containerid:alias)") + if err := cmd.Parse(args); err != nil { return nil, nil, cmd, err } @@ -220,10 +258,28 @@ func ParseRun(args []string, capabilities *Capabilities) (*Config, *HostConfig, hostname = parts[0] domainname = parts[1] } + + ports, portBindings, err := parsePortSpecs(flPublish) + if err != nil { + return nil, nil, cmd, err + } + + // Merge in exposed ports to the map of published ports + for _, e := range flExpose { + if strings.Contains(e, ":") { + return nil, nil, cmd, fmt.Errorf("Invalid port format for -expose: %s", e) + } + p := NewPort(splitProtoPort(e)) + if _, exists := ports[p]; !exists { + ports[p] = struct{}{} + } + } + config := &Config{ - Hostname: hostname, + Hostname: *flHostname, Domainname: domainname, - PortSpecs: flPorts, + PortSpecs: nil, // Deprecated + ExposedPorts: ports, User: *flUser, Tty: *flTty, NetworkDisabled: !*flNetwork, @@ -243,10 +299,13 @@ func ParseRun(args []string, capabilities *Capabilities) (*Config, *HostConfig, Privileged: *flPrivileged, WorkingDir: *flWorkingDir, } + hostConfig := &HostConfig{ Binds: binds, ContainerIDFile: *flContainerIDFile, LxcConf: lxcConf, + PortBindings: portBindings, + Links: flLinks, } if capabilities != nil && *flMemory > 0 && !capabilities.SwapLimit { @@ -261,36 +320,38 @@ func ParseRun(args []string, capabilities *Capabilities) (*Config, *HostConfig, return config, hostConfig, cmd, nil } -type PortMapping map[string]string +type PortMapping map[string]string // Deprecated type NetworkSettings struct { IPAddress string IPPrefixLen int Gateway string Bridge string - PortMapping map[string]PortMapping + PortMapping map[string]PortMapping // Deprecated + Ports map[Port][]PortBinding } -// returns a more easy to process description of the port mapping defined in the settings func (settings *NetworkSettings) PortMappingAPI() []APIPort { var mapping []APIPort - for private, public := range settings.PortMapping["Tcp"] { - pubint, _ := strconv.ParseInt(public, 0, 0) - privint, _ := strconv.ParseInt(private, 0, 0) - mapping = append(mapping, APIPort{ - PrivatePort: privint, - PublicPort: pubint, - Type: "tcp", - }) - } - for private, public := range settings.PortMapping["Udp"] { - pubint, _ := strconv.ParseInt(public, 0, 0) - privint, _ := strconv.ParseInt(private, 0, 0) - mapping = append(mapping, APIPort{ - PrivatePort: privint, - PublicPort: pubint, - Type: "udp", - }) + for port, bindings := range settings.Ports { + p, _ := parsePort(port.Port()) + if len(bindings) == 0 { + mapping = append(mapping, APIPort{ + PublicPort: int64(p), + Type: port.Proto(), + }) + continue + } + for _, binding := range bindings { + p, _ := parsePort(port.Port()) + h, _ := parsePort(binding.HostPort) + mapping = append(mapping, APIPort{ + PrivatePort: int64(p), + PublicPort: int64(h), + Type: port.Proto(), + IP: binding.HostIp, + }) + } } return mapping } @@ -590,7 +651,7 @@ func (container *Container) Start(hostConfig *HostConfig) (err error) { if container.runtime.networkManager.disabled { container.Config.NetworkDisabled = true } else { - if err := container.allocateNetwork(); err != nil { + if err := container.allocateNetwork(hostConfig); err != nil { return err } } @@ -780,6 +841,46 @@ func (container *Container) Start(hostConfig *HostConfig) (err error) { "-e", "container=lxc", "-e", "HOSTNAME="+container.Config.Hostname, ) + + // Init any links between the parent and children + runtime := container.runtime + + children, err := runtime.Children(fmt.Sprintf("/%s", container.ID)) + if err != nil { + return err + } + + if len(children) > 0 { + container.activeLinks = make(map[string]*Link, len(children)) + + // If we encounter an error make sure that we rollback any network + // config and ip table changes + rollback := func() { + for _, link := range container.activeLinks { + link.Disable() + } + container.activeLinks = nil + } + + for p, child := range children { + link, err := NewLink(container, child, p, runtime.networkManager.bridgeIface) + if err != nil { + rollback() + return err + } + + container.activeLinks[link.Alias()] = link + if err := link.Enable(); err != nil { + rollback() + return err + } + + for _, envVar := range link.ToEnv() { + params = append(params, "-e", envVar) + } + } + } + if container.Config.WorkingDir != "" { workingDir := path.Clean(container.Config.WorkingDir) utils.Debugf("[working dir] working dir is %s", workingDir) @@ -882,7 +983,7 @@ func (container *Container) StderrPipe() (io.ReadCloser, error) { return utils.NewBufReader(reader), nil } -func (container *Container) allocateNetwork() error { +func (container *Container) allocateNetwork(hostConfig *HostConfig) error { if container.Config.NetworkDisabled { return nil } @@ -909,36 +1010,59 @@ func (container *Container) allocateNetwork() error { } } - var portSpecs []string - if !container.State.Ghost { - portSpecs = container.Config.PortSpecs - } else { - for backend, frontend := range container.NetworkSettings.PortMapping["Tcp"] { - portSpecs = append(portSpecs, fmt.Sprintf("%s:%s/tcp", frontend, backend)) + if container.Config.PortSpecs != nil { + utils.Debugf("Migrating port mappings for container: %s", strings.Join(container.Config.PortSpecs, ", ")) + if err := migratePortMappings(container.Config); err != nil { + return err } - for backend, frontend := range container.NetworkSettings.PortMapping["Udp"] { - portSpecs = append(portSpecs, fmt.Sprintf("%s:%s/udp", frontend, backend)) + container.Config.PortSpecs = nil + } + + portSpecs := make(map[Port]struct{}) + bindings := make(map[Port][]PortBinding) + + if !container.State.Ghost { + if container.Config.ExposedPorts != nil { + portSpecs = container.Config.ExposedPorts + } + if hostConfig.PortBindings != nil { + bindings = hostConfig.PortBindings + } + } else { + if container.NetworkSettings.Ports != nil { + for port, binding := range container.NetworkSettings.Ports { + portSpecs[port] = struct{}{} + bindings[port] = binding + } } } - container.NetworkSettings.PortMapping = make(map[string]PortMapping) - container.NetworkSettings.PortMapping["Tcp"] = make(PortMapping) - container.NetworkSettings.PortMapping["Udp"] = make(PortMapping) - for _, spec := range portSpecs { - nat, err := iface.AllocatePort(spec) - if err != nil { - iface.Release() - return err + container.NetworkSettings.PortMapping = nil + + for port := range portSpecs { + binding := bindings[port] + for i := 0; i < len(binding); i++ { + b := binding[i] + nat, err := iface.AllocatePort(port, b) + if err != nil { + iface.Release() + return err + } + utils.Debugf("Allocate port: %s:%s->%s", nat.Binding.HostIp, port, nat.Binding.HostPort) + binding[i] = nat.Binding } - proto := strings.Title(nat.Proto) - backend, frontend := strconv.Itoa(nat.Backend), strconv.Itoa(nat.Frontend) - container.NetworkSettings.PortMapping[proto][backend] = frontend + bindings[port] = binding } + container.SaveHostConfig(hostConfig) + + container.NetworkSettings.Ports = bindings container.network = iface + container.NetworkSettings.Bridge = container.runtime.networkManager.bridgeIface container.NetworkSettings.IPAddress = iface.IPNet.IP.String() container.NetworkSettings.IPPrefixLen, _ = iface.IPNet.Mask.Size() container.NetworkSettings.Gateway = iface.Gateway.String() + return nil } @@ -1021,6 +1145,14 @@ func (container *Container) monitor(hostConfig *HostConfig) { func (container *Container) cleanup() { container.releaseNetwork() + + // Disable all active links + if container.activeLinks != nil { + for _, link := range container.activeLinks { + link.Disable() + } + } + if container.Config.OpenStdin { if err := container.stdin.Close(); err != nil { utils.Errorf("%s: Error close stdin: %s", container.ID, err) @@ -1292,3 +1424,9 @@ func (container *Container) Copy(resource string) (Archive, error) { } return TarFilter(basePath, Uncompressed, filter) } + +// Returns true if the container exposes a certain port +func (container *Container) Exposes(p Port) bool { + _, exists := container.Config.ExposedPorts[p] + return exists +} diff --git a/container_test.go b/container_test.go index 9fdddc904..10027cbec 100644 --- a/container_test.go +++ b/container_test.go @@ -18,7 +18,7 @@ import ( func TestIDFormat(t *testing.T) { runtime := mkRuntime(t) defer nuke(runtime) - container1, err := runtime.Create( + container1, _, err := runtime.Create( &Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"/bin/sh", "-c", "echo hello world"}, @@ -388,7 +388,7 @@ func TestRun(t *testing.T) { func TestOutput(t *testing.T) { runtime := mkRuntime(t) defer nuke(runtime) - container, err := runtime.Create( + container, _, err := runtime.Create( &Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"echo", "-n", "foobar"}, @@ -411,7 +411,7 @@ func TestKillDifferentUser(t *testing.T) { runtime := mkRuntime(t) defer nuke(runtime) - container, err := runtime.Create(&Config{ + container, _, err := runtime.Create(&Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"cat"}, OpenStdin: true, @@ -471,7 +471,7 @@ func TestCreateVolume(t *testing.T) { if err != nil { t.Fatal(err) } - c, err := runtime.Create(config) + c, _, err := runtime.Create(config) if err != nil { t.Fatal(err) } @@ -486,7 +486,7 @@ func TestCreateVolume(t *testing.T) { func TestKill(t *testing.T) { runtime := mkRuntime(t) defer nuke(runtime) - container, err := runtime.Create(&Config{ + container, _, err := runtime.Create(&Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"sleep", "2"}, }, @@ -530,7 +530,7 @@ func TestExitCode(t *testing.T) { runtime := mkRuntime(t) defer nuke(runtime) - trueContainer, err := runtime.Create(&Config{ + trueContainer, _, err := runtime.Create(&Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"/bin/true", ""}, }) @@ -545,7 +545,7 @@ func TestExitCode(t *testing.T) { t.Errorf("Unexpected exit code %d (expected 0)", trueContainer.State.ExitCode) } - falseContainer, err := runtime.Create(&Config{ + falseContainer, _, err := runtime.Create(&Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"/bin/false", ""}, }) @@ -564,7 +564,7 @@ func TestExitCode(t *testing.T) { func TestRestart(t *testing.T) { runtime := mkRuntime(t) defer nuke(runtime) - container, err := runtime.Create(&Config{ + container, _, err := runtime.Create(&Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"echo", "-n", "foobar"}, }, @@ -594,7 +594,7 @@ func TestRestart(t *testing.T) { func TestRestartStdin(t *testing.T) { runtime := mkRuntime(t) defer nuke(runtime) - container, err := runtime.Create(&Config{ + container, _, err := runtime.Create(&Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"cat"}, @@ -672,7 +672,7 @@ func TestUser(t *testing.T) { defer nuke(runtime) // Default user must be root - container, err := runtime.Create(&Config{ + container, _, err := runtime.Create(&Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"id"}, }, @@ -690,7 +690,7 @@ func TestUser(t *testing.T) { } // Set a username - container, err = runtime.Create(&Config{ + container, _, err = runtime.Create(&Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"id"}, @@ -710,7 +710,7 @@ func TestUser(t *testing.T) { } // Set a UID - container, err = runtime.Create(&Config{ + container, _, err = runtime.Create(&Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"id"}, @@ -730,7 +730,7 @@ func TestUser(t *testing.T) { } // Set a different user by uid - container, err = runtime.Create(&Config{ + container, _, err = runtime.Create(&Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"id"}, @@ -752,7 +752,7 @@ func TestUser(t *testing.T) { } // Set a different user by username - container, err = runtime.Create(&Config{ + container, _, err = runtime.Create(&Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"id"}, @@ -772,7 +772,7 @@ func TestUser(t *testing.T) { } // Test an wrong username - container, err = runtime.Create(&Config{ + container, _, err = runtime.Create(&Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"id"}, @@ -793,7 +793,7 @@ func TestMultipleContainers(t *testing.T) { runtime := mkRuntime(t) defer nuke(runtime) - container1, err := runtime.Create(&Config{ + container1, _, err := runtime.Create(&Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"sleep", "2"}, }, @@ -803,7 +803,7 @@ func TestMultipleContainers(t *testing.T) { } defer runtime.Destroy(container1) - container2, err := runtime.Create(&Config{ + container2, _, err := runtime.Create(&Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"sleep", "2"}, }, @@ -847,7 +847,7 @@ func TestMultipleContainers(t *testing.T) { func TestStdin(t *testing.T) { runtime := mkRuntime(t) defer nuke(runtime) - container, err := runtime.Create(&Config{ + container, _, err := runtime.Create(&Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"cat"}, @@ -892,7 +892,7 @@ func TestStdin(t *testing.T) { func TestTty(t *testing.T) { runtime := mkRuntime(t) defer nuke(runtime) - container, err := runtime.Create(&Config{ + container, _, err := runtime.Create(&Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"cat"}, @@ -937,7 +937,7 @@ func TestTty(t *testing.T) { func TestEnv(t *testing.T) { runtime := mkRuntime(t) defer nuke(runtime) - container, err := runtime.Create(&Config{ + container, _, err := runtime.Create(&Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"env"}, }, @@ -986,7 +986,7 @@ func TestEnv(t *testing.T) { func TestEntrypoint(t *testing.T) { runtime := mkRuntime(t) defer nuke(runtime) - container, err := runtime.Create( + container, _, err := runtime.Create( &Config{ Image: GetTestImage(runtime).ID, Entrypoint: []string{"/bin/echo"}, @@ -1009,7 +1009,7 @@ func TestEntrypoint(t *testing.T) { func TestEntrypointNoCmd(t *testing.T) { runtime := mkRuntime(t) defer nuke(runtime) - container, err := runtime.Create( + container, _, err := runtime.Create( &Config{ Image: GetTestImage(runtime).ID, Entrypoint: []string{"/bin/echo", "foobar"}, @@ -1060,7 +1060,7 @@ func TestLXCConfig(t *testing.T) { cpuMin := 100 cpuMax := 10000 cpu := cpuMin + rand.Intn(cpuMax-cpuMin) - container, err := runtime.Create(&Config{ + container, _, err := runtime.Create(&Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"/bin/true"}, @@ -1084,7 +1084,7 @@ func TestLXCConfig(t *testing.T) { func TestCustomLxcConfig(t *testing.T) { runtime := mkRuntime(t) defer nuke(runtime) - container, err := runtime.Create(&Config{ + container, _, err := runtime.Create(&Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"/bin/true"}, @@ -1115,7 +1115,7 @@ func BenchmarkRunSequencial(b *testing.B) { runtime := mkRuntime(b) defer nuke(runtime) for i := 0; i < b.N; i++ { - container, err := runtime.Create(&Config{ + container, _, err := runtime.Create(&Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"echo", "-n", "foo"}, }, @@ -1147,7 +1147,7 @@ func BenchmarkRunParallel(b *testing.B) { complete := make(chan error) tasks = append(tasks, complete) go func(i int, complete chan error) { - container, err := runtime.Create(&Config{ + container, _, err := runtime.Create(&Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"echo", "-n", "foo"}, }, @@ -1297,7 +1297,7 @@ func TestBindMounts(t *testing.T) { func TestVolumesFromReadonlyMount(t *testing.T) { runtime := mkRuntime(t) defer nuke(runtime) - container, err := runtime.Create( + container, _, err := runtime.Create( &Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"/bin/echo", "-n", "foobar"}, @@ -1316,7 +1316,7 @@ func TestVolumesFromReadonlyMount(t *testing.T) { t.Fail() } - container2, err := runtime.Create( + container2, _, err := runtime.Create( &Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"/bin/echo", "-n", "foobar"}, @@ -1352,7 +1352,7 @@ func TestRestartWithVolumes(t *testing.T) { runtime := mkRuntime(t) defer nuke(runtime) - container, err := runtime.Create(&Config{ + container, _, err := runtime.Create(&Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"echo", "-n", "foobar"}, Volumes: map[string]struct{}{"/test": {}}, @@ -1395,7 +1395,7 @@ func TestVolumesFromWithVolumes(t *testing.T) { runtime := mkRuntime(t) defer nuke(runtime) - container, err := runtime.Create(&Config{ + container, _, err := runtime.Create(&Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"sh", "-c", "echo -n bar > /test/foo"}, Volumes: map[string]struct{}{"/test": {}}, @@ -1422,7 +1422,7 @@ func TestVolumesFromWithVolumes(t *testing.T) { t.Fail() } - container2, err := runtime.Create( + container2, _, err := runtime.Create( &Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"cat", "/test/foo"}, @@ -1463,7 +1463,7 @@ func TestOnlyLoopbackExistsWhenUsingDisableNetworkOption(t *testing.T) { if err != nil { t.Fatal(err) } - c, err := runtime.Create(config) + c, _, err := runtime.Create(config) if err != nil { t.Fatal(err) } @@ -1529,7 +1529,7 @@ func TestMultipleVolumesFrom(t *testing.T) { runtime := mkRuntime(t) defer nuke(runtime) - container, err := runtime.Create(&Config{ + container, _, err := runtime.Create(&Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"sh", "-c", "echo -n bar > /test/foo"}, Volumes: map[string]struct{}{"/test": {}}, @@ -1556,7 +1556,7 @@ func TestMultipleVolumesFrom(t *testing.T) { t.Fail() } - container2, err := runtime.Create( + container2, _, err := runtime.Create( &Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"sh", "-c", "echo -n bar > /other/foo"}, @@ -1577,7 +1577,7 @@ func TestMultipleVolumesFrom(t *testing.T) { t.Fatal(err) } - container3, err := runtime.Create( + container3, _, err := runtime.Create( &Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"/bin/echo", "-n", "foobar"}, diff --git a/docker/docker.go b/docker/docker.go index 750d63c06..e2f000786 100644 --- a/docker/docker.go +++ b/docker/docker.go @@ -7,6 +7,7 @@ import ( "github.com/dotcloud/docker/utils" "io/ioutil" "log" + "net" "os" "os/signal" "strconv" @@ -37,7 +38,11 @@ func main() { flDns := flag.String("dns", "", "Set custom dns servers") flHosts := docker.ListOpts{fmt.Sprintf("unix://%s", docker.DEFAULTUNIXSOCKET)} flag.Var(&flHosts, "H", "tcp://host:port to bind/connect to or unix://path/to/socket to use") + flEnableIptables := flag.Bool("iptables", true, "Disable iptables within docker") + flDefaultIp := flag.String("ip", "0.0.0.0", "Default ip address to use when binding a containers ports") + flag.Parse() + if *flVersion { showVersion() return @@ -49,10 +54,9 @@ func main() { flHosts[i] = utils.ParseHost(docker.DEFAULTHTTPHOST, docker.DEFAULTHTTPPORT, flHost) } + bridge := docker.DefaultNetworkBridge if *bridgeName != "" { - docker.NetworkBridgeIface = *bridgeName - } else { - docker.NetworkBridgeIface = docker.DefaultNetworkBridge + bridge = *bridgeName } if *flDebug { os.Setenv("DEBUG", "1") @@ -64,7 +68,25 @@ func main() { flag.Usage() return } - if err := daemon(*pidfile, *flGraphPath, flHosts, *flAutoRestart, *flEnableCors, *flDns); err != nil { + var dns []string + if *flDns != "" { + dns = []string{*flDns} + } + + ip := net.ParseIP(*flDefaultIp) + + config := &docker.DaemonConfig{ + Pidfile: *pidfile, + GraphPath: *flGraphPath, + AutoRestart: *flAutoRestart, + EnableCors: *flEnableCors, + Dns: dns, + EnableIptables: *flEnableIptables, + BridgeIface: bridge, + ProtoAddresses: flHosts, + DefaultIp: ip, + } + if err := daemon(config); err != nil { log.Fatal(err) os.Exit(-1) } @@ -115,30 +137,26 @@ func removePidFile(pidfile string) { } } -func daemon(pidfile string, flGraphPath string, protoAddrs []string, autoRestart, enableCors bool, flDns string) error { - if err := createPidFile(pidfile); err != nil { +func daemon(config *docker.DaemonConfig) error { + if err := createPidFile(config.Pidfile); err != nil { log.Fatal(err) } - defer removePidFile(pidfile) + defer removePidFile(config.Pidfile) c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt, os.Kill, os.Signal(syscall.SIGTERM)) go func() { sig := <-c log.Printf("Received signal '%v', exiting\n", sig) - removePidFile(pidfile) + removePidFile(config.Pidfile) os.Exit(0) }() - var dns []string - if flDns != "" { - dns = []string{flDns} - } - server, err := docker.NewServer(flGraphPath, autoRestart, enableCors, dns) + server, err := docker.NewServer(config) if err != nil { return err } - chErrors := make(chan error, len(protoAddrs)) - for _, protoAddr := range protoAddrs { + chErrors := make(chan error, len(config.ProtoAddresses)) + for _, protoAddr := range config.ProtoAddresses { protoAddrParts := strings.SplitN(protoAddr, "://", 2) if protoAddrParts[0] == "unix" { syscall.Unlink(protoAddrParts[1]) @@ -154,7 +172,7 @@ func daemon(pidfile string, flGraphPath string, protoAddrs []string, autoRestart chErrors <- docker.ListenAndServe(protoAddrParts[0], protoAddrParts[1], server, true) }() } - for i := 0; i < len(protoAddrs); i += 1 { + for i := 0; i < len(config.ProtoAddresses); i += 1 { err := <-chErrors if err != nil { return err diff --git a/docs/sources/commandline/cli.rst b/docs/sources/commandline/cli.rst index 71a902d46..db0a03dfc 100644 --- a/docs/sources/commandline/cli.rst +++ b/docs/sources/commandline/cli.rst @@ -93,8 +93,8 @@ Examples: .. _cli_build_examples: -Examples -~~~~~~~~ +Examples: +~~~~~~~~~ .. code-block:: bash @@ -400,6 +400,33 @@ Insert file from github Kill a running container +.. _cli_link: + +``link`` +-------- + +:: + + Usage: docker link CURRENT_NAME NEW_NAME + + Link a container to a new name. + + +Examples: +~~~~~~~~~ + +.. code-block:: bash + + $ docker link /59669e088202c2ebe150b4346cb3301562d073b51261176a354a74e8f618bfbc /redis + $ docker ls + NAME ID IMAGE + /redis 59669e088202c2ebe150b4346cb3301562d073b51261176a354a74e8f618bfbc crosbymichael/redis:latest + /59669e088202c2ebe150b4346cb3301562d073b51261176a354a74e8f618bfbc 59669e088202c2ebe150b4346cb3301562d073b51261176a354a74e8f618bfbc crosbymichael/redis:latest + + +This will create a new link for the existing name ``/59669e088202c2ebe150b4346cb3301562d073b51261176a354a74e8f618bfbc`` +with the new name ``/redis`` so that we can new reference the same container under the new name ``/redis``. + .. _cli_login: ``login`` @@ -427,7 +454,6 @@ Insert file from github ``logs`` -------- - :: Usage: docker logs [OPTIONS] CONTAINER @@ -507,6 +533,29 @@ Insert file from github Usage: docker rm [OPTIONS] CONTAINER Remove one or more containers + -link="": Remove the link instead of the actual container + + +Examples: +~~~~~~~~~ + +.. code-block:: bash + + $ docker rm /redis + /redis + + +This will remove the container referenced under the link ``/redis``. + + +.. code-block:: bash + + $ docker rm -link /webapp/redis + /webapp/redis + + +This will remove the underlying link between ``/webapp`` and the ``/redis`` containers removing all +network communication. .. _cli_rmi: @@ -530,7 +579,7 @@ Insert file from github Run a command in a new container - -a=map[]: Attach to stdin, stdout or stderr. + -a=map[]: Attach to stdin, stdout or stderr -c=0: CPU shares (relative weight) -cidfile="": Write the container ID to the file -d=false: Detached mode: Run container in the background, print new container id @@ -546,13 +595,15 @@ Insert file from github -u="": Username or UID -dns=[]: Set custom dns servers for the container -v=[]: Create a bind mount with: [host-dir]:[container-dir]:[rw|ro]. If "container-dir" is missing, then docker creates a new volume. - -volumes-from="": Mount all volumes from the given container. - -entrypoint="": Overwrite the default entrypoint set by the image. + -volumes-from="": Mount all volumes from the given container + -entrypoint="": Overwrite the default entrypoint set by the image -w="": Working directory inside the container -lxc-conf=[]: Add custom lxc options -lxc-conf="lxc.cgroup.cpuset.cpus = 0,1" + -expose=[]: Expose a port from the container without publishing it to your host + -link="": Add link to another container (containerid:alias) Examples -~~~~~~~~ +-------- .. code-block:: bash @@ -600,6 +651,38 @@ working directory, by changing into the directory to the value returned by ``pwd``. So this combination executes the command using the container, but inside the current working directory. +.. code-block:: bash + + docker run -p 127.0.0.0::80 ubuntu bash + +This the ``-p`` flag now allows you to bind a port to a specific +interface of the host machine. In this example port ``80`` of the +container will have a dynamically allocated port bound to 127.0.0.1 +of the host. + +.. code-block:: bash + + docker run -p 127.0.0.1:80:80 ubuntu bash + +This will bind port ``80`` of the container to port ``80`` on 127.0.0.1 of your +host machine. + +.. code-block:: bash + + docker run -expose 80 ubuntu bash + +This will expose port ``80`` of the container for use within a link +without publishing the port to the host system's interfaces. + +.. code-block:: bash + + docker run -link /redis:redis ubuntu bash + +The ``-link`` flag will link the container named ``/redis`` into the +newly created container with the alias ``redis``. The new container +can access the network and environment of the redis container via +environment variables. + .. _cli_search: ``search`` diff --git a/docs/sources/examples/index.rst b/docs/sources/examples/index.rst index 01251680b..74a7e7c40 100644 --- a/docs/sources/examples/index.rst +++ b/docs/sources/examples/index.rst @@ -1,6 +1,6 @@ :title: Docker Examples :description: Examples on how to use Docker -:keywords: docker, hello world, node, nodejs, python, couch, couchdb, redis, ssh, sshd, examples, postgresql +:keywords: docker, hello world, node, nodejs, python, couch, couchdb, redis, ssh, sshd, examples, postgresql, link .. _example_list: @@ -24,3 +24,4 @@ to more substantial services like you might find in production. postgresql_service mongodb running_riak_service + linking_into_redis diff --git a/docs/sources/examples/linking_into_redis.rst b/docs/sources/examples/linking_into_redis.rst new file mode 100644 index 000000000..6c9b7a4ff --- /dev/null +++ b/docs/sources/examples/linking_into_redis.rst @@ -0,0 +1,146 @@ +:title: Linking to an Redis container +:description: Running redis linked into your web app +:keywords: docker, example, networking, redis, link + +.. _linking_redis: + +Linking Redis +============= + +.. include:: example_header.inc + +Building a redis container to link as a child of our web application. + +Building the redis container +---------------------------- + +We will use a pre-build version of redis from the index under +the name ``crosbymichael/redis``. If you are interested in the +Dockerfile that was used to build this container here it is. + +.. code-block:: bash + + # Build redis from source + # Make sure you have the redis source code checked out in + # the same directory as this Dockerfile + FROM ubuntu + + RUN echo "deb http://archive.ubuntu.com/ubuntu precise main universe" > /etc/apt/sources.list + RUN apt-get update + RUN apt-get upgrade -y + + RUN apt-get install -y gcc make g++ build-essential libc6-dev tcl + + ADD . /redis + + RUN (cd /redis && make) + RUN (cd /redis && make test) + + RUN mkdir -p /redis-data + VOLUME ["/redis-data"] + EXPOSE 6379 + + ENTRYPOINT ["/redis/src/redis-server"] + CMD ["--dir", "/redis-data"] + + +We need to ``EXPOSE`` the default port of 6379 so that our link knows what ports +to connect to our redis container on. If you do not expose any ports for the +image then docker will not be able to establish the link between containers. + + +Run the redis container +----------------------- + +.. code-block:: bash + + docker run -d -e PASSWORD=docker crosbymichael/redis --requirepass=docker + +This will run our redis container using the default port of 6379 and using +as password to secure our service. Next we will link the redis container to +a new name using ``docker link`` and ``docker ls``. + + +Linking an existing container +----------------------------- + +.. code-block:: bash + + docker ls + + NAME ID IMAGE + /39588b6a45100ef5b328b2c302ea085624f29e6cbab70f88be04793af02cec89 39588b6a45100ef5b328b2c302ea085624f29e6cbab70f88be04793af02cec89 crosbymichael/redis:latest + + +Docker will automatically create an initial link with the container's id but +because the is long and not very user friendly we can link the container with +a new name. + +.. code-block:: bash + + docker link /39588b6a45100ef5b328b2c302ea085624f29e6cbab70f88be04793af02cec89 /redis + + docker ls + + NAME ID IMAGE + /redis 39588b6a45100ef5b328b2c302ea085624f29e6cbab70f88be04793af02cec89 crosbymichael/redis:latest + /39588b6a45100ef5b328b2c302ea085624f29e6cbab70f88be04793af02cec89 39588b6a45100ef5b328b2c302ea085624f29e6cbab70f88be04793af02cec89 crosbymichael/redis:latest + +Now we can reference our running redis service using the friendly name ``/redis``. +We can issue all the commands that you would expect; start, stop, attach, using the new name. + +Linking redis as a child +------------------------ + +Next we can start a new web application that has a dependency on redis and apply a link +to connect both containers. If you noticed when running our redis service we did not use +the ``-p`` option to publish the redis port to the host system. Redis exposed port 6379 +but we did not publish the port. This allows docker to prevent all network traffic to +the redis container except when explicitly specified within a link. This is a big win +for security. + + +Now lets start our web application with a link into redis. + +.. code-block:: bash + + docker run -t -i -link /redis:db ubuntu bash + + root@4c01db0b339c:/# env + + HOSTNAME=4c01db0b339c + DB_NAME=/4c01db0b339cf19958731255a796ee072040a652f51652a4ade190ab8c27006f/db + TERM=xterm + DB_PORT=tcp://172.17.0.8:6379 + DB_PORT_6379_TCP=tcp://172.17.0.8:6379 + PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + PWD=/ + DB_ENV_PASSWORD=dockerpass + SHLVL=1 + HOME=/ + container=lxc + _=/usr/bin/env + root@4c01db0b339c:/# + + +When we inspect the environment of the linked container we can see a few extra environment +variables have been added. When you specified ``-link /redis:db`` you are telling docker +to link the container named ``/redis`` into this new container with the alias ``db``. +Environment variables are prefixed with the alias so that the parent container can access +network and environment information from the child. + +.. code-block:: bash + + # The name of the child container + DB_NAME=/4c01db0b339cf19958731255a796ee072040a652f51652a4ade190ab8c27006f/db + # The default protocol, ip, and port of the service running in the container + DB_PORT=tcp://172.17.0.8:6379 + # A specific protocol, ip, and port of various services + DB_PORT_6379_TCP=tcp://172.17.0.8:6379 + # Get environment variables of the container + DB_ENV_PASSWORD=dockerpass + + +Accessing the network information along with the environment of the child container allows +us to easily connect to the redis service on the specific ip and port and use the password +specified in the environment. diff --git a/gograph/MAINTAINERS b/gograph/MAINTAINERS new file mode 100644 index 000000000..1e998f8ac --- /dev/null +++ b/gograph/MAINTAINERS @@ -0,0 +1 @@ +Michael Crosby (@crosbymichael) diff --git a/gograph/gograph.go b/gograph/gograph.go new file mode 100644 index 000000000..876bd6b58 --- /dev/null +++ b/gograph/gograph.go @@ -0,0 +1,455 @@ +package gograph + +import ( + _ "code.google.com/p/gosqlite/sqlite3" + "database/sql" + "fmt" + "os" + "path" +) + +const ( + createEntityTable = ` + CREATE TABLE IF NOT EXISTS entity ( + id text NOT NULL PRIMARY KEY + );` + + createEdgeTable = ` + CREATE TABLE IF NOT EXISTS edge ( + "entity_id" text NOT NULL, + "parent_id" text NULL, + "name" text NOT NULL, + CONSTRAINT "parent_fk" FOREIGN KEY ("parent_id") REFERENCES "entity" ("id"), + CONSTRAINT "entity_fk" FOREIGN KEY ("entity_id") REFERENCES "entity" ("id") + ); + + CREATE UNIQUE INDEX "name_parent_ix" ON "edge" (parent_id, name); + ` +) + +// Entity with a unique id +type Entity struct { + id string +} + +// An Edge connects two entities together +type Edge struct { + EntityID string + Name string + ParentID string +} + +type Entities map[string]*Entity +type Edges []*Edge + +type WalkFunc func(fullPath string, entity *Entity) error + +// Graph database for storing entities and their relationships +type Database struct { + dbPath string +} + +// Create a new graph database initialized with a root entity +func NewDatabase(dbPath string) (*Database, error) { + db := &Database{dbPath} + if _, err := os.Stat(dbPath); err == nil { + return db, nil + } + conn, err := db.openConn() + if err != nil { + return nil, err + } + defer conn.Close() + + if _, err := conn.Exec(createEntityTable); err != nil { + return nil, err + } + if _, err := conn.Exec(createEdgeTable); err != nil { + return nil, err + } + + rollback := func() { + conn.Exec("ROLLBACK") + } + + // Create root entities + if _, err := conn.Exec("BEGIN"); err != nil { + return nil, err + } + if _, err := conn.Exec("INSERT INTO entity (id) VALUES (?);", "0"); err != nil { + rollback() + return nil, err + } + + if _, err := conn.Exec("INSERT INTO edge (entity_id, name) VALUES(?,?);", "0", "/"); err != nil { + rollback() + return nil, err + } + + if _, err := conn.Exec("COMMIT"); err != nil { + return nil, err + } + return db, nil +} + +// Set the entity id for a given path +func (db *Database) Set(fullPath, id string) (*Entity, error) { + conn, err := db.openConn() + if err != nil { + return nil, err + } + defer conn.Close() + rollback := func() { + conn.Exec("ROLLBACK") + } + if _, err := conn.Exec("BEGIN"); err != nil { + return nil, err + } + var entityId string + if err := conn.QueryRow("SELECT id FROM entity WHERE id = ?;", id).Scan(&entityId); err != nil { + if err == sql.ErrNoRows { + if _, err := conn.Exec("INSERT INTO entity (id) VALUES(?);", id); err != nil { + rollback() + return nil, err + } + } else { + rollback() + return nil, err + } + } + e := &Entity{id} + + parentPath, name := splitPath(fullPath) + if err := db.setEdge(conn, parentPath, name, e); err != nil { + rollback() + return nil, err + } + + if _, err := conn.Exec("COMMIT"); err != nil { + return nil, err + } + return e, nil +} + +func (db *Database) setEdge(conn *sql.DB, parentPath, name string, e *Entity) error { + parent, err := db.get(conn, parentPath) + if err != nil { + return err + } + if parent.id == e.id { + return fmt.Errorf("Cannot set self as child") + } + + if _, err := conn.Exec("INSERT INTO edge (parent_id, name, entity_id) VALUES (?,?,?);", parent.id, name, e.id); err != nil { + return err + } + return nil +} + +// Return the root "/" entity for the database +func (db *Database) RootEntity() *Entity { + return &Entity{ + id: "0", + } +} + +// Return the entity for a given path +func (db *Database) Get(name string) *Entity { + conn, err := db.openConn() + if err != nil { + return nil + } + e, err := db.get(conn, name) + if err != nil { + return nil + } + return e +} + +func (db *Database) get(conn *sql.DB, name string) (*Entity, error) { + e := db.RootEntity() + // We always know the root name so return it if + // it is requested + if name == "/" { + return e, nil + } + + parts := split(name) + for i := 1; i < len(parts); i++ { + p := parts[i] + + next := db.child(conn, e, p) + if next == nil { + return nil, fmt.Errorf("Cannot find child") + } + e = next + } + return e, nil + +} + +// List all entities by from the name +// The key will be the full path of the entity +func (db *Database) List(name string, depth int) Entities { + out := Entities{} + conn, err := db.openConn() + if err != nil { + return out + } + defer conn.Close() + + for c := range db.children(conn, name, depth) { + out[c.FullPath] = c.Entity + } + return out +} + +func (db *Database) Walk(name string, walkFunc WalkFunc, depth int) error { + conn, err := db.openConn() + if err != nil { + return err + } + defer conn.Close() + + for c := range db.children(conn, name, depth) { + if err := walkFunc(c.FullPath, c.Entity); err != nil { + return err + } + } + return nil +} + +// Return the refrence count for a specified id +func (db *Database) Refs(id string) int { + conn, err := db.openConn() + if err != nil { + return -1 + } + defer conn.Close() + + var count int + if err := conn.QueryRow("SELECT COUNT(*) FROM edge WHERE entity_id = ?;", id).Scan(&count); err != nil { + return 0 + } + return count +} + +// Return all the id's path references +func (db *Database) RefPaths(id string) Edges { + refs := Edges{} + conn, err := db.openConn() + if err != nil { + return refs + } + defer conn.Close() + + rows, err := conn.Query("SELECT name, parent_id FROM edge WHERE entity_id = ?;", id) + if err != nil { + return refs + } + defer rows.Close() + + for rows.Next() { + var name string + var parentId string + if err := rows.Scan(&name, &parentId); err != nil { + return refs + } + refs = append(refs, &Edge{ + EntityID: id, + Name: name, + ParentID: parentId, + }) + } + return refs +} + +// Delete the reference to an entity at a given path +func (db *Database) Delete(name string) error { + if name == "/" { + return fmt.Errorf("Cannot delete root entity") + } + conn, err := db.openConn() + if err != nil { + return err + } + defer conn.Close() + + parentPath, n := splitPath(name) + parent, err := db.get(conn, parentPath) + if err != nil { + return err + } + + if _, err := conn.Exec("DELETE FROM edge WHERE parent_id = ? AND name = ?;", parent.id, n); err != nil { + return err + } + return nil +} + +// Remove the entity with the specified id +// Walk the graph to make sure all references to the entity +// are removed and return the number of references removed +func (db *Database) Purge(id string) (int, error) { + conn, err := db.openConn() + if err != nil { + return -1, err + } + defer conn.Close() + + rollback := func() { + conn.Exec("ROLLBACK") + } + + if _, err := conn.Exec("BEGIN"); err != nil { + return -1, err + } + + // Delete all edges + rows, err := conn.Exec("DELETE FROM edge WHERE entity_id = ?;", id) + if err != nil { + rollback() + return -1, err + } + + changes, err := rows.RowsAffected() + if err != nil { + return -1, err + } + + // Delete entity + if _, err := conn.Exec("DELETE FROM entity where id = ?;", id); err != nil { + rollback() + return -1, err + } + + if _, err := conn.Exec("COMMIT"); err != nil { + return -1, err + } + return int(changes), nil +} + +// Rename an edge for a given path +func (db *Database) Rename(currentName, newName string) error { + parentPath, name := splitPath(currentName) + newParentPath, newEdgeName := splitPath(newName) + + if parentPath != newParentPath { + return fmt.Errorf("Cannot rename when root paths do not match %s != %s", parentPath, newParentPath) + } + + conn, err := db.openConn() + if err != nil { + return err + } + defer conn.Close() + + parent, err := db.get(conn, parentPath) + if err != nil { + return err + } + + rows, err := conn.Exec("UPDATE edge SET name = ? WHERE parent_id = ? AND name = ?;", newEdgeName, parent.id, name) + if err != nil { + return err + } + i, err := rows.RowsAffected() + if err != nil { + return err + } + if i == 0 { + return fmt.Errorf("Cannot locate edge for %s %s", parent.id, name) + } + return nil +} + +type WalkMeta struct { + Parent *Entity + Entity *Entity + FullPath string + Edge *Edge +} + +func (db *Database) children(conn *sql.DB, name string, depth int) <-chan WalkMeta { + out := make(chan WalkMeta) + e, err := db.get(conn, name) + if err != nil { + close(out) + return out + } + + go func() { + rows, err := conn.Query("SELECT entity_id, name FROM edge where parent_id = ?;", e.id) + if err != nil { + close(out) + } + defer rows.Close() + + for rows.Next() { + var entityId, entityName string + if err := rows.Scan(&entityId, &entityName); err != nil { + // Log error + continue + } + child := &Entity{entityId} + edge := &Edge{ + ParentID: e.id, + Name: entityName, + EntityID: child.id, + } + + meta := WalkMeta{ + Parent: e, + Entity: child, + FullPath: path.Join(name, edge.Name), + Edge: edge, + } + + out <- meta + if depth == 0 { + continue + } + nDepth := depth + if depth != -1 { + nDepth -= 1 + } + sc := db.children(conn, meta.FullPath, nDepth) + for c := range sc { + out <- c + } + } + close(out) + }() + return out +} + +// Return the entity based on the parent path and name +func (db *Database) child(conn *sql.DB, parent *Entity, name string) *Entity { + var id string + if err := conn.QueryRow("SELECT entity_id FROM edge WHERE parent_id = ? AND name = ?;", parent.id, name).Scan(&id); err != nil { + return nil + } + return &Entity{id} +} + +func (db *Database) openConn() (*sql.DB, error) { + return sql.Open("sqlite3", db.dbPath) +} + +// Return the id used to reference this entity +func (e *Entity) ID() string { + return e.id +} + +// Return the paths sorted by depth +func (e Entities) Paths() []string { + out := make([]string, len(e)) + var i int + for k := range e { + out[i] = k + i++ + } + sortByDepth(out) + + return out +} diff --git a/gograph/gograph_test.go b/gograph/gograph_test.go new file mode 100644 index 000000000..519b19969 --- /dev/null +++ b/gograph/gograph_test.go @@ -0,0 +1,452 @@ +package gograph + +import ( + "os" + "path" + "strconv" + "testing" +) + +func newTestDb(t *testing.T) *Database { + db, err := NewDatabase(path.Join(os.TempDir(), "sqlite.db")) + if err != nil { + t.Fatal(err) + } + return db +} + +func destroyTestDb(db *Database) { + os.Remove(db.dbPath) +} + +func TestNewDatabase(t *testing.T) { + db := newTestDb(t) + if db == nil { + t.Fatal("Datbase should not be nil") + } + defer destroyTestDb(db) +} + +func TestCreateRootEnity(t *testing.T) { + db := newTestDb(t) + defer destroyTestDb(db) + root := db.RootEntity() + if root == nil { + t.Fatal("Root entity should not be nil") + } +} + +func TestGetRootEntity(t *testing.T) { + db := newTestDb(t) + defer destroyTestDb(db) + + e := db.Get("/") + if e == nil { + t.Fatal("Entity should not be nil") + } + if e.ID() != "0" { + t.Fatalf("Enity id should be 0, got %s", e.ID()) + } +} + +func TestSetEntityWithDifferentName(t *testing.T) { + db := newTestDb(t) + defer destroyTestDb(db) + + db.Set("/test", "1") + if _, err := db.Set("/other", "1"); err != nil { + t.Fatal(err) + } +} + +func TestCreateChild(t *testing.T) { + db := newTestDb(t) + defer destroyTestDb(db) + + child, err := db.Set("/db", "1") + if err != nil { + t.Fatal(err) + } + if child == nil { + t.Fatal("Child should not be nil") + } + if child.ID() != "1" { + t.Fail() + } +} + +func TestListAllRootChildren(t *testing.T) { + db := newTestDb(t) + defer destroyTestDb(db) + + for i := 1; i < 6; i++ { + a := strconv.Itoa(i) + if _, err := db.Set("/"+a, a); err != nil { + t.Fatal(err) + } + } + entries := db.List("/", -1) + if len(entries) != 5 { + t.Fatalf("Expect 5 entries for / got %d", len(entries)) + } +} + +func TestListAllSubChildren(t *testing.T) { + db := newTestDb(t) + defer destroyTestDb(db) + + _, err := db.Set("/webapp", "1") + if err != nil { + t.Fatal(err) + } + child2, err := db.Set("/db", "2") + if err != nil { + t.Fatal(err) + } + child4, err := db.Set("/logs", "4") + if err != nil { + t.Fatal(err) + } + if _, err := db.Set("/db/logs", child4.ID()); err != nil { + t.Fatal(err) + } + + child3, err := db.Set("/sentry", "3") + if err != nil { + t.Fatal(err) + } + if _, err := db.Set("/webapp/sentry", child3.ID()); err != nil { + t.Fatal(err) + } + if _, err := db.Set("/webapp/db", child2.ID()); err != nil { + t.Fatal(err) + } + + entries := db.List("/webapp", 1) + if len(entries) != 3 { + t.Fatalf("Expect 3 entries for / got %d", len(entries)) + } + + entries = db.List("/webapp", 0) + if len(entries) != 2 { + t.Fatalf("Expect 2 entries for / got %d", len(entries)) + } +} + +func TestAddSelfAsChild(t *testing.T) { + db := newTestDb(t) + defer destroyTestDb(db) + + child, err := db.Set("/test", "1") + if err != nil { + t.Fatal(err) + } + if _, err := db.Set("/test/other", child.ID()); err == nil { + t.Fatal("Error should not be nil") + } +} + +func TestAddChildToNonExistantRoot(t *testing.T) { + db := newTestDb(t) + defer destroyTestDb(db) + + if _, err := db.Set("/myapp", "1"); err != nil { + t.Fatal(err) + } + + if _, err := db.Set("/myapp/proxy/db", "2"); err == nil { + t.Fatal("Error should not be nil") + } +} + +func TestWalkAll(t *testing.T) { + db := newTestDb(t) + defer destroyTestDb(db) + _, err := db.Set("/webapp", "1") + if err != nil { + t.Fatal(err) + } + child2, err := db.Set("/db", "2") + if err != nil { + t.Fatal(err) + } + child4, err := db.Set("/db/logs", "4") + if err != nil { + t.Fatal(err) + } + if _, err := db.Set("/webapp/logs", child4.ID()); err != nil { + t.Fatal(err) + } + + child3, err := db.Set("/sentry", "3") + if err != nil { + t.Fatal(err) + } + if _, err := db.Set("/webapp/sentry", child3.ID()); err != nil { + t.Fatal(err) + } + if _, err := db.Set("/webapp/db", child2.ID()); err != nil { + t.Fatal(err) + } + + child5, err := db.Set("/gograph", "5") + if err != nil { + t.Fatal(err) + } + if _, err := db.Set("/webapp/same-ref-diff-name", child5.ID()); err != nil { + t.Fatal(err) + } + + if err := db.Walk("/", func(p string, e *Entity) error { + t.Logf("Path: %s Entity: %s", p, e.ID()) + return nil + }, -1); err != nil { + t.Fatal(err) + } +} + +func TestGetEntityByPath(t *testing.T) { + db := newTestDb(t) + defer destroyTestDb(db) + _, err := db.Set("/webapp", "1") + if err != nil { + t.Fatal(err) + } + child2, err := db.Set("/db", "2") + if err != nil { + t.Fatal(err) + } + child4, err := db.Set("/logs", "4") + if err != nil { + t.Fatal(err) + } + if _, err := db.Set("/db/logs", child4.ID()); err != nil { + t.Fatal(err) + } + + child3, err := db.Set("/sentry", "3") + if err != nil { + t.Fatal(err) + } + if _, err := db.Set("/webapp/sentry", child3.ID()); err != nil { + t.Fatal(err) + } + if _, err := db.Set("/webapp/db", child2.ID()); err != nil { + t.Fatal(err) + } + + child5, err := db.Set("/gograph", "5") + if err != nil { + t.Fatal(err) + } + if _, err := db.Set("/webapp/same-ref-diff-name", child5.ID()); err != nil { + t.Fatal(err) + } + + entity := db.Get("/webapp/db/logs") + if entity == nil { + t.Fatal("Entity should not be nil") + } + if entity.ID() != "4" { + t.Fatalf("Expected to get entity with id 4, got %s", entity.ID()) + } +} + +func TestEnitiesPaths(t *testing.T) { + db := newTestDb(t) + defer destroyTestDb(db) + _, err := db.Set("/webapp", "1") + if err != nil { + t.Fatal(err) + } + child2, err := db.Set("/db", "2") + if err != nil { + t.Fatal(err) + } + child4, err := db.Set("/logs", "4") + if err != nil { + t.Fatal(err) + } + if _, err := db.Set("/db/logs", child4.ID()); err != nil { + t.Fatal(err) + } + + child3, err := db.Set("/sentry", "3") + if err != nil { + t.Fatal(err) + } + if _, err := db.Set("/webapp/sentry", child3.ID()); err != nil { + t.Fatal(err) + } + if _, err := db.Set("/webapp/db", child2.ID()); err != nil { + t.Fatal(err) + } + + child5, err := db.Set("/gograph", "5") + if err != nil { + t.Fatal(err) + } + if _, err := db.Set("/webapp/same-ref-diff-name", child5.ID()); err != nil { + t.Fatal(err) + } + + out := db.List("/", -1) + for _, p := range out.Paths() { + t.Log(p) + } +} + +func TestDeleteRootEntity(t *testing.T) { + db := newTestDb(t) + defer destroyTestDb(db) + + if err := db.Delete("/"); err == nil { + t.Fatal("Error should not be nil") + } +} + +func TestDeleteEntity(t *testing.T) { + db := newTestDb(t) + defer destroyTestDb(db) + _, err := db.Set("/webapp", "1") + if err != nil { + t.Fatal(err) + } + child2, err := db.Set("/db", "2") + if err != nil { + t.Fatal(err) + } + child4, err := db.Set("/logs", "4") + if err != nil { + t.Fatal(err) + } + if _, err := db.Set("/db/logs", child4.ID()); err != nil { + t.Fatal(err) + } + + child3, err := db.Set("/sentry", "3") + if err != nil { + t.Fatal(err) + } + if _, err := db.Set("/webapp/sentry", child3.ID()); err != nil { + t.Fatal(err) + } + if _, err := db.Set("/webapp/db", child2.ID()); err != nil { + t.Fatal(err) + } + + child5, err := db.Set("/gograph", "5") + if err != nil { + t.Fatal(err) + } + if _, err := db.Set("/webapp/same-ref-diff-name", child5.ID()); err != nil { + t.Fatal(err) + } + + if err := db.Delete("/webapp/sentry"); err != nil { + t.Fatal(err) + } + entity := db.Get("/webapp/sentry") + if entity != nil { + t.Fatal("Entity /webapp/sentry should be nil") + } +} + +func TestCountRefs(t *testing.T) { + db := newTestDb(t) + defer destroyTestDb(db) + + db.Set("/webapp", "1") + + if db.Refs("1") != 1 { + t.Fatal("Expect reference count to be 1") + } + + db.Set("/db", "2") + db.Set("/webapp/db", "2") + if db.Refs("2") != 2 { + t.Fatal("Expect reference count to be 2") + } +} + +func TestPurgeId(t *testing.T) { + db := newTestDb(t) + defer destroyTestDb(db) + + db.Set("/webapp", "1") + + if db.Refs("1") != 1 { + t.Fatal("Expect reference count to be 1") + } + + db.Set("/db", "2") + db.Set("/webapp/db", "2") + + count, err := db.Purge("2") + if err != nil { + t.Fatal(err) + } + if count != 2 { + t.Fatal("Expected 2 references to be removed") + } +} + +func TestRename(t *testing.T) { + db := newTestDb(t) + defer destroyTestDb(db) + + db.Set("/webapp", "1") + + if db.Refs("1") != 1 { + t.Fatal("Expect reference count to be 1") + } + + db.Set("/db", "2") + db.Set("/webapp/db", "2") + + if db.Get("/webapp/db") == nil { + t.Fatal("Cannot find entity at path /webapp/db") + } + + if err := db.Rename("/webapp/db", "/webapp/newdb"); err != nil { + t.Fatal(err) + } + if db.Get("/webapp/db") != nil { + t.Fatal("Entity should not exist at /webapp/db") + } + if db.Get("/webapp/newdb") == nil { + t.Fatal("Cannot find entity at path /webapp/newdb") + } + +} + +func TestCreateMultipleNames(t *testing.T) { + db := newTestDb(t) + defer destroyTestDb(db) + + db.Set("/db", "1") + if _, err := db.Set("/myapp", "1"); err != nil { + t.Fatal(err) + } + + db.Walk("/", func(p string, e *Entity) error { + t.Logf("%s\n", p) + return nil + }, -1) +} + +func TestRefPaths(t *testing.T) { + db := newTestDb(t) + defer destroyTestDb(db) + + db.Set("/webapp", "1") + + db.Set("/db", "2") + db.Set("/webapp/db", "2") + + refs := db.RefPaths("2") + if len(refs) != 2 { + t.Fatalf("Expected reference count to be 2, got %d", len(refs)) + } + +} diff --git a/gograph/sort.go b/gograph/sort.go new file mode 100644 index 000000000..cc936cb84 --- /dev/null +++ b/gograph/sort.go @@ -0,0 +1,27 @@ +package gograph + +import "sort" + +type pathSorter struct { + paths []string + by func(i, j string) bool +} + +func sortByDepth(paths []string) { + s := &pathSorter{paths, func(i, j string) bool { + return pathDepth(i) > pathDepth(j) + }} + sort.Sort(s) +} + +func (s *pathSorter) Len() int { + return len(s.paths) +} + +func (s *pathSorter) Swap(i, j int) { + s.paths[i], s.paths[j] = s.paths[j], s.paths[i] +} + +func (s *pathSorter) Less(i, j int) bool { + return s.by(s.paths[i], s.paths[j]) +} diff --git a/gograph/sort_test.go b/gograph/sort_test.go new file mode 100644 index 000000000..40431039a --- /dev/null +++ b/gograph/sort_test.go @@ -0,0 +1,29 @@ +package gograph + +import ( + "testing" +) + +func TestSort(t *testing.T) { + paths := []string{ + "/", + "/myreallylongname", + "/app/db", + } + + sortByDepth(paths) + + if len(paths) != 3 { + t.Fatalf("Expected 3 parts got %d", len(paths)) + } + + if paths[0] != "/app/db" { + t.Fatalf("Expected /app/db got %s", paths[0]) + } + if paths[1] != "/myreallylongname" { + t.Fatalf("Expected /myreallylongname got %s", paths[1]) + } + if paths[2] != "/" { + t.Fatalf("Expected / got %s", paths[2]) + } +} diff --git a/gograph/utils.go b/gograph/utils.go new file mode 100644 index 000000000..c20dd124a --- /dev/null +++ b/gograph/utils.go @@ -0,0 +1,32 @@ +package gograph + +import ( + "path" + "strings" +) + +// Split p on / +func split(p string) []string { + return strings.Split(p, "/") +} + +// Returns the depth or number of / in a given path +func pathDepth(p string) int { + parts := split(p) + if len(parts) == 2 && parts[1] == "" { + return 1 + } + return len(parts) +} + +func splitPath(p string) (parent, name string) { + if p[0] != '/' { + p = "/" + p + } + parent, name = path.Split(p) + l := len(parent) + if parent[l-1] == '/' { + parent = parent[:l-1] + } + return +} diff --git a/hack/make.sh b/hack/make.sh index 98b62ea6a..cac4534ea 100755 --- a/hack/make.sh +++ b/hack/make.sh @@ -44,7 +44,8 @@ if [ -n "$(git status --porcelain)" ]; then fi # Use these flags when compiling the tests and final binary -LDFLAGS="-X main.GITCOMMIT $GITCOMMIT -X main.VERSION $VERSION -d -w" +LDFLAGS='-X main.GITCOMMIT "'$GITCOMMIT'" -X main.VERSION "'$VERSION'" -w -linkmode external -extldflags "-lpthread -static -Wl,--unresolved-symbols=ignore-in-object-files"' +BUILDFLAGS='-tags netgo' bundle() { diff --git a/hack/make/binary b/hack/make/binary index cff9f5c73..4c2c24897 100644 --- a/hack/make/binary +++ b/hack/make/binary @@ -2,6 +2,6 @@ DEST=$1 -if go build -o $DEST/docker-$VERSION -ldflags "$LDFLAGS" ./docker; then - echo "Created binary: $DEST/docker-$VERSION" -fi +go build -o $DEST/docker-$VERSION -ldflags "$LDFLAGS" $BUILDFLAGS ./docker + +echo "Created binary: $DEST/docker-$VERSION" diff --git a/hack/make/test b/hack/make/test index c09727702..4f166cccd 100644 --- a/hack/make/test +++ b/hack/make/test @@ -14,7 +14,7 @@ bundle_test() { for test_dir in $(find_test_dirs); do ( set -x cd $test_dir - go test -v -ldflags "$LDFLAGS" $TESTFLAGS + go test -v -ldflags "$LDFLAGS" $BUILDFLAGS ) done } 2>&1 | tee $DEST/test.log } diff --git a/iptables/MAINTAINERS b/iptables/MAINTAINERS new file mode 100644 index 000000000..1e998f8ac --- /dev/null +++ b/iptables/MAINTAINERS @@ -0,0 +1 @@ +Michael Crosby (@crosbymichael) diff --git a/iptables/iptables.go b/iptables/iptables.go new file mode 100644 index 000000000..5974d4d9c --- /dev/null +++ b/iptables/iptables.go @@ -0,0 +1,105 @@ +package iptables + +import ( + "errors" + "fmt" + "net" + "os/exec" + "strconv" + "strings" +) + +type Action string + +const ( + Add Action = "-A" + Delete Action = "-D" +) + +var ( + ErrIptablesNotFound = errors.New("Iptables not found") + nat = []string{"-t", "nat"} +) + +type Chain struct { + Name string + Bridge string +} + +func NewChain(name, bridge string) (*Chain, error) { + if err := Raw("-t", "nat", "-N", name); err != nil { + return nil, err + } + chain := &Chain{ + Name: name, + Bridge: bridge, + } + + if err := chain.Prerouting(Add, "-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 { + return nil, fmt.Errorf("Failed to inject docker in OUTPUT chain: %s", err) + } + return chain, nil +} + +func RemoveExistingChain(name string) error { + chain := &Chain{ + Name: name, + } + return chain.Remove() +} + +func (c *Chain) Forward(action Action, ip net.IP, port int, proto, dest_addr string, dest_port int) error { + return Raw("-t", "nat", fmt.Sprint(action), c.Name, + "-p", proto, + "-d", ip.String(), + "--dport", strconv.Itoa(port), + "!", "-i", c.Bridge, + "-j", "DNAT", + "--to-destination", net.JoinHostPort(dest_addr, strconv.Itoa(dest_port))) +} + +func (c *Chain) Prerouting(action Action, args ...string) error { + a := append(nat, fmt.Sprint(action), "PREROUTING") + if len(args) > 0 { + a = append(a, args...) + } + return Raw(append(a, "-j", c.Name)...) +} + +func (c *Chain) Output(action Action, args ...string) error { + a := append(nat, fmt.Sprint(action), "OUTPUT") + if len(args) > 0 { + a = append(a, args...) + } + return Raw(append(a, "-j", c.Name)...) +} + +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 + + c.Prerouting(Delete) + c.Output(Delete) + + Raw("-t", "nat", "-F", c.Name) + Raw("-t", "nat", "-X", c.Name) + + return nil +} + +func Raw(args ...string) error { + path, err := exec.LookPath("iptables") + if err != nil { + return ErrIptablesNotFound + } + if err := exec.Command(path, args...).Run(); err != nil { + return fmt.Errorf("iptables failed: iptables %v", strings.Join(args, " ")) + } + return nil + +} diff --git a/iptables/iptables_test.go b/iptables/iptables_test.go new file mode 100644 index 000000000..aad8acdb8 --- /dev/null +++ b/iptables/iptables_test.go @@ -0,0 +1,18 @@ +package iptables + +import ( + "os" + "testing" +) + +func TestIptables(t *testing.T) { + if err := Raw("-L"); err != nil { + t.Fatal(err) + } + path := os.Getenv("PATH") + os.Setenv("PATH", "") + defer os.Setenv("PATH", path) + if err := Raw("-L"); err == nil { + t.Fatal("Not finding iptables in the PATH should cause an error") + } +} diff --git a/links.go b/links.go new file mode 100644 index 000000000..f1087ec34 --- /dev/null +++ b/links.go @@ -0,0 +1,141 @@ +package docker + +import ( + "fmt" + "github.com/dotcloud/docker/iptables" + "path" + "strings" +) + +type Link struct { + ParentIP string + ChildIP string + Name string + BridgeInterface string + ChildEnvironment []string + Ports []Port + IsEnabled bool +} + +func NewLink(parent, child *Container, name, bridgeInterface string) (*Link, error) { + if parent.ID == child.ID { + return nil, fmt.Errorf("Cannot link to self: %s == %s", parent.ID, child.ID) + } + if !child.State.Running { + return nil, fmt.Errorf("Cannot link to a non running container: %s AS %s", child.ID, name) + } + + ports := make([]Port, len(child.Config.ExposedPorts)) + var i int + for p := range child.Config.ExposedPorts { + ports[i] = p + i++ + } + + l := &Link{ + BridgeInterface: bridgeInterface, + Name: name, + ChildIP: child.NetworkSettings.IPAddress, + ParentIP: parent.NetworkSettings.IPAddress, + ChildEnvironment: child.Config.Env, + Ports: ports, + } + return l, nil + +} + +func (l *Link) Alias() string { + _, alias := path.Split(l.Name) + return alias +} + +func (l *Link) ToEnv() []string { + env := []string{} + alias := strings.ToUpper(l.Alias()) + + if p := l.getDefaultPort(); p != nil { + env = append(env, fmt.Sprintf("%s_PORT=%s://%s:%s", alias, p.Proto(), l.ChildIP, p.Port())) + } + + // Load exposed ports into the environment + 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())) + } + + // Load the linked container's name into the environment + env = append(env, fmt.Sprintf("%s_NAME=%s", alias, l.Name)) + + if l.ChildEnvironment != nil { + for _, v := range l.ChildEnvironment { + parts := strings.Split(v, "=") + if len(parts) != 2 { + continue + } + // Ignore a few variables that are added during docker build + if parts[0] == "HOME" || parts[0] == "PATH" { + continue + } + env = append(env, fmt.Sprintf("%s_ENV_%s=%s", alias, parts[0], parts[1])) + } + } + return env +} + +// Default port rules +func (l *Link) getDefaultPort() *Port { + var p Port + i := len(l.Ports) + + if i == 0 { + return nil + } else if i > 1 { + sortPorts(l.Ports, func(ip, jp Port) bool { + // If the two ports have the same number, tcp takes priority + // Sort in desc order + return ip.Int() < jp.Int() || (ip.Int() == jp.Int() && strings.ToLower(ip.Proto()) == "tcp") + }) + } + p = l.Ports[0] + return &p +} + +func (l *Link) Enable() error { + if err := l.toggle("-I", false); err != nil { + return err + } + l.IsEnabled = true + return nil +} + +func (l *Link) Disable() { + // We do not care about errors here because the link may not + // exist in iptables + l.toggle("-D", true) + + l.IsEnabled = false +} + +func (l *Link) toggle(action string, ignoreErrors bool) error { + for _, p := range l.Ports { + if err := iptables.Raw(action, "FORWARD", + "-i", l.BridgeInterface, "-o", l.BridgeInterface, + "-p", p.Proto(), + "-s", l.ParentIP, + "--dport", p.Port(), + "-d", l.ChildIP, + "-j", "ACCEPT"); !ignoreErrors && err != nil { + return err + } + + if err := iptables.Raw(action, "FORWARD", + "-i", l.BridgeInterface, "-o", l.BridgeInterface, + "-p", p.Proto(), + "-s", l.ChildIP, + "--sport", p.Port(), + "-d", l.ParentIP, + "-j", "ACCEPT"); !ignoreErrors && err != nil { + return err + } + } + return nil +} diff --git a/links_test.go b/links_test.go new file mode 100644 index 000000000..64608e15c --- /dev/null +++ b/links_test.go @@ -0,0 +1,104 @@ +package docker + +import ( + "strings" + "testing" +) + +func newMockLinkContainer(id string, ip string) *Container { + return &Container{ + Config: &Config{}, + ID: id, + NetworkSettings: &NetworkSettings{ + IPAddress: ip, + }, + } +} + +func TestLinkNew(t *testing.T) { + toID := GenerateID() + fromID := GenerateID() + + from := newMockLinkContainer(fromID, "172.0.17.2") + from.Config.Env = []string{} + from.State = State{Running: true} + ports := make(map[Port]struct{}) + + ports[Port("6379/tcp")] = struct{}{} + + from.Config.ExposedPorts = ports + + to := newMockLinkContainer(toID, "172.0.17.3") + + link, err := NewLink(to, from, "/db/docker", "172.0.17.1") + if err != nil { + t.Fatal(err) + } + + if link == nil { + t.FailNow() + } + if link.Name != "/db/docker" { + t.Fail() + } + if link.Alias() != "docker" { + t.Fail() + } + if link.ParentIP != "172.0.17.3" { + t.Fail() + } + if link.ChildIP != "172.0.17.2" { + t.Fail() + } + if link.BridgeInterface != "172.0.17.1" { + t.Fail() + } + for _, p := range link.Ports { + if p != Port("6379/tcp") { + t.Fail() + } + } +} + +func TestLinkEnv(t *testing.T) { + toID := GenerateID() + fromID := GenerateID() + + from := newMockLinkContainer(fromID, "172.0.17.2") + from.Config.Env = []string{"PASSWORD=gordon"} + from.State = State{Running: true} + ports := make(map[Port]struct{}) + + ports[Port("6379/tcp")] = struct{}{} + + from.Config.ExposedPorts = ports + + to := newMockLinkContainer(toID, "172.0.17.3") + + link, err := NewLink(to, from, "/db/docker", "172.0.17.1") + 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 tcp://172.0.17.2:6379, got %s", env["DOCKER_PORT"]) + } + if env["DOCKER_PORT_6379_TCP"] != "tcp://172.0.17.2:6379" { + t.Fatalf("Expected tcp://172.0.17.2:6379, got %s", env["DOCKER_PORT_6379_TCP"]) + } + 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"]) + } +} diff --git a/network.go b/network.go index 59d82577d..b480dc144 100644 --- a/network.go +++ b/network.go @@ -4,6 +4,8 @@ import ( "encoding/binary" "errors" "fmt" + "github.com/dotcloud/docker/iptables" + "github.com/dotcloud/docker/proxy" "github.com/dotcloud/docker/utils" "log" "net" @@ -13,8 +15,6 @@ import ( "sync" ) -var NetworkBridgeIface string - const ( DefaultNetworkBridge = "docker0" DisableNetworkBridge = "none" @@ -81,18 +81,6 @@ func ip(args ...string) (string, error) { return string(output), nil } -// Wrapper around the iptables command -func iptables(args ...string) error { - path, err := exec.LookPath("iptables") - if err != nil { - return fmt.Errorf("command not found: iptables") - } - if err := exec.Command(path, args...).Run(); err != nil { - return fmt.Errorf("iptables failed: iptables %v", strings.Join(args, " ")) - } - return nil -} - func checkRouteOverlaps(routes string, dockerNetwork *net.IPNet) error { utils.Debugf("Routes:\n\n%s", routes) for _, line := range strings.Split(routes, "\n") { @@ -124,7 +112,7 @@ func checkRouteOverlaps(routes string, dockerNetwork *net.IPNet) error { // CreateBridgeIface creates a network bridge interface on the host system with the name `ifaceName`, // and attempts to configure it with an address which doesn't conflict with any other interface on the host. // If it can't find an address which doesn't conflict, it will return an error. -func CreateBridgeIface(ifaceName string) error { +func CreateBridgeIface(config *DaemonConfig) error { addrs := []string{ // Here we don't follow the convention of using the 1st IP of the range for the gateway. // This is to use the same gateway IPs as the /24 ranges, which predate the /16 ranges. @@ -163,23 +151,29 @@ func CreateBridgeIface(ifaceName string) error { } } if ifaceAddr == "" { - return fmt.Errorf("Could not find a free IP address range for interface '%s'. Please configure its address manually and run 'docker -b %s'", ifaceName, ifaceName) + return fmt.Errorf("Could not find a free IP address range for interface '%s'. Please configure its address manually and run 'docker -b %s'", config.BridgeIface, config.BridgeIface) } - utils.Debugf("Creating bridge %s with network %s", ifaceName, ifaceAddr) + utils.Debugf("Creating bridge %s with network %s", config.BridgeIface, ifaceAddr) - if output, err := ip("link", "add", ifaceName, "type", "bridge"); err != nil { + if output, err := ip("link", "add", config.BridgeIface, "type", "bridge"); err != nil { return fmt.Errorf("Error creating bridge: %s (output: %s)", err, output) } - if output, err := ip("addr", "add", ifaceAddr, "dev", ifaceName); err != nil { + if output, err := ip("addr", "add", ifaceAddr, "dev", config.BridgeIface); err != nil { return fmt.Errorf("Unable to add private network: %s (%s)", err, output) } - if output, err := ip("link", "set", ifaceName, "up"); err != nil { + if output, err := ip("link", "set", config.BridgeIface, "up"); err != nil { return fmt.Errorf("Unable to start network bridge: %s (%s)", err, output) } - if err := iptables("-t", "nat", "-A", "POSTROUTING", "-s", ifaceAddr, - "!", "-d", ifaceAddr, "-j", "MASQUERADE"); err != nil { - return fmt.Errorf("Unable to enable network bridge NAT: %s", err) + if config.EnableIptables { + if err := iptables.Raw("-t", "nat", "-A", "POSTROUTING", "-s", ifaceAddr, + "!", "-d", ifaceAddr, "-j", "MASQUERADE"); err != nil { + return fmt.Errorf("Unable to enable network bridge NAT: %s", err) + } + // Prevent inter-container communication by default + if err := iptables.Raw("-A", "FORWARD", "-i", config.BridgeIface, "-o", config.BridgeIface, "-j", "DROP"); err != nil { + return fmt.Errorf("Unable to prevent intercontainer communication: %s", err) + } } return nil } @@ -216,58 +210,27 @@ func getIfaceAddr(name string) (net.Addr, error) { // It keeps track of all mappings and is able to unmap at will type PortMapper struct { tcpMapping map[int]*net.TCPAddr - tcpProxies map[int]Proxy + tcpProxies map[int]proxy.Proxy udpMapping map[int]*net.UDPAddr - udpProxies map[int]Proxy + udpProxies map[int]proxy.Proxy + + iptables *iptables.Chain + defaultIp net.IP } -func (mapper *PortMapper) cleanup() error { - // Ignore errors - This could mean the chains were never set up - iptables("-t", "nat", "-D", "PREROUTING", "-m", "addrtype", "--dst-type", "LOCAL", "-j", "DOCKER") - iptables("-t", "nat", "-D", "OUTPUT", "-m", "addrtype", "--dst-type", "LOCAL", "!", "--dst", "127.0.0.0/8", "-j", "DOCKER") - iptables("-t", "nat", "-D", "OUTPUT", "-m", "addrtype", "--dst-type", "LOCAL", "-j", "DOCKER") // Created in versions <= 0.1.6 - // Also cleanup rules created by older versions, or -X might fail. - iptables("-t", "nat", "-D", "PREROUTING", "-j", "DOCKER") - iptables("-t", "nat", "-D", "OUTPUT", "-j", "DOCKER") - iptables("-t", "nat", "-F", "DOCKER") - iptables("-t", "nat", "-X", "DOCKER") - mapper.tcpMapping = make(map[int]*net.TCPAddr) - mapper.tcpProxies = make(map[int]Proxy) - mapper.udpMapping = make(map[int]*net.UDPAddr) - mapper.udpProxies = make(map[int]Proxy) - return nil -} - -func (mapper *PortMapper) setup() error { - if err := iptables("-t", "nat", "-N", "DOCKER"); err != nil { - return fmt.Errorf("Failed to create DOCKER chain: %s", err) - } - if err := iptables("-t", "nat", "-A", "PREROUTING", "-m", "addrtype", "--dst-type", "LOCAL", "-j", "DOCKER"); err != nil { - return fmt.Errorf("Failed to inject docker in PREROUTING chain: %s", err) - } - if err := iptables("-t", "nat", "-A", "OUTPUT", "-m", "addrtype", "--dst-type", "LOCAL", "!", "--dst", "127.0.0.0/8", "-j", "DOCKER"); err != nil { - return fmt.Errorf("Failed to inject docker in OUTPUT chain: %s", err) - } - return nil -} - -func (mapper *PortMapper) iptablesForward(rule string, port int, proto string, dest_addr string, dest_port int) error { - return iptables("-t", "nat", rule, "DOCKER", "-p", proto, "--dport", strconv.Itoa(port), - "!", "-i", NetworkBridgeIface, - "-j", "DNAT", "--to-destination", net.JoinHostPort(dest_addr, strconv.Itoa(dest_port))) -} - -func (mapper *PortMapper) Map(port int, backendAddr net.Addr) error { +func (mapper *PortMapper) Map(ip net.IP, port int, backendAddr net.Addr) error { if _, isTCP := backendAddr.(*net.TCPAddr); isTCP { backendPort := backendAddr.(*net.TCPAddr).Port backendIP := backendAddr.(*net.TCPAddr).IP - if err := mapper.iptablesForward("-A", port, "tcp", backendIP.String(), backendPort); err != nil { - return err + if mapper.iptables != nil { + if err := mapper.iptables.Forward(iptables.Add, ip, port, "tcp", backendIP.String(), backendPort); err != nil { + return err + } } mapper.tcpMapping[port] = backendAddr.(*net.TCPAddr) - proxy, err := NewProxy(&net.TCPAddr{IP: net.IPv4(0, 0, 0, 0), Port: port}, backendAddr) + proxy, err := proxy.NewProxy(&net.TCPAddr{IP: ip, Port: port}, backendAddr) if err != nil { - mapper.Unmap(port, "tcp") + mapper.Unmap(ip, port, "tcp") return err } mapper.tcpProxies[port] = proxy @@ -275,13 +238,15 @@ func (mapper *PortMapper) Map(port int, backendAddr net.Addr) error { } else { backendPort := backendAddr.(*net.UDPAddr).Port backendIP := backendAddr.(*net.UDPAddr).IP - if err := mapper.iptablesForward("-A", port, "udp", backendIP.String(), backendPort); err != nil { - return err + if mapper.iptables != nil { + if err := mapper.iptables.Forward(iptables.Add, ip, port, "udp", backendIP.String(), backendPort); err != nil { + return err + } } mapper.udpMapping[port] = backendAddr.(*net.UDPAddr) - proxy, err := NewProxy(&net.UDPAddr{IP: net.IPv4(0, 0, 0, 0), Port: port}, backendAddr) + proxy, err := proxy.NewProxy(&net.UDPAddr{IP: ip, Port: port}, backendAddr) if err != nil { - mapper.Unmap(port, "udp") + mapper.Unmap(ip, port, "udp") return err } mapper.udpProxies[port] = proxy @@ -290,7 +255,7 @@ func (mapper *PortMapper) Map(port int, backendAddr net.Addr) error { return nil } -func (mapper *PortMapper) Unmap(port int, proto string) error { +func (mapper *PortMapper) Unmap(ip net.IP, port int, proto string) error { if proto == "tcp" { backendAddr, ok := mapper.tcpMapping[port] if !ok { @@ -300,8 +265,10 @@ func (mapper *PortMapper) Unmap(port int, proto string) error { proxy.Close() delete(mapper.tcpProxies, port) } - if err := mapper.iptablesForward("-D", port, proto, backendAddr.IP.String(), backendAddr.Port); err != nil { - return err + if mapper.iptables != nil { + if err := mapper.iptables.Forward(iptables.Delete, ip, port, proto, backendAddr.IP.String(), backendAddr.Port); err != nil { + return err + } } delete(mapper.tcpMapping, port) } else { @@ -313,21 +280,37 @@ func (mapper *PortMapper) Unmap(port int, proto string) error { proxy.Close() delete(mapper.udpProxies, port) } - if err := mapper.iptablesForward("-D", port, proto, backendAddr.IP.String(), backendAddr.Port); err != nil { - return err + if mapper.iptables != nil { + if err := mapper.iptables.Forward(iptables.Delete, ip, port, proto, backendAddr.IP.String(), backendAddr.Port); err != nil { + return err + } } delete(mapper.udpMapping, port) } return nil } -func newPortMapper() (*PortMapper, error) { - mapper := &PortMapper{} - if err := mapper.cleanup(); err != nil { +func newPortMapper(config *DaemonConfig) (*PortMapper, error) { + // We can always try removing the iptables + if err := iptables.RemoveExistingChain("DOCKER"); err != nil { return nil, err } - if err := mapper.setup(); err != nil { - return nil, err + var chain *iptables.Chain + if config.EnableIptables { + var err error + chain, err = iptables.NewChain("DOCKER", config.BridgeIface) + if err != nil { + return nil, fmt.Errorf("Failed to create DOCKER chain: %s", err) + } + } + + mapper := &PortMapper{ + tcpMapping: make(map[int]*net.TCPAddr), + tcpProxies: make(map[int]proxy.Proxy), + udpMapping: make(map[int]*net.UDPAddr), + udpProxies: make(map[int]proxy.Proxy), + iptables: chain, + defaultIp: config.DefaultIp, } return mapper, nil } @@ -519,40 +502,56 @@ type NetworkInterface struct { disabled bool } -// Allocate an external TCP port and map it to the interface -func (iface *NetworkInterface) AllocatePort(spec string) (*Nat, error) { +// Allocate an external port and map it to the interface +func (iface *NetworkInterface) AllocatePort(port Port, binding PortBinding) (*Nat, error) { if iface.disabled { return nil, fmt.Errorf("Trying to allocate port for interface %v, which is disabled", iface) // FIXME } - nat, err := parseNat(spec) + ip := iface.manager.portMapper.defaultIp + + if binding.HostIp != "" { + ip = net.ParseIP(binding.HostIp) + } else { + binding.HostIp = ip.String() + } + + nat := &Nat{ + Port: port, + Binding: binding, + } + + containerPort, err := parsePort(port.Port()) if err != nil { return nil, err } - if nat.Proto == "tcp" { - extPort, err := iface.manager.tcpPortAllocator.Acquire(nat.Frontend) + hostPort, _ := parsePort(nat.Binding.HostPort) + + if nat.Port.Proto() == "tcp" { + extPort, err := iface.manager.tcpPortAllocator.Acquire(hostPort) if err != nil { return nil, err } - backend := &net.TCPAddr{IP: iface.IPNet.IP, Port: nat.Backend} - if err := iface.manager.portMapper.Map(extPort, backend); err != nil { + + backend := &net.TCPAddr{IP: iface.IPNet.IP, Port: containerPort} + if err := iface.manager.portMapper.Map(ip, extPort, backend); err != nil { iface.manager.tcpPortAllocator.Release(extPort) return nil, err } - nat.Frontend = extPort + nat.Binding.HostPort = strconv.Itoa(extPort) } else { - extPort, err := iface.manager.udpPortAllocator.Acquire(nat.Frontend) + extPort, err := iface.manager.udpPortAllocator.Acquire(hostPort) if err != nil { return nil, err } - backend := &net.UDPAddr{IP: iface.IPNet.IP, Port: nat.Backend} - if err := iface.manager.portMapper.Map(extPort, backend); err != nil { + backend := &net.UDPAddr{IP: iface.IPNet.IP, Port: containerPort} + if err := iface.manager.portMapper.Map(ip, extPort, backend); err != nil { iface.manager.udpPortAllocator.Release(extPort) return nil, err } - nat.Frontend = extPort + nat.Binding.HostPort = strconv.Itoa(extPort) } iface.extPorts = append(iface.extPorts, nat) @@ -560,83 +559,37 @@ func (iface *NetworkInterface) AllocatePort(spec string) (*Nat, error) { } type Nat struct { - Proto string - Frontend int - Backend int + Port Port + Binding PortBinding } -func parseNat(spec string) (*Nat, error) { - var nat Nat - - if strings.Contains(spec, "/") { - specParts := strings.Split(spec, "/") - if len(specParts) != 2 { - return nil, fmt.Errorf("Invalid port format.") - } - proto := specParts[1] - spec = specParts[0] - if proto != "tcp" && proto != "udp" { - return nil, fmt.Errorf("Invalid port format: unknown protocol %v.", proto) - } - nat.Proto = proto - } else { - nat.Proto = "tcp" - } - - if strings.Contains(spec, ":") { - specParts := strings.Split(spec, ":") - if len(specParts) != 2 { - return nil, fmt.Errorf("Invalid port format.") - } - // If spec starts with ':', external and internal ports must be the same. - // This might fail if the requested external port is not available. - var sameFrontend bool - if len(specParts[0]) == 0 { - sameFrontend = true - } else { - front, err := strconv.ParseUint(specParts[0], 10, 16) - if err != nil { - return nil, err - } - nat.Frontend = int(front) - } - back, err := strconv.ParseUint(specParts[1], 10, 16) - if err != nil { - return nil, err - } - nat.Backend = int(back) - if sameFrontend { - nat.Frontend = nat.Backend - } - } else { - port, err := strconv.ParseUint(spec, 10, 16) - if err != nil { - return nil, err - } - nat.Backend = int(port) - } - - return &nat, nil +func (n *Nat) String() string { + return fmt.Sprintf("%s:%d:%d/%s", n.Binding.HostIp, n.Binding.HostPort, n.Port.Port(), n.Port.Proto()) } // Release: Network cleanup - release all resources func (iface *NetworkInterface) Release() { - if iface.disabled { return } for _, nat := range iface.extPorts { - utils.Debugf("Unmaping %v/%v", nat.Proto, nat.Frontend) - if err := iface.manager.portMapper.Unmap(nat.Frontend, nat.Proto); err != nil { - log.Printf("Unable to unmap port %v/%v: %v", nat.Proto, nat.Frontend, err) + hostPort, err := parsePort(nat.Binding.HostPort) + if err != nil { + log.Printf("Unable to get host port: %s", err) + continue } - if nat.Proto == "tcp" { - if err := iface.manager.tcpPortAllocator.Release(nat.Frontend); err != nil { - log.Printf("Unable to release port tcp/%v: %v", nat.Frontend, err) + ip := net.ParseIP(nat.Binding.HostIp) + utils.Debugf("Unmaping %s/%s", nat.Port.Proto, nat.Binding.HostPort) + if err := iface.manager.portMapper.Unmap(ip, hostPort, nat.Port.Proto()); err != nil { + log.Printf("Unable to unmap port %s: %s", nat, err) + } + if nat.Port.Proto() == "tcp" { + if err := iface.manager.tcpPortAllocator.Release(hostPort); err != nil { + log.Printf("Unable to release port %s", nat) } - } else if err := iface.manager.udpPortAllocator.Release(nat.Frontend); err != nil { - log.Printf("Unable to release port udp/%v: %v", nat.Frontend, err) + } else if err := iface.manager.udpPortAllocator.Release(hostPort); err != nil { + log.Printf("Unable to release port %s: %s", nat, err) } } @@ -704,22 +657,21 @@ func (manager *NetworkManager) Close() error { return err3 } -func newNetworkManager(bridgeIface string) (*NetworkManager, error) { - - if bridgeIface == DisableNetworkBridge { +func newNetworkManager(config *DaemonConfig) (*NetworkManager, error) { + if config.BridgeIface == DisableNetworkBridge { manager := &NetworkManager{ disabled: true, } return manager, nil } - addr, err := getIfaceAddr(bridgeIface) + addr, err := getIfaceAddr(config.BridgeIface) if err != nil { // If the iface is not found, try to create it - if err := CreateBridgeIface(bridgeIface); err != nil { + if err := CreateBridgeIface(config); err != nil { return nil, err } - addr, err = getIfaceAddr(bridgeIface) + addr, err = getIfaceAddr(config.BridgeIface) if err != nil { return nil, err } @@ -737,13 +689,13 @@ func newNetworkManager(bridgeIface string) (*NetworkManager, error) { return nil, err } - portMapper, err := newPortMapper() + portMapper, err := newPortMapper(config) if err != nil { return nil, err } manager := &NetworkManager{ - bridgeIface: bridgeIface, + bridgeIface: config.BridgeIface, bridgeNetwork: network, ipAllocator: ipAllocator, tcpPortAllocator: tcpPortAllocator, diff --git a/network_test.go b/network_test.go index bd3a16a1b..a0492b490 100644 --- a/network_test.go +++ b/network_test.go @@ -2,117 +2,9 @@ package docker import ( "net" - "os" "testing" ) -func TestIptables(t *testing.T) { - if err := iptables("-L"); err != nil { - t.Fatal(err) - } - path := os.Getenv("PATH") - os.Setenv("PATH", "") - defer os.Setenv("PATH", path) - if err := iptables("-L"); err == nil { - t.Fatal("Not finding iptables in the PATH should cause an error") - } -} - -func TestParseNat(t *testing.T) { - if nat, err := parseNat("4500"); err == nil { - if nat.Frontend != 0 || nat.Backend != 4500 || nat.Proto != "tcp" { - t.Errorf("-p 4500 should produce 0->4500/tcp, got %d->%d/%s", - nat.Frontend, nat.Backend, nat.Proto) - } - } else { - t.Fatal(err) - } - - if nat, err := parseNat(":4501"); err == nil { - if nat.Frontend != 4501 || nat.Backend != 4501 || nat.Proto != "tcp" { - t.Errorf("-p :4501 should produce 4501->4501/tcp, got %d->%d/%s", - nat.Frontend, nat.Backend, nat.Proto) - } - } else { - t.Fatal(err) - } - - if nat, err := parseNat("4502:4503"); err == nil { - if nat.Frontend != 4502 || nat.Backend != 4503 || nat.Proto != "tcp" { - t.Errorf("-p 4502:4503 should produce 4502->4503/tcp, got %d->%d/%s", - nat.Frontend, nat.Backend, nat.Proto) - } - } else { - t.Fatal(err) - } - - if nat, err := parseNat("4502:4503/tcp"); err == nil { - if nat.Frontend != 4502 || nat.Backend != 4503 || nat.Proto != "tcp" { - t.Errorf("-p 4502:4503/tcp should produce 4502->4503/tcp, got %d->%d/%s", - nat.Frontend, nat.Backend, nat.Proto) - } - } else { - t.Fatal(err) - } - - if nat, err := parseNat("4502:4503/udp"); err == nil { - if nat.Frontend != 4502 || nat.Backend != 4503 || nat.Proto != "udp" { - t.Errorf("-p 4502:4503/udp should produce 4502->4503/udp, got %d->%d/%s", - nat.Frontend, nat.Backend, nat.Proto) - } - } else { - t.Fatal(err) - } - - if nat, err := parseNat(":4503/udp"); err == nil { - if nat.Frontend != 4503 || nat.Backend != 4503 || nat.Proto != "udp" { - t.Errorf("-p :4503/udp should produce 4503->4503/udp, got %d->%d/%s", - nat.Frontend, nat.Backend, nat.Proto) - } - } else { - t.Fatal(err) - } - - if nat, err := parseNat(":4503/tcp"); err == nil { - if nat.Frontend != 4503 || nat.Backend != 4503 || nat.Proto != "tcp" { - t.Errorf("-p :4503/tcp should produce 4503->4503/tcp, got %d->%d/%s", - nat.Frontend, nat.Backend, nat.Proto) - } - } else { - t.Fatal(err) - } - - if nat, err := parseNat("4503/tcp"); err == nil { - if nat.Frontend != 0 || nat.Backend != 4503 || nat.Proto != "tcp" { - t.Errorf("-p 4503/tcp should produce 0->4503/tcp, got %d->%d/%s", - nat.Frontend, nat.Backend, nat.Proto) - } - } else { - t.Fatal(err) - } - - if nat, err := parseNat("4503/udp"); err == nil { - if nat.Frontend != 0 || nat.Backend != 4503 || nat.Proto != "udp" { - t.Errorf("-p 4503/udp should produce 0->4503/udp, got %d->%d/%s", - nat.Frontend, nat.Backend, nat.Proto) - } - } else { - t.Fatal(err) - } - - if _, err := parseNat("4503/tcpgarbage"); err == nil { - t.Fatal(err) - } - - if _, err := parseNat("4503/tcp/udp"); err == nil { - t.Fatal(err) - } - - if _, err := parseNat("4503/"); err == nil { - t.Fatal(err) - } -} - func TestPortAllocation(t *testing.T) { allocator, err := newPortAllocator() if err != nil { diff --git a/proxy/MAINTAINERS b/proxy/MAINTAINERS new file mode 100644 index 000000000..1e998f8ac --- /dev/null +++ b/proxy/MAINTAINERS @@ -0,0 +1 @@ +Michael Crosby (@crosbymichael) diff --git a/network_proxy_test.go b/proxy/network_proxy_test.go similarity index 99% rename from network_proxy_test.go rename to proxy/network_proxy_test.go index c27393eb5..b57c23c1e 100644 --- a/network_proxy_test.go +++ b/proxy/network_proxy_test.go @@ -1,4 +1,4 @@ -package docker +package proxy import ( "bytes" diff --git a/proxy/proxy.go b/proxy/proxy.go new file mode 100644 index 000000000..7a711f657 --- /dev/null +++ b/proxy/proxy.go @@ -0,0 +1,29 @@ +package proxy + +import ( + "fmt" + "net" +) + +type Proxy interface { + // Start forwarding traffic back and forth the front and back-end + // addresses. + Run() + // Stop forwarding traffic and close both ends of the Proxy. + Close() + // Return the address on which the proxy is listening. + FrontendAddr() net.Addr + // Return the proxied address. + BackendAddr() net.Addr +} + +func NewProxy(frontendAddr, backendAddr net.Addr) (Proxy, error) { + switch frontendAddr.(type) { + case *net.UDPAddr: + return NewUDPProxy(frontendAddr.(*net.UDPAddr), backendAddr.(*net.UDPAddr)) + case *net.TCPAddr: + return NewTCPProxy(frontendAddr.(*net.TCPAddr), backendAddr.(*net.TCPAddr)) + default: + panic(fmt.Errorf("Unsupported protocol")) + } +} diff --git a/proxy/tcp_proxy.go b/proxy/tcp_proxy.go new file mode 100644 index 000000000..e7c460f61 --- /dev/null +++ b/proxy/tcp_proxy.go @@ -0,0 +1,93 @@ +package proxy + +import ( + "github.com/dotcloud/docker/utils" + "io" + "log" + "net" + "syscall" +) + +type TCPProxy struct { + listener *net.TCPListener + frontendAddr *net.TCPAddr + backendAddr *net.TCPAddr +} + +func NewTCPProxy(frontendAddr, backendAddr *net.TCPAddr) (*TCPProxy, error) { + listener, err := net.ListenTCP("tcp", frontendAddr) + if err != nil { + return nil, err + } + // If the port in frontendAddr was 0 then ListenTCP will have a picked + // a port to listen on, hence the call to Addr to get that actual port: + return &TCPProxy{ + listener: listener, + frontendAddr: listener.Addr().(*net.TCPAddr), + backendAddr: backendAddr, + }, nil +} + +func (proxy *TCPProxy) clientLoop(client *net.TCPConn, quit chan bool) { + backend, err := net.DialTCP("tcp", nil, proxy.backendAddr) + if err != nil { + log.Printf("Can't forward traffic to backend tcp/%v: %v\n", proxy.backendAddr, err.Error()) + client.Close() + return + } + + event := make(chan int64) + var broker = func(to, from *net.TCPConn) { + written, err := io.Copy(to, from) + if err != nil { + // If the socket we are writing to is shutdown with + // SHUT_WR, forward it to the other end of the pipe: + if err, ok := err.(*net.OpError); ok && err.Err == syscall.EPIPE { + from.CloseWrite() + } + } + to.CloseRead() + event <- written + } + utils.Debugf("Forwarding traffic between tcp/%v and tcp/%v", client.RemoteAddr(), backend.RemoteAddr()) + go broker(client, backend) + go broker(backend, client) + + var transferred int64 = 0 + for i := 0; i < 2; i++ { + select { + case written := <-event: + transferred += written + case <-quit: + // Interrupt the two brokers and "join" them. + client.Close() + backend.Close() + for ; i < 2; i++ { + transferred += <-event + } + goto done + } + } + client.Close() + backend.Close() +done: + utils.Debugf("%v bytes transferred between tcp/%v and tcp/%v", transferred, client.RemoteAddr(), backend.RemoteAddr()) +} + +func (proxy *TCPProxy) Run() { + quit := make(chan bool) + defer close(quit) + utils.Debugf("Starting proxy on tcp/%v for tcp/%v", proxy.frontendAddr, proxy.backendAddr) + for { + client, err := proxy.listener.Accept() + if err != nil { + utils.Debugf("Stopping proxy on tcp/%v for tcp/%v (%v)", proxy.frontendAddr, proxy.backendAddr, err.Error()) + return + } + go proxy.clientLoop(client.(*net.TCPConn), quit) + } +} + +func (proxy *TCPProxy) Close() { proxy.listener.Close() } +func (proxy *TCPProxy) FrontendAddr() net.Addr { return proxy.frontendAddr } +func (proxy *TCPProxy) BackendAddr() net.Addr { return proxy.backendAddr } diff --git a/network_proxy.go b/proxy/udp_proxy.go similarity index 55% rename from network_proxy.go rename to proxy/udp_proxy.go index 86545801b..7d34988f7 100644 --- a/network_proxy.go +++ b/proxy/udp_proxy.go @@ -1,10 +1,8 @@ -package docker +package proxy import ( "encoding/binary" - "fmt" "github.com/dotcloud/docker/utils" - "io" "log" "net" "sync" @@ -17,107 +15,6 @@ const ( UDPBufSize = 2048 ) -type Proxy interface { - // Start forwarding traffic back and forth the front and back-end - // addresses. - Run() - // Stop forwarding traffic and close both ends of the Proxy. - Close() - // Return the address on which the proxy is listening. - FrontendAddr() net.Addr - // Return the proxied address. - BackendAddr() net.Addr -} - -type TCPProxy struct { - listener *net.TCPListener - frontendAddr *net.TCPAddr - backendAddr *net.TCPAddr -} - -func NewTCPProxy(frontendAddr, backendAddr *net.TCPAddr) (*TCPProxy, error) { - listener, err := net.ListenTCP("tcp", frontendAddr) - if err != nil { - return nil, err - } - // If the port in frontendAddr was 0 then ListenTCP will have a picked - // a port to listen on, hence the call to Addr to get that actual port: - return &TCPProxy{ - listener: listener, - frontendAddr: listener.Addr().(*net.TCPAddr), - backendAddr: backendAddr, - }, nil -} - -func (proxy *TCPProxy) clientLoop(client *net.TCPConn, quit chan bool) { - backend, err := net.DialTCP("tcp", nil, proxy.backendAddr) - if err != nil { - log.Printf("Can't forward traffic to backend tcp/%v: %v\n", proxy.backendAddr, err.Error()) - client.Close() - return - } - - event := make(chan int64) - var broker = func(to, from *net.TCPConn) { - written, err := io.Copy(to, from) - if err != nil { - // If the socket we are writing to is shutdown with - // SHUT_WR, forward it to the other end of the pipe: - if err, ok := err.(*net.OpError); ok && err.Err == syscall.EPIPE { - from.CloseWrite() - } - } - to.CloseRead() - event <- written - } - utils.Debugf("Forwarding traffic between tcp/%v and tcp/%v", client.RemoteAddr(), backend.RemoteAddr()) - go broker(client, backend) - go broker(backend, client) - - var transferred int64 = 0 - for i := 0; i < 2; i++ { - select { - case written := <-event: - transferred += written - case <-quit: - // Interrupt the two brokers and "join" them. - client.Close() - backend.Close() - for ; i < 2; i++ { - transferred += <-event - } - goto done - } - } - client.Close() - backend.Close() -done: - utils.Debugf("%v bytes transferred between tcp/%v and tcp/%v", transferred, client.RemoteAddr(), backend.RemoteAddr()) -} - -func (proxy *TCPProxy) Run() { - quit := make(chan bool) - defer close(quit) - - utils.Debugf("Starting proxy on tcp/%v for tcp/%v", proxy.frontendAddr, proxy.backendAddr) - for { - client, err := proxy.listener.Accept() - if err != nil { - if utils.IsClosedError(err) { - utils.Debugf("Stopping proxy on tcp/%v for tcp/%v (socket was closed)", proxy.frontendAddr, proxy.backendAddr) - } else { - utils.Errorf("Stopping proxy on tcp/%v for tcp/%v (%v)", proxy.frontendAddr, proxy.backendAddr, err.Error()) - } - return - } - go proxy.clientLoop(client.(*net.TCPConn), quit) - } -} - -func (proxy *TCPProxy) Close() { proxy.listener.Close() } -func (proxy *TCPProxy) FrontendAddr() net.Addr { return proxy.frontendAddr } -func (proxy *TCPProxy) BackendAddr() net.Addr { return proxy.backendAddr } - // A net.Addr where the IP is split into two fields so you can use it as a key // in a map: type connTrackKey struct { @@ -253,14 +150,3 @@ func (proxy *UDPProxy) Close() { func (proxy *UDPProxy) FrontendAddr() net.Addr { return proxy.frontendAddr } func (proxy *UDPProxy) BackendAddr() net.Addr { return proxy.backendAddr } - -func NewProxy(frontendAddr, backendAddr net.Addr) (Proxy, error) { - switch frontendAddr.(type) { - case *net.UDPAddr: - return NewUDPProxy(frontendAddr.(*net.UDPAddr), backendAddr.(*net.UDPAddr)) - case *net.TCPAddr: - return NewTCPProxy(frontendAddr.(*net.TCPAddr), backendAddr.(*net.TCPAddr)) - default: - panic(fmt.Errorf("Unsupported protocol")) - } -} diff --git a/runtime.go b/runtime.go index f218521ad..d7a6e8800 100644 --- a/runtime.go +++ b/runtime.go @@ -3,6 +3,7 @@ package docker import ( "container/list" "fmt" + "github.com/dotcloud/docker/gograph" "github.com/dotcloud/docker/utils" "io" "io/ioutil" @@ -24,7 +25,6 @@ type Capabilities struct { } type Runtime struct { - root string repository string containers *list.List networkManager *NetworkManager @@ -33,10 +33,10 @@ type Runtime struct { idIndex *utils.TruncIndex capabilities *Capabilities kernelVersion *utils.KernelVersionInfo - autoRestart bool volumes *Graph srv *Server - Dns []string + config *DaemonConfig + containerGraph *gograph.Database } var sysInitPath string @@ -67,10 +67,15 @@ func (runtime *Runtime) getContainerElement(id string) *list.Element { // 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 (runtime *Runtime) Get(name string) *Container { + if c, _ := runtime.GetByName(name); c != nil { + return c + } + id, err := runtime.idIndex.Get(name) if err != nil { return nil } + e := runtime.getContainerElement(id) if e == nil { return nil @@ -88,10 +93,9 @@ func (runtime *Runtime) containerRoot(id string) string { return path.Join(runtime.repository, id) } -// Load reads the contents of a container from disk and registers -// it with Register. +// Load reads the contents of a container from disk // This is typically done at startup. -func (runtime *Runtime) Load(id string) (*Container, error) { +func (runtime *Runtime) load(id string) (*Container, error) { container := &Container{root: runtime.containerRoot(id)} if err := container.FromDisk(); err != nil { return nil, err @@ -102,9 +106,6 @@ func (runtime *Runtime) Load(id string) (*Container, error) { if container.State.Running { container.State.Ghost = true } - if err := runtime.Register(container); err != nil { - return nil, err - } return container, nil } @@ -149,11 +150,11 @@ func (runtime *Runtime) Register(container *Container) error { } if !strings.Contains(string(output), "RUNNING") { utils.Debugf("Container %s was supposed to be running be is not.", container.ID) - if runtime.autoRestart { + if runtime.config.AutoRestart { utils.Debugf("Restarting") container.State.Ghost = false container.State.setStopped(0) - hostConfig := &HostConfig{} + hostConfig, _ := container.ReadHostConfig() if err := container.Start(hostConfig); err != nil { return err } @@ -173,9 +174,9 @@ func (runtime *Runtime) Register(container *Container) error { if !container.State.Running { close(container.waitLock) } else if !nomonitor { - container.allocateNetwork() - // hostConfig isn't needed here and can be nil - go container.monitor(nil) + hostConfig, _ := container.ReadHostConfig() + container.allocateNetwork(hostConfig) + go container.monitor(hostConfig) } return nil } @@ -203,6 +204,7 @@ func (runtime *Runtime) Destroy(container *Container) error { if err := container.Stop(3); err != nil { return err } + if mounted, err := container.Mounted(); err != nil { return err } else if mounted { @@ -210,6 +212,11 @@ func (runtime *Runtime) Destroy(container *Container) error { return fmt.Errorf("Unable to unmount container %v: %v", container.ID, err) } } + + if _, err := runtime.containerGraph.Purge(container.ID); err != nil { + utils.Debugf("Unable to remove container from link graph: %s", err) + } + // Deregister the container before removing its directory, to avoid race conditions runtime.idIndex.Delete(container.ID) runtime.containers.Remove(element) @@ -228,9 +235,10 @@ func (runtime *Runtime) restore() error { if err != nil { return err } + containers := []*Container{} for i, v := range dir { id := v.Name() - container, err := runtime.Load(id) + container, err := runtime.load(id) if i%21 == 0 && os.Getenv("DEBUG") == "" && os.Getenv("TEST") == "" { fmt.Printf("\b%c", wheel[i%4]) } @@ -239,10 +247,30 @@ func (runtime *Runtime) restore() error { continue } utils.Debugf("Loaded container %v", container.ID) + containers = append(containers, container) + } + sortContainers(containers, func(i, j *Container) bool { + ic, _ := i.ReadHostConfig() + jc, _ := j.ReadHostConfig() + + if ic == nil || ic.Links == nil { + return true + } + if jc == nil || jc.Links == nil { + return false + } + return len(ic.Links) < len(jc.Links) + }) + for _, container := range containers { + if err := runtime.Register(container); err != nil { + utils.Debugf("Failed to register container %s: %s", container.ID, err) + continue + } } if os.Getenv("DEBUG") == "" && os.Getenv("TEST") == "" { fmt.Printf("\bdone.\n") } + return nil } @@ -275,27 +303,45 @@ func (runtime *Runtime) UpdateCapabilities(quiet bool) { } // Create creates a new container from the given configuration. -func (runtime *Runtime) Create(config *Config) (*Container, error) { +func (runtime *Runtime) Create(config *Config) (*Container, []string, error) { // Lookup image img, err := runtime.repositories.LookupImage(config.Image) if err != nil { - return nil, err + return nil, nil, err } + warnings := []string{} if img.Config != nil { - if err := MergeConfig(config, img.Config); err != nil { - return nil, err + if img.Config.PortSpecs != nil && warnings != nil { + for _, p := range img.Config.PortSpecs { + if strings.Contains(p, ":") { + warnings = append(warnings, "This image expects private ports to be mapped to public ports on your host. "+ + "This has been deprecated and the public mappings will not be honored."+ + "Use -p to publish the ports.") + break + } + } } + if err := MergeConfig(config, img.Config); err != nil { + return nil, nil, err + } + } if len(config.Entrypoint) != 0 && config.Cmd == nil { config.Cmd = []string{} } else if config.Cmd == nil || len(config.Cmd) == 0 { - return nil, fmt.Errorf("No command specified") + return nil, nil, fmt.Errorf("No command specified") } // Generate id id := GenerateID() + + // Set the default enitity in the graph + if _, err := runtime.containerGraph.Set(fmt.Sprintf("/%s", id), id); err != nil { + return nil, nil, err + } + // Generate default hostname // FIXME: the lxc template no longer needs to set a default hostname if config.Hostname == "" { @@ -329,36 +375,36 @@ func (runtime *Runtime) Create(config *Config) (*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 nil, err + return nil, nil, err } resolvConf, err := utils.GetResolvConf() if err != nil { - return nil, err + return nil, nil, err } - if len(config.Dns) == 0 && len(runtime.Dns) == 0 && utils.CheckLocalDns(resolvConf) { + if len(config.Dns) == 0 && len(runtime.config.Dns) == 0 && utils.CheckLocalDns(resolvConf) { //"WARNING: Docker detected local DNS server on resolv.conf. Using default external servers: %v", defaultDns - runtime.Dns = defaultDns + runtime.config.Dns = defaultDns } // If custom dns exists, then create a resolv.conf for the container - if len(config.Dns) > 0 || len(runtime.Dns) > 0 { + if len(config.Dns) > 0 || len(runtime.config.Dns) > 0 { var dns []string if len(config.Dns) > 0 { dns = config.Dns } else { - dns = runtime.Dns + dns = runtime.config.Dns } container.ResolvConfPath = path.Join(container.root, "resolv.conf") f, err := os.Create(container.ResolvConfPath) if err != nil { - return nil, err + return nil, nil, err } defer f.Close() for _, dns := range dns { if _, err := f.Write([]byte("nameserver " + dns + "\n")); err != nil { - return nil, err + return nil, nil, err } } } else { @@ -367,7 +413,7 @@ func (runtime *Runtime) Create(config *Config) (*Container, error) { // Step 2: save the container json if err := container.ToDisk(); err != nil { - return nil, err + return nil, nil, err } // Step 3: if hostname, build hostname and hosts files @@ -397,9 +443,9 @@ ff02::2 ip6-allrouters // Step 4: register the container if err := runtime.Register(container); err != nil { - return nil, err + return nil, nil, err } - return container, nil + return container, warnings, nil } // Commit creates a new filesystem image from the current state of a container. @@ -429,13 +475,85 @@ func (runtime *Runtime) Commit(container *Container, repository, tag, comment, a return img, nil } -// FIXME: harmonize with NewGraph() -func NewRuntime(flGraphPath string, autoRestart bool, dns []string) (*Runtime, error) { - runtime, err := NewRuntimeFromDirectory(flGraphPath, autoRestart) +func (runtime *Runtime) GetByName(name string) (*Container, error) { + if id, err := runtime.idIndex.Get(name); err == nil { + name = id + } + + entity := runtime.containerGraph.Get(name) + if entity == nil { + return nil, fmt.Errorf("Could not find entity for %s", name) + } + e := runtime.getContainerElement(entity.ID()) + if e == nil { + return nil, fmt.Errorf("Could not find container for entity id %s", entity.ID()) + } + return e.Value.(*Container), nil +} + +func (runtime *Runtime) Children(name string) (map[string]*Container, error) { + children := make(map[string]*Container) + + err := runtime.containerGraph.Walk(name, func(p string, e *gograph.Entity) error { + c := runtime.Get(e.ID()) + if c == nil { + return fmt.Errorf("Could not get container for name %s and id %s", e.ID(), p) + } + children[p] = c + return nil + }, 0) + + if err != nil { + return nil, err + } + return children, nil +} + +func (runtime *Runtime) RenameLink(oldName, newName string) error { + if id, err := runtime.idIndex.Get(oldName); err == nil { + oldName = id + } + entity := runtime.containerGraph.Get(oldName) + if entity == nil { + return fmt.Errorf("Could not find entity for %s", oldName) + } + + // This is not rename but adding a new link for the default name + // Strip the leading '/' + if entity.ID() == oldName[1:] { + _, err := runtime.containerGraph.Set(newName, entity.ID()) + return err + } + return runtime.containerGraph.Rename(oldName, newName) +} + +func (runtime *Runtime) Link(parentName, childName, alias string) error { + if id, err := runtime.idIndex.Get(parentName); err == nil { + parentName = id + } + parent := runtime.containerGraph.Get(parentName) + if parent == nil { + return fmt.Errorf("Could not get container for %s", parentName) + } + if id, err := runtime.idIndex.Get(childName); err == nil { + childName = id + } + child := runtime.containerGraph.Get(childName) + if child == nil { + return fmt.Errorf("Could not get container for %s", childName) + } + cc := runtime.Get(child.ID()) + + _, err := runtime.containerGraph.Set(path.Join(parentName, alias), cc.ID) + return err +} + +// FIXME: harmonize with NewGraph() +func NewRuntime(config *DaemonConfig) (*Runtime, error) { + runtime, err := NewRuntimeFromDirectory(config) if err != nil { return nil, err } - runtime.Dns = dns if k, err := utils.GetKernelVersion(); err != nil { log.Printf("WARNING: %s\n", err) @@ -449,34 +567,39 @@ func NewRuntime(flGraphPath string, autoRestart bool, dns []string) (*Runtime, e return runtime, nil } -func NewRuntimeFromDirectory(root string, autoRestart bool) (*Runtime, error) { - runtimeRepo := path.Join(root, "containers") +func NewRuntimeFromDirectory(config *DaemonConfig) (*Runtime, error) { + runtimeRepo := path.Join(config.GraphPath, "containers") if err := os.MkdirAll(runtimeRepo, 0700); err != nil && !os.IsExist(err) { return nil, err } - g, err := NewGraph(path.Join(root, "graph")) + g, err := NewGraph(path.Join(config.GraphPath, "graph")) if err != nil { return nil, err } - volumes, err := NewGraph(path.Join(root, "volumes")) + volumes, err := NewGraph(path.Join(config.GraphPath, "volumes")) if err != nil { return nil, err } - repositories, err := NewTagStore(path.Join(root, "repositories"), g) + repositories, err := NewTagStore(path.Join(config.GraphPath, "repositories"), g) if err != nil { return nil, fmt.Errorf("Couldn't create Tag store: %s", err) } - if NetworkBridgeIface == "" { - NetworkBridgeIface = DefaultNetworkBridge + if config.BridgeIface == "" { + config.BridgeIface = DefaultNetworkBridge } - netManager, err := newNetworkManager(NetworkBridgeIface) + netManager, err := newNetworkManager(config) if err != nil { return nil, err } + + graph, err := gograph.NewDatabase(path.Join(config.GraphPath, "linkgraph.db")) + if err != nil { + return nil, err + } + runtime := &Runtime{ - root: root, repository: runtimeRepo, containers: list.New(), networkManager: netManager, @@ -484,8 +607,9 @@ func NewRuntimeFromDirectory(root string, autoRestart bool) (*Runtime, error) { repositories: repositories, idIndex: utils.NewTruncIndex(), capabilities: &Capabilities{}, - autoRestart: autoRestart, volumes: volumes, + config: config, + containerGraph: graph, } if err := runtime.restore(); err != nil { diff --git a/runtime_test.go b/runtime_test.go index 75081fff1..542c98926 100644 --- a/runtime_test.go +++ b/runtime_test.go @@ -43,7 +43,7 @@ func nuke(runtime *Runtime) error { } wg.Wait() runtime.networkManager.Close() - return os.RemoveAll(runtime.root) + return os.RemoveAll(runtime.config.GraphPath) } func cleanup(runtime *Runtime) error { @@ -85,8 +85,6 @@ func init() { log.Fatal("docker tests need to be run as root") } - NetworkBridgeIface = unitTestNetworkBridge - // Setup the base runtime, which will be duplicated for each test. // (no tests are run directly in the base) setupBaseImage() @@ -98,7 +96,12 @@ func init() { func setupBaseImage() { - runtime, err := NewRuntimeFromDirectory(unitTestStoreBase, false) + config := &DaemonConfig{ + GraphPath: unitTestStoreBase, + AutoRestart: false, + BridgeIface: unitTestNetworkBridge, + } + runtime, err := NewRuntimeFromDirectory(config) if err != nil { log.Fatalf("Unable to create a runtime for tests:", err) } @@ -106,7 +109,6 @@ func setupBaseImage() { // Create the "Server" srv := &Server{ runtime: runtime, - enableCors: false, pullingPool: make(map[string]struct{}), pushingPool: make(map[string]struct{}), } @@ -171,7 +173,7 @@ func TestRuntimeCreate(t *testing.T) { t.Errorf("Expected 0 containers, %v found", len(runtime.List())) } - container, err := runtime.Create(&Config{ + container, _, err := runtime.Create(&Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"ls", "-al"}, }, @@ -212,7 +214,7 @@ func TestRuntimeCreate(t *testing.T) { } // Make sure crete with bad parameters returns an error - _, err = runtime.Create( + _, _, err = runtime.Create( &Config{ Image: GetTestImage(runtime).ID, }, @@ -221,7 +223,7 @@ func TestRuntimeCreate(t *testing.T) { t.Fatal("Builder.Create should throw an error when Cmd is missing") } - _, err = runtime.Create( + _, _, err = runtime.Create( &Config{ Image: GetTestImage(runtime).ID, Cmd: []string{}, @@ -258,7 +260,7 @@ func TestRuntimeCreate(t *testing.T) { func TestDestroy(t *testing.T) { runtime := mkRuntime(t) defer nuke(runtime) - container, err := runtime.Create(&Config{ + container, _, err := runtime.Create(&Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"ls", "-al"}, }, @@ -332,6 +334,7 @@ func startEchoServerContainer(t *testing.T, proto string) (*Runtime, *Container, port := 5554 var container *Container var strPort string + var p Port for { port += 1 strPort = strconv.Itoa(port) @@ -344,22 +347,33 @@ func startEchoServerContainer(t *testing.T, proto string) (*Runtime, *Container, t.Fatal(fmt.Errorf("Unknown protocol %v", proto)) } t.Log("Trying port", strPort) - container, err = runtime.Create(&Config{ - Image: GetTestImage(runtime).ID, - Cmd: []string{"sh", "-c", cmd}, - PortSpecs: []string{fmt.Sprintf("%s/%s", strPort, proto)}, + ep := make(map[Port]struct{}, 1) + p = Port(fmt.Sprintf("%s/%s", strPort, proto)) + ep[p] = struct{}{} + + container, _, err = runtime.Create(&Config{ + Image: GetTestImage(runtime).ID, + Cmd: []string{"sh", "-c", cmd}, + PortSpecs: []string{fmt.Sprintf("%s/%s", strPort, proto)}, + ExposedPorts: ep, }) - if container != nil { - break - } if err != nil { nuke(runtime) t.Fatal(err) } + + if container != nil { + break + } t.Logf("Port %v already in use", strPort) } - hostConfig := &HostConfig{} + hostConfig := &HostConfig{ + PortBindings: make(map[Port][]PortBinding), + } + hostConfig.PortBindings[p] = []PortBinding{ + {}, + } if err := container.Start(hostConfig); err != nil { nuke(runtime) t.Fatal(err) @@ -374,7 +388,7 @@ func startEchoServerContainer(t *testing.T, proto string) (*Runtime, *Container, // Even if the state is running, lets give some time to lxc to spawn the process container.WaitTimeout(500 * time.Millisecond) - strPort = container.NetworkSettings.PortMapping[strings.Title(proto)][strPort] + strPort = container.NetworkSettings.Ports[p][0].HostPort return runtime, container, strPort } @@ -506,7 +520,8 @@ func TestRestore(t *testing.T) { // Here are are simulating a docker restart - that is, reloading all containers // from scratch - runtime2, err := NewRuntimeFromDirectory(runtime1.root, false) + runtime1.config.AutoRestart = false + runtime2, err := NewRuntimeFromDirectory(runtime1.config) if err != nil { t.Fatal(err) } @@ -533,3 +548,271 @@ func TestRestore(t *testing.T) { } container2.State.Running = false } + +func TestReloadContainerLinks(t *testing.T) { + runtime1 := mkRuntime(t) + defer nuke(runtime1) + // Create a container with one instance of docker + container1, _, _ := mkContainer(runtime1, []string{"_", "ls", "-al"}, t) + defer runtime1.Destroy(container1) + + // Create a second container meant to be killed + container2, _, _ := mkContainer(runtime1, []string{"-i", "_", "/bin/cat"}, t) + defer runtime1.Destroy(container2) + + // Start the container non blocking + hostConfig := &HostConfig{} + if err := container2.Start(hostConfig); err != nil { + t.Fatal(err) + } + h1 := &HostConfig{} + // Add a link to container 2 + h1.Links = []string{utils.TruncateID(container2.ID) + ":first"} + if err := container1.Start(h1); err != nil { + t.Fatal(err) + } + + if !container2.State.Running { + t.Fatalf("Container %v should appear as running but isn't", container2.ID) + } + + if !container1.State.Running { + t.Fatalf("Container %s should appear as running bu isn't", container1.ID) + } + + if len(runtime1.List()) != 2 { + t.Errorf("Expected 2 container, %v found", len(runtime1.List())) + } + + if !container2.State.Running { + t.Fatalf("Container %v should appear as running but isn't", container2.ID) + } + + // Here are are simulating a docker restart - that is, reloading all containers + // from scratch + runtime1.config.AutoRestart = true + runtime2, err := NewRuntimeFromDirectory(runtime1.config) + if err != nil { + t.Fatal(err) + } + defer nuke(runtime2) + if len(runtime2.List()) != 2 { + t.Errorf("Expected 2 container, %v found", len(runtime2.List())) + } + runningCount := 0 + for _, c := range runtime2.List() { + if c.State.Running { + t.Logf("Running container found: %v (%v)", c.ID, c.Path) + runningCount++ + } + } + if runningCount != 2 { + t.Fatalf("Expected 2 container alive, %d found", runningCount) + } + + // Make sure container 2 ( the child of container 1 ) was registered and started first + // with the runtime + first := runtime2.containers.Front() + if first.Value.(*Container).ID != container2.ID { + t.Fatalf("Container 2 %s should be registered first in the runtime", container2.ID) + } + + t.Logf("Number of links: %d", runtime2.containerGraph.Refs("engine")) + // Verify that the link is still registered in the runtime + entity := runtime2.containerGraph.Get(fmt.Sprintf("/%s", container1.ID)) + if entity == nil { + t.Fatal("Entity should not be nil") + } +} + +func TestDefaultContainerName(t *testing.T) { + runtime := mkRuntime(t) + defer nuke(runtime) + srv := &Server{runtime: runtime} + + config, _, _, err := ParseRun([]string{GetTestImage(runtime).ID, "echo test"}, nil) + if err != nil { + t.Fatal(err) + } + + shortId, _, err := srv.ContainerCreate(config) + if err != nil { + t.Fatal(err) + } + container := runtime.Get(shortId) + containerID := container.ID + + paths := runtime.containerGraph.RefPaths(containerID) + if paths == nil || len(paths) == 0 { + t.Fatalf("Could not find edges for %s", containerID) + } + edge := paths[0] + if edge.ParentID != "0" { + t.Fatalf("Expected engine got %s", edge.ParentID) + } + if edge.EntityID != containerID { + t.Fatalf("Expected %s got %s", containerID, edge.EntityID) + } + if edge.Name != containerID { + t.Fatalf("Expected %s got %s", containerID, edge.Name) + } +} + +func TestDefaultContainerRename(t *testing.T) { + runtime := mkRuntime(t) + defer nuke(runtime) + srv := &Server{runtime: runtime} + + config, _, _, err := ParseRun([]string{GetTestImage(runtime).ID, "echo test"}, nil) + if err != nil { + t.Fatal(err) + } + + shortId, _, err := srv.ContainerCreate(config) + if err != nil { + t.Fatal(err) + } + container := runtime.Get(shortId) + containerID := container.ID + + if err := runtime.RenameLink(fmt.Sprintf("/%s", containerID), "/webapp"); err != nil { + t.Fatal(err) + } + + webapp, err := runtime.GetByName("/webapp") + if err != nil { + t.Fatal(err) + } + + if webapp.ID != container.ID { + t.Fatalf("Expect webapp id to match container id: %s != %s", webapp.ID, container.ID) + } +} + +func TestLinkChildContainer(t *testing.T) { + runtime := mkRuntime(t) + defer nuke(runtime) + srv := &Server{runtime: runtime} + + config, _, _, err := ParseRun([]string{GetTestImage(runtime).ID, "echo test"}, nil) + if err != nil { + t.Fatal(err) + } + + shortId, _, err := srv.ContainerCreate(config) + if err != nil { + t.Fatal(err) + } + container := runtime.Get(shortId) + + if err := runtime.RenameLink(fmt.Sprintf("/%s", container.ID), "/webapp"); err != nil { + t.Fatal(err) + } + + webapp, err := runtime.GetByName("/webapp") + if err != nil { + t.Fatal(err) + } + + if webapp.ID != container.ID { + t.Fatalf("Expect webapp id to match container id: %s != %s", webapp.ID, container.ID) + } + + config, _, _, err = ParseRun([]string{GetTestImage(runtime).ID, "echo test"}, nil) + if err != nil { + t.Fatal(err) + } + + shortId, _, err = srv.ContainerCreate(config) + if err != nil { + t.Fatal(err) + } + + childContainer := runtime.Get(shortId) + if err := runtime.RenameLink(fmt.Sprintf("/%s", childContainer.ID), "/db"); err != nil { + t.Fatal(err) + } + + if err := runtime.Link("/webapp", "/db", "db"); err != nil { + t.Fatal(err) + } + + // Get the child by it's new name + db, err := runtime.GetByName("/webapp/db") + if err != nil { + t.Fatal(err) + } + if db.ID != childContainer.ID { + t.Fatalf("Expect db id to match container id: %s != %s", db.ID, childContainer.ID) + } +} + +func TestGetAllChildren(t *testing.T) { + runtime := mkRuntime(t) + defer nuke(runtime) + srv := &Server{runtime: runtime} + + config, _, _, err := ParseRun([]string{GetTestImage(runtime).ID, "echo test"}, nil) + if err != nil { + t.Fatal(err) + } + + shortId, _, err := srv.ContainerCreate(config) + if err != nil { + t.Fatal(err) + } + container := runtime.Get(shortId) + + if err := runtime.RenameLink(fmt.Sprintf("/%s", container.ID), "/webapp"); err != nil { + t.Fatal(err) + } + + webapp, err := runtime.GetByName("/webapp") + if err != nil { + t.Fatal(err) + } + + if webapp.ID != container.ID { + t.Fatalf("Expect webapp id to match container id: %s != %s", webapp.ID, container.ID) + } + + config, _, _, err = ParseRun([]string{GetTestImage(runtime).ID, "echo test"}, nil) + if err != nil { + t.Fatal(err) + } + + shortId, _, err = srv.ContainerCreate(config) + if err != nil { + t.Fatal(err) + } + + childContainer := runtime.Get(shortId) + if err := runtime.RenameLink(fmt.Sprintf("/%s", childContainer.ID), "/db"); err != nil { + t.Fatal(err) + } + + if err := runtime.Link("/webapp", "/db", "db"); err != nil { + t.Fatal(err) + } + + children, err := runtime.Children("/webapp") + if err != nil { + t.Fatal(err) + } + + if children == nil { + t.Fatal("Children should not be nil") + } + if len(children) == 0 { + t.Fatal("Children should not be empty") + } + + for key, value := range children { + if key != "/webapp/db" { + t.Fatalf("Expected /webapp/db got %s", key) + } + if value.ID != childContainer.ID { + t.Fatalf("Expected id %s got %s", childContainer.ID, value.ID) + } + } +} diff --git a/server.go b/server.go index 27d1968dc..b1aca9e0f 100644 --- a/server.go +++ b/server.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "github.com/dotcloud/docker/auth" + "github.com/dotcloud/docker/gograph" "github.com/dotcloud/docker/registry" "github.com/dotcloud/docker/utils" "io" @@ -103,7 +104,7 @@ func (srv *Server) ContainerExport(name string, out io.Writer) error { } func (srv *Server) ImagesSearch(term string) ([]APISearch, error) { - r, err := registry.NewRegistry(srv.runtime.root, nil, srv.HTTPRequestFactory(nil)) + r, err := registry.NewRegistry(srv.runtime.config.GraphPath, nil, srv.HTTPRequestFactory(nil)) if err != nil { return nil, err } @@ -140,7 +141,7 @@ func (srv *Server) ImageInsert(name, url, path string, out io.Writer, sf *utils. return "", err } - c, err := srv.runtime.Create(config) + c, _, err := srv.runtime.Create(config) if err != nil { return "", err } @@ -358,7 +359,7 @@ func (srv *Server) ContainerChanges(name string) ([]Change, error) { func (srv *Server) Containers(all, size bool, n int, since, before string) []APIContainers { var foundBefore bool var displayed int - retContainers := []APIContainers{} + out := []APIContainers{} for _, container := range srv.runtime.List() { if !container.State.Running && !all && n == -1 && since == "" && before == "" { @@ -380,23 +381,35 @@ func (srv *Server) Containers(all, size bool, n int, since, before string) []API break } displayed++ - - c := APIContainers{ - ID: container.ID, - } - c.Image = srv.runtime.repositories.ImageName(container.Image) - c.Command = fmt.Sprintf("%s %s", container.Path, strings.Join(container.Args, " ")) - c.Created = container.Created.Unix() - c.Status = container.State.String() - c.Ports = container.NetworkSettings.PortMappingAPI() - if size { - c.SizeRw, c.SizeRootFs = container.GetSize() - } - retContainers = append(retContainers, c) + c := createAPIContainer(container, size, srv.runtime) + out = append(out, c) } - return retContainers + return out } +func createAPIContainer(container *Container, size bool, runtime *Runtime) APIContainers { + c := APIContainers{ + ID: container.ID, + } + names := []string{} + runtime.containerGraph.Walk("/", func(p string, e *gograph.Entity) error { + if e.ID() == container.ID { + names = append(names, p) + } + return nil + }, -1) + c.Names = names + + c.Image = runtime.repositories.ImageName(container.Image) + c.Command = fmt.Sprintf("%s %s", container.Path, strings.Join(container.Args, " ")) + c.Created = container.Created.Unix() + c.Status = container.State.String() + c.Ports = container.NetworkSettings.PortMappingAPI() + if size { + c.SizeRw, c.SizeRootFs = container.GetSize() + } + return c +} func (srv *Server) ContainerCommit(name, repo, tag, author, comment string, config *Config) (string, error) { container := srv.runtime.Get(name) if container == nil { @@ -635,7 +648,7 @@ func (srv *Server) poolRemove(kind, key string) error { } func (srv *Server) ImagePull(localName string, tag string, out io.Writer, sf *utils.StreamFormatter, authConfig *auth.AuthConfig, metaHeaders map[string][]string, parallel bool) error { - r, err := registry.NewRegistry(srv.runtime.root, authConfig, srv.HTTPRequestFactory(metaHeaders)) + r, err := registry.NewRegistry(srv.runtime.config.GraphPath, authConfig, srv.HTTPRequestFactory(metaHeaders)) if err != nil { return err } @@ -844,7 +857,7 @@ func (srv *Server) ImagePush(localName string, out io.Writer, sf *utils.StreamFo out = utils.NewWriteFlusher(out) img, err := srv.runtime.graph.Get(localName) - r, err2 := registry.NewRegistry(srv.runtime.root, authConfig, srv.HTTPRequestFactory(metaHeaders)) + r, err2 := registry.NewRegistry(srv.runtime.config.GraphPath, authConfig, srv.HTTPRequestFactory(metaHeaders)) if err2 != nil { return err2 } @@ -909,10 +922,9 @@ func (srv *Server) ImageImport(src, repo, tag string, in io.Reader, out io.Write return nil } -func (srv *Server) ContainerCreate(config *Config) (string, error) { - +func (srv *Server) ContainerCreate(config *Config) (string, []string, error) { if config.Memory != 0 && config.Memory < 524288 { - return "", fmt.Errorf("Memory limit must be given in bytes (minimum 524288 bytes)") + return "", nil, fmt.Errorf("Memory limit must be given in bytes (minimum 524288 bytes)") } if config.Memory > 0 && !srv.runtime.capabilities.MemoryLimit { @@ -922,7 +934,7 @@ func (srv *Server) ContainerCreate(config *Config) (string, error) { if config.Memory > 0 && !srv.runtime.capabilities.SwapLimit { config.MemorySwap = -1 } - container, err := srv.runtime.Create(config) + container, buildWarnings, err := srv.runtime.Create(config) if err != nil { if srv.runtime.graph.IsNotExist(err) { @@ -931,12 +943,12 @@ func (srv *Server) ContainerCreate(config *Config) (string, error) { tag = DEFAULTTAG } - return "", fmt.Errorf("No such image: %s (tag: %s)", config.Image, tag) + return "", nil, fmt.Errorf("No such image: %s (tag: %s)", config.Image, tag) } - return "", err + return "", nil, err } srv.LogEvent("create", container.ShortID(), srv.runtime.repositories.ImageName(container.Image)) - return container.ShortID(), nil + return container.ShortID(), buildWarnings, nil } func (srv *Server) ContainerRestart(name string, t int) error { @@ -951,7 +963,34 @@ func (srv *Server) ContainerRestart(name string, t int) error { return nil } -func (srv *Server) ContainerDestroy(name string, removeVolume bool) error { +func (srv *Server) ContainerDestroy(name string, removeVolume, removeLink bool) error { + if removeLink { + p := name + if p[0] != '/' { + p = "/" + p + } + parent, n := path.Split(p) + l := len(parent) + if parent[l-1] == '/' { + parent = parent[:l-1] + } + + pe := srv.runtime.containerGraph.Get(parent) + parentContainer := srv.runtime.Get(pe.ID()) + + if parentContainer != nil && parentContainer.activeLinks != nil { + if link, exists := parentContainer.activeLinks[n]; exists { + link.Disable() + } else { + utils.Debugf("Could not find active link for %s", name) + } + } + + if err := srv.runtime.containerGraph.Delete(name); err != nil { + return err + } + return nil + } if container := srv.runtime.Get(name); container != nil { if container.State.Running { return fmt.Errorf("Impossible to remove a running container, please stop it first") @@ -1143,14 +1182,32 @@ func (srv *Server) ImageGetCached(imgID string, config *Config) (*Image, error) } func (srv *Server) ContainerStart(name string, hostConfig *HostConfig) error { - if container := srv.runtime.Get(name); container != nil { - if err := container.Start(hostConfig); err != nil { - return fmt.Errorf("Error starting container %s: %s", name, err) - } - srv.LogEvent("start", container.ShortID(), srv.runtime.repositories.ImageName(container.Image)) - } else { + runtime := srv.runtime + container := runtime.Get(name) + if container == nil { return fmt.Errorf("No such container: %s", name) } + + // Register links + if hostConfig != nil && hostConfig.Links != nil { + for _, l := range hostConfig.Links { + parts, err := parseLink(l) + if err != nil { + return err + } + + childName := parts["name"] + if err := runtime.Link(fmt.Sprintf("/%s", container.ID), childName, parts["alias"]); err != nil { + return err + } + } + } + + if err := container.Start(hostConfig); err != nil { + return fmt.Errorf("Error starting container %s: %s", name, err) + } + srv.LogEvent("start", container.ShortID(), runtime.repositories.ImageName(container.Image)) + return nil } @@ -1302,17 +1359,16 @@ func (srv *Server) ContainerCopy(name string, resource string, out io.Writer) er } -func NewServer(flGraphPath string, autoRestart, enableCors bool, dns ListOpts) (*Server, error) { +func NewServer(config *DaemonConfig) (*Server, error) { if runtime.GOARCH != "amd64" { log.Fatalf("The docker runtime currently only supports amd64 (not %s). This will change in the future. Aborting.", runtime.GOARCH) } - runtime, err := NewRuntime(flGraphPath, autoRestart, dns) + runtime, err := NewRuntime(config) if err != nil { return nil, err } srv := &Server{ runtime: runtime, - enableCors: enableCors, pullingPool: make(map[string]struct{}), pushingPool: make(map[string]struct{}), events: make([]utils.JSONMessage, 0, 64), //only keeps the 64 last events @@ -1350,7 +1406,6 @@ func (srv *Server) LogEvent(action, id, from string) { type Server struct { sync.Mutex runtime *Runtime - enableCors bool pullingPool map[string]struct{} pushingPool map[string]struct{} events []utils.JSONMessage diff --git a/server_test.go b/server_test.go index c13754b5e..4639177ad 100644 --- a/server_test.go +++ b/server_test.go @@ -89,7 +89,7 @@ func TestCreateRm(t *testing.T) { t.Fatal(err) } - id, err := srv.ContainerCreate(config) + id, _, err := srv.ContainerCreate(config) if err != nil { t.Fatal(err) } @@ -98,7 +98,7 @@ func TestCreateRm(t *testing.T) { t.Errorf("Expected 1 container, %v found", len(runtime.List())) } - if err = srv.ContainerDestroy(id, true); err != nil { + if err = srv.ContainerDestroy(id, true, false); err != nil { t.Fatal(err) } @@ -119,7 +119,7 @@ func TestCreateRmVolumes(t *testing.T) { t.Fatal(err) } - id, err := srv.ContainerCreate(config) + id, _, err := srv.ContainerCreate(config) if err != nil { t.Fatal(err) } @@ -138,7 +138,7 @@ func TestCreateRmVolumes(t *testing.T) { t.Fatal(err) } - if err = srv.ContainerDestroy(id, true); err != nil { + if err = srv.ContainerDestroy(id, true, false); err != nil { t.Fatal(err) } @@ -158,7 +158,7 @@ func TestCommit(t *testing.T) { t.Fatal(err) } - id, err := srv.ContainerCreate(config) + id, _, err := srv.ContainerCreate(config) if err != nil { t.Fatal(err) } @@ -179,7 +179,7 @@ func TestCreateStartRestartStopStartKillRm(t *testing.T) { t.Fatal(err) } - id, err := srv.ContainerCreate(config) + id, _, err := srv.ContainerCreate(config) if err != nil { t.Fatal(err) } @@ -214,7 +214,7 @@ func TestCreateStartRestartStopStartKillRm(t *testing.T) { } // FIXME: this failed once with a race condition ("Unable to remove filesystem for xxx: directory not empty") - if err = srv.ContainerDestroy(id, true); err != nil { + if err = srv.ContainerDestroy(id, true, false); err != nil { t.Fatal(err) } @@ -230,7 +230,7 @@ func TestRunWithTooLowMemoryLimit(t *testing.T) { srv := &Server{runtime: runtime} defer nuke(runtime) // Try to create a container with a memory limit of 1 byte less than the minimum allowed limit. - _, err = srv.ContainerCreate( + _, _, err = srv.ContainerCreate( &Config{ Image: GetTestImage(runtime).ID, Memory: 524287, @@ -402,7 +402,7 @@ func TestRmi(t *testing.T) { t.Fatal(err) } - containerID, err := srv.ContainerCreate(config) + containerID, _, err := srv.ContainerCreate(config) if err != nil { t.Fatal(err) } @@ -423,7 +423,7 @@ func TestRmi(t *testing.T) { t.Fatal(err) } - containerID, err = srv.ContainerCreate(config) + containerID, _, err = srv.ContainerCreate(config) if err != nil { t.Fatal(err) } diff --git a/sorter.go b/sorter.go index a81884148..ad931ebcb 100644 --- a/sorter.go +++ b/sorter.go @@ -34,3 +34,72 @@ func sortImagesByCreationAndTag(images []APIImages) { sort.Sort(sorter) } + +type portSorter struct { + ports []Port + by func(i, j Port) bool +} + +func (s *portSorter) Len() int { + return len(s.ports) +} + +func (s *portSorter) Swap(i, j int) { + s.ports[i], s.ports[j] = s.ports[j], s.ports[i] +} + +func (s *portSorter) Less(i, j int) bool { + ip := s.ports[i] + jp := s.ports[j] + + return s.by(ip, jp) +} + +func sortPorts(ports []Port, predicate func(i, j Port) bool) { + s := &portSorter{ports, predicate} + sort.Sort(s) +} + +type containerSorter struct { + containers []*Container + by func(i, j *Container) bool +} + +func (s *containerSorter) Len() int { + return len(s.containers) +} + +func (s *containerSorter) Swap(i, j int) { + s.containers[i], s.containers[j] = s.containers[j], s.containers[i] +} + +func (s *containerSorter) Less(i, j int) bool { + return s.by(s.containers[i], s.containers[j]) +} + +func sortContainers(containers []*Container, predicate func(i, j *Container) bool) { + s := &containerSorter{containers, predicate} + sort.Sort(s) +} + +type apiLinkSorter struct { + links []APILink + by func(i, j APILink) bool +} + +func (s *apiLinkSorter) Len() int { + return len(s.links) +} + +func (s *apiLinkSorter) Swap(i, j int) { + s.links[i], s.links[j] = s.links[j], s.links[i] +} + +func (s *apiLinkSorter) Less(i, j int) bool { + return s.by(s.links[i], s.links[j]) +} + +func sortLinks(links []APILink, predicate func(i, j APILink) bool) { + s := &apiLinkSorter{links, predicate} + sort.Sort(s) +} diff --git a/sorter_test.go b/sorter_test.go index 5519708ec..d61b1a711 100644 --- a/sorter_test.go +++ b/sorter_test.go @@ -1,6 +1,7 @@ package docker import ( + "fmt" "testing" ) @@ -55,3 +56,38 @@ func TestServerListOrderedImagesByCreationDateAndTag(t *testing.T) { t.Error("Expected []APIImges to be ordered by most recent creation date and tag name.") } } + +func TestSortUniquePorts(t *testing.T) { + ports := []Port{ + Port("6379/tcp"), + Port("22/tcp"), + } + + sortPorts(ports, func(ip, jp Port) bool { + return ip.Int() < jp.Int() || (ip.Int() == jp.Int() && ip.Proto() == "tcp") + }) + + first := ports[0] + if fmt.Sprint(first) != "22/tcp" { + t.Log(fmt.Sprint(first)) + t.Fail() + } +} + +func TestSortSamePortWithDifferentProto(t *testing.T) { + ports := []Port{ + Port("8888/tcp"), + Port("8888/udp"), + Port("6379/tcp"), + Port("6379/udp"), + } + + sortPorts(ports, func(ip, jp Port) bool { + return ip.Int() < jp.Int() || (ip.Int() == jp.Int() && ip.Proto() == "tcp") + }) + + first := ports[0] + if fmt.Sprint(first) != "6379/tcp" { + t.Fail() + } +} diff --git a/utils.go b/utils.go index 8d821e87f..45949cb35 100644 --- a/utils.go +++ b/utils.go @@ -2,6 +2,8 @@ package docker import ( "fmt" + "github.com/dotcloud/docker/utils" + "strconv" "strings" ) @@ -27,6 +29,7 @@ func CompareConfig(a, b *Config) bool { len(a.Dns) != len(b.Dns) || len(a.Env) != len(b.Env) || len(a.PortSpecs) != len(b.PortSpecs) || + len(a.ExposedPorts) != len(b.ExposedPorts) || len(a.Entrypoint) != len(b.Entrypoint) || len(a.Volumes) != len(b.Volumes) { return false @@ -52,6 +55,11 @@ func CompareConfig(a, b *Config) bool { return false } } + for k := range a.ExposedPorts { + if _, exists := b.ExposedPorts[k]; !exists { + return false + } + } for i := 0; i < len(a.Entrypoint); i++ { if a.Entrypoint[i] != b.Entrypoint[i] { return false @@ -78,26 +86,38 @@ func MergeConfig(userConf, imageConf *Config) error { if userConf.CpuShares == 0 { userConf.CpuShares = imageConf.CpuShares } - if userConf.PortSpecs == nil || len(userConf.PortSpecs) == 0 { - userConf.PortSpecs = imageConf.PortSpecs - } else { - for _, imagePortSpec := range imageConf.PortSpecs { - found := false - imageNat, err := parseNat(imagePortSpec) - if err != nil { - return err + if userConf.ExposedPorts == nil || len(userConf.ExposedPorts) == 0 { + userConf.ExposedPorts = imageConf.ExposedPorts + } + + if userConf.PortSpecs != nil && len(userConf.PortSpecs) > 0 { + if userConf.ExposedPorts == nil { + userConf.ExposedPorts = make(map[Port]struct{}) + } + ports, _, err := parsePortSpecs(userConf.PortSpecs) + if err != nil { + return err + } + for port := range ports { + if _, exists := userConf.ExposedPorts[port]; !exists { + userConf.ExposedPorts[port] = struct{}{} } - for _, userPortSpec := range userConf.PortSpecs { - userNat, err := parseNat(userPortSpec) - if err != nil { - return err - } - if imageNat.Proto == userNat.Proto && imageNat.Backend == userNat.Backend { - found = true - } - } - if !found { - userConf.PortSpecs = append(userConf.PortSpecs, imagePortSpec) + } + userConf.PortSpecs = nil + } + if imageConf.PortSpecs != nil && len(imageConf.PortSpecs) > 0 { + utils.Debugf("Migrating image port specs to containter: %s", strings.Join(imageConf.PortSpecs, ", ")) + if userConf.ExposedPorts == nil { + userConf.ExposedPorts = make(map[Port]struct{}) + } + + ports, _, err := parsePortSpecs(imageConf.PortSpecs) + if err != nil { + return err + } + for port := range ports { + if _, exists := userConf.ExposedPorts[port]; !exists { + userConf.ExposedPorts[port] = struct{}{} } } } @@ -174,3 +194,98 @@ func parseLxcOpt(opt string) (string, string, error) { } return strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]), nil } + +// We will receive port specs in the format of ip:public:private/proto and these need to be +// parsed in the internal types +func parsePortSpecs(ports []string) (map[Port]struct{}, map[Port][]PortBinding, error) { + exposedPorts := make(map[Port]struct{}, len(ports)) + bindings := make(map[Port][]PortBinding) + + for _, rawPort := range ports { + proto := "tcp" + if i := strings.LastIndex(rawPort, "/"); i != -1 { + proto = rawPort[i+1:] + rawPort = rawPort[:i] + } + if !strings.Contains(rawPort, ":") { + rawPort = fmt.Sprintf("::%s", rawPort) + } else if len(strings.Split(rawPort, ":")) == 2 { + rawPort = fmt.Sprintf(":%s", rawPort) + } + + parts, err := utils.PartParser("ip:hostPort:containerPort", rawPort) + if err != nil { + return nil, nil, err + } + containerPort := parts["containerPort"] + rawIp := parts["ip"] + hostPort := parts["hostPort"] + + if containerPort == "" { + return nil, nil, fmt.Errorf("No port specified: %s", rawPort) + } + + port := NewPort(proto, containerPort) + if _, exists := exposedPorts[port]; !exists { + exposedPorts[port] = struct{}{} + } + + binding := PortBinding{ + HostIp: rawIp, + HostPort: hostPort, + } + bslice, exists := bindings[port] + if !exists { + bslice = []PortBinding{} + } + bindings[port] = append(bslice, binding) + } + return exposedPorts, bindings, nil +} + +// Splits a port in the format of port/proto +func splitProtoPort(rawPort string) (string, string) { + parts := strings.Split(rawPort, "/") + l := len(parts) + if l == 0 { + return "", "" + } + if l == 1 { + return "tcp", rawPort + } + return parts[0], parts[1] +} + +func parsePort(rawPort string) (int, error) { + port, err := strconv.ParseUint(rawPort, 10, 16) + if err != nil { + return 0, err + } + return int(port), nil +} + +func migratePortMappings(config *Config) error { + if config.PortSpecs != nil { + // We don't have to worry about migrating the bindings to the host + // This is our breaking change + ports, _, err := parsePortSpecs(config.PortSpecs) + if err != nil { + return err + } + config.PortSpecs = nil + + if config.ExposedPorts == nil { + config.ExposedPorts = make(map[Port]struct{}, len(ports)) + } + for k, v := range ports { + config.ExposedPorts[k] = v + } + } + return nil +} + +// Links come in the format of +// name:alias +func parseLink(rawLink string) (map[string]string, error) { + return utils.PartParser("name:alias", rawLink) +} diff --git a/utils/utils.go b/utils/utils.go index b0327dd40..bddf376b3 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -1038,3 +1038,22 @@ func IsClosedError(err error) bool { */ return strings.HasSuffix(err.Error(), "use of closed network connection") } + +func PartParser(template, data string) (map[string]string, error) { + // ip:public:private + templateParts := strings.Split(template, ":") + parts := strings.Split(data, ":") + if len(parts) != len(templateParts) { + return nil, fmt.Errorf("Invalid format to parse. %s should match template %s", data, template) + } + out := make(map[string]string, len(templateParts)) + + for i, t := range templateParts { + value := "" + if len(parts) > i { + value = parts[i] + } + out[t] = value + } + return out, nil +} diff --git a/utils/utils_test.go b/utils/utils_test.go index 9a55e7f62..a5fef4e28 100644 --- a/utils/utils_test.go +++ b/utils/utils_test.go @@ -421,3 +421,23 @@ func TestDependencyGraph(t *testing.T) { t.Fatalf("Expected [d], found %v instead", res[2]) } } + +func TestParsePortMapping(t *testing.T) { + data, err := PartParser("ip:public:private", "192.168.1.1:80:8080") + if err != nil { + t.Fatal(err) + } + + if len(data) != 3 { + t.FailNow() + } + if data["ip"] != "192.168.1.1" { + t.Fail() + } + if data["public"] != "80" { + t.Fail() + } + if data["private"] != "8080" { + t.Fail() + } +} diff --git a/utils_test.go b/utils_test.go index 3f99de570..d9efd9039 100644 --- a/utils_test.go +++ b/utils_test.go @@ -66,7 +66,11 @@ func newTestRuntime(prefix string) (runtime *Runtime, err error) { return nil, err } - runtime, err = NewRuntimeFromDirectory(root, false) + config := &DaemonConfig{ + GraphPath: root, + AutoRestart: false, + } + runtime, err = NewRuntimeFromDirectory(config) if err != nil { return nil, err } @@ -125,7 +129,7 @@ func mkContainer(r *Runtime, args []string, t *testing.T) (*Container, *HostConf if config.Image == "_" { config.Image = GetTestImage(r).ID } - c, err := r.Create(config) + c, _, err := r.Create(config) if err != nil { return nil, nil, err } @@ -253,12 +257,12 @@ func TestMergeConfig(t *testing.T) { } } - if len(configUser.PortSpecs) != 3 { - t.Fatalf("Expected 3 portSpecs, 1111:1111, 3333:2222 and 3333:3333, found %d", len(configUser.PortSpecs)) + if len(configUser.ExposedPorts) != 3 { + t.Fatalf("Expected 3 portSpecs, 1111, 2222 and 3333, found %d", len(configUser.PortSpecs)) } - for _, portSpecs := range configUser.PortSpecs { - if portSpecs != "1111:1111" && portSpecs != "3333:2222" && portSpecs != "3333:3333" { - t.Fatalf("Expected 1111:1111 or 3333:2222 or 3333:3333, found %s", portSpecs) + for portSpecs := range configUser.ExposedPorts { + if portSpecs.Port() != "1111" && portSpecs.Port() != "2222" && portSpecs.Port() != "3333" { + t.Fatalf("Expected 1111 or 2222 or 3333, found %s", portSpecs) } } if len(configUser.Env) != 3 { @@ -284,48 +288,6 @@ func TestMergeConfig(t *testing.T) { } } -func TestMergeConfigPublicPortNotHonored(t *testing.T) { - volumesImage := make(map[string]struct{}) - volumesImage["/test1"] = struct{}{} - volumesImage["/test2"] = struct{}{} - configImage := &Config{ - Dns: []string{"1.1.1.1", "2.2.2.2"}, - PortSpecs: []string{"1111", "2222"}, - Env: []string{"VAR1=1", "VAR2=2"}, - Volumes: volumesImage, - } - - volumesUser := make(map[string]struct{}) - volumesUser["/test3"] = struct{}{} - configUser := &Config{ - Dns: []string{"3.3.3.3"}, - PortSpecs: []string{"1111:3333"}, - Env: []string{"VAR2=3", "VAR3=3"}, - Volumes: volumesUser, - } - - MergeConfig(configUser, configImage) - - contains := func(a []string, expect string) bool { - for _, p := range a { - if p == expect { - return true - } - } - return false - } - - if !contains(configUser.PortSpecs, "2222") { - t.Logf("Expected '2222' Ports: %v", configUser.PortSpecs) - t.Fail() - } - - if !contains(configUser.PortSpecs, "1111:3333") { - t.Logf("Expected '1111:3333' Ports: %v", configUser.PortSpecs) - t.Fail() - } -} - func TestParseLxcConfOpt(t *testing.T) { opts := []string{"lxc.utsname=docker", "lxc.utsname = docker "} @@ -342,3 +304,129 @@ func TestParseLxcConfOpt(t *testing.T) { } } } + +func TestParseNetworkOptsPrivateOnly(t *testing.T) { + ports, bindings, err := parsePortSpecs([]string{"192.168.1.100::80"}) + if err != nil { + t.Fatal(err) + } + if len(ports) != 1 { + t.Logf("Expected 1 got %d", len(ports)) + t.FailNow() + } + if len(bindings) != 1 { + t.Logf("Expected 1 got %d", len(bindings)) + t.FailNow() + } + for k := range ports { + if k.Proto() != "tcp" { + t.Logf("Expected tcp got %s", k.Proto()) + t.Fail() + } + if k.Port() != "80" { + t.Logf("Expected 80 got %s", k.Port()) + t.Fail() + } + b, exists := bindings[k] + if !exists { + t.Log("Binding does not exist") + t.FailNow() + } + if len(b) != 1 { + t.Logf("Expected 1 got %d", len(b)) + t.FailNow() + } + s := b[0] + if s.HostPort != "" { + t.Logf("Expected \"\" got %s", s.HostPort) + t.Fail() + } + if s.HostIp != "192.168.1.100" { + t.Fail() + } + } +} + +func TestParseNetworkOptsPublic(t *testing.T) { + ports, bindings, err := parsePortSpecs([]string{"192.168.1.100:8080:80"}) + if err != nil { + t.Fatal(err) + } + if len(ports) != 1 { + t.Logf("Expected 1 got %d", len(ports)) + t.FailNow() + } + if len(bindings) != 1 { + t.Logf("Expected 1 got %d", len(bindings)) + t.FailNow() + } + for k := range ports { + if k.Proto() != "tcp" { + t.Logf("Expected tcp got %s", k.Proto()) + t.Fail() + } + if k.Port() != "80" { + t.Logf("Expected 80 got %s", k.Port()) + t.Fail() + } + b, exists := bindings[k] + if !exists { + t.Log("Binding does not exist") + t.FailNow() + } + if len(b) != 1 { + t.Logf("Expected 1 got %d", len(b)) + t.FailNow() + } + s := b[0] + if s.HostPort != "8080" { + t.Logf("Expected 8080 got %s", s.HostPort) + t.Fail() + } + if s.HostIp != "192.168.1.100" { + t.Fail() + } + } +} + +func TestParseNetworkOptsUdp(t *testing.T) { + ports, bindings, err := parsePortSpecs([]string{"192.168.1.100::6000/udp"}) + if err != nil { + t.Fatal(err) + } + if len(ports) != 1 { + t.Logf("Expected 1 got %d", len(ports)) + t.FailNow() + } + if len(bindings) != 1 { + t.Logf("Expected 1 got %d", len(bindings)) + t.FailNow() + } + for k := range ports { + if k.Proto() != "udp" { + t.Logf("Expected udp got %s", k.Proto()) + t.Fail() + } + if k.Port() != "6000" { + t.Logf("Expected 6000 got %s", k.Port()) + t.Fail() + } + b, exists := bindings[k] + if !exists { + t.Log("Binding does not exist") + t.FailNow() + } + if len(b) != 1 { + t.Logf("Expected 1 got %d", len(b)) + t.FailNow() + } + s := b[0] + if s.HostPort != "" { + t.Logf("Expected \"\" got %s", s.HostPort) + t.Fail() + } + if s.HostIp != "192.168.1.100" { + t.Fail() + } + } +} diff --git a/vendor/src/code.google.com/p/gosqlite/sqlite/sqlite.go b/vendor/src/code.google.com/p/gosqlite/sqlite/sqlite.go new file mode 100644 index 000000000..d2fbb6202 --- /dev/null +++ b/vendor/src/code.google.com/p/gosqlite/sqlite/sqlite.go @@ -0,0 +1,404 @@ +// 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. + +// Package sqlite provides access to the SQLite library, version 3. +package sqlite + +/* +#cgo LDFLAGS: -lsqlite3 + +#include +#include + +// These wrappers are necessary because SQLITE_TRANSIENT +// is a pointer constant, and cgo doesn't translate them correctly. +// The definition in sqlite3.h is: +// +// typedef void (*sqlite3_destructor_type)(void*); +// #define SQLITE_STATIC ((sqlite3_destructor_type)0) +// #define SQLITE_TRANSIENT ((sqlite3_destructor_type)-1) + +static int my_bind_text(sqlite3_stmt *stmt, int n, char *p, int np) { + return sqlite3_bind_text(stmt, n, p, np, SQLITE_TRANSIENT); +} +static int my_bind_blob(sqlite3_stmt *stmt, int n, void *p, int np) { + return sqlite3_bind_blob(stmt, n, p, np, SQLITE_TRANSIENT); +} + +*/ +import "C" + +import ( + "errors" + "fmt" + "reflect" + "strconv" + "time" + "unsafe" +) + +type Errno int + +func (e Errno) Error() string { + s := errText[e] + if s == "" { + return fmt.Sprintf("errno %d", int(e)) + } + return s +} + +var ( + ErrError error = Errno(1) // /* SQL error or missing database */ + ErrInternal error = Errno(2) // /* Internal logic error in SQLite */ + ErrPerm error = Errno(3) // /* Access permission denied */ + ErrAbort error = Errno(4) // /* Callback routine requested an abort */ + ErrBusy error = Errno(5) // /* The database file is locked */ + ErrLocked error = Errno(6) // /* A table in the database is locked */ + ErrNoMem error = Errno(7) // /* A malloc() failed */ + ErrReadOnly error = Errno(8) // /* Attempt to write a readonly database */ + ErrInterrupt error = Errno(9) // /* Operation terminated by sqlite3_interrupt()*/ + ErrIOErr error = Errno(10) // /* Some kind of disk I/O error occurred */ + ErrCorrupt error = Errno(11) // /* The database disk image is malformed */ + ErrFull error = Errno(13) // /* Insertion failed because database is full */ + ErrCantOpen error = Errno(14) // /* Unable to open the database file */ + ErrEmpty error = Errno(16) // /* Database is empty */ + ErrSchema error = Errno(17) // /* The database schema changed */ + ErrTooBig error = Errno(18) // /* String or BLOB exceeds size limit */ + ErrConstraint error = Errno(19) // /* Abort due to constraint violation */ + ErrMismatch error = Errno(20) // /* Data type mismatch */ + ErrMisuse error = Errno(21) // /* Library used incorrectly */ + ErrNolfs error = Errno(22) // /* Uses OS features not supported on host */ + ErrAuth error = Errno(23) // /* Authorization denied */ + ErrFormat error = Errno(24) // /* Auxiliary database format error */ + ErrRange error = Errno(25) // /* 2nd parameter to sqlite3_bind out of range */ + ErrNotDB error = Errno(26) // /* File opened that is not a database file */ + Row = Errno(100) // /* sqlite3_step() has another row ready */ + Done = Errno(101) // /* sqlite3_step() has finished executing */ +) + +var errText = map[Errno]string{ + 1: "SQL error or missing database", + 2: "Internal logic error in SQLite", + 3: "Access permission denied", + 4: "Callback routine requested an abort", + 5: "The database file is locked", + 6: "A table in the database is locked", + 7: "A malloc() failed", + 8: "Attempt to write a readonly database", + 9: "Operation terminated by sqlite3_interrupt()*/", + 10: "Some kind of disk I/O error occurred", + 11: "The database disk image is malformed", + 12: "NOT USED. Table or record not found", + 13: "Insertion failed because database is full", + 14: "Unable to open the database file", + 15: "NOT USED. Database lock protocol error", + 16: "Database is empty", + 17: "The database schema changed", + 18: "String or BLOB exceeds size limit", + 19: "Abort due to constraint violation", + 20: "Data type mismatch", + 21: "Library used incorrectly", + 22: "Uses OS features not supported on host", + 23: "Authorization denied", + 24: "Auxiliary database format error", + 25: "2nd parameter to sqlite3_bind out of range", + 26: "File opened that is not a database file", + 100: "sqlite3_step() has another row ready", + 101: "sqlite3_step() has finished executing", +} + +func (c *Conn) error(rv C.int) error { + if c == nil || c.db == nil { + return errors.New("nil sqlite database") + } + if rv == 0 { + return nil + } + if rv == 21 { // misuse + return Errno(rv) + } + return errors.New(Errno(rv).Error() + ": " + C.GoString(C.sqlite3_errmsg(c.db))) +} + +type Conn struct { + db *C.sqlite3 +} + +func Version() string { + p := C.sqlite3_libversion() + return C.GoString(p) +} + +func Open(filename string) (*Conn, error) { + if C.sqlite3_threadsafe() == 0 { + return nil, errors.New("sqlite library was not compiled for thread-safe operation") + } + + var db *C.sqlite3 + name := C.CString(filename) + defer C.free(unsafe.Pointer(name)) + rv := C.sqlite3_open_v2(name, &db, + C.SQLITE_OPEN_FULLMUTEX| + C.SQLITE_OPEN_READWRITE| + C.SQLITE_OPEN_CREATE, + nil) + if rv != 0 { + return nil, Errno(rv) + } + if db == nil { + return nil, errors.New("sqlite succeeded without returning a database") + } + return &Conn{db}, nil +} + +func NewBackup(dst *Conn, dstTable string, src *Conn, srcTable string) (*Backup, error) { + dname := C.CString(dstTable) + sname := C.CString(srcTable) + defer C.free(unsafe.Pointer(dname)) + defer C.free(unsafe.Pointer(sname)) + + sb := C.sqlite3_backup_init(dst.db, dname, src.db, sname) + if sb == nil { + return nil, dst.error(C.sqlite3_errcode(dst.db)) + } + return &Backup{sb, dst, src}, nil +} + +type Backup struct { + sb *C.sqlite3_backup + dst, src *Conn +} + +func (b *Backup) Step(npage int) error { + rv := C.sqlite3_backup_step(b.sb, C.int(npage)) + if rv == 0 || Errno(rv) == ErrBusy || Errno(rv) == ErrLocked { + return nil + } + return Errno(rv) +} + +type BackupStatus struct { + Remaining int + PageCount int +} + +func (b *Backup) Status() BackupStatus { + return BackupStatus{int(C.sqlite3_backup_remaining(b.sb)), int(C.sqlite3_backup_pagecount(b.sb))} +} + +func (b *Backup) Run(npage int, period time.Duration, c chan<- BackupStatus) error { + var err error + for { + err = b.Step(npage) + if err != nil { + break + } + if c != nil { + c <- b.Status() + } + time.Sleep(period) + } + return b.dst.error(C.sqlite3_errcode(b.dst.db)) +} + +func (b *Backup) Close() error { + if b.sb == nil { + return errors.New("backup already closed") + } + C.sqlite3_backup_finish(b.sb) + b.sb = nil + return nil +} + +func (c *Conn) BusyTimeout(ms int) error { + rv := C.sqlite3_busy_timeout(c.db, C.int(ms)) + if rv == 0 { + return nil + } + return Errno(rv) +} + +func (c *Conn) Exec(cmd string, args ...interface{}) error { + s, err := c.Prepare(cmd) + if err != nil { + return err + } + defer s.Finalize() + err = s.Exec(args...) + if err != nil { + return err + } + rv := C.sqlite3_step(s.stmt) + if Errno(rv) != Done { + return c.error(rv) + } + return nil +} + +type Stmt struct { + c *Conn + stmt *C.sqlite3_stmt + err error + t0 time.Time + sql string + args string +} + +func (c *Conn) Prepare(cmd string) (*Stmt, error) { + if c == nil || c.db == nil { + return nil, errors.New("nil sqlite database") + } + cmdstr := C.CString(cmd) + defer C.free(unsafe.Pointer(cmdstr)) + var stmt *C.sqlite3_stmt + var tail *C.char + rv := C.sqlite3_prepare_v2(c.db, cmdstr, C.int(len(cmd)+1), &stmt, &tail) + if rv != 0 { + return nil, c.error(rv) + } + return &Stmt{c: c, stmt: stmt, sql: cmd, t0: time.Now()}, nil +} + +func (s *Stmt) Exec(args ...interface{}) error { + s.args = fmt.Sprintf(" %v", []interface{}(args)) + rv := C.sqlite3_reset(s.stmt) + if rv != 0 { + return s.c.error(rv) + } + + n := int(C.sqlite3_bind_parameter_count(s.stmt)) + if n != len(args) { + return errors.New(fmt.Sprintf("incorrect argument count for Stmt.Exec: have %d want %d", len(args), n)) + } + + for i, v := range args { + var str string + switch v := v.(type) { + case []byte: + var p *byte + if len(v) > 0 { + p = &v[0] + } + if rv := C.my_bind_blob(s.stmt, C.int(i+1), unsafe.Pointer(p), C.int(len(v))); rv != 0 { + return s.c.error(rv) + } + continue + + case bool: + if v { + str = "1" + } else { + str = "0" + } + + default: + str = fmt.Sprint(v) + } + + cstr := C.CString(str) + rv := C.my_bind_text(s.stmt, C.int(i+1), cstr, C.int(len(str))) + C.free(unsafe.Pointer(cstr)) + if rv != 0 { + return s.c.error(rv) + } + } + return nil +} + +func (s *Stmt) Error() error { + return s.err +} + +func (s *Stmt) Next() bool { + rv := C.sqlite3_step(s.stmt) + err := Errno(rv) + if err == Row { + return true + } + if err != Done { + s.err = s.c.error(rv) + } + return false +} + +func (s *Stmt) Reset() error { + C.sqlite3_reset(s.stmt) + return nil +} + +func (s *Stmt) Scan(args ...interface{}) error { + n := int(C.sqlite3_column_count(s.stmt)) + if n != len(args) { + return errors.New(fmt.Sprintf("incorrect argument count for Stmt.Scan: have %d want %d", len(args), n)) + } + + for i, v := range args { + n := C.sqlite3_column_bytes(s.stmt, C.int(i)) + p := C.sqlite3_column_blob(s.stmt, C.int(i)) + if p == nil && n > 0 { + return errors.New("got nil blob") + } + var data []byte + if n > 0 { + data = (*[1 << 30]byte)(unsafe.Pointer(p))[0:n] + } + switch v := v.(type) { + case *[]byte: + *v = data + case *string: + *v = string(data) + case *bool: + *v = string(data) == "1" + case *int: + x, err := strconv.Atoi(string(data)) + if err != nil { + return errors.New("arg " + strconv.Itoa(i) + " as int: " + err.Error()) + } + *v = x + case *int64: + x, err := strconv.ParseInt(string(data), 10, 64) + if err != nil { + return errors.New("arg " + strconv.Itoa(i) + " as int64: " + err.Error()) + } + *v = x + case *float64: + x, err := strconv.ParseFloat(string(data), 64) + if err != nil { + return errors.New("arg " + strconv.Itoa(i) + " as float64: " + err.Error()) + } + *v = x + default: + return errors.New("unsupported type in Scan: " + reflect.TypeOf(v).String()) + } + } + return nil +} + +func (s *Stmt) SQL() string { + return s.sql + s.args +} + +func (s *Stmt) Nanoseconds() int64 { + return time.Now().Sub(s.t0).Nanoseconds() +} + +func (s *Stmt) Finalize() error { + rv := C.sqlite3_finalize(s.stmt) + if rv != 0 { + return s.c.error(rv) + } + return nil +} + +func (c *Conn) Close() error { + if c == nil || c.db == nil { + return errors.New("nil sqlite database") + } + rv := C.sqlite3_close(c.db) + if rv != 0 { + return c.error(rv) + } + c.db = nil + return nil +} diff --git a/vendor/src/code.google.com/p/gosqlite/sqlite3/driver.go b/vendor/src/code.google.com/p/gosqlite/sqlite3/driver.go new file mode 100644 index 000000000..982e08ec0 --- /dev/null +++ b/vendor/src/code.google.com/p/gosqlite/sqlite3/driver.go @@ -0,0 +1,498 @@ +// 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. + +// Package sqlite3 provides access to the SQLite library, version 3. +// +// The package has no exported API. +// It registers a driver for the standard Go database/sql package. +// +// import _ "code.google.com/p/gosqlite/sqlite3" +// +// (For an alternate, earlier API, see the code.google.com/p/gosqlite/sqlite package.) +package sqlite + +/* +#cgo LDFLAGS: -lsqlite3 + +#include +#include + +// These wrappers are necessary because SQLITE_TRANSIENT +// is a pointer constant, and cgo doesn't translate them correctly. +// The definition in sqlite3.h is: +// +// typedef void (*sqlite3_destructor_type)(void*); +// #define SQLITE_STATIC ((sqlite3_destructor_type)0) +// #define SQLITE_TRANSIENT ((sqlite3_destructor_type)-1) + +static int my_bind_text(sqlite3_stmt *stmt, int n, char *p, int np) { + return sqlite3_bind_text(stmt, n, p, np, SQLITE_TRANSIENT); +} +static int my_bind_blob(sqlite3_stmt *stmt, int n, void *p, int np) { + return sqlite3_bind_blob(stmt, n, p, np, SQLITE_TRANSIENT); +} + +*/ +import "C" + +import ( + "database/sql" + "database/sql/driver" + "errors" + "fmt" + "io" + "strings" + "time" + "unsafe" +) + +func init() { + sql.Register("sqlite3", impl{}) +} + +type errno int + +func (e errno) Error() string { + s := errText[e] + if s == "" { + return fmt.Sprintf("errno %d", int(e)) + } + return s +} + +var ( + errError error = errno(1) // /* SQL error or missing database */ + errInternal error = errno(2) // /* Internal logic error in SQLite */ + errPerm error = errno(3) // /* Access permission denied */ + errAbort error = errno(4) // /* Callback routine requested an abort */ + errBusy error = errno(5) // /* The database file is locked */ + errLocked error = errno(6) // /* A table in the database is locked */ + errNoMem error = errno(7) // /* A malloc() failed */ + errReadOnly error = errno(8) // /* Attempt to write a readonly database */ + errInterrupt error = errno(9) // /* Operation terminated by sqlite3_interrupt()*/ + errIOErr error = errno(10) // /* Some kind of disk I/O error occurred */ + errCorrupt error = errno(11) // /* The database disk image is malformed */ + errFull error = errno(13) // /* Insertion failed because database is full */ + errCantOpen error = errno(14) // /* Unable to open the database file */ + errEmpty error = errno(16) // /* Database is empty */ + errSchema error = errno(17) // /* The database schema changed */ + errTooBig error = errno(18) // /* String or BLOB exceeds size limit */ + errConstraint error = errno(19) // /* Abort due to constraint violation */ + errMismatch error = errno(20) // /* Data type mismatch */ + errMisuse error = errno(21) // /* Library used incorrectly */ + errNolfs error = errno(22) // /* Uses OS features not supported on host */ + errAuth error = errno(23) // /* Authorization denied */ + errFormat error = errno(24) // /* Auxiliary database format error */ + errRange error = errno(25) // /* 2nd parameter to sqlite3_bind out of range */ + errNotDB error = errno(26) // /* File opened that is not a database file */ + stepRow = errno(100) // /* sqlite3_step() has another row ready */ + stepDone = errno(101) // /* sqlite3_step() has finished executing */ +) + +var errText = map[errno]string{ + 1: "SQL error or missing database", + 2: "Internal logic error in SQLite", + 3: "Access permission denied", + 4: "Callback routine requested an abort", + 5: "The database file is locked", + 6: "A table in the database is locked", + 7: "A malloc() failed", + 8: "Attempt to write a readonly database", + 9: "Operation terminated by sqlite3_interrupt()*/", + 10: "Some kind of disk I/O error occurred", + 11: "The database disk image is malformed", + 12: "NOT USED. Table or record not found", + 13: "Insertion failed because database is full", + 14: "Unable to open the database file", + 15: "NOT USED. Database lock protocol error", + 16: "Database is empty", + 17: "The database schema changed", + 18: "String or BLOB exceeds size limit", + 19: "Abort due to constraint violation", + 20: "Data type mismatch", + 21: "Library used incorrectly", + 22: "Uses OS features not supported on host", + 23: "Authorization denied", + 24: "Auxiliary database format error", + 25: "2nd parameter to sqlite3_bind out of range", + 26: "File opened that is not a database file", + 100: "sqlite3_step() has another row ready", + 101: "sqlite3_step() has finished executing", +} + +type impl struct{} + +func (impl) Open(name string) (driver.Conn, error) { + if C.sqlite3_threadsafe() == 0 { + return nil, errors.New("sqlite library was not compiled for thread-safe operation") + } + + var db *C.sqlite3 + cname := C.CString(name) + defer C.free(unsafe.Pointer(cname)) + rv := C.sqlite3_open_v2(cname, &db, + C.SQLITE_OPEN_FULLMUTEX| + C.SQLITE_OPEN_READWRITE| + C.SQLITE_OPEN_CREATE, + nil) + if rv != 0 { + return nil, errno(rv) + } + if db == nil { + return nil, errors.New("sqlite succeeded without returning a database") + } + return &conn{db: db}, nil +} + +type conn struct { + db *C.sqlite3 + closed bool + tx bool +} + +func (c *conn) error(rv C.int) error { + if rv == 0 { + return nil + } + if rv == 21 || c.closed { + return errno(rv) + } + return errors.New(errno(rv).Error() + ": " + C.GoString(C.sqlite3_errmsg(c.db))) +} + +func (c *conn) Prepare(cmd string) (driver.Stmt, error) { + if c.closed { + panic("database/sql/driver: misuse of sqlite driver: Prepare after Close") + } + cmdstr := C.CString(cmd) + defer C.free(unsafe.Pointer(cmdstr)) + var s *C.sqlite3_stmt + var tail *C.char + rv := C.sqlite3_prepare_v2(c.db, cmdstr, C.int(len(cmd)+1), &s, &tail) + if rv != 0 { + return nil, c.error(rv) + } + return &stmt{c: c, stmt: s, sql: cmd, t0: time.Now()}, nil +} + +func (c *conn) Close() error { + if c.closed { + panic("database/sql/driver: misuse of sqlite driver: multiple Close") + } + c.closed = true + rv := C.sqlite3_close(c.db) + c.db = nil + return c.error(rv) +} + +func (c *conn) exec(cmd string) error { + cstring := C.CString(cmd) + defer C.free(unsafe.Pointer(cstring)) + rv := C.sqlite3_exec(c.db, cstring, nil, nil, nil) + return c.error(rv) +} + +func (c *conn) Begin() (driver.Tx, error) { + if c.tx { + panic("database/sql/driver: misuse of sqlite driver: multiple Tx") + } + if err := c.exec("BEGIN TRANSACTION"); err != nil { + return nil, err + } + c.tx = true + return &tx{c}, nil +} + +type tx struct { + c *conn +} + +func (t *tx) Commit() error { + if t.c == nil || !t.c.tx { + panic("database/sql/driver: misuse of sqlite driver: extra Commit") + } + t.c.tx = false + err := t.c.exec("COMMIT TRANSACTION") + t.c = nil + return err +} + +func (t *tx) Rollback() error { + if t.c == nil || !t.c.tx { + panic("database/sql/driver: misuse of sqlite driver: extra Rollback") + } + t.c.tx = false + err := t.c.exec("ROLLBACK") + t.c = nil + return err +} + +type stmt struct { + c *conn + stmt *C.sqlite3_stmt + err error + t0 time.Time + sql string + args string + closed bool + rows bool + colnames []string + coltypes []string +} + +func (s *stmt) Close() error { + if s.rows { + panic("database/sql/driver: misuse of sqlite driver: Close with active Rows") + } + if s.closed { + panic("database/sql/driver: misuse of sqlite driver: double Close of Stmt") + } + s.closed = true + rv := C.sqlite3_finalize(s.stmt) + if rv != 0 { + return s.c.error(rv) + } + return nil +} + +func (s *stmt) NumInput() int { + if s.closed { + panic("database/sql/driver: misuse of sqlite driver: NumInput after Close") + } + return int(C.sqlite3_bind_parameter_count(s.stmt)) +} + +func (s *stmt) reset() error { + return s.c.error(C.sqlite3_reset(s.stmt)) +} + +func (s *stmt) start(args []driver.Value) error { + if err := s.reset(); err != nil { + return err + } + + n := int(C.sqlite3_bind_parameter_count(s.stmt)) + if n != len(args) { + return fmt.Errorf("incorrect argument count for command: have %d want %d", len(args), n) + } + + for i, v := range args { + var str string + switch v := v.(type) { + case nil: + if rv := C.sqlite3_bind_null(s.stmt, C.int(i+1)); rv != 0 { + return s.c.error(rv) + } + continue + + case float64: + if rv := C.sqlite3_bind_double(s.stmt, C.int(i+1), C.double(v)); rv != 0 { + return s.c.error(rv) + } + continue + + case int64: + if rv := C.sqlite3_bind_int64(s.stmt, C.int(i+1), C.sqlite3_int64(v)); rv != 0 { + return s.c.error(rv) + } + continue + + case []byte: + var p *byte + if len(v) > 0 { + p = &v[0] + } + if rv := C.my_bind_blob(s.stmt, C.int(i+1), unsafe.Pointer(p), C.int(len(v))); rv != 0 { + return s.c.error(rv) + } + continue + + case bool: + var vi int64 + if v { + vi = 1 + } + if rv := C.sqlite3_bind_int64(s.stmt, C.int(i+1), C.sqlite3_int64(vi)); rv != 0 { + return s.c.error(rv) + } + continue + + case time.Time: + str = v.UTC().Format(timefmt[0]) + + case string: + str = v + + default: + str = fmt.Sprint(v) + } + + cstr := C.CString(str) + rv := C.my_bind_text(s.stmt, C.int(i+1), cstr, C.int(len(str))) + C.free(unsafe.Pointer(cstr)) + if rv != 0 { + return s.c.error(rv) + } + } + + return nil +} + +func (s *stmt) Exec(args []driver.Value) (driver.Result, error) { + if s.closed { + panic("database/sql/driver: misuse of sqlite driver: Exec after Close") + } + if s.rows { + panic("database/sql/driver: misuse of sqlite driver: Exec with active Rows") + } + + err := s.start(args) + if err != nil { + return nil, err + } + + rv := C.sqlite3_step(s.stmt) + if errno(rv) != stepDone { + if rv == 0 { + rv = 21 // errMisuse + } + return nil, s.c.error(rv) + } + + id := int64(C.sqlite3_last_insert_rowid(s.c.db)) + rows := int64(C.sqlite3_changes(s.c.db)) + return &result{id, rows}, nil +} + +func (s *stmt) Query(args []driver.Value) (driver.Rows, error) { + if s.closed { + panic("database/sql/driver: misuse of sqlite driver: Query after Close") + } + if s.rows { + panic("database/sql/driver: misuse of sqlite driver: Query with active Rows") + } + + err := s.start(args) + if err != nil { + return nil, err + } + + s.rows = true + if s.colnames == nil { + n := int64(C.sqlite3_column_count(s.stmt)) + s.colnames = make([]string, n) + s.coltypes = make([]string, n) + for i := range s.colnames { + s.colnames[i] = C.GoString(C.sqlite3_column_name(s.stmt, C.int(i))) + s.coltypes[i] = strings.ToLower(C.GoString(C.sqlite3_column_decltype(s.stmt, C.int(i)))) + } + } + return &rows{s}, nil +} + +type rows struct { + s *stmt +} + +func (r *rows) Columns() []string { + if r.s == nil { + panic("database/sql/driver: misuse of sqlite driver: Columns of closed Rows") + } + return r.s.colnames +} + +const maxslice = 1<<31 - 1 + +var timefmt = []string{ + "2006-01-02 15:04:05.999999999", + "2006-01-02T15:04:05.999999999", + "2006-01-02 15:04:05", + "2006-01-02T15:04:05", + "2006-01-02 15:04", + "2006-01-02T15:04", + "2006-01-02", +} + +func (r *rows) Next(dst []driver.Value) error { + if r.s == nil { + panic("database/sql/driver: misuse of sqlite driver: Next of closed Rows") + } + + rv := C.sqlite3_step(r.s.stmt) + if errno(rv) != stepRow { + if errno(rv) == stepDone { + return io.EOF + } + if rv == 0 { + rv = 21 + } + return r.s.c.error(rv) + } + + for i := range dst { + switch typ := C.sqlite3_column_type(r.s.stmt, C.int(i)); typ { + default: + return fmt.Errorf("unexpected sqlite3 column type %d", typ) + case C.SQLITE_INTEGER: + val := int64(C.sqlite3_column_int64(r.s.stmt, C.int(i))) + switch r.s.coltypes[i] { + case "timestamp", "datetime": + dst[i] = time.Unix(val, 0).UTC() + case "boolean": + dst[i] = val > 0 + default: + dst[i] = val + } + + case C.SQLITE_FLOAT: + dst[i] = float64(C.sqlite3_column_double(r.s.stmt, C.int(i))) + + case C.SQLITE_BLOB, C.SQLITE_TEXT: + n := int(C.sqlite3_column_bytes(r.s.stmt, C.int(i))) + var b []byte + if n > 0 { + p := C.sqlite3_column_blob(r.s.stmt, C.int(i)) + b = (*[maxslice]byte)(unsafe.Pointer(p))[:n] + } + dst[i] = b + switch r.s.coltypes[i] { + case "timestamp", "datetime": + dst[i] = time.Time{} + s := string(b) + for _, f := range timefmt { + if t, err := time.Parse(f, s); err == nil { + dst[i] = t + break + } + } + } + + case C.SQLITE_NULL: + dst[i] = nil + } + } + return nil +} + +func (r *rows) Close() error { + if r.s == nil { + panic("database/sql/driver: misuse of sqlite driver: Close of closed Rows") + } + r.s.rows = false + r.s = nil + return nil +} + +type result struct { + id int64 + rows int64 +} + +func (r *result) LastInsertId() (int64, error) { + return r.id, nil +} + +func (r *result) RowsAffected() (int64, error) { + return r.rows, nil +} From 6f8d4793245ce3f3e49d9584a11a86f79a0c0fa8 Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Wed, 9 Oct 2013 18:28:17 -0700 Subject: [PATCH 02/17] Remove docker ls from docs and cli usage --- commands.go | 1 - docs/sources/examples/linking_into_redis.rst | 16 +--------------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/commands.go b/commands.go index d69ed9319..22f220319 100644 --- a/commands.go +++ b/commands.go @@ -98,7 +98,6 @@ func (cli *DockerCli) CmdHelp(args ...string) error { {"kill", "Kill a running container"}, {"login", "Register or Login to the docker registry server"}, {"logs", "Fetch the logs of a container"}, - {"ls", "List links for containers"}, {"port", "Lookup the public-facing port which is NAT-ed to PRIVATE_PORT"}, {"top", "Lookup the running processes of a container"}, {"ps", "List containers"}, diff --git a/docs/sources/examples/linking_into_redis.rst b/docs/sources/examples/linking_into_redis.rst index 6c9b7a4ff..1712e62d6 100644 --- a/docs/sources/examples/linking_into_redis.rst +++ b/docs/sources/examples/linking_into_redis.rst @@ -58,20 +58,12 @@ Run the redis container This will run our redis container using the default port of 6379 and using as password to secure our service. Next we will link the redis container to -a new name using ``docker link`` and ``docker ls``. +a new name using ``docker link``. Linking an existing container ----------------------------- -.. code-block:: bash - - docker ls - - NAME ID IMAGE - /39588b6a45100ef5b328b2c302ea085624f29e6cbab70f88be04793af02cec89 39588b6a45100ef5b328b2c302ea085624f29e6cbab70f88be04793af02cec89 crosbymichael/redis:latest - - Docker will automatically create an initial link with the container's id but because the is long and not very user friendly we can link the container with a new name. @@ -80,12 +72,6 @@ a new name. docker link /39588b6a45100ef5b328b2c302ea085624f29e6cbab70f88be04793af02cec89 /redis - docker ls - - NAME ID IMAGE - /redis 39588b6a45100ef5b328b2c302ea085624f29e6cbab70f88be04793af02cec89 crosbymichael/redis:latest - /39588b6a45100ef5b328b2c302ea085624f29e6cbab70f88be04793af02cec89 39588b6a45100ef5b328b2c302ea085624f29e6cbab70f88be04793af02cec89 crosbymichael/redis:latest - Now we can reference our running redis service using the friendly name ``/redis``. We can issue all the commands that you would expect; start, stop, attach, using the new name. From 7fff719cf3760ed92546f40c57044198c4635422 Mon Sep 17 00:00:00 2001 From: Solomon Hykes Date: Wed, 9 Oct 2013 19:44:31 -0700 Subject: [PATCH 03/17] gograph: fix a bug which caused the unicity of (parent, name) to not be enforced --- gograph/gograph.go | 5 +++++ gograph/gograph_test.go | 12 ++++++++++++ 2 files changed, 17 insertions(+) diff --git a/gograph/gograph.go b/gograph/gograph.go index 876bd6b58..28ce74081 100644 --- a/gograph/gograph.go +++ b/gograph/gograph.go @@ -22,7 +22,9 @@ const ( CONSTRAINT "parent_fk" FOREIGN KEY ("parent_id") REFERENCES "entity" ("id"), CONSTRAINT "entity_fk" FOREIGN KEY ("entity_id") REFERENCES "entity" ("id") ); + ` + createEdgeIndices = ` CREATE UNIQUE INDEX "name_parent_ix" ON "edge" (parent_id, name); ` ) @@ -67,6 +69,9 @@ func NewDatabase(dbPath string) (*Database, error) { 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") diff --git a/gograph/gograph_test.go b/gograph/gograph_test.go index 519b19969..f21d8bb85 100644 --- a/gograph/gograph_test.go +++ b/gograph/gograph_test.go @@ -59,6 +59,18 @@ func TestSetEntityWithDifferentName(t *testing.T) { } } +func TestSetDuplicateEntity(t *testing.T) { + db := newTestDb(t) + defer destroyTestDb(db) + + if _, err := db.Set("/foo", "42"); err != nil { + t.Fatal(err) + } + if _, err := db.Set("/foo", "43"); err == nil { + t.Fatalf("Creating an entry with a duplciate path did not cause an error") + } +} + func TestCreateChild(t *testing.T) { db := newTestDb(t) defer destroyTestDb(db) From 0fae30e8c30f22422109016ecbc426ac3cb565c7 Mon Sep 17 00:00:00 2001 From: Solomon Hykes Date: Wed, 9 Oct 2013 19:45:45 -0700 Subject: [PATCH 04/17] Typos and FIXMEs --- gograph/gograph.go | 2 ++ gograph/gograph_test.go | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/gograph/gograph.go b/gograph/gograph.go index 28ce74081..21f2966e2 100644 --- a/gograph/gograph.go +++ b/gograph/gograph.go @@ -104,9 +104,11 @@ func (db *Database) Set(fullPath, id string) (*Entity, error) { return nil, err } defer conn.Close() + // FIXME: is rollback implicit when closing the connection? rollback := func() { conn.Exec("ROLLBACK") } + // FIXME: use exclusive transactions to avoid race conditions if _, err := conn.Exec("BEGIN"); err != nil { return nil, err } diff --git a/gograph/gograph_test.go b/gograph/gograph_test.go index f21d8bb85..5c12128ca 100644 --- a/gograph/gograph_test.go +++ b/gograph/gograph_test.go @@ -22,7 +22,7 @@ func destroyTestDb(db *Database) { func TestNewDatabase(t *testing.T) { db := newTestDb(t) if db == nil { - t.Fatal("Datbase should not be nil") + t.Fatal("Database should not be nil") } defer destroyTestDb(db) } From 2b17d79e5b6867c67b03db59bf40c3db2b047c19 Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Thu, 10 Oct 2013 13:48:22 -0700 Subject: [PATCH 05/17] Add flag for inter-container communication --- config.go | 19 ++++++++++--------- docker/docker.go | 20 +++++++++++--------- network.go | 13 ++++++++++--- 3 files changed, 31 insertions(+), 21 deletions(-) diff --git a/config.go b/config.go index d32179056..8f2d22c25 100644 --- a/config.go +++ b/config.go @@ -5,13 +5,14 @@ import ( ) type DaemonConfig struct { - Pidfile string - GraphPath string - ProtoAddresses []string - AutoRestart bool - EnableCors bool - Dns []string - EnableIptables bool - BridgeIface string - DefaultIp net.IP + Pidfile string + GraphPath string + ProtoAddresses []string + AutoRestart bool + EnableCors bool + Dns []string + EnableIptables bool + BridgeIface string + DefaultIp net.IP + InterContainerCommunication bool } diff --git a/docker/docker.go b/docker/docker.go index e2f000786..c2e95da9d 100644 --- a/docker/docker.go +++ b/docker/docker.go @@ -40,6 +40,7 @@ func main() { flag.Var(&flHosts, "H", "tcp://host:port to bind/connect to or unix://path/to/socket to use") flEnableIptables := flag.Bool("iptables", true, "Disable iptables within docker") flDefaultIp := flag.String("ip", "0.0.0.0", "Default ip address to use when binding a containers ports") + flInterContainerComm := flag.Bool("enable-container-comm", false, "Enable inter-container communication") flag.Parse() @@ -76,15 +77,16 @@ func main() { ip := net.ParseIP(*flDefaultIp) config := &docker.DaemonConfig{ - Pidfile: *pidfile, - GraphPath: *flGraphPath, - AutoRestart: *flAutoRestart, - EnableCors: *flEnableCors, - Dns: dns, - EnableIptables: *flEnableIptables, - BridgeIface: bridge, - ProtoAddresses: flHosts, - DefaultIp: ip, + Pidfile: *pidfile, + GraphPath: *flGraphPath, + AutoRestart: *flAutoRestart, + EnableCors: *flEnableCors, + Dns: dns, + EnableIptables: *flEnableIptables, + BridgeIface: bridge, + ProtoAddresses: flHosts, + DefaultIp: ip, + InterContainerCommunication: *flInterContainerComm, } if err := daemon(config); err != nil { log.Fatal(err) diff --git a/network.go b/network.go index b480dc144..c3f3253ae 100644 --- a/network.go +++ b/network.go @@ -165,14 +165,21 @@ func CreateBridgeIface(config *DaemonConfig) error { if output, err := ip("link", "set", config.BridgeIface, "up"); err != nil { return fmt.Errorf("Unable to start network bridge: %s (%s)", err, output) } + if config.EnableIptables { if err := iptables.Raw("-t", "nat", "-A", "POSTROUTING", "-s", ifaceAddr, "!", "-d", ifaceAddr, "-j", "MASQUERADE"); err != nil { return fmt.Errorf("Unable to enable network bridge NAT: %s", err) } - // Prevent inter-container communication by default - if err := iptables.Raw("-A", "FORWARD", "-i", config.BridgeIface, "-o", config.BridgeIface, "-j", "DROP"); err != nil { - return fmt.Errorf("Unable to prevent intercontainer communication: %s", err) + + if !config.InterContainerCommunication { + utils.Debugf("Disable inter-container communication") + if err := iptables.Raw("-A", "FORWARD", "-i", config.BridgeIface, "-o", config.BridgeIface, "-j", "DROP"); err != nil { + return fmt.Errorf("Unable to prevent intercontainer communication: %s", err) + } + } else { + utils.Debugf("Enable inter-container communication") + iptables.Raw("-D", "FORWARD", "-i", config.BridgeIface, "-o", config.BridgeIface, "-j", "DROP") } } return nil From 7e3e662ba53e1403d8022d3dc551523379d0040b Mon Sep 17 00:00:00 2001 From: Alexander Larsson Date: Thu, 10 Oct 2013 10:04:06 +0200 Subject: [PATCH 06/17] Add netlink submodule This contains various operations that can replace the use of shelling out to "ip". --- netlink/netlink.go | 548 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 548 insertions(+) create mode 100644 netlink/netlink.go diff --git a/netlink/netlink.go b/netlink/netlink.go new file mode 100644 index 000000000..239614b5f --- /dev/null +++ b/netlink/netlink.go @@ -0,0 +1,548 @@ +package netlink + +import ( + "encoding/binary" + "fmt" + "net" + "syscall" + "unsafe" +) + +var nextSeqNr int + +func nativeEndian() binary.ByteOrder { + var x uint32 = 0x01020304 + if *(*byte)(unsafe.Pointer(&x)) == 0x01 { + return binary.BigEndian + } + return binary.LittleEndian +} + +func getSeq() int { + nextSeqNr = nextSeqNr + 1 + return nextSeqNr +} + +func getIpFamily(ip net.IP) int { + if len(ip) <= net.IPv4len { + return syscall.AF_INET + } + if ip.To4() != nil { + return syscall.AF_INET + } + return syscall.AF_INET6 +} + +type NetlinkRequestData interface { + ToWireFormat() []byte +} + +type IfInfomsg struct { + syscall.IfInfomsg +} + +func newIfInfomsg(family int) *IfInfomsg { + msg := &IfInfomsg{} + msg.Family = uint8(family) + msg.Type = uint16(0) + msg.Index = int32(0) + msg.Flags = uint32(0) + msg.Change = uint32(0) + + return msg +} + +func (msg *IfInfomsg) ToWireFormat() []byte { + native := nativeEndian() + + len := syscall.SizeofIfInfomsg + b := make([]byte, len) + b[0] = msg.Family + b[1] = 0 + native.PutUint16(b[2:4], msg.Type) + native.PutUint32(b[4:8], uint32(msg.Index)) + native.PutUint32(b[8:12], msg.Flags) + native.PutUint32(b[12:16], msg.Change) + return b +} + +type IfAddrmsg struct { + syscall.IfAddrmsg +} + +func newIfAddrmsg(family int) *IfAddrmsg { + msg := &IfAddrmsg{} + msg.Family = uint8(family) + msg.Prefixlen = uint8(0) + msg.Flags = uint8(0) + msg.Scope = uint8(0) + msg.Index = uint32(0) + + return msg +} + +func (msg *IfAddrmsg) ToWireFormat() []byte { + native := nativeEndian() + + len := syscall.SizeofIfAddrmsg + b := make([]byte, len) + b[0] = msg.Family + b[1] = msg.Prefixlen + b[2] = msg.Flags + b[3] = msg.Scope + native.PutUint32(b[4:8], msg.Index) + return b +} + +type RtMsg struct { + syscall.RtMsg +} + +func newRtMsg(family int) *RtMsg { + msg := &RtMsg{} + msg.Family = uint8(family) + msg.Table = syscall.RT_TABLE_MAIN + msg.Scope = syscall.RT_SCOPE_UNIVERSE + msg.Protocol = syscall.RTPROT_BOOT + msg.Type = syscall.RTN_UNICAST + + return msg +} + +func (msg *RtMsg) ToWireFormat() []byte { + native := nativeEndian() + + len := syscall.SizeofRtMsg + b := make([]byte, len) + b[0] = msg.Family + b[1] = msg.Dst_len + b[2] = msg.Src_len + b[3] = msg.Tos + b[4] = msg.Table + b[5] = msg.Protocol + b[6] = msg.Scope + b[7] = msg.Type + native.PutUint32(b[8:12], msg.Flags) + return b +} + +func rtaAlignOf(attrlen int) int { + return (attrlen + syscall.RTA_ALIGNTO - 1) & ^(syscall.RTA_ALIGNTO - 1) +} + +type RtAttr struct { + syscall.RtAttr + Data []byte +} + +func newRtAttr(attrType int, data []byte) *RtAttr { + attr := &RtAttr{} + attr.Type = uint16(attrType) + attr.Data = data + + return attr +} + +func (attr *RtAttr) ToWireFormat() []byte { + native := nativeEndian() + + len := syscall.SizeofRtAttr + len(attr.Data) + b := make([]byte, rtaAlignOf(len)) + native.PutUint16(b[0:2], uint16(len)) + native.PutUint16(b[2:4], attr.Type) + for i, d := range attr.Data { + b[4+i] = d + } + + return b +} + +type NetlinkRequest struct { + syscall.NlMsghdr + Data []NetlinkRequestData +} + +func (rr *NetlinkRequest) ToWireFormat() []byte { + native := nativeEndian() + + length := rr.Len + dataBytes := make([][]byte, len(rr.Data)) + for i, data := range rr.Data { + dataBytes[i] = data.ToWireFormat() + length = length + uint32(len(dataBytes[i])) + } + b := make([]byte, length) + native.PutUint32(b[0:4], length) + native.PutUint16(b[4:6], rr.Type) + native.PutUint16(b[6:8], rr.Flags) + native.PutUint32(b[8:12], rr.Seq) + native.PutUint32(b[12:16], rr.Pid) + + i := 16 + for _, data := range dataBytes { + for _, dataByte := range data { + b[i] = dataByte + i = i + 1 + } + } + return b +} + +func (rr *NetlinkRequest) AddData(data NetlinkRequestData) { + rr.Data = append(rr.Data, data) +} + +func newNetlinkRequest(proto, flags int) *NetlinkRequest { + rr := &NetlinkRequest{} + rr.Len = uint32(syscall.NLMSG_HDRLEN) + rr.Type = uint16(proto) + rr.Flags = syscall.NLM_F_REQUEST | uint16(flags) + rr.Seq = uint32(getSeq()) + return rr +} + +type NetlinkSocket struct { + fd int + lsa syscall.SockaddrNetlink +} + +func getNetlinkSocket() (*NetlinkSocket, error) { + fd, err := syscall.Socket(syscall.AF_NETLINK, syscall.SOCK_RAW, syscall.NETLINK_ROUTE) + if err != nil { + return nil, err + } + s := &NetlinkSocket{ + fd: fd, + } + s.lsa.Family = syscall.AF_NETLINK + if err := syscall.Bind(fd, &s.lsa); err != nil { + syscall.Close(fd) + return nil, err + } + + return s, nil +} + +func (s *NetlinkSocket) Close() { + syscall.Close(s.fd) +} + +func (s *NetlinkSocket) Send(request *NetlinkRequest) error { + if err := syscall.Sendto(s.fd, request.ToWireFormat(), 0, &s.lsa); err != nil { + return err + } + return nil +} + +func (s *NetlinkSocket) Recieve() ([]syscall.NetlinkMessage, error) { + rb := make([]byte, syscall.Getpagesize()) + nr, _, err := syscall.Recvfrom(s.fd, rb, 0) + if err != nil { + return nil, err + } + if nr < syscall.NLMSG_HDRLEN { + return nil, fmt.Errorf("Got short response from netlink") + } + rb = rb[:nr] + return syscall.ParseNetlinkMessage(rb) +} + +func (s *NetlinkSocket) GetPid() (uint32, error) { + lsa, err := syscall.Getsockname(s.fd) + if err != nil { + return 0, err + } + switch v := lsa.(type) { + case *syscall.SockaddrNetlink: + return v.Pid, nil + } + return 0, fmt.Errorf("Wrong socket type") +} + +func (s *NetlinkSocket) HandleAck(seq uint32) error { + native := nativeEndian() + + pid, err := s.GetPid() + if err != nil { + return err + } + +done: + for { + msgs, err := s.Recieve() + if err != nil { + return err + } + for _, m := range msgs { + if m.Header.Seq != seq { + return fmt.Errorf("Wrong Seq nr %d, expected %d", m.Header.Seq, seq) + } + if m.Header.Pid != pid { + return fmt.Errorf("Wrong pid %d, expected %d", m.Header.Pid, pid) + } + if m.Header.Type == syscall.NLMSG_DONE { + break done + } + if m.Header.Type == syscall.NLMSG_ERROR { + error := int32(native.Uint32(m.Data[0:4])) + if error == 0 { + break done + } + return syscall.Errno(-error) + } + } + } + + return nil +} + +// Add a new default gateway. Identical to: +// ip route add default via $ip +func AddDefaultGw(ip net.IP) error { + s, err := getNetlinkSocket() + if err != nil { + return err + } + defer s.Close() + + family := getIpFamily(ip) + + wb := newNetlinkRequest(syscall.RTM_NEWROUTE, syscall.NLM_F_CREATE|syscall.NLM_F_EXCL|syscall.NLM_F_ACK) + + msg := newRtMsg(family) + wb.AddData(msg) + + var ipData []byte + if family == syscall.AF_INET { + ipData = ip.To4() + } else { + ipData = ip.To16() + } + + gateway := newRtAttr(syscall.RTA_GATEWAY, ipData) + + wb.AddData(gateway) + + if err := s.Send(wb); err != nil { + return err + } + + return s.HandleAck(wb.Seq) + +} + +// Bring up a particular network interface +func NetworkLinkUp(iface *net.Interface) error { + s, err := getNetlinkSocket() + if err != nil { + return err + } + defer s.Close() + + wb := newNetlinkRequest(syscall.RTM_NEWLINK, syscall.NLM_F_ACK) + + msg := newIfInfomsg(syscall.AF_UNSPEC) + msg.Change = syscall.IFF_UP + msg.Flags = syscall.IFF_UP + msg.Index = int32(iface.Index) + wb.AddData(msg) + + if err := s.Send(wb); err != nil { + return err + } + + return s.HandleAck(wb.Seq) +} + +// Add an Ip address to an interface. This is identical to: +// ip addr add $ip/$ipNet dev $iface +func NetworkLinkAddIp(iface *net.Interface, ip net.IP, ipNet *net.IPNet) error { + s, err := getNetlinkSocket() + if err != nil { + return err + } + defer s.Close() + + family := getIpFamily(ip) + + wb := newNetlinkRequest(syscall.RTM_NEWADDR, syscall.NLM_F_CREATE|syscall.NLM_F_EXCL|syscall.NLM_F_ACK) + + msg := newIfAddrmsg(family) + msg.Index = uint32(iface.Index) + prefixLen, _ := ipNet.Mask.Size() + msg.Prefixlen = uint8(prefixLen) + wb.AddData(msg) + + var ipData []byte + if family == syscall.AF_INET { + ipData = ip.To4() + } else { + ipData = ip.To16() + } + + localData := newRtAttr(syscall.IFA_LOCAL, ipData) + wb.AddData(localData) + + addrData := newRtAttr(syscall.IFA_ADDRESS, ipData) + wb.AddData(addrData) + + if err := s.Send(wb); err != nil { + return err + } + + return s.HandleAck(wb.Seq) +} + +func zeroTerminated(s string) []byte { + bytes := make([]byte, len(s)+1) + for i := 0; i < len(s); i++ { + bytes[i] = s[i] + } + bytes[len(s)] = 0 + return bytes +} + +func nonZeroTerminated(s string) []byte { + bytes := make([]byte, len(s)) + for i := 0; i < len(s); i++ { + bytes[i] = s[i] + } + return bytes +} + +// Add a new network link of a specified type. This is identical to +// running: ip add link $name type $linkType +func NetworkLinkAdd(name string, linkType string) error { + s, err := getNetlinkSocket() + if err != nil { + return err + } + defer s.Close() + + wb := newNetlinkRequest(syscall.RTM_NEWLINK, syscall.NLM_F_CREATE|syscall.NLM_F_EXCL|syscall.NLM_F_ACK) + + msg := newIfInfomsg(syscall.AF_UNSPEC) + wb.AddData(msg) + + nameData := newRtAttr(syscall.IFLA_IFNAME, zeroTerminated(name)) + wb.AddData(nameData) + + IFLA_INFO_KIND := 1 + + kindData := newRtAttr(IFLA_INFO_KIND, nonZeroTerminated(linkType)) + + infoData := newRtAttr(syscall.IFLA_LINKINFO, kindData.ToWireFormat()) + wb.AddData(infoData) + + if err := s.Send(wb); err != nil { + return err + } + + return s.HandleAck(wb.Seq) +} + +// Returns an array of IPNet for all the currently routed subnets on ipv4 +// This is similar to the first column of "ip route" output +func NetworkGetRoutes() ([]*net.IPNet, error) { + native := nativeEndian() + + s, err := getNetlinkSocket() + if err != nil { + return nil, err + } + defer s.Close() + + wb := newNetlinkRequest(syscall.RTM_GETROUTE, syscall.NLM_F_DUMP) + + msg := newIfInfomsg(syscall.AF_UNSPEC) + wb.AddData(msg) + + if err := s.Send(wb); err != nil { + return nil, err + } + + pid, err := s.GetPid() + if err != nil { + return nil, err + } + + res := make([]*net.IPNet, 0) + +done: + for { + msgs, err := s.Recieve() + if err != nil { + return nil, err + } + for _, m := range msgs { + if m.Header.Seq != wb.Seq { + return nil, fmt.Errorf("Wrong Seq nr %d, expected 1", m.Header.Seq) + } + if m.Header.Pid != pid { + return nil, fmt.Errorf("Wrong pid %d, expected %d", m.Header.Pid, pid) + } + if m.Header.Type == syscall.NLMSG_DONE { + break done + } + if m.Header.Type == syscall.NLMSG_ERROR { + error := int32(native.Uint32(m.Data[0:4])) + if error == 0 { + break done + } + return nil, syscall.Errno(-error) + } + if m.Header.Type != syscall.RTM_NEWROUTE { + continue + } + + var iface *net.Interface = nil + var ipNet *net.IPNet = nil + + msg := (*RtMsg)(unsafe.Pointer(&m.Data[0:syscall.SizeofRtMsg][0])) + + if msg.Flags&syscall.RTM_F_CLONED != 0 { + // Ignore cloned routes + continue + } + + if msg.Table != syscall.RT_TABLE_MAIN { + // Ignore non-main tables + continue + } + + if msg.Family != syscall.AF_INET { + // Ignore non-ipv4 routes + continue + } + + if msg.Dst_len == 0 { + // Ignore default routes + continue + } + + attrs, err := syscall.ParseNetlinkRouteAttr(&m) + if err != nil { + return nil, err + } + for _, attr := range attrs { + switch attr.Attr.Type { + case syscall.RTA_DST: + ip := attr.Value + ipNet = &net.IPNet{ + IP: ip, + Mask: net.CIDRMask(int(msg.Dst_len), 8*len(ip)), + } + case syscall.RTA_OIF: + index := int(native.Uint32(attr.Value[0:4])) + iface, _ = net.InterfaceByIndex(index) + _ = iface + } + } + if ipNet != nil { + res = append(res, ipNet) + } + } + } + + return res, nil +} From 04f8ffa140274955007c0181b02438519e03b631 Mon Sep 17 00:00:00 2001 From: Alexander Larsson Date: Tue, 17 Sep 2013 15:02:12 +0200 Subject: [PATCH 07/17] Use netlink directly instead of /bin/ip in Sysinit The sysinit code only uses /bin/ip to set a default gateway. This is pretty easy to do via netlink directly, so we can avoid the ip dependency. --- sysinit.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/sysinit.go b/sysinit.go index 34f1cbdac..f7b0e7808 100644 --- a/sysinit.go +++ b/sysinit.go @@ -3,8 +3,10 @@ package docker import ( "flag" "fmt" + "github.com/dotcloud/docker/netlink" "github.com/dotcloud/docker/utils" "log" + "net" "os" "os/exec" "strconv" @@ -17,7 +19,14 @@ func setupNetworking(gw string) { if gw == "" { return } - if _, err := ip("route", "add", "default", "via", gw); err != nil { + + ip := net.ParseIP(gw) + if ip == nil { + log.Fatalf("Unable to set up networking, %s is not a valid IP", gw) + return + } + + if err := netlink.AddDefaultGw(ip); err != nil { log.Fatalf("Unable to set up networking: %v", err) } } From 9268e5e42f2a0d4537c3153820ab7525e058b4f5 Mon Sep 17 00:00:00 2001 From: Alexander Larsson Date: Thu, 19 Sep 2013 10:57:11 +0200 Subject: [PATCH 08/17] network: Stop shelling out to /sbin/ip We have our own netlink version of all the required calls now, so we can just skip the /sbin/ip dependency. --- network.go | 66 +++++++++++++++---------------------------------- network_test.go | 13 +++++----- 2 files changed, 27 insertions(+), 52 deletions(-) diff --git a/network.go b/network.go index c3f3253ae..5ea76daaf 100644 --- a/network.go +++ b/network.go @@ -6,12 +6,11 @@ import ( "fmt" "github.com/dotcloud/docker/iptables" "github.com/dotcloud/docker/proxy" + "github.com/dotcloud/docker/netlink" "github.com/dotcloud/docker/utils" "log" "net" - "os/exec" "strconv" - "strings" "sync" ) @@ -68,42 +67,10 @@ func networkSize(mask net.IPMask) int32 { return int32(binary.BigEndian.Uint32(m)) + 1 } -//Wrapper around the ip command -func ip(args ...string) (string, error) { - path, err := exec.LookPath("ip") - if err != nil { - return "", fmt.Errorf("command not found: ip") - } - output, err := exec.Command(path, args...).CombinedOutput() - if err != nil { - return "", fmt.Errorf("ip failed: ip %v", strings.Join(args, " ")) - } - return string(output), nil -} - -func checkRouteOverlaps(routes string, dockerNetwork *net.IPNet) error { - utils.Debugf("Routes:\n\n%s", routes) - for _, line := range strings.Split(routes, "\n") { - if strings.Trim(line, "\r\n\t ") == "" || strings.Contains(line, "default") { - continue - } - _, network, err := net.ParseCIDR(strings.Split(line, " ")[0]) - if err != nil { - // is this a mask-less IP address? - if ip := net.ParseIP(strings.Split(line, " ")[0]); ip == nil { - // fail only if it's neither a network nor a mask-less IP address - return fmt.Errorf("Unexpected ip route output: %s (%s)", err, line) - } else { - _, network, err = net.ParseCIDR(ip.String() + "/32") - if err != nil { - return err - } - } - } - if err == nil && network != nil { - if networkOverlaps(dockerNetwork, network) { - return fmt.Errorf("Network %s is already routed: '%s'", dockerNetwork, line) - } +func checkRouteOverlaps(networks []*net.IPNet, dockerNetwork *net.IPNet) error { + for _, network := range networks { + if networkOverlaps(dockerNetwork, network) { + return fmt.Errorf("Network %s is already routed: '%s'", dockerNetwork, network) } } return nil @@ -139,7 +106,7 @@ func CreateBridgeIface(config *DaemonConfig) error { if err != nil { return err } - routes, err := ip("route") + routes, err := netlink.NetworkGetRoutes() if err != nil { return err } @@ -155,15 +122,22 @@ func CreateBridgeIface(config *DaemonConfig) error { } utils.Debugf("Creating bridge %s with network %s", config.BridgeIface, ifaceAddr) - if output, err := ip("link", "add", config.BridgeIface, "type", "bridge"); err != nil { - return fmt.Errorf("Error creating bridge: %s (output: %s)", err, output) + if err := netlink.NetworkLinkAdd(config.BridgeIface, "bridge"); err != nil { + return fmt.Errorf("Error creating bridge: %s", err) } - - if output, err := ip("addr", "add", ifaceAddr, "dev", config.BridgeIface); err != nil { - return fmt.Errorf("Unable to add private network: %s (%s)", err, output) + iface, err := net.InterfaceByName(config.BridgeIface) + if err != nil { + return err } - if output, err := ip("link", "set", config.BridgeIface, "up"); err != nil { - return fmt.Errorf("Unable to start network bridge: %s (%s)", err, output) + ipAddr, ipNet, err := net.ParseCIDR(ifaceAddr) + if err != nil { + return err + } + if netlink.NetworkLinkAddIp(iface, ipAddr, ipNet); err != nil { + return fmt.Errorf("Unable to add private network: %s", err) + } + if err := netlink.NetworkLinkUp(iface); err != nil { + return fmt.Errorf("Unable to start network bridge: %s", err) } if config.EnableIptables { diff --git a/network_test.go b/network_test.go index a0492b490..7304e205a 100644 --- a/network_test.go +++ b/network_test.go @@ -277,12 +277,13 @@ func TestNetworkOverlaps(t *testing.T) { } func TestCheckRouteOverlaps(t *testing.T) { - routes := `default via 10.0.2.2 dev eth0 -10.0.2.0 dev eth0 proto kernel scope link src 10.0.2.15 -10.0.3.0/24 dev lxcbr0 proto kernel scope link src 10.0.3.1 -10.0.42.0/24 dev testdockbr0 proto kernel scope link src 10.0.42.1 -172.16.42.0/24 dev docker0 proto kernel scope link src 172.16.42.1 -192.168.142.0/24 dev eth1 proto kernel scope link src 192.168.142.142` + routesData := []string{"10.0.2.0/32", "10.0.3.0/24", "10.0.42.0/24", "172.16.42.0/24", "192.168.142.0/24"} + + routes := []*net.IPNet{} + for _, addr := range routesData { + _, netX, _ := net.ParseCIDR(addr) + routes = append(routes, netX) + } _, netX, _ := net.ParseCIDR("172.16.0.1/24") if err := checkRouteOverlaps(routes, netX); err != nil { From 34c7fb0c4390a1a53d90658a4cc5e20b311b0d64 Mon Sep 17 00:00:00 2001 From: Alexander Larsson Date: Wed, 18 Sep 2013 10:53:00 +0200 Subject: [PATCH 09/17] Add a network test This just tries pinging first 127.0.0.1 and then 8.8.8.8. We can't ping via a dns name, because libresolv.so is missing from the test image, so dns resolving doesn't work. --- container_test.go | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/container_test.go b/container_test.go index 10027cbec..9330988b7 100644 --- a/container_test.go +++ b/container_test.go @@ -407,6 +407,44 @@ func TestOutput(t *testing.T) { } } +func TestContainerNetwork(t *testing.T) { + runtime := mkRuntime(t) + defer nuke(runtime) + container, err := runtime.Create( + &Config{ + Image: GetTestImage(runtime).ID, + Cmd: []string{"ping", "-c", "1", "127.0.0.1"}, + }, + ) + if err != nil { + t.Fatal(err) + } + defer runtime.Destroy(container) + if err := container.Run(); err != nil { + t.Fatal(err) + } + if container.State.ExitCode != 0 { + t.Errorf("Unexpected ping 127.0.0.1 exit code %d (expected 0)", container.State.ExitCode) + } + + container, err = runtime.Create( + &Config{ + Image: GetTestImage(runtime).ID, + Cmd: []string{"ping", "-c", "1", "8.8.8.8"}, + }, + ) + if err != nil { + t.Fatal(err) + } + defer runtime.Destroy(container) + if err := container.Run(); err != nil { + t.Fatal(err) + } + if container.State.ExitCode != 0 { + t.Errorf("Unexpected ping 8.8.8.8 exit code %d (expected 0)", container.State.ExitCode) + } +} + func TestKillDifferentUser(t *testing.T) { runtime := mkRuntime(t) defer nuke(runtime) From 1484434196fdb53d6ae555bf0e69d759fb27fedd Mon Sep 17 00:00:00 2001 From: Alexander Larsson Date: Thu, 10 Oct 2013 10:49:18 +0200 Subject: [PATCH 10/17] Move ListOps to utils submodule This will be needed for later use in docker-init without a docker dependency --- commands.go | 12 ------------ container.go | 14 +++++++------- docker/docker.go | 2 +- sysinit.go | 4 ++-- utils.go | 2 +- utils/utils.go | 12 ++++++++++++ 6 files changed, 23 insertions(+), 23 deletions(-) diff --git a/commands.go b/commands.go index 22f220319..0ab3de2e2 100644 --- a/commands.go +++ b/commands.go @@ -1402,18 +1402,6 @@ func (cli *DockerCli) CmdSearch(args ...string) error { // Ports type - Used to parse multiple -p flags type ports []int -// ListOpts type -type ListOpts []string - -func (opts *ListOpts) String() string { - return fmt.Sprint(*opts) -} - -func (opts *ListOpts) Set(value string) error { - *opts = append(*opts, value) - return nil -} - // AttachOpts stores arguments to 'docker run -a', eg. which streams to attach to type AttachOpts map[string]bool diff --git a/container.go b/container.go index 3f9a01519..a810c871b 100644 --- a/container.go +++ b/container.go @@ -167,30 +167,30 @@ func ParseRun(args []string, capabilities *Capabilities) (*Config, *HostConfig, flCpuShares := cmd.Int64("c", 0, "CPU shares (relative weight)") - var flPublish ListOpts + var flPublish utils.ListOpts cmd.Var(&flPublish, "p", "Publish a container's port to the host (use 'docker port' to see the actual mapping)") - var flExpose ListOpts + var flExpose utils.ListOpts cmd.Var(&flExpose, "expose", "Expose a port from the container without publishing it to your host") - var flEnv ListOpts + var flEnv utils.ListOpts cmd.Var(&flEnv, "e", "Set environment variables") - var flDns ListOpts + var flDns utils.ListOpts cmd.Var(&flDns, "dns", "Set custom dns servers") flVolumes := NewPathOpts() cmd.Var(flVolumes, "v", "Bind mount a volume (e.g. from the host: -v /host:/container, from docker: -v /container)") - var flVolumesFrom ListOpts + var flVolumesFrom utils.ListOpts cmd.Var(&flVolumesFrom, "volumes-from", "Mount volumes from the specified container") flEntrypoint := cmd.String("entrypoint", "", "Overwrite the default entrypoint of the image") - var flLxcOpts ListOpts + var flLxcOpts utils.ListOpts cmd.Var(&flLxcOpts, "lxc-conf", "Add custom lxc options -lxc-conf=\"lxc.cgroup.cpuset.cpus = 0,1\"") - var flLinks ListOpts + var flLinks utils.ListOpts cmd.Var(&flLinks, "link", "Add link to another container (containerid:alias)") if err := cmd.Parse(args); err != nil { diff --git a/docker/docker.go b/docker/docker.go index c2e95da9d..569177d5b 100644 --- a/docker/docker.go +++ b/docker/docker.go @@ -36,7 +36,7 @@ func main() { flGraphPath := flag.String("g", "/var/lib/docker", "Path to graph storage base dir.") flEnableCors := flag.Bool("api-enable-cors", false, "Enable CORS requests in the remote api.") flDns := flag.String("dns", "", "Set custom dns servers") - flHosts := docker.ListOpts{fmt.Sprintf("unix://%s", docker.DEFAULTUNIXSOCKET)} + flHosts := utils.ListOpts{fmt.Sprintf("unix://%s", docker.DEFAULTUNIXSOCKET)} flag.Var(&flHosts, "H", "tcp://host:port to bind/connect to or unix://path/to/socket to use") flEnableIptables := flag.Bool("iptables", true, "Disable iptables within docker") flDefaultIp := flag.String("ip", "0.0.0.0", "Default ip address to use when binding a containers ports") diff --git a/sysinit.go b/sysinit.go index f7b0e7808..0643882e8 100644 --- a/sysinit.go +++ b/sysinit.go @@ -67,7 +67,7 @@ func changeUser(u string) { } // Clear environment pollution introduced by lxc-start -func cleanupEnv(env ListOpts) { +func cleanupEnv(env utils.ListOpts) { os.Clearenv() for _, kv := range env { parts := strings.SplitN(kv, "=", 2) @@ -102,7 +102,7 @@ func SysInit() { var gw = flag.String("g", "", "gateway address") var workdir = flag.String("w", "", "workdir") - var flEnv ListOpts + var flEnv utils.ListOpts flag.Var(&flEnv, "e", "Set environment variables") flag.Parse() diff --git a/utils.go b/utils.go index 45949cb35..6da1c0aee 100644 --- a/utils.go +++ b/utils.go @@ -175,7 +175,7 @@ func MergeConfig(userConf, imageConf *Config) error { return nil } -func parseLxcConfOpts(opts ListOpts) ([]KeyValuePair, error) { +func parseLxcConfOpts(opts utils.ListOpts) ([]KeyValuePair, error) { out := make([]KeyValuePair, len(opts)) for i, o := range opts { k, v, err := parseLxcOpt(o) diff --git a/utils/utils.go b/utils/utils.go index bddf376b3..541ac446c 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -22,6 +22,18 @@ import ( "time" ) +// ListOpts type +type ListOpts []string + +func (opts *ListOpts) String() string { + return fmt.Sprint(*opts) +} + +func (opts *ListOpts) Set(value string) error { + *opts = append(*opts, value) + return nil +} + // Go is a basic promise implementation: it wraps calls a function in a goroutine, // and returns a channel which will later return the function's return value. func Go(f func() error) chan error { From e6e6d89cc18990387f076e5f31ec28c75ef5e494 Mon Sep 17 00:00:00 2001 From: Alexander Larsson Date: Thu, 10 Oct 2013 10:53:38 +0200 Subject: [PATCH 11/17] Move SysInit to a submodule This allows docker-init to not import the main docker module, which means it won't e.g. pick up any sqlite dependencies. --- docker-init/docker-init.go | 16 ++++++++++++++++ docker/docker.go | 3 ++- runtime_test.go | 3 ++- sysinit.go => sysinit/sysinit.go | 2 +- 4 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 docker-init/docker-init.go rename sysinit.go => sysinit/sysinit.go (99%) diff --git a/docker-init/docker-init.go b/docker-init/docker-init.go new file mode 100644 index 000000000..0c363f4ac --- /dev/null +++ b/docker-init/docker-init.go @@ -0,0 +1,16 @@ +package main + +import ( + "github.com/dotcloud/docker/sysinit" +) + +var ( + GITCOMMIT string + VERSION string +) + +func main() { + // Running in init mode + sysinit.SysInit() + return +} diff --git a/docker/docker.go b/docker/docker.go index 569177d5b..971f83486 100644 --- a/docker/docker.go +++ b/docker/docker.go @@ -4,6 +4,7 @@ import ( "flag" "fmt" "github.com/dotcloud/docker" + "github.com/dotcloud/docker/sysinit" "github.com/dotcloud/docker/utils" "io/ioutil" "log" @@ -23,7 +24,7 @@ var ( func main() { if selfPath := utils.SelfPath(); selfPath == "/sbin/init" || selfPath == "/.dockerinit" { // Running in init mode - docker.SysInit() + sysinit.SysInit() return } // FIXME: Switch d and D ? (to be more sshd like) diff --git a/runtime_test.go b/runtime_test.go index 542c98926..8f73cec27 100644 --- a/runtime_test.go +++ b/runtime_test.go @@ -3,6 +3,7 @@ package docker import ( "bytes" "fmt" + "github.com/dotcloud/docker/sysinit" "github.com/dotcloud/docker/utils" "io" "log" @@ -77,7 +78,7 @@ func init() { // Hack to run sys init during unit testing if selfPath := utils.SelfPath(); selfPath == "/sbin/init" || selfPath == "/.dockerinit" { - SysInit() + sysinit.SysInit() return } diff --git a/sysinit.go b/sysinit/sysinit.go similarity index 99% rename from sysinit.go rename to sysinit/sysinit.go index 0643882e8..6b2591fd1 100644 --- a/sysinit.go +++ b/sysinit/sysinit.go @@ -1,4 +1,4 @@ -package docker +package sysinit import ( "flag" From 1db7cb2f012a4ce86e5c40909248bb914e0844f2 Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Wed, 16 Oct 2013 17:08:14 -0700 Subject: [PATCH 12/17] Updated fixes post rebase from master Removed test cases that are no longer applicable with links and the port changes. Remove test case where a test was hitting an external ip. --- api.go | 12 ++++-------- container_test.go | 19 +------------------ runtime.go | 1 - runtime_test.go | 15 ++------------- 4 files changed, 7 insertions(+), 40 deletions(-) diff --git a/api.go b/api.go index 3ff74846f..494a6353b 100644 --- a/api.go +++ b/api.go @@ -71,12 +71,12 @@ func httpError(w http.ResponseWriter, err error) { statusCode = http.StatusUnauthorized } else if strings.Contains(err.Error(), "hasn't been activated") { statusCode = http.StatusForbidden - } - + } + if err != nil { utils.Errorf("HTTP Error: statusCode=%d %s", statusCode, err.Error()) - http.Error(w, err.Error(), statusCode) - } + http.Error(w, err.Error(), statusCode) + } } func writeJSON(w http.ResponseWriter, code int, v interface{}) error { @@ -635,12 +635,8 @@ func postContainersStart(srv *Server, version float64, w http.ResponseWriter, r if vars == nil { return fmt.Errorf("Missing parameter") } - var err error name := vars["name"] name = decodeName(name) - if err != nil { - return err - } if err := srv.ContainerStart(name, hostConfig); err != nil { return err } diff --git a/container_test.go b/container_test.go index 9330988b7..222d5a656 100644 --- a/container_test.go +++ b/container_test.go @@ -410,7 +410,7 @@ func TestOutput(t *testing.T) { func TestContainerNetwork(t *testing.T) { runtime := mkRuntime(t) defer nuke(runtime) - container, err := runtime.Create( + container, _, err := runtime.Create( &Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"ping", "-c", "1", "127.0.0.1"}, @@ -426,23 +426,6 @@ func TestContainerNetwork(t *testing.T) { if container.State.ExitCode != 0 { t.Errorf("Unexpected ping 127.0.0.1 exit code %d (expected 0)", container.State.ExitCode) } - - container, err = runtime.Create( - &Config{ - Image: GetTestImage(runtime).ID, - Cmd: []string{"ping", "-c", "1", "8.8.8.8"}, - }, - ) - if err != nil { - t.Fatal(err) - } - defer runtime.Destroy(container) - if err := container.Run(); err != nil { - t.Fatal(err) - } - if container.State.ExitCode != 0 { - t.Errorf("Unexpected ping 8.8.8.8 exit code %d (expected 0)", container.State.ExitCode) - } } func TestKillDifferentUser(t *testing.T) { diff --git a/runtime.go b/runtime.go index d7a6e8800..8239ddb0b 100644 --- a/runtime.go +++ b/runtime.go @@ -325,7 +325,6 @@ func (runtime *Runtime) Create(config *Config) (*Container, []string, error) { if err := MergeConfig(config, img.Config); err != nil { return nil, nil, err } - } if len(config.Entrypoint) != 0 && config.Cmd == nil { diff --git a/runtime_test.go b/runtime_test.go index 8f73cec27..a81284bb5 100644 --- a/runtime_test.go +++ b/runtime_test.go @@ -239,23 +239,12 @@ func TestRuntimeCreate(t *testing.T) { Cmd: []string{"/bin/ls"}, PortSpecs: []string{"80"}, } - container, err = runtime.Create(config) + container, _, err = runtime.Create(config) - image, err := runtime.Commit(container, "testrepo", "testtag", "", "", config) + _, err = runtime.Commit(container, "testrepo", "testtag", "", "", config) if err != nil { t.Error(err) } - - _, err = runtime.Create( - &Config{ - Image: image.ID, - PortSpecs: []string{"80000:80"}, - }, - ) - if err == nil { - t.Fatal("Builder.Create should throw an error when PortSpecs is invalid") - } - } func TestDestroy(t *testing.T) { From 2e181c5635f820583909d0c49bdeac00ad09a302 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Thu, 17 Oct 2013 17:59:46 +0000 Subject: [PATCH 13/17] fix typo in docs --- docs/sources/examples/linking_into_redis.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/examples/linking_into_redis.rst b/docs/sources/examples/linking_into_redis.rst index 1712e62d6..70a83b433 100644 --- a/docs/sources/examples/linking_into_redis.rst +++ b/docs/sources/examples/linking_into_redis.rst @@ -54,7 +54,7 @@ Run the redis container .. code-block:: bash - docker run -d -e PASSWORD=docker crosbymichael/redis --requirepass=docker + docker run -d -e PASSWORD=docker crosbymichael/redis --requirepass docker This will run our redis container using the default port of 6379 and using as password to secure our service. Next we will link the redis container to From 4f51ad7d0ec9ee571e0ad37e3b0f59da3b4acba3 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Thu, 17 Oct 2013 18:40:38 +0000 Subject: [PATCH 14/17] refactor notrunc in commands.go to add names support --- commands.go | 40 +++++++++++++++------------------------- 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/commands.go b/commands.go index 0ab3de2e2..8652fd05d 100644 --- a/commands.go +++ b/commands.go @@ -996,25 +996,19 @@ func (cli *DockerCli) CmdImages(args ...string) error { out.Tag = "" } + if !*noTrunc { + out.ID = utils.TruncateID(out.ID) + } + if !*quiet { - fmt.Fprintf(w, "%s\t%s\t", out.Repository, out.Tag) - if *noTrunc { - fmt.Fprintf(w, "%s\t", out.ID) - } else { - fmt.Fprintf(w, "%s\t", utils.TruncateID(out.ID)) - } - fmt.Fprintf(w, "%s ago\t", utils.HumanDuration(time.Now().Sub(time.Unix(out.Created, 0)))) + fmt.Fprintf(w, "%s\t%s\t%s\t%s ago\t", out.Repository, out.Tag, out.ID, utils.HumanDuration(time.Now().Sub(time.Unix(out.Created, 0)))) if out.VirtualSize > 0 { fmt.Fprintf(w, "%s (virtual %s)\n", utils.HumanSize(out.Size), utils.HumanSize(out.VirtualSize)) } else { fmt.Fprintf(w, "%s\n", utils.HumanSize(out.Size)) } } else { - if *noTrunc { - fmt.Fprintln(w, out.ID) - } else { - fmt.Fprintln(w, utils.TruncateID(out.ID)) - } + fmt.Fprintln(w, out.ID) } } @@ -1093,17 +1087,17 @@ func (cli *DockerCli) CmdPs(args ...string) error { } for _, out := range outs { - for i := 0; i < len(out.Names); i++ { - out.Names[i] = utils.Trunc(out.Names[i], 10) + if !*noTrunc { + out.ID = utils.TruncateID(out.ID) } - - names := strings.Join(out.Names, ",") if !*quiet { - if *noTrunc { - fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s ago\t%s\t%s\t", out.ID, out.Image, out.Command, utils.HumanDuration(time.Now().Sub(time.Unix(out.Created, 0))), out.Status, displayablePorts(out.Ports), names) - } else { - fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s ago\t%s\t%s\t", utils.TruncateID(out.ID), out.Image, utils.Trunc(out.Command, 20), utils.HumanDuration(time.Now().Sub(time.Unix(out.Created, 0))), out.Status, displayablePorts(out.Ports), names) + if !*noTrunc { + out.Command = utils.Trunc(out.Command, 20) + for i := 0; i < len(out.Names); i++ { + out.Names[i] = utils.Trunc(out.Names[i], 10) + } } + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s ago\t%s\t%s\t", out.ID, out.Image, out.Command, utils.HumanDuration(time.Now().Sub(time.Unix(out.Created, 0))), out.Status, displayablePorts(out.Ports), strings.Join(out.Names, ",")) if *size { if out.SizeRootFs > 0 { fmt.Fprintf(w, "%s (virtual %s)\n", utils.HumanSize(out.SizeRw), utils.HumanSize(out.SizeRootFs)) @@ -1114,11 +1108,7 @@ func (cli *DockerCli) CmdPs(args ...string) error { fmt.Fprint(w, "\n") } } else { - if *noTrunc { - fmt.Fprintln(w, out.ID) - } else { - fmt.Fprintln(w, utils.TruncateID(out.ID)) - } + fmt.Fprintln(w, out.ID) } } From bf25342e11483edd19f9eacf8a5b135ad515daf5 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Thu, 17 Oct 2013 19:43:25 +0000 Subject: [PATCH 15/17] replace == by HasPrefix for names and improve error message --- api.go | 3 +++ gograph/gograph.go | 6 +++--- runtime.go | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/api.go b/api.go index 494a6353b..a34269681 100644 --- a/api.go +++ b/api.go @@ -1070,6 +1070,9 @@ func postContainerLink(srv *Server, version float64, w http.ResponseWriter, r *h } if err := srv.runtime.RenameLink(currentName, newName); err != nil { + if strings.HasSuffix(err.Error(), "name are not unique") { + return fmt.Errorf("Conflict, %s already exists", newName) + } return err } diff --git a/gograph/gograph.go b/gograph/gograph.go index 21f2966e2..38de5f4a5 100644 --- a/gograph/gograph.go +++ b/gograph/gograph.go @@ -288,7 +288,7 @@ func (db *Database) Delete(name string) error { return err } - if _, err := conn.Exec("DELETE FROM edge WHERE parent_id = ? AND name = ?;", parent.id, n); err != nil { + if _, err := conn.Exec("DELETE FROM edge WHERE parent_id = ? AND name LIKE ?;", parent.id, n+"%"); err != nil { return err } return nil @@ -356,7 +356,7 @@ func (db *Database) Rename(currentName, newName string) error { return err } - rows, err := conn.Exec("UPDATE edge SET name = ? WHERE parent_id = ? AND name = ?;", newEdgeName, parent.id, name) + rows, err := conn.Exec("UPDATE edge SET name = ? WHERE parent_id = ? AND name LIKE ?;", newEdgeName, parent.id, name+"%") if err != nil { return err } @@ -433,7 +433,7 @@ func (db *Database) children(conn *sql.DB, name string, depth int) <-chan WalkMe // Return the entity based on the parent path and name func (db *Database) child(conn *sql.DB, parent *Entity, name string) *Entity { var id string - if err := conn.QueryRow("SELECT entity_id FROM edge WHERE parent_id = ? AND name = ?;", parent.id, name).Scan(&id); err != nil { + if err := conn.QueryRow("SELECT entity_id FROM edge WHERE parent_id = ? AND name LIKE ?;", parent.id, name+"%").Scan(&id); err != nil { return nil } return &Entity{id} diff --git a/runtime.go b/runtime.go index 8239ddb0b..df5d4c043 100644 --- a/runtime.go +++ b/runtime.go @@ -519,7 +519,7 @@ func (runtime *Runtime) RenameLink(oldName, newName string) error { // This is not rename but adding a new link for the default name // Strip the leading '/' - if entity.ID() == oldName[1:] { + if strings.HasPrefix(entity.ID(), oldName[1:]) { _, err := runtime.containerGraph.Set(newName, entity.ID()) return err } From d596a8a72c89ce0ba3dd3a23dbd883f9b4eef7c0 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Thu, 17 Oct 2013 23:22:14 +0000 Subject: [PATCH 16/17] no more name encoding --- api.go | 16 ---------------- commands.go | 38 ++++++++++++++++++++------------------ runtime.go | 3 +++ 3 files changed, 23 insertions(+), 34 deletions(-) diff --git a/api.go b/api.go index a34269681..0d62a3a43 100644 --- a/api.go +++ b/api.go @@ -15,7 +15,6 @@ import ( "mime" "net" "net/http" - "net/url" "os" "os/exec" "regexp" @@ -138,7 +137,6 @@ func postContainersKill(srv *Server, version float64, w http.ResponseWriter, r * return fmt.Errorf("Missing parameter") } name := vars["name"] - name = decodeName(name) if err := srv.ContainerKill(name); err != nil { return err } @@ -151,7 +149,6 @@ func getContainersExport(srv *Server, version float64, w http.ResponseWriter, r return fmt.Errorf("Missing parameter") } name := vars["name"] - name = decodeName(name) if err := srv.ContainerExport(name, w); err != nil { utils.Errorf("%s", err) @@ -562,7 +559,6 @@ func postContainersRestart(srv *Server, version float64, w http.ResponseWriter, return fmt.Errorf("Missing parameter") } name := vars["name"] - name = decodeName(name) if err := srv.ContainerRestart(name, t); err != nil { return err } @@ -578,7 +574,6 @@ func deleteContainers(srv *Server, version float64, w http.ResponseWriter, r *ht return fmt.Errorf("Missing parameter") } name := vars["name"] - name = decodeName(name) removeVolume, err := getBoolParam(r.Form.Get("v")) if err != nil { @@ -636,7 +631,6 @@ func postContainersStart(srv *Server, version float64, w http.ResponseWriter, r return fmt.Errorf("Missing parameter") } name := vars["name"] - name = decodeName(name) if err := srv.ContainerStart(name, hostConfig); err != nil { return err } @@ -657,7 +651,6 @@ func postContainersStop(srv *Server, version float64, w http.ResponseWriter, r * return fmt.Errorf("Missing parameter") } name := vars["name"] - name = decodeName(name) if err := srv.ContainerStop(name, t); err != nil { return err @@ -671,7 +664,6 @@ func postContainersWait(srv *Server, version float64, w http.ResponseWriter, r * return fmt.Errorf("Missing parameter") } name := vars["name"] - name = decodeName(name) status, err := srv.ContainerWait(name) if err != nil { @@ -732,7 +724,6 @@ func postContainersAttach(srv *Server, version float64, w http.ResponseWriter, r return fmt.Errorf("Missing parameter") } name := vars["name"] - name = decodeName(name) c, err := srv.ContainerInspect(name) if err != nil { @@ -805,7 +796,6 @@ func wsContainersAttach(srv *Server, version float64, w http.ResponseWriter, r * return fmt.Errorf("Missing parameter") } name := vars["name"] - name = decodeName(name) if _, err := srv.ContainerInspect(name); err != nil { return err @@ -828,7 +818,6 @@ func getContainersByName(srv *Server, version float64, w http.ResponseWriter, r return fmt.Errorf("Missing parameter") } name := vars["name"] - name = decodeName(name) container, err := srv.ContainerInspect(name) if err != nil { @@ -1079,11 +1068,6 @@ func postContainerLink(srv *Server, version float64, w http.ResponseWriter, r *h return nil } -func decodeName(name string) string { - s, _ := url.QueryUnescape(name) - return s -} - func createRouter(srv *Server, logging bool) (*mux.Router, error) { r := mux.NewRouter() diff --git a/commands.go b/commands.go index 8652fd05d..cc8819a5f 100644 --- a/commands.go +++ b/commands.go @@ -23,6 +23,7 @@ import ( "os/signal" "path/filepath" "reflect" + "regexp" "runtime" "sort" "strconv" @@ -510,8 +511,7 @@ func (cli *DockerCli) CmdStop(args ...string) error { v.Set("t", strconv.Itoa(*nSeconds)) for _, name := range cmd.Args() { - encName := cleanName(name) - _, _, err := cli.call("POST", "/containers/"+encName+"/stop?"+v.Encode(), nil) + _, _, err := cli.call("POST", "/containers/"+name+"/stop?"+v.Encode(), nil) if err != nil { fmt.Fprintf(cli.err, "%s\n", err) } else { @@ -536,8 +536,7 @@ func (cli *DockerCli) CmdRestart(args ...string) error { v.Set("t", strconv.Itoa(*nSeconds)) for _, name := range cmd.Args() { - encName := cleanName(name) - _, _, err := cli.call("POST", "/containers/"+encName+"/restart?"+v.Encode(), nil) + _, _, err := cli.call("POST", "/containers/"+name+"/restart?"+v.Encode(), nil) if err != nil { fmt.Fprintf(cli.err, "%s\n", err) } else { @@ -559,8 +558,7 @@ func (cli *DockerCli) CmdStart(args ...string) error { var encounteredError error for _, name := range args { - encName := cleanName(name) - _, _, err := cli.call("POST", "/containers/"+encName+"/start", nil) + _, _, err := cli.call("POST", "/containers/"+name+"/start", nil) if err != nil { fmt.Fprintf(cli.err, "%s\n", err) encounteredError = fmt.Errorf("Error: failed to start one or more containers") @@ -582,11 +580,10 @@ func (cli *DockerCli) CmdInspect(args ...string) error { } fmt.Fprintf(cli.out, "[") for i, name := range args { - encName := cleanName(name) if i > 0 { fmt.Fprintf(cli.out, ",") } - obj, _, err := cli.call("GET", "/containers/"+encName+"/json", nil) + obj, _, err := cli.call("GET", "/containers/"+name+"/json", nil) if err != nil { obj, _, err = cli.call("GET", "/images/"+name+"/json", nil) if err != nil { @@ -761,8 +758,7 @@ func (cli *DockerCli) CmdRm(args ...string) error { val.Set("link", "1") } for _, name := range cmd.Args() { - encName := cleanName(name) - _, _, err := cli.call("DELETE", "/containers/"+encName+"?"+val.Encode(), nil) + _, _, err := cli.call("DELETE", "/containers/"+name+"?"+val.Encode(), nil) if err != nil { fmt.Fprintf(cli.err, "%s\n", err) } else { @@ -784,8 +780,7 @@ func (cli *DockerCli) CmdKill(args ...string) error { } for _, name := range args { - encName := cleanName(name) - _, _, err := cli.call("POST", "/containers/"+encName+"/kill", nil) + _, _, err := cli.call("POST", "/containers/"+name+"/kill", nil) if err != nil { fmt.Fprintf(cli.err, "%s\n", err) } else { @@ -1293,7 +1288,7 @@ func (cli *DockerCli) CmdLogs(args ...string) error { cmd.Usage() return nil } - name := cleanName(cmd.Arg(0)) + name := cmd.Arg(0) if err := cli.hijack("POST", "/containers/"+name+"/attach?logs=1&stdout=1&stderr=1", false, nil, cli.out, cli.err); err != nil { return err @@ -1311,7 +1306,6 @@ func (cli *DockerCli) CmdAttach(args ...string) error { return nil } name := cmd.Arg(0) - name = cleanName(name) body, _, err := cli.call("GET", "/containers/"+name+"/json", nil) if err != nil { return err @@ -1696,6 +1690,10 @@ func (cli *DockerCli) call(method, path string, data interface{}) ([]byte, int, params = bytes.NewBuffer(buf) } + // fixme: refactor client to support redirect + re := regexp.MustCompile("/+") + path = re.ReplaceAllString(path, "/") + req, err := http.NewRequest(method, fmt.Sprintf("/v%g%s", APIVERSION, path), params) if err != nil { return nil, -1, err @@ -1741,6 +1739,11 @@ func (cli *DockerCli) stream(method, path string, in io.Reader, out io.Writer, h if (method == "POST" || method == "PUT") && in == nil { in = bytes.NewReader([]byte{}) } + + // fixme: refactor client to support redirect + re := regexp.MustCompile("/+") + path = re.ReplaceAllString(path, "/") + req, err := http.NewRequest(method, fmt.Sprintf("/v%g%s", APIVERSION, path), in) if err != nil { return err @@ -1797,6 +1800,9 @@ func (cli *DockerCli) stream(method, path string, in io.Reader, out io.Writer, h } func (cli *DockerCli) hijack(method, path string, setRawTerminal bool, in io.ReadCloser, stdout, stderr io.Writer) error { + // fixme: refactor client to support redirect + re := regexp.MustCompile("/+") + path = re.ReplaceAllString(path, "/") req, err := http.NewRequest(method, fmt.Sprintf("/v%g%s", APIVERSION, path), nil) if err != nil { @@ -1974,10 +1980,6 @@ func getExitCode(cli *DockerCli, containerId string) (int, error) { return c.State.ExitCode, nil } -func cleanName(name string) string { - return strings.Replace(name, "/", "%252F", -1) -} - func NewDockerCli(in io.ReadCloser, out, err io.Writer, proto, addr string) *DockerCli { var ( isTerminal = false diff --git a/runtime.go b/runtime.go index df5d4c043..718ee96ad 100644 --- a/runtime.go +++ b/runtime.go @@ -67,6 +67,9 @@ func (runtime *Runtime) getContainerElement(id string) *list.Element { // 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 (runtime *Runtime) Get(name string) *Container { + if name[0] != '/' { + name = "/" + name + } if c, _ := runtime.GetByName(name); c != nil { return c } From fbf85e2e0aac902753635b75c6ad0424c216875e Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Fri, 18 Oct 2013 14:15:24 -0700 Subject: [PATCH 17/17] fix merge issue and gofmt --- network.go | 2 +- runtime_test.go | 3 --- state.go | 10 +++++----- utils_test.go | 6 +++--- 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/network.go b/network.go index 5ea76daaf..593ed6cab 100644 --- a/network.go +++ b/network.go @@ -5,8 +5,8 @@ import ( "errors" "fmt" "github.com/dotcloud/docker/iptables" - "github.com/dotcloud/docker/proxy" "github.com/dotcloud/docker/netlink" + "github.com/dotcloud/docker/proxy" "github.com/dotcloud/docker/utils" "log" "net" diff --git a/runtime_test.go b/runtime_test.go index a81284bb5..1ada84db0 100644 --- a/runtime_test.go +++ b/runtime_test.go @@ -95,7 +95,6 @@ func init() { startFds, startGoroutines = utils.GetTotalUsedFds(), runtime.NumGoroutine() } - func setupBaseImage() { config := &DaemonConfig{ GraphPath: unitTestStoreBase, @@ -123,7 +122,6 @@ func setupBaseImage() { } } - func spawnGlobalDaemon() { if globalRuntime != nil { utils.Debugf("Global runtime already exists. Skipping.") @@ -132,7 +130,6 @@ func spawnGlobalDaemon() { globalRuntime = mkRuntime(log.New(os.Stderr, "", 0)) srv := &Server{ runtime: globalRuntime, - enableCors: false, pullingPool: make(map[string]struct{}), pushingPool: make(map[string]struct{}), } diff --git a/state.go b/state.go index 484032be3..af7de9f4d 100644 --- a/state.go +++ b/state.go @@ -9,12 +9,12 @@ import ( type State struct { sync.Mutex - Running bool - Pid int - ExitCode int - StartedAt time.Time + Running bool + Pid int + ExitCode int + StartedAt time.Time FinishedAt time.Time - Ghost bool + Ghost bool } // String returns a human-readable description of the state diff --git a/utils_test.go b/utils_test.go index d9efd9039..917c4c159 100644 --- a/utils_test.go +++ b/utils_test.go @@ -1,15 +1,15 @@ package docker import ( - "github.com/dotcloud/docker/utils" "fmt" + "github.com/dotcloud/docker/utils" "io" "io/ioutil" "os" "path" + "runtime" "strings" "testing" - "runtime" ) // This file contains utility functions for docker's unit test suite. @@ -26,7 +26,7 @@ func mkRuntime(f Fataler) *Runtime { pc, _, _, _ := runtime.Caller(1) callerLongName := runtime.FuncForPC(pc).Name() parts := strings.Split(callerLongName, ".") - callerShortName := parts[len(parts) - 1] + callerShortName := parts[len(parts)-1] if globalTestID == "" { globalTestID = GenerateID()[:4] }