diff --git a/Dockerfile b/Dockerfile index 67963c836b..ff80e4ad9c 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.2rc2.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..638e78a713 100644 --- a/api.go +++ b/api.go @@ -72,12 +72,12 @@ func httpError(w http.ResponseWriter, err error) { statusCode = http.StatusUnauthorized } else if strings.Contains(err.Error(), "hasn't been activated") { statusCode = http.StatusForbidden - } - + } + if err != nil { utils.Errorf("HTTP Error: statusCode=%d %s", statusCode, err.Error()) - http.Error(w, err.Error(), statusCode) - } + http.Error(w, err.Error(), statusCode) + } } func writeJSON(w http.ResponseWriter, code int, v interface{}) error { @@ -154,7 +154,6 @@ func postContainersKill(srv *Server, version float64, w http.ResponseWriter, r * } } } - if err := srv.ContainerKill(name, signal); err != nil { return err } @@ -522,8 +521,12 @@ func postImagesPush(srv *Server, version float64, w http.ResponseWriter, r *http } func postContainersCreate(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := parseForm(r); err != nil { + return nil + } config := &Config{} out := &APIRun{} + name := r.Form.Get("name") if err := json.NewDecoder(r.Body).Decode(config); err != nil { return 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, name) 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.") @@ -589,12 +595,17 @@ func deleteContainers(srv *Server, version float64, w http.ResponseWriter, r *ht return fmt.Errorf("Missing parameter") } name := vars["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) @@ -641,6 +652,10 @@ func postContainersStart(srv *Server, version float64, w http.ResponseWriter, r return fmt.Errorf("Missing parameter") } name := vars["name"] + // Register any links from the host config before starting the container + if err := srv.RegisterLinks(name, hostConfig); err != nil { + return err + } if err := srv.ContainerStart(name, hostConfig); err != nil { return err } @@ -674,6 +689,7 @@ func postContainersWait(srv *Server, version float64, w http.ResponseWriter, r * return fmt.Errorf("Missing parameter") } name := vars["name"] + status, err := srv.ContainerWait(name) if err != nil { return err @@ -994,7 +1010,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) } diff --git a/api_params.go b/api_params.go index 5f1a338057..40738f9fe9 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 { diff --git a/api_test.go b/api_test.go index 71b91c285e..34c89f8294 100644 --- a/api_test.go +++ b/api_test.go @@ -349,10 +349,10 @@ 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"}, - }) + }, "") if err != nil { t.Fatal(err) } @@ -386,11 +386,12 @@ 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"}, }, + "", ) if err != nil { t.Fatal(err) @@ -436,11 +437,12 @@ 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"}, }, + "", ) if err != nil { t.Fatal(err) @@ -479,12 +481,13 @@ 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"}, OpenStdin: true, }, + "", ) if err != nil { t.Fatal(err) @@ -561,11 +564,12 @@ 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"}, }, + "", ) if err != nil { t.Fatal(err) @@ -592,11 +596,12 @@ 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"}, }, + "", ) if err != nil { t.Fatal(err) @@ -686,12 +691,13 @@ 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"}, OpenStdin: true, }, + "", ) if err != nil { t.Fatal(err) @@ -728,12 +734,13 @@ 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"}, OpenStdin: true, }, + "", ) if err != nil { t.Fatal(err) @@ -782,12 +789,13 @@ 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"}, OpenStdin: true, }, + "", ) if err != nil { t.Fatal(err) @@ -834,12 +842,13 @@ 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"}, OpenStdin: true, }, + "", ) if err != nil { t.Fatal(err) @@ -881,12 +890,13 @@ 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"}, OpenStdin: true, }, + "", ) if err != nil { t.Fatal(err) @@ -923,12 +933,13 @@ 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"}, OpenStdin: true, }, + "", ) if err != nil { t.Fatal(err) @@ -1012,12 +1023,13 @@ 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"}, OpenStdin: true, }, + "", ) if err != nil { t.Fatal(err) @@ -1104,10 +1116,10 @@ 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"}, - }) + }, "") if err != nil { t.Fatal(err) } @@ -1142,7 +1154,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 +1178,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,11 +1306,12 @@ 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"}, }, + "", ) if err != nil { t.Fatal(err) diff --git a/buildfile.go b/buildfile.go index 6aae0469cf..effad01ef0 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 d187bc0a60..6987e89583 100644 --- a/commands.go +++ b/commands.go @@ -23,6 +23,7 @@ import ( "os/signal" "path/filepath" "reflect" + "regexp" "runtime" "sort" "strconv" @@ -721,11 +722,11 @@ func (cli *DockerCli) CmdPort(args ...string) error { } port := cmd.Arg(1) - proto := "Tcp" + proto := "tcp" parts := strings.SplitN(port, "/", 2) if len(parts) == 2 && len(parts[1]) != 0 { port = parts[0] - proto = strings.ToUpper(parts[1][:1]) + strings.ToLower(parts[1][1:]) + proto = parts[1] } body, _, err := cli.call("GET", "/containers/"+cmd.Arg(0)+"/json", nil) if err != nil { @@ -737,8 +738,14 @@ func (cli *DockerCli) CmdPort(args ...string) error { return err } - if frontend, exists := out.NetworkSettings.PortMapping[proto][port]; exists { - fmt.Fprintf(cli.out, "%s\n", frontend) + if frontends, exists := out.NetworkSettings.Ports[Port(port+"/"+proto)]; exists { + if frontends == nil { + fmt.Fprintf(cli.out, "%s\n", port) + } else { + for _, frontend := range frontends { + fmt.Fprintf(cli.out, "%s:%s\n", frontend.HostIp, frontend.HostPort) + } + } } else { return fmt.Errorf("Error: No private port '%s' allocated on %s", cmd.Arg(1), cmd.Arg(0)) } @@ -814,6 +821,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 } @@ -825,6 +834,9 @@ 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) if err != nil { @@ -1059,25 +1071,19 @@ func (cli *DockerCli) CmdImages(args ...string) error { out.Tag = "" } + if !*noTrunc { + out.ID = utils.TruncateID(out.ID) + } + if !*quiet { - fmt.Fprintf(w, "%s\t%s\t", out.Repository, out.Tag) - if *noTrunc { - fmt.Fprintf(w, "%s\t", out.ID) - } else { - fmt.Fprintf(w, "%s\t", utils.TruncateID(out.ID)) - } - fmt.Fprintf(w, "%s ago\t", utils.HumanDuration(time.Now().Sub(time.Unix(out.Created, 0)))) + fmt.Fprintf(w, "%s\t%s\t%s\t%s ago\t", out.Repository, out.Tag, out.ID, utils.HumanDuration(time.Now().Sub(time.Unix(out.Created, 0)))) if out.VirtualSize > 0 { fmt.Fprintf(w, "%s (virtual %s)\n", utils.HumanSize(out.Size), utils.HumanSize(out.VirtualSize)) } else { fmt.Fprintf(w, "%s\n", utils.HumanSize(out.Size)) } } else { - if *noTrunc { - fmt.Fprintln(w, out.ID) - } else { - fmt.Fprintln(w, utils.TruncateID(out.ID)) - } + fmt.Fprintln(w, out.ID) } } @@ -1091,10 +1097,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) @@ -1147,7 +1153,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 { @@ -1156,12 +1162,20 @@ func (cli *DockerCli) CmdPs(args ...string) error { } for _, out := range outs { + if !*noTrunc { + out.ID = utils.TruncateID(out.ID) + } + + // Remove the leading / from the names + for i := 0; i < len(out.Names); i++ { + out.Names[i] = out.Names[i][1:] + } + 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)) - } 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)) + if !*noTrunc { + out.Command = utils.Trunc(out.Command, 20) } + fmt.Fprintf(w, "%s\t%s\t%s\t%s ago\t%s\t%s\t%s\t", out.ID, out.Image, out.Command, utils.HumanDuration(time.Now().Sub(time.Unix(out.Created, 0))), out.Status, displayablePorts(out.Ports), strings.Join(out.Names, ",")) if *size { if out.SizeRootFs > 0 { fmt.Fprintf(w, "%s (virtual %s)\n", utils.HumanSize(out.SizeRw), utils.HumanSize(out.SizeRootFs)) @@ -1172,11 +1186,7 @@ func (cli *DockerCli) CmdPs(args ...string) error { fmt.Fprint(w, "\n") } } else { - if *noTrunc { - fmt.Fprintln(w, out.ID) - } else { - fmt.Fprintln(w, utils.TruncateID(out.ID)) - } + fmt.Fprintln(w, out.ID) } } @@ -1303,8 +1313,9 @@ func (cli *DockerCli) CmdLogs(args ...string) error { cmd.Usage() return nil } + name := 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 @@ -1321,8 +1332,8 @@ 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) + body, _, err := cli.call("GET", "/containers/"+name+"/json", nil) if err != nil { return err } @@ -1411,18 +1422,6 @@ func (cli *DockerCli) CmdSearch(args ...string) error { // Ports type - Used to parse multiple -p flags type ports []int -// ListOpts type -type ListOpts []string - -func (opts *ListOpts) String() string { - return fmt.Sprint(*opts) -} - -func (opts *ListOpts) Set(value string) error { - *opts = append(*opts, value) - return nil -} - // AttachOpts stores arguments to 'docker run -a', eg. which streams to attach to type AttachOpts map[string]bool @@ -1523,6 +1522,7 @@ func (cli *DockerCli) CmdRun(args ...string) error { flSigProxy := cmd.Lookup("sig-proxy") sigProxy, _ := strconv.ParseBool(flSigProxy.Value.String()) + flName := cmd.Lookup("name") var containerIDFile *os.File if len(hostConfig.ContainerIDFile) > 0 { @@ -1535,9 +1535,14 @@ func (cli *DockerCli) CmdRun(args ...string) error { } defer containerIDFile.Close() } + containerValues := url.Values{} + name := flName.Value.String() + if name != "" { + containerValues.Set("name", name) + } //create the container - body, statusCode, err := cli.call("POST", "/containers/create", config) + body, statusCode, err := cli.call("POST", "/containers/create?"+containerValues.Encode(), config) //if image not found try to pull it if statusCode == 404 { _, tag := utils.ParseRepositoryTag(config.Image) @@ -1578,7 +1583,7 @@ func (cli *DockerCli) CmdRun(args ...string) error { if err != nil { return err } - body, _, err = cli.call("POST", "/containers/create", config) + body, _, err = cli.call("POST", "/containers/create?"+containerValues.Encode(), config) if err != nil { return err } @@ -1754,6 +1759,10 @@ func (cli *DockerCli) call(method, path string, data interface{}) ([]byte, int, params = bytes.NewBuffer(buf) } + // fixme: refactor client to support redirect + re := regexp.MustCompile("/+") + path = re.ReplaceAllString(path, "/") + req, err := http.NewRequest(method, fmt.Sprintf("/v%g%s", APIVERSION, path), params) if err != nil { return nil, -1, err @@ -1800,6 +1809,11 @@ func (cli *DockerCli) stream(method, path string, in io.Reader, out io.Writer, h if (method == "POST" || method == "PUT") && in == nil { in = bytes.NewReader([]byte{}) } + + // fixme: refactor client to support redirect + re := regexp.MustCompile("/+") + path = re.ReplaceAllString(path, "/") + req, err := http.NewRequest(method, fmt.Sprintf("/v%g%s", APIVERSION, path), in) if err != nil { return err @@ -1856,6 +1870,9 @@ func (cli *DockerCli) stream(method, path string, in io.Reader, out io.Writer, h } func (cli *DockerCli) hijack(method, path string, setRawTerminal bool, in io.ReadCloser, stdout, stderr io.Writer, started chan bool) error { + // fixme: refactor client to support redirect + re := regexp.MustCompile("/+") + path = re.ReplaceAllString(path, "/") req, err := http.NewRequest(method, fmt.Sprintf("/v%g%s", APIVERSION, path), nil) if err != nil { diff --git a/config.go b/config.go new file mode 100644 index 0000000000..8f2d22c255 --- /dev/null +++ b/config.go @@ -0,0 +1,18 @@ +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 + InterContainerCommunication bool +} diff --git a/container.go b/container.go index 54a51baad9..ce0ce479a3 100644 --- a/container.go +++ b/container.go @@ -44,6 +44,7 @@ type Container struct { ResolvConfPath string HostnamePath string HostsPath string + Name string cmd *exec.Cmd stdout *utils.WriteBroadcaster @@ -59,6 +60,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 +74,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 +95,8 @@ type HostConfig struct { Binds []string ContainerIDFile string LxcConf []KeyValuePair + PortBindings map[Port][]PortBinding + Links []string } type BindMap struct { @@ -113,6 +119,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") != "" { @@ -134,6 +168,7 @@ func ParseRun(args []string, capabilities *Capabilities) (*Config, *HostConfig, flPrivileged := cmd.Bool("privileged", false, "Give extended privileges to this container") flAutoRemove := cmd.Bool("rm", false, "Automatically remove the container when it exits (incompatible with -d)") flSigProxy := cmd.Bool("sig-proxy", false, "Proxify all received signal to the process (even in non-tty mode)") + cmd.String("name", "", "Assign a name to the container") if capabilities != nil && *flMemory > 0 && !capabilities.MemoryLimit { //fmt.Fprintf(stdout, "WARNING: Your kernel does not support memory limit capabilities. Limitation discarded.\n") @@ -142,26 +177,32 @@ 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 utils.ListOpts + cmd.Var(&flPublish, "p", "Publish a container's port to the host (use 'docker port' to see the actual mapping)") - var flEnv ListOpts + var flExpose utils.ListOpts + cmd.Var(&flExpose, "expose", "Expose a port from the container without publishing it to your host") + + var flEnv utils.ListOpts cmd.Var(&flEnv, "e", "Set environment variables") - var flDns ListOpts + var flDns utils.ListOpts cmd.Var(&flDns, "dns", "Set custom dns servers") flVolumes := NewPathOpts() cmd.Var(flVolumes, "v", "Bind mount a volume (e.g. from the host: -v /host:/container, from docker: -v /container)") - var flVolumesFrom ListOpts + var flVolumesFrom utils.ListOpts cmd.Var(&flVolumesFrom, "volumes-from", "Mount volumes from the specified container") flEntrypoint := cmd.String("entrypoint", "", "Overwrite the default entrypoint of the image") - var flLxcOpts ListOpts + var flLxcOpts utils.ListOpts cmd.Var(&flLxcOpts, "lxc-conf", "Add custom lxc options -lxc-conf=\"lxc.cgroup.cpuset.cpus = 0,1\"") + var flLinks utils.ListOpts + cmd.Var(&flLinks, "link", "Add link to another container (name:alias)") + if err := cmd.Parse(args); err != nil { return nil, nil, cmd, err } @@ -230,10 +271,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 +312,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 +333,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 +666,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 +856,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(container.Name) + 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 +1029,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 +1056,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 } @@ -994,7 +1121,7 @@ func (container *Container) releaseNetwork() { container.NetworkSettings = &NetworkSettings{} } -// FIXME: replace this with a control socket within docker-init +// FIXME: replace this with a control socket within dockerinit func (container *Container) waitLxc() error { for { output, err := exec.Command("lxc-info", "-n", container.ID).CombinedOutput() @@ -1064,6 +1191,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 +1480,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..262d6fcb1d 100644 --- a/container_test.go +++ b/container_test.go @@ -18,11 +18,12 @@ 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"}, }, + "", ) if err != nil { t.Fatal(err) @@ -388,11 +389,12 @@ 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"}, }, + "", ) if err != nil { t.Fatal(err) @@ -407,16 +409,39 @@ func TestOutput(t *testing.T) { } } +func TestContainerNetwork(t *testing.T) { + runtime := mkRuntime(t) + defer nuke(runtime) + container, _, err := runtime.Create( + &Config{ + Image: GetTestImage(runtime).ID, + Cmd: []string{"ping", "-c", "1", "127.0.0.1"}, + }, + "", + ) + if err != nil { + t.Fatal(err) + } + defer runtime.Destroy(container) + if err := container.Run(); err != nil { + t.Fatal(err) + } + if container.State.ExitCode != 0 { + t.Errorf("Unexpected ping 127.0.0.1 exit code %d (expected 0)", container.State.ExitCode) + } +} + 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, User: "daemon", }, + "", ) if err != nil { t.Fatal(err) @@ -471,7 +496,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,10 +511,11 @@ 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"}, }, + "", ) if err != nil { t.Fatal(err) @@ -530,10 +556,10 @@ 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", ""}, - }) + }, "") if err != nil { t.Fatal(err) } @@ -545,10 +571,10 @@ 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", ""}, - }) + }, "") if err != nil { t.Fatal(err) } @@ -564,10 +590,11 @@ 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"}, }, + "", ) if err != nil { t.Fatal(err) @@ -594,12 +621,13 @@ 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"}, OpenStdin: true, }, + "", ) if err != nil { t.Fatal(err) @@ -672,10 +700,11 @@ 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"}, }, + "", ) if err != nil { t.Fatal(err) @@ -690,12 +719,13 @@ 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"}, User: "root", }, + "", ) if err != nil { t.Fatal(err) @@ -710,12 +740,13 @@ 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"}, User: "0", }, + "", ) if err != nil || container.State.ExitCode != 0 { t.Fatal(err) @@ -730,12 +761,13 @@ 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"}, User: "1", }, + "", ) if err != nil { t.Fatal(err) @@ -752,12 +784,13 @@ 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"}, User: "daemon", }, + "", ) if err != nil { t.Fatal(err) @@ -772,12 +805,13 @@ 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"}, User: "unknownuser", }, + "", ) if err != nil { t.Fatal(err) @@ -793,20 +827,22 @@ 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"}, }, + "", ) if err != nil { t.Fatal(err) } defer runtime.Destroy(container1) - container2, err := runtime.Create(&Config{ + container2, _, err := runtime.Create(&Config{ Image: GetTestImage(runtime).ID, Cmd: []string{"sleep", "2"}, }, + "", ) if err != nil { t.Fatal(err) @@ -847,12 +883,13 @@ 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"}, OpenStdin: true, }, + "", ) if err != nil { t.Fatal(err) @@ -892,12 +929,13 @@ 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"}, OpenStdin: true, }, + "", ) if err != nil { t.Fatal(err) @@ -937,10 +975,11 @@ 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"}, }, + "", ) if err != nil { t.Fatal(err) @@ -986,12 +1025,13 @@ 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"}, Cmd: []string{"-n", "foobar"}, }, + "", ) if err != nil { t.Fatal(err) @@ -1009,11 +1049,12 @@ 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"}, }, + "", ) if err != nil { t.Fatal(err) @@ -1060,7 +1101,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"}, @@ -1068,6 +1109,7 @@ func TestLXCConfig(t *testing.T) { Memory: int64(mem), CpuShares: int64(cpu), }, + "", ) if err != nil { t.Fatal(err) @@ -1084,12 +1126,13 @@ 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"}, Hostname: "foobar", }, + "", ) if err != nil { t.Fatal(err) @@ -1115,10 +1158,11 @@ 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"}, }, + "", ) if err != nil { b.Fatal(err) @@ -1147,10 +1191,11 @@ 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"}, }, + "", ) if err != nil { complete <- err @@ -1297,12 +1342,13 @@ 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"}, Volumes: map[string]struct{}{"/test": {}}, }, + "", ) if err != nil { t.Fatal(err) @@ -1316,12 +1362,13 @@ 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"}, VolumesFrom: container.ID, }, + "", ) if err != nil { t.Fatal(err) @@ -1352,11 +1399,12 @@ 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": {}}, }, + "", ) if err != nil { t.Fatal(err) @@ -1395,11 +1443,12 @@ 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": {}}, }, + "", ) if err != nil { t.Fatal(err) @@ -1422,13 +1471,14 @@ 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"}, VolumesFrom: container.ID, Volumes: map[string]struct{}{"/test": {}}, }, + "", ) if err != nil { t.Fatal(err) @@ -1463,7 +1513,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,11 +1579,12 @@ 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": {}}, }, + "", ) if err != nil { t.Fatal(err) @@ -1556,12 +1607,13 @@ 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"}, Volumes: map[string]struct{}{"/other": {}}, }, + "", ) if err != nil { t.Fatal(err) @@ -1577,12 +1629,12 @@ 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"}, VolumesFrom: strings.Join([]string{container.ID, container2.ID}, ","), - }) + }, "") if err != nil { t.Fatal(err) diff --git a/docker/docker.go b/docker/docker.go index 71b0770475..362e899ba1 100644 --- a/docker/docker.go +++ b/docker/docker.go @@ -4,9 +4,11 @@ import ( "flag" "fmt" "github.com/dotcloud/docker" + "github.com/dotcloud/docker/sysinit" "github.com/dotcloud/docker/utils" "io/ioutil" "log" + "net" "os" "os/signal" "strconv" @@ -22,7 +24,7 @@ var ( func main() { if selfPath := utils.SelfPath(); selfPath == "/sbin/init" || selfPath == "/.dockerinit" { // Running in init mode - docker.SysInit() + sysinit.SysInit() return } // FIXME: Switch d and D ? (to be more sshd like) @@ -35,9 +37,14 @@ func main() { flGraphPath := flag.String("g", "/var/lib/docker", "Path to graph storage base dir.") flEnableCors := flag.Bool("api-enable-cors", false, "Enable CORS requests in the remote api.") flDns := flag.String("dns", "", "Set custom dns servers") - flHosts := docker.ListOpts{fmt.Sprintf("unix://%s", docker.DEFAULTUNIXSOCKET)} + flHosts := utils.ListOpts{fmt.Sprintf("unix://%s", docker.DEFAULTUNIXSOCKET)} flag.Var(&flHosts, "H", "tcp://host:port to bind/connect to or unix://path/to/socket to use") + flEnableIptables := flag.Bool("iptables", true, "Disable iptables within docker") + flDefaultIp := flag.String("ip", "0.0.0.0", "Default ip address to use when binding a containers ports") + flInterContainerComm := flag.Bool("enable-container-comm", false, "Enable inter-container communication") + flag.Parse() + if *flVersion { showVersion() return @@ -54,10 +61,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 +75,26 @@ 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, + InterContainerCommunication: *flInterContainerComm, + } + if err := daemon(config); err != nil { log.Fatal(err) } } else { @@ -117,30 +142,30 @@ 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) + + server, err := docker.NewServer(config) + if err != nil { + return err + } + defer server.Close() 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) + server.Close() + removePidFile(config.Pidfile) os.Exit(0) }() - var dns []string - if flDns != "" { - dns = []string{flDns} - } - server, err := docker.NewServer(flGraphPath, autoRestart, enableCors, dns) - 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]) @@ -149,13 +174,15 @@ func daemon(pidfile string, flGraphPath string, protoAddrs []string, autoRestart log.Println("/!\\ DON'T BIND ON ANOTHER IP ADDRESS THAN 127.0.0.1 IF YOU DON'T KNOW WHAT YOU'RE DOING /!\\") } } else { + server.Close() + removePidFile(config.Pidfile) log.Fatal("Invalid protocol format.") } go func() { 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/dockerinit/dockerinit.go b/dockerinit/dockerinit.go new file mode 100644 index 0000000000..0c363f4ac3 --- /dev/null +++ b/dockerinit/dockerinit.go @@ -0,0 +1,16 @@ +package main + +import ( + "github.com/dotcloud/docker/sysinit" +) + +var ( + GITCOMMIT string + VERSION string +) + +func main() { + // Running in init mode + sysinit.SysInit() + return +} diff --git a/docs/sources/api/docker_remote_api_v1.6.rst b/docs/sources/api/docker_remote_api_v1.6.rst index 4a743a75ac..4d844c7a9a 100644 --- a/docs/sources/api/docker_remote_api_v1.6.rst +++ b/docs/sources/api/docker_remote_api_v1.6.rst @@ -151,6 +151,7 @@ Create a container } :jsonparam config: the container's configuration + :query name: container name to use :statuscode 201: no error :statuscode 404: no such container :statuscode 406: impossible to attach (container not running) diff --git a/docs/sources/commandline/cli.rst b/docs/sources/commandline/cli.rst index cc0e46c14c..0badcd67c1 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 @@ -430,7 +430,6 @@ Insert file from github ``logs`` -------- - :: Usage: docker logs [OPTIONS] CONTAINER @@ -510,6 +509,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 +555,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 +571,17 @@ 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 (name:alias) + -name="": Assign the specified name to the container. If no name is specific docker will generate a random name Examples -~~~~~~~~ +-------- .. code-block:: bash @@ -604,6 +629,46 @@ 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 -name console -t -i ubuntu bash + +This will create and run a new container with the container name +being ``console``. + +.. code-block:: bash + + docker run -link /redis:redis -name console 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. The ``-name`` flag will assign the name ``console`` +to the newly created container. + .. _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..4e0e612ba4 --- /dev/null +++ b/docs/sources/examples/linking_into_redis.rst @@ -0,0 +1,120 @@ +: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 -name redis crosbymichael/redis --requirepass=docker + +This will run our redis container using the default port of 6379 and using docker +as password to secure our service. By specifying the ``-name`` flag on run +we will assign the name ``redis`` to this container. +We can issue all the commands that you would expect; start, stop, attach, using the name. +The name also allows us to link other containers into this one. If you do not specify a +name on docker run, docker will automatically generate a name for your container. + +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 -name webapp ubuntu bash + + root@4c01db0b339c:/# env + + HOSTNAME=4c01db0b339c + DB_NAME=/webapp/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=/webapp/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..94ce90afb4 --- /dev/null +++ b/gograph/gograph.go @@ -0,0 +1,423 @@ +package gograph + +import ( + "database/sql" + "fmt" + "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") + ); + ` + + createEdgeIndices = ` + CREATE UNIQUE INDEX IF NOT EXISTS "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 { + conn *sql.DB +} + +// Create a new graph database initialized with a root entity +func NewDatabase(conn *sql.DB, init bool) (*Database, error) { + if conn == nil { + return nil, fmt.Errorf("Database connection cannot be nil") + } + db := &Database{conn} + + if init { + if _, err := conn.Exec(createEntityTable); err != nil { + return nil, err + } + if _, err := conn.Exec(createEdgeTable); err != nil { + return nil, err + } + if _, err := conn.Exec(createEdgeIndices); err != nil { + return nil, err + } + + rollback := func() { + conn.Exec("ROLLBACK") + } + + // Create root entities + if _, err := conn.Exec("BEGIN"); err != nil { + 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 +} + +// Close the underlying connection to the database +func (db *Database) Close() error { + return db.conn.Close() +} + +// Set the entity id for a given path +func (db *Database) Set(fullPath, id string) (*Entity, error) { + // FIXME: is rollback implicit when closing the connection? + rollback := func() { + db.conn.Exec("ROLLBACK") + } + if _, err := db.conn.Exec("BEGIN EXCLUSIVE"); err != nil { + return nil, err + } + var entityId string + if err := db.conn.QueryRow("SELECT id FROM entity WHERE id = ?;", id).Scan(&entityId); err != nil { + if err == sql.ErrNoRows { + if _, err := db.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(parentPath, name, e); err != nil { + rollback() + return nil, err + } + + if _, err := db.conn.Exec("COMMIT"); err != nil { + return nil, err + } + return e, nil +} + +// Return true if a name already exists in the database +func (db *Database) Exists(name string) bool { + return db.Get(name) != nil +} + +func (db *Database) setEdge(parentPath, name string, e *Entity) error { + parent, err := db.get(parentPath) + if err != nil { + return err + } + if parent.id == e.id { + return fmt.Errorf("Cannot set self as child") + } + + if _, err := db.conn.Exec("INSERT INTO edge (parent_id, name, entity_id) VALUES (?,?,?);", parent.id, name, e.id); err != nil { + 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 { + e, err := db.get(name) + if err != nil { + return nil + } + return e +} + +func (db *Database) get(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] + if p == "" { + continue + } + + next := db.child(e, p) + if next == nil { + return nil, fmt.Errorf("Cannot find child for %s", name) + } + 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{} + e, err := db.get(name) + if err != nil { + return out + } + for c := range db.children(e, name, depth) { + out[c.FullPath] = c.Entity + } + return out +} + +func (db *Database) Walk(name string, walkFunc WalkFunc, depth int) error { + e, err := db.get(name) + if err != nil { + return err + } + for c := range db.children(e, 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 { + var count int + if err := db.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{} + + rows, err := db.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") + } + + parentPath, n := splitPath(name) + parent, err := db.get(parentPath) + if err != nil { + return err + } + + if _, err := db.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) { + rollback := func() { + db.conn.Exec("ROLLBACK") + } + + if _, err := db.conn.Exec("BEGIN"); err != nil { + return -1, err + } + + // Delete all edges + rows, err := db.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 := db.conn.Exec("DELETE FROM entity where id = ?;", id); err != nil { + rollback() + return -1, err + } + + if _, err := db.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) + } + + parent, err := db.get(parentPath) + if err != nil { + return err + } + + rows, err := db.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(e *Entity, name string, depth int) <-chan WalkMeta { + out := make(chan WalkMeta) + if e == nil { + close(out) + return out + } + + go func() { + rows, err := db.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(child, 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(parent *Entity, name string) *Entity { + var id string + if err := db.conn.QueryRow("SELECT entity_id FROM edge WHERE parent_id = ? AND name = ?;", parent.id, name).Scan(&id); err != nil { + return nil + } + return &Entity{id} +} + +// 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..dba8ed1080 --- /dev/null +++ b/gograph/gograph_test.go @@ -0,0 +1,503 @@ +package gograph + +import ( + _ "code.google.com/p/gosqlite/sqlite3" + "database/sql" + "os" + "path" + "strconv" + "testing" +) + +func newTestDb(t *testing.T) (*Database, string) { + p := path.Join(os.TempDir(), "sqlite.db") + conn, err := sql.Open("sqlite3", p) + db, err := NewDatabase(conn, true) + if err != nil { + t.Fatal(err) + } + return db, p +} + +func destroyTestDb(dbPath string) { + os.Remove(dbPath) +} + +func TestNewDatabase(t *testing.T) { + db, dbpath := newTestDb(t) + if db == nil { + t.Fatal("Database should not be nil") + } + db.Close() + defer destroyTestDb(dbpath) +} + +func TestCreateRootEnity(t *testing.T) { + db, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + root := db.RootEntity() + if root == nil { + t.Fatal("Root entity should not be nil") + } +} + +func TestGetRootEntity(t *testing.T) { + db, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + + 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, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + + db.Set("/test", "1") + if _, err := db.Set("/other", "1"); err != nil { + t.Fatal(err) + } +} + +func TestSetDuplicateEntity(t *testing.T) { + db, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + + if _, err := db.Set("/foo", "42"); err != nil { + t.Fatal(err) + } + if _, err := db.Set("/foo", "43"); err == nil { + t.Fatalf("Creating an entry with a duplciate path did not cause an error") + } +} + +func TestCreateChild(t *testing.T) { + db, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + + 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, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + + 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, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + + _, 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, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + + 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, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + + 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, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + _, 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, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + _, 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, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + _, 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, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + + if err := db.Delete("/"); err == nil { + t.Fatal("Error should not be nil") + } +} + +func TestDeleteEntity(t *testing.T) { + db, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + _, 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, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + + 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, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + + 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, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + + 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, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + + 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, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + + 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)) + } +} + +func TestExistsTrue(t *testing.T) { + db, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + + db.Set("/testing", "1") + + if !db.Exists("/testing") { + t.Fatalf("/tesing should exist") + } +} + +func TestExistsFalse(t *testing.T) { + db, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + + db.Set("/toerhe", "1") + + if db.Exists("/testing") { + t.Fatalf("/tesing should not exist") + } + +} + +func TestGetNameWithTrailingSlash(t *testing.T) { + db, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + + db.Set("/todo", "1") + + e := db.Get("/todo/") + if e == nil { + t.Fatalf("Entity should not be nil") + } +} diff --git a/gograph/sort.go b/gograph/sort.go new file mode 100644 index 0000000000..a0af6b4025 --- /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..4896242796 --- /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/PACKAGERS.md b/hack/PACKAGERS.md index ab8d74da8f..11d4698f3e 100644 --- a/hack/PACKAGERS.md +++ b/hack/PACKAGERS.md @@ -40,7 +40,6 @@ To build docker, you will need the following system dependencies * A clean checkout of the source must be added to a valid Go [workspace](http://golang.org/doc/code.html#Workspaces) under the path *src/github.com/dotcloud/docker*. See - ## Go dependencies All Go dependencies are vendored under ./vendor. They are used by the official build, @@ -55,7 +54,6 @@ NOTE: if you''re not able to package the exact version (to the exact commit) of please get in touch so we can remediate! Who knows what discrepancies can be caused by even the slightest deviation. We promise to do our best to make everybody happy. - ## Disabling CGO Make sure to disable CGO on your system, and then recompile the standard library on the build @@ -71,7 +69,7 @@ cd /tmp && echo 'package main' > t.go && go test -a -i -v To build the docker binary, run the following command with the source checkout as the working directory: -``` +```bash ./hack/make.sh binary ``` @@ -82,7 +80,7 @@ You are encouraged to use ./hack/make.sh without modification. If you must absol your own script (are you really, really sure you need to? make.sh is really not that complicated), then please take care the respect the following: -* In *./hack/make.sh*: $LDFLAGS, $VERSION and $GITCOMMIT +* In *./hack/make.sh*: $LDFLAGS, $BUILDFLAGS, $VERSION and $GITCOMMIT * In *./hack/make/binary*: the exact build command to run You may be tempted to tweak these settings. In particular, being a rigorous maintainer, you may want @@ -92,6 +90,16 @@ You would do the users of your distro a disservice and "void the docker warranty A good comparison is Busybox: all distros package it as a statically linked binary, because it just makes sense. Docker is the same way. +If you *must* have a non-static Docker binary, please use: + +```bash +./hack/make.sh dynbinary +``` + +This will create *./bundles/$VERSION/dynbinary/docker-$VERSION* and *./bundles/$VERSION/binary/dockerinit-$VERSION*. +The first of these would usually be installed at */usr/bin/docker*, while the second must be installed +at */usr/libexec/docker/dockerinit*. + ## Testing Docker Before releasing your binary, make sure to run the tests! Run the following command with the source @@ -106,7 +114,6 @@ dependencies to be installed (see below). The test suite will also download a small test container, so you will need internet connectivity. - ## Runtime dependencies To run properly, docker needs the following software to be installed at runtime: @@ -133,6 +140,6 @@ for your distro''s process supervisor of choice. Docker should be run as root, with the following arguments: -``` +```bash docker -d ``` diff --git a/hack/make.sh b/hack/make.sh index 4eecfd0484..7e81417e46 100755 --- a/hack/make.sh +++ b/hack/make.sh @@ -35,6 +35,8 @@ grep -q "$RESOLVCONF" /proc/mounts || { DEFAULT_BUNDLES=( binary test + dynbinary + dyntest ubuntu ) @@ -45,8 +47,9 @@ 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' +LDFLAGS_STATIC='-X github.com/dotcloud/docker/utils.IAMSTATIC true -linkmode external -extldflags "-lpthread -static -Wl,--unresolved-symbols=ignore-in-object-files"' +BUILDFLAGS='-tags netgo' bundle() { bundlescript=$1 diff --git a/hack/make/binary b/hack/make/binary index cff9f5c733..6363c2ab09 100644 --- a/hack/make/binary +++ b/hack/make/binary @@ -2,6 +2,5 @@ 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 $LDFLAGS_STATIC" $BUILDFLAGS ./docker +echo "Created binary: $DEST/docker-$VERSION" diff --git a/hack/make/dynbinary b/hack/make/dynbinary new file mode 100644 index 0000000000..e630c94f53 --- /dev/null +++ b/hack/make/dynbinary @@ -0,0 +1,15 @@ +#!/bin/sh + +DEST=$1 + +# dockerinit still needs to be a static binary, even if docker is dynamic +CGO_ENABLED=0 go build -a -o $DEST/dockerinit-$VERSION -ldflags "$LDFLAGS -d" $BUILDFLAGS ./dockerinit +echo "Created binary: $DEST/dockerinit-$VERSION" +ln -sf dockerinit-$VERSION $DEST/dockerinit + +# sha1 our new dockerinit to ensure separate docker and dockerinit always run in a perfect pair compiled for one another +export DOCKER_INITSHA1="$(sha1sum $DEST/dockerinit-$VERSION | cut -d' ' -f1)" +# exported so that "dyntest" can easily access it later without recalculating it + +go build -o $DEST/docker-$VERSION -ldflags "$LDFLAGS -X github.com/dotcloud/docker/utils.INITSHA1 \"$DOCKER_INITSHA1\"" $BUILDFLAGS ./docker +echo "Created binary: $DEST/docker-$VERSION" diff --git a/hack/make/dyntest b/hack/make/dyntest new file mode 100644 index 0000000000..7de144e8db --- /dev/null +++ b/hack/make/dyntest @@ -0,0 +1,42 @@ +#!/bin/sh + +DEST=$1 +INIT=$DEST/../dynbinary/dockerinit-$VERSION + +set -e + +if [ ! -x "$INIT" ]; then + echo >&2 'error: dynbinary must be run before dyntest' + false +fi + +# Run Docker's test suite, including sub-packages, and store their output as a bundle +# If $TESTFLAGS is set in the environment, it is passed as extra arguments to 'go test'. +# You can use this to select certain tests to run, eg. +# +# TESTFLAGS='-run ^TestBuild$' ./hack/make.sh test +# +bundle_test() { + { + date + for test_dir in $(find_test_dirs); do ( + set -x + cd $test_dir + go test -i -ldflags "$LDFLAGS" $BUILDFLAGS + export TEST_DOCKERINIT_PATH=$DEST/../dynbinary/dockerinit-$VERSION + go test -v -ldflags "$LDFLAGS -X github.com/dotcloud/docker/utils.INITSHA1 \"$DOCKER_INITSHA1\"" $BUILDFLAGS $TESTFLAGS + ) done + } 2>&1 | tee $DEST/test.log +} + + +# This helper function walks the current directory looking for directories +# holding Go test files, and prints their paths on standard output, one per +# line. +find_test_dirs() { + find . -name '*_test.go' | grep -v '^./vendor' | + { while read f; do dirname $f; done; } | + sort -u +} + +bundle_test diff --git a/hack/make/test b/hack/make/test index 9554f2946b..8acd63461d 100644 --- a/hack/make/test +++ b/hack/make/test @@ -1,3 +1,5 @@ +#!/bin/sh + DEST=$1 set -e @@ -14,8 +16,8 @@ bundle_test() { for test_dir in $(find_test_dirs); do ( set -x cd $test_dir - go test -i - go test -v -ldflags "$LDFLAGS" $TESTFLAGS + go test -i -ldflags "$LDFLAGS $LDFLAGS_STATIC" $BUILDFLAGS + go test -v -ldflags "$LDFLAGS $LDFLAGS_STATIC" $BUILDFLAGS $TESTFLAGS ) done } 2>&1 | tee $DEST/test.log } diff --git a/hack/make/ubuntu b/hack/make/ubuntu index 26eea0a5cc..5834172fd1 100644 --- a/hack/make/ubuntu +++ b/hack/make/ubuntu @@ -3,8 +3,7 @@ DEST=$1 PKGVERSION="$VERSION" -if test -n "$(git status --porcelain)" -then +if [ -n "$(git status --porcelain)" ]; then PKGVERSION="$PKGVERSION-$(date +%Y%m%d%H%M%S)-$GITCOMMIT" fi 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..463f443fda --- /dev/null +++ b/iptables/iptables.go @@ -0,0 +1,110 @@ +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 +} + +// Check if an existing rule exists +func Exists(args ...string) bool { + return Raw(append([]string{"-C"}, args...)...) == 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..4371248fb1 --- /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 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/lxc_template.go b/lxc_template.go index 37232a89d3..382c16029d 100644 --- a/lxc_template.go +++ b/lxc_template.go @@ -94,7 +94,7 @@ lxc.mount.entry = devpts {{$ROOTFS}}/dev/pts devpts newinstance,ptmxmode=0666,no #lxc.mount.entry = varlock {{$ROOTFS}}/var/lock tmpfs size=1024k,nosuid,nodev,noexec 0 0 lxc.mount.entry = shm {{$ROOTFS}}/dev/shm tmpfs size=65536k,nosuid,nodev,noexec 0 0 -# Inject docker-init +# Inject dockerinit lxc.mount.entry = {{.SysInitPath}} {{$ROOTFS}}/.dockerinit none bind,ro 0 0 # In order to get a working DNS environment, mount bind (ro) the host's /etc/resolv.conf into the container diff --git a/namesgenerator/names-generator.go b/namesgenerator/names-generator.go new file mode 100644 index 0000000000..afbaf74cff --- /dev/null +++ b/namesgenerator/names-generator.go @@ -0,0 +1,30 @@ +package namesgenerator + +import ( + "fmt" + "math/rand" + "time" +) + +type NameChecker interface { + Exists(name string) bool +} + +var ( + colors = [...]string{"white", "silver", "gray", "black", "blue", "green", "cyan", "yellow", "gold", "orange", "brown", "red", "violet", "pink", "magenta", "purple"} + animals = [...]string{"ant", "bird", "cat", "chicken", "cow", "dog", "fish", "fox", "horse", "lion", "monkey", "pig", "sheep", "tiger", "whale", "wolf"} +) + +func GenerateRandomName(checker NameChecker) (string, error) { + retry := 5 + rand.Seed(time.Now().UnixNano()) + name := fmt.Sprintf("%s_%s", colors[rand.Intn(len(colors))], animals[rand.Intn(len(animals))]) + for checker != nil && checker.Exists(name) && retry > 0 { + name = fmt.Sprintf("%s%d", name, rand.Intn(10)) + retry = retry - 1 + } + if retry == 0 { + return name, fmt.Errorf("Error generating random name") + } + return name, nil +} diff --git a/namesgenerator/names-generator_test.go b/namesgenerator/names-generator_test.go new file mode 100644 index 0000000000..7ca225ac7a --- /dev/null +++ b/namesgenerator/names-generator_test.go @@ -0,0 +1,28 @@ +package namesgenerator + +import ( + "testing" +) + +type FalseChecker struct{} + +func (n *FalseChecker) Exists(name string) bool { + return false +} + +type TrueChecker struct{} + +func (n *TrueChecker) Exists(name string) bool { + return true +} + +func TestGenerateRandomName(t *testing.T) { + if _, err := GenerateRandomName(&FalseChecker{}); err != nil { + t.Error(err) + } + + if _, err := GenerateRandomName(&TrueChecker{}); err == nil { + t.Error("An error was expected") + } + +} diff --git a/netlink/netlink.go b/netlink/netlink.go new file mode 100644 index 0000000000..239614b5fb --- /dev/null +++ b/netlink/netlink.go @@ -0,0 +1,548 @@ +package netlink + +import ( + "encoding/binary" + "fmt" + "net" + "syscall" + "unsafe" +) + +var nextSeqNr int + +func nativeEndian() binary.ByteOrder { + var x uint32 = 0x01020304 + if *(*byte)(unsafe.Pointer(&x)) == 0x01 { + return binary.BigEndian + } + return binary.LittleEndian +} + +func getSeq() int { + nextSeqNr = nextSeqNr + 1 + return nextSeqNr +} + +func getIpFamily(ip net.IP) int { + if len(ip) <= net.IPv4len { + return syscall.AF_INET + } + if ip.To4() != nil { + return syscall.AF_INET + } + return syscall.AF_INET6 +} + +type NetlinkRequestData interface { + ToWireFormat() []byte +} + +type IfInfomsg struct { + syscall.IfInfomsg +} + +func newIfInfomsg(family int) *IfInfomsg { + msg := &IfInfomsg{} + msg.Family = uint8(family) + msg.Type = uint16(0) + msg.Index = int32(0) + msg.Flags = uint32(0) + msg.Change = uint32(0) + + return msg +} + +func (msg *IfInfomsg) ToWireFormat() []byte { + native := nativeEndian() + + len := syscall.SizeofIfInfomsg + b := make([]byte, len) + b[0] = msg.Family + b[1] = 0 + native.PutUint16(b[2:4], msg.Type) + native.PutUint32(b[4:8], uint32(msg.Index)) + native.PutUint32(b[8:12], msg.Flags) + native.PutUint32(b[12:16], msg.Change) + return b +} + +type IfAddrmsg struct { + syscall.IfAddrmsg +} + +func newIfAddrmsg(family int) *IfAddrmsg { + msg := &IfAddrmsg{} + msg.Family = uint8(family) + msg.Prefixlen = uint8(0) + msg.Flags = uint8(0) + msg.Scope = uint8(0) + msg.Index = uint32(0) + + return msg +} + +func (msg *IfAddrmsg) ToWireFormat() []byte { + native := nativeEndian() + + len := syscall.SizeofIfAddrmsg + b := make([]byte, len) + b[0] = msg.Family + b[1] = msg.Prefixlen + b[2] = msg.Flags + b[3] = msg.Scope + native.PutUint32(b[4:8], msg.Index) + return b +} + +type RtMsg struct { + syscall.RtMsg +} + +func newRtMsg(family int) *RtMsg { + msg := &RtMsg{} + msg.Family = uint8(family) + msg.Table = syscall.RT_TABLE_MAIN + msg.Scope = syscall.RT_SCOPE_UNIVERSE + msg.Protocol = syscall.RTPROT_BOOT + msg.Type = syscall.RTN_UNICAST + + return msg +} + +func (msg *RtMsg) ToWireFormat() []byte { + native := nativeEndian() + + len := syscall.SizeofRtMsg + b := make([]byte, len) + b[0] = msg.Family + b[1] = msg.Dst_len + b[2] = msg.Src_len + b[3] = msg.Tos + b[4] = msg.Table + b[5] = msg.Protocol + b[6] = msg.Scope + b[7] = msg.Type + native.PutUint32(b[8:12], msg.Flags) + return b +} + +func rtaAlignOf(attrlen int) int { + return (attrlen + syscall.RTA_ALIGNTO - 1) & ^(syscall.RTA_ALIGNTO - 1) +} + +type RtAttr struct { + syscall.RtAttr + Data []byte +} + +func newRtAttr(attrType int, data []byte) *RtAttr { + attr := &RtAttr{} + attr.Type = uint16(attrType) + attr.Data = data + + return attr +} + +func (attr *RtAttr) ToWireFormat() []byte { + native := nativeEndian() + + len := syscall.SizeofRtAttr + len(attr.Data) + b := make([]byte, rtaAlignOf(len)) + native.PutUint16(b[0:2], uint16(len)) + native.PutUint16(b[2:4], attr.Type) + for i, d := range attr.Data { + b[4+i] = d + } + + return b +} + +type NetlinkRequest struct { + syscall.NlMsghdr + Data []NetlinkRequestData +} + +func (rr *NetlinkRequest) ToWireFormat() []byte { + native := nativeEndian() + + length := rr.Len + dataBytes := make([][]byte, len(rr.Data)) + for i, data := range rr.Data { + dataBytes[i] = data.ToWireFormat() + length = length + uint32(len(dataBytes[i])) + } + b := make([]byte, length) + native.PutUint32(b[0:4], length) + native.PutUint16(b[4:6], rr.Type) + native.PutUint16(b[6:8], rr.Flags) + native.PutUint32(b[8:12], rr.Seq) + native.PutUint32(b[12:16], rr.Pid) + + i := 16 + for _, data := range dataBytes { + for _, dataByte := range data { + b[i] = dataByte + i = i + 1 + } + } + return b +} + +func (rr *NetlinkRequest) AddData(data NetlinkRequestData) { + rr.Data = append(rr.Data, data) +} + +func newNetlinkRequest(proto, flags int) *NetlinkRequest { + rr := &NetlinkRequest{} + rr.Len = uint32(syscall.NLMSG_HDRLEN) + rr.Type = uint16(proto) + rr.Flags = syscall.NLM_F_REQUEST | uint16(flags) + rr.Seq = uint32(getSeq()) + return rr +} + +type NetlinkSocket struct { + fd int + lsa syscall.SockaddrNetlink +} + +func getNetlinkSocket() (*NetlinkSocket, error) { + fd, err := syscall.Socket(syscall.AF_NETLINK, syscall.SOCK_RAW, syscall.NETLINK_ROUTE) + if err != nil { + return nil, err + } + s := &NetlinkSocket{ + fd: fd, + } + s.lsa.Family = syscall.AF_NETLINK + if err := syscall.Bind(fd, &s.lsa); err != nil { + syscall.Close(fd) + return nil, err + } + + return s, nil +} + +func (s *NetlinkSocket) Close() { + syscall.Close(s.fd) +} + +func (s *NetlinkSocket) Send(request *NetlinkRequest) error { + if err := syscall.Sendto(s.fd, request.ToWireFormat(), 0, &s.lsa); err != nil { + return err + } + return nil +} + +func (s *NetlinkSocket) Recieve() ([]syscall.NetlinkMessage, error) { + rb := make([]byte, syscall.Getpagesize()) + nr, _, err := syscall.Recvfrom(s.fd, rb, 0) + if err != nil { + return nil, err + } + if nr < syscall.NLMSG_HDRLEN { + return nil, fmt.Errorf("Got short response from netlink") + } + rb = rb[:nr] + return syscall.ParseNetlinkMessage(rb) +} + +func (s *NetlinkSocket) GetPid() (uint32, error) { + lsa, err := syscall.Getsockname(s.fd) + if err != nil { + return 0, err + } + switch v := lsa.(type) { + case *syscall.SockaddrNetlink: + return v.Pid, nil + } + return 0, fmt.Errorf("Wrong socket type") +} + +func (s *NetlinkSocket) HandleAck(seq uint32) error { + native := nativeEndian() + + pid, err := s.GetPid() + if err != nil { + return err + } + +done: + for { + msgs, err := s.Recieve() + if err != nil { + return err + } + for _, m := range msgs { + if m.Header.Seq != seq { + return fmt.Errorf("Wrong Seq nr %d, expected %d", m.Header.Seq, seq) + } + if m.Header.Pid != pid { + return fmt.Errorf("Wrong pid %d, expected %d", m.Header.Pid, pid) + } + if m.Header.Type == syscall.NLMSG_DONE { + break done + } + if m.Header.Type == syscall.NLMSG_ERROR { + error := int32(native.Uint32(m.Data[0:4])) + if error == 0 { + break done + } + return syscall.Errno(-error) + } + } + } + + return nil +} + +// Add a new default gateway. Identical to: +// ip route add default via $ip +func AddDefaultGw(ip net.IP) error { + s, err := getNetlinkSocket() + if err != nil { + return err + } + defer s.Close() + + family := getIpFamily(ip) + + wb := newNetlinkRequest(syscall.RTM_NEWROUTE, syscall.NLM_F_CREATE|syscall.NLM_F_EXCL|syscall.NLM_F_ACK) + + msg := newRtMsg(family) + wb.AddData(msg) + + var ipData []byte + if family == syscall.AF_INET { + ipData = ip.To4() + } else { + ipData = ip.To16() + } + + gateway := newRtAttr(syscall.RTA_GATEWAY, ipData) + + wb.AddData(gateway) + + if err := s.Send(wb); err != nil { + return err + } + + return s.HandleAck(wb.Seq) + +} + +// Bring up a particular network interface +func NetworkLinkUp(iface *net.Interface) error { + s, err := getNetlinkSocket() + if err != nil { + return err + } + defer s.Close() + + wb := newNetlinkRequest(syscall.RTM_NEWLINK, syscall.NLM_F_ACK) + + msg := newIfInfomsg(syscall.AF_UNSPEC) + msg.Change = syscall.IFF_UP + msg.Flags = syscall.IFF_UP + msg.Index = int32(iface.Index) + wb.AddData(msg) + + if err := s.Send(wb); err != nil { + return err + } + + return s.HandleAck(wb.Seq) +} + +// Add an Ip address to an interface. This is identical to: +// ip addr add $ip/$ipNet dev $iface +func NetworkLinkAddIp(iface *net.Interface, ip net.IP, ipNet *net.IPNet) error { + s, err := getNetlinkSocket() + if err != nil { + return err + } + defer s.Close() + + family := getIpFamily(ip) + + wb := newNetlinkRequest(syscall.RTM_NEWADDR, syscall.NLM_F_CREATE|syscall.NLM_F_EXCL|syscall.NLM_F_ACK) + + msg := newIfAddrmsg(family) + msg.Index = uint32(iface.Index) + prefixLen, _ := ipNet.Mask.Size() + msg.Prefixlen = uint8(prefixLen) + wb.AddData(msg) + + var ipData []byte + if family == syscall.AF_INET { + ipData = ip.To4() + } else { + ipData = ip.To16() + } + + localData := newRtAttr(syscall.IFA_LOCAL, ipData) + wb.AddData(localData) + + addrData := newRtAttr(syscall.IFA_ADDRESS, ipData) + wb.AddData(addrData) + + if err := s.Send(wb); err != nil { + return err + } + + return s.HandleAck(wb.Seq) +} + +func zeroTerminated(s string) []byte { + bytes := make([]byte, len(s)+1) + for i := 0; i < len(s); i++ { + bytes[i] = s[i] + } + bytes[len(s)] = 0 + return bytes +} + +func nonZeroTerminated(s string) []byte { + bytes := make([]byte, len(s)) + for i := 0; i < len(s); i++ { + bytes[i] = s[i] + } + return bytes +} + +// Add a new network link of a specified type. This is identical to +// running: ip add link $name type $linkType +func NetworkLinkAdd(name string, linkType string) error { + s, err := getNetlinkSocket() + if err != nil { + return err + } + defer s.Close() + + wb := newNetlinkRequest(syscall.RTM_NEWLINK, syscall.NLM_F_CREATE|syscall.NLM_F_EXCL|syscall.NLM_F_ACK) + + msg := newIfInfomsg(syscall.AF_UNSPEC) + wb.AddData(msg) + + nameData := newRtAttr(syscall.IFLA_IFNAME, zeroTerminated(name)) + wb.AddData(nameData) + + IFLA_INFO_KIND := 1 + + kindData := newRtAttr(IFLA_INFO_KIND, nonZeroTerminated(linkType)) + + infoData := newRtAttr(syscall.IFLA_LINKINFO, kindData.ToWireFormat()) + wb.AddData(infoData) + + if err := s.Send(wb); err != nil { + return err + } + + return s.HandleAck(wb.Seq) +} + +// Returns an array of IPNet for all the currently routed subnets on ipv4 +// This is similar to the first column of "ip route" output +func NetworkGetRoutes() ([]*net.IPNet, error) { + native := nativeEndian() + + s, err := getNetlinkSocket() + if err != nil { + return nil, err + } + defer s.Close() + + wb := newNetlinkRequest(syscall.RTM_GETROUTE, syscall.NLM_F_DUMP) + + msg := newIfInfomsg(syscall.AF_UNSPEC) + wb.AddData(msg) + + if err := s.Send(wb); err != nil { + return nil, err + } + + pid, err := s.GetPid() + if err != nil { + return nil, err + } + + res := make([]*net.IPNet, 0) + +done: + for { + msgs, err := s.Recieve() + if err != nil { + return nil, err + } + for _, m := range msgs { + if m.Header.Seq != wb.Seq { + return nil, fmt.Errorf("Wrong Seq nr %d, expected 1", m.Header.Seq) + } + if m.Header.Pid != pid { + return nil, fmt.Errorf("Wrong pid %d, expected %d", m.Header.Pid, pid) + } + if m.Header.Type == syscall.NLMSG_DONE { + break done + } + if m.Header.Type == syscall.NLMSG_ERROR { + error := int32(native.Uint32(m.Data[0:4])) + if error == 0 { + break done + } + return nil, syscall.Errno(-error) + } + if m.Header.Type != syscall.RTM_NEWROUTE { + continue + } + + var iface *net.Interface = nil + var ipNet *net.IPNet = nil + + msg := (*RtMsg)(unsafe.Pointer(&m.Data[0:syscall.SizeofRtMsg][0])) + + if msg.Flags&syscall.RTM_F_CLONED != 0 { + // Ignore cloned routes + continue + } + + if msg.Table != syscall.RT_TABLE_MAIN { + // Ignore non-main tables + continue + } + + if msg.Family != syscall.AF_INET { + // Ignore non-ipv4 routes + continue + } + + if msg.Dst_len == 0 { + // Ignore default routes + continue + } + + attrs, err := syscall.ParseNetlinkRouteAttr(&m) + if err != nil { + return nil, err + } + for _, attr := range attrs { + switch attr.Attr.Type { + case syscall.RTA_DST: + ip := attr.Value + ipNet = &net.IPNet{ + IP: ip, + Mask: net.CIDRMask(int(msg.Dst_len), 8*len(ip)), + } + case syscall.RTA_OIF: + index := int(native.Uint32(attr.Value[0:4])) + iface, _ = net.InterfaceByIndex(index) + _ = iface + } + } + if ipNet != nil { + res = append(res, ipNet) + } + } + } + + return res, nil +} diff --git a/network.go b/network.go index 59d82577d8..c237ccc86d 100644 --- a/network.go +++ b/network.go @@ -4,17 +4,16 @@ import ( "encoding/binary" "errors" "fmt" + "github.com/dotcloud/docker/iptables" + "github.com/dotcloud/docker/netlink" + "github.com/dotcloud/docker/proxy" "github.com/dotcloud/docker/utils" "log" "net" - "os/exec" "strconv" - "strings" "sync" ) -var NetworkBridgeIface string - const ( DefaultNetworkBridge = "docker0" DisableNetworkBridge = "none" @@ -68,54 +67,10 @@ func networkSize(mask net.IPMask) int32 { return int32(binary.BigEndian.Uint32(m)) + 1 } -//Wrapper around the ip command -func ip(args ...string) (string, error) { - path, err := exec.LookPath("ip") - if err != nil { - return "", fmt.Errorf("command not found: ip") - } - output, err := exec.Command(path, args...).CombinedOutput() - if err != nil { - return "", fmt.Errorf("ip failed: ip %v", strings.Join(args, " ")) - } - return string(output), nil -} - -// 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") { - if strings.Trim(line, "\r\n\t ") == "" || strings.Contains(line, "default") { - continue - } - _, network, err := net.ParseCIDR(strings.Split(line, " ")[0]) - if err != nil { - // is this a mask-less IP address? - if ip := net.ParseIP(strings.Split(line, " ")[0]); ip == nil { - // fail only if it's neither a network nor a mask-less IP address - return fmt.Errorf("Unexpected ip route output: %s (%s)", err, line) - } else { - _, network, err = net.ParseCIDR(ip.String() + "/32") - if err != nil { - return err - } - } - } - if err == nil && network != nil { - if networkOverlaps(dockerNetwork, network) { - return fmt.Errorf("Network %s is already routed: '%s'", dockerNetwork, line) - } +func checkRouteOverlaps(networks []*net.IPNet, dockerNetwork *net.IPNet) error { + for _, network := range networks { + if networkOverlaps(dockerNetwork, network) { + return fmt.Errorf("Network %s is already routed: '%s'", dockerNetwork, network) } } return nil @@ -124,7 +79,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. @@ -151,7 +106,7 @@ func CreateBridgeIface(ifaceName string) error { if err != nil { return err } - routes, err := ip("route") + routes, err := netlink.NetworkGetRoutes() if err != nil { return err } @@ -163,23 +118,33 @@ 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 { - return fmt.Errorf("Error creating bridge: %s (output: %s)", err, output) + if err := netlink.NetworkLinkAdd(config.BridgeIface, "bridge"); err != nil { + return fmt.Errorf("Error creating bridge: %s", err) + } + iface, err := net.InterfaceByName(config.BridgeIface) + if err != nil { + return err + } + ipAddr, ipNet, err := net.ParseCIDR(ifaceAddr) + if err != nil { + return err + } + if netlink.NetworkLinkAddIp(iface, ipAddr, ipNet); err != nil { + return fmt.Errorf("Unable to add private network: %s", err) + } + if err := netlink.NetworkLinkUp(iface); err != nil { + return fmt.Errorf("Unable to start network bridge: %s", err) } - if output, err := ip("addr", "add", ifaceAddr, "dev", ifaceName); err != nil { - return fmt.Errorf("Unable to add private network: %s (%s)", err, output) - } - if output, err := ip("link", "set", ifaceName, "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) + } } return nil } @@ -216,58 +181,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 +209,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 +226,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 +236,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 +251,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 +473,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 +530,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,28 +628,44 @@ 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 } } network := addr.(*net.IPNet) + // Configure iptables for link support + if config.EnableIptables { + args := []string{"FORWARD", "-i", config.BridgeIface, "-o", config.BridgeIface, "-j", "DROP"} + + if !config.InterContainerCommunication { + if !iptables.Exists(args...) { + utils.Debugf("Disable inter-container communication") + if err := iptables.Raw(append([]string{"-A"}, args...)...); err != nil { + return nil, fmt.Errorf("Unable to prevent intercontainer communication: %s", err) + } + } + } else { + utils.Debugf("Enable inter-container communication") + iptables.Raw(append([]string{"-D"}, args...)...) + } + } + ipAllocator := newIPAllocator(network) tcpPortAllocator, err := newPortAllocator() @@ -737,13 +677,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..7304e205a5 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 { @@ -385,12 +277,13 @@ func TestNetworkOverlaps(t *testing.T) { } func TestCheckRouteOverlaps(t *testing.T) { - routes := `default via 10.0.2.2 dev eth0 -10.0.2.0 dev eth0 proto kernel scope link src 10.0.2.15 -10.0.3.0/24 dev lxcbr0 proto kernel scope link src 10.0.3.1 -10.0.42.0/24 dev testdockbr0 proto kernel scope link src 10.0.42.1 -172.16.42.0/24 dev docker0 proto kernel scope link src 172.16.42.1 -192.168.142.0/24 dev eth1 proto kernel scope link src 192.168.142.142` + routesData := []string{"10.0.2.0/32", "10.0.3.0/24", "10.0.42.0/24", "172.16.42.0/24", "192.168.142.0/24"} + + routes := []*net.IPNet{} + for _, addr := range routesData { + _, netX, _ := net.ParseCIDR(addr) + routes = append(routes, netX) + } _, netX, _ := net.ParseCIDR("172.16.0.1/24") if err := checkRouteOverlaps(routes, netX); err != nil { 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..e5c535f9fc 100644 --- a/runtime.go +++ b/runtime.go @@ -1,8 +1,11 @@ package docker import ( + _ "code.google.com/p/gosqlite/sqlite3" "container/list" + "database/sql" "fmt" + "github.com/dotcloud/docker/gograph" "github.com/dotcloud/docker/utils" "io" "io/ioutil" @@ -24,7 +27,6 @@ type Capabilities struct { } type Runtime struct { - root string repository string containers *list.List networkManager *NetworkManager @@ -32,16 +34,10 @@ type Runtime struct { repositories *TagStore idIndex *utils.TruncIndex capabilities *Capabilities - autoRestart bool volumes *Graph srv *Server - Dns []string -} - -var sysInitPath string - -func init() { - sysInitPath = utils.SelfPath() + config *DaemonConfig + containerGraph *gograph.Database } // List returns an array of all containers registered in the runtime. @@ -66,10 +62,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 +88,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 +101,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 +145,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 +169,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 +199,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 +207,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 +230,11 @@ func (runtime *Runtime) restore() error { if err != nil { return err } + containers := make(map[string]*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 +243,41 @@ func (runtime *Runtime) restore() error { continue } utils.Debugf("Loaded container %v", container.ID) + containers[container.ID] = container } + + register := func(container *Container) { + if err := runtime.Register(container); err != nil { + utils.Debugf("Failed to register container %s: %s", container.ID, err) + } + } + + if entities := runtime.containerGraph.List("/", -1); entities != nil { + for _, p := range entities.Paths() { + e := entities[p] + if container, ok := containers[e.ID()]; ok { + register(container) + delete(containers, e.ID()) + } + } + } + + // Any containers that are left over do not exist in the graph + for _, container := range containers { + // Try to set the default name for a container if it exists prior to links + name := generateRandomName(runtime) + container.Name = name + + if _, err := runtime.containerGraph.Set(name, container.ID); err != nil { + utils.Debugf("Setting default id - %s", err) + } + register(container) + } + if os.Getenv("DEBUG") == "" && os.Getenv("TEST") == "" { fmt.Printf("\bdone.\n") } + return nil } @@ -273,28 +309,57 @@ func (runtime *Runtime) UpdateCapabilities(quiet bool) { } } -// Create creates a new container from the given configuration. -func (runtime *Runtime) Create(config *Config) (*Container, error) { +// Create creates a new container from the given configuration with a given name. +func (runtime *Runtime) Create(config *Config, name string) (*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 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, err + 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") + } + + sysInitPath := utils.DockerInitPath() + if sysInitPath == "" { + return nil, nil, fmt.Errorf("Could not locate dockerinit: This usually means docker was built incorrectly. See http://docs.docker.io/en/latest/contributing/devenvironment for official build instructions.") } // Generate id id := GenerateID() + + if name == "" { + name = generateRandomName(runtime) + } + if name[0] != '/' { + name = "/" + name + } + + // Set the enitity in the graph using the default name specified + if _, err := runtime.containerGraph.Set(name, 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 == "" { @@ -323,41 +388,42 @@ func (runtime *Runtime) Create(config *Config) (*Container, error) { NetworkSettings: &NetworkSettings{}, // FIXME: do we need to store this in the container? SysInitPath: sysInitPath, + Name: name, } container.root = runtime.containerRoot(container.ID) // 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 +432,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 +462,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 +494,59 @@ 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) getFullName(name string) string { + if name[0] != '/' { + name = "/" + name + } + return name +} + +func (runtime *Runtime) GetByName(name string) (*Container, error) { + entity := runtime.containerGraph.Get(runtime.getFullName(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) { + name = runtime.getFullName(name) + 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) RegisterLink(parent, child *Container, alias string) error { + fullName := path.Join(parent.Name, alias) + if !runtime.containerGraph.Exists(fullName) { + _, err := runtime.containerGraph.Set(fullName, child.ID) + return err + } + return nil +} + +// 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 +559,52 @@ 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 } + + gographPath := path.Join(config.GraphPath, "linkgraph.db") + initDatabase := false + if _, err := os.Stat(gographPath); err != nil { + if os.IsNotExist(err) { + initDatabase = true + } else { + return nil, err + } + } + conn, err := sql.Open("sqlite3", gographPath) + if err != nil { + return nil, err + } + graph, err := gograph.NewDatabase(conn, initDatabase) + if err != nil { + return nil, err + } + runtime := &Runtime{ - root: root, repository: runtimeRepo, containers: list.New(), networkManager: netManager, @@ -482,8 +612,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 { @@ -492,6 +623,11 @@ func NewRuntimeFromDirectory(root string, autoRestart bool) (*Runtime, error) { return runtime, nil } +func (runtime *Runtime) Close() error { + runtime.networkManager.Close() + return runtime.containerGraph.Close() +} + // History is a convenience type for storing a list of containers, // ordered by creation date. type History []*Container diff --git a/runtime_test.go b/runtime_test.go index 8e7c169bfb..c7cea75616 100644 --- a/runtime_test.go +++ b/runtime_test.go @@ -3,11 +3,13 @@ package docker import ( "bytes" "fmt" + "github.com/dotcloud/docker/sysinit" "github.com/dotcloud/docker/utils" "io" "log" "net" "os" + "path/filepath" "runtime" "strconv" "strings" @@ -42,8 +44,10 @@ func nuke(runtime *Runtime) error { }(container) } wg.Wait() - runtime.networkManager.Close() - return os.RemoveAll(runtime.root) + runtime.Close() + + os.Remove(filepath.Join(runtime.config.GraphPath, "linkgraph.db")) + return os.RemoveAll(runtime.config.GraphPath) } func cleanup(runtime *Runtime) error { @@ -77,7 +81,7 @@ func init() { // Hack to run sys init during unit testing if selfPath := utils.SelfPath(); selfPath == "/sbin/init" || selfPath == "/.dockerinit" { - SysInit() + sysinit.SysInit() return } @@ -85,7 +89,24 @@ func init() { log.Fatal("docker tests need to be run as root") } - NetworkBridgeIface = unitTestNetworkBridge + // Copy dockerinit into our current testing directory, if provided (so we can test a separate dockerinit binary) + if dockerinit := os.Getenv("TEST_DOCKERINIT_PATH"); dockerinit != "" { + src, err := os.Open(dockerinit) + if err != nil { + log.Fatalf("Unable to open TEST_DOCKERINIT_PATH: %s\n", err) + } + defer src.Close() + dst, err := os.OpenFile(filepath.Join(filepath.Dir(utils.SelfPath()), "dockerinit"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0555) + if err != nil { + log.Fatalf("Unable to create dockerinit in test directory: %s\n", err) + } + defer dst.Close() + if _, err := io.Copy(dst, src); err != nil { + log.Fatalf("Unable to copy dockerinit to TEST_DOCKERINIT_PATH: %s\n", err) + } + dst.Close() + src.Close() + } // Setup the base runtime, which will be duplicated for each test. // (no tests are run directly in the base) @@ -96,9 +117,13 @@ func init() { startFds, startGoroutines = utils.GetTotalUsedFds(), runtime.NumGoroutine() } - 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 +131,6 @@ func setupBaseImage() { // Create the "Server" srv := &Server{ runtime: runtime, - enableCors: false, pullingPool: make(map[string]struct{}), pushingPool: make(map[string]struct{}), } @@ -120,7 +144,6 @@ func setupBaseImage() { } } - func spawnGlobalDaemon() { if globalRuntime != nil { utils.Debugf("Global runtime already exists. Skipping.") @@ -129,7 +152,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,10 +193,11 @@ 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"}, }, + "", ) if err != nil { t.Fatal(err) @@ -211,16 +234,17 @@ 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{}, }, + "", ); err == nil { t.Fatal("Builder.Create should throw an error when Cmd is empty") } @@ -230,33 +254,22 @@ 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"}, - }) + }, "") if err != nil { t.Fatal(err) } @@ -327,6 +340,7 @@ func startEchoServerContainer(t *testing.T, proto string) (*Runtime, *Container, strPort string runtime = mkRuntime(t) port = 5554 + p Port ) for { @@ -340,22 +354,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)}, - }) - if container != nil { - break - } + 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 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 +395,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 +527,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 +555,266 @@ 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{"-i", "_", "/bin/sh"}, 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{"/" + container2.ID + ":first"} + if err := runtime1.RegisterLink(container1, container2, "first"); err != nil { + t.Fatal(err) + } + 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 but isn't", container1.ID) + } + + if len(runtime1.List()) != 2 { + t.Errorf("Expected 2 container, %v found", len(runtime1.List())) + } + + // 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("0")) + // Verify that the link is still registered in the runtime + entity := runtime2.containerGraph.Get(container1.Name) + 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, "some_name") + if err != nil { + t.Fatal(err) + } + container := runtime.Get(shortId) + containerID := container.ID + + if container.Name != "/some_name" { + t.Fatalf("Expect /some_name got %s", container.Name) + } + + 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 != "some_name" { + t.Fatalf("Expected some_name got %s", edge.Name) + } +} + +func TestRandomContainerName(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 container.Name == "" { + t.Fatalf("Expected not empty container name") + } + + 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 == "" { + t.Fatalf("Expected not empty container name") + } +} + +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, "/webapp") + if err != nil { + t.Fatal(err) + } + container := runtime.Get(shortId) + + 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.RegisterLink(webapp, childContainer, "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, "/webapp") + if err != nil { + t.Fatal(err) + } + container := runtime.Get(shortId) + + 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.RegisterLink(webapp, childContainer, "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..2cb6771178 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" @@ -23,6 +24,10 @@ import ( "time" ) +func (srv *Server) Close() error { + return srv.runtime.Close() +} + func (srv *Server) DockerVersion() APIVersion { return APIVersion{ Version: VERSION, @@ -114,7 +119,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 +156,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 +374,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 +396,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 +663,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 +872,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 +937,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, name string) (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 +949,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, name) if err != nil { if srv.runtime.graph.IsNotExist(err) { @@ -942,12 +958,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,8 +978,38 @@ func (srv *Server) ContainerRestart(name string, t int) error { return nil } -func (srv *Server) ContainerDestroy(name string, removeVolume bool) error { - if container := srv.runtime.Get(name); container != nil { +func (srv *Server) ContainerDestroy(name string, removeVolume, removeLink bool) error { + container := srv.runtime.Get(name) + + if removeLink { + if container == nil { + return fmt.Errorf("No such link: %s", name) + } + name = srv.runtime.getFullName(name) + + parent, n := path.Split(name) + + pe := srv.runtime.containerGraph.Get(parent) + if pe == nil { + return fmt.Errorf("Cannot get parent %s for name %s", parent, name) + } + 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 != nil { if container.State.Running { return fmt.Errorf("Impossible to remove a running container, please stop it first") } @@ -1161,15 +1207,55 @@ func (srv *Server) ImageGetCached(imgID string, config *Config) (*Image, error) return nil, nil } -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 { +func (srv *Server) RegisterLinks(name string, hostConfig *HostConfig) error { + 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 + } + child, err := srv.runtime.GetByName(parts["name"]) + if err != nil { + return err + } + if child == nil { + return fmt.Errorf("Could not get container for %s", parts["name"]) + } + + if err := runtime.RegisterLink(container, child, parts["alias"]); err != nil { + return err + } + } + + // After we load all the links into the runtime + // set them to nil on the hostconfig + hostConfig.Links = nil + if err := container.SaveHostConfig(hostConfig); err != nil { + return err + } + } + return nil +} + +func (srv *Server) ContainerStart(name string, hostConfig *HostConfig) error { + runtime := srv.runtime + container := runtime.Get(name) + if container == nil { + return fmt.Errorf("No such container: %s", name) + } + + 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 +1407,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 +1454,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..7a8ea4e41c 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,13 +224,14 @@ 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, CpuShares: 1000, Cmd: []string{"/bin/cat"}, }, + "", ); err == nil { t.Errorf("Memory limit is smaller than the allowed limit. Container creation should've failed!") } @@ -397,7 +398,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 +419,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..d4331eaf1f 100644 --- a/sorter.go +++ b/sorter.go @@ -34,3 +34,50 @@ 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) +} 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/state.go b/state.go index 484032be36..af7de9f4da 100644 --- a/state.go +++ b/state.go @@ -9,12 +9,12 @@ import ( type State struct { sync.Mutex - Running bool - Pid int - ExitCode int - StartedAt time.Time + Running bool + Pid int + ExitCode int + StartedAt time.Time FinishedAt time.Time - Ghost bool + Ghost bool } // String returns a human-readable description of the state diff --git a/sysinit.go b/sysinit/sysinit.go similarity index 85% rename from sysinit.go rename to sysinit/sysinit.go index c569d57264..a37db72423 100644 --- a/sysinit.go +++ b/sysinit/sysinit.go @@ -1,10 +1,12 @@ -package docker +package sysinit import ( "flag" "fmt" + "github.com/dotcloud/docker/netlink" "github.com/dotcloud/docker/utils" "log" + "net" "os" "os/exec" "strconv" @@ -17,7 +19,14 @@ func setupNetworking(gw string) { if gw == "" { return } - if _, err := ip("route", "add", "default", "via", gw); err != nil { + + ip := net.ParseIP(gw) + if ip == nil { + log.Fatalf("Unable to set up networking, %s is not a valid IP", gw) + return + } + + if err := netlink.AddDefaultGw(ip); err != nil { log.Fatalf("Unable to set up networking: %v", err) } } @@ -60,7 +69,7 @@ func changeUser(u string) { } // Clear environment pollution introduced by lxc-start -func cleanupEnv(env ListOpts) { +func cleanupEnv(env utils.ListOpts) { os.Clearenv() for _, kv := range env { parts := strings.SplitN(kv, "=", 2) @@ -88,14 +97,14 @@ func executeProgram(name string, args []string) { // up the environment before running the actual process func SysInit() { if len(os.Args) <= 1 { - fmt.Println("You should not invoke docker-init manually") + fmt.Println("You should not invoke dockerinit manually") os.Exit(1) } var u = flag.String("u", "", "username or uid") var gw = flag.String("g", "", "gateway address") var workdir = flag.String("w", "", "workdir") - var flEnv ListOpts + var flEnv utils.ListOpts flag.Var(&flEnv, "e", "Set environment variables") flag.Parse() diff --git a/utils.go b/utils.go index 8d821e87ff..cffee05615 100644 --- a/utils.go +++ b/utils.go @@ -2,6 +2,9 @@ package docker import ( "fmt" + "github.com/dotcloud/docker/namesgenerator" + "github.com/dotcloud/docker/utils" + "strconv" "strings" ) @@ -27,6 +30,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 +56,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 +87,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{}{} } } } @@ -155,7 +176,7 @@ func MergeConfig(userConf, imageConf *Config) error { return nil } -func parseLxcConfOpts(opts ListOpts) ([]KeyValuePair, error) { +func parseLxcConfOpts(opts utils.ListOpts) ([]KeyValuePair, error) { out := make([]KeyValuePair, len(opts)) for i, o := range opts { k, v, err := parseLxcOpt(o) @@ -174,3 +195,115 @@ 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) +} + +type checker struct { + runtime *Runtime +} + +func (c *checker) Exists(name string) bool { + return c.runtime.containerGraph.Exists("/" + name) +} + +// Generate a random and unique name +func generateRandomName(runtime *Runtime) string { + n, err := namesgenerator.GenerateRandomName(&checker{runtime}) + if err != nil { + panic(err) + } + return n +} diff --git a/utils/utils.go b/utils/utils.go index 389203b985..d53094397f 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -2,6 +2,7 @@ package utils import ( "bytes" + "crypto/sha1" "crypto/sha256" "encoding/hex" "encoding/json" @@ -21,6 +22,23 @@ import ( "time" ) +var ( + IAMSTATIC bool // whether or not Docker itself was compiled statically via ./hack/make.sh binary + INITSHA1 string // sha1sum of separate static dockerinit, if Docker itself was compiled dynamically via ./hack/make.sh dynbinary +) + +// ListOpts type +type ListOpts []string + +func (opts *ListOpts) String() string { + return fmt.Sprint(*opts) +} + +func (opts *ListOpts) Set(value string) error { + *opts = append(*opts, value) + return nil +} + // Go is a basic promise implementation: it wraps calls a function in a goroutine, // and returns a channel which will later return the function's return value. func Go(f func() error) chan error { @@ -178,6 +196,67 @@ func SelfPath() string { return path } +func dockerInitSha1(target string) string { + f, err := os.Open(target) + if err != nil { + return "" + } + defer f.Close() + h := sha1.New() + _, err = io.Copy(h, f) + if err != nil { + return "" + } + return hex.EncodeToString(h.Sum(nil)) +} + +func isValidDockerInitPath(target string, selfPath string) bool { // target and selfPath should be absolute (InitPath and SelfPath already do this) + if IAMSTATIC { + if target == selfPath { + return true + } + targetFileInfo, err := os.Lstat(target) + if err != nil { + return false + } + selfPathFileInfo, err := os.Lstat(selfPath) + if err != nil { + return false + } + return os.SameFile(targetFileInfo, selfPathFileInfo) + } + return INITSHA1 != "" && dockerInitSha1(target) == INITSHA1 +} + +// Figure out the path of our dockerinit (which may be SelfPath()) +func DockerInitPath() string { + selfPath := SelfPath() + if isValidDockerInitPath(selfPath, selfPath) { + // if we're valid, don't bother checking anything else + return selfPath + } + var possibleInits = []string{ + filepath.Join(filepath.Dir(selfPath), "dockerinit"), + // "/usr/libexec includes internal binaries that are not intended to be executed directly by users or shell scripts. Applications may use a single subdirectory under /usr/libexec." + "/usr/libexec/docker/dockerinit", + "/usr/local/libexec/docker/dockerinit", + } + for _, dockerInit := range possibleInits { + path, err := exec.LookPath(dockerInit) + if err == nil { + path, err = filepath.Abs(path) + if err != nil { + // LookPath already validated that this file exists and is executable (following symlinks), so how could Abs fail? + panic(err) + } + if isValidDockerInitPath(path, selfPath) { + return path + } + } + } + return "" +} + type NopWriter struct{} func (*NopWriter) Write(buf []byte) (int, error) { @@ -1044,3 +1123,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..439cf0a7d1 100644 --- a/utils_test.go +++ b/utils_test.go @@ -1,15 +1,15 @@ package docker import ( - "github.com/dotcloud/docker/utils" "fmt" + "github.com/dotcloud/docker/utils" "io" "io/ioutil" "os" "path" + "runtime" "strings" "testing" - "runtime" ) // This file contains utility functions for docker's unit test suite. @@ -26,7 +26,7 @@ func mkRuntime(f Fataler) *Runtime { pc, _, _, _ := runtime.Caller(1) callerLongName := runtime.FuncForPC(pc).Name() parts := strings.Split(callerLongName, ".") - callerShortName := parts[len(parts) - 1] + callerShortName := parts[len(parts)-1] if globalTestID == "" { globalTestID = GenerateID()[:4] } @@ -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 +}