Skip to content

Commit

Permalink
CLI and docs for recursively read-only mounts
Browse files Browse the repository at this point in the history
For moby/moby PR 45278

Signed-off-by: Akihiro Suda <[email protected]>
  • Loading branch information
AkihiroSuda committed May 29, 2023
1 parent 945bfd5 commit c9bd3ab
Show file tree
Hide file tree
Showing 16 changed files with 509 additions and 19 deletions.
6 changes: 3 additions & 3 deletions cli/command/container/opts.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ import (
"strings"
"time"

"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/compose/loader"
"github.com/docker/cli/opts"
"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/api/types/strslice"
"github.com/docker/docker/api/types/versions"
"github.com/docker/docker/errdefs"
"github.com/docker/go-connections/nat"
"github.com/pkg/errors"
Expand Down Expand Up @@ -1061,8 +1061,8 @@ func validateAttach(val string) (string, error) {

func validateAPIVersion(c *containerConfig, serverAPIVersion string) error {
for _, m := range c.HostConfig.Mounts {
if m.BindOptions != nil && m.BindOptions.NonRecursive && versions.LessThan(serverAPIVersion, "1.40") {
return errors.Errorf("bind-nonrecursive requires API v1.40 or later")
if err := command.ValidateMountWithAPIVersion(m, serverAPIVersion); err != nil {
return err
}
}
return nil
Expand Down
6 changes: 3 additions & 3 deletions cli/command/service/opts.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import (
"strings"
"time"

"github.com/docker/cli/cli/command"
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/api/types/versions"
"github.com/docker/docker/client"
gogotypes "github.com/gogo/protobuf/types"
"github.com/google/shlex"
Expand Down Expand Up @@ -1033,8 +1033,8 @@ const (

func validateAPIVersion(c swarm.ServiceSpec, serverAPIVersion string) error {
for _, m := range c.TaskTemplate.ContainerSpec.Mounts {
if m.BindOptions != nil && m.BindOptions.NonRecursive && versions.LessThan(serverAPIVersion, "1.40") {
return errors.Errorf("bind-nonrecursive requires API v1.40 or later")
if err := command.ValidateMountWithAPIVersion(m, serverAPIVersion); err != nil {
return err
}
}
return nil
Expand Down
15 changes: 15 additions & 0 deletions cli/command/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (

"github.com/docker/cli/cli/streams"
"github.com/docker/docker/api/types/filters"
mounttypes "github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/versions"
"github.com/moby/sys/sequential"
"github.com/pkg/errors"
"github.com/spf13/pflag"
Expand Down Expand Up @@ -195,3 +197,16 @@ func StringSliceReplaceAt(s, old, new []string, requireIndex int) ([]string, boo
out = append(out, s[idx+len(old):]...)
return out, true
}

func ValidateMountWithAPIVersion(m mounttypes.Mount, serverAPIVersion string) error {
if m.BindOptions != nil {
if m.BindOptions.NonRecursive && versions.LessThan(serverAPIVersion, "1.40") {
return errors.Errorf("bind-nonrecursive requires API v1.40 or later")
}
// bind-readonly-nonrecursive can be safely ignored when API < 1.44
if m.BindOptions.ReadOnlyForceRecursive && versions.LessThan(serverAPIVersion, "1.44") {
return errors.Errorf("bind-readonly-forcerecursive requires API v1.44 or later")
}
}
return nil
}
38 changes: 35 additions & 3 deletions docs/reference/commandline/service_create.md
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,8 @@ volumes in a service:
<td>
<p>The Engine mounts binds and volumes <tt>read-write</tt> unless <tt>readonly</tt> option
is given when mounting the bind or volume. Note that setting <tt>readonly</tt> for a
bind-mount does not make its submounts <tt>readonly</tt> on the current Linux implementation. See also <tt>bind-nonrecursive</tt>.</p>
bind-mount does not make its submounts <tt>readonly</tt> if Docker Engine is older than v25.0,
or Linux kernel is older than v5.12. See also <a href="#options-for-bind-mounts">Options for Bind Mounts</a>.</p>
<ul>
<li><tt>true</tt> or <tt>1</tt> or no value: Mounts the bind or volume read-only.</li>
<li><tt>false</tt> or <tt>0</tt>: Mounts the bind or volume read-write.</li>
Expand All @@ -402,7 +403,7 @@ volumes in a service:
</tr>
</table>

#### Options for Bind Mounts
#### <a name="options-for-bind-mounts"></a> Options for Bind Mounts

The following options can only be used for bind mounts (`type=bind`):

Expand Down Expand Up @@ -434,7 +435,8 @@ The following options can only be used for bind mounts (`type=bind`):
<td><b>bind-nonrecursive</b></td>
<td>
By default, submounts are recursively bind-mounted as well. However, this behavior can be confusing when a
bind mount is configured with <tt>readonly</tt> option, because submounts are not mounted as read-only.
bind mount is configured with <tt>readonly</tt> option, because submounts are not mounted as read-only
if Docker Engine is older than v25, or Linux kernel is older than v5.12.
Set <tt>bind-nonrecursive</tt> to disable recursive bind-mount.<br />
<br />
A value is optional:<br />
Expand All @@ -445,6 +447,36 @@ The following options can only be used for bind mounts (`type=bind`):
</ul>
</td>
</tr>
<tr>
<td><b>bind-readonly-nonrecursive</b> or <b>bind-ro-nonrecursive</b></td>
<td>
If set to <tt>true</tt>, submounts are recursively bind-mounted
(unless <tt>bind-nonrecursive</tt> is set to <tt>true</tt> in conjunction),
but they are not recursively made read-only. This corresponds to the default behavior of Docker v24 and older.
A <tt>false</tt> value is ignored when the Docker daemon is running on Linux kernel older than v5.12.<br />
<br />
A value is optional:<br />
<br />
<ul>
<li><tt>true</tt> or <tt>1</tt>: Disables recursive read-only bind-mount.</li>
<li><tt>false</tt> or <tt>0</tt>: Default if you do not provide a value. Enables recursive read-only bind-mount (if possible).</li>
</ul>
</td>
</tr>
<tr>
<td><b>bind-readonly-forcerecursive</b> or <b>bind-ro-forcerecursive</b></td>
<td>
If set to <tt>true</tt>, and submounts cannot be made recursively read-only, the Docker daemon raises an error.<br />
This option should be used in conjunction with <tt>bind-propagation=rprivate</tt>.
<br />
A value is optional:<br />
<br />
<ul>
<li><tt>true</tt> or <tt>1</tt>: Force recursive read-only bind-mount.</li>
<li><tt>false</tt> or <tt>0</tt>: Default if you do not provide a value. Do not force recursive read-only bind-mount.</li>
</ul>
</td>
</tr>
</table>

##### Bind propagation
Expand Down
10 changes: 9 additions & 1 deletion docs/reference/run.md
Original file line number Diff line number Diff line change
Expand Up @@ -1714,13 +1714,21 @@ $ docker run -d --tmpfs /run:rw,noexec,nosuid,size=65536k my_image
### VOLUME (shared filesystems)

-v, --volume=[host-src:]container-dest[:<options>]: Bind mount a volume.
The comma-delimited `options` are [rw|ro], [z|Z],
The comma-delimited `options` are [rw|ro|ro-non-recursive|ro-force-recursive|rro], [z|Z],
[[r]shared|[r]slave|[r]private], and [nocopy].
The 'host-src' is an absolute path or a name value.

If neither 'rw' or 'ro' is specified then the volume is mounted in
read-write mode.

Starting with Docker Engine v25, the `ro` mode makes its submounts read-only when running on
Linux kernel v5.12 or newer.
To fall back to the behavior of Docker v24, specify `ro-non-recursive`.
To explicitly make the mount recursively read-only, specify `ro-force-recursive`
or `rro`.
The `ro-force-recursive` (`rro`) mode should be used in conjunction with `bind-propagation=rprivate`.
The `ro-force-recursive` (`rro`) mode fails when running on Linux kernel older than v5.12.

The `nocopy` mode is used to disable automatically copying the requested volume
path in the container to the volume storage location.
For named volumes, `copy` is the default mode. Copy modes are not supported
Expand Down
21 changes: 18 additions & 3 deletions man/docker-run.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -468,15 +468,22 @@ according to RFC4862.
* `ro`, `readonly`: `true` or `false` (default).

**Note**: setting `readonly` for a bind mount does not make its submounts
read-only on the current Linux implementation. See also `bind-nonrecursive`.
read-only if Docker Engine is older than v25, or Linux kernel is older than v5.12. See also `bind` options below.

Options specific to `bind`:

* `bind-propagation`: `shared`, `slave`, `private`, `rshared`, `rslave`, or `rprivate`(default). See also `mount(2)`.
* `consistency`: `consistent`(default), `cached`, or `delegated`. Currently, only effective for Docker for Mac.
* `bind-nonrecursive`: `true` or `false` (default). If set to `true`,
submounts are not recursively bind-mounted. This option is useful for
`readonly` bind mount.
`readonly` bind mount when running on Linux kernel older than v5.12, which leaves submounts writable.
* `bind-ro-nonrecursive`, `bind-readonly-nonrecursive`: `true` or `false` (default). If set to `true`,
submounts are recursively bind-mounted (unless `bind-nonrecursive` is set to `true` in conjunction),
but they are not recursively made read-only. This corresponds to the default behavior of Docker Engine v24 and older.
A `false` value is ignored when the Docker daemon is running on Linux kernel older than v5.12.
* `bind-ro-forcerecursive`,`bind-readonly-forcerecursive`: `true` or `false` (default). If set to `true`,
and submounts cannot be made recursively read-only, the Docker daemon raises an error.
This option should be used in conjunction with `bind-propagation=rprivate`.

Options specific to `volume`:

Expand Down Expand Up @@ -719,7 +726,7 @@ any options, the systems uses the following options:
container. If 'HOST-DIR' is omitted, Docker automatically creates the new
volume on the host. The `OPTIONS` are a comma delimited list and can be:

* [rw|ro]
* [rw|ro|ro-non-recursive|ro-force-recursive|rro]
* [z|Z]
* [`[r]shared`|`[r]slave`|`[r]private`]
* [`delegated`|`cached`|`consistent`]
Expand Down Expand Up @@ -747,6 +754,14 @@ You can also specify the consistency requirement for the mount, either
`:consistent` (the default), `:cached`, or `:delegated`. Multiple options are
separated by commas, e.g. `:ro,cached`.

Starting with Docker Engine v25, the `:ro` mode makes its submounts read-only when running on
Linux kernel v5.12 or newer.
To fall back to the behavior of Docker Engine v24, specify `:ro-non-recursive`.
To explicitly make the mount recursively read-only, specify `:ro-force-recursive`
or `:rro`.
The `:ro-force-recursive` (`:rro`) mode should be used in conjunction with `bind-propagation=rprivate`.
The `:ro-force-recursive` (`:rro`) mode fails when running on Linux kernel older than v5.12.

Labeling systems like SELinux require that proper labels are placed on volume
content mounted into a container. Without a label, the security system might
prevent the processes running inside the container from using the content. By
Expand Down
12 changes: 12 additions & 0 deletions opts/mount.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,18 @@ func (m *MountOpt) Set(value string) error {
case "bind-nonrecursive":
bindOptions().NonRecursive = true
continue
case "bind-readonly-nonrecursive", "bind-ro-nonrecursive":
// ReadOnlyNonRecursive makes the mount non-recursively read-only, but still leaves the mount recursive
// (unless NonRecursive is set to true in conjunction).
bindOptions().ReadOnlyNonRecursive = true
// Implies ReadOnly = true
mount.ReadOnly = true
continue
case "bind-readonly-forcerecursive", "bind-ro-forcerecursive":
bindOptions().ReadOnlyForceRecursive = true
// Implies ReadOnly = true
mount.ReadOnly = true
continue
default:
return fmt.Errorf("invalid field '%s' must be a key=value pair", field)
}
Expand Down
69 changes: 69 additions & 0 deletions opts/mount_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,3 +217,72 @@ func TestMountOptSetTmpfsError(t *testing.T) {
assert.ErrorContains(t, m.Set("type=tmpfs,target=/foo,tmpfs-mode=foo"), "invalid value for tmpfs-mode")
assert.ErrorContains(t, m.Set("type=tmpfs"), "target is required")
}

func TestMountOptSetBindNonRecursive(t *testing.T) {
// Makes the mount itself non-recursive
t.Run("bind-nonrecursive", func(t *testing.T) {
var mount MountOpt
assert.NilError(t, mount.Set("type=bind,source=/foo,target=/bar,bind-nonrecursive"))
assert.Check(t, is.DeepEqual([]mounttypes.Mount{
{
Type: mounttypes.TypeBind,
Source: "/foo",
Target: "/bar",
BindOptions: &mounttypes.BindOptions{
NonRecursive: true,
},
},
}, mount.Value()))
})

// The mount itself is still recursive, but it is made read-only non-recursively
t.Run("bind-readonly-nonrecursive", func(t *testing.T) {
var mount MountOpt
assert.NilError(t, mount.Set("type=bind,source=/foo,target=/bar,bind-readonly-nonrecursive"))
assert.Check(t, is.DeepEqual([]mounttypes.Mount{
{
Type: mounttypes.TypeBind,
Source: "/foo",
Target: "/bar",
ReadOnly: true,
BindOptions: &mounttypes.BindOptions{
ReadOnlyNonRecursive: true,
},
},
}, mount.Value()))
})

t.Run("bind-readonly-forcerecursive", func(t *testing.T) {
var mount MountOpt
assert.NilError(t, mount.Set("type=bind,source=/foo,target=/bar,bind-readonly-forcerecursive"))
assert.Check(t, is.DeepEqual([]mounttypes.Mount{
{
Type: mounttypes.TypeBind,
Source: "/foo",
Target: "/bar",
ReadOnly: true,
BindOptions: &mounttypes.BindOptions{
ReadOnlyForceRecursive: true,
},
},
}, mount.Value()))
})

// Valid combination, but not really useful
t.Run("bind-nonrecursive,bind-readonly-nonrecursive", func(t *testing.T) {
var mount MountOpt
assert.NilError(t, mount.Set("type=bind,source=/foo,target=/bar,bind-nonrecursive,bind-readonly-nonrecursive"))
assert.Check(t, is.DeepEqual([]mounttypes.Mount{
{
Type: mounttypes.TypeBind,
Source: "/foo",
Target: "/bar",
ReadOnly: true,
BindOptions: &mounttypes.BindOptions{
NonRecursive: true,
ReadOnlyNonRecursive: true,
},
},
}, mount.Value()))
})
}
3 changes: 2 additions & 1 deletion vendor.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ require (
github.com/containerd/containerd v1.6.21
github.com/creack/pty v1.1.18
github.com/docker/distribution v2.8.2+incompatible
github.com/docker/docker v24.0.0-rc.2.0.20230523155306-cf4df9d8ae4c+incompatible // master (v25.0.0-dev)
github.com/docker/docker v24.0.0-rc.2.0.20230528104423-2ebd97dec1a2+incompatible // master (v25.0.0-dev)
github.com/docker/docker-credential-helpers v0.7.0
github.com/docker/go-connections v0.4.0
github.com/docker/go-units v0.5.0
Expand Down Expand Up @@ -62,6 +62,7 @@ require (
github.com/miekg/pkcs11 v1.1.1 // indirect
github.com/moby/sys/symlink v0.2.0 // indirect
github.com/opencontainers/runc v1.1.7 // indirect
github.com/opencontainers/runtime-spec v1.1.0-rc.2 // indirect
github.com/prometheus/client_golang v1.14.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.37.0 // indirect
Expand Down
6 changes: 4 additions & 2 deletions vendor.sum
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@ github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xb
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v24.0.0-rc.2.0.20230523155306-cf4df9d8ae4c+incompatible h1:stJU/EC2yJHujjvqyEAHeNxsIXtwuCvvYwImyaJ0wtI=
github.com/docker/docker v24.0.0-rc.2.0.20230523155306-cf4df9d8ae4c+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v24.0.0-rc.2.0.20230528104423-2ebd97dec1a2+incompatible h1:N7Y6lZFkcPXwoNXTImu92izjoh5o9VU8NVApLdFIHe8=
github.com/docker/docker v24.0.0-rc.2.0.20230528104423-2ebd97dec1a2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A=
github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0=
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=
Expand Down Expand Up @@ -305,6 +305,8 @@ github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b h1
github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ=
github.com/opencontainers/runc v1.1.7 h1:y2EZDS8sNng4Ksf0GUYNhKbTShZJPJg1FiXJNH/uoCk=
github.com/opencontainers/runc v1.1.7/go.mod h1:CbUumNnWCuTGFukNXahoo/RFBZvDAgRh/smNYNOhA50=
github.com/opencontainers/runtime-spec v1.1.0-rc.2 h1:ucBtEms2tamYYW/SvGpvq9yUN0NEVL6oyLEwDcTSrk8=
github.com/opencontainers/runtime-spec v1.1.0-rc.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
Expand Down
10 changes: 10 additions & 0 deletions vendor/github.com/docker/docker/api/swagger.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion vendor/github.com/docker/docker/api/types/mount/mount.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion vendor/github.com/docker/docker/api/types/types.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit c9bd3ab

Please sign in to comment.