Skip to content

Conversation

@AkihiroSuda
Copy link
Collaborator

@AkihiroSuda AkihiroSuda commented May 29, 2023

For:

See opts/mount_test.go:TestMountOptSetBindRecursive() for the behavior:

  • bind-recursive=true, bind-recursive=enabled: read-only mounts are recursively read-only if Engine >= v25 && kernel >= v5.12
  • bind-recursive=false, bind-recursive=disabled: alias of bind-nonrecursive=true. Now this form is preferred over bind-nonrecursive=true.
  • bind-recursive=writable: conforms to Docker v24; read-only mounts are recursively mounted but not recursively read-only
  • bind-recursive=readonly: force recursively read-only, or raise an error

Documentation will be added separately after reaching consensus on the design.

Backup (2023-05-29)

AkihiroSuda@6ebf6ba

- What I did

Added the CLI and the docs for:

- How I did it

The -v options are implemented on the daemon side.

  • ro-non-recursive
  • ro-force-recursive (alias: rro)

The following "CSV" options for --mount are newly added in this CLI-side PR:

  • bind-readonly-nonrecursive (alias: bind-ro-nonrecursive) (Do not confuse this with the exiswting bind-nonrecursive)
  • bind-readonly-forcerecursive (alias: bind-ro-forcerecursive)

- How to verify it
Run docker run --mount type=bind,source=/mnt,target=/mnt,bind-ro-forcerecursive,bind-propagation=rprivate, and make sure the mount is made recursively read-only.

- Description for the changelog

Add CLI and docs for recursively read-only mounts

- A picture of a cute animal (not mandatory but encouraged)
🐧

@codecov-commenter
Copy link

codecov-commenter commented May 29, 2023

Codecov Report

Merging #4316 (fc6976d) into master (05bec8d) will increase coverage by 0.00%.
The diff coverage is 50.00%.

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #4316   +/-   ##
=======================================
  Coverage   59.70%   59.71%           
=======================================
  Files         288      288           
  Lines       24816    24858   +42     
=======================================
+ Hits        14817    14843   +26     
- Misses       9113     9128   +15     
- Partials      886      887    +1     

@AkihiroSuda
Copy link
Collaborator Author

@thaJeztah PTAL 🙏

@thaJeztah
Copy link
Member

Oh! Thanks for the ping, had this one on my list, started looking, and then forgot 😅

