Skip to content

Commit 964a6d2

Browse files
authored
Merge pull request #6792 from vvoland/bind-create
container/opts: Add bind-create-src mount option
2 parents 210147d + 32aa575 commit 964a6d2

File tree

5 files changed

+107
-2
lines changed

5 files changed

+107
-2
lines changed

docs/reference/commandline/service_create.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,22 @@ The following options can only be used for bind mounts (`type=bind`):
457457
When the option is not specified, the default behavior corresponds to setting <tt>enabled</tt>.
458458
</td>
459459
</tr>
460+
<tr>
461+
<td><b>bind-create-src</b></td>
462+
<td>
463+
By default, bind mounts require the source path to exist on the daemon host. This is a significant difference
464+
from the <tt>-v</tt> flag, which creates the source path if it doesn't exist.<br />
465+
<br />
466+
Set <tt>bind-create-src</tt> to create the source path on the daemon host if it doesn't exist.<br />
467+
<br />
468+
A value is optional:<br />
469+
<br />
470+
<ul>
471+
<li><tt>true</tt> or <tt>1</tt>: Create path on the daemon host if it doesn't exist.</li>
472+
<li><tt>false</tt> or <tt>0</tt>: Default behavior. Produces an error if the source path doesn't exist on the daemon host.</li>
473+
</ul>
474+
</td>
475+
</tr>
460476
</table>
461477

462478
##### Bind propagation
@@ -591,7 +607,8 @@ or `--volume` flag for `docker run`, with some important exceptions:
591607

592608
- When you use `--mount` with `type=bind`, the host-path must refer to an *existing*
593609
path on the host. The path will not be created for you and the service will fail
594-
with an error if the path does not exist.
610+
with an error if the path does not exist. You can use `bind-create-src` to
611+
create the host path if it doesn't exist.
595612

596613
- The `--mount` flag does not allow you to relabel a volume with `Z` or `z` flags,
597614
which are used for `selinux` labeling.

docs/reference/run.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,14 @@ two paths. The `source` path is the location on the host that you want to
252252
bind mount into the container. The `target` path is the mount destination
253253
inside the container.
254254

255+
By default, bind mounts require the source path to exist on the daemon host. If the
256+
source path doesn't exist, an error is returned. To create the source path on
257+
the daemon host if it doesn't exist, use the `bind-create-src` option:
258+
259+
```console
260+
$ docker run -it --mount type=bind,source=[PATH],target=[PATH],bind-create-src busybox
261+
```
262+
255263
Bind mounts are read-write by default, meaning that you can both read and write
256264
files to and from the mounted location from the container. Changes that you
257265
make, such as adding or editing files, are reflected on the host filesystem:

e2e/container/run_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"io"
66
"math/rand"
77
"os/exec"
8+
"path/filepath"
89
"strings"
910
"syscall"
1011
"testing"
@@ -160,6 +161,36 @@ func TestMountSubvolume(t *testing.T) {
160161
}
161162
}
162163

