Skip to content

Commit 6ebf6ba

Browse files
committed
CLI and docs for recursively read-only mounts
For moby/moby PR 45278 Signed-off-by: Akihiro Suda <[email protected]>
1 parent 945bfd5 commit 6ebf6ba

16 files changed

Lines changed: 510 additions & 19 deletions

File tree

cli/command/container/opts.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ import (
1313
"strings"
1414
"time"
1515

16+
"github.com/docker/cli/cli/command"
1617
"github.com/docker/cli/cli/compose/loader"
1718
"github.com/docker/cli/opts"
1819
"github.com/docker/docker/api/types/container"
1920
mounttypes "github.com/docker/docker/api/types/mount"
2021
networktypes "github.com/docker/docker/api/types/network"
2122
"github.com/docker/docker/api/types/strslice"
22-
"github.com/docker/docker/api/types/versions"
2323
"github.com/docker/docker/errdefs"
2424
"github.com/docker/go-connections/nat"
2525
"github.com/pkg/errors"
@@ -1061,8 +1061,8 @@ func validateAttach(val string) (string, error) {
10611061

10621062
func validateAPIVersion(c *containerConfig, serverAPIVersion string) error {
10631063
for _, m := range c.HostConfig.Mounts {
1064-
if m.BindOptions != nil && m.BindOptions.NonRecursive && versions.LessThan(serverAPIVersion, "1.40") {
1065-
return errors.Errorf("bind-nonrecursive requires API v1.40 or later")
1064+
if err := command.ValidateMountWithAPIVersion(m, serverAPIVersion); err != nil {
1065+
return err
10661066
}
10671067
}
10681068
return nil

cli/command/service/opts.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ import (
88
"strings"
99
"time"
1010

11+
"github.com/docker/cli/cli/command"
1112
"github.com/docker/cli/opts"
1213
"github.com/docker/docker/api/types"
1314
"github.com/docker/docker/api/types/container"
1415
"github.com/docker/docker/api/types/swarm"
15-
"github.com/docker/docker/api/types/versions"
1616
"github.com/docker/docker/client"
1717
gogotypes "github.com/gogo/protobuf/types"
1818
"github.com/google/shlex"
@@ -1033,8 +1033,8 @@ const (
10331033

10341034
func validateAPIVersion(c swarm.ServiceSpec, serverAPIVersion string) error {
10351035
for _, m := range c.TaskTemplate.ContainerSpec.Mounts {
1036-
if m.BindOptions != nil && m.BindOptions.NonRecursive && versions.LessThan(serverAPIVersion, "1.40") {
1037-
return errors.Errorf("bind-nonrecursive requires API v1.40 or later")
1036+
if err := command.ValidateMountWithAPIVersion(m, serverAPIVersion); err != nil {
1037+
return err
10381038
}
10391039
}
10401040
return nil

cli/command/utils.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111

1212
"github.com/docker/cli/cli/streams"
1313
"github.com/docker/docker/api/types/filters"
14+
mounttypes "github.com/docker/docker/api/types/mount"
15+
"github.com/docker/docker/api/types/versions"
1416
"github.com/moby/sys/sequential"
1517
"github.com/pkg/errors"
1618
"github.com/spf13/pflag"
@@ -195,3 +197,17 @@ func StringSliceReplaceAt(s, old, new []string, requireIndex int) ([]string, boo
195197
out = append(out, s[idx+len(old):]...)
196198
return out, true
197199
}
200+
201+
// ValidateMountWithAPIVersion validates a mount with the server API version.
202+
func ValidateMountWithAPIVersion(m mounttypes.Mount, serverAPIVersion string) error {
203+
if m.BindOptions != nil {
204+
if m.BindOptions.NonRecursive && versions.LessThan(serverAPIVersion, "1.40") {
205+
return errors.Errorf("bind-nonrecursive requires API v1.40 or later")
206+
}
207+
// bind-readonly-nonrecursive can be safely ignored when API < 1.44
208+
if m.BindOptions.ReadOnlyForceRecursive && versions.LessThan(serverAPIVersion, "1.44") {
209+
return errors.Errorf("bind-readonly-forcerecursive requires API v1.44 or later")
210+
}
211+
}
212+
return nil
213+
}

docs/reference/commandline/service_create.md

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,8 @@ volumes in a service:
393393
<td>
394394
<p>The Engine mounts binds and volumes <tt>read-write</tt> unless <tt>readonly</tt> option
395395
is given when mounting the bind or volume. Note that setting <tt>readonly</tt> for a
396-
bind-mount does not make its submounts <tt>readonly</tt> on the current Linux implementation. See also <tt>bind-nonrecursive</tt>.</p>
396+
bind-mount does not make its submounts <tt>readonly</tt> if Docker Engine is older than v25.0,
397+
or Linux kernel is older than v5.12. See also <a href="#options-for-bind-mounts">Options for Bind Mounts</a>.</p>
397398
<ul>
398399
<li><tt>true</tt> or <tt>1</tt> or no value: Mounts the bind or volume read-only.</li>
399400
<li><tt>false</tt> or <tt>0</tt>: Mounts the bind or volume read-write.</li>
@@ -402,7 +403,7 @@ volumes in a service:
402403
</tr>
403404
</table>
404405

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

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

@@ -434,7 +435,8 @@ The following options can only be used for bind mounts (`type=bind`):
434435
<td><b>bind-nonrecursive</b></td>
435436
<td>
436437
By default, submounts are recursively bind-mounted as well. However, this behavior can be confusing when a
437-
bind mount is configured with <tt>readonly</tt> option, because submounts are not mounted as read-only.
438+
bind mount is configured with <tt>readonly</tt> option, because submounts are not mounted as read-only
439+
if Docker Engine is older than v25, or Linux kernel is older than v5.12.
438440
Set <tt>bind-nonrecursive</tt> to disable recursive bind-mount.<br />
439441
<br />
440442
A value is optional:<br />
@@ -445,6 +447,36 @@ The following options can only be used for bind mounts (`type=bind`):
445447
</ul>
446448
</td>
447449
</tr>
450+
<tr>
451+
<td><b>bind-readonly-nonrecursive</b> or <b>bind-ro-nonrecursive</b></td>
452+
<td>
453+
If set to <tt>true</tt>, submounts are recursively bind-mounted
454+
(unless <tt>bind-nonrecursive</tt> is set to <tt>true</tt> in conjunction),
455+
but they are not recursively made read-only. This corresponds to the default behavior of Docker v24 and older.
456+
A <tt>false</tt> value is ignored when the Docker daemon is running on Linux kernel older than v5.12.<br />
457+
<br />
458+
A value is optional:<br />
459+
<br />
460+
<ul>
461+
<li><tt>true</tt> or <tt>1</tt>: Disables recursive read-only bind-mount.</li>
462+
<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>
463+
</ul>
464+
</td>
465+
</tr>
466+
<tr>
467+
<td><b>bind-readonly-forcerecursive</b> or <b>bind-ro-forcerecursive</b></td>
468+
<td>
469+
If set to <tt>true</tt>, and submounts cannot be made recursively read-only, the Docker daemon raises an error.<br />
470+
This option should be used in conjunction with <tt>bind-propagation=rprivate</tt>.
471+
<br />
472+
A value is optional:<br />
473+
<br />
474+
<ul>
475+
<li><tt>true</tt> or <tt>1</tt>: Force recursive read-only bind-mount.</li>
476+
<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>
477+
</ul>
478+
</td>
479+
</tr>
448480
</table>
449481

450482
##### Bind propagation

docs/reference/run.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1714,13 +1714,21 @@ $ docker run -d --tmpfs /run:rw,noexec,nosuid,size=65536k my_image
17141714
### VOLUME (shared filesystems)
17151715

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

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

1724+
Starting with Docker Engine v25, the `ro` mode makes its submounts read-only when running on
1725+
Linux kernel v5.12 or newer.
1726+
To fall back to the behavior of Docker v24, specify `ro-non-recursive`.
1727+
To explicitly make the mount recursively read-only, specify `ro-force-recursive`
1728+
or `rro`.
1729+
The `ro-force-recursive` (`rro`) mode should be used in conjunction with `bind-propagation=rprivate`.
1730+
The `ro-force-recursive` (`rro`) mode fails when running on Linux kernel older than v5.12.
1731+
17241732
The `nocopy` mode is used to disable automatically copying the requested volume
17251733
path in the container to the volume storage location.
17261734
For named volumes, `copy` is the default mode. Copy modes are not supported

man/docker-run.1.md

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -468,15 +468,22 @@ according to RFC4862.
468468
* `ro`, `readonly`: `true` or `false` (default).
469469

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

473473
Options specific to `bind`:
474474

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

481488
Options specific to `volume`:
482489

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

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

757+
Starting with Docker Engine v25, the `:ro` mode makes its submounts read-only when running on
758+
Linux kernel v5.12 or newer.
759+
To fall back to the behavior of Docker Engine v24, specify `:ro-non-recursive`.
760+
To explicitly make the mount recursively read-only, specify `:ro-force-recursive`
761+
or `:rro`.
762+
The `:ro-force-recursive` (`:rro`) mode should be used in conjunction with `bind-propagation=rprivate`.
763+
The `:ro-force-recursive` (`:rro`) mode fails when running on Linux kernel older than v5.12.
764+
750765
Labeling systems like SELinux require that proper labels are placed on volume
751766
content mounted into a container. Without a label, the security system might
752767
prevent the processes running inside the container from using the content. By

opts/mount.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,18 @@ func (m *MountOpt) Set(value string) error {
8181
case "bind-nonrecursive":
8282
bindOptions().NonRecursive = true
8383
continue
84+
case "bind-readonly-nonrecursive", "bind-ro-nonrecursive":
85+
// ReadOnlyNonRecursive makes the mount non-recursively read-only, but still leaves the mount recursive
86+
// (unless NonRecursive is set to true in conjunction).
87+
bindOptions().ReadOnlyNonRecursive = true
88+
// Implies ReadOnly = true
89+
mount.ReadOnly = true
90+
continue
91+
case "bind-readonly-forcerecursive", "bind-ro-forcerecursive":
92+
bindOptions().ReadOnlyForceRecursive = true
93+
// Implies ReadOnly = true
94+
mount.ReadOnly = true
95+
continue
8496
default:
8597
return fmt.Errorf("invalid field '%s' must be a key=value pair", field)
8698
}

opts/mount_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,3 +217,72 @@ func TestMountOptSetTmpfsError(t *testing.T) {
217217
assert.ErrorContains(t, m.Set("type=tmpfs,target=/foo,tmpfs-mode=foo"), "invalid value for tmpfs-mode")
218218
assert.ErrorContains(t, m.Set("type=tmpfs"), "target is required")
219219
}
220+
221+
func TestMountOptSetBindNonRecursive(t *testing.T) {
222+
// Makes the mount itself non-recursive
223+
t.Run("bind-nonrecursive", func(t *testing.T) {
224+
var mount MountOpt
225+
assert.NilError(t, mount.Set("type=bind,source=/foo,target=/bar,bind-nonrecursive"))
226+
assert.Check(t, is.DeepEqual([]mounttypes.Mount{
227+
{
228+
Type: mounttypes.TypeBind,
229+
Source: "/foo",
230+
Target: "/bar",
231+
BindOptions: &mounttypes.BindOptions{
232+
NonRecursive: true,
233+
},
234+
},
235+
}, mount.Value()))
236+
})
237+
238+
// The mount itself is still recursive, but it is made read-only non-recursively
239+
t.Run("bind-readonly-nonrecursive", func(t *testing.T) {
240+
var mount MountOpt
241+
assert.NilError(t, mount.Set("type=bind,source=/foo,target=/bar,bind-readonly-nonrecursive"))
242+
assert.Check(t, is.DeepEqual([]mounttypes.Mount{
243+
{
244+
Type: mounttypes.TypeBind,
245+
Source: "/foo",
246+
Target: "/bar",
247+
ReadOnly: true,
248+
BindOptions: &mounttypes.BindOptions{
249+
ReadOnlyNonRecursive: true,
250+
},
251+
},
252+
}, mount.Value()))
253+
})
254+
255+
t.Run("bind-readonly-forcerecursive", func(t *testing.T) {
256+
var mount MountOpt
257+
assert.NilError(t, mount.Set("type=bind,source=/foo,target=/bar,bind-readonly-forcerecursive"))
258+
assert.Check(t, is.DeepEqual([]mounttypes.Mount{
259+
{
260+
Type: mounttypes.TypeBind,
261+
Source: "/foo",
262+
Target: "/bar",
263+
ReadOnly: true,
264+
BindOptions: &mounttypes.BindOptions{
265+
ReadOnlyForceRecursive: true,
266+
},
267+
},
268+
}, mount.Value()))
269+
})
270+
271+
// Valid combination, but not really useful
272+
t.Run("bind-nonrecursive,bind-readonly-nonrecursive", func(t *testing.T) {
273+
var mount MountOpt
274+
assert.NilError(t, mount.Set("type=bind,source=/foo,target=/bar,bind-nonrecursive,bind-readonly-nonrecursive"))
275+
assert.Check(t, is.DeepEqual([]mounttypes.Mount{
276+
{
277+
Type: mounttypes.TypeBind,
278+
Source: "/foo",
279+
Target: "/bar",
280+
ReadOnly: true,
281+
BindOptions: &mounttypes.BindOptions{
282+
NonRecursive: true,
283+
ReadOnlyNonRecursive: true,
284+
},
285+
},
286+
}, mount.Value()))
287+
})
288+
}

vendor.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ require (
1010
github.com/containerd/containerd v1.6.21
1111
github.com/creack/pty v1.1.18
1212
github.com/docker/distribution v2.8.2+incompatible
13-
github.com/docker/docker v24.0.0-rc.2.0.20230523155306-cf4df9d8ae4c+incompatible // master (v25.0.0-dev)
13+
github.com/docker/docker v24.0.0-rc.2.0.20230528104423-2ebd97dec1a2+incompatible // master (v25.0.0-dev)
1414
github.com/docker/docker-credential-helpers v0.7.0
1515
github.com/docker/go-connections v0.4.0
1616
github.com/docker/go-units v0.5.0
@@ -62,6 +62,7 @@ require (
6262
github.com/miekg/pkcs11 v1.1.1 // indirect
6363
github.com/moby/sys/symlink v0.2.0 // indirect
6464
github.com/opencontainers/runc v1.1.7 // indirect
65+
github.com/opencontainers/runtime-spec v1.1.0-rc.2 // indirect
6566
github.com/prometheus/client_golang v1.14.0 // indirect
6667
github.com/prometheus/client_model v0.3.0 // indirect
6768
github.com/prometheus/common v0.37.0 // indirect

vendor.sum

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,8 @@ github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xb
9696
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
9797
github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
9898
github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
99-
github.com/docker/docker v24.0.0-rc.2.0.20230523155306-cf4df9d8ae4c+incompatible h1:stJU/EC2yJHujjvqyEAHeNxsIXtwuCvvYwImyaJ0wtI=
100-
github.com/docker/docker v24.0.0-rc.2.0.20230523155306-cf4df9d8ae4c+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
99+
github.com/docker/docker v24.0.0-rc.2.0.20230528104423-2ebd97dec1a2+incompatible h1:N7Y6lZFkcPXwoNXTImu92izjoh5o9VU8NVApLdFIHe8=
100+
github.com/docker/docker v24.0.0-rc.2.0.20230528104423-2ebd97dec1a2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
101101
github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A=
102102
github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0=
103103
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=
@@ -305,6 +305,8 @@ github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b h1
305305
github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ=
306306
github.com/opencontainers/runc v1.1.7 h1:y2EZDS8sNng4Ksf0GUYNhKbTShZJPJg1FiXJNH/uoCk=
307307
github.com/opencontainers/runc v1.1.7/go.mod h1:CbUumNnWCuTGFukNXahoo/RFBZvDAgRh/smNYNOhA50=
308+
github.com/opencontainers/runtime-spec v1.1.0-rc.2 h1:ucBtEms2tamYYW/SvGpvq9yUN0NEVL6oyLEwDcTSrk8=
309+
github.com/opencontainers/runtime-spec v1.1.0-rc.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
308310
github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU=
309311
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
310312
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=

0 commit comments

Comments
 (0)