Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions man/docker-run.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,21 @@ according to RFC4862.
If set to `disabled`, submounts are not recursively bind-mounted.
If set to `writable`, submounts are recursively bind-mounted but not made recursively read-only.
If set to `readonly`, submounts are recursively bind-mounted and forcibly made recursively read-only.
* `bind-idmap`: make the mount an id-mapped mount, translating file
ownership within the mount without changing it on the backing
filesystem. `bind-idmap` (or `bind-idmap=match-user`) presents the
mount source's owner as the container's running user: host files
owned by the source's owner appear owned by the container user, and
files created by the container user through the mount are owned by
the source's owner on the host. `bind-idmap=match-user:USER` targets
an explicit container user instead (a name, UID, or UID:GID, as
accepted by `--user`); files with other owners within the mount
appear unmapped. `bind-idmap=userns` makes the mount follow the
mapping of the container's private user namespace (which requires the
container to run in one, e.g. userns-remap), covering the container's
whole ID range. Requires Engine API v1.56 or newer, a Linux rootful
daemon, kernel support for id-mapped mounts on the backing filesystem
(Linux 5.12 or newer for most filesystems), and runc 1.2 or newer.

Options specific to `volume`:

Expand Down
12 changes: 11 additions & 1 deletion opts/mount.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func (m *MountOpt) Set(value string) error {

if !hasValue {
switch key {
case "readonly", "ro", "volume-nocopy", "bind-nonrecursive", "bind-create-src":
case "readonly", "ro", "volume-nocopy", "bind-nonrecursive", "bind-create-src", "bind-idmap":
// boolean values
default:
return fmt.Errorf("invalid field '%s' must be a key=value pair", field)
Expand Down Expand Up @@ -107,6 +107,16 @@ func (m *MountOpt) Set(value string) error {
if err != nil {
return err
}
case "bind-idmap":
// Without a value, default to presenting the mount source's
// owner as the container's running user.
idMapping := ensureBindIDMapping(&mount)
idMapping.Source = mounttypes.IDMappingSourceMatchUser
if hasValue {
if err := parseIDMapValue(idMapping, val); err != nil {
return err
}
}
case "volume-subpath":
ensureVolumeOptions(&mount).Subpath = val
case "volume-nocopy":
Expand Down
61 changes: 61 additions & 0 deletions opts/mount_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -586,3 +586,64 @@ func TestMountOptSetBindRecursive(t *testing.T) {
}, m.Value()))
})
}

func TestMountOptSetBindIDMap(t *testing.T) {
tests := []struct {
value string
exp *mount.IDMapping
expErr string
}{
{
value: "bind-idmap",
exp: &mount.IDMapping{Source: mount.IDMappingSourceMatchUser},
},
{
value: "bind-idmap=match-user",
exp: &mount.IDMapping{Source: mount.IDMappingSourceMatchUser},
},
{
value: "bind-idmap=match-user:nginx",
exp: &mount.IDMapping{Source: mount.IDMappingSourceMatchUser, User: "nginx"},
},
{
value: "bind-idmap=match-user:1234:5678",
exp: &mount.IDMapping{Source: mount.IDMappingSourceMatchUser, User: "1234:5678"},
},
{
value: "bind-idmap=userns",
exp: &mount.IDMapping{Source: mount.IDMappingSourceUserns},
},
{
value: "bind-idmap=userns:nginx",
expErr: `invalid value for 'bind-idmap': "userns:nginx" ("userns" does not take a user)`,
},
{
value: "bind-idmap=match-user:",
expErr: `invalid value for 'bind-idmap': "match-user:" (empty user after "match-user:")`,
},
{
value: "bind-idmap=auto",
expErr: `invalid value for 'bind-idmap': "auto" (must be "match-user", "match-user:USER", or "userns")`,
},
}

for _, tc := range tests {
t.Run(tc.value, func(t *testing.T) {
var m MountOpt
err := m.Set("type=bind,source=/foo,target=/bar," + tc.value)
if tc.expErr != "" {
assert.Error(t, err, tc.expErr)
return
}
assert.NilError(t, err)
assert.Assert(t, m.values[0].BindOptions != nil)
assert.Check(t, is.DeepEqual(m.values[0].BindOptions.IDMapping, tc.exp))
})
}

t.Run("not a bind mount", func(t *testing.T) {
var m MountOpt
err := m.Set("type=volume,source=foo,target=/bar,bind-idmap")
assert.Error(t, err, "cannot mix 'bind-*' options with mount type 'volume'")
})
}
38 changes: 38 additions & 0 deletions opts/mount_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,44 @@ func ensureBindOptions(m *mount.Mount) *mount.BindOptions {
return m.BindOptions
}

func ensureBindIDMapping(m *mount.Mount) *mount.IDMapping {
bindOptions := ensureBindOptions(m)
if bindOptions.IDMapping == nil {
bindOptions.IDMapping = &mount.IDMapping{}
}
return bindOptions.IDMapping
}

