Bladeren bron

Merge pull request #26837 from AkihiroSuda/newtmpfs

api: add TypeTmpfs to api/types/mount
Vincent Demeester 8 jaren geleden
bovenliggende
commit
87ddc8b0a5

+ 48 - 3
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.
+}

+ 16 - 2
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

+ 2 - 2
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

+ 6 - 2
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)

+ 5 - 1
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 {

+ 1 - 1
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,

+ 5 - 1
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**:

+ 190 - 10
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)
+		}
+	}
+}

+ 4 - 16
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 {

+ 7 - 0
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")}
 	}

+ 2 - 0
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
 }

+ 57 - 0
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
+}

+ 23 - 0
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)
+		}
+	}
+}

+ 16 - 0
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)
+}