diff --git a/api/types/mount/mount.go b/api/types/mount/mount.go index 5516ed09db..8ac89402f9 100644 --- a/api/types/mount/mount.go +++ b/api/types/mount/mount.go @@ -1,24 +1,35 @@ package mount +import ( + "os" +) + // Type represents the type of a mount. type Type string +// Type constants const ( - // TypeBind BIND + // TypeBind is the type for mounting host dir TypeBind Type = "bind" - // TypeVolume VOLUME + // TypeVolume is the type for remote storage volumes TypeVolume Type = "volume" + // TypeTmpfs is the type for mounting tmpfs + TypeTmpfs Type = "tmpfs" ) // Mount represents a mount (volume). type Mount struct { - Type Type `json:",omitempty"` + Type Type `json:",omitempty"` + // Source specifies the name of the mount. Depending on mount type, this + // may be a volume name or a host path, or even ignored. + // Source is not supported for tmpfs (must be an empty value) Source string `json:",omitempty"` Target string `json:",omitempty"` ReadOnly bool `json:",omitempty"` BindOptions *BindOptions `json:",omitempty"` VolumeOptions *VolumeOptions `json:",omitempty"` + TmpfsOptions *TmpfsOptions `json:",omitempty"` } // Propagation represents the propagation of a mount. @@ -56,3 +67,37 @@ type Driver struct { Name string `json:",omitempty"` Options map[string]string `json:",omitempty"` } + +// TmpfsOptions defines options specific to mounts of type "tmpfs". +type TmpfsOptions struct { + // Size sets the size of the tmpfs, in bytes. + // + // This will be converted to an operating system specific value + // depending on the host. For example, on linux, it will be convered to + // use a 'k', 'm' or 'g' syntax. BSD, though not widely supported with + // docker, uses a straight byte value. + // + // Percentages are not supported. + SizeBytes int64 `json:",omitempty"` + // Mode of the tmpfs upon creation + Mode os.FileMode `json:",omitempty"` + + // TODO(stevvooe): There are several more tmpfs flags, specified in the + // daemon, that are accepted. Only the most basic are added for now. + // + // From docker/docker/pkg/mount/flags.go: + // + // var validFlags = map[string]bool{ + // "": true, + // "size": true, X + // "mode": true, X + // "uid": true, + // "gid": true, + // "nr_inodes": true, + // "nr_blocks": true, + // "mpol": true, + // } + // + // Some of these may be straightforward to add, but others, such as + // uid/gid have implications in a clustered system. +} diff --git a/container/container_unix.go b/container/container_unix.go index d74ab093c8..4bc38aec83 100644 --- a/container/container_unix.go +++ b/container/container_unix.go @@ -12,6 +12,7 @@ import ( "github.com/Sirupsen/logrus" containertypes "github.com/docker/docker/api/types/container" + mounttypes "github.com/docker/docker/api/types/mount" "github.com/docker/docker/pkg/chrootarchive" "github.com/docker/docker/pkg/stringid" "github.com/docker/docker/pkg/symlink" @@ -406,7 +407,7 @@ func copyOwnership(source, destination string) error { } // TmpfsMounts returns the list of tmpfs mounts -func (container *Container) TmpfsMounts() []Mount { +func (container *Container) TmpfsMounts() ([]Mount, error) { var mounts []Mount for dest, data := range container.HostConfig.Tmpfs { mounts = append(mounts, Mount{ @@ -415,7 +416,20 @@ func (container *Container) TmpfsMounts() []Mount { Data: data, }) } - return mounts + for dest, mnt := range container.MountPoints { + if mnt.Type == mounttypes.TypeTmpfs { + data, err := volume.ConvertTmpfsOptions(mnt.Spec.TmpfsOptions) + if err != nil { + return nil, err + } + mounts = append(mounts, Mount{ + Source: "tmpfs", + Destination: dest, + Data: data, + }) + } + } + return mounts, nil } // cleanResourcePath cleans a resource path and prepares to combine with mnt path diff --git a/container/container_windows.go b/container/container_windows.go index d4ef35bfc6..69d53452df 100644 --- a/container/container_windows.go +++ b/container/container_windows.go @@ -82,9 +82,9 @@ func (container *Container) UnmountVolumes(forceSyscall bool, volumeEventLog fun } // TmpfsMounts returns the list of tmpfs mounts -func (container *Container) TmpfsMounts() []Mount { +func (container *Container) TmpfsMounts() ([]Mount, error) { var mounts []Mount - return mounts + return mounts, nil } // UpdateContainer updates configuration of a container diff --git a/daemon/oci_linux.go b/daemon/oci_linux.go index 4372f8ee94..5cac76843d 100644 --- a/daemon/oci_linux.go +++ b/daemon/oci_linux.go @@ -473,7 +473,7 @@ func setMounts(daemon *Daemon, s *specs.Spec, c *container.Container, mounts []c } if m.Source == "tmpfs" { - data := c.HostConfig.Tmpfs[m.Destination] + data := m.Data options := []string{"noexec", "nosuid", "nodev", string(volume.DefaultPropagationMode)} if data != "" { options = append(options, strings.Split(data, ",")...) @@ -707,7 +707,11 @@ func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) { return nil, err } ms = append(ms, c.IpcMounts()...) - ms = append(ms, c.TmpfsMounts()...) + tmpfsMounts, err := c.TmpfsMounts() + if err != nil { + return nil, err + } + ms = append(ms, tmpfsMounts...) sort.Sort(mounts(ms)) if err := setMounts(daemon, &s, c, ms); err != nil { return nil, fmt.Errorf("linux mounts: %v", err) diff --git a/daemon/volumes_unix.go b/daemon/volumes_unix.go index fb07d27129..8cecce2c02 100644 --- a/daemon/volumes_unix.go +++ b/daemon/volumes_unix.go @@ -24,7 +24,11 @@ func (daemon *Daemon) setupMounts(c *container.Container) ([]container.Mount, er var mounts []container.Mount // TODO: tmpfs mounts should be part of Mountpoints tmpfsMounts := make(map[string]bool) - for _, m := range c.TmpfsMounts() { + tmpfsMountInfo, err := c.TmpfsMounts() + if err != nil { + return nil, err + } + for _, m := range tmpfsMountInfo { tmpfsMounts[m.Destination] = true } for _, m := range c.MountPoints { diff --git a/docs/reference/api/docker_remote_api.md b/docs/reference/api/docker_remote_api.md index d3b6cb08a5..39736ebf91 100644 --- a/docs/reference/api/docker_remote_api.md +++ b/docs/reference/api/docker_remote_api.md @@ -139,7 +139,7 @@ This section lists each version from latest to oldest. Each listing includes a * `DELETE /volumes/(name)` now accepts a `force` query parameter to force removal of volumes that were already removed out of band by the volume driver plugin. * `POST /containers/create/` and `POST /containers/(name)/update` now validates restart policies. * `POST /containers/create` now validates IPAMConfig in NetworkingConfig, and returns error for invalid IPv4 and IPv6 addresses (`--ip` and `--ip6` in `docker create/run`). -* `POST /containers/create` now takes a `Mounts` field in `HostConfig` which replaces `Binds` and `Volumes`. *note*: `Binds` and `Volumes` are still available but are exclusive with `Mounts` +* `POST /containers/create` now takes a `Mounts` field in `HostConfig` which replaces `Binds`, `Volumes`, and `Tmpfs`. *note*: `Binds`, `Volumes`, and `Tmpfs` are still available and can be combined with `Mounts`. * `POST /build` now performs a preliminary validation of the `Dockerfile` before starting the build, and returns an error if the syntax is incorrect. Note that this change is _unversioned_ and applied to all API versions. * `POST /build` accepts `cachefrom` parameter to specify images used for build cache. * `GET /networks/` endpoint now correctly returns a list of *all* networks, diff --git a/docs/reference/api/docker_remote_api_v1.25.md b/docs/reference/api/docker_remote_api_v1.25.md index d8de085976..79bc75040e 100644 --- a/docs/reference/api/docker_remote_api_v1.25.md +++ b/docs/reference/api/docker_remote_api_v1.25.md @@ -511,10 +511,11 @@ Create a container - **Mounts** – Specification for mounts to be added to the container. - **Target** – Container path. - **Source** – Mount source (e.g. a volume name, a host path). - - **Type** – The mount type (`bind`, or `volume`). + - **Type** – The mount type (`bind`, `volume`, or `tmpfs`). Available types (for the `Type` field): - **bind** - Mounts a file or directory from the host into the container. Must exist prior to creating the container. - **volume** - Creates a volume with the given name and options (or uses a pre-existing volume with the same name and options). These are **not** removed when the container is removed. + - **tmpfs** - Create a tmpfs with the given options. The mount source cannot be specified for tmpfs. - **ReadOnly** – A boolean indicating whether the mount should be read-only. - **BindOptions** - Optional configuration for the `bind` type. - **Propagation** – A propagation mode with the value `[r]private`, `[r]shared`, or `[r]slave`. @@ -525,6 +526,9 @@ Create a container - **DriverConfig** – Map of driver-specific options. - **Name** - Name of the driver to use to create the volume. - **Options** - key/value map of driver specific options. + - **TmpfsOptions** – Optional configuration for the `tmpfs` type. + - **SizeBytes** – The size for the tmpfs mount in bytes. + - **Mode** – The permission mode for the tmpfs mount in an integer. **Query parameters**: diff --git a/integration-cli/docker_api_containers_test.go b/integration-cli/docker_api_containers_test.go index c375a2d3e0..97ae216378 100644 --- a/integration-cli/docker_api_containers_test.go +++ b/integration-cli/docker_api_containers_test.go @@ -1569,13 +1569,80 @@ func (s *DockerSuite) TestContainersAPICreateMountsValidation(c *check.C) { 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, ""}, + { + config: cfg{ + Image: "busybox", + HostConfig: hc{ + Mounts: []m{{ + Type: "notreal", + Target: destPath}}}}, + status: http.StatusBadRequest, + msg: "mount type unknown", + }, + { + config: cfg{ + Image: "busybox", + HostConfig: hc{ + Mounts: []m{{ + Type: "bind"}}}}, + status: http.StatusBadRequest, + msg: "Target must not be empty", + }, + { + config: cfg{ + Image: "busybox", + HostConfig: hc{ + Mounts: []m{{ + Type: "bind", + Target: destPath}}}}, + status: http.StatusBadRequest, + msg: "Source must not be empty", + }, + { + config: cfg{ + Image: "busybox", + HostConfig: hc{ + Mounts: []m{{ + Type: "bind", + Source: notExistPath, + Target: destPath}}}}, + status: http.StatusBadRequest, + msg: "bind source path does not exist", + }, + { + config: cfg{ + Image: "busybox", + HostConfig: hc{ + Mounts: []m{{ + Type: "volume"}}}}, + status: http.StatusBadRequest, + msg: "Target must not be empty", + }, + { + config: cfg{ + Image: "busybox", + HostConfig: hc{ + Mounts: []m{{ + Type: "volume", + Source: "hello", + Target: destPath}}}}, + status: http.StatusCreated, + msg: "", + }, + { + config: cfg{ + Image: "busybox", + HostConfig: hc{ + Mounts: []m{{ + Type: "volume", + Source: "hello2", + Target: destPath, + VolumeOptions: &mounttypes.VolumeOptions{ + DriverConfig: &mounttypes.Driver{ + Name: "local"}}}}}}, + status: http.StatusCreated, + msg: "", + }, } if SameHostDaemon.Condition() { @@ -1583,14 +1650,85 @@ func (s *DockerSuite) TestContainersAPICreateMountsValidation(c *check.C) { 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"}, + { + config: cfg{ + Image: "busybox", + HostConfig: hc{ + Mounts: []m{{ + Type: "bind", + Source: tmpDir, + Target: destPath}}}}, + status: http.StatusCreated, + msg: "", + }, + { + config: cfg{ + Image: "busybox", + HostConfig: hc{ + Mounts: []m{{ + Type: "bind", + Source: tmpDir, + Target: destPath, + VolumeOptions: &mounttypes.VolumeOptions{}}}}}, + status: http.StatusBadRequest, + msg: "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, ""}, + { + config: 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"}}}}}}}, + status: http.StatusCreated, + msg: "", + }, + { + config: cfg{ + Image: "busybox", + HostConfig: hc{ + Mounts: []m{{ + Type: "tmpfs", + Target: destPath}}}}, + status: http.StatusCreated, + msg: "", + }, + { + config: cfg{ + Image: "busybox", + HostConfig: hc{ + Mounts: []m{{ + Type: "tmpfs", + Target: destPath, + TmpfsOptions: &mounttypes.TmpfsOptions{ + SizeBytes: 4096 * 1024, + Mode: 0700, + }}}}}, + status: http.StatusCreated, + msg: "", + }, + + { + config: cfg{ + Image: "busybox", + HostConfig: hc{ + Mounts: []m{{ + Type: "tmpfs", + Source: "/shouldnotbespecified", + Target: destPath}}}}, + status: http.StatusBadRequest, + msg: "Source must not be specified", + }, }...) } @@ -1759,3 +1897,45 @@ func (s *DockerSuite) TestContainersAPICreateMountsCreate(c *check.C) { } } } + +func (s *DockerSuite) TestContainersAPICreateMountsTmpfs(c *check.C) { + testRequires(c, DaemonIsLinux) + type testCase struct { + cfg map[string]interface{} + expectedOptions []string + } + target := "/foo" + cases := []testCase{ + { + cfg: map[string]interface{}{ + "Type": "tmpfs", + "Target": target}, + expectedOptions: []string{"rw", "nosuid", "nodev", "noexec", "relatime"}, + }, + { + cfg: map[string]interface{}{ + "Type": "tmpfs", + "Target": target, + "TmpfsOptions": map[string]interface{}{ + "SizeBytes": 4096 * 1024, "Mode": 0700}}, + expectedOptions: []string{"rw", "nosuid", "nodev", "noexec", "relatime", "size=4096k", "mode=700"}, + }, + } + + for i, x := range cases { + cName := fmt.Sprintf("test-tmpfs-%d", i) + data := map[string]interface{}{ + "Image": "busybox", + "Cmd": []string{"/bin/sh", "-c", + fmt.Sprintf("mount | grep 'tmpfs on %s'", target)}, + "HostConfig": map[string]interface{}{"Mounts": []map[string]interface{}{x.cfg}}, + } + status, resp, err := sockRequest("POST", "/containers/create?name="+cName, 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", cName) + for _, option := range x.expectedOptions { + c.Assert(out, checker.Contains, option) + } + } +} diff --git a/runconfig/config.go b/runconfig/config.go index 16e5e5c09f..508681cfe0 100644 --- a/runconfig/config.go +++ b/runconfig/config.go @@ -48,7 +48,7 @@ func DecodeContainerConfig(src io.Reader) (*container.Config, *container.HostCon } // Now validate all the volumes and binds - if err := validateVolumesAndBindSettings(w.Config, hc); err != nil { + if err := validateMountSettings(w.Config, hc); err != nil { return nil, nil, nil, err } } @@ -76,22 +76,10 @@ func DecodeContainerConfig(src io.Reader) (*container.Config, *container.HostCon return w.Config, hc, w.NetworkingConfig, nil } -// validateVolumesAndBindSettings validates each of the volumes and bind settings +// validateMountSettings validates each of the volumes and bind settings // passed by the caller to ensure they are valid. -func validateVolumesAndBindSettings(c *container.Config, hc *container.HostConfig) error { - if len(hc.Mounts) > 0 { - if len(hc.Binds) > 0 { - return conflictError(fmt.Errorf("must not specify both Binds and Mounts")) - } - - if len(c.Volumes) > 0 { - return conflictError(fmt.Errorf("must not specify both Volumes and Mounts")) - } - - if len(hc.VolumeDriver) > 0 { - return conflictError(fmt.Errorf("must not specify both VolumeDriver and Mounts")) - } - } +func validateMountSettings(c *container.Config, hc *container.HostConfig) error { + // it is ok to have len(hc.Mounts) > 0 && (len(hc.Binds) > 0 || len (c.Volumes) > 0 || len (hc.Tmpfs) > 0 ) // Ensure all volumes and binds are valid. for spec := range c.Volumes { diff --git a/volume/validate.go b/volume/validate.go index ddba14fbc5..de41e0bf1e 100644 --- a/volume/validate.go +++ b/volume/validate.go @@ -87,6 +87,13 @@ func validateMountConfig(mnt *mount.Mount, options ...func(*validateOpts)) error return &errMountConfig{mnt, err} } } + case mount.TypeTmpfs: + if len(mnt.Source) != 0 { + return &errMountConfig{mnt, errExtraField("Source")} + } + if _, err := ConvertTmpfsOptions(mnt.TmpfsOptions); err != nil { + return &errMountConfig{mnt, err} + } default: return &errMountConfig{mnt, errors.New("mount type unknown")} } diff --git a/volume/volume.go b/volume/volume.go index df47b0f1b0..ea8d660826 100644 --- a/volume/volume.go +++ b/volume/volume.go @@ -286,6 +286,8 @@ func ParseMountSpec(cfg mounttypes.Mount, options ...func(*validateOpts)) (*Moun mp.Propagation = cfg.BindOptions.Propagation } } + case mounttypes.TypeTmpfs: + // NOP } return mp, nil } diff --git a/volume/volume_linux.go b/volume/volume_linux.go new file mode 100644 index 0000000000..991aa66123 --- /dev/null +++ b/volume/volume_linux.go @@ -0,0 +1,57 @@ +// +build linux + +package volume + +import ( + "fmt" + "strings" + + mounttypes "github.com/docker/docker/api/types/mount" +) + +// ConvertTmpfsOptions converts *mounttypes.TmpfsOptions to the raw option string +// for mount(2). +// The logic is copy-pasted from daemon/cluster/executer/container.getMountMask. +// It will be deduplicated when we migrated the cluster to the new mount scheme. +func ConvertTmpfsOptions(opt *mounttypes.TmpfsOptions) (string, error) { + if opt == nil { + return "", nil + } + var rawOpts []string + if opt.Mode != 0 { + rawOpts = append(rawOpts, fmt.Sprintf("mode=%o", opt.Mode)) + } + + if opt.SizeBytes != 0 { + // calculate suffix here, making this linux specific, but that is + // okay, since API is that way anyways. + + // we do this by finding the suffix that divides evenly into the + // value, returing the value itself, with no suffix, if it fails. + // + // For the most part, we don't enforce any semantic to this values. + // The operating system will usually align this and enforce minimum + // and maximums. + var ( + size = opt.SizeBytes + suffix string + ) + for _, r := range []struct { + suffix string + divisor int64 + }{ + {"g", 1 << 30}, + {"m", 1 << 20}, + {"k", 1 << 10}, + } { + if size%r.divisor == 0 { + size = size / r.divisor + suffix = r.suffix + break + } + } + + rawOpts = append(rawOpts, fmt.Sprintf("size=%d%s", size, suffix)) + } + return strings.Join(rawOpts, ","), nil +} diff --git a/volume/volume_linux_test.go b/volume/volume_linux_test.go new file mode 100644 index 0000000000..05e5e16a71 --- /dev/null +++ b/volume/volume_linux_test.go @@ -0,0 +1,23 @@ +// +build linux + +package volume + +import ( + "testing" + + mounttypes "github.com/docker/docker/api/types/mount" +) + +func TestConvertTmpfsOptions(t *testing.T) { + type testCase struct { + opt mounttypes.TmpfsOptions + } + cases := []testCase{ + {mounttypes.TmpfsOptions{SizeBytes: 1024 * 1024, Mode: 0700}}, + } + for _, c := range cases { + if _, err := ConvertTmpfsOptions(&c.opt); err != nil { + t.Fatalf("could not convert %+v to string: %v", c.opt, err) + } + } +} diff --git a/volume/volume_unsupported.go b/volume/volume_unsupported.go new file mode 100644 index 0000000000..ca7445c7de --- /dev/null +++ b/volume/volume_unsupported.go @@ -0,0 +1,16 @@ +// +build !linux + +package volume + +import ( + "fmt" + "runtime" + + mounttypes "github.com/docker/docker/api/types/mount" +) + +// ConvertTmpfsOptions converts *mounttypes.TmpfsOptions to the raw option string +// for mount(2). +func ConvertTmpfsOptions(opt *mounttypes.TmpfsOptions) (string, error) { + return "", fmt.Errorf("%s does not support tmpfs", runtime.GOOS) +}