diff --git a/api/swagger.yaml b/api/swagger.yaml index 5677340dbd..e4ec9c3741 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -442,6 +442,9 @@ definitions: Mode: description: "The permission mode for the tmpfs mount in an integer." type: "integer" + Options: + description: "The list of options to be passed to the tmpfs mount in a string." + type: "string" RestartPolicy: description: | diff --git a/api/types/mount/mount.go b/api/types/mount/mount.go index 6fe04da257..cf4953a0ce 100644 --- a/api/types/mount/mount.go +++ b/api/types/mount/mount.go @@ -119,7 +119,8 @@ type TmpfsOptions struct { SizeBytes int64 `json:",omitempty"` // Mode of the tmpfs upon creation Mode os.FileMode `json:",omitempty"` - + // Options passed directly to the tmpfs mount + Options string `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. // diff --git a/daemon/cluster/convert/container.go b/daemon/cluster/convert/container.go index b6b610e10e..5b026347b1 100644 --- a/daemon/cluster/convert/container.go +++ b/daemon/cluster/convert/container.go @@ -136,6 +136,7 @@ func containerSpecFromGRPC(c *swarmapi.ContainerSpec) *types.ContainerSpec { mount.TmpfsOptions = &mounttypes.TmpfsOptions{ SizeBytes: m.TmpfsOptions.SizeBytes, Mode: m.TmpfsOptions.Mode, + Options: m.TmpfsOptions.Options, } } containerSpec.Mounts = append(containerSpec.Mounts, mount) @@ -423,6 +424,7 @@ func containerToGRPC(c *types.ContainerSpec) (*swarmapi.ContainerSpec, error) { mount.TmpfsOptions = &swarmapi.Mount_TmpfsOptions{ SizeBytes: m.TmpfsOptions.SizeBytes, Mode: m.TmpfsOptions.Mode, + Options: m.TmpfsOptions.Options, } } diff --git a/daemon/cluster/executor/container/container.go b/daemon/cluster/executor/container/container.go index 0257030e5e..40ccd72810 100644 --- a/daemon/cluster/executor/container/container.go +++ b/daemon/cluster/executor/container/container.go @@ -364,6 +364,7 @@ func convertMount(m api.Mount) enginemount.Mount { mount.TmpfsOptions = &enginemount.TmpfsOptions{ SizeBytes: m.TmpfsOptions.SizeBytes, Mode: m.TmpfsOptions.Mode, + Options: m.TmpfsOptions.Options, } } diff --git a/daemon/cluster/executor/container/container_test.go b/daemon/cluster/executor/container/container_test.go index 8055e111fe..691f888306 100644 --- a/daemon/cluster/executor/container/container_test.go +++ b/daemon/cluster/executor/container/container_test.go @@ -4,8 +4,10 @@ import ( "testing" "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/mount" swarmapi "github.com/moby/swarmkit/v2/api" "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" ) func TestIsolationConversion(t *testing.T) { @@ -117,6 +119,7 @@ func TestCredentialSpecConversion(t *testing.T) { to: []string{"credentialspec=registry://testing"}, }, } + for _, c := range cases { c := c t.Run(c.name, func(t *testing.T) { @@ -139,3 +142,75 @@ func TestCredentialSpecConversion(t *testing.T) { }) } } + +func TestTmpfsConversion(t *testing.T) { + cases := []struct { + name string + from []swarmapi.Mount + to []mount.Mount + }{ + { + name: "tmpfs-exec", + from: []swarmapi.Mount{ + { + Source: "/foo", + Target: "/bar", + Type: swarmapi.MountTypeTmpfs, + TmpfsOptions: &swarmapi.Mount_TmpfsOptions{ + Options: "exec", + }, + }, + }, + to: []mount.Mount{ + { + Source: "/foo", + Target: "/bar", + Type: mount.TypeTmpfs, + TmpfsOptions: &mount.TmpfsOptions{ + Options: "exec", + }, + }, + }, + }, + { + name: "tmpfs-noexec", + from: []swarmapi.Mount{ + { + Source: "/foo", + Target: "/bar", + Type: swarmapi.MountTypeTmpfs, + TmpfsOptions: &swarmapi.Mount_TmpfsOptions{ + Options: "noexec", + }, + }, + }, + to: []mount.Mount{ + { + Source: "/foo", + Target: "/bar", + Type: mount.TypeTmpfs, + TmpfsOptions: &mount.TmpfsOptions{ + Options: "noexec", + }, + }, + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + task := swarmapi.Task{ + Spec: swarmapi.TaskSpec{ + Runtime: &swarmapi.TaskSpec_Container{ + Container: &swarmapi.ContainerSpec{ + Image: "alpine:latest", + Mounts: c.from, + }, + }, + }, + } + config := containerConfig{task: &task} + assert.Check(t, is.DeepEqual(c.to, config.hostConfig(nil).Mounts)) + }) + } +} diff --git a/docs/api/version-history.md b/docs/api/version-history.md index 6942c65e79..b8c1edb883 100644 --- a/docs/api/version-history.md +++ b/docs/api/version-history.md @@ -30,6 +30,7 @@ keywords: "API, Docker, rcli, REST, documentation" values originally submitted to the `POST /containers/create` endpoint. The newly introduced `DNSNames` should now be used instead when short container IDs are needed. +* `POST /containers/create` now takes `Options` as part of `HostConfig.Mounts` to set options for tmpfs mounts. ## v1.44 API changes diff --git a/volume/mounts/linux_parser.go b/volume/mounts/linux_parser.go index 1532187c77..0d086090de 100644 --- a/volume/mounts/linux_parser.go +++ b/volume/mounts/linux_parser.go @@ -204,6 +204,22 @@ func linuxValidMountMode(mode string) bool { return true } +var validTmpfsOptions = map[string]bool{ + "exec": true, + "noexec": true, +} + +func validateTmpfsOptions(rawOptions string) ([]string, error) { + var options []string + for _, opt := range strings.Split(rawOptions, ",") { + if _, ok := validTmpfsOptions[opt]; !ok { + return nil, errors.New("invalid option: " + opt) + } + options = append(options, opt) + } + return options, nil +} + func (p *linuxParser) ReadWrite(mode string) bool { if !linuxValidMountMode(mode) { return false @@ -406,6 +422,15 @@ func (p *linuxParser) ConvertTmpfsOptions(opt *mount.TmpfsOptions, readOnly bool rawOpts = append(rawOpts, fmt.Sprintf("size=%d%s", size, suffix)) } + + if opt != nil && len(opt.Options) > 0 { + tmpfsOpts, err := validateTmpfsOptions(opt.Options) + if err != nil { + return "", err + } + rawOpts = append(rawOpts, tmpfsOpts...) + } + return strings.Join(rawOpts, ","), nil } diff --git a/volume/mounts/linux_parser_test.go b/volume/mounts/linux_parser_test.go index d4c7a3856e..a9de82a1eb 100644 --- a/volume/mounts/linux_parser_test.go +++ b/volume/mounts/linux_parser_test.go @@ -238,6 +238,7 @@ func TestConvertTmpfsOptions(t *testing.T) { readOnly bool expectedSubstrings []string unexpectedSubstrings []string + err bool } cases := []testCase{ { @@ -252,10 +253,26 @@ func TestConvertTmpfsOptions(t *testing.T) { expectedSubstrings: []string{"ro"}, unexpectedSubstrings: []string{}, }, + { + opt: mount.TmpfsOptions{Options: "exec"}, + readOnly: true, + expectedSubstrings: []string{"ro", "exec"}, + unexpectedSubstrings: []string{"noexec"}, + }, + { + opt: mount.TmpfsOptions{Options: "INVALID"}, + err: true, + }, } p := NewLinuxParser() for _, tc := range cases { data, err := p.ConvertTmpfsOptions(&tc.opt, tc.readOnly) + if tc.err { + if err == nil { + t.Fatalf("expected error for %+v, got nil", tc.opt) + } + continue + } if err != nil { t.Fatalf("could not convert %+v (readOnly: %v) to string: %v", tc.opt, tc.readOnly, err)