diff --git a/docs/reference/commandline/service_create.md b/docs/reference/commandline/service_create.md index 691e2db2e05f..54c82437c378 100644 --- a/docs/reference/commandline/service_create.md +++ b/docs/reference/commandline/service_create.md @@ -457,6 +457,22 @@ The following options can only be used for bind mounts (`type=bind`): When the option is not specified, the default behavior corresponds to setting enabled. + + bind-create-host-path + + By default, bind mounts require the source path to exist on the host. This is a significant difference + from the -v flag, which creates the source path if it doesn't exist.
+
+ Set bind-create-host-path to create the source path on the host if it doesn't exist.
+
+ A value is optional:
+
+ + + ##### Bind propagation diff --git a/e2e/container/run_test.go b/e2e/container/run_test.go index d52230e23899..b6cfb8626216 100644 --- a/e2e/container/run_test.go +++ b/e2e/container/run_test.go @@ -5,6 +5,7 @@ import ( "io" "math/rand" "os/exec" + "path/filepath" "strings" "syscall" "testing" @@ -160,6 +161,36 @@ func TestMountSubvolume(t *testing.T) { } } +func TestMountBindCreateMountpoint(t *testing.T) { + environment.SkipIfDaemonNotLinux(t) + + for _, tc := range []struct { + name string + value string + expectSuccess bool + }{ + {name: "flag only", value: "bind-create-host-path", expectSuccess: true}, + {name: "true", value: "bind-create-host-path=true", expectSuccess: true}, + {name: "1", value: "bind-create-host-path=1", expectSuccess: true}, + {name: "false", value: "bind-create-host-path=false", expectSuccess: false}, + {name: "0", value: "bind-create-host-path=0", expectSuccess: false}, + } { + t.Run(tc.name, func(t *testing.T) { + srcPath := filepath.Join("/tmp", t.Name(), "does", "not", "exist") + result := icmd.RunCommand("docker", "run", "--rm", + "--mount", "type=bind,src="+srcPath+",dst=/mnt,"+tc.value, + fixtures.AlpineImage, "cat", "/proc/mounts") + if tc.expectSuccess { + result.Assert(t, icmd.Success) + assert.Check(t, is.Contains(result.Stdout(), "/mnt")) + } else { + result.Assert(t, icmd.Expected{ExitCode: 125}) + assert.Check(t, is.Contains(result.Stderr(), srcPath)) + } + }) + } +} + func TestProcessTermination(t *testing.T) { var out bytes.Buffer cmd := icmd.Command("docker", "run", "--rm", "-i", fixtures.AlpineImage, diff --git a/opts/mount.go b/opts/mount.go index 642f6500ab4c..077829be0382 100644 --- a/opts/mount.go +++ b/opts/mount.go @@ -57,7 +57,7 @@ func (m *MountOpt) Set(value string) error { if !hasValue { switch key { - case "readonly", "ro", "volume-nocopy", "bind-nonrecursive": + case "readonly", "ro", "volume-nocopy", "bind-nonrecursive", "bind-create-host-path": // boolean values default: return fmt.Errorf("invalid field '%s' must be a key=value pair", field) @@ -102,6 +102,11 @@ func (m *MountOpt) Set(value string) error { default: return fmt.Errorf(`invalid value for %s: %s (must be "enabled", "disabled", "writable", or "readonly")`, key, val) } + case "bind-create-host-path": + ensureBindOptions(&mount).CreateMountpoint, err = parseBoolValue(key, val, hasValue) + if err != nil { + return err + } case "volume-subpath": ensureVolumeOptions(&mount).Subpath = val case "volume-nocopy": diff --git a/opts/mount_test.go b/opts/mount_test.go index 9520948b9af1..92870cfbb2e4 100644 --- a/opts/mount_test.go +++ b/opts/mount_test.go @@ -475,6 +475,50 @@ func TestMountOptSetTmpfsNoError(t *testing.T) { } } +func TestMountOptSetBindCreateHostPath(t *testing.T) { + tests := []struct { + value string + exp bool + expErr string + }{ + {value: "", exp: false}, + {value: "bind-create-host-path", exp: true}, + {value: "bind-create-host-path=", expErr: `invalid value for 'bind-create-host-path': value is empty`}, + {value: "bind-create-host-path= true", expErr: `invalid value for 'bind-create-host-path' in 'bind-create-host-path= true': value should not have whitespace`}, + {value: "bind-create-host-path=no", expErr: `invalid value for 'bind-create-host-path': invalid boolean value ("no"): must be one of "true", "1", "false", or "0" (default "true")`}, + {value: "bind-create-host-path=1", exp: true}, + {value: "bind-create-host-path=true", exp: true}, + {value: "bind-create-host-path=0", exp: false}, + {value: "bind-create-host-path=false", exp: false}, + } + + for _, tc := range tests { + name := tc.value + if name == "" { + name = "not set" + } + t.Run(name, func(t *testing.T) { + val := "type=bind,target=/foo,source=/foo" + if tc.value != "" { + val += "," + tc.value + } + var m MountOpt + err := m.Set(val) + if tc.expErr != "" { + assert.Error(t, err, tc.expErr) + return + } + assert.NilError(t, err) + if tc.value == "" { + assert.Check(t, is.Nil(m.values[0].BindOptions)) + } else { + assert.Check(t, m.values[0].BindOptions != nil) + assert.Check(t, is.Equal(m.values[0].BindOptions.CreateMountpoint, tc.exp)) + } + }) + } +} + func TestMountOptSetBindRecursive(t *testing.T) { t.Run("enabled", func(t *testing.T) { var m MountOpt