Signed-off-by: Akihiro Suda <suda.akihiro@lab.ntt.co.jp>
| ... | ... |
@@ -35,7 +35,7 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command {
|
| 35 | 35 |
flags.Var(&opts.containerLabels, flagContainerLabel, "Container labels") |
| 36 | 36 |
flags.VarP(&opts.env, flagEnv, "e", "Set environment variables") |
| 37 | 37 |
flags.Var(&opts.envFile, flagEnvFile, "Read in a file of environment variables") |
| 38 |
- flags.Var(&opts.mounts, flagMount, "Attach a mount to the service") |
|
| 38 |
+ flags.Var(&opts.mounts, flagMount, "Attach a filesystem mount to the service") |
|
| 39 | 39 |
flags.StringSliceVar(&opts.constraints, flagConstraint, []string{}, "Placement constraints")
|
| 40 | 40 |
flags.StringSliceVar(&opts.networks, flagNetwork, []string{}, "Network attachments")
|
| 41 | 41 |
flags.VarP(&opts.endpoint.ports, flagPublish, "p", "Publish a port as a node port") |
| ... | ... |
@@ -1,7 +1,6 @@ |
| 1 | 1 |
package service |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
- "encoding/csv" |
|
| 5 | 4 |
"fmt" |
| 6 | 5 |
"math/big" |
| 7 | 6 |
"strconv" |
| ... | ... |
@@ -9,7 +8,6 @@ import ( |
| 9 | 9 |
"time" |
| 10 | 10 |
|
| 11 | 11 |
"github.com/docker/docker/api/types/container" |
| 12 |
- mounttypes "github.com/docker/docker/api/types/mount" |
|
| 13 | 12 |
"github.com/docker/docker/api/types/swarm" |
| 14 | 13 |
"github.com/docker/docker/opts" |
| 15 | 14 |
runconfigopts "github.com/docker/docker/runconfig/opts" |
| ... | ... |
@@ -149,143 +147,6 @@ func (i *Uint64Opt) Value() *uint64 {
|
| 149 | 149 |
return i.value |
| 150 | 150 |
} |
| 151 | 151 |
|
| 152 |
-// MountOpt is a Value type for parsing mounts |
|
| 153 |
-type MountOpt struct {
|
|
| 154 |
- values []mounttypes.Mount |
|
| 155 |
-} |
|
| 156 |
- |
|
| 157 |
-// Set a new mount value |
|
| 158 |
-func (m *MountOpt) Set(value string) error {
|
|
| 159 |
- csvReader := csv.NewReader(strings.NewReader(value)) |
|
| 160 |
- fields, err := csvReader.Read() |
|
| 161 |
- if err != nil {
|
|
| 162 |
- return err |
|
| 163 |
- } |
|
| 164 |
- |
|
| 165 |
- mount := mounttypes.Mount{}
|
|
| 166 |
- |
|
| 167 |
- volumeOptions := func() *mounttypes.VolumeOptions {
|
|
| 168 |
- if mount.VolumeOptions == nil {
|
|
| 169 |
- mount.VolumeOptions = &mounttypes.VolumeOptions{
|
|
| 170 |
- Labels: make(map[string]string), |
|
| 171 |
- } |
|
| 172 |
- } |
|
| 173 |
- if mount.VolumeOptions.DriverConfig == nil {
|
|
| 174 |
- mount.VolumeOptions.DriverConfig = &mounttypes.Driver{}
|
|
| 175 |
- } |
|
| 176 |
- return mount.VolumeOptions |
|
| 177 |
- } |
|
| 178 |
- |
|
| 179 |
- bindOptions := func() *mounttypes.BindOptions {
|
|
| 180 |
- if mount.BindOptions == nil {
|
|
| 181 |
- mount.BindOptions = new(mounttypes.BindOptions) |
|
| 182 |
- } |
|
| 183 |
- return mount.BindOptions |
|
| 184 |
- } |
|
| 185 |
- |
|
| 186 |
- setValueOnMap := func(target map[string]string, value string) {
|
|
| 187 |
- parts := strings.SplitN(value, "=", 2) |
|
| 188 |
- if len(parts) == 1 {
|
|
| 189 |
- target[value] = "" |
|
| 190 |
- } else {
|
|
| 191 |
- target[parts[0]] = parts[1] |
|
| 192 |
- } |
|
| 193 |
- } |
|
| 194 |
- |
|
| 195 |
- mount.Type = mounttypes.TypeVolume // default to volume mounts |
|
| 196 |
- // Set writable as the default |
|
| 197 |
- for _, field := range fields {
|
|
| 198 |
- parts := strings.SplitN(field, "=", 2) |
|
| 199 |
- key := strings.ToLower(parts[0]) |
|
| 200 |
- |
|
| 201 |
- if len(parts) == 1 {
|
|
| 202 |
- switch key {
|
|
| 203 |
- case "readonly", "ro": |
|
| 204 |
- mount.ReadOnly = true |
|
| 205 |
- continue |
|
| 206 |
- case "volume-nocopy": |
|
| 207 |
- volumeOptions().NoCopy = true |
|
| 208 |
- continue |
|
| 209 |
- } |
|
| 210 |
- } |
|
| 211 |
- |
|
| 212 |
- if len(parts) != 2 {
|
|
| 213 |
- return fmt.Errorf("invalid field '%s' must be a key=value pair", field)
|
|
| 214 |
- } |
|
| 215 |
- |
|
| 216 |
- value := parts[1] |
|
| 217 |
- switch key {
|
|
| 218 |
- case "type": |
|
| 219 |
- mount.Type = mounttypes.Type(strings.ToLower(value)) |
|
| 220 |
- case "source", "src": |
|
| 221 |
- mount.Source = value |
|
| 222 |
- case "target", "dst", "destination": |
|
| 223 |
- mount.Target = value |
|
| 224 |
- case "readonly", "ro": |
|
| 225 |
- mount.ReadOnly, err = strconv.ParseBool(value) |
|
| 226 |
- if err != nil {
|
|
| 227 |
- return fmt.Errorf("invalid value for %s: %s", key, value)
|
|
| 228 |
- } |
|
| 229 |
- case "bind-propagation": |
|
| 230 |
- bindOptions().Propagation = mounttypes.Propagation(strings.ToLower(value)) |
|
| 231 |
- case "volume-nocopy": |
|
| 232 |
- volumeOptions().NoCopy, err = strconv.ParseBool(value) |
|
| 233 |
- if err != nil {
|
|
| 234 |
- return fmt.Errorf("invalid value for populate: %s", value)
|
|
| 235 |
- } |
|
| 236 |
- case "volume-label": |
|
| 237 |
- setValueOnMap(volumeOptions().Labels, value) |
|
| 238 |
- case "volume-driver": |
|
| 239 |
- volumeOptions().DriverConfig.Name = value |
|
| 240 |
- case "volume-opt": |
|
| 241 |
- if volumeOptions().DriverConfig.Options == nil {
|
|
| 242 |
- volumeOptions().DriverConfig.Options = make(map[string]string) |
|
| 243 |
- } |
|
| 244 |
- setValueOnMap(volumeOptions().DriverConfig.Options, value) |
|
| 245 |
- default: |
|
| 246 |
- return fmt.Errorf("unexpected key '%s' in '%s'", key, field)
|
|
| 247 |
- } |
|
| 248 |
- } |
|
| 249 |
- |
|
| 250 |
- if mount.Type == "" {
|
|
| 251 |
- return fmt.Errorf("type is required")
|
|
| 252 |
- } |
|
| 253 |
- |
|
| 254 |
- if mount.Target == "" {
|
|
| 255 |
- return fmt.Errorf("target is required")
|
|
| 256 |
- } |
|
| 257 |
- |
|
| 258 |
- if mount.Type == mounttypes.TypeBind && mount.VolumeOptions != nil {
|
|
| 259 |
- return fmt.Errorf("cannot mix 'volume-*' options with mount type '%s'", mounttypes.TypeBind)
|
|
| 260 |
- } |
|
| 261 |
- if mount.Type == mounttypes.TypeVolume && mount.BindOptions != nil {
|
|
| 262 |
- return fmt.Errorf("cannot mix 'bind-*' options with mount type '%s'", mounttypes.TypeVolume)
|
|
| 263 |
- } |
|
| 264 |
- |
|
| 265 |
- m.values = append(m.values, mount) |
|
| 266 |
- return nil |
|
| 267 |
-} |
|
| 268 |
- |
|
| 269 |
-// Type returns the type of this option |
|
| 270 |
-func (m *MountOpt) Type() string {
|
|
| 271 |
- return "mount" |
|
| 272 |
-} |
|
| 273 |
- |
|
| 274 |
-// String returns a string repr of this option |
|
| 275 |
-func (m *MountOpt) String() string {
|
|
| 276 |
- mounts := []string{}
|
|
| 277 |
- for _, mount := range m.values {
|
|
| 278 |
- repr := fmt.Sprintf("%s %s %s", mount.Type, mount.Source, mount.Target)
|
|
| 279 |
- mounts = append(mounts, repr) |
|
| 280 |
- } |
|
| 281 |
- return strings.Join(mounts, ", ") |
|
| 282 |
-} |
|
| 283 |
- |
|
| 284 |
-// Value returns the mounts |
|
| 285 |
-func (m *MountOpt) Value() []mounttypes.Mount {
|
|
| 286 |
- return m.values |
|
| 287 |
-} |
|
| 288 |
- |
|
| 289 | 152 |
type updateOptions struct {
|
| 290 | 153 |
parallelism uint64 |
| 291 | 154 |
delay time.Duration |
| ... | ... |
@@ -460,7 +321,7 @@ type serviceOptions struct {
|
| 460 | 460 |
workdir string |
| 461 | 461 |
user string |
| 462 | 462 |
groups []string |
| 463 |
- mounts MountOpt |
|
| 463 |
+ mounts opts.MountOpt |
|
| 464 | 464 |
|
| 465 | 465 |
resources resourceOptions |
| 466 | 466 |
stopGrace DurationOpt |
| ... | ... |
@@ -6,7 +6,6 @@ import ( |
| 6 | 6 |
"time" |
| 7 | 7 |
|
| 8 | 8 |
"github.com/docker/docker/api/types/container" |
| 9 |
- mounttypes "github.com/docker/docker/api/types/mount" |
|
| 10 | 9 |
"github.com/docker/docker/pkg/testutil/assert" |
| 11 | 10 |
) |
| 12 | 11 |
|
| ... | ... |
@@ -68,151 +67,6 @@ func TestUint64OptSetAndValue(t *testing.T) {
|
| 68 | 68 |
assert.Equal(t, *opt.Value(), uint64(14445)) |
| 69 | 69 |
} |
| 70 | 70 |
|
| 71 |
-func TestMountOptString(t *testing.T) {
|
|
| 72 |
- mount := MountOpt{
|
|
| 73 |
- values: []mounttypes.Mount{
|
|
| 74 |
- {
|
|
| 75 |
- Type: mounttypes.TypeBind, |
|
| 76 |
- Source: "/home/path", |
|
| 77 |
- Target: "/target", |
|
| 78 |
- }, |
|
| 79 |
- {
|
|
| 80 |
- Type: mounttypes.TypeVolume, |
|
| 81 |
- Source: "foo", |
|
| 82 |
- Target: "/target/foo", |
|
| 83 |
- }, |
|
| 84 |
- }, |
|
| 85 |
- } |
|
| 86 |
- expected := "bind /home/path /target, volume foo /target/foo" |
|
| 87 |
- assert.Equal(t, mount.String(), expected) |
|
| 88 |
-} |
|
| 89 |
- |
|
| 90 |
-func TestMountOptSetBindNoErrorBind(t *testing.T) {
|
|
| 91 |
- for _, testcase := range []string{
|
|
| 92 |
- // tests several aliases that should have same result. |
|
| 93 |
- "type=bind,target=/target,source=/source", |
|
| 94 |
- "type=bind,src=/source,dst=/target", |
|
| 95 |
- "type=bind,source=/source,dst=/target", |
|
| 96 |
- "type=bind,src=/source,target=/target", |
|
| 97 |
- } {
|
|
| 98 |
- var mount MountOpt |
|
| 99 |
- |
|
| 100 |
- assert.NilError(t, mount.Set(testcase)) |
|
| 101 |
- |
|
| 102 |
- mounts := mount.Value() |
|
| 103 |
- assert.Equal(t, len(mounts), 1) |
|
| 104 |
- assert.Equal(t, mounts[0], mounttypes.Mount{
|
|
| 105 |
- Type: mounttypes.TypeBind, |
|
| 106 |
- Source: "/source", |
|
| 107 |
- Target: "/target", |
|
| 108 |
- }) |
|
| 109 |
- } |
|
| 110 |
-} |
|
| 111 |
- |
|
| 112 |
-func TestMountOptSetVolumeNoError(t *testing.T) {
|
|
| 113 |
- for _, testcase := range []string{
|
|
| 114 |
- // tests several aliases that should have same result. |
|
| 115 |
- "type=volume,target=/target,source=/source", |
|
| 116 |
- "type=volume,src=/source,dst=/target", |
|
| 117 |
- "type=volume,source=/source,dst=/target", |
|
| 118 |
- "type=volume,src=/source,target=/target", |
|
| 119 |
- } {
|
|
| 120 |
- var mount MountOpt |
|
| 121 |
- |
|
| 122 |
- assert.NilError(t, mount.Set(testcase)) |
|
| 123 |
- |
|
| 124 |
- mounts := mount.Value() |
|
| 125 |
- assert.Equal(t, len(mounts), 1) |
|
| 126 |
- assert.Equal(t, mounts[0], mounttypes.Mount{
|
|
| 127 |
- Type: mounttypes.TypeVolume, |
|
| 128 |
- Source: "/source", |
|
| 129 |
- Target: "/target", |
|
| 130 |
- }) |
|
| 131 |
- } |
|
| 132 |
-} |
|
| 133 |
- |
|
| 134 |
-// TestMountOptDefaultType ensures that a mount without the type defaults to a |
|
| 135 |
-// volume mount. |
|
| 136 |
-func TestMountOptDefaultType(t *testing.T) {
|
|
| 137 |
- var mount MountOpt |
|
| 138 |
- assert.NilError(t, mount.Set("target=/target,source=/foo"))
|
|
| 139 |
- assert.Equal(t, mount.values[0].Type, mounttypes.TypeVolume) |
|
| 140 |
-} |
|
| 141 |
- |
|
| 142 |
-func TestMountOptSetErrorNoTarget(t *testing.T) {
|
|
| 143 |
- var mount MountOpt |
|
| 144 |
- assert.Error(t, mount.Set("type=volume,source=/foo"), "target is required")
|
|
| 145 |
-} |
|
| 146 |
- |
|
| 147 |
-func TestMountOptSetErrorInvalidKey(t *testing.T) {
|
|
| 148 |
- var mount MountOpt |
|
| 149 |
- assert.Error(t, mount.Set("type=volume,bogus=foo"), "unexpected key 'bogus'")
|
|
| 150 |
-} |
|
| 151 |
- |
|
| 152 |
-func TestMountOptSetErrorInvalidField(t *testing.T) {
|
|
| 153 |
- var mount MountOpt |
|
| 154 |
- assert.Error(t, mount.Set("type=volume,bogus"), "invalid field 'bogus'")
|
|
| 155 |
-} |
|
| 156 |
- |
|
| 157 |
-func TestMountOptSetErrorInvalidReadOnly(t *testing.T) {
|
|
| 158 |
- var mount MountOpt |
|
| 159 |
- assert.Error(t, mount.Set("type=volume,readonly=no"), "invalid value for readonly: no")
|
|
| 160 |
- assert.Error(t, mount.Set("type=volume,readonly=invalid"), "invalid value for readonly: invalid")
|
|
| 161 |
-} |
|
| 162 |
- |
|
| 163 |
-func TestMountOptDefaultEnableReadOnly(t *testing.T) {
|
|
| 164 |
- var m MountOpt |
|
| 165 |
- assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo"))
|
|
| 166 |
- assert.Equal(t, m.values[0].ReadOnly, false) |
|
| 167 |
- |
|
| 168 |
- m = MountOpt{}
|
|
| 169 |
- assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo,readonly"))
|
|
| 170 |
- assert.Equal(t, m.values[0].ReadOnly, true) |
|
| 171 |
- |
|
| 172 |
- m = MountOpt{}
|
|
| 173 |
- assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo,readonly=1"))
|
|
| 174 |
- assert.Equal(t, m.values[0].ReadOnly, true) |
|
| 175 |
- |
|
| 176 |
- m = MountOpt{}
|
|
| 177 |
- assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo,readonly=true"))
|
|
| 178 |
- assert.Equal(t, m.values[0].ReadOnly, true) |
|
| 179 |
- |
|
| 180 |
- m = MountOpt{}
|
|
| 181 |
- assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo,readonly=0"))
|
|
| 182 |
- assert.Equal(t, m.values[0].ReadOnly, false) |
|
| 183 |
-} |
|
| 184 |
- |
|
| 185 |
-func TestMountOptVolumeNoCopy(t *testing.T) {
|
|
| 186 |
- var m MountOpt |
|
| 187 |
- assert.NilError(t, m.Set("type=volume,target=/foo,volume-nocopy"))
|
|
| 188 |
- assert.Equal(t, m.values[0].Source, "") |
|
| 189 |
- |
|
| 190 |
- m = MountOpt{}
|
|
| 191 |
- assert.NilError(t, m.Set("type=volume,target=/foo,source=foo"))
|
|
| 192 |
- assert.Equal(t, m.values[0].VolumeOptions == nil, true) |
|
| 193 |
- |
|
| 194 |
- m = MountOpt{}
|
|
| 195 |
- assert.NilError(t, m.Set("type=volume,target=/foo,source=foo,volume-nocopy=true"))
|
|
| 196 |
- assert.Equal(t, m.values[0].VolumeOptions != nil, true) |
|
| 197 |
- assert.Equal(t, m.values[0].VolumeOptions.NoCopy, true) |
|
| 198 |
- |
|
| 199 |
- m = MountOpt{}
|
|
| 200 |
- assert.NilError(t, m.Set("type=volume,target=/foo,source=foo,volume-nocopy"))
|
|
| 201 |
- assert.Equal(t, m.values[0].VolumeOptions != nil, true) |
|
| 202 |
- assert.Equal(t, m.values[0].VolumeOptions.NoCopy, true) |
|
| 203 |
- |
|
| 204 |
- m = MountOpt{}
|
|
| 205 |
- assert.NilError(t, m.Set("type=volume,target=/foo,source=foo,volume-nocopy=1"))
|
|
| 206 |
- assert.Equal(t, m.values[0].VolumeOptions != nil, true) |
|
| 207 |
- assert.Equal(t, m.values[0].VolumeOptions.NoCopy, true) |
|
| 208 |
-} |
|
| 209 |
- |
|
| 210 |
-func TestMountOptTypeConflict(t *testing.T) {
|
|
| 211 |
- var m MountOpt |
|
| 212 |
- assert.Error(t, m.Set("type=bind,target=/foo,source=/foo,volume-nocopy=true"), "cannot mix")
|
|
| 213 |
- assert.Error(t, m.Set("type=volume,target=/foo,source=/foo,bind-propagation=rprivate"), "cannot mix")
|
|
| 214 |
-} |
|
| 215 |
- |
|
| 216 | 71 |
func TestHealthCheckOptionsToHealthConfig(t *testing.T) {
|
| 217 | 72 |
dur := time.Second |
| 218 | 73 |
opt := healthCheckOptions{
|
| ... | ... |
@@ -404,7 +404,7 @@ func removeItems( |
| 404 | 404 |
|
| 405 | 405 |
func updateMounts(flags *pflag.FlagSet, mounts *[]mounttypes.Mount) {
|
| 406 | 406 |
if flags.Changed(flagMountAdd) {
|
| 407 |
- values := flags.Lookup(flagMountAdd).Value.(*MountOpt).Value() |
|
| 407 |
+ values := flags.Lookup(flagMountAdd).Value.(*opts.MountOpt).Value() |
|
| 408 | 408 |
*mounts = append(*mounts, values...) |
| 409 | 409 |
} |
| 410 | 410 |
toRemove := buildToRemoveSet(flags, flagMountRemove) |
| ... | ... |
@@ -137,6 +137,7 @@ complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l link -d 'Add |
| 137 | 137 |
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)' |
| 138 | 138 |
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)' |
| 139 | 139 |
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)" |
| 140 |
+complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l mount -d 'Attach a filesystem mount to the container' |
|
| 140 | 141 |
complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l name -d 'Assign a name to the container' |
| 141 | 142 |
complete -c docker -A -f -n '__fish_seen_subcommand_from create' -l net -d 'Set the Network mode for the container' |
| 142 | 143 |
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 |
| 328 | 328 |
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)' |
| 329 | 329 |
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)' |
| 330 | 330 |
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)" |
| 331 |
+complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l mount -d 'Attach a filesystem mount to the container' |
|
| 331 | 332 |
complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l name -d 'Assign a name to the container' |
| 332 | 333 |
complete -c docker -A -f -n '__fish_seen_subcommand_from run' -l net -d 'Set the Network mode for the container' |
| 333 | 334 |
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() {
|
| 1101 | 1101 |
"($help)--limit-memory=[Limit Memory]:value: " |
| 1102 | 1102 |
"($help)--log-driver=[Logging driver for service]:logging driver:__docker_log_drivers" |
| 1103 | 1103 |
"($help)*--log-opt=[Logging driver options]:log driver options:__docker_log_options" |
| 1104 |
- "($help)*--mount=[Attach a mount to the service]:mount: " |
|
| 1104 |
+ "($help)*--mount=[Attach a filesystem mount to the service]:mount: " |
|
| 1105 | 1105 |
"($help)*--network=[Network attachments]:network: " |
| 1106 | 1106 |
"($help)--no-healthcheck[Disable any container-specified HEALTHCHECK]" |
| 1107 | 1107 |
"($help)*"{-p=,--publish=}"[Publish a port as a node port]:port: "
|
| ... | ... |
@@ -1481,6 +1481,7 @@ __docker_subcommand() {
|
| 1481 | 1481 |
"($help)--log-driver=[Default driver for container logs]:logging driver:__docker_log_drivers" |
| 1482 | 1482 |
"($help)*--log-opt=[Log driver specific options]:log driver options:__docker_log_options" |
| 1483 | 1483 |
"($help)--mac-address=[Container MAC address]:MAC address: " |
| 1484 |
+ "($help)*--mount=[Attach a filesystem mount to the container]:mount: " |
|
| 1484 | 1485 |
"($help)--name=[Container name]:name: " |
| 1485 | 1486 |
"($help)--network=[Connect a container to a network]:network mode:(bridge none container host)" |
| 1486 | 1487 |
"($help)*--network-alias=[Add network-scoped alias for the container]:alias: " |
| ... | ... |
@@ -78,6 +78,7 @@ Options: |
| 78 | 78 |
--memory-reservation string Memory soft limit |
| 79 | 79 |
--memory-swap string Swap limit equal to memory plus swap: '-1' to enable unlimited swap |
| 80 | 80 |
--memory-swappiness int Tune container memory swappiness (0 to 100) (default -1) |
| 81 |
+ --mount value Attach a filesytem mount to the container (default []) |
|
| 81 | 82 |
--name string Assign a name to the container |
| 82 | 83 |
--network-alias value Add network-scoped alias for the container (default []) |
| 83 | 84 |
--network string Connect a container to a network (default "default") |
| ... | ... |
@@ -84,6 +84,7 @@ Options: |
| 84 | 84 |
--memory-reservation string Memory soft limit |
| 85 | 85 |
--memory-swap string Swap limit equal to memory plus swap: '-1' to enable unlimited swap |
| 86 | 86 |
--memory-swappiness int Tune container memory swappiness (0 to 100) (default -1) |
| 87 |
+ --mount value Attach a filesystem mount to the container (default []) |
|
| 87 | 88 |
--name string Assign a name to the container |
| 88 | 89 |
--network-alias value Add network-scoped alias for the container (default []) |
| 89 | 90 |
--network string Connect a container to a network |
| ... | ... |
@@ -255,6 +256,21 @@ Docker daemon. |
| 255 | 255 |
|
| 256 | 256 |
For in-depth information about volumes, refer to [manage data in containers](https://docs.docker.com/engine/tutorials/dockervolumes/) |
| 257 | 257 |
|
| 258 |
+### Add bin-mounts or volumes using the --mounts flag |
|
| 259 |
+ |
|
| 260 |
+The `--mounts` flag allows you to mount volumes, host-directories and `tmpfs` |
|
| 261 |
+mounts in a container. |
|
| 262 |
+ |
|
| 263 |
+The `--mount` flag supports most options that are supported by the `-v` or the |
|
| 264 |
+`--volume` flag, but uses a different syntax. For in-depth information on the |
|
| 265 |
+`--mount` flag, and a comparison between `--volume` and `--mount`, refer to |
|
| 266 |
+the [service create command reference](service_create.md#add-bind-mounts-or-volumes). |
|
| 267 |
+ |
|
| 268 |
+Examples: |
|
| 269 |
+ |
|
| 270 |
+ $ docker run --read-only --mount type=volume,target=/icanwrite busybox touch /icanwrite/here |
|
| 271 |
+ $ docker run -t -i --mount type=bind,src=/data,dst=/data busybox sh |
|
| 272 |
+ |
|
| 258 | 273 |
### Publish or expose port (-p, --expose) |
| 259 | 274 |
|
| 260 | 275 |
$ docker run -p 127.0.0.1:80:8080 ubuntu bash |
| ... | ... |
@@ -38,7 +38,7 @@ Options: |
| 38 | 38 |
--log-driver string Logging driver for service |
| 39 | 39 |
--log-opt value Logging driver options (default []) |
| 40 | 40 |
--mode string Service mode (replicated or global) (default "replicated") |
| 41 |
- --mount value Attach a mount to the service |
|
| 41 |
+ --mount value Attach a filesystem mount to the service |
|
| 42 | 42 |
--name string Service name |
| 43 | 43 |
--network value Network attachments (default []) |
| 44 | 44 |
--no-healthcheck Disable any container-specified HEALTHCHECK |
| ... | ... |
@@ -4588,3 +4588,181 @@ func (s *DockerSuite) TestRunDuplicateMount(c *check.C) {
|
| 4588 | 4588 |
out = inspectFieldJSON(c, name, "Config.Volumes") |
| 4589 | 4589 |
c.Assert(out, checker.Contains, "null") |
| 4590 | 4590 |
} |
| 4591 |
+ |
|
| 4592 |
+func (s *DockerSuite) TestRunMount(c *check.C) {
|
|
| 4593 |
+ testRequires(c, DaemonIsLinux, SameHostDaemon, NotUserNamespace) |
|
| 4594 |
+ |
|
| 4595 |
+ // mnt1, mnt2, and testCatFooBar are commonly used in multiple test cases |
|
| 4596 |
+ tmpDir, err := ioutil.TempDir("", "mount")
|
|
| 4597 |
+ if err != nil {
|
|
| 4598 |
+ c.Fatal(err) |
|
| 4599 |
+ } |
|
| 4600 |
+ defer os.RemoveAll(tmpDir) |
|
| 4601 |
+ mnt1, mnt2 := path.Join(tmpDir, "mnt1"), path.Join(tmpDir, "mnt2") |
|
| 4602 |
+ if err := os.Mkdir(mnt1, 0755); err != nil {
|
|
| 4603 |
+ c.Fatal(err) |
|
| 4604 |
+ } |
|
| 4605 |
+ if err := os.Mkdir(mnt2, 0755); err != nil {
|
|
| 4606 |
+ c.Fatal(err) |
|
| 4607 |
+ } |
|
| 4608 |
+ if err := ioutil.WriteFile(path.Join(mnt1, "test1"), []byte("test1"), 0644); err != nil {
|
|
| 4609 |
+ c.Fatal(err) |
|
| 4610 |
+ } |
|
| 4611 |
+ if err := ioutil.WriteFile(path.Join(mnt2, "test2"), []byte("test2"), 0644); err != nil {
|
|
| 4612 |
+ c.Fatal(err) |
|
| 4613 |
+ } |
|
| 4614 |
+ testCatFooBar := func(cName string) error {
|
|
| 4615 |
+ out, _ := dockerCmd(c, "exec", cName, "cat", "/foo/test1") |
|
| 4616 |
+ if out != "test1" {
|
|
| 4617 |
+ return fmt.Errorf("%s not mounted on /foo", mnt1)
|
|
| 4618 |
+ } |
|
| 4619 |
+ out, _ = dockerCmd(c, "exec", cName, "cat", "/bar/test2") |
|
| 4620 |
+ if out != "test2" {
|
|
| 4621 |
+ return fmt.Errorf("%s not mounted on /bar", mnt2)
|
|
| 4622 |
+ } |
|
| 4623 |
+ return nil |
|
| 4624 |
+ } |
|
| 4625 |
+ |
|
| 4626 |
+ type testCase struct {
|
|
| 4627 |
+ equivalents [][]string |
|
| 4628 |
+ valid bool |
|
| 4629 |
+ // fn should be nil if valid==false |
|
| 4630 |
+ fn func(cName string) error |
|
| 4631 |
+ } |
|
| 4632 |
+ cases := []testCase{
|
|
| 4633 |
+ {
|
|
| 4634 |
+ equivalents: [][]string{
|
|
| 4635 |
+ {
|
|
| 4636 |
+ "--mount", fmt.Sprintf("type=bind,src=%s,dst=/foo", mnt1),
|
|
| 4637 |
+ "--mount", fmt.Sprintf("type=bind,src=%s,dst=/bar", mnt2),
|
|
| 4638 |
+ }, |
|
| 4639 |
+ {
|
|
| 4640 |
+ "--mount", fmt.Sprintf("type=bind,src=%s,dst=/foo", mnt1),
|
|
| 4641 |
+ "--mount", fmt.Sprintf("type=bind,src=%s,target=/bar", mnt2),
|
|
| 4642 |
+ }, |
|
| 4643 |
+ {
|
|
| 4644 |
+ "--volume", fmt.Sprintf("%s:/foo", mnt1),
|
|
| 4645 |
+ "--mount", fmt.Sprintf("type=bind,src=%s,target=/bar", mnt2),
|
|
| 4646 |
+ }, |
|
| 4647 |
+ }, |
|
| 4648 |
+ valid: true, |
|
| 4649 |
+ fn: testCatFooBar, |
|
| 4650 |
+ }, |
|
| 4651 |
+ {
|
|
| 4652 |
+ equivalents: [][]string{
|
|
| 4653 |
+ {
|
|
| 4654 |
+ "--mount", fmt.Sprintf("type=volume,src=%s,dst=/foo", mnt1),
|
|
| 4655 |
+ "--mount", fmt.Sprintf("type=volume,src=%s,dst=/bar", mnt2),
|
|
| 4656 |
+ }, |
|
| 4657 |
+ {
|
|
| 4658 |
+ "--mount", fmt.Sprintf("type=volume,src=%s,dst=/foo", mnt1),
|
|
| 4659 |
+ "--mount", fmt.Sprintf("type=volume,src=%s,target=/bar", mnt2),
|
|
| 4660 |
+ }, |
|
| 4661 |
+ }, |
|
| 4662 |
+ valid: false, |
|
| 4663 |
+ }, |
|
| 4664 |
+ {
|
|
| 4665 |
+ equivalents: [][]string{
|
|
| 4666 |
+ {
|
|
| 4667 |
+ "--mount", fmt.Sprintf("type=bind,src=%s,dst=/foo", mnt1),
|
|
| 4668 |
+ "--mount", fmt.Sprintf("type=volume,src=%s,dst=/bar", mnt2),
|
|
| 4669 |
+ }, |
|
| 4670 |
+ {
|
|
| 4671 |
+ "--volume", fmt.Sprintf("%s:/foo", mnt1),
|
|
| 4672 |
+ "--mount", fmt.Sprintf("type=volume,src=%s,target=/bar", mnt2),
|
|
| 4673 |
+ }, |
|
| 4674 |
+ }, |
|
| 4675 |
+ valid: false, |
|
| 4676 |
+ fn: testCatFooBar, |
|
| 4677 |
+ }, |
|
| 4678 |
+ {
|
|
| 4679 |
+ equivalents: [][]string{
|
|
| 4680 |
+ {
|
|
| 4681 |
+ "--read-only", |
|
| 4682 |
+ "--mount", "type=volume,dst=/bar", |
|
| 4683 |
+ }, |
|
| 4684 |
+ }, |
|
| 4685 |
+ valid: true, |
|
| 4686 |
+ fn: func(cName string) error {
|
|
| 4687 |
+ _, _, err := dockerCmdWithError("exec", cName, "touch", "/bar/icanwritehere")
|
|
| 4688 |
+ return err |
|
| 4689 |
+ }, |
|
| 4690 |
+ }, |
|
| 4691 |
+ {
|
|
| 4692 |
+ equivalents: [][]string{
|
|
| 4693 |
+ {
|
|
| 4694 |
+ "--read-only", |
|
| 4695 |
+ "--mount", fmt.Sprintf("type=bind,src=%s,dst=/foo", mnt1),
|
|
| 4696 |
+ "--mount", "type=volume,dst=/bar", |
|
| 4697 |
+ }, |
|
| 4698 |
+ {
|
|
| 4699 |
+ "--read-only", |
|
| 4700 |
+ "--volume", fmt.Sprintf("%s:/foo", mnt1),
|
|
| 4701 |
+ "--mount", "type=volume,dst=/bar", |
|
| 4702 |
+ }, |
|
| 4703 |
+ }, |
|
| 4704 |
+ valid: true, |
|
| 4705 |
+ fn: func(cName string) error {
|
|
| 4706 |
+ out, _ := dockerCmd(c, "exec", cName, "cat", "/foo/test1") |
|
| 4707 |
+ if out != "test1" {
|
|
| 4708 |
+ return fmt.Errorf("%s not mounted on /foo", mnt1)
|
|
| 4709 |
+ } |
|
| 4710 |
+ _, _, err := dockerCmdWithError("exec", cName, "touch", "/bar/icanwritehere")
|
|
| 4711 |
+ return err |
|
| 4712 |
+ }, |
|
| 4713 |
+ }, |
|
| 4714 |
+ {
|
|
| 4715 |
+ equivalents: [][]string{
|
|
| 4716 |
+ {
|
|
| 4717 |
+ "--mount", fmt.Sprintf("type=bind,src=%s,dst=/foo", mnt1),
|
|
| 4718 |
+ "--mount", fmt.Sprintf("type=bind,src=%s,dst=/foo", mnt2),
|
|
| 4719 |
+ }, |
|
| 4720 |
+ {
|
|
| 4721 |
+ "--mount", fmt.Sprintf("type=bind,src=%s,dst=/foo", mnt1),
|
|
| 4722 |
+ "--mount", fmt.Sprintf("type=bind,src=%s,target=/foo", mnt2),
|
|
| 4723 |
+ }, |
|
| 4724 |
+ {
|
|
| 4725 |
+ "--volume", fmt.Sprintf("%s:/foo", mnt1),
|
|
| 4726 |
+ "--mount", fmt.Sprintf("type=bind,src=%s,target=/foo", mnt2),
|
|
| 4727 |
+ }, |
|
| 4728 |
+ }, |
|
| 4729 |
+ valid: false, |
|
| 4730 |
+ }, |
|
| 4731 |
+ {
|
|
| 4732 |
+ equivalents: [][]string{
|
|
| 4733 |
+ {
|
|
| 4734 |
+ "--volume", fmt.Sprintf("%s:/foo", mnt1),
|
|
| 4735 |
+ "--mount", fmt.Sprintf("type=volume,src=%s,target=/foo", mnt2),
|
|
| 4736 |
+ }, |
|
| 4737 |
+ }, |
|
| 4738 |
+ valid: false, |
|
| 4739 |
+ }, |
|
| 4740 |
+ {
|
|
| 4741 |
+ equivalents: [][]string{
|
|
| 4742 |
+ {
|
|
| 4743 |
+ "--mount", "type=volume,target=/foo", |
|
| 4744 |
+ "--mount", "type=volume,target=/foo", |
|
| 4745 |
+ }, |
|
| 4746 |
+ }, |
|
| 4747 |
+ valid: false, |
|
| 4748 |
+ }, |
|
| 4749 |
+ } |
|
| 4750 |
+ |
|
| 4751 |
+ for i, testCase := range cases {
|
|
| 4752 |
+ for j, opts := range testCase.equivalents {
|
|
| 4753 |
+ cName := fmt.Sprintf("mount-%d-%d", i, j)
|
|
| 4754 |
+ _, _, err := dockerCmdWithError(append([]string{"run", "-i", "-d", "--name", cName},
|
|
| 4755 |
+ append(opts, []string{"busybox", "top"}...)...)...)
|
|
| 4756 |
+ if testCase.valid {
|
|
| 4757 |
+ c.Assert(err, check.IsNil, |
|
| 4758 |
+ check.Commentf("got error while creating a container with %v (%s)", opts, cName))
|
|
| 4759 |
+ c.Assert(testCase.fn(cName), check.IsNil, |
|
| 4760 |
+ check.Commentf("got error while executing test for %v (%s)", opts, cName))
|
|
| 4761 |
+ dockerCmd(c, "rm", "-f", cName) |
|
| 4762 |
+ } else {
|
|
| 4763 |
+ c.Assert(err, checker.NotNil, |
|
| 4764 |
+ check.Commentf("got nil while creating a container with %v (%s)", opts, cName))
|
|
| 4765 |
+ } |
|
| 4766 |
+ } |
|
| 4767 |
+ } |
|
| 4768 |
+} |
| ... | ... |
@@ -53,6 +53,7 @@ docker-create - Create a new container |
| 53 | 53 |
[**--memory-reservation**[=*MEMORY-RESERVATION*]] |
| 54 | 54 |
[**--memory-swap**[=*LIMIT*]] |
| 55 | 55 |
[**--memory-swappiness**[=*MEMORY-SWAPPINESS*]] |
| 56 |
+[**--mount**[=*MOUNT*]] |
|
| 56 | 57 |
[**--name**[=*NAME*]] |
| 57 | 58 |
[**--network-alias**[=*[]*]] |
| 58 | 59 |
[**--network**[=*"bridge"*]] |
| ... | ... |
@@ -55,6 +55,7 @@ docker-run - Run a command in a new container |
| 55 | 55 |
[**--memory-reservation**[=*MEMORY-RESERVATION*]] |
| 56 | 56 |
[**--memory-swap**[=*LIMIT*]] |
| 57 | 57 |
[**--memory-swappiness**[=*MEMORY-SWAPPINESS*]] |
| 58 |
+[**--mount**[=*MOUNT*]] |
|
| 58 | 59 |
[**--name**[=*NAME*]] |
| 59 | 60 |
[**--network-alias**[=*[]*]] |
| 60 | 61 |
[**--network**[=*"bridge"*]] |
| 61 | 62 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,147 @@ |
| 0 |
+package opts |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "encoding/csv" |
|
| 4 |
+ "fmt" |
|
| 5 |
+ "strconv" |
|
| 6 |
+ "strings" |
|
| 7 |
+ |
|
| 8 |
+ mounttypes "github.com/docker/docker/api/types/mount" |
|
| 9 |
+) |
|
| 10 |
+ |
|
| 11 |
+// MountOpt is a Value type for parsing mounts |
|
| 12 |
+type MountOpt struct {
|
|
| 13 |
+ values []mounttypes.Mount |
|
| 14 |
+} |
|
| 15 |
+ |
|
| 16 |
+// Set a new mount value |
|
| 17 |
+func (m *MountOpt) Set(value string) error {
|
|
| 18 |
+ csvReader := csv.NewReader(strings.NewReader(value)) |
|
| 19 |
+ fields, err := csvReader.Read() |
|
| 20 |
+ if err != nil {
|
|
| 21 |
+ return err |
|
| 22 |
+ } |
|
| 23 |
+ |
|
| 24 |
+ mount := mounttypes.Mount{}
|
|
| 25 |
+ |
|
| 26 |
+ volumeOptions := func() *mounttypes.VolumeOptions {
|
|
| 27 |
+ if mount.VolumeOptions == nil {
|
|
| 28 |
+ mount.VolumeOptions = &mounttypes.VolumeOptions{
|
|
| 29 |
+ Labels: make(map[string]string), |
|
| 30 |
+ } |
|
| 31 |
+ } |
|
| 32 |
+ if mount.VolumeOptions.DriverConfig == nil {
|
|
| 33 |
+ mount.VolumeOptions.DriverConfig = &mounttypes.Driver{}
|
|
| 34 |
+ } |
|
| 35 |
+ return mount.VolumeOptions |
|
| 36 |
+ } |
|
| 37 |
+ |
|
| 38 |
+ bindOptions := func() *mounttypes.BindOptions {
|
|
| 39 |
+ if mount.BindOptions == nil {
|
|
| 40 |
+ mount.BindOptions = new(mounttypes.BindOptions) |
|
| 41 |
+ } |
|
| 42 |
+ return mount.BindOptions |
|
| 43 |
+ } |
|
| 44 |
+ |
|
| 45 |
+ setValueOnMap := func(target map[string]string, value string) {
|
|
| 46 |
+ parts := strings.SplitN(value, "=", 2) |
|
| 47 |
+ if len(parts) == 1 {
|
|
| 48 |
+ target[value] = "" |
|
| 49 |
+ } else {
|
|
| 50 |
+ target[parts[0]] = parts[1] |
|
| 51 |
+ } |
|
| 52 |
+ } |
|
| 53 |
+ |
|
| 54 |
+ mount.Type = mounttypes.TypeVolume // default to volume mounts |
|
| 55 |
+ // Set writable as the default |
|
| 56 |
+ for _, field := range fields {
|
|
| 57 |
+ parts := strings.SplitN(field, "=", 2) |
|
| 58 |
+ key := strings.ToLower(parts[0]) |
|
| 59 |
+ |
|
| 60 |
+ if len(parts) == 1 {
|
|
| 61 |
+ switch key {
|
|
| 62 |
+ case "readonly", "ro": |
|
| 63 |
+ mount.ReadOnly = true |
|
| 64 |
+ continue |
|
| 65 |
+ case "volume-nocopy": |
|
| 66 |
+ volumeOptions().NoCopy = true |
|
| 67 |
+ continue |
|
| 68 |
+ } |
|
| 69 |
+ } |
|
| 70 |
+ |
|
| 71 |
+ if len(parts) != 2 {
|
|
| 72 |
+ return fmt.Errorf("invalid field '%s' must be a key=value pair", field)
|
|
| 73 |
+ } |
|
| 74 |
+ |
|
| 75 |
+ value := parts[1] |
|
| 76 |
+ switch key {
|
|
| 77 |
+ case "type": |
|
| 78 |
+ mount.Type = mounttypes.Type(strings.ToLower(value)) |
|
| 79 |
+ case "source", "src": |
|
| 80 |
+ mount.Source = value |
|
| 81 |
+ case "target", "dst", "destination": |
|
| 82 |
+ mount.Target = value |
|
| 83 |
+ case "readonly", "ro": |
|
| 84 |
+ mount.ReadOnly, err = strconv.ParseBool(value) |
|
| 85 |
+ if err != nil {
|
|
| 86 |
+ return fmt.Errorf("invalid value for %s: %s", key, value)
|
|
| 87 |
+ } |
|
| 88 |
+ case "bind-propagation": |
|
| 89 |
+ bindOptions().Propagation = mounttypes.Propagation(strings.ToLower(value)) |
|
| 90 |
+ case "volume-nocopy": |
|
| 91 |
+ volumeOptions().NoCopy, err = strconv.ParseBool(value) |
|
| 92 |
+ if err != nil {
|
|
| 93 |
+ return fmt.Errorf("invalid value for populate: %s", value)
|
|
| 94 |
+ } |
|
| 95 |
+ case "volume-label": |
|
| 96 |
+ setValueOnMap(volumeOptions().Labels, value) |
|
| 97 |
+ case "volume-driver": |
|
| 98 |
+ volumeOptions().DriverConfig.Name = value |
|
| 99 |
+ case "volume-opt": |
|
| 100 |
+ if volumeOptions().DriverConfig.Options == nil {
|
|
| 101 |
+ volumeOptions().DriverConfig.Options = make(map[string]string) |
|
| 102 |
+ } |
|
| 103 |
+ setValueOnMap(volumeOptions().DriverConfig.Options, value) |
|
| 104 |
+ default: |
|
| 105 |
+ return fmt.Errorf("unexpected key '%s' in '%s'", key, field)
|
|
| 106 |
+ } |
|
| 107 |
+ } |
|
| 108 |
+ |
|
| 109 |
+ if mount.Type == "" {
|
|
| 110 |
+ return fmt.Errorf("type is required")
|
|
| 111 |
+ } |
|
| 112 |
+ |
|
| 113 |
+ if mount.Target == "" {
|
|
| 114 |
+ return fmt.Errorf("target is required")
|
|
| 115 |
+ } |
|
| 116 |
+ |
|
| 117 |
+ if mount.Type == mounttypes.TypeBind && mount.VolumeOptions != nil {
|
|
| 118 |
+ return fmt.Errorf("cannot mix 'volume-*' options with mount type '%s'", mounttypes.TypeBind)
|
|
| 119 |
+ } |
|
| 120 |
+ if mount.Type == mounttypes.TypeVolume && mount.BindOptions != nil {
|
|
| 121 |
+ return fmt.Errorf("cannot mix 'bind-*' options with mount type '%s'", mounttypes.TypeVolume)
|
|
| 122 |
+ } |
|
| 123 |
+ |
|
| 124 |
+ m.values = append(m.values, mount) |
|
| 125 |
+ return nil |
|
| 126 |
+} |
|
| 127 |
+ |
|
| 128 |
+// Type returns the type of this option |
|
| 129 |
+func (m *MountOpt) Type() string {
|
|
| 130 |
+ return "mount" |
|
| 131 |
+} |
|
| 132 |
+ |
|
| 133 |
+// String returns a string repr of this option |
|
| 134 |
+func (m *MountOpt) String() string {
|
|
| 135 |
+ mounts := []string{}
|
|
| 136 |
+ for _, mount := range m.values {
|
|
| 137 |
+ repr := fmt.Sprintf("%s %s %s", mount.Type, mount.Source, mount.Target)
|
|
| 138 |
+ mounts = append(mounts, repr) |
|
| 139 |
+ } |
|
| 140 |
+ return strings.Join(mounts, ", ") |
|
| 141 |
+} |
|
| 142 |
+ |
|
| 143 |
+// Value returns the mounts |
|
| 144 |
+func (m *MountOpt) Value() []mounttypes.Mount {
|
|
| 145 |
+ return m.values |
|
| 146 |
+} |
| 0 | 147 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,153 @@ |
| 0 |
+package opts |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "testing" |
|
| 4 |
+ |
|
| 5 |
+ mounttypes "github.com/docker/docker/api/types/mount" |
|
| 6 |
+ "github.com/docker/docker/pkg/testutil/assert" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+func TestMountOptString(t *testing.T) {
|
|
| 10 |
+ mount := MountOpt{
|
|
| 11 |
+ values: []mounttypes.Mount{
|
|
| 12 |
+ {
|
|
| 13 |
+ Type: mounttypes.TypeBind, |
|
| 14 |
+ Source: "/home/path", |
|
| 15 |
+ Target: "/target", |
|
| 16 |
+ }, |
|
| 17 |
+ {
|
|
| 18 |
+ Type: mounttypes.TypeVolume, |
|
| 19 |
+ Source: "foo", |
|
| 20 |
+ Target: "/target/foo", |
|
| 21 |
+ }, |
|
| 22 |
+ }, |
|
| 23 |
+ } |
|
| 24 |
+ expected := "bind /home/path /target, volume foo /target/foo" |
|
| 25 |
+ assert.Equal(t, mount.String(), expected) |
|
| 26 |
+} |
|
| 27 |
+ |
|
| 28 |
+func TestMountOptSetBindNoErrorBind(t *testing.T) {
|
|
| 29 |
+ for _, testcase := range []string{
|
|
| 30 |
+ // tests several aliases that should have same result. |
|
| 31 |
+ "type=bind,target=/target,source=/source", |
|
| 32 |
+ "type=bind,src=/source,dst=/target", |
|
| 33 |
+ "type=bind,source=/source,dst=/target", |
|
| 34 |
+ "type=bind,src=/source,target=/target", |
|
| 35 |
+ } {
|
|
| 36 |
+ var mount MountOpt |
|
| 37 |
+ |
|
| 38 |
+ assert.NilError(t, mount.Set(testcase)) |
|
| 39 |
+ |
|
| 40 |
+ mounts := mount.Value() |
|
| 41 |
+ assert.Equal(t, len(mounts), 1) |
|
| 42 |
+ assert.Equal(t, mounts[0], mounttypes.Mount{
|
|
| 43 |
+ Type: mounttypes.TypeBind, |
|
| 44 |
+ Source: "/source", |
|
| 45 |
+ Target: "/target", |
|
| 46 |
+ }) |
|
| 47 |
+ } |
|
| 48 |
+} |
|
| 49 |
+ |
|
| 50 |
+func TestMountOptSetVolumeNoError(t *testing.T) {
|
|
| 51 |
+ for _, testcase := range []string{
|
|
| 52 |
+ // tests several aliases that should have same result. |
|
| 53 |
+ "type=volume,target=/target,source=/source", |
|
| 54 |
+ "type=volume,src=/source,dst=/target", |
|
| 55 |
+ "type=volume,source=/source,dst=/target", |
|
| 56 |
+ "type=volume,src=/source,target=/target", |
|
| 57 |
+ } {
|
|
| 58 |
+ var mount MountOpt |
|
| 59 |
+ |
|
| 60 |
+ assert.NilError(t, mount.Set(testcase)) |
|
| 61 |
+ |
|
| 62 |
+ mounts := mount.Value() |
|
| 63 |
+ assert.Equal(t, len(mounts), 1) |
|
| 64 |
+ assert.Equal(t, mounts[0], mounttypes.Mount{
|
|
| 65 |
+ Type: mounttypes.TypeVolume, |
|
| 66 |
+ Source: "/source", |
|
| 67 |
+ Target: "/target", |
|
| 68 |
+ }) |
|
| 69 |
+ } |
|
| 70 |
+} |
|
| 71 |
+ |
|
| 72 |
+// TestMountOptDefaultType ensures that a mount without the type defaults to a |
|
| 73 |
+// volume mount. |
|
| 74 |
+func TestMountOptDefaultType(t *testing.T) {
|
|
| 75 |
+ var mount MountOpt |
|
| 76 |
+ assert.NilError(t, mount.Set("target=/target,source=/foo"))
|
|
| 77 |
+ assert.Equal(t, mount.values[0].Type, mounttypes.TypeVolume) |
|
| 78 |
+} |
|
| 79 |
+ |
|
| 80 |
+func TestMountOptSetErrorNoTarget(t *testing.T) {
|
|
| 81 |
+ var mount MountOpt |
|
| 82 |
+ assert.Error(t, mount.Set("type=volume,source=/foo"), "target is required")
|
|
| 83 |
+} |
|
| 84 |
+ |
|
| 85 |
+func TestMountOptSetErrorInvalidKey(t *testing.T) {
|
|
| 86 |
+ var mount MountOpt |
|
| 87 |
+ assert.Error(t, mount.Set("type=volume,bogus=foo"), "unexpected key 'bogus'")
|
|
| 88 |
+} |
|
| 89 |
+ |
|
| 90 |
+func TestMountOptSetErrorInvalidField(t *testing.T) {
|
|
| 91 |
+ var mount MountOpt |
|
| 92 |
+ assert.Error(t, mount.Set("type=volume,bogus"), "invalid field 'bogus'")
|
|
| 93 |
+} |
|
| 94 |
+ |
|
| 95 |
+func TestMountOptSetErrorInvalidReadOnly(t *testing.T) {
|
|
| 96 |
+ var mount MountOpt |
|
| 97 |
+ assert.Error(t, mount.Set("type=volume,readonly=no"), "invalid value for readonly: no")
|
|
| 98 |
+ assert.Error(t, mount.Set("type=volume,readonly=invalid"), "invalid value for readonly: invalid")
|
|
| 99 |
+} |
|
| 100 |
+ |
|
| 101 |
+func TestMountOptDefaultEnableReadOnly(t *testing.T) {
|
|
| 102 |
+ var m MountOpt |
|
| 103 |
+ assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo"))
|
|
| 104 |
+ assert.Equal(t, m.values[0].ReadOnly, false) |
|
| 105 |
+ |
|
| 106 |
+ m = MountOpt{}
|
|
| 107 |
+ assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo,readonly"))
|
|
| 108 |
+ assert.Equal(t, m.values[0].ReadOnly, true) |
|
| 109 |
+ |
|
| 110 |
+ m = MountOpt{}
|
|
| 111 |
+ assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo,readonly=1"))
|
|
| 112 |
+ assert.Equal(t, m.values[0].ReadOnly, true) |
|
| 113 |
+ |
|
| 114 |
+ m = MountOpt{}
|
|
| 115 |
+ assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo,readonly=true"))
|
|
| 116 |
+ assert.Equal(t, m.values[0].ReadOnly, true) |
|
| 117 |
+ |
|
| 118 |
+ m = MountOpt{}
|
|
| 119 |
+ assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo,readonly=0"))
|
|
| 120 |
+ assert.Equal(t, m.values[0].ReadOnly, false) |
|
| 121 |
+} |
|
| 122 |
+ |
|
| 123 |
+func TestMountOptVolumeNoCopy(t *testing.T) {
|
|
| 124 |
+ var m MountOpt |
|
| 125 |
+ assert.NilError(t, m.Set("type=volume,target=/foo,volume-nocopy"))
|
|
| 126 |
+ assert.Equal(t, m.values[0].Source, "") |
|
| 127 |
+ |
|
| 128 |
+ m = MountOpt{}
|
|
| 129 |
+ assert.NilError(t, m.Set("type=volume,target=/foo,source=foo"))
|
|
| 130 |
+ assert.Equal(t, m.values[0].VolumeOptions == nil, true) |
|
| 131 |
+ |
|
| 132 |
+ m = MountOpt{}
|
|
| 133 |
+ assert.NilError(t, m.Set("type=volume,target=/foo,source=foo,volume-nocopy=true"))
|
|
| 134 |
+ assert.Equal(t, m.values[0].VolumeOptions != nil, true) |
|
| 135 |
+ assert.Equal(t, m.values[0].VolumeOptions.NoCopy, true) |
|
| 136 |
+ |
|
| 137 |
+ m = MountOpt{}
|
|
| 138 |
+ assert.NilError(t, m.Set("type=volume,target=/foo,source=foo,volume-nocopy"))
|
|
| 139 |
+ assert.Equal(t, m.values[0].VolumeOptions != nil, true) |
|
| 140 |
+ assert.Equal(t, m.values[0].VolumeOptions.NoCopy, true) |
|
| 141 |
+ |
|
| 142 |
+ m = MountOpt{}
|
|
| 143 |
+ assert.NilError(t, m.Set("type=volume,target=/foo,source=foo,volume-nocopy=1"))
|
|
| 144 |
+ assert.Equal(t, m.values[0].VolumeOptions != nil, true) |
|
| 145 |
+ assert.Equal(t, m.values[0].VolumeOptions.NoCopy, true) |
|
| 146 |
+} |
|
| 147 |
+ |
|
| 148 |
+func TestMountOptTypeConflict(t *testing.T) {
|
|
| 149 |
+ var m MountOpt |
|
| 150 |
+ assert.Error(t, m.Set("type=bind,target=/foo,source=/foo,volume-nocopy=true"), "cannot mix")
|
|
| 151 |
+ assert.Error(t, m.Set("type=volume,target=/foo,source=/foo,bind-propagation=rprivate"), "cannot mix")
|
|
| 152 |
+} |
| ... | ... |
@@ -26,6 +26,7 @@ type ContainerOptions struct {
|
| 26 | 26 |
attach opts.ListOpts |
| 27 | 27 |
volumes opts.ListOpts |
| 28 | 28 |
tmpfs opts.ListOpts |
| 29 |
+ mounts opts.MountOpt |
|
| 29 | 30 |
blkioWeightDevice WeightdeviceOpt |
| 30 | 31 |
deviceReadBps ThrottledeviceOpt |
| 31 | 32 |
deviceWriteBps ThrottledeviceOpt |
| ... | ... |
@@ -210,6 +211,7 @@ func AddFlags(flags *pflag.FlagSet) *ContainerOptions {
|
| 210 | 210 |
flags.Var(&copts.tmpfs, "tmpfs", "Mount a tmpfs directory") |
| 211 | 211 |
flags.Var(&copts.volumesFrom, "volumes-from", "Mount volumes from the specified container(s)") |
| 212 | 212 |
flags.VarP(&copts.volumes, "volume", "v", "Bind mount a volume") |
| 213 |
+ flags.Var(&copts.mounts, "mount", "Attach a filesystem mount to the container") |
|
| 213 | 214 |
|
| 214 | 215 |
// Health-checking |
| 215 | 216 |
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 |
| 347 | 347 |
} |
| 348 | 348 |
} |
| 349 | 349 |
|
| 350 |
+ mounts := copts.mounts.Value() |
|
| 351 |
+ |
|
| 350 | 352 |
var binds []string |
| 351 | 353 |
volumes := copts.volumes.GetMap() |
| 352 | 354 |
// add any bind targets to the list of container volumes |
| ... | ... |
@@ -612,6 +616,7 @@ func Parse(flags *pflag.FlagSet, copts *ContainerOptions) (*container.Config, *c |
| 612 | 612 |
Tmpfs: tmpfs, |
| 613 | 613 |
Sysctls: copts.sysctls.GetAll(), |
| 614 | 614 |
Runtime: copts.runtime, |
| 615 |
+ Mounts: mounts, |
|
| 615 | 616 |
} |
| 616 | 617 |
|
| 617 | 618 |
// only set this value if the user provided the flag, else it should default to nil |