// parseIDMapValue parses the value of the "bind-idmap" mount option into
// idMapping:
//
// - "match-user", optionally followed by ":USER" (a name, UID, or UID:GID,
// as accepted by --user): the mount source's owner appears inside the
// container as USER, or as the container's running user when omitted.
// - "userns": the mount follows the mapping of the container's private
// user namespace (e.g. userns-remap).
func parseIDMapValue(idMapping *mount.IDMapping, val string) error {
source, user, hasUser := strings.Cut(val, ":")
switch mount.IDMappingSource(source) {
case mount.IDMappingSourceMatchUser:
idMapping.Source = mount.IDMappingSourceMatchUser
if hasUser {
if user == "" {
return fmt.Errorf(`invalid value for 'bind-idmap': %q (empty user after "match-user:")`, val)
}
idMapping.User = user
}
case mount.IDMappingSourceUserns:
if hasUser {
return fmt.Errorf(`invalid value for 'bind-idmap': %q ("userns" does not take a user)`, val)
}
idMapping.Source = mount.IDMappingSourceUserns
default:
return fmt.Errorf(`invalid value for 'bind-idmap': %q (must be "match-user", "match-user:USER", or "userns")`, val)
}
return nil
}

func ensureTmpfsOptions(m *mount.Mount) *mount.TmpfsOptions {
if m.TmpfsOptions == nil {
m.TmpfsOptions = &mount.TmpfsOptions{}
Expand Down
4 changes: 4 additions & 0 deletions vendor.mod
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,7 @@ require (
google.golang.org/grpc v1.82.1 // indirect
google.golang.org/protobuf v1.36.11 // indirect
)

replace github.com/moby/moby/api => github.com/ndeloof/docker/api v0.0.0-20260814082315-ecec044ee64e

replace github.com/moby/moby/client => github.com/ndeloof/docker/client v0.0.0-20260814082315-ecec044ee64e
8 changes: 4 additions & 4 deletions vendor.sum
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,6 @@ github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3N
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.3.3 h1:OxxR9paxsluYi+zDUEXTTaIxtkK3viymW+Ka7vRhhME=
github.com/moby/go-archive v0.3.3/go.mod h1:Npdv43fFqlhZW7Xo8fbm3ZMYFvAGNviUPqX21VERbcE=
github.com/moby/moby/api v1.55.0 h1:2/sexvQyqIWS8pRSCFddBfpW2qE7vR7FCL+vN8pxwMc=
github.com/moby/moby/api v1.55.0/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
github.com/moby/moby/client v0.5.1 h1:tYNaJno4c0HXz12y5BiqEDy0rVTYkWzI26lGvnTMiJw=
github.com/moby/moby/client v0.5.1/go.mod h1:odLstlZ6uSnfvAgVxMpvgmb8SUdd+siH2T0GBuxVAlM=
github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=
github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/swarmkit/v2 v2.1.2 h1:1WDZAI6HVYNKdCG4zlXnTAPyLsLwuhRGWlHoOUf5Z6I=
Expand Down Expand Up @@ -146,6 +142,10 @@ github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDk
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/ndeloof/docker/api v0.0.0-20260814082315-ecec044ee64e h1:Mdy2uGEATfETjYk1qlrumzqYZDJ1hR51FGloJOMT9+0=
github.com/ndeloof/docker/api v0.0.0-20260814082315-ecec044ee64e/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
github.com/ndeloof/docker/client v0.0.0-20260814082315-ecec044ee64e h1:G8SMRFexOCX0Eu7WErs8NCB98EairvH9M6KbmkNlpqY=
github.com/ndeloof/docker/client v0.0.0-20260814082315-ecec044ee64e/go.mod h1:odLstlZ6uSnfvAgVxMpvgmb8SUdd+siH2T0GBuxVAlM=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
Expand Down
41 changes: 41 additions & 0 deletions vendor/github.com/moby/moby/api/types/mount/mount.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion vendor/github.com/moby/moby/client/client.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions vendor/modules.txt
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ github.com/moby/go-archive
github.com/moby/go-archive/compression
github.com/moby/go-archive/internal/archiveoptions
github.com/moby/go-archive/tarheader
# github.com/moby/moby/api v1.55.0
# github.com/moby/moby/api v1.55.0 => github.com/ndeloof/docker/api v0.0.0-20260814082315-ecec044ee64e
## explicit; go 1.24
github.com/moby/moby/api/pkg/authconfig
github.com/moby/moby/api/pkg/stdcopy
Expand All @@ -190,7 +190,7 @@ github.com/moby/moby/api/types/storage
github.com/moby/moby/api/types/swarm
github.com/moby/moby/api/types/system
github.com/moby/moby/api/types/volume
# github.com/moby/moby/client v0.5.1
# github.com/moby/moby/client v0.5.1 => github.com/ndeloof/docker/client v0.0.0-20260814082315-ecec044ee64e
## explicit; go 1.24
github.com/moby/moby/client
github.com/moby/moby/client/internal
Expand Down Expand Up @@ -567,3 +567,5 @@ gotest.tools/v3/skip
# tags.cncf.io/container-device-interface v1.1.0
## explicit; go 1.21
tags.cncf.io/container-device-interface/pkg/parser
# github.com/moby/moby/api => github.com/ndeloof/docker/api v0.0.0-20260814082315-ecec044ee64e
# github.com/moby/moby/client => github.com/ndeloof/docker/client v0.0.0-20260814082315-ecec044ee64e
Loading