container/opts: Add bind-create-src mount option

Add support for the `bind-create-src` option in bind mounts, which
instructs the daemon to create the source mountpoint on the host if it
doesn't exist.

This allows to replace the legacy `-v /src/dir:/dst` with the `--mount`.

Usage:
--mount type=bind,src=/host/path,dst=/container/path,bind-create-src
--mount type=bind,src=/host/path,dst=/container/path,bind-create-src=true

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
This commit is contained in:
Paweł Gronowski
2026-03-04 18:00:01 +01:00
parent ddb986472a
commit c747cff9ab
3 changed files with 81 additions and 1 deletions
+31
View File
@@ -5,6 +5,7 @@ import (
"io" "io"
"math/rand" "math/rand"
"os/exec" "os/exec"
"path/filepath"
"strings" "strings"
"syscall" "syscall"
"testing" "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-src", expectSuccess: true},
{name: "true", value: "bind-create-src=true", expectSuccess: true},
{name: "1", value: "bind-create-src=1", expectSuccess: true},
{name: "false", value: "bind-create-src=false", expectSuccess: false},
{name: "0", value: "bind-create-src=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) { func TestProcessTermination(t *testing.T) {
var out bytes.Buffer var out bytes.Buffer
cmd := icmd.Command("docker", "run", "--rm", "-i", fixtures.AlpineImage, cmd := icmd.Command("docker", "run", "--rm", "-i", fixtures.AlpineImage,
+6 -1
View File
@@ -57,7 +57,7 @@ func (m *MountOpt) Set(value string) error {
if !hasValue { if !hasValue {
switch key { switch key {
case "readonly", "ro", "volume-nocopy", "bind-nonrecursive": case "readonly", "ro", "volume-nocopy", "bind-nonrecursive", "bind-create-src":
// boolean values // boolean values
default: default:
return fmt.Errorf("invalid field '%s' must be a key=value pair", field) 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: default:
return fmt.Errorf(`invalid value for %s: %s (must be "enabled", "disabled", "writable", or "readonly")`, key, val) return fmt.Errorf(`invalid value for %s: %s (must be "enabled", "disabled", "writable", or "readonly")`, key, val)
} }
case "bind-create-src":
ensureBindOptions(&mount).CreateMountpoint, err = parseBoolValue(key, val, hasValue)
if err != nil {
return err
}
case "volume-subpath": case "volume-subpath":
ensureVolumeOptions(&mount).Subpath = val ensureVolumeOptions(&mount).Subpath = val
case "volume-nocopy": case "volume-nocopy":
+44
View File
@@ -475,6 +475,50 @@ func TestMountOptSetTmpfsNoError(t *testing.T) {
} }
} }
func TestMountOptSetBindCreateSrc(t *testing.T) {
tests := []struct {
value string
exp bool
expErr string
}{
{value: "", exp: false},
{value: "bind-create-src", exp: true},
{value: "bind-create-src=", expErr: `invalid value for 'bind-create-src': value is empty`},
{value: "bind-create-src= true", expErr: `invalid value for 'bind-create-src' in 'bind-create-src= true': value should not have whitespace`},
{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")`},
{value: "bind-create-src=1", exp: true},
{value: "bind-create-src=true", exp: true},
{value: "bind-create-src=0", exp: false},
{value: "bind-create-src=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) { func TestMountOptSetBindRecursive(t *testing.T) {
t.Run("enabled", func(t *testing.T) { t.Run("enabled", func(t *testing.T) {
var m MountOpt var m MountOpt