diff --git a/api.go b/api.go index dd33f40a66..de4113231c 100644 --- a/api.go +++ b/api.go @@ -551,11 +551,20 @@ func deleteImages(srv *Server, version float64, w http.ResponseWriter, r *http.R } func postContainersStart(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + hostConfig := &HostConfig{} + + // allow a nil body for backwards compatibility + if r.Body != nil { + if err := json.NewDecoder(r.Body).Decode(hostConfig); err != nil { + return err + } + } + if vars == nil { return fmt.Errorf("Missing parameter") } name := vars["name"] - if err := srv.ContainerStart(name); err != nil { + if err := srv.ContainerStart(name, hostConfig); err != nil { return err } w.WriteHeader(http.StatusNoContent) diff --git a/api_test.go b/api_test.go index 12ea5ee962..c2e7db4bd0 100644 --- a/api_test.go +++ b/api_test.go @@ -873,7 +873,8 @@ func TestPostContainersKill(t *testing.T) { } defer runtime.Destroy(container) - if err := container.Start(); err != nil { + hostConfig := &HostConfig{} + if err := container.Start(hostConfig); err != nil { t.Fatal(err) } @@ -917,7 +918,8 @@ func TestPostContainersRestart(t *testing.T) { } defer runtime.Destroy(container) - if err := container.Start(); err != nil { + hostConfig := &HostConfig{} + if err := container.Start(hostConfig); err != nil { t.Fatal(err) } @@ -973,8 +975,15 @@ func TestPostContainersStart(t *testing.T) { } defer runtime.Destroy(container) + hostConfigJSON, err := json.Marshal(&HostConfig{}) + + req, err := http.NewRequest("POST", "/containers/"+container.ID+"/start", bytes.NewReader(hostConfigJSON)) + if err != nil { + t.Fatal(err) + } + r := httptest.NewRecorder() - if err := postContainersStart(srv, APIVERSION, r, nil, map[string]string{"name": container.ID}); err != nil { + if err := postContainersStart(srv, APIVERSION, r, req, map[string]string{"name": container.ID}); err != nil { t.Fatal(err) } if r.Code != http.StatusNoContent { @@ -989,7 +998,7 @@ func TestPostContainersStart(t *testing.T) { } r = httptest.NewRecorder() - if err = postContainersStart(srv, APIVERSION, r, nil, map[string]string{"name": container.ID}); err == nil { + if err = postContainersStart(srv, APIVERSION, r, req, map[string]string{"name": container.ID}); err == nil { t.Fatalf("A running containter should be able to be started") } @@ -1019,7 +1028,8 @@ func TestPostContainersStop(t *testing.T) { } defer runtime.Destroy(container) - if err := container.Start(); err != nil { + hostConfig := &HostConfig{} + if err := container.Start(hostConfig); err != nil { t.Fatal(err) } @@ -1068,7 +1078,8 @@ func TestPostContainersWait(t *testing.T) { } defer runtime.Destroy(container) - if err := container.Start(); err != nil { + hostConfig := &HostConfig{} + if err := container.Start(hostConfig); err != nil { t.Fatal(err) } @@ -1113,7 +1124,8 @@ func TestPostContainersAttach(t *testing.T) { defer runtime.Destroy(container) // Start the process - if err := container.Start(); err != nil { + hostConfig := &HostConfig{} + if err := container.Start(hostConfig); err != nil { t.Fatal(err) } diff --git a/buildfile.go b/buildfile.go index 9cbaac4e76..63625e1a40 100644 --- a/buildfile.go +++ b/buildfile.go @@ -87,7 +87,7 @@ func (b *buildFile) CmdRun(args string) error { if b.image == "" { return fmt.Errorf("Please provide a source image with `from` prior to run") } - config, _, err := ParseRun([]string{b.image, "/bin/sh", "-c", args}, nil) + config, _, _, err := ParseRun([]string{b.image, "/bin/sh", "-c", args}, nil) if err != nil { return err } @@ -263,7 +263,8 @@ func (b *buildFile) run() (string, error) { fmt.Fprintf(b.out, " ---> Running in %s\n", utils.TruncateID(c.ID)) //start the container - if err := c.Start(); err != nil { + hostConfig := &HostConfig{} + if err := c.Start(hostConfig); err != nil { return "", err } diff --git a/commands.go b/commands.go index 7175c07b0b..6e83a79c60 100644 --- a/commands.go +++ b/commands.go @@ -1235,7 +1235,7 @@ func (cli *DockerCli) CmdTag(args ...string) error { } func (cli *DockerCli) CmdRun(args ...string) error { - config, cmd, err := ParseRun(args, nil) + config, hostConfig, cmd, err := ParseRun(args, nil) if err != nil { return err } @@ -1274,7 +1274,7 @@ func (cli *DockerCli) CmdRun(args ...string) error { } //start the container - if _, _, err = cli.call("POST", "/containers/"+runResult.ID+"/start", nil); err != nil { + if _, _, err = cli.call("POST", "/containers/"+runResult.ID+"/start", hostConfig); err != nil { return err } diff --git a/container.go b/container.go index d9fcf1c76e..77f71ba684 100644 --- a/container.go +++ b/container.go @@ -52,6 +52,8 @@ type Container struct { waitLock chan struct{} Volumes map[string]string + + Binds []BindMap } type Config struct { @@ -75,7 +77,17 @@ type Config struct { VolumesFrom string } -func ParseRun(args []string, capabilities *Capabilities) (*Config, *flag.FlagSet, error) { +type HostConfig struct { + Binds []string +} + +type BindMap struct { + SrcPath string + DstPath string + Mode string +} + +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 len(args) > 0 && args[0] != "--help" { cmd.SetOutput(ioutil.Discard) @@ -111,11 +123,14 @@ func ParseRun(args []string, capabilities *Capabilities) (*Config, *flag.FlagSet flVolumesFrom := cmd.String("volumes-from", "", "Mount volumes from the specified container") + var flBinds ListOpts + cmd.Var(&flBinds, "b", "Bind mount a volume from the host (e.g. -b /host:/container)") + if err := cmd.Parse(args); err != nil { - return nil, cmd, err + return nil, nil, cmd, err } if *flDetach && len(flAttach) > 0 { - return nil, cmd, fmt.Errorf("Conflicting options: -a and -d") + return nil, nil, cmd, fmt.Errorf("Conflicting options: -a and -d") } // If neither -d or -a are set, attach to everything by default if len(flAttach) == 0 && !*flDetach { @@ -127,6 +142,15 @@ func ParseRun(args []string, capabilities *Capabilities) (*Config, *flag.FlagSet } } } + + // add any bind targets to the list of container volumes + type empty struct{} + for _, bind := range flBinds { + arr := strings.Split(bind, ":") + dstDir := arr[1] + flVolumes[dstDir] = empty{} + } + parsedArgs := cmd.Args() runCmd := []string{} image := "" @@ -154,6 +178,9 @@ func ParseRun(args []string, capabilities *Capabilities) (*Config, *flag.FlagSet Volumes: flVolumes, VolumesFrom: *flVolumesFrom, } + hostConfig := &HostConfig{ + Binds: flBinds, + } if capabilities != nil && *flMemory > 0 && !capabilities.SwapLimit { //fmt.Fprintf(stdout, "WARNING: Your kernel does not support swap limit capabilities. Limitation discarded.\n") @@ -164,7 +191,7 @@ func ParseRun(args []string, capabilities *Capabilities) (*Config, *flag.FlagSet if config.OpenStdin && config.AttachStdin { config.StdinOnce = true } - return config, cmd, nil + return config, hostConfig, cmd, nil } type NetworkSettings struct { @@ -430,7 +457,7 @@ func (container *Container) Attach(stdin io.ReadCloser, stdinCloser io.Closer, s }) } -func (container *Container) Start() error { +func (container *Container) Start(hostConfig *HostConfig) error { container.State.lock() defer container.State.unlock() @@ -483,6 +510,42 @@ func (container *Container) Start() error { } } + // Create the requested bind mounts + binds := []BindMap{} + // Define illegal container destinations + illegal_dsts := []string{"/", "."} + + for _, bind := range hostConfig.Binds { + var src, dst, mode string + arr := strings.Split(bind, ":") + if len(arr) == 2 { + src = arr[0] + dst = arr[1] + mode = "rw" + } else if len(arr) == 3 { + src = arr[0] + dst = arr[1] + mode = arr[2] + } else { + return fmt.Errorf("Invalid bind specification: %s", bind) + } + + // Bail if trying to mount to an illegal destination + for _, illegal := range illegal_dsts { + if dst == illegal { + return fmt.Errorf("Illegal bind destination: %s", dst) + } + } + + bindMap := BindMap{ + SrcPath: src, + DstPath: dst, + Mode: mode, + } + binds = append(binds, bindMap) + } + container.Binds = binds + if err := container.generateLXCConfig(); err != nil { return err } @@ -552,7 +615,8 @@ func (container *Container) Start() error { } func (container *Container) Run() error { - if err := container.Start(); err != nil { + hostConfig := &HostConfig{} + if err := container.Start(hostConfig); err != nil { return err } container.Wait() @@ -565,7 +629,8 @@ func (container *Container) Output() (output []byte, err error) { return nil, err } defer pipe.Close() - if err := container.Start(); err != nil { + hostConfig := &HostConfig{} + if err := container.Start(hostConfig); err != nil { return nil, err } output, err = ioutil.ReadAll(pipe) @@ -768,7 +833,8 @@ func (container *Container) Restart(seconds int) error { if err := container.Stop(seconds); err != nil { return err } - if err := container.Start(); err != nil { + hostConfig := &HostConfig{} + if err := container.Start(hostConfig); err != nil { return err } return nil diff --git a/container_test.go b/container_test.go index 736f6814d0..cefbd8cb05 100644 --- a/container_test.go +++ b/container_test.go @@ -7,6 +7,7 @@ import ( "io/ioutil" "math/rand" "os" + "path" "regexp" "sort" "strings" @@ -70,7 +71,8 @@ func TestMultipleAttachRestart(t *testing.T) { if err != nil { t.Fatal(err) } - if err := container.Start(); err != nil { + hostConfig := &HostConfig{} + if err := container.Start(hostConfig); err != nil { t.Fatal(err) } l1, err := bufio.NewReader(stdout1).ReadString('\n') @@ -111,7 +113,7 @@ func TestMultipleAttachRestart(t *testing.T) { if err != nil { t.Fatal(err) } - if err := container.Start(); err != nil { + if err := container.Start(hostConfig); err != nil { t.Fatal(err) } @@ -306,7 +308,8 @@ func TestCommitAutoRun(t *testing.T) { if err != nil { t.Fatal(err) } - if err := container2.Start(); err != nil { + hostConfig := &HostConfig{} + if err := container2.Start(hostConfig); err != nil { t.Fatal(err) } container2.Wait() @@ -388,7 +391,8 @@ func TestCommitRun(t *testing.T) { if err != nil { t.Fatal(err) } - if err := container2.Start(); err != nil { + hostConfig := &HostConfig{} + if err := container2.Start(hostConfig); err != nil { t.Fatal(err) } container2.Wait() @@ -436,7 +440,8 @@ func TestStart(t *testing.T) { t.Fatal(err) } - if err := container.Start(); err != nil { + hostConfig := &HostConfig{} + if err := container.Start(hostConfig); err != nil { t.Fatal(err) } @@ -446,7 +451,7 @@ func TestStart(t *testing.T) { if !container.State.Running { t.Errorf("Container should be running") } - if err := container.Start(); err == nil { + if err := container.Start(hostConfig); err == nil { t.Fatalf("A running containter should be able to be started") } @@ -528,7 +533,8 @@ func TestKillDifferentUser(t *testing.T) { if container.State.Running { t.Errorf("Container shouldn't be running") } - if err := container.Start(); err != nil { + hostConfig := &HostConfig{} + if err := container.Start(hostConfig); err != nil { t.Fatal(err) } @@ -599,7 +605,8 @@ func TestKill(t *testing.T) { if container.State.Running { t.Errorf("Container shouldn't be running") } - if err := container.Start(); err != nil { + hostConfig := &HostConfig{} + if err := container.Start(hostConfig); err != nil { t.Fatal(err) } @@ -724,7 +731,8 @@ func TestRestartStdin(t *testing.T) { if err != nil { t.Fatal(err) } - if err := container.Start(); err != nil { + hostConfig := &HostConfig{} + if err := container.Start(hostConfig); err != nil { t.Fatal(err) } if _, err := io.WriteString(stdin, "hello world"); err != nil { @@ -754,7 +762,7 @@ func TestRestartStdin(t *testing.T) { if err != nil { t.Fatal(err) } - if err := container.Start(); err != nil { + if err := container.Start(hostConfig); err != nil { t.Fatal(err) } if _, err := io.WriteString(stdin, "hello world #2"); err != nil { @@ -916,10 +924,11 @@ func TestMultipleContainers(t *testing.T) { defer runtime.Destroy(container2) // Start both containers - if err := container1.Start(); err != nil { + hostConfig := &HostConfig{} + if err := container1.Start(hostConfig); err != nil { t.Fatal(err) } - if err := container2.Start(); err != nil { + if err := container2.Start(hostConfig); err != nil { t.Fatal(err) } @@ -971,7 +980,8 @@ func TestStdin(t *testing.T) { if err != nil { t.Fatal(err) } - if err := container.Start(); err != nil { + hostConfig := &HostConfig{} + if err := container.Start(hostConfig); err != nil { t.Fatal(err) } defer stdin.Close() @@ -1018,7 +1028,8 @@ func TestTty(t *testing.T) { if err != nil { t.Fatal(err) } - if err := container.Start(); err != nil { + hostConfig := &HostConfig{} + if err := container.Start(hostConfig); err != nil { t.Fatal(err) } defer stdin.Close() @@ -1060,7 +1071,8 @@ func TestEnv(t *testing.T) { t.Fatal(err) } defer stdout.Close() - if err := container.Start(); err != nil { + hostConfig := &HostConfig{} + if err := container.Start(hostConfig); err != nil { t.Fatal(err) } container.Wait() @@ -1196,7 +1208,8 @@ func BenchmarkRunParallel(b *testing.B) { return } defer runtime.Destroy(container) - if err := container.Start(); err != nil { + hostConfig := &HostConfig{} + if err := container.Start(hostConfig); err != nil { complete <- err return } @@ -1225,3 +1238,98 @@ func BenchmarkRunParallel(b *testing.B) { b.Fatal(errors) } } + +func TestBindMounts(t *testing.T) { + runtime, err := newTestRuntime() + if err != nil { + t.Fatal(err) + } + defer nuke(runtime) + tmpDir, err := ioutil.TempDir("", "docker-test") + if err != nil { + t.Fatal(err) + } + tmpFile := path.Join(tmpDir, "touch-me") + _, err = os.Create(tmpFile) + if err != nil { + t.Fatal(err) + } + // test reading from bind mount + bind_str := fmt.Sprintf("%s:/tmp:ro", tmpDir) + container, err := NewBuilder(runtime).Create(&Config{ + Image: GetTestImage(runtime).ID, + Cmd: []string{"ls", "/tmp"}, + }, + ) + if err != nil { + t.Fatal(err) + } + defer runtime.Destroy(container) + + stdout, err := container.StdoutPipe() + if err != nil { + t.Fatal(err) + } + defer stdout.Close() + hostConfig := &HostConfig{ + Binds: []string{bind_str}, + } + if err := container.Start(hostConfig); err != nil { + t.Fatal(err) + } + container.Wait() + output, err := ioutil.ReadAll(stdout) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(output), "touch-me") { + t.Fatal("Container failed to read from bind mount") + } + // test writing to bind mount + bind_str2 := fmt.Sprintf("%s:/tmp:rw", tmpDir) + container2, err := NewBuilder(runtime).Create(&Config{ + Image: GetTestImage(runtime).ID, + Cmd: []string{"touch", "/tmp/holla"}, + }, + ) + if err != nil { + t.Fatal(err) + } + defer runtime.Destroy(container2) + + hostConfig2 := &HostConfig{ + Binds: []string{bind_str2}, + } + if err := container2.Start(hostConfig2); err != nil { + t.Fatal(err) + } + container2.Wait() + _, err = ioutil.ReadFile(tmpDir + "/holla") + if err != nil { + t.Fatal("Container failed to write to bind mount") + } + // test mounting to an illegal destination directory + bind_str3 := fmt.Sprintf("%s:.", tmpDir) + container3, err := NewBuilder(runtime).Create(&Config{ + Image: GetTestImage(runtime).ID, + Cmd: []string{"ls", "."}, + }, + ) + if err != nil { + t.Fatal(err) + } + defer runtime.Destroy(container3) + + stdout3, err := container3.StdoutPipe() + if err != nil { + t.Fatal(err) + } + defer stdout3.Close() + hostConfig3 := &HostConfig{ + Binds: []string{bind_str3}, + } + if err := container3.Start(hostConfig3); err == nil { + t.Fatal("Container bind mounted illegal directory") + } + +} diff --git a/docs/sources/api/docker_remote_api.rst b/docs/sources/api/docker_remote_api.rst index 5c3a7ba2cb..c6e50a3f06 100644 --- a/docs/sources/api/docker_remote_api.rst +++ b/docs/sources/api/docker_remote_api.rst @@ -42,6 +42,9 @@ List containers (/containers/json): - You can use size=1 to get the size of the containers +Start containers (/containers//start): + +- You can now pass host-specific configuration (e.g. bind mounts) in the POST body for start calls :doc:`docker_remote_api_v1.2` ***************************** diff --git a/docs/sources/api/docker_remote_api_v1.3.rst b/docs/sources/api/docker_remote_api_v1.3.rst index a8e14ec461..8eeb010d98 100644 --- a/docs/sources/api/docker_remote_api_v1.3.rst +++ b/docs/sources/api/docker_remote_api_v1.3.rst @@ -294,23 +294,30 @@ Start a container .. http:post:: /containers/(id)/start - Start the container ``id`` + Start the container ``id`` - **Example request**: + **Example request**: - .. sourcecode:: http + .. sourcecode:: http - POST /containers/e90e34656806/start HTTP/1.1 - - **Example response**: + POST /containers/(id)/start HTTP/1.1 + Content-Type: application/json - .. sourcecode:: http + { + "Binds":["/tmp:/tmp"] + } - HTTP/1.1 200 OK - - :statuscode 200: no error - :statuscode 404: no such container - :statuscode 500: server error + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 204 No Content + Content-Type: text/plain + + :jsonparam hostConfig: the container's host configuration (optional) + :statuscode 200: no error + :statuscode 404: no such container + :statuscode 500: server error Stop a contaier diff --git a/docs/sources/commandline/command/run.rst b/docs/sources/commandline/command/run.rst index 8227d9f310..c119ce11e2 100644 --- a/docs/sources/commandline/command/run.rst +++ b/docs/sources/commandline/command/run.rst @@ -25,3 +25,4 @@ -d=[]: Set custom dns servers for the container -v=[]: Creates a new volume and mounts it at the specified path. -volumes-from="": Mount all volumes from the given container. + -b=[]: Create a bind mount with: [host-dir]:[container-dir]:[rw|ro] diff --git a/lxc_template.go b/lxc_template.go index 45408d4bfb..3f15b9abba 100644 --- a/lxc_template.go +++ b/lxc_template.go @@ -88,6 +88,10 @@ lxc.mount.entry = {{.ResolvConfPath}} {{$ROOTFS}}/etc/resolv.conf none bind,ro 0 lxc.mount.entry = {{$realPath}} {{$ROOTFS}}/{{$virtualPath}} none bind,rw 0 0 {{end}} {{end}} +{{if .Binds}}# User-defined bind mounts +{{range $bindMap := .Binds}}lxc.mount.entry = {{$bindMap.SrcPath}} {{$ROOTFS}}{{$bindMap.DstPath}} none bind,{{$bindMap.Mode}} 0 0 +{{end}} +{{end}} # drop linux capabilities (apply mainly to the user root in the container) # (Note: 'lxc.cap.keep' is coming soon and should replace this under the diff --git a/runtime.go b/runtime.go index 29f5067931..06b1f8e1b9 100644 --- a/runtime.go +++ b/runtime.go @@ -144,7 +144,9 @@ func (runtime *Runtime) Register(container *Container) error { utils.Debugf("Restarting") container.State.Ghost = false container.State.setStopped(0) - if err := container.Start(); err != nil { + // assume empty host config + hostConfig := &HostConfig{} + if err := container.Start(hostConfig); err != nil { return err } nomonitor = true diff --git a/runtime_test.go b/runtime_test.go index 23b1be0257..61a8f346f7 100644 --- a/runtime_test.go +++ b/runtime_test.go @@ -327,7 +327,8 @@ func findAvailalblePort(runtime *Runtime, port int) (*Container, error) { if err != nil { return nil, err } - if err := container.Start(); err != nil { + hostConfig := &HostConfig{} + if err := container.Start(hostConfig); err != nil { if strings.Contains(err.Error(), "address already in use") { return nil, nil } @@ -437,7 +438,8 @@ func TestRestore(t *testing.T) { defer runtime1.Destroy(container2) // Start the container non blocking - if err := container2.Start(); err != nil { + hostConfig := &HostConfig{} + if err := container2.Start(hostConfig); err != nil { t.Fatal(err) } diff --git a/server.go b/server.go index ee023044a0..cdc01c6b09 100644 --- a/server.go +++ b/server.go @@ -87,7 +87,7 @@ func (srv *Server) ImageInsert(name, url, path string, out io.Writer, sf *utils. } defer file.Body.Close() - config, _, err := ParseRun([]string{img.ID, "echo", "insert", url, path}, srv.runtime.capabilities) + config, _, _, err := ParseRun([]string{img.ID, "echo", "insert", url, path}, srv.runtime.capabilities) if err != nil { return "", err } @@ -934,9 +934,9 @@ func (srv *Server) ImageGetCached(imgId string, config *Config) (*Image, error) return nil, nil } -func (srv *Server) ContainerStart(name string) error { +func (srv *Server) ContainerStart(name string, hostConfig *HostConfig) error { if container := srv.runtime.Get(name); container != nil { - if err := container.Start(); err != nil { + if err := container.Start(hostConfig); err != nil { return fmt.Errorf("Error starting container %s: %s", name, err.Error()) } } else { diff --git a/server_test.go b/server_test.go index 7fdec18f61..8d1ea8be94 100644 --- a/server_test.go +++ b/server_test.go @@ -65,7 +65,7 @@ func TestCreateRm(t *testing.T) { srv := &Server{runtime: runtime} - config, _, err := ParseRun([]string{GetTestImage(runtime).ID, "echo test"}, nil) + config, _, _, err := ParseRun([]string{GetTestImage(runtime).ID, "echo test"}, nil) if err != nil { t.Fatal(err) } @@ -98,7 +98,7 @@ func TestCreateStartRestartStopStartKillRm(t *testing.T) { srv := &Server{runtime: runtime} - config, _, err := ParseRun([]string{GetTestImage(runtime).ID, "/bin/cat"}, nil) + config, hostConfig, _, err := ParseRun([]string{GetTestImage(runtime).ID, "/bin/cat"}, nil) if err != nil { t.Fatal(err) } @@ -112,7 +112,7 @@ func TestCreateStartRestartStopStartKillRm(t *testing.T) { t.Errorf("Expected 1 container, %v found", len(runtime.List())) } - err = srv.ContainerStart(id) + err = srv.ContainerStart(id, hostConfig) if err != nil { t.Fatal(err) } @@ -127,7 +127,7 @@ func TestCreateStartRestartStopStartKillRm(t *testing.T) { t.Fatal(err) } - err = srv.ContainerStart(id) + err = srv.ContainerStart(id, hostConfig) if err != nil { t.Fatal(err) }