1
0
mirror of https://github.com/moby/moby.git synced 2025-07-29 07:21:35 +03:00

Add new HostConfig field, Mounts.

`Mounts` allows users to specify in a much safer way the volumes they
want to use in the container.
This replaces `Binds` and `Volumes`, which both still exist, but
`Mounts` and `Binds`/`Volumes` are exclussive.
The CLI will continue to use `Binds` and `Volumes` due to concerns with
parsing the volume specs on the client side and cross-platform support
(for now).

The new API follows exactly the services mount API.

Example usage of `Mounts`:

```
$ curl -XPOST localhost:2375/containers/create -d '{
  "Image": "alpine:latest",
  "HostConfig": {
    "Mounts": [{
      "Type": "Volume",
      "Target": "/foo"
      },{
      "Type": "bind",
      "Source": "/var/run/docker.sock",
      "Target": "/var/run/docker.sock",
      },{
      "Type": "volume",
      "Name": "important_data",
      "Target": "/var/data",
      "ReadOnly": true,
      "VolumeOptions": {
	"DriverConfig": {
	  Name: "awesomeStorage",
	  Options: {"size": "10m"},
	  Labels: {"some":"label"}
	}
      }]
    }
}'
```

There are currently 2 types of mounts:

  - **bind**: Paths on the host that get mounted into the
    container. Paths must exist prior to creating the container.
  - **volume**: Volumes that persist after the
    container is removed.

Not all fields are available in each type, and validation is done to
ensure these fields aren't mixed up between types.

Signed-off-by: Brian Goff <cpuguy83@gmail.com>
This commit is contained in:
Brian Goff
2016-04-26 14:25:35 -04:00
parent 675144ff8d
commit fc7b904dce
30 changed files with 922 additions and 330 deletions

View File