164+
func TestMountBindCreateMountpoint(t *testing.T) {
165+
environment.SkipIfDaemonNotLinux(t)
166+
167+
for _, tc := range []struct {
168+
name string
169+
value string
170+
expectSuccess bool
171+
}{
172+
{name: "flag only", value: "bind-create-src", expectSuccess: true},
173+
{name: "true", value: "bind-create-src=true", expectSuccess: true},
174+
{name: "1", value: "bind-create-src=1", expectSuccess: true},
175+
{name: "false", value: "bind-create-src=false", expectSuccess: false},
176+
{name: "0", value: "bind-create-src=0", expectSuccess: false},
177+
} {
178+
t.Run(tc.name, func(t *testing.T) {
179+
srcPath := filepath.Join("/tmp", t.Name(), "does", "not", "exist")
180+
result := icmd.RunCommand("docker", "run", "--rm",
181+
"--mount", "type=bind,src="+srcPath+",dst=/mnt,"+tc.value,
182+
fixtures.AlpineImage, "cat", "/proc/mounts")
183+
if tc.expectSuccess {
184+
result.Assert(t, icmd.Success)
185+
assert.Check(t, is.Contains(result.Stdout(), "/mnt"))
186+
} else {
187+
result.Assert(t, icmd.Expected{ExitCode: 125})
188+
assert.Check(t, is.Contains(result.Stderr(), srcPath))
189+
}
190+
})
191+
}
192+
}
193+
163194
func TestProcessTermination(t *testing.T) {
164195
var out bytes.Buffer
165196
cmd := icmd.Command("docker", "run", "--rm", "-i", fixtures.AlpineImage,

opts/mount.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ func (m *MountOpt) Set(value string) error {
5757

5858
if !hasValue {
5959
switch key {
60-
case "readonly", "ro", "volume-nocopy", "bind-nonrecursive":
60+
case "readonly", "ro", "volume-nocopy", "bind-nonrecursive", "bind-create-src":
6161
// boolean values
6262
default:
6363
return fmt.Errorf("invalid field '%s' must be a key=value pair", field)
@@ -102,6 +102,11 @@ func (m *MountOpt) Set(value string) error {
102102
default:
103103
return fmt.Errorf(`invalid value for %s: %s (must be "enabled", "disabled", "writable", or "readonly")`, key, val)
104104
}
105+
case "bind-create-src":
106+
ensureBindOptions(&mount).CreateMountpoint, err = parseBoolValue(key, val, hasValue)
107+
if err != nil {
108+
return err
109+
}
105110
case "volume-subpath":
106111
ensureVolumeOptions(&mount).Subpath = val
107112
case "volume-nocopy":

opts/mount_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,50 @@ func TestMountOptSetTmpfsNoError(t *testing.T) {
475475
}
476476
}
477477

478+
func TestMountOptSetBindCreateSrc(t *testing.T) {
479+
tests := []struct {
480+
value string
481+
exp bool
482+
expErr string
483+
}{
484+
{value: "", exp: false},
485+
{value: "bind-create-src", exp: true},
486+
{value: "bind-create-src=", expErr: `invalid value for 'bind-create-src': value is empty`},
487+
{value: "bind-create-src= true", expErr: `invalid value for 'bind-create-src' in 'bind-create-src= true': value should not have whitespace`},
488+
{value: "bind-create-src=no", expErr: `invalid value for 'bind-create-src': invalid boolean value ("no"): must be one of "true", "1", "false", or "0" (default "true")`},
489+
{value: "bind-create-src=1", exp: true},
490+
{value: "bind-create-src=true", exp: true},
491+
{value: "bind-create-src=0", exp: false},
492+
{value: "bind-create-src=false", exp: false},
493+
}
494+
495+
for _, tc := range tests {
496+
name := tc.value
497+
if name == "" {
498+
name = "not set"
499+
}
500+
t.Run(name, func(t *testing.T) {
501+
val := "type=bind,target=/foo,source=/foo"
502+
if tc.value != "" {
503+
val += "," + tc.value
504+
}
505+
var m MountOpt
506+
err := m.Set(val)
507+
if tc.expErr != "" {
508+
assert.Error(t, err, tc.expErr)
509+
return
510+
}
511+
assert.NilError(t, err)
512+
if tc.value == "" {
513+
assert.Check(t, is.Nil(m.values[0].BindOptions))
514+
} else {
515+
assert.Check(t, m.values[0].BindOptions != nil)
516+
assert.Check(t, is.Equal(m.values[0].BindOptions.CreateMountpoint, tc.exp))
517+
}
518+
})
519+
}
520+
}
521+
478522
func TestMountOptSetBindRecursive(t *testing.T) {
479523
t.Run("enabled", func(t *testing.T) {
480524
var m MountOpt

0 commit comments

Comments
 (0)