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:
Akihiro Suda 2018-10-10 19:20:13 +09:00
parent 12bba16306
commit 596cdffb9f
12 changed files with 160 additions and 35 deletions

View file

@ -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,

View file

@ -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"

View file

@ -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.

View file

@ -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"`
} }

View file

@ -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 {

View file

@ -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")
} }

View file

@ -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
} }

View file

@ -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

View file

@ -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))
}
}

View file

@ -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) {

View file

@ -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)
}
}

View file

@ -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")}
} }