@ -6,10 +6,12 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httputil"
"net/url"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
@ -17,10 +19,14 @@ import (
"github.com/docker/docker/api/types"
containertypes "github.com/docker/docker/api/types/container"
mounttypes "github.com/docker/docker/api/types/mount"
networktypes "github.com/docker/docker/api/types/network"
"github.com/docker/docker/pkg/integration"
"github.com/docker/docker/pkg/integration/checker"
"github.com/docker/docker/pkg/ioutils"
"github.com/docker/docker/pkg/mount"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/docker/volume"
"github.com/go-check/check"
)
@ -1525,3 +1531,212 @@ func (s *DockerSuite) TestContainerApiStatsWithNetworkDisabled(c *check.C) {
c.Assert(dec.Decode(&s), checker.IsNil)
}
}
func (s *DockerSuite) TestContainersApiCreateMountsValidation(c *check.C) {
type m mounttypes.Mount
type hc struct{ Mounts []m }
type cfg struct {
Image string
HostConfig hc
}
type testCase struct {
config cfg
status int
msg string
}
prefix, slash := getPrefixAndSlashFromDaemonPlatform()
destPath := prefix + slash + "foo"
notExistPath := prefix + slash + "notexist"
cases := []testCase{
{cfg{Image: "busybox", HostConfig: hc{Mounts: []m{{Type: "notreal", Target: destPath}}}}, http.StatusBadRequest, "mount type unknown"},
{cfg{Image: "busybox", HostConfig: hc{Mounts: []m{{Type: "bind"}}}}, http.StatusBadRequest, "Target must not be empty"},
{cfg{Image: "busybox", HostConfig: hc{Mounts: []m{{Type: "bind", Target: destPath}}}}, http.StatusBadRequest, "Source must not be empty"},
{cfg{Image: "busybox", HostConfig: hc{Mounts: []m{{Type: "bind", Source: notExistPath, Target: destPath}}}}, http.StatusBadRequest, "bind source path does not exist"},
{cfg{Image: "busybox", HostConfig: hc{Mounts: []m{{Type: "volume"}}}}, http.StatusBadRequest, "Target must not be empty"},
{cfg{Image: "busybox", HostConfig: hc{Mounts: []m{{Type: "volume", Source: "hello", Target: destPath}}}}, http.StatusCreated, ""},
{cfg{Image: "busybox", HostConfig: hc{Mounts: []m{{Type: "volume", Source: "hello2", Target: destPath, VolumeOptions: &mounttypes.VolumeOptions{DriverConfig: &mounttypes.Driver{Name: "local"}}}}}}, http.StatusCreated, ""},
}
if SameHostDaemon.Condition() {
tmpDir, err := ioutils.TempDir("", "test-mounts-api")
c.Assert(err, checker.IsNil)
defer os.RemoveAll(tmpDir)
cases = append(cases, []testCase{
{cfg{Image: "busybox", HostConfig: hc{Mounts: []m{{Type: "bind", Source: tmpDir, Target: destPath}}}}, http.StatusCreated, ""},
{cfg{Image: "busybox", HostConfig: hc{Mounts: []m{{Type: "bind", Source: tmpDir, Target: destPath, VolumeOptions: &mounttypes.VolumeOptions{}}}}}, http.StatusBadRequest, "VolumeOptions must not be specified"},
}...)
}
if DaemonIsLinux.Condition() {
cases = append(cases, []testCase{
{cfg{Image: "busybox", HostConfig: hc{Mounts: []m{{Type: "volume", Source: "hello3", Target: destPath, VolumeOptions: &mounttypes.VolumeOptions{DriverConfig: &mounttypes.Driver{Name: "local", Options: map[string]string{"o": "size=1"}}}}}}}, http.StatusCreated, ""},
}...)
}
for i, x := range cases {
c.Logf("case %d", i)
status, b, err := sockRequest("POST", "/containers/create", x.config)
c.Assert(err, checker.IsNil)
c.Assert(status, checker.Equals, x.status, check.Commentf("%s\n%v", string(b), cases[i].config))
if len(x.msg) > 0 {
c.Assert(string(b), checker.Contains, x.msg, check.Commentf("%v", cases[i].config))
}
}
}
func (s *DockerSuite) TestContainerApiCreateMountsBindRead(c *check.C) {
testRequires(c, NotUserNamespace, SameHostDaemon)
// also with data in the host side
prefix, slash := getPrefixAndSlashFromDaemonPlatform()
destPath := prefix + slash + "foo"
tmpDir, err := ioutil.TempDir("", "test-mounts-api-bind")
c.Assert(err, checker.IsNil)
defer os.RemoveAll(tmpDir)
err = ioutil.WriteFile(filepath.Join(tmpDir, "bar"), []byte("hello"), 666)
c.Assert(err, checker.IsNil)
data := map[string]interface{}{
"Image": "busybox",
"Cmd": []string{"/bin/sh", "-c", "cat /foo/bar"},
"HostConfig": map[string]interface{}{"Mounts": []map[string]interface{}{{"Type": "bind", "Source": tmpDir, "Target": destPath}}},
}
status, resp, err := sockRequest("POST", "/containers/create?name=test", data)
c.Assert(err, checker.IsNil, check.Commentf(string(resp)))
c.Assert(status, checker.Equals, http.StatusCreated, check.Commentf(string(resp)))
out, _ := dockerCmd(c, "start", "-a", "test")
c.Assert(out, checker.Equals, "hello")
}
// Test Mounts comes out as expected for the MountPoint
func (s *DockerSuite) TestContainersApiCreateMountsCreate(c *check.C) {
prefix, slash := getPrefixAndSlashFromDaemonPlatform()
destPath := prefix + slash + "foo"
var (
err error
testImg string
)
if daemonPlatform != "windows" {
testImg, err = buildImage("test-mount-config", `
FROM busybox
RUN mkdir `+destPath+` && touch `+destPath+slash+`bar
CMD cat `+destPath+slash+`bar
`, true)
} else {
testImg = "busybox"
}
c.Assert(err, checker.IsNil)
type testCase struct {
cfg mounttypes.Mount
expected types.MountPoint
}
cases := []testCase{
// use literal strings here for `Type` instead of the defined constants in the volume package to keep this honest
// Validation of the actual `Mount` struct is done in another test is not needed here
{mounttypes.Mount{Type: "volume", Target: destPath}, types.MountPoint{Driver: volume.DefaultDriverName, Type: "volume", RW: true, Destination: destPath}},
{mounttypes.Mount{Type: "volume", Target: destPath + slash}, types.MountPoint{Driver: volume.DefaultDriverName, Type: "volume", RW: true, Destination: destPath}},
{mounttypes.Mount{Type: "volume", Target: destPath, Source: "test1"}, types.MountPoint{Type: "volume", Name: "test1", RW: true, Destination: destPath}},
{mounttypes.Mount{Type: "volume", Target: destPath, ReadOnly: true, Source: "test2"}, types.MountPoint{Type: "volume", Name: "test2", RW: false, Destination: destPath}},
{mounttypes.Mount{Type: "volume", Target: destPath, Source: "test3", VolumeOptions: &mounttypes.VolumeOptions{DriverConfig: &mounttypes.Driver{Name: volume.DefaultDriverName}}}, types.MountPoint{Driver: volume.DefaultDriverName, Type: "volume", Name: "test3", RW: true, Destination: destPath}},
}
if SameHostDaemon.Condition() {
// setup temp dir for testing binds
tmpDir1, err := ioutil.TempDir("", "test-mounts-api-1")
c.Assert(err, checker.IsNil)
defer os.RemoveAll(tmpDir1)
cases = append(cases, []testCase{
{mounttypes.Mount{Type: "bind", Source: tmpDir1, Target: destPath}, types.MountPoint{Type: "bind", RW: true, Destination: destPath, Source: tmpDir1}},
{mounttypes.Mount{Type: "bind", Source: tmpDir1, Target: destPath, ReadOnly: true}, types.MountPoint{Type: "bind", RW: false, Destination: destPath, Source: tmpDir1}},
}...)
// for modes only supported on Linux
if DaemonIsLinux.Condition() {
tmpDir3, err := ioutils.TempDir("", "test-mounts-api-3")
c.Assert(err, checker.IsNil)
defer os.RemoveAll(tmpDir3)
c.Assert(mount.Mount(tmpDir3, tmpDir3, "none", "bind,rw"), checker.IsNil)
c.Assert(mount.ForceMount("", tmpDir3, "none", "shared"), checker.IsNil)
cases = append(cases, []testCase{
{mounttypes.Mount{Type: "bind", Source: tmpDir3, Target: destPath}, types.MountPoint{Type: "bind", RW: true, Destination: destPath, Source: tmpDir3}},
{mounttypes.Mount{Type: "bind", Source: tmpDir3, Target: destPath, ReadOnly: true}, types.MountPoint{Type: "bind", RW: false, Destination: destPath, Source: tmpDir3}},
{mounttypes.Mount{Type: "bind", Source: tmpDir3, Target: destPath, ReadOnly: true, BindOptions: &mounttypes.BindOptions{Propagation: "shared"}}, types.MountPoint{Type: "bind", RW: false, Destination: destPath, Source: tmpDir3, Propagation: "shared"}},
}...)
}
}
if daemonPlatform != "windows" { // Windows does not support volume populate
cases = append(cases, []testCase{
{mounttypes.Mount{Type: "volume", Target: destPath, VolumeOptions: &mounttypes.VolumeOptions{NoCopy: true}}, types.MountPoint{Driver: volume.DefaultDriverName, Type: "volume", RW: true, Destination: destPath}},
{mounttypes.Mount{Type: "volume", Target: destPath + slash, VolumeOptions: &mounttypes.VolumeOptions{NoCopy: true}}, types.MountPoint{Driver: volume.DefaultDriverName, Type: "volume", RW: true, Destination: destPath}},
{mounttypes.Mount{Type: "volume", Target: destPath, Source: "test4", VolumeOptions: &mounttypes.VolumeOptions{NoCopy: true}}, types.MountPoint{Type: "volume", Name: "test4", RW: true, Destination: destPath}},
{mounttypes.Mount{Type: "volume", Target: destPath, Source: "test5", ReadOnly: true, VolumeOptions: &mounttypes.VolumeOptions{NoCopy: true}}, types.MountPoint{Type: "volume", Name: "test5", RW: false, Destination: destPath}},
}...)
}
type wrapper struct {
containertypes.Config
HostConfig containertypes.HostConfig
}
type createResp struct {
ID string `json:"Id"`
}
for i, x := range cases {
c.Logf("case %d - config: %v", i, x.cfg)
status, data, err := sockRequest("POST", "/containers/create", wrapper{containertypes.Config{Image: testImg}, containertypes.HostConfig{Mounts: []mounttypes.Mount{x.cfg}}})
c.Assert(err, checker.IsNil, check.Commentf(string(data)))
c.Assert(status, checker.Equals, http.StatusCreated, check.Commentf(string(data)))
var resp createResp
err = json.Unmarshal(data, &resp)
c.Assert(err, checker.IsNil, check.Commentf(string(data)))
id := resp.ID
var mps []types.MountPoint
err = json.NewDecoder(strings.NewReader(inspectFieldJSON(c, id, "Mounts"))).Decode(&mps)
c.Assert(err, checker.IsNil)
c.Assert(mps, checker.HasLen, 1)
c.Assert(mps[0].Destination, checker.Equals, x.expected.Destination)
if len(x.expected.Source) > 0 {
c.Assert(mps[0].Source, checker.Equals, x.expected.Source)
}
if len(x.expected.Name) > 0 {
c.Assert(mps[0].Name, checker.Equals, x.expected.Name)
}
if len(x.expected.Driver) > 0 {
c.Assert(mps[0].Driver, checker.Equals, x.expected.Driver)
}
c.Assert(mps[0].RW, checker.Equals, x.expected.RW)
c.Assert(mps[0].Type, checker.Equals, x.expected.Type)
c.Assert(mps[0].Mode, checker.Equals, x.expected.Mode)
if len(x.expected.Propagation) > 0 {
c.Assert(mps[0].Propagation, checker.Equals, x.expected.Propagation)
}
out, _, err := dockerCmdWithError("start", "-a", id)
if (x.cfg.Type != "volume" || (x.cfg.VolumeOptions != nil && x.cfg.VolumeOptions.NoCopy)) && daemonPlatform != "windows" {
c.Assert(err, checker.NotNil, check.Commentf("%s\n%v", out, mps[0]))
} else {
c.Assert(err, checker.IsNil, check.Commentf("%s\n%v", out, mps[0]))
}
dockerCmd(c, "rm", "-fv", id)
if x.cfg.Type == "volume" && len(x.cfg.Source) > 0 {
// This should still exist even though we removed the container
dockerCmd(c, "volume", "inspect", mps[0].Name)
} else {
// This should be removed automatically when we removed the container
out, _, err := dockerCmdWithError("volume", "inspect", mps[0].Name)
c.Assert(err, checker.NotNil, check.Commentf(out))
}
}
}