// ValidateMountWithAPIVersion validates a mount with the server API version.
func ValidateMountWithAPIVersion(m mounttypes.Mount, serverAPIVersion string) error {
if m.BindOptions != nil {
if m.BindOptions.NonRecursive && versions.LessThan(serverAPIVersion, "1.40") {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the overhead is neglectable, so probably not worth doing; we could consider having a if versions.GreaterThanOrEqualTo(serverAPIVersion, "1.44") somewhere to skip the checks.

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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably use a full version for these v25.0.0, but probably we should consider rewriting this section to assume "latest" (it now works).

</td>
</tr>
<tr>
<td><b>bind-readonly-nonrecursive</b> or <b>bind-ro-nonrecursive</b></td>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm considering if we should (soft) deprecated bind-nonrecursive, and instead have a "recursive mounts" option that takes a (non-boolean) value, e.g. something like;

  • recursive-mounts=enabled (default)
  • recursive-mounts=disabled
  • recursive-mounts=readonly (should error if bind-propagation is set by user (to anything other than rprivate), but perhaps handled by API)
  • recursive-mounts=force-readonly (should error if bind-propagation is set by user (to anything other than rprivate), but perhaps handled by API)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about this?

                case "bind-recursive":
                        valS := val
                        // Allow boolean as an alias to "enabled" or "disabled"
                        if b, err := strconv.ParseBool(valS); err == nil {
                                if b {
                                        valS = "enabled"
                                } else {
                                        valS = "disabled"
                                }
                        }
                        switch valS {
                        case "enabled": // read-only mounts are recursively read-only if Engine >= v25 && kernel >= v5.
12
                                // NOP
                        case "disabled": // alias of bind-nonrecursive=true
                                bindOptions().NonRecursive = true
                        case "readonly-classic": // conforms to Docker v24; read-only mounts are recursively mounted bu
t not recursively read-only
                                bindOptions().ReadOnlyNonRecursive = true
                                mount.ReadOnly = true
                        case "readonly-force": // force recursively read-only, or raise an error
                                bindOptions().ReadOnlyForceRecursive = true
                                bindOptions().Propagation = mounttypes.PropagationRPrivate
                                mount.ReadOnly = true
                        default:
                                return fmt.Errorf("invalid value for %s: %s (must be \"enabled\", \"disabled\", \"reado
nly-classic\", or \"readonly-force\")",
                                        key, val)
                        }

@AkihiroSuda AkihiroSuda marked this pull request as draft June 12, 2023 12:00
@neersighted neersighted mentioned this pull request Jul 10, 2023
26 tasks
@AkihiroSuda AkihiroSuda changed the title CLI and docs for recursively read-only mounts mount: add bind-recursive=<bool|string> and deprecate bind-nonrecursive=<bool> Jul 20, 2023
@AkihiroSuda AkihiroSuda marked this pull request as ready for review July 20, 2023 14:10
@AkihiroSuda AkihiroSuda requested a review from thaJeztah July 20, 2023 14:10
@AkihiroSuda
Copy link
Collaborator Author

AkihiroSuda commented Sep 22, 2023

@thaJeztah Could you take a look?
#4316 (comment)

@thaJeztah
Copy link
Member

Sorry, should've posted some comments here. I've been starting to review this one multiple times, and trying to construct a table with possible combinations (and what their result would be), and my head exploded each time 😂

However, the other day it occurred to me what I think was the part that I had trouble with;

  • the advanced syntax provides two separate options to control the "amount of read-only" and whether recursive mounts are included in the mount (readonly and bind-recursive)
  • ☝️ the confusing bit there was "how do these relate"?

So the goal we're aiming for here is;

  1. provide the "easy" option (-v host-path:container-path:ro) that covers the "90% of use-cases"
  2. provide the same through --mount, but allow more advanced options for the 10% of use-cases (giving them more control)

However I think (and that's the part I need to look at again, as I was considering this away from my computer), to achieve the same result as 1., but using the advanced syntax (2.), I now MUST provide all the options (i.e. when using --mount ).

While it's good to allow the user to have more fine-grained control, my ideal would be to make that "optional"; even with the advanced --mount syntax.

So my thinking is; this:

--mount type=bind,source=host-path,target=container-path,readonly

To be the equivalent of:

-v host-path:container-path:ro

(i.e.; try recursive-readonly, and fallback to non-recursive)

But if the user needs to have more fine-grained control, then allow them to set the bind-recursive options;

e.g.: force recursive read-only:

--mount type=bind,source=host-path,target=container-path,readonly,bind-recursive=readonly-force

Maybe we don't even need the readonly-force, and it could just be readonly (as it's now limited to only the recursive parts)

e.g.: read-only, but no recursive mounts:

--mount type=bind,source=host-path,target=container-path,readonly,bind-recursive=disabled

@AkihiroSuda
Copy link
Collaborator Author

Thanks, renamed readonly-force to readonly.

opts/mount.go Outdated
Comment on lines 132 to 138
case "readonly-classic": // conforms to Docker v24; read-only mounts are recursively mounted but not recursively read-only
bindOptions().ReadOnlyNonRecursive = true
mount.ReadOnly = true
case "readonly": // force recursively read-only, or raise an error
bindOptions().ReadOnlyForceRecursive = true
bindOptions().Propagation = mounttypes.PropagationRPrivate
mount.ReadOnly = true
Copy link
Member

@thaJeztah thaJeztah Sep 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we're getting close (sorry had to jump out earlier as I had some calls); if we consider the bind-recursive option to be controlling options for recursive mounts (and not for the "main" mount), should we update mount.Readonly at all here?

My thinking (please reply if I'm completely off the mark ❤️);

  • readonly is the "overall" option; when enabled, by default it makes both the "main" mount, and any nested mounts read-only (i.e. "best-effort recursive-readonly"
  • bind-recursive=disabled disables nested mounts
  • bind-recursive=readonly forces nested mounts to be read-only
    • 👆 this can also be done separate from readonly, so --mount type=bind,source=host-path,target=container-path,bind-recursive=readonly (without "global" readonly) would then mean: the main mount is read-write, but nested mounts (if any) would be mounted read-only
    • 👆 ❓ is that combination technically possible?
  • which leaves readonly-classic if we don't set mount.ReadOnly there, would that make it the same as enabled ? (i.e.; nested mounts are mounted, but "read-write") ? If that's the case, then maybe we don't need that option 🤔

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mount.ReadOnly = true is set here, as mount.ReadOnly == false && bindOptions().ReadOnlyXXX = true doesn't seem a valid configuration.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's the part I was wondering; is it (technically) possible to make the "main" mount (so target) read-write, but making nested mounts read-only?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically; my thinking here is to make nested mounts "inherit" the readonly option as a default, with the option to differentiate (through the bind-recursive options). In that case it's (I think) easier to comprehend (by default, they'll all be attempted the same, but if I need them to be different, I can control that through this option)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's the part I was wondering; is it (technically) possible to make the "main" mount (so target) read-write, but making nested mounts read-only?

Theoretically possible by scanning procfs, but I don't think we want to implement it

Copy link
Member

@neersighted neersighted left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a lot better UX-wise, with the recursion divorced from the readonly option. Nonetheless, I think it's not quite there, mostly because confusing combinations are possible and should be caught in validation instead of silently overriding each other.

That's hard, since we don't have validation yet/the way this code is structured makes it rather obtuse. Still, we could e.g. use a fixed source order with a slice that defines a strong ordering, and check that way, or we could think of another method.

Let me know if my comments make sense and I can help with figuring out how to implement them.

opts/mount.go Outdated
// NOP
case "disabled": // alias of bind-nonrecursive=true
bindOptions().NonRecursive = true
case "readonly-classic": // conforms to Docker v24; read-only mounts are recursively mounted but not recursively read-only
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

readonly-classic doesn't feel right to me, as we're trying to split the "recursion" from the "readonlyness" with this.

How about writable, which makes it clear that recursive mounts will be writable/that was the legacy behavior?


I also think setting ReadOnly here is a smell -- we should instead have some check that errors when ReadOnly has not been set by the user (e.g. readonly=false,bind-recursive=readonly should be an error in validation).

Copy link
Collaborator Author

@AkihiroSuda AkihiroSuda Sep 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to "writable", but what about calling this "classic" or "legacy"?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think classic or legacy don't really describe what it does, and when documenting this option and the default for old engine versions, it really hammers home the old behavior. I don't object to an alias under those names, but I would prefer avoiding it as I think they're meaningless unless you have an excellent understanding of the kernel/engine history around this.

opts/mount.go Outdated
mount.ReadOnly = true
case "readonly": // force recursively read-only, or raise an error
bindOptions().ReadOnlyForceRecursive = true
bindOptions().Propagation = mounttypes.PropagationRPrivate
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should overwrite the propagation here unconditionally. I think it's fine to set it implicitly from a UX perspective, but if the user specifies any different propagation it should be a validation error.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I do like the trivial validation overall, but I'm not sure about this case. I don't think that requiring the user to set the propagation explicitly is good UX; I'd prefer this to be set implicitly, but to error if the user explicitly sets a different propagation. Unfortunately that doesn't work with the simple validation you've added, but maybe you have some ideas to that end?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default implicit "rprivate" propagation sometimes flips to "rslave":

So I guess we should demand explicit "rprivate" here

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I had that in mind. What I meant to say is that we want to be explicit at the API level, but I'm not sure that we want to require the user to explicitly specify a propagation mode to use bind-recursive=readonly. However with the current validation, this is messy.

I am okay with what you have; it makes the feature usable, and bind-recursive=readonly is definitely a power-user feature/we're "upgrading" users by default to the new enabled behavior anyway.

How about a // TODO: to implicitly set propagation and error if the user specifies a propagation in a future refactor/UX polish pass?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added

opts/mount.go Outdated
return fmt.Errorf("cannot mix 'tmpfs-*' options with mount type '%s'", mount.Type)
}

if mount.BindOptions != nil && mount.BindOptions.ReadOnlyForceRecursive {
Copy link
Member

@neersighted neersighted Sep 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably needs a check for ReadOnlyNonRecursive as well, as readonly=false,bind-recursive=writable makes no sense.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as readonly=false,bind-recursive=readonly

Ah, yes, that was the case I mentioned above (and was hoping it would work, as I could see use-cases for a "writable" bind-mount with nested mounts read-only), but currently not a combination that was possible; #4316 (comment)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to be sure; in the current implementation, does --mount type=bind,readonly,..... do the same as -v source:target:ro (so best-effort recursive), or do we need to make changes to the readonly option to make that happen (I lost track again on which combination did that, and what parts we did on the daemon side)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably needs a check for ReadOnlyNonRecursive as well, as readonly=false,bind-recursive=readonly makes no sense.

Done (I assume you meant readonly=false,bind-recursive=writable)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to be sure; in the current implementation, does --mount type=bind,readonly,..... do the same as -v source:target:ro (so best-effort recursive), or do we need to make changes to the readonly option to make that happen (I lost track again on which combination did that, and what parts we did on the daemon side)

The former one.

@thaJeztah
Copy link
Member

FWIW; we discussed this PR Yesterday, and w.r.t. validation it's possible that we need to make some follow-up changes, because of the way these options are parsed (i.e., order can matter if multiple options interact with the same "API" options behind the scenes).

We are looking at doing a first beta for v25.0 soon (to get the release train warmed up), and I think for that the main goal is to have the UX "sorted" (as good as possible), but after that we can still tweak / fix validation corner cases etc.

Copy link
Member

@neersighted neersighted left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall LGTM with reservations (about the need for the user to explicitly set the propagation mode); as per my review comment, I think a TODO would be fine here, and we should get this in.

…rsive=<bool>`

See `opts/mount_test.go:TestMountOptSetBindRecursive()` for the behavior.

Documentation will be added separately after reaching consensus on the
design.

Signed-off-by: Akihiro Suda <[email protected]>
Copy link
Member

@thaJeztah thaJeztah left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

added a docs/revisit label, as we will have to do some docs follow-ups, as well as looking at completion scripts (not sure if we provide completion for these currently)

@thaJeztah thaJeztah merged commit dcc1610 into docker:master Oct 2, 2023
@AkihiroSuda
Copy link
Collaborator Author

docs:

Comment on lines +119 to +128
valS := val
// Allow boolean as an alias to "enabled" or "disabled"
if b, err := strconv.ParseBool(valS); err == nil {
if b {
valS = "enabled"
} else {
valS = "disabled"
}
}
switch valS {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened a PR for discussion to remove the boolean variants (motivation outlined in that PR);

thaJeztah added a commit to thaJeztah/cli that referenced this pull request Aug 5, 2025
The `bind-nonrecursive` option was replaced with the [`bind-recursive`]
option (see [cli-4316], [cli-4671]). The option was still accepted, but
printed a deprecation warning:

    bind-nonrecursive is deprecated, use bind-recursive=disabled instead

In the v29.0 release, this warning is removed, and returned as an error.
Users should use the equivalent `bind-recursive=disabled` option instead.

[`bind-recursive`]: https://docs.docker.com/engine/storage/bind-mounts/#recursive-mounts
[cli-4316]: docker#4316
[cli-4671]: docker#4671

Signed-off-by: Sebastiaan van Stijn <[email protected]>
thaJeztah added a commit to thaJeztah/cli that referenced this pull request Aug 19, 2025
The `bind-nonrecursive` option was replaced with the [`bind-recursive`]
option (see [cli-4316], [cli-4671]), but the deprecated docs was not
updated to mention.

Based on abfe4d4 in master

[`bind-recursive`]: https://docs.docker.com/engine/storage/bind-mounts/#recursive-mounts
[cli-4316]: docker#4316
[cli-4671]: docker#4671

Signed-off-by: Sebastiaan van Stijn <[email protected]>
(cherry picked from commit abfe4d4)
Signed-off-by: Sebastiaan van Stijn <[email protected]>
thaJeztah added a commit to thaJeztah/cli that referenced this pull request Aug 19, 2025
The `bind-nonrecursive` option was replaced with the [`bind-recursive`]
option (see [cli-4316], [cli-4671]), but the deprecated docs was not
updated to mention.

Based on abfe4d4 in master

[`bind-recursive`]: https://docs.docker.com/engine/storage/bind-mounts/#recursive-mounts
[cli-4316]: docker#4316
[cli-4671]: docker#4671

Signed-off-by: Sebastiaan van Stijn <[email protected]>
(cherry picked from commit abfe4d4)
Signed-off-by: Sebastiaan van Stijn <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants