Browse code

mount: add BindOptions.NonRecursive (API v1.40)

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>

Akihiro Suda authored on 2018/10/10 19:20:13
Showing 12 changed files
... ...
@@ -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
 		}