зеркало из https://github.com/microsoft/docker.git
cli: add `--mount` to `docker run`
Signed-off-by: Akihiro Suda <suda.akihiro@lab.ntt.co.jp>
This commit is contained in:
Родитель
0f9ba0ea70
Коммит
273eeb813c
|
@ -35,7 +35,7 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command {
|
|||
flags.Var(&opts.containerLabels, flagContainerLabel, "Container labels")
|
||||
flags.VarP(&opts.env, flagEnv, "e", "Set environment variables")
|
||||
flags.Var(&opts.envFile, flagEnvFile, "Read in a file of environment variables")
|
||||
flags.Var(&opts.mounts, flagMount, "Attach a mount to the service")
|
||||
flags.Var(&opts.mounts, flagMount, "Attach a filesystem mount to the service")
|
||||
flags.StringSliceVar(&opts.constraints, flagConstraint, []string{}, "Placement constraints")
|
||||
flags.StringSliceVar(&opts.networks, flagNetwork, []string{}, "Network attachments")
|
||||
flags.VarP(&opts.endpoint.ports, flagPublish, "p", "Publish a port as a node port")
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strconv"
|
||||
|
@ -9,7 +8,6 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
mounttypes "github.com/docker/docker/api/types/mount"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/opts"
|
||||
runconfigopts "github.com/docker/docker/runconfig/opts"
|
||||
|
@ -149,143 +147,6 @@ func (i *Uint64Opt) Value() *uint64 {
|
|||
return i.value
|
||||
}
|
||||
|
||||
// MountOpt is a Value type for parsing mounts
|
||||
type MountOpt struct {
|
||||
values []mounttypes.Mount
|
||||
}
|
||||
|
||||
// Set a new mount value
|
||||
func (m *MountOpt) Set(value string) error {
|
||||
csvReader := csv.NewReader(strings.NewReader(value))
|
||||
fields, err := csvReader.Read()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mount := mounttypes.Mount{}
|
||||
|
||||
volumeOptions := func() *mounttypes.VolumeOptions {
|
||||
if mount.VolumeOptions == nil {
|
||||
mount.VolumeOptions = &mounttypes.VolumeOptions{
|
||||
Labels: make(map[string]string),
|
||||
}
|
||||
}
|
||||
if mount.VolumeOptions.DriverConfig == nil {
|
||||
mount.VolumeOptions.DriverConfig = &mounttypes.Driver{}
|
||||
}
|
||||
return mount.VolumeOptions
|
||||
}
|
||||
|
||||
bindOptions := func() *mounttypes.BindOptions {
|
||||
if mount.BindOptions == nil {
|
||||
mount.BindOptions = new(mounttypes.BindOptions)
|
||||
}
|
||||
return mount.BindOptions
|
||||
}
|
||||
|
||||
setValueOnMap := func(target map[string]string, value string) {
|
||||
parts := strings.SplitN(value, "=", 2)
|
||||
if len(parts) == 1 {
|
||||
target[value] = ""
|
||||
} else {
|
||||
target[parts[0]] = parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
mount.Type = mounttypes.TypeVolume // default to volume mounts
|
||||
// Set writable as the default
|
||||
for _, field := range fields {
|
||||
parts := strings.SplitN(field, "=", 2)
|
||||
key := strings.ToLower(parts[0])
|
||||
|
||||
if len(parts) == 1 {
|
||||
switch key {
|
||||
case "readonly", "ro":
|
||||
mount.ReadOnly = true
|
||||
continue
|
||||
case "volume-nocopy":
|
||||
volumeOptions().NoCopy = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("invalid field '%s' must be a key=value pair", field)
|
||||
}
|
||||
|
||||
value := parts[1]
|
||||
switch key {
|
||||
case "type":
|
||||
mount.Type = mounttypes.Type(strings.ToLower(value))
|
||||
case "source", "src":
|
||||
mount.Source = value
|
||||
case "target", "dst", "destination":
|
||||
mount.Target = value
|
||||
case "readonly", "ro":
|
||||
mount.ReadOnly, err = strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid value for %s: %s", key, value)
|
||||
}
|
||||
case "bind-propagation":
|
||||
bindOptions().Propagation = mounttypes.Propagation(strings.ToLower(value))
|
||||
case "volume-nocopy":
|
||||
volumeOptions().NoCopy, err = strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid value for populate: %s", value)
|
||||
}
|
||||
case "volume-label":
|
||||
setValueOnMap(volumeOptions().Labels, value)
|
||||
case "volume-driver":
|
||||
volumeOptions().DriverConfig.Name = value
|
||||
case "volume-opt":
|
||||
if volumeOptions().DriverConfig.Options == nil {
|
||||
volumeOptions().DriverConfig.Options = make(map[string]string)
|
||||
}
|
||||
setValueOnMap(volumeOptions().DriverConfig.Options, value)
|
||||
default:
|
||||
return fmt.Errorf("unexpected key '%s' in '%s'", key, field)
|
||||
}
|
||||
}
|
||||
|
||||
if mount.Type == "" {
|
||||
return fmt.Errorf("type is required")
|
||||
}
|
||||
|
||||
if mount.Target == "" {
|
||||
return fmt.Errorf("target is required")
|
||||
}
|
||||
|
||||
if mount.Type == mounttypes.TypeBind && mount.VolumeOptions != nil {
|
||||
return fmt.Errorf("cannot mix 'volume-*' options with mount type '%s'", mounttypes.TypeBind)
|
||||
}
|
||||
if mount.Type == mounttypes.TypeVolume && mount.BindOptions != nil {
|
||||
return fmt.Errorf("cannot mix 'bind-*' options with mount type '%s'", mounttypes.TypeVolume)
|
||||
}
|
||||
|
||||
m.values = append(m.values, mount)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Type returns the type of this option
|
||||
func (m *MountOpt) Type() string {
|
||||
return "mount"
|
||||
}
|
||||
|
||||
// String returns a string repr of this option
|
||||
func (m *MountOpt) String() string {
|
||||
mounts := []string{}
|
||||
for _, mount := range m.values {
|
||||
repr := fmt.Sprintf("%s %s %s", mount.Type, mount.Source, mount.Target)
|
||||
mounts = append(mounts, repr)
|
||||
}
|
||||
return strings.Join(mounts, ", ")
|
||||
}
|
||||
|
||||
// Value returns the mounts
|
||||
func (m *MountOpt) Value() []mounttypes.Mount {
|
||||
return m.values
|
||||
}
|
||||
|
||||
type updateOptions struct {
|
||||
parallelism uint64
|
||||
delay time.Duration
|
||||
|
@ -460,7 +321,7 @@ type serviceOptions struct {
|
|||
workdir string
|
||||
user string
|
||||
groups []string
|
||||
mounts MountOpt
|
||||
mounts opts.MountOpt
|
||||
|
||||
resources resourceOptions
|
||||
stopGrace DurationOpt
|
||||
|
|
|
@ -6,7 +6,6 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
mounttypes "github.com/docker/docker/api/types/mount"
|
||||
"github.com/docker/docker/pkg/testutil/assert"
|
||||
)
|
||||
|
||||
|
@ -68,151 +67,6 @@ func TestUint64OptSetAndValue(t *testing.T) {
|
|||
assert.Equal(t, *opt.Value(), uint64(14445))
|
||||
}
|
||||
|
||||
func TestMountOptString(t *testing.T) {
|
||||
mount := MountOpt{
|
||||
values: []mounttypes.Mount{
|
||||
{
|
||||
Type: mounttypes.TypeBind,
|
||||
Source: "/home/path",
|
||||
Target: "/target",
|
||||
},
|
||||
{
|
||||
Type: mounttypes.TypeVolume,
|
||||
Source: "foo",
|
||||
Target: "/target/foo",
|
||||
},
|
||||
},
|
||||
}
|
||||
expected := "bind /home/path /target, volume foo /target/foo"
|
||||
assert.Equal(t, mount.String(), expected)
|
||||
}
|
||||
|
||||
func TestMountOptSetBindNoErrorBind(t *testing.T) {
|
||||
for _, testcase := range []string{
|
||||
// tests several aliases that should have same result.
|
||||
"type=bind,target=/target,source=/source",
|
||||
"type=bind,src=/source,dst=/target",
|
||||
"type=bind,source=/source,dst=/target",
|
||||
"type=bind,src=/source,target=/target",
|
||||
} {
|
||||
var mount MountOpt
|
||||
|
||||
assert.NilError(t, mount.Set(testcase))
|
||||
|
||||
mounts := mount.Value()
|
||||
assert.Equal(t, len(mounts), 1)
|
||||
assert.Equal(t, mounts[0], mounttypes.Mount{
|
||||
Type: mounttypes.TypeBind,
|
||||
Source: "/source",
|
||||
Target: "/target",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMountOptSetVolumeNoError(t *testing.T) {
|
||||
for _, testcase := range []string{
|
||||
// tests several aliases that should have same result.
|
||||
"type=volume,target=/target,source=/source",
|
||||
"type=volume,src=/source,dst=/target",
|
||||
"type=volume,source=/source,dst=/target",
|
||||
"type=volume,src=/source,target=/target",
|
||||
} {
|
||||
var mount MountOpt
|
||||
|
||||
assert.NilError(t, mount.Set(testcase))
|
||||
|
||||
mounts := mount.Value()
|
||||
assert.Equal(t, len(mounts), 1)
|
||||
assert.Equal(t, mounts[0], mounttypes.Mount{
|
||||
Type: mounttypes.TypeVolume,
|
||||
Source: "/source",
|
||||
Target: "/target",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestMountOptDefaultType ensures that a mount without the type defaults to a
|
||||
// volume mount.
|
||||
func TestMountOptDefaultType(t *testing.T) {
|
||||
var mount MountOpt
|
||||
assert.NilError(t, mount.Set("target=/target,source=/foo"))
|
||||
assert.Equal(t, mount.values[0].Type, mounttypes.TypeVolume)
|
||||
}
|
||||
|
||||
func TestMountOptSetErrorNoTarget(t *testing.T) {
|
||||
var mount MountOpt
|
||||
assert.Error(t, mount.Set("type=volume,source=/foo"), "target is required")
|
||||
}
|
||||
|
||||
func TestMountOptSetErrorInvalidKey(t *testing.T) {
|
||||
var mount MountOpt
|
||||
assert.Error(t, mount.Set("type=volume,bogus=foo"), "unexpected key 'bogus'")
|
||||
}
|
||||
|
||||
func TestMountOptSetErrorInvalidField(t *testing.T) {
|
||||
var mount MountOpt
|
||||
assert.Error(t, mount.Set("type=volume,bogus"), "invalid field 'bogus'")
|
||||
}
|
||||
|
||||
func TestMountOptSetErrorInvalidReadOnly(t *testing.T) {
|
||||
var mount MountOpt
|
||||
assert.Error(t, mount.Set("type=volume,readonly=no"), "invalid value for readonly: no")
|
||||
assert.Error(t, mount.Set("type=volume,readonly=invalid"), "invalid value for readonly: invalid")
|
||||
}
|
||||
|
||||
func TestMountOptDefaultEnableReadOnly(t *testing.T) {
|
||||
var m MountOpt
|
||||
assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo"))
|
||||
assert.Equal(t, m.values[0].ReadOnly, false)
|
||||
|
||||
m = MountOpt{}
|
||||
assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo,readonly"))
|
||||
assert.Equal(t, m.values[0].ReadOnly, true)
|
||||
|
||||
m = MountOpt{}
|
||||
assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo,readonly=1"))
|
||||
assert.Equal(t, m.values[0].ReadOnly, true)
|
||||
|
||||
m = MountOpt{}
|
||||
assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo,readonly=true"))
|
||||
assert.Equal(t, m.values[0].ReadOnly, true)
|
||||
|
||||
m = MountOpt{}
|
||||
assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo,readonly=0"))
|
||||
assert.Equal(t, m.values[0].ReadOnly, false)
|
||||
}
|
||||
|
||||
func TestMountOptVolumeNoCopy(t *testing.T) {
|
||||
var m MountOpt
|
||||
assert.NilError(t, m.Set("type=volume,target=/foo,volume-nocopy"))
|
||||
assert.Equal(t, m.values[0].Source, "")
|
||||
|
||||
m = MountOpt{}
|
||||
assert.NilError(t, m.Set("type=volume,target=/foo,source=foo"))
|
||||
assert.Equal(t, m.values[0].VolumeOptions == nil, true)
|
||||
|
||||
m = MountOpt{}
|
||||
assert.NilError(t, m.Set("type=volume,target=/foo,source=foo,volume-nocopy=true"))
|
||||
assert.Equal(t, m.values[0].VolumeOptions != nil, true)
|
||||
assert.Equal(t, m.values[0].VolumeOptions.NoCopy, true)
|
||||
|
||||
m = MountOpt{}
|
||||
assert.NilError(t, m.Set("type=volume,target=/foo,source=foo,volume-nocopy"))
|
||||
assert.Equal(t, m.values[0].VolumeOptions != nil, true)
|
||||
assert.Equal(t, m.values[0].VolumeOptions.NoCopy, true)
|
||||
|
||||
m = MountOpt{}
|
||||
assert.NilError(t, m.Set("type=volume,target=/foo,source=foo,volume-nocopy=1"))
|
||||
assert.Equal(t, m.values[0].VolumeOptions != nil, true)
|
||||
assert.Equal(t, m.values[0].VolumeOptions.NoCopy, true)
|
||||
}
|
||||
|
||||
func TestMountOptTypeConflict(t *testing.T) {
|
||||
var m MountOpt
|
||||
assert.Error(t, m.Set("type=bind,target=/foo,source=/foo,volume-nocopy=true"), "cannot mix")
|
||||
assert.Error(t, m.Set("type=volume,target=/foo,source=/foo,bind-propagation=rprivate"), "cannot mix")
|
||||
}
|
||||
|
||||
func TestHealthCheckOptionsToHealthConfig(t *testing.T) {
|
||||
dur := time.Second
|
||||
opt := healthCheckOptions{
|
||||
|
|
|
@ -404,7 +404,7 @@ func removeItems(
|
|||
|
||||
func updateMounts(flags *pflag.FlagSet, mounts *[]mounttypes.Mount) {
|
||||
if flags.Changed(flagMountAdd) {
|
||||
values := flags.Lookup(flagMountAdd).Value.(*MountOpt).Value()
|
||||
values := flags.Lookup(flagMountAdd).Value.(*opts.MountOpt).Value()
|
||||
*mounts = append(*mounts, values...)
|
||||
}
|
||||
toRemove := buildToRemoveSet(flags, flagMountRemove)
|
||||
|
|
|
@ -1268,6 +1268,7 @@ _docker_container_run() {
|
|||
--memory-swap
|
||||
--memory-swappiness
|
||||
--memory-reservation
|
||||
--mount
|
||||
--name
|
||||
--network
|
||||
--network-alias
|
||||
|
|
|
@ -137,6 +137,7 @@ complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l link -d 'Add
|
|||
complete -c docker -A -f -n '__fish_seen_subcommand_from create' -s m -l memory -d 'Memory limit (format: <number>[<unit>], where unit = b, k, m or g)'
|
||||
complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l mac-address -d 'Container MAC address (e.g. 92:d0:c6:0a:29:33)'
|
||||
complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l memory-swap -d "Total memory usage (memory + swap), set '-1' to disable swap (format: <number>[<unit>], where unit = b, k, m or g)"
|
||||
complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l mount -d 'Attach a filesystem mount to the container'
|
||||
complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l name -d 'Assign a name to the container'
|
||||
complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l net -d 'Set the Network mode for the container'
|
||||
complete -c docker -A -f -n '__fish_seen_subcommand_from create' -s P -l publish-all -d 'Publish all exposed ports to random ports on the host interfaces'
|
||||
|
@ -328,6 +329,7 @@ complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l link -d 'Add li
|
|||
complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s m -l memory -d 'Memory limit (format: <number>[<unit>], where unit = b, k, m or g)'
|
||||
complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l mac-address -d 'Container MAC address (e.g. 92:d0:c6:0a:29:33)'
|
||||
complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l memory-swap -d "Total memory usage (memory + swap), set '-1' to disable swap (format: <number>[<unit>], where unit = b, k, m or g)"
|
||||
complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l mount -d 'Attach a filesystem mount to the container'
|
||||
complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l name -d 'Assign a name to the container'
|
||||
complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l net -d 'Set the Network mode for the container'
|
||||
complete -c docker -A -f -n '__fish_seen_subcommand_from run' -s P -l publish-all -d 'Publish all exposed ports to random ports on the host interfaces'
|
||||
|
|
|
@ -1101,7 +1101,7 @@ __docker_service_subcommand() {
|
|||
"($help)--limit-memory=[Limit Memory]:value: "
|
||||
"($help)--log-driver=[Logging driver for service]:logging driver:__docker_log_drivers"
|
||||
"($help)*--log-opt=[Logging driver options]:log driver options:__docker_log_options"
|
||||
"($help)*--mount=[Attach a mount to the service]:mount: "
|
||||
"($help)*--mount=[Attach a filesystem mount to the service]:mount: "
|
||||
"($help)*--network=[Network attachments]:network: "
|
||||
"($help)--no-healthcheck[Disable any container-specified HEALTHCHECK]"
|
||||
"($help)*"{-p=,--publish=}"[Publish a port as a node port]:port: "
|
||||
|
@ -1481,6 +1481,7 @@ __docker_subcommand() {
|
|||
"($help)--log-driver=[Default driver for container logs]:logging driver:__docker_log_drivers"
|
||||
"($help)*--log-opt=[Log driver specific options]:log driver options:__docker_log_options"
|
||||
"($help)--mac-address=[Container MAC address]:MAC address: "
|
||||
"($help)*--mount=[Attach a filesystem mount to the container]:mount: "
|
||||
"($help)--name=[Container name]:name: "
|
||||
"($help)--network=[Connect a container to a network]:network mode:(bridge none container host)"
|
||||
"($help)*--network-alias=[Add network-scoped alias for the container]:alias: "
|
||||
|
|
|
@ -78,6 +78,7 @@ Options:
|
|||
--memory-reservation string Memory soft limit
|
||||
--memory-swap string Swap limit equal to memory plus swap: '-1' to enable unlimited swap
|
||||
--memory-swappiness int Tune container memory swappiness (0 to 100) (default -1)
|
||||
--mount value Attach a filesytem mount to the container (default [])
|
||||
--name string Assign a name to the container
|
||||
--network-alias value Add network-scoped alias for the container (default [])
|
||||
--network string Connect a container to a network (default "default")
|
||||
|
|
|
@ -84,6 +84,7 @@ Options:
|
|||
--memory-reservation string Memory soft limit
|
||||
--memory-swap string Swap limit equal to memory plus swap: '-1' to enable unlimited swap
|
||||
--memory-swappiness int Tune container memory swappiness (0 to 100) (default -1)
|
||||
--mount value Attach a filesystem mount to the container (default [])
|
||||
--name string Assign a name to the container
|
||||
--network-alias value Add network-scoped alias for the container (default [])
|
||||
--network string Connect a container to a network
|
||||
|
@ -255,6 +256,21 @@ Docker daemon.
|
|||
|
||||
For in-depth information about volumes, refer to [manage data in containers](https://docs.docker.com/engine/tutorials/dockervolumes/)
|
||||
|
||||
### Add bin-mounts or volumes using the --mounts flag
|
||||
|
||||
The `--mounts` flag allows you to mount volumes, host-directories and `tmpfs`
|
||||
mounts in a container.
|
||||
|
||||
The `--mount` flag supports most options that are supported by the `-v` or the
|
||||
`--volume` flag, but uses a different syntax. For in-depth information on the
|
||||
`--mount` flag, and a comparison between `--volume` and `--mount`, refer to
|
||||
the [service create command reference](service_create.md#add-bind-mounts-or-volumes).
|
||||
|
||||
Examples:
|
||||
|
||||
$ docker run --read-only --mount type=volume,target=/icanwrite busybox touch /icanwrite/here
|
||||
$ docker run -t -i --mount type=bind,src=/data,dst=/data busybox sh
|
||||
|
||||
### Publish or expose port (-p, --expose)
|
||||
|
||||
$ docker run -p 127.0.0.1:80:8080 ubuntu bash
|
||||
|
|
|
@ -38,7 +38,7 @@ Options:
|
|||
--log-driver string Logging driver for service
|
||||
--log-opt value Logging driver options (default [])
|
||||
--mode string Service mode (replicated or global) (default "replicated")
|
||||
--mount value Attach a mount to the service
|
||||
--mount value Attach a filesystem mount to the service
|
||||
--name string Service name
|
||||
--network value Network attachments (default [])
|
||||
--no-healthcheck Disable any container-specified HEALTHCHECK
|
||||
|
|
|
@ -4588,3 +4588,181 @@ func (s *DockerSuite) TestRunDuplicateMount(c *check.C) {
|
|||
out = inspectFieldJSON(c, name, "Config.Volumes")
|
||||
c.Assert(out, checker.Contains, "null")
|
||||
}
|
||||
|
||||
func (s *DockerSuite) TestRunMount(c *check.C) {
|
||||
testRequires(c, DaemonIsLinux, SameHostDaemon, NotUserNamespace)
|
||||
|
||||
// mnt1, mnt2, and testCatFooBar are commonly used in multiple test cases
|
||||
tmpDir, err := ioutil.TempDir("", "mount")
|
||||
if err != nil {
|
||||
c.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
mnt1, mnt2 := path.Join(tmpDir, "mnt1"), path.Join(tmpDir, "mnt2")
|
||||
if err := os.Mkdir(mnt1, 0755); err != nil {
|
||||
c.Fatal(err)
|
||||
}
|
||||
if err := os.Mkdir(mnt2, 0755); err != nil {
|
||||
c.Fatal(err)
|
||||
}
|
||||
if err := ioutil.WriteFile(path.Join(mnt1, "test1"), []byte("test1"), 0644); err != nil {
|
||||
c.Fatal(err)
|
||||
}
|
||||
if err := ioutil.WriteFile(path.Join(mnt2, "test2"), []byte("test2"), 0644); err != nil {
|
||||
c.Fatal(err)
|
||||
}
|
||||
testCatFooBar := func(cName string) error {
|
||||
out, _ := dockerCmd(c, "exec", cName, "cat", "/foo/test1")
|
||||
if out != "test1" {
|
||||
return fmt.Errorf("%s not mounted on /foo", mnt1)
|
||||
}
|
||||
out, _ = dockerCmd(c, "exec", cName, "cat", "/bar/test2")
|
||||
if out != "test2" {
|
||||
return fmt.Errorf("%s not mounted on /bar", mnt2)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type testCase struct {
|
||||
equivalents [][]string
|
||||
valid bool
|
||||
// fn should be nil if valid==false
|
||||
fn func(cName string) error
|
||||
}
|
||||
cases := []testCase{
|
||||
{
|
||||
equivalents: [][]string{
|
||||
{
|
||||
"--mount", fmt.Sprintf("type=bind,src=%s,dst=/foo", mnt1),
|
||||
"--mount", fmt.Sprintf("type=bind,src=%s,dst=/bar", mnt2),
|
||||
},
|
||||
{
|
||||
"--mount", fmt.Sprintf("type=bind,src=%s,dst=/foo", mnt1),
|
||||
"--mount", fmt.Sprintf("type=bind,src=%s,target=/bar", mnt2),
|
||||
},
|
||||
{
|
||||
"--volume", fmt.Sprintf("%s:/foo", mnt1),
|
||||
"--mount", fmt.Sprintf("type=bind,src=%s,target=/bar", mnt2),
|
||||
},
|
||||
},
|
||||
valid: true,
|
||||
fn: testCatFooBar,
|
||||
},
|
||||
{
|
||||
equivalents: [][]string{
|
||||
{
|
||||
"--mount", fmt.Sprintf("type=volume,src=%s,dst=/foo", mnt1),
|
||||
"--mount", fmt.Sprintf("type=volume,src=%s,dst=/bar", mnt2),
|
||||
},
|
||||
{
|
||||
"--mount", fmt.Sprintf("type=volume,src=%s,dst=/foo", mnt1),
|
||||
"--mount", fmt.Sprintf("type=volume,src=%s,target=/bar", mnt2),
|
||||
},
|
||||
},
|
||||
valid: false,
|
||||
},
|
||||
{
|
||||
equivalents: [][]string{
|
||||
{
|
||||
"--mount", fmt.Sprintf("type=bind,src=%s,dst=/foo", mnt1),
|
||||
"--mount", fmt.Sprintf("type=volume,src=%s,dst=/bar", mnt2),
|
||||
},
|
||||
{
|
||||
"--volume", fmt.Sprintf("%s:/foo", mnt1),
|
||||
"--mount", fmt.Sprintf("type=volume,src=%s,target=/bar", mnt2),
|
||||
},
|
||||
},
|
||||
valid: false,
|
||||
fn: testCatFooBar,
|
||||
},
|
||||
{
|
||||
equivalents: [][]string{
|
||||
{
|
||||
"--read-only",
|
||||
"--mount", "type=volume,dst=/bar",
|
||||
},
|
||||
},
|
||||
valid: true,
|
||||
fn: func(cName string) error {
|
||||
_, _, err := dockerCmdWithError("exec", cName, "touch", "/bar/icanwritehere")
|
||||
return err
|
||||
},
|
||||
},
|
||||
{
|
||||
equivalents: [][]string{
|
||||
{
|
||||
"--read-only",
|
||||
"--mount", fmt.Sprintf("type=bind,src=%s,dst=/foo", mnt1),
|
||||
"--mount", "type=volume,dst=/bar",
|
||||
},
|
||||
{
|
||||
"--read-only",
|
||||
"--volume", fmt.Sprintf("%s:/foo", mnt1),
|
||||
"--mount", "type=volume,dst=/bar",
|
||||
},
|
||||
},
|
||||
valid: true,
|
||||
fn: func(cName string) error {
|
||||
out, _ := dockerCmd(c, "exec", cName, "cat", "/foo/test1")
|
||||
if out != "test1" {
|
||||
return fmt.Errorf("%s not mounted on /foo", mnt1)
|
||||
}
|
||||
_, _, err := dockerCmdWithError("exec", cName, "touch", "/bar/icanwritehere")
|
||||
return err
|
||||
},
|
||||
},
|
||||
{
|
||||
equivalents: [][]string{
|
||||
{
|
||||
"--mount", fmt.Sprintf("type=bind,src=%s,dst=/foo", mnt1),
|
||||
"--mount", fmt.Sprintf("type=bind,src=%s,dst=/foo", mnt2),
|
||||
},
|
||||
{
|
||||
"--mount", fmt.Sprintf("type=bind,src=%s,dst=/foo", mnt1),
|
||||
"--mount", fmt.Sprintf("type=bind,src=%s,target=/foo", mnt2),
|
||||
},
|
||||
{
|
||||
"--volume", fmt.Sprintf("%s:/foo", mnt1),
|
||||
"--mount", fmt.Sprintf("type=bind,src=%s,target=/foo", mnt2),
|
||||
},
|
||||
},
|
||||
valid: false,
|
||||
},
|
||||
{
|
||||
equivalents: [][]string{
|
||||
{
|
||||
"--volume", fmt.Sprintf("%s:/foo", mnt1),
|
||||
"--mount", fmt.Sprintf("type=volume,src=%s,target=/foo", mnt2),
|
||||
},
|
||||
},
|
||||
valid: false,
|
||||
},
|
||||
{
|
||||
equivalents: [][]string{
|
||||
{
|
||||
"--mount", "type=volume,target=/foo",
|
||||
"--mount", "type=volume,target=/foo",
|
||||
},
|
||||
},
|
||||
valid: false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, testCase := range cases {
|
||||
for j, opts := range testCase.equivalents {
|
||||
cName := fmt.Sprintf("mount-%d-%d", i, j)
|
||||
_, _, err := dockerCmdWithError(append([]string{"run", "-i", "-d", "--name", cName},
|
||||
append(opts, []string{"busybox", "top"}...)...)...)
|
||||
if testCase.valid {
|
||||
c.Assert(err, check.IsNil,
|
||||
check.Commentf("got error while creating a container with %v (%s)", opts, cName))
|
||||
c.Assert(testCase.fn(cName), check.IsNil,
|
||||
check.Commentf("got error while executing test for %v (%s)", opts, cName))
|
||||
dockerCmd(c, "rm", "-f", cName)
|
||||
} else {
|
||||
c.Assert(err, checker.NotNil,
|
||||
check.Commentf("got nil while creating a container with %v (%s)", opts, cName))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,6 +53,7 @@ docker-create - Create a new container
|
|||
[**--memory-reservation**[=*MEMORY-RESERVATION*]]
|
||||
[**--memory-swap**[=*LIMIT*]]
|
||||
[**--memory-swappiness**[=*MEMORY-SWAPPINESS*]]
|
||||
[**--mount**[=*MOUNT*]]
|
||||
[**--name**[=*NAME*]]
|
||||
[**--network-alias**[=*[]*]]
|
||||
[**--network**[=*"bridge"*]]
|
||||
|
|
|
@ -55,6 +55,7 @@ docker-run - Run a command in a new container
|
|||
[**--memory-reservation**[=*MEMORY-RESERVATION*]]
|
||||
[**--memory-swap**[=*LIMIT*]]
|
||||
[**--memory-swappiness**[=*MEMORY-SWAPPINESS*]]
|
||||
[**--mount**[=*MOUNT*]]
|
||||
[**--name**[=*NAME*]]
|
||||
[**--network-alias**[=*[]*]]
|
||||
[**--network**[=*"bridge"*]]
|
||||
|
|
|
@ -0,0 +1,147 @@
|
|||
package opts
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
mounttypes "github.com/docker/docker/api/types/mount"
|
||||
)
|
||||
|
||||
// MountOpt is a Value type for parsing mounts
|
||||
type MountOpt struct {
|
||||
values []mounttypes.Mount
|
||||
}
|
||||
|
||||
// Set a new mount value
|
||||
func (m *MountOpt) Set(value string) error {
|
||||
csvReader := csv.NewReader(strings.NewReader(value))
|
||||
fields, err := csvReader.Read()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mount := mounttypes.Mount{}
|
||||
|
||||
volumeOptions := func() *mounttypes.VolumeOptions {
|
||||
if mount.VolumeOptions == nil {
|
||||
mount.VolumeOptions = &mounttypes.VolumeOptions{
|
||||
Labels: make(map[string]string),
|
||||
}
|
||||
}
|
||||
if mount.VolumeOptions.DriverConfig == nil {
|
||||
mount.VolumeOptions.DriverConfig = &mounttypes.Driver{}
|
||||
}
|
||||
return mount.VolumeOptions
|
||||
}
|
||||
|
||||
bindOptions := func() *mounttypes.BindOptions {
|
||||
if mount.BindOptions == nil {
|
||||
mount.BindOptions = new(mounttypes.BindOptions)
|
||||
}
|
||||
return mount.BindOptions
|
||||
}
|
||||
|
||||
setValueOnMap := func(target map[string]string, value string) {
|
||||
parts := strings.SplitN(value, "=", 2)
|
||||
if len(parts) == 1 {
|
||||
target[value] = ""
|
||||
} else {
|
||||
target[parts[0]] = parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
mount.Type = mounttypes.TypeVolume // default to volume mounts
|
||||
// Set writable as the default
|
||||
for _, field := range fields {
|
||||
parts := strings.SplitN(field, "=", 2)
|
||||
key := strings.ToLower(parts[0])
|
||||
|
||||
if len(parts) == 1 {
|
||||
switch key {
|
||||
case "readonly", "ro":
|
||||
mount.ReadOnly = true
|
||||
continue
|
||||
case "volume-nocopy":
|
||||
volumeOptions().NoCopy = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("invalid field '%s' must be a key=value pair", field)
|
||||
}
|
||||
|
||||
value := parts[1]
|
||||
switch key {
|
||||
case "type":
|
||||
mount.Type = mounttypes.Type(strings.ToLower(value))
|
||||
case "source", "src":
|
||||
mount.Source = value
|
||||
case "target", "dst", "destination":
|
||||
mount.Target = value
|
||||
case "readonly", "ro":
|
||||
mount.ReadOnly, err = strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid value for %s: %s", key, value)
|
||||
}
|
||||
case "bind-propagation":
|
||||
bindOptions().Propagation = mounttypes.Propagation(strings.ToLower(value))
|
||||
case "volume-nocopy":
|
||||
volumeOptions().NoCopy, err = strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid value for populate: %s", value)
|
||||
}
|
||||
case "volume-label":
|
||||
setValueOnMap(volumeOptions().Labels, value)
|
||||
case "volume-driver":
|
||||
volumeOptions().DriverConfig.Name = value
|
||||
case "volume-opt":
|
||||
if volumeOptions().DriverConfig.Options == nil {
|
||||
volumeOptions().DriverConfig.Options = make(map[string]string)
|
||||
}
|
||||
setValueOnMap(volumeOptions().DriverConfig.Options, value)
|
||||
default:
|
||||
return fmt.Errorf("unexpected key '%s' in '%s'", key, field)
|
||||
}
|
||||
}
|
||||
|
||||
if mount.Type == "" {
|
||||
return fmt.Errorf("type is required")
|
||||
}
|
||||
|
||||
if mount.Target == "" {
|
||||
return fmt.Errorf("target is required")
|
||||
}
|
||||
|
||||
if mount.Type == mounttypes.TypeBind && mount.VolumeOptions != nil {
|
||||
return fmt.Errorf("cannot mix 'volume-*' options with mount type '%s'", mounttypes.TypeBind)
|
||||
}
|
||||
if mount.Type == mounttypes.TypeVolume && mount.BindOptions != nil {
|
||||
return fmt.Errorf("cannot mix 'bind-*' options with mount type '%s'", mounttypes.TypeVolume)
|
||||
}
|
||||
|
||||
m.values = append(m.values, mount)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Type returns the type of this option
|
||||
func (m *MountOpt) Type() string {
|
||||
return "mount"
|
||||
}
|
||||
|
||||
// String returns a string repr of this option
|
||||
func (m *MountOpt) String() string {
|
||||
mounts := []string{}
|
||||
for _, mount := range m.values {
|
||||
repr := fmt.Sprintf("%s %s %s", mount.Type, mount.Source, mount.Target)
|
||||
mounts = append(mounts, repr)
|
||||
}
|
||||
return strings.Join(mounts, ", ")
|
||||
}
|
||||
|
||||
// Value returns the mounts
|
||||
func (m *MountOpt) Value() []mounttypes.Mount {
|
||||
return m.values
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
package opts
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
mounttypes "github.com/docker/docker/api/types/mount"
|
||||
"github.com/docker/docker/pkg/testutil/assert"
|
||||
)
|
||||
|
||||
func TestMountOptString(t *testing.T) {
|
||||
mount := MountOpt{
|
||||
values: []mounttypes.Mount{
|
||||
{
|
||||
Type: mounttypes.TypeBind,
|
||||
Source: "/home/path",
|
||||
Target: "/target",
|
||||
},
|
||||
{
|
||||
Type: mounttypes.TypeVolume,
|
||||
Source: "foo",
|
||||
Target: "/target/foo",
|
||||
},
|
||||
},
|
||||
}
|
||||
expected := "bind /home/path /target, volume foo /target/foo"
|
||||
assert.Equal(t, mount.String(), expected)
|
||||
}
|
||||
|
||||
func TestMountOptSetBindNoErrorBind(t *testing.T) {
|
||||
for _, testcase := range []string{
|
||||
// tests several aliases that should have same result.
|
||||
"type=bind,target=/target,source=/source",
|
||||
"type=bind,src=/source,dst=/target",
|
||||
"type=bind,source=/source,dst=/target",
|
||||
"type=bind,src=/source,target=/target",
|
||||
} {
|
||||
var mount MountOpt
|
||||
|
||||
assert.NilError(t, mount.Set(testcase))
|
||||
|
||||
mounts := mount.Value()
|
||||
assert.Equal(t, len(mounts), 1)
|
||||
assert.Equal(t, mounts[0], mounttypes.Mount{
|
||||
Type: mounttypes.TypeBind,
|
||||
Source: "/source",
|
||||
Target: "/target",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMountOptSetVolumeNoError(t *testing.T) {
|
||||
for _, testcase := range []string{
|
||||
// tests several aliases that should have same result.
|
||||
"type=volume,target=/target,source=/source",
|
||||
"type=volume,src=/source,dst=/target",
|
||||
"type=volume,source=/source,dst=/target",
|
||||
"type=volume,src=/source,target=/target",
|
||||
} {
|
||||
var mount MountOpt
|
||||
|
||||
assert.NilError(t, mount.Set(testcase))
|
||||
|
||||
mounts := mount.Value()
|
||||
assert.Equal(t, len(mounts), 1)
|
||||
assert.Equal(t, mounts[0], mounttypes.Mount{
|
||||
Type: mounttypes.TypeVolume,
|
||||
Source: "/source",
|
||||
Target: "/target",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestMountOptDefaultType ensures that a mount without the type defaults to a
|
||||
// volume mount.
|
||||
func TestMountOptDefaultType(t *testing.T) {
|
||||
var mount MountOpt
|
||||
assert.NilError(t, mount.Set("target=/target,source=/foo"))
|
||||
assert.Equal(t, mount.values[0].Type, mounttypes.TypeVolume)
|
||||
}
|
||||
|
||||
func TestMountOptSetErrorNoTarget(t *testing.T) {
|
||||
var mount MountOpt
|
||||
assert.Error(t, mount.Set("type=volume,source=/foo"), "target is required")
|
||||
}
|
||||
|
||||
func TestMountOptSetErrorInvalidKey(t *testing.T) {
|
||||
var mount MountOpt
|
||||
assert.Error(t, mount.Set("type=volume,bogus=foo"), "unexpected key 'bogus'")
|
||||
}
|
||||
|
||||
func TestMountOptSetErrorInvalidField(t *testing.T) {
|
||||
var mount MountOpt
|
||||
assert.Error(t, mount.Set("type=volume,bogus"), "invalid field 'bogus'")
|
||||
}
|
||||
|
||||
func TestMountOptSetErrorInvalidReadOnly(t *testing.T) {
|
||||
var mount MountOpt
|
||||
assert.Error(t, mount.Set("type=volume,readonly=no"), "invalid value for readonly: no")
|
||||
assert.Error(t, mount.Set("type=volume,readonly=invalid"), "invalid value for readonly: invalid")
|
||||
}
|
||||
|
||||
func TestMountOptDefaultEnableReadOnly(t *testing.T) {
|
||||
var m MountOpt
|
||||
assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo"))
|
||||
assert.Equal(t, m.values[0].ReadOnly, false)
|
||||
|
||||
m = MountOpt{}
|
||||
assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo,readonly"))
|
||||
assert.Equal(t, m.values[0].ReadOnly, true)
|
||||
|
||||
m = MountOpt{}
|
||||
assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo,readonly=1"))
|
||||
assert.Equal(t, m.values[0].ReadOnly, true)
|
||||
|
||||
m = MountOpt{}
|
||||
assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo,readonly=true"))
|
||||
assert.Equal(t, m.values[0].ReadOnly, true)
|
||||
|
||||
m = MountOpt{}
|
||||
assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo,readonly=0"))
|
||||
assert.Equal(t, m.values[0].ReadOnly, false)
|
||||
}
|
||||
|
||||
func TestMountOptVolumeNoCopy(t *testing.T) {
|
||||
var m MountOpt
|
||||
assert.NilError(t, m.Set("type=volume,target=/foo,volume-nocopy"))
|
||||
assert.Equal(t, m.values[0].Source, "")
|
||||
|
||||
m = MountOpt{}
|
||||
assert.NilError(t, m.Set("type=volume,target=/foo,source=foo"))
|
||||
assert.Equal(t, m.values[0].VolumeOptions == nil, true)
|
||||
|
||||
m = MountOpt{}
|
||||
assert.NilError(t, m.Set("type=volume,target=/foo,source=foo,volume-nocopy=true"))
|
||||
assert.Equal(t, m.values[0].VolumeOptions != nil, true)
|
||||
assert.Equal(t, m.values[0].VolumeOptions.NoCopy, true)
|
||||
|
||||
m = MountOpt{}
|
||||
assert.NilError(t, m.Set("type=volume,target=/foo,source=foo,volume-nocopy"))
|
||||
assert.Equal(t, m.values[0].VolumeOptions != nil, true)
|
||||
assert.Equal(t, m.values[0].VolumeOptions.NoCopy, true)
|
||||
|
||||
m = MountOpt{}
|
||||
assert.NilError(t, m.Set("type=volume,target=/foo,source=foo,volume-nocopy=1"))
|
||||
assert.Equal(t, m.values[0].VolumeOptions != nil, true)
|
||||
assert.Equal(t, m.values[0].VolumeOptions.NoCopy, true)
|
||||
}
|
||||
|
||||
func TestMountOptTypeConflict(t *testing.T) {
|
||||
var m MountOpt
|
||||
assert.Error(t, m.Set("type=bind,target=/foo,source=/foo,volume-nocopy=true"), "cannot mix")
|
||||
assert.Error(t, m.Set("type=volume,target=/foo,source=/foo,bind-propagation=rprivate"), "cannot mix")
|
||||
}
|
|
@ -26,6 +26,7 @@ type ContainerOptions struct {
|
|||
attach opts.ListOpts
|
||||
volumes opts.ListOpts
|
||||
tmpfs opts.ListOpts
|
||||
mounts opts.MountOpt
|
||||
blkioWeightDevice WeightdeviceOpt
|
||||
deviceReadBps ThrottledeviceOpt
|
||||
deviceWriteBps ThrottledeviceOpt
|
||||
|
@ -210,6 +211,7 @@ func AddFlags(flags *pflag.FlagSet) *ContainerOptions {
|
|||
flags.Var(&copts.tmpfs, "tmpfs", "Mount a tmpfs directory")
|
||||
flags.Var(&copts.volumesFrom, "volumes-from", "Mount volumes from the specified container(s)")
|
||||
flags.VarP(&copts.volumes, "volume", "v", "Bind mount a volume")
|
||||
flags.Var(&copts.mounts, "mount", "Attach a filesystem mount to the container")
|
||||
|
||||
// Health-checking
|
||||
flags.StringVar(&copts.healthCmd, "health-cmd", "", "Command to run to check health")
|
||||
|
@ -347,6 +349,8 @@ func Parse(flags *pflag.FlagSet, copts *ContainerOptions) (*container.Config, *c
|
|||
}
|
||||
}
|
||||
|
||||
mounts := copts.mounts.Value()
|
||||
|
||||
var binds []string
|
||||
volumes := copts.volumes.GetMap()
|
||||
// add any bind targets to the list of container volumes
|
||||
|
@ -612,6 +616,7 @@ func Parse(flags *pflag.FlagSet, copts *ContainerOptions) (*container.Config, *c
|
|||
Tmpfs: tmpfs,
|
||||
Sysctls: copts.sysctls.GetAll(),
|
||||
Runtime: copts.runtime,
|
||||
Mounts: mounts,
|
||||
}
|
||||
|
||||
// only set this value if the user provided the flag, else it should default to nil
|
||||
|
|
Загрузка…
Ссылка в новой задаче