This allows non-recursive bind-mount, i.e. mount(2) with "bind" rather than "rbind".
Swarm-mode will be supported in a separate PR because of mutual vendoring.
Signed-off-by: Akihiro Suda <suda.akihiro@lab.ntt.co.jp>
| ... | ... |
@@ -465,6 +465,16 @@ func (s *containerRouter) postContainersCreate(ctx context.Context, w http.Respo |
| 465 | 465 |
hostConfig.AutoRemove = false |
| 466 | 466 |
} |
| 467 | 467 |
|
| 468 |
+ // When using API 1.39 and under, BindOptions.NonRecursive should be ignored because it |
|
| 469 |
+ // was added in API 1.40. |
|
| 470 |
+ if hostConfig != nil && versions.LessThan(version, "1.40") {
|
|
| 471 |
+ for _, m := range hostConfig.Mounts {
|
|
| 472 |
+ if bo := m.BindOptions; bo != nil {
|
|
| 473 |
+ bo.NonRecursive = false |
|
| 474 |
+ } |
|
| 475 |
+ } |
|
| 476 |
+ } |
|
| 477 |
+ |
|
| 468 | 478 |
ccr, err := s.backend.ContainerCreate(types.ContainerCreateConfig{
|
| 469 | 479 |
Name: name, |
| 470 | 480 |
Config: config, |
| ... | ... |
@@ -265,6 +265,10 @@ definitions: |
| 265 | 265 |
- "rshared" |
| 266 | 266 |
- "slave" |
| 267 | 267 |
- "rslave" |
| 268 |
+ NonRecursive: |
|
| 269 |
+ description: "Disable recursive bind mount." |
|
| 270 |
+ type: "boolean" |
|
| 271 |
+ default: false |
|
| 268 | 272 |
VolumeOptions: |
| 269 | 273 |
description: "Optional configuration for the `volume` type." |
| 270 | 274 |
type: "object" |
| ... | ... |
@@ -79,7 +79,8 @@ const ( |
| 79 | 79 |
|
| 80 | 80 |
// BindOptions defines options specific to mounts of type "bind". |
| 81 | 81 |
type BindOptions struct {
|
| 82 |
- Propagation Propagation `json:",omitempty"` |
|
| 82 |
+ Propagation Propagation `json:",omitempty"` |
|
| 83 |
+ NonRecursive bool `json:",omitempty"` |
|
| 83 | 84 |
} |
| 84 | 85 |
|
| 85 | 86 |
// VolumeOptions represents the options for a mount of type volume. |
| ... | ... |
@@ -4,9 +4,10 @@ package container // import "github.com/docker/docker/container" |
| 4 | 4 |
|
| 5 | 5 |
// Mount contains information for a mount operation. |
| 6 | 6 |
type Mount struct {
|
| 7 |
- Source string `json:"source"` |
|
| 8 |
- Destination string `json:"destination"` |
|
| 9 |
- Writable bool `json:"writable"` |
|
| 10 |
- Data string `json:"data"` |
|
| 11 |
- Propagation string `json:"mountpropagation"` |
|
| 7 |
+ Source string `json:"source"` |
|
| 8 |
+ Destination string `json:"destination"` |
|
| 9 |
+ Writable bool `json:"writable"` |
|
| 10 |
+ Data string `json:"data"` |
|
| 11 |
+ Propagation string `json:"mountpropagation"` |
|
| 12 |
+ NonRecursive bool `json:"nonrecursive"` |
|
| 12 | 13 |
} |
| ... | ... |
@@ -321,6 +321,12 @@ func containerToGRPC(c *types.ContainerSpec) (*swarmapi.ContainerSpec, error) {
|
| 321 | 321 |
} else if string(m.BindOptions.Propagation) != "" {
|
| 322 | 322 |
return nil, fmt.Errorf("invalid MountPropagation: %q", m.BindOptions.Propagation)
|
| 323 | 323 |
} |
| 324 |
+ |
|
| 325 |
+ if m.BindOptions.NonRecursive {
|
|
| 326 |
+ // TODO(AkihiroSuda): NonRecursive is unsupported for Swarm-mode now because of mutual vendoring |
|
| 327 |
+ // across moby and swarmkit. Will be available soon after the moby PR gets merged. |
|
| 328 |
+ return nil, fmt.Errorf("invalid NonRecursive: %q", m.BindOptions.Propagation)
|
|
| 329 |
+ } |
|
| 324 | 330 |
} |
| 325 | 331 |
|
| 326 | 332 |
if m.VolumeOptions != nil {
|
| ... | ... |
@@ -565,7 +565,11 @@ func setMounts(daemon *Daemon, s *specs.Spec, c *container.Container, mounts []c |
| 565 | 565 |
} |
| 566 | 566 |
} |
| 567 | 567 |
|
| 568 |
- opts := []string{"rbind"}
|
|
| 568 |
+ bindMode := "rbind" |
|
| 569 |
+ if m.NonRecursive {
|
|
| 570 |
+ bindMode = "bind" |
|
| 571 |
+ } |
|
| 572 |
+ opts := []string{bindMode}
|
|
| 569 | 573 |
if !m.Writable {
|
| 570 | 574 |
opts = append(opts, "ro") |
| 571 | 575 |
} |
| ... | ... |
@@ -9,6 +9,7 @@ import ( |
| 9 | 9 |
"strconv" |
| 10 | 10 |
"strings" |
| 11 | 11 |
|
| 12 |
+ mounttypes "github.com/docker/docker/api/types/mount" |
|
| 12 | 13 |
"github.com/docker/docker/container" |
| 13 | 14 |
"github.com/docker/docker/pkg/fileutils" |
| 14 | 15 |
"github.com/docker/docker/pkg/mount" |
| ... | ... |
@@ -58,6 +59,9 @@ func (daemon *Daemon) setupMounts(c *container.Container) ([]container.Mount, er |
| 58 | 58 |
Writable: m.RW, |
| 59 | 59 |
Propagation: string(m.Propagation), |
| 60 | 60 |
} |
| 61 |
+ if m.Spec.Type == mounttypes.TypeBind && m.Spec.BindOptions != nil {
|
|
| 62 |
+ mnt.NonRecursive = m.Spec.BindOptions.NonRecursive |
|
| 63 |
+ } |
|
| 61 | 64 |
if m.Volume != nil {
|
| 62 | 65 |
attributes := map[string]string{
|
| 63 | 66 |
"driver": m.Volume.DriverName(), |
| ... | ... |
@@ -129,11 +133,15 @@ func (daemon *Daemon) mountVolumes(container *container.Container) error {
|
| 129 | 129 |
return err |
| 130 | 130 |
} |
| 131 | 131 |
|
| 132 |
- opts := "rbind,ro" |
|
| 132 |
+ bindMode := "rbind" |
|
| 133 |
+ if m.NonRecursive {
|
|
| 134 |
+ bindMode = "bind" |
|
| 135 |
+ } |
|
| 136 |
+ writeMode := "ro" |
|
| 133 | 137 |
if m.Writable {
|
| 134 |
- opts = "rbind,rw" |
|
| 138 |
+ writeMode = "rw" |
|
| 135 | 139 |
} |
| 136 |
- |
|
| 140 |
+ opts := strings.Join([]string{bindMode, writeMode}, ",")
|
|
| 137 | 141 |
if err := mount.Mount(m.Source, dest, bindMountType, opts); err != nil {
|
| 138 | 142 |
return err |
| 139 | 143 |
} |
| ... | ... |
@@ -27,6 +27,8 @@ keywords: "API, Docker, rcli, REST, documentation" |
| 27 | 27 |
on the node.label. The format of the label filter is `node.label=<key>`/`node.label=<key>=<value>` |
| 28 | 28 |
to return those with the specified labels, or `node.label!=<key>`/`node.label!=<key>=<value>` |
| 29 | 29 |
to return those without the specified labels. |
| 30 |
+* `POST /containers/create`, `GET /containers/{id}/json`, and `GET /containers/json` now supports
|
|
| 31 |
+ `BindOptions.NonRecursive`. |
|
| 30 | 32 |
|
| 31 | 33 |
## V1.39 API changes |
| 32 | 34 |
|
| ... | ... |
@@ -5,17 +5,22 @@ import ( |
| 5 | 5 |
"fmt" |
| 6 | 6 |
"path/filepath" |
| 7 | 7 |
"testing" |
| 8 |
+ "time" |
|
| 8 | 9 |
|
| 9 | 10 |
"github.com/docker/docker/api/types" |
| 10 |
- "github.com/docker/docker/api/types/container" |
|
| 11 |
- "github.com/docker/docker/api/types/mount" |
|
| 11 |
+ containertypes "github.com/docker/docker/api/types/container" |
|
| 12 |
+ mounttypes "github.com/docker/docker/api/types/mount" |
|
| 12 | 13 |
"github.com/docker/docker/api/types/network" |
| 14 |
+ "github.com/docker/docker/api/types/versions" |
|
| 13 | 15 |
"github.com/docker/docker/client" |
| 16 |
+ "github.com/docker/docker/integration/internal/container" |
|
| 14 | 17 |
"github.com/docker/docker/internal/test/request" |
| 18 |
+ "github.com/docker/docker/pkg/mount" |
|
| 15 | 19 |
"github.com/docker/docker/pkg/system" |
| 16 | 20 |
"gotest.tools/assert" |
| 17 | 21 |
is "gotest.tools/assert/cmp" |
| 18 | 22 |
"gotest.tools/fs" |
| 23 |
+ "gotest.tools/poll" |
|
| 19 | 24 |
"gotest.tools/skip" |
| 20 | 25 |
) |
| 21 | 26 |
|
| ... | ... |
@@ -32,11 +37,11 @@ func TestContainerNetworkMountsNoChown(t *testing.T) {
|
| 32 | 32 |
|
| 33 | 33 |
tmpNWFileMount := tmpDir.Join("nwfile")
|
| 34 | 34 |
|
| 35 |
- config := container.Config{
|
|
| 35 |
+ config := containertypes.Config{
|
|
| 36 | 36 |
Image: "busybox", |
| 37 | 37 |
} |
| 38 |
- hostConfig := container.HostConfig{
|
|
| 39 |
- Mounts: []mount.Mount{
|
|
| 38 |
+ hostConfig := containertypes.HostConfig{
|
|
| 39 |
+ Mounts: []mounttypes.Mount{
|
|
| 40 | 40 |
{
|
| 41 | 41 |
Type: "bind", |
| 42 | 42 |
Source: tmpNWFileMount, |
| ... | ... |
@@ -93,39 +98,39 @@ func TestMountDaemonRoot(t *testing.T) {
|
| 93 | 93 |
|
| 94 | 94 |
for _, test := range []struct {
|
| 95 | 95 |
desc string |
| 96 |
- propagation mount.Propagation |
|
| 97 |
- expected mount.Propagation |
|
| 96 |
+ propagation mounttypes.Propagation |
|
| 97 |
+ expected mounttypes.Propagation |
|
| 98 | 98 |
}{
|
| 99 | 99 |
{
|
| 100 | 100 |
desc: "default", |
| 101 | 101 |
propagation: "", |
| 102 |
- expected: mount.PropagationRSlave, |
|
| 102 |
+ expected: mounttypes.PropagationRSlave, |
|
| 103 | 103 |
}, |
| 104 | 104 |
{
|
| 105 | 105 |
desc: "private", |
| 106 |
- propagation: mount.PropagationPrivate, |
|
| 106 |
+ propagation: mounttypes.PropagationPrivate, |
|
| 107 | 107 |
}, |
| 108 | 108 |
{
|
| 109 | 109 |
desc: "rprivate", |
| 110 |
- propagation: mount.PropagationRPrivate, |
|
| 110 |
+ propagation: mounttypes.PropagationRPrivate, |
|
| 111 | 111 |
}, |
| 112 | 112 |
{
|
| 113 | 113 |
desc: "slave", |
| 114 |
- propagation: mount.PropagationSlave, |
|
| 114 |
+ propagation: mounttypes.PropagationSlave, |
|
| 115 | 115 |
}, |
| 116 | 116 |
{
|
| 117 | 117 |
desc: "rslave", |
| 118 |
- propagation: mount.PropagationRSlave, |
|
| 119 |
- expected: mount.PropagationRSlave, |
|
| 118 |
+ propagation: mounttypes.PropagationRSlave, |
|
| 119 |
+ expected: mounttypes.PropagationRSlave, |
|
| 120 | 120 |
}, |
| 121 | 121 |
{
|
| 122 | 122 |
desc: "shared", |
| 123 |
- propagation: mount.PropagationShared, |
|
| 123 |
+ propagation: mounttypes.PropagationShared, |
|
| 124 | 124 |
}, |
| 125 | 125 |
{
|
| 126 | 126 |
desc: "rshared", |
| 127 |
- propagation: mount.PropagationRShared, |
|
| 128 |
- expected: mount.PropagationRShared, |
|
| 127 |
+ propagation: mounttypes.PropagationRShared, |
|
| 128 |
+ expected: mounttypes.PropagationRShared, |
|
| 129 | 129 |
}, |
| 130 | 130 |
} {
|
| 131 | 131 |
t.Run(test.desc, func(t *testing.T) {
|
| ... | ... |
@@ -139,26 +144,26 @@ func TestMountDaemonRoot(t *testing.T) {
|
| 139 | 139 |
bindSpecRoot := info.DockerRootDir + ":" + "/foo" + propagationSpec |
| 140 | 140 |
bindSpecSub := filepath.Join(info.DockerRootDir, "containers") + ":/foo" + propagationSpec |
| 141 | 141 |
|
| 142 |
- for name, hc := range map[string]*container.HostConfig{
|
|
| 142 |
+ for name, hc := range map[string]*containertypes.HostConfig{
|
|
| 143 | 143 |
"bind root": {Binds: []string{bindSpecRoot}},
|
| 144 | 144 |
"bind subpath": {Binds: []string{bindSpecSub}},
|
| 145 | 145 |
"mount root": {
|
| 146 |
- Mounts: []mount.Mount{
|
|
| 146 |
+ Mounts: []mounttypes.Mount{
|
|
| 147 | 147 |
{
|
| 148 |
- Type: mount.TypeBind, |
|
| 148 |
+ Type: mounttypes.TypeBind, |
|
| 149 | 149 |
Source: info.DockerRootDir, |
| 150 | 150 |
Target: "/foo", |
| 151 |
- BindOptions: &mount.BindOptions{Propagation: test.propagation},
|
|
| 151 |
+ BindOptions: &mounttypes.BindOptions{Propagation: test.propagation},
|
|
| 152 | 152 |
}, |
| 153 | 153 |
}, |
| 154 | 154 |
}, |
| 155 | 155 |
"mount subpath": {
|
| 156 |
- Mounts: []mount.Mount{
|
|
| 156 |
+ Mounts: []mounttypes.Mount{
|
|
| 157 | 157 |
{
|
| 158 |
- Type: mount.TypeBind, |
|
| 158 |
+ Type: mounttypes.TypeBind, |
|
| 159 | 159 |
Source: filepath.Join(info.DockerRootDir, "containers"), |
| 160 | 160 |
Target: "/foo", |
| 161 |
- BindOptions: &mount.BindOptions{Propagation: test.propagation},
|
|
| 161 |
+ BindOptions: &mounttypes.BindOptions{Propagation: test.propagation},
|
|
| 162 | 162 |
}, |
| 163 | 163 |
}, |
| 164 | 164 |
}, |
| ... | ... |
@@ -167,7 +172,7 @@ func TestMountDaemonRoot(t *testing.T) {
|
| 167 | 167 |
hc := hc |
| 168 | 168 |
t.Parallel() |
| 169 | 169 |
|
| 170 |
- c, err := client.ContainerCreate(ctx, &container.Config{
|
|
| 170 |
+ c, err := client.ContainerCreate(ctx, &containertypes.Config{
|
|
| 171 | 171 |
Image: "busybox", |
| 172 | 172 |
Cmd: []string{"true"},
|
| 173 | 173 |
}, hc, nil, "") |
| ... | ... |
@@ -206,3 +211,58 @@ func TestMountDaemonRoot(t *testing.T) {
|
| 206 | 206 |
}) |
| 207 | 207 |
} |
| 208 | 208 |
} |
| 209 |
+ |
|
| 210 |
+func TestContainerBindMountNonRecursive(t *testing.T) {
|
|
| 211 |
+ skip.If(t, testEnv.DaemonInfo.OSType != "linux" || testEnv.IsRemoteDaemon()) |
|
| 212 |
+ skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.40"), "BindOptions.NonRecursive requires API v1.40") |
|
| 213 |
+ |
|
| 214 |
+ defer setupTest(t)() |
|
| 215 |
+ |
|
| 216 |
+ tmpDir1 := fs.NewDir(t, "tmpdir1", fs.WithMode(0755), |
|
| 217 |
+ fs.WithDir("mnt", fs.WithMode(0755)))
|
|
| 218 |
+ defer tmpDir1.Remove() |
|
| 219 |
+ tmpDir1Mnt := filepath.Join(tmpDir1.Path(), "mnt") |
|
| 220 |
+ tmpDir2 := fs.NewDir(t, "tmpdir2", fs.WithMode(0755), |
|
| 221 |
+ fs.WithFile("file", "should not be visible when NonRecursive", fs.WithMode(0644)))
|
|
| 222 |
+ defer tmpDir2.Remove() |
|
| 223 |
+ |
|
| 224 |
+ err := mount.Mount(tmpDir2.Path(), tmpDir1Mnt, "none", "bind,ro") |
|
| 225 |
+ if err != nil {
|
|
| 226 |
+ t.Fatal(err) |
|
| 227 |
+ } |
|
| 228 |
+ defer func() {
|
|
| 229 |
+ if err := mount.Unmount(tmpDir1Mnt); err != nil {
|
|
| 230 |
+ t.Fatal(err) |
|
| 231 |
+ } |
|
| 232 |
+ }() |
|
| 233 |
+ |
|
| 234 |
+ // implicit is recursive (NonRecursive: false) |
|
| 235 |
+ implicit := mounttypes.Mount{
|
|
| 236 |
+ Type: "bind", |
|
| 237 |
+ Source: tmpDir1.Path(), |
|
| 238 |
+ Target: "/foo", |
|
| 239 |
+ ReadOnly: true, |
|
| 240 |
+ } |
|
| 241 |
+ recursive := implicit |
|
| 242 |
+ recursive.BindOptions = &mounttypes.BindOptions{
|
|
| 243 |
+ NonRecursive: false, |
|
| 244 |
+ } |
|
| 245 |
+ recursiveVerifier := []string{"test", "-f", "/foo/mnt/file"}
|
|
| 246 |
+ nonRecursive := implicit |
|
| 247 |
+ nonRecursive.BindOptions = &mounttypes.BindOptions{
|
|
| 248 |
+ NonRecursive: true, |
|
| 249 |
+ } |
|
| 250 |
+ nonRecursiveVerifier := []string{"test", "!", "-f", "/foo/mnt/file"}
|
|
| 251 |
+ |
|
| 252 |
+ ctx := context.Background() |
|
| 253 |
+ client := request.NewAPIClient(t) |
|
| 254 |
+ containers := []string{
|
|
| 255 |
+ container.Run(t, ctx, client, container.WithMount(implicit), container.WithCmd(recursiveVerifier...)), |
|
| 256 |
+ container.Run(t, ctx, client, container.WithMount(recursive), container.WithCmd(recursiveVerifier...)), |
|
| 257 |
+ container.Run(t, ctx, client, container.WithMount(nonRecursive), container.WithCmd(nonRecursiveVerifier...)), |
|
| 258 |
+ } |
|
| 259 |
+ |
|
| 260 |
+ for _, c := range containers {
|
|
| 261 |
+ poll.WaitOn(t, container.IsSuccessful(ctx, client, c), poll.WithDelay(100*time.Millisecond)) |
|
| 262 |
+ } |
|
| 263 |
+} |
| ... | ... |
@@ -4,6 +4,7 @@ import ( |
| 4 | 4 |
"fmt" |
| 5 | 5 |
|
| 6 | 6 |
containertypes "github.com/docker/docker/api/types/container" |
| 7 |
+ mounttypes "github.com/docker/docker/api/types/mount" |
|
| 7 | 8 |
networktypes "github.com/docker/docker/api/types/network" |
| 8 | 9 |
"github.com/docker/docker/api/types/strslice" |
| 9 | 10 |
"github.com/docker/go-connections/nat" |
| ... | ... |
@@ -68,6 +69,13 @@ func WithWorkingDir(dir string) func(*TestContainerConfig) {
|
| 68 | 68 |
} |
| 69 | 69 |
} |
| 70 | 70 |
|
| 71 |
+// WithMount adds an mount |
|
| 72 |
+func WithMount(m mounttypes.Mount) func(*TestContainerConfig) {
|
|
| 73 |
+ return func(c *TestContainerConfig) {
|
|
| 74 |
+ c.HostConfig.Mounts = append(c.HostConfig.Mounts, m) |
|
| 75 |
+ } |
|
| 76 |
+} |
|
| 77 |
+ |
|
| 71 | 78 |
// WithVolume sets the volume of the container |
| 72 | 79 |
func WithVolume(name string) func(*TestContainerConfig) {
|
| 73 | 80 |
return func(c *TestContainerConfig) {
|
| ... | ... |
@@ -5,6 +5,7 @@ import ( |
| 5 | 5 |
"strings" |
| 6 | 6 |
|
| 7 | 7 |
"github.com/docker/docker/client" |
| 8 |
+ "github.com/pkg/errors" |
|
| 8 | 9 |
"gotest.tools/poll" |
| 9 | 10 |
) |
| 10 | 11 |
|
| ... | ... |
@@ -39,3 +40,20 @@ func IsInState(ctx context.Context, client client.APIClient, containerID string, |
| 39 | 39 |
return poll.Continue("waiting for container to be one of (%s), currently %s", strings.Join(state, ", "), inspect.State.Status)
|
| 40 | 40 |
} |
| 41 | 41 |
} |
| 42 |
+ |
|
| 43 |
+// IsSuccessful verifies state.Status == "exited" && state.ExitCode == 0 |
|
| 44 |
+func IsSuccessful(ctx context.Context, client client.APIClient, containerID string) func(log poll.LogT) poll.Result {
|
|
| 45 |
+ return func(log poll.LogT) poll.Result {
|
|
| 46 |
+ inspect, err := client.ContainerInspect(ctx, containerID) |
|
| 47 |
+ if err != nil {
|
|
| 48 |
+ return poll.Error(err) |
|
| 49 |
+ } |
|
| 50 |
+ if inspect.State.Status == "exited" {
|
|
| 51 |
+ if inspect.State.ExitCode == 0 {
|
|
| 52 |
+ return poll.Success() |
|
| 53 |
+ } |
|
| 54 |
+ return poll.Error(errors.Errorf("expected exit code 0, got %d", inspect.State.ExitCode))
|
|
| 55 |
+ } |
|
| 56 |
+ return poll.Continue("waiting for container to be \"exited\", currently %s", inspect.State.Status)
|
|
| 57 |
+ } |
|
| 58 |
+} |
| ... | ... |
@@ -97,6 +97,9 @@ func (p *linuxParser) validateMountConfigImpl(mnt *mount.Mount, validateBindSour |
| 97 | 97 |
return &errMountConfig{mnt, fmt.Errorf("must not set ReadOnly mode when using anonymous volumes")}
|
| 98 | 98 |
} |
| 99 | 99 |
case mount.TypeTmpfs: |
| 100 |
+ if mnt.BindOptions != nil {
|
|
| 101 |
+ return &errMountConfig{mnt, errExtraField("BindOptions")}
|
|
| 102 |
+ } |
|
| 100 | 103 |
if len(mnt.Source) != 0 {
|
| 101 | 104 |
return &errMountConfig{mnt, errExtraField("Source")}
|
| 102 | 105 |
} |