api: add TypeTmpfs to api/types/mount

Signed-off-by: Akihiro Suda <suda.akihiro@lab.ntt.co.jp>
This commit is contained in:
Akihiro Suda 2016-09-22 20:14:15 +00:00
parent 9e206b5512
commit 18768fdc2e
14 changed files with 382 additions and 38 deletions

View File

@ -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.
}

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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 {

View File

@ -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,

View File

@ -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**:

View File

@ -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)
}
}
}

View File

@ -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 {

View File

@ -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")}
}

View File

@ -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
}

57
volume/volume_linux.go Normal file
View File

@ -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
}

View File

@ -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)
}
}
}

View File

@ -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)
}