diff --git a/Dockerfile b/Dockerfile index 67963c836b..3c858c8989 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 6c1c47a001..cbaa5ab969 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" @@ -154,7 +156,7 @@ func postContainersKill(srv *Server, version float64, w http.ResponseWriter, r * } } } - + name = decodeName(name) if err := srv.ContainerKill(name, signal); err != nil { return err } @@ -167,6 +169,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) @@ -534,16 +537,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.") @@ -574,6 +580,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 } @@ -589,12 +596,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) @@ -640,7 +653,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 } @@ -661,6 +679,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 @@ -674,6 +693,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 @@ -733,6 +754,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 { @@ -805,6 +827,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 @@ -827,6 +850,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 { @@ -994,7 +1018,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) } @@ -1010,6 +1034,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() @@ -1030,6 +1123,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, @@ -1048,6 +1142,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 5f1a338057..5242d02221 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,6 +50,7 @@ type APIContainers struct { Ports []APIPort SizeRw int64 SizeRootFs int64 + Names []string } func (self *APIContainers) ToLegacy() APIContainersOld { @@ -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 71b91c285e..94a19a1362 100644 --- a/api_test.go +++ b/api_test.go @@ -349,7 +349,7 @@ func TestGetContainersJSON(t *testing.T) { beginLen := runtime.containers.Len() - container, err := runtime.Create(&Config{ + container, _, err := runtime.Create(&Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"echo", "test"}, }) @@ -386,7 +386,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"}, @@ -436,7 +436,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"}, @@ -479,7 +479,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"}, @@ -561,7 +561,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"}, @@ -592,7 +592,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"}, @@ -686,7 +686,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"}, @@ -728,7 +728,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/top"}, @@ -782,7 +782,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"}, @@ -834,7 +834,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/top"}, @@ -881,7 +881,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"}, @@ -923,7 +923,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"}, @@ -1012,7 +1012,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"}, @@ -1104,7 +1104,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"}, }) @@ -1142,7 +1142,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) @@ -1165,7 +1166,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() @@ -1292,7 +1294,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 6aae0469cf..a41443a965 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 } @@ -430,7 +433,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 56e15ab2f0..9a8ae57310 100644 --- a/commands.go +++ b/commands.go @@ -92,6 +92,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"}, {"ps", "List containers"}, {"pull", "Pull an image or a repository from the docker registry server"}, @@ -504,7 +505,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 { @@ -529,7 +531,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 { @@ -605,7 +608,8 @@ func (cli *DockerCli) CmdStart(args ...string) error { var encounteredError error for _, name := range cmd.Args() { - _, _, err := cli.call("POST", "/containers/"+name+"/start", nil) + encName := cleanName(name) + _, _, err := cli.call("POST", "/containers/"+encName+"/start", nil) if err != nil { if !*attach || !*openStdin { fmt.Fprintf(cli.err, "%s\n", err) @@ -811,6 +815,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 } @@ -822,8 +828,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 { @@ -845,7 +855,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 { @@ -1088,10 +1099,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) @@ -1144,7 +1155,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 { @@ -1153,11 +1164,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 { @@ -1183,6 +1199,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") @@ -1300,8 +1374,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, nil); err != nil { + if err := cli.hijack("POST", "/containers/"+name+"/attach?logs=1&stdout=1&stderr=1", false, nil, cli.out, cli.err, nil); err != nil { return err } return nil @@ -1318,8 +1393,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 } @@ -2028,6 +2104,10 @@ func getExitCode(cli *DockerCli, containerId string) (bool, int, error) { return c.State.Running, 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 0000000000..d321790567 --- /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 54a51baad9..c6899d2bc5 100644 --- a/container.go +++ b/container.go @@ -59,6 +59,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 { @@ -71,7 +73,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. @@ -91,6 +94,8 @@ type HostConfig struct { Binds []string ContainerIDFile string LxcConf []KeyValuePair + PortBindings map[Port][]PortBinding + Links []string } type BindMap struct { @@ -113,6 +118,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") != "" { @@ -142,8 +175,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") @@ -162,6 +198,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 } @@ -230,10 +269,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, @@ -253,10 +310,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 { @@ -271,36 +331,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 } @@ -602,7 +664,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 } } @@ -792,6 +854,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) @@ -925,7 +1027,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 } @@ -952,36 +1054,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 } @@ -1064,6 +1189,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) @@ -1345,3 +1478,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 ea65d0fa77..b36898e85b 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 71b0770475..fbde547231 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 @@ -54,10 +59,9 @@ func main() { } } + bridge := docker.DefaultNetworkBridge if *bridgeName != "" { - docker.NetworkBridgeIface = *bridgeName - } else { - docker.NetworkBridgeIface = docker.DefaultNetworkBridge + bridge = *bridgeName } if *flDebug { os.Setenv("DEBUG", "1") @@ -69,7 +73,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) } } else { @@ -117,30 +139,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]) @@ -155,7 +173,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 cc0e46c14c..4ced41894a 100644 --- a/docs/sources/commandline/cli.rst +++ b/docs/sources/commandline/cli.rst @@ -96,8 +96,8 @@ Examples: .. _cli_build_examples: -Examples -~~~~~~~~ +Examples: +~~~~~~~~~ .. code-block:: bash @@ -403,6 +403,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`` @@ -430,7 +457,6 @@ Insert file from github ``logs`` -------- - :: Usage: docker logs [OPTIONS] CONTAINER @@ -510,6 +536,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: @@ -533,7 +582,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 @@ -549,14 +598,16 @@ 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" -sig-proxy=false: Proxify all received signal to the process (even in non-tty mode) + -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 @@ -604,6 +655,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 01251680b6..74a7e7c406 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 0000000000..6c9b7a4ffc --- /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 0000000000..1e998f8ac1 --- /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 0000000000..876bd6b58e --- /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 0000000000..519b199699 --- /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 0000000000..cc936cb840 --- /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 0000000000..40431039a5 --- /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 0000000000..c20dd124a2 --- /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 4eecfd0484..9d960a0efc 100755 --- a/hack/make.sh +++ b/hack/make.sh @@ -45,7 +45,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 cff9f5c733..4c2c248975 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 9554f2946b..c8ed1bc7c0 100644 --- a/hack/make/test +++ b/hack/make/test @@ -15,7 +15,7 @@ bundle_test() { set -x cd $test_dir go test -i - go test -v -ldflags "$LDFLAGS" $TESTFLAGS + go test -v -ldflags "$LDFLAGS" $BUILDFLAGS $TESTFLAGS ) done } 2>&1 | tee $DEST/test.log } diff --git a/iptables/MAINTAINERS b/iptables/MAINTAINERS new file mode 100644 index 0000000000..1e998f8ac1 --- /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 0000000000..5974d4d9c6 --- /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 0000000000..aad8acdb81 --- /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 0000000000..f1087ec34a --- /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 0000000000..64608e15c1 --- /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 59d82577d8..b480dc1440 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 bd3a16a1be..a0492b4907 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 0000000000..1e998f8ac1 --- /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 be506795b0..9e382567c5 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 0000000000..7a711f657b --- /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 0000000000..e7c460f61d --- /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 86545801b5..7d34988f70 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 81516e1e94..2b063351fc 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 @@ -32,10 +32,10 @@ type Runtime struct { repositories *TagStore idIndex *utils.TruncIndex capabilities *Capabilities - autoRestart bool volumes *Graph srv *Server - Dns []string + config *DaemonConfig + containerGraph *gograph.Database } var sysInitPath string @@ -66,10 +66,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 @@ -87,10 +92,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 @@ -101,9 +105,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 } @@ -148,11 +149,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 } @@ -172,9 +173,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 } @@ -202,6 +203,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 { @@ -209,6 +211,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) @@ -227,9 +234,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]) } @@ -238,10 +246,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 } @@ -274,27 +302,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 == "" { @@ -328,36 +374,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 { @@ -366,7 +412,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 @@ -396,9 +442,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. @@ -428,13 +474,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) @@ -447,34 +565,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, @@ -482,8 +605,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 8e7c169bfb..7b788f9cbf 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{}), } @@ -129,7 +131,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{}), } @@ -171,7 +172,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"}, }, @@ -211,12 +212,12 @@ func TestRuntimeCreate(t *testing.T) { t.Errorf("Exists() returned false for a newly created container") } - // Make sure crete with bad parameters returns an error - if _, err = runtime.Create(&Config{Image: GetTestImage(runtime).ID}); err == nil { + // Make sure create with bad parameters returns an error + if _, _, err = runtime.Create(&Config{Image: GetTestImage(runtime).ID}); err == nil { t.Fatal("Builder.Create should throw an error when Cmd is missing") } - if _, err := runtime.Create( + if _, _, err := runtime.Create( &Config{ Image: GetTestImage(runtime).ID, Cmd: []string{}, @@ -230,30 +231,19 @@ 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) { runtime := mkRuntime(t) defer nuke(runtime) - container, err := runtime.Create(&Config{ + container, _, err := runtime.Create(&Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"ls", "-al"}, }) @@ -327,6 +317,7 @@ func startEchoServerContainer(t *testing.T, proto string) (*Runtime, *Container, strPort string runtime = mkRuntime(t) port = 5554 + p Port ) for { @@ -340,22 +331,34 @@ func startEchoServerContainer(t *testing.T, proto string) (*Runtime, *Container, } else { t.Fatal(fmt.Errorf("Unknown protocol %v", proto)) } - 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, trying another one", strPort) } - if err := container.Start(&HostConfig{}); err != nil { + hostConfig := &HostConfig{ + PortBindings: make(map[Port][]PortBinding), + } + hostConfig.PortBindings[p] = []PortBinding{ + {}, + } + if err := container.Start(hostConfig); err != nil { nuke(runtime) t.Fatal(err) } @@ -369,7 +372,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 } @@ -501,7 +504,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) } @@ -528,3 +532,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 676026579c..4f99c0f1d7 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" @@ -114,7 +115,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 } @@ -151,7 +152,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 } @@ -369,7 +370,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 == "" { @@ -391,23 +392,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 { @@ -646,7 +659,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 } @@ -855,7 +868,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 } @@ -920,10 +933,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 { @@ -933,7 +945,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) { @@ -942,12 +954,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 { @@ -962,7 +974,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") @@ -1162,14 +1201,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 } @@ -1321,17 +1378,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 @@ -1369,7 +1425,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 c7bfa415db..ef7b1a9106 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) } @@ -209,7 +209,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) } @@ -224,7 +224,7 @@ func TestRunWithTooLowMemoryLimit(t *testing.T) { defer nuke(runtime) // Try to create a container with a memory limit of 1 byte less than the minimum allowed limit. - if _, err := (*Server).ContainerCreate(&Server{runtime: runtime}, + if _, _, err := (*Server).ContainerCreate(&Server{runtime: runtime}, &Config{ Image: GetTestImage(runtime).ID, Memory: 524287, @@ -397,7 +397,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) } @@ -418,7 +418,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 a818841486..ad931ebcb1 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 5519708ece..d61b1a7112 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 8d821e87ff..45949cb350 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 389203b985..0654874089 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -1044,3 +1044,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 0c775dbb0b..49f19bf759 100644 --- a/utils/utils_test.go +++ b/utils/utils_test.go @@ -424,3 +424,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 3f99de570b..d9efd90397 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 0000000000..d2fbb62023 --- /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 0000000000..982e08ec04 --- /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 +}