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>
This commit is contained in:
parent
12bba16306
commit
596cdffb9f
12 changed files with 160 additions and 35 deletions
|
@ -465,6 +465,16 @@ func (s *containerRouter) postContainersCreate(ctx context.Context, w http.Respo
|
||||||
hostConfig.AutoRemove = false
|
hostConfig.AutoRemove = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When using API 1.39 and under, BindOptions.NonRecursive should be ignored because it
|
||||||
|
// was added in API 1.40.
|
||||||
|
if hostConfig != nil && versions.LessThan(version, "1.40") {
|
||||||
|
for _, m := range hostConfig.Mounts {
|
||||||
|
if bo := m.BindOptions; bo != nil {
|
||||||
|
bo.NonRecursive = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ccr, err := s.backend.ContainerCreate(types.ContainerCreateConfig{
|
ccr, err := s.backend.ContainerCreate(types.ContainerCreateConfig{
|
||||||
Name: name,
|
Name: name,
|
||||||
Config: config,
|
Config: config,
|
||||||
|
|
|
@ -265,6 +265,10 @@ definitions:
|
||||||
- "rshared"
|
- "rshared"
|
||||||
- "slave"
|
- "slave"
|
||||||
- "rslave"
|
- "rslave"
|
||||||
|
NonRecursive:
|
||||||
|
description: "Disable recursive bind mount."
|
||||||
|
type: "boolean"
|
||||||
|
default: false
|
||||||
VolumeOptions:
|
VolumeOptions:
|
||||||
description: "Optional configuration for the `volume` type."
|
description: "Optional configuration for the `volume` type."
|
||||||
type: "object"
|
type: "object"
|
||||||
|
|
|
@ -79,7 +79,8 @@ const (
|
||||||
|
|
||||||
// BindOptions defines options specific to mounts of type "bind".
|
// BindOptions defines options specific to mounts of type "bind".
|
||||||
type BindOptions struct {
|
type BindOptions struct {
|
||||||
Propagation Propagation `json:",omitempty"`
|
Propagation Propagation `json:",omitempty"`
|
||||||
|
NonRecursive bool `json:",omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// VolumeOptions represents the options for a mount of type volume.
|
// VolumeOptions represents the options for a mount of type volume.
|
||||||
|
|
|
@ -4,9 +4,10 @@ package container // import "github.com/docker/docker/container"
|
||||||
|
|
||||||
// Mount contains information for a mount operation.
|
// Mount contains information for a mount operation.
|
||||||
type Mount struct {
|
type Mount struct {
|
||||||
Source string `json:"source"`
|
Source string `json:"source"`
|
||||||
Destination string `json:"destination"`
|
Destination string `json:"destination"`
|
||||||
Writable bool `json:"writable"`
|
Writable bool `json:"writable"`
|
||||||
Data string `json:"data"`
|
Data string `json:"data"`
|
||||||
Propagation string `json:"mountpropagation"`
|
Propagation string `json:"mountpropagation"`
|
||||||
|
NonRecursive bool `json:"nonrecursive"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -321,6 +321,12 @@ func containerToGRPC(c *types.ContainerSpec) (*swarmapi.ContainerSpec, error) {
|
||||||
} else if string(m.BindOptions.Propagation) != "" {
|
} else if string(m.BindOptions.Propagation) != "" {
|
||||||
return nil, fmt.Errorf("invalid MountPropagation: %q", m.BindOptions.Propagation)
|
return nil, fmt.Errorf("invalid MountPropagation: %q", m.BindOptions.Propagation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if m.BindOptions.NonRecursive {
|
||||||
|
// TODO(AkihiroSuda): NonRecursive is unsupported for Swarm-mode now because of mutual vendoring
|
||||||
|
// across moby and swarmkit. Will be available soon after the moby PR gets merged.
|
||||||
|
return nil, fmt.Errorf("invalid NonRecursive: %q", m.BindOptions.Propagation)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.VolumeOptions != nil {
|
if m.VolumeOptions != nil {
|
||||||
|
|
|
@ -565,7 +565,11 @@ func setMounts(daemon *Daemon, s *specs.Spec, c *container.Container, mounts []c
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := []string{"rbind"}
|
bindMode := "rbind"
|
||||||
|
if m.NonRecursive {
|
||||||
|
bindMode = "bind"
|
||||||
|
}
|
||||||
|
opts := []string{bindMode}
|
||||||
if !m.Writable {
|
if !m.Writable {
|
||||||
opts = append(opts, "ro")
|
opts = append(opts, "ro")
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
mounttypes "github.com/docker/docker/api/types/mount"
|
||||||
"github.com/docker/docker/container"
|
"github.com/docker/docker/container"
|
||||||
"github.com/docker/docker/pkg/fileutils"
|
"github.com/docker/docker/pkg/fileutils"
|
||||||
"github.com/docker/docker/pkg/mount"
|
"github.com/docker/docker/pkg/mount"
|
||||||
|
@ -58,6 +59,9 @@ func (daemon *Daemon) setupMounts(c *container.Container) ([]container.Mount, er
|
||||||
Writable: m.RW,
|
Writable: m.RW,
|
||||||
Propagation: string(m.Propagation),
|
Propagation: string(m.Propagation),
|
||||||
}
|
}
|
||||||
|
if m.Spec.Type == mounttypes.TypeBind && m.Spec.BindOptions != nil {
|
||||||
|
mnt.NonRecursive = m.Spec.BindOptions.NonRecursive
|
||||||
|
}
|
||||||
if m.Volume != nil {
|
if m.Volume != nil {
|
||||||
attributes := map[string]string{
|
attributes := map[string]string{
|
||||||
"driver": m.Volume.DriverName(),
|
"driver": m.Volume.DriverName(),
|
||||||
|
@ -129,11 +133,15 @@ func (daemon *Daemon) mountVolumes(container *container.Container) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := "rbind,ro"
|
bindMode := "rbind"
|
||||||
if m.Writable {
|
if m.NonRecursive {
|
||||||
opts = "rbind,rw"
|
bindMode = "bind"
|
||||||
}
|
}
|
||||||
|
writeMode := "ro"
|
||||||
|
if m.Writable {
|
||||||
|
writeMode = "rw"
|
||||||
|
}
|
||||||
|
opts := strings.Join([]string{bindMode, writeMode}, ",")
|
||||||
if err := mount.Mount(m.Source, dest, bindMountType, opts); err != nil {
|
if err := mount.Mount(m.Source, dest, bindMountType, opts); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,8 @@ keywords: "API, Docker, rcli, REST, documentation"
|
||||||
on the node.label. The format of the label filter is `node.label=<key>`/`node.label=<key>=<value>`
|
on the node.label. The format of the label filter is `node.label=<key>`/`node.label=<key>=<value>`
|
||||||
to return those with the specified labels, or `node.label!=<key>`/`node.label!=<key>=<value>`
|
to return those with the specified labels, or `node.label!=<key>`/`node.label!=<key>=<value>`
|
||||||
to return those without the specified labels.
|
to return those without the specified labels.
|
||||||
|
* `POST /containers/create`, `GET /containers/{id}/json`, and `GET /containers/json` now supports
|
||||||
|
`BindOptions.NonRecursive`.
|
||||||
|
|
||||||
## V1.39 API changes
|
## V1.39 API changes
|
||||||
|
|
||||||
|
|
|
@ -5,17 +5,22 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/container"
|
containertypes "github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/api/types/mount"
|
mounttypes "github.com/docker/docker/api/types/mount"
|
||||||
"github.com/docker/docker/api/types/network"
|
"github.com/docker/docker/api/types/network"
|
||||||
|
"github.com/docker/docker/api/types/versions"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
|
"github.com/docker/docker/integration/internal/container"
|
||||||
"github.com/docker/docker/internal/test/request"
|
"github.com/docker/docker/internal/test/request"
|
||||||
|
"github.com/docker/docker/pkg/mount"
|
||||||
"github.com/docker/docker/pkg/system"
|
"github.com/docker/docker/pkg/system"
|
||||||
"gotest.tools/assert"
|
"gotest.tools/assert"
|
||||||
is "gotest.tools/assert/cmp"
|
is "gotest.tools/assert/cmp"
|
||||||
"gotest.tools/fs"
|
"gotest.tools/fs"
|
||||||
|
"gotest.tools/poll"
|
||||||
"gotest.tools/skip"
|
"gotest.tools/skip"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -32,11 +37,11 @@ func TestContainerNetworkMountsNoChown(t *testing.T) {
|
||||||
|
|
||||||
tmpNWFileMount := tmpDir.Join("nwfile")
|
tmpNWFileMount := tmpDir.Join("nwfile")
|
||||||
|
|
||||||
config := container.Config{
|
config := containertypes.Config{
|
||||||
Image: "busybox",
|
Image: "busybox",
|
||||||
}
|
}
|
||||||
hostConfig := container.HostConfig{
|
hostConfig := containertypes.HostConfig{
|
||||||
Mounts: []mount.Mount{
|
Mounts: []mounttypes.Mount{
|
||||||
{
|
{
|
||||||
Type: "bind",
|
Type: "bind",
|
||||||
Source: tmpNWFileMount,
|
Source: tmpNWFileMount,
|
||||||
|
@ -93,39 +98,39 @@ func TestMountDaemonRoot(t *testing.T) {
|
||||||
|
|
||||||
for _, test := range []struct {
|
for _, test := range []struct {
|
||||||
desc string
|
desc string
|
||||||
propagation mount.Propagation
|
propagation mounttypes.Propagation
|
||||||
expected mount.Propagation
|
expected mounttypes.Propagation
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
desc: "default",
|
desc: "default",
|
||||||
propagation: "",
|
propagation: "",
|
||||||
expected: mount.PropagationRSlave,
|
expected: mounttypes.PropagationRSlave,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "private",
|
desc: "private",
|
||||||
propagation: mount.PropagationPrivate,
|
propagation: mounttypes.PropagationPrivate,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "rprivate",
|
desc: "rprivate",
|
||||||
propagation: mount.PropagationRPrivate,
|
propagation: mounttypes.PropagationRPrivate,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "slave",
|
desc: "slave",
|
||||||
propagation: mount.PropagationSlave,
|
propagation: mounttypes.PropagationSlave,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "rslave",
|
desc: "rslave",
|
||||||
propagation: mount.PropagationRSlave,
|
propagation: mounttypes.PropagationRSlave,
|
||||||
expected: mount.PropagationRSlave,
|
expected: mounttypes.PropagationRSlave,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "shared",
|
desc: "shared",
|
||||||
propagation: mount.PropagationShared,
|
propagation: mounttypes.PropagationShared,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "rshared",
|
desc: "rshared",
|
||||||
propagation: mount.PropagationRShared,
|
propagation: mounttypes.PropagationRShared,
|
||||||
expected: mount.PropagationRShared,
|
expected: mounttypes.PropagationRShared,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(test.desc, func(t *testing.T) {
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
@ -139,26 +144,26 @@ func TestMountDaemonRoot(t *testing.T) {
|
||||||
bindSpecRoot := info.DockerRootDir + ":" + "/foo" + propagationSpec
|
bindSpecRoot := info.DockerRootDir + ":" + "/foo" + propagationSpec
|
||||||
bindSpecSub := filepath.Join(info.DockerRootDir, "containers") + ":/foo" + propagationSpec
|
bindSpecSub := filepath.Join(info.DockerRootDir, "containers") + ":/foo" + propagationSpec
|
||||||
|
|
||||||
for name, hc := range map[string]*container.HostConfig{
|
for name, hc := range map[string]*containertypes.HostConfig{
|
||||||
"bind root": {Binds: []string{bindSpecRoot}},
|
"bind root": {Binds: []string{bindSpecRoot}},
|
||||||
"bind subpath": {Binds: []string{bindSpecSub}},
|
"bind subpath": {Binds: []string{bindSpecSub}},
|
||||||
"mount root": {
|
"mount root": {
|
||||||
Mounts: []mount.Mount{
|
Mounts: []mounttypes.Mount{
|
||||||
{
|
{
|
||||||
Type: mount.TypeBind,
|
Type: mounttypes.TypeBind,
|
||||||
Source: info.DockerRootDir,
|
Source: info.DockerRootDir,
|
||||||
Target: "/foo",
|
Target: "/foo",
|
||||||
BindOptions: &mount.BindOptions{Propagation: test.propagation},
|
BindOptions: &mounttypes.BindOptions{Propagation: test.propagation},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"mount subpath": {
|
"mount subpath": {
|
||||||
Mounts: []mount.Mount{
|
Mounts: []mounttypes.Mount{
|
||||||
{
|
{
|
||||||
Type: mount.TypeBind,
|
Type: mounttypes.TypeBind,
|
||||||
Source: filepath.Join(info.DockerRootDir, "containers"),
|
Source: filepath.Join(info.DockerRootDir, "containers"),
|
||||||
Target: "/foo",
|
Target: "/foo",
|
||||||
BindOptions: &mount.BindOptions{Propagation: test.propagation},
|
BindOptions: &mounttypes.BindOptions{Propagation: test.propagation},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -167,7 +172,7 @@ func TestMountDaemonRoot(t *testing.T) {
|
||||||
hc := hc
|
hc := hc
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
c, err := client.ContainerCreate(ctx, &container.Config{
|
c, err := client.ContainerCreate(ctx, &containertypes.Config{
|
||||||
Image: "busybox",
|
Image: "busybox",
|
||||||
Cmd: []string{"true"},
|
Cmd: []string{"true"},
|
||||||
}, hc, nil, "")
|
}, hc, nil, "")
|
||||||
|
@ -206,3 +211,58 @@ func TestMountDaemonRoot(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContainerBindMountNonRecursive(t *testing.T) {
|
||||||
|
skip.If(t, testEnv.DaemonInfo.OSType != "linux" || testEnv.IsRemoteDaemon())
|
||||||
|
skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.40"), "BindOptions.NonRecursive requires API v1.40")
|
||||||
|
|
||||||
|
defer setupTest(t)()
|
||||||
|
|
||||||
|
tmpDir1 := fs.NewDir(t, "tmpdir1", fs.WithMode(0755),
|
||||||
|
fs.WithDir("mnt", fs.WithMode(0755)))
|
||||||
|
defer tmpDir1.Remove()
|
||||||
|
tmpDir1Mnt := filepath.Join(tmpDir1.Path(), "mnt")
|
||||||
|
tmpDir2 := fs.NewDir(t, "tmpdir2", fs.WithMode(0755),
|
||||||
|
fs.WithFile("file", "should not be visible when NonRecursive", fs.WithMode(0644)))
|
||||||
|
defer tmpDir2.Remove()
|
||||||
|
|
||||||
|
err := mount.Mount(tmpDir2.Path(), tmpDir1Mnt, "none", "bind,ro")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := mount.Unmount(tmpDir1Mnt); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// implicit is recursive (NonRecursive: false)
|
||||||
|
implicit := mounttypes.Mount{
|
||||||
|
Type: "bind",
|
||||||
|
Source: tmpDir1.Path(),
|
||||||
|
Target: "/foo",
|
||||||
|
ReadOnly: true,
|
||||||
|
}
|
||||||
|
recursive := implicit
|
||||||
|
recursive.BindOptions = &mounttypes.BindOptions{
|
||||||
|
NonRecursive: false,
|
||||||
|
}
|
||||||
|
recursiveVerifier := []string{"test", "-f", "/foo/mnt/file"}
|
||||||
|
nonRecursive := implicit
|
||||||
|
nonRecursive.BindOptions = &mounttypes.BindOptions{
|
||||||
|
NonRecursive: true,
|
||||||
|
}
|
||||||
|
nonRecursiveVerifier := []string{"test", "!", "-f", "/foo/mnt/file"}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
client := request.NewAPIClient(t)
|
||||||
|
containers := []string{
|
||||||
|
container.Run(t, ctx, client, container.WithMount(implicit), container.WithCmd(recursiveVerifier...)),
|
||||||
|
container.Run(t, ctx, client, container.WithMount(recursive), container.WithCmd(recursiveVerifier...)),
|
||||||
|
container.Run(t, ctx, client, container.WithMount(nonRecursive), container.WithCmd(nonRecursiveVerifier...)),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range containers {
|
||||||
|
poll.WaitOn(t, container.IsSuccessful(ctx, client, c), poll.WithDelay(100*time.Millisecond))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
containertypes "github.com/docker/docker/api/types/container"
|
containertypes "github.com/docker/docker/api/types/container"
|
||||||
|
mounttypes "github.com/docker/docker/api/types/mount"
|
||||||
networktypes "github.com/docker/docker/api/types/network"
|
networktypes "github.com/docker/docker/api/types/network"
|
||||||
"github.com/docker/docker/api/types/strslice"
|
"github.com/docker/docker/api/types/strslice"
|
||||||
"github.com/docker/go-connections/nat"
|
"github.com/docker/go-connections/nat"
|
||||||
|
@ -68,6 +69,13 @@ func WithWorkingDir(dir string) func(*TestContainerConfig) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithMount adds an mount
|
||||||
|
func WithMount(m mounttypes.Mount) func(*TestContainerConfig) {
|
||||||
|
return func(c *TestContainerConfig) {
|
||||||
|
c.HostConfig.Mounts = append(c.HostConfig.Mounts, m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// WithVolume sets the volume of the container
|
// WithVolume sets the volume of the container
|
||||||
func WithVolume(name string) func(*TestContainerConfig) {
|
func WithVolume(name string) func(*TestContainerConfig) {
|
||||||
return func(c *TestContainerConfig) {
|
return func(c *TestContainerConfig) {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
|
"github.com/pkg/errors"
|
||||||
"gotest.tools/poll"
|
"gotest.tools/poll"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -39,3 +40,20 @@ func IsInState(ctx context.Context, client client.APIClient, containerID string,
|
||||||
return poll.Continue("waiting for container to be one of (%s), currently %s", strings.Join(state, ", "), inspect.State.Status)
|
return poll.Continue("waiting for container to be one of (%s), currently %s", strings.Join(state, ", "), inspect.State.Status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsSuccessful verifies state.Status == "exited" && state.ExitCode == 0
|
||||||
|
func IsSuccessful(ctx context.Context, client client.APIClient, containerID string) func(log poll.LogT) poll.Result {
|
||||||
|
return func(log poll.LogT) poll.Result {
|
||||||
|
inspect, err := client.ContainerInspect(ctx, containerID)
|
||||||
|
if err != nil {
|
||||||
|
return poll.Error(err)
|
||||||
|
}
|
||||||
|
if inspect.State.Status == "exited" {
|
||||||
|
if inspect.State.ExitCode == 0 {
|
||||||
|
return poll.Success()
|
||||||
|
}
|
||||||
|
return poll.Error(errors.Errorf("expected exit code 0, got %d", inspect.State.ExitCode))
|
||||||
|
}
|
||||||
|
return poll.Continue("waiting for container to be \"exited\", currently %s", inspect.State.Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -97,6 +97,9 @@ func (p *linuxParser) validateMountConfigImpl(mnt *mount.Mount, validateBindSour
|
||||||
return &errMountConfig{mnt, fmt.Errorf("must not set ReadOnly mode when using anonymous volumes")}
|
return &errMountConfig{mnt, fmt.Errorf("must not set ReadOnly mode when using anonymous volumes")}
|
||||||
}
|
}
|
||||||
case mount.TypeTmpfs:
|
case mount.TypeTmpfs:
|
||||||
|
if mnt.BindOptions != nil {
|
||||||
|
return &errMountConfig{mnt, errExtraField("BindOptions")}
|
||||||
|
}
|
||||||
if len(mnt.Source) != 0 {
|
if len(mnt.Source) != 0 {
|
||||||
return &errMountConfig{mnt, errExtraField("Source")}
|
return &errMountConfig{mnt, errExtraField("Source")}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue