cli: add `--mount` to `docker run`

Signed-off-by: Akihiro Suda <suda.akihiro@lab.ntt.co.jp>
This commit is contained in:
Akihiro Suda 2016-10-25 03:26:54 +00:00
Родитель 0f9ba0ea70
Коммит 273eeb813c
16 изменённых файлов: 511 добавлений и 290 удалений

Просмотреть файл

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

147
opts/mount.go Normal file
Просмотреть файл

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

153
opts/mount_test.go Normal file
Просмотреть файл

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