Browse code

Merge pull request #30597 from dnephin/add-expanded-mount-format-to-stack-deploy

Add expanded mount format to stack deploy

Justin Cormack authored on 2017/03/15 02:53:28
Showing 11 changed files
... ...
@@ -1,21 +1,19 @@
1 1
 package convert
2 2
 
3 3
 import (
4
-	"fmt"
5
-	"strings"
6
-
7 4
 	"github.com/docker/docker/api/types/mount"
8 5
 	composetypes "github.com/docker/docker/cli/compose/types"
6
+	"github.com/pkg/errors"
9 7
 )
10 8
 
11 9
 type volumes map[string]composetypes.VolumeConfig
12 10
 
13 11
 // Volumes from compose-file types to engine api types
14
-func Volumes(serviceVolumes []string, stackVolumes volumes, namespace Namespace) ([]mount.Mount, error) {
12
+func Volumes(serviceVolumes []composetypes.ServiceVolumeConfig, stackVolumes volumes, namespace Namespace) ([]mount.Mount, error) {
15 13
 	var mounts []mount.Mount
16 14
 
17
-	for _, volumeSpec := range serviceVolumes {
18
-		mount, err := convertVolumeToMount(volumeSpec, stackVolumes, namespace)
15
+	for _, volumeConfig := range serviceVolumes {
16
+		mount, err := convertVolumeToMount(volumeConfig, stackVolumes, namespace)
19 17
 		if err != nil {
20 18
 			return nil, err
21 19
 		}
... ...
@@ -24,108 +22,65 @@ func Volumes(serviceVolumes []string, stackVolumes volumes, namespace Namespace)
24 24
 	return mounts, nil
25 25
 }
26 26
 
27
-func convertVolumeToMount(volumeSpec string, stackVolumes volumes, namespace Namespace) (mount.Mount, error) {
28
-	var source, target string
29
-	var mode []string
30
-
31
-	// TODO: split Windows path mappings properly
32
-	parts := strings.SplitN(volumeSpec, ":", 3)
33
-
34
-	for _, part := range parts {
35
-		if strings.TrimSpace(part) == "" {
36
-			return mount.Mount{}, fmt.Errorf("invalid volume: %s", volumeSpec)
37
-		}
27
+func convertVolumeToMount(
28
+	volume composetypes.ServiceVolumeConfig,
29
+	stackVolumes volumes,
30
+	namespace Namespace,
31
+) (mount.Mount, error) {
32
+	result := mount.Mount{
33
+		Type:     mount.Type(volume.Type),
34
+		Source:   volume.Source,
35
+		Target:   volume.Target,
36
+		ReadOnly: volume.ReadOnly,
38 37
 	}
39 38
 
40
-	switch len(parts) {
41
-	case 3:
42
-		source = parts[0]
43
-		target = parts[1]
44
-		mode = strings.Split(parts[2], ",")
45
-	case 2:
46
-		source = parts[0]
47
-		target = parts[1]
48
-	case 1:
49
-		target = parts[0]
39
+	// Anonymous volumes
40
+	if volume.Source == "" {
41
+		return result, nil
50 42
 	}
51
-
52
-	if source == "" {
53
-		// Anonymous volume
54
-		return mount.Mount{
55
-			Type:   mount.TypeVolume,
56
-			Target: target,
57
-		}, nil
43
+	if volume.Type == "volume" && volume.Bind != nil {
44
+		return result, errors.New("bind options are incompatible with type volume")
45
+	}
46
+	if volume.Type == "bind" && volume.Volume != nil {
47
+		return result, errors.New("volume options are incompatible with type bind")
58 48
 	}
59 49
 
60
-	// TODO: catch Windows paths here
61
-	if strings.HasPrefix(source, "/") {
62
-		return mount.Mount{
63
-			Type:        mount.TypeBind,
64
-			Source:      source,
65
-			Target:      target,
66
-			ReadOnly:    isReadOnly(mode),
67
-			BindOptions: getBindOptions(mode),
68
-		}, nil
50
+	if volume.Bind != nil {
51
+		result.BindOptions = &mount.BindOptions{
52
+			Propagation: mount.Propagation(volume.Bind.Propagation),
53
+		}
54
+	}
55
+	// Binds volumes
56
+	if volume.Type == "bind" {
57
+		return result, nil
69 58
 	}
70 59
 
71
-	stackVolume, exists := stackVolumes[source]
60
+	stackVolume, exists := stackVolumes[volume.Source]
72 61
 	if !exists {
73
-		return mount.Mount{}, fmt.Errorf("undefined volume: %s", source)
62
+		return result, errors.Errorf("undefined volume: %s", volume.Source)
74 63
 	}
75 64
 
76
-	var volumeOptions *mount.VolumeOptions
77
-	if stackVolume.External.Name != "" {
78
-		volumeOptions = &mount.VolumeOptions{
79
-			NoCopy: isNoCopy(mode),
80
-		}
81
-		source = stackVolume.External.Name
82
-	} else {
83
-		volumeOptions = &mount.VolumeOptions{
84
-			Labels: AddStackLabel(namespace, stackVolume.Labels),
85
-			NoCopy: isNoCopy(mode),
86
-		}
65
+	result.Source = namespace.Scope(volume.Source)
66
+	result.VolumeOptions = &mount.VolumeOptions{}
87 67
 
88
-		if stackVolume.Driver != "" {
89
-			volumeOptions.DriverConfig = &mount.Driver{
90
-				Name:    stackVolume.Driver,
91
-				Options: stackVolume.DriverOpts,
92
-			}
93
-		}
94
-		source = namespace.Scope(source)
68
+	if volume.Volume != nil {
69
+		result.VolumeOptions.NoCopy = volume.Volume.NoCopy
95 70
 	}
96
-	return mount.Mount{
97
-		Type:          mount.TypeVolume,
98
-		Source:        source,
99
-		Target:        target,
100
-		ReadOnly:      isReadOnly(mode),
101
-		VolumeOptions: volumeOptions,
102
-	}, nil
103
-}
104 71
 
105
-func modeHas(mode []string, field string) bool {
106
-	for _, item := range mode {
107
-		if item == field {
108
-			return true
109
-		}
72
+	// External named volumes
73
+	if stackVolume.External.External {
74
+		result.Source = stackVolume.External.Name
75
+		return result, nil
110 76
 	}
111
-	return false
112
-}
113 77
 
114
-func isReadOnly(mode []string) bool {
115
-	return modeHas(mode, "ro")
116
-}
117
-
118
-func isNoCopy(mode []string) bool {
119
-	return modeHas(mode, "nocopy")
120
-}
121
-
122
-func getBindOptions(mode []string) *mount.BindOptions {
123
-	for _, item := range mode {
124
-		for _, propagation := range mount.Propagations {
125
-			if mount.Propagation(item) == propagation {
126
-				return &mount.BindOptions{Propagation: mount.Propagation(item)}
127
-			}
78
+	result.VolumeOptions.Labels = AddStackLabel(namespace, stackVolume.Labels)
79
+	if stackVolume.Driver != "" || stackVolume.DriverOpts != nil {
80
+		result.VolumeOptions.DriverConfig = &mount.Driver{
81
+			Name:    stackVolume.Driver,
82
+			Options: stackVolume.DriverOpts,
128 83
 		}
129 84
 	}
130
-	return nil
85
+
86
+	// Named volumes
87
+	return result, nil
131 88
 }
... ...
@@ -8,51 +8,48 @@ import (
8 8
 	"github.com/docker/docker/pkg/testutil/assert"
9 9
 )
10 10
 
11
-func TestIsReadOnly(t *testing.T) {
12
-	assert.Equal(t, isReadOnly([]string{"foo", "bar", "ro"}), true)
13
-	assert.Equal(t, isReadOnly([]string{"ro"}), true)
14
-	assert.Equal(t, isReadOnly([]string{}), false)
15
-	assert.Equal(t, isReadOnly([]string{"foo", "rw"}), false)
16
-	assert.Equal(t, isReadOnly([]string{"foo"}), false)
17
-}
18
-
19
-func TestIsNoCopy(t *testing.T) {
20
-	assert.Equal(t, isNoCopy([]string{"foo", "bar", "nocopy"}), true)
21
-	assert.Equal(t, isNoCopy([]string{"nocopy"}), true)
22
-	assert.Equal(t, isNoCopy([]string{}), false)
23
-	assert.Equal(t, isNoCopy([]string{"foo", "rw"}), false)
24
-}
25
-
26
-func TestGetBindOptions(t *testing.T) {
27
-	opts := getBindOptions([]string{"slave"})
28
-	expected := mount.BindOptions{Propagation: mount.PropagationSlave}
29
-	assert.Equal(t, *opts, expected)
30
-}
31
-
32
-func TestGetBindOptionsNone(t *testing.T) {
33
-	opts := getBindOptions([]string{"ro"})
34
-	assert.Equal(t, opts, (*mount.BindOptions)(nil))
35
-}
36
-
37 11
 func TestConvertVolumeToMountAnonymousVolume(t *testing.T) {
38
-	stackVolumes := volumes{}
39
-	namespace := NewNamespace("foo")
12
+	config := composetypes.ServiceVolumeConfig{
13
+		Type:   "volume",
14
+		Target: "/foo/bar",
15
+	}
40 16
 	expected := mount.Mount{
41 17
 		Type:   mount.TypeVolume,
42 18
 		Target: "/foo/bar",
43 19
 	}
44
-	mount, err := convertVolumeToMount("/foo/bar", stackVolumes, namespace)
20
+	mount, err := convertVolumeToMount(config, volumes{}, NewNamespace("foo"))
45 21
 	assert.NilError(t, err)
46 22
 	assert.DeepEqual(t, mount, expected)
47 23
 }
48 24
 
49
-func TestConvertVolumeToMountInvalidFormat(t *testing.T) {
25
+func TestConvertVolumeToMountConflictingOptionsBind(t *testing.T) {
50 26
 	namespace := NewNamespace("foo")
51
-	invalids := []string{"::", "::cc", ":bb:", "aa::", "aa::cc", "aa:bb:", " : : ", " : :cc", " :bb: ", "aa: : ", "aa: :cc", "aa:bb: "}
52
-	for _, vol := range invalids {
53
-		_, err := convertVolumeToMount(vol, volumes{}, namespace)
54
-		assert.Error(t, err, "invalid volume: "+vol)
27
+
28
+	config := composetypes.ServiceVolumeConfig{
29
+		Type:   "volume",
30
+		Source: "foo",
31
+		Target: "/target",
32
+		Bind: &composetypes.ServiceVolumeBind{
33
+			Propagation: "slave",
34
+		},
55 35
 	}
36
+	_, err := convertVolumeToMount(config, volumes{}, namespace)
37
+	assert.Error(t, err, "bind options are incompatible")
38
+}
39
+
40
+func TestConvertVolumeToMountConflictingOptionsVolume(t *testing.T) {
41
+	namespace := NewNamespace("foo")
42
+
43
+	config := composetypes.ServiceVolumeConfig{
44
+		Type:   "bind",
45
+		Source: "/foo",
46
+		Target: "/target",
47
+		Volume: &composetypes.ServiceVolumeVolume{
48
+			NoCopy: true,
49
+		},
50
+	}
51
+	_, err := convertVolumeToMount(config, volumes{}, namespace)
52
+	assert.Error(t, err, "volume options are incompatible")
56 53
 }
57 54
 
58 55
 func TestConvertVolumeToMountNamedVolume(t *testing.T) {
... ...
@@ -84,9 +81,19 @@ func TestConvertVolumeToMountNamedVolume(t *testing.T) {
84 84
 					"opt": "value",
85 85
 				},
86 86
 			},
87
+			NoCopy: true,
88
+		},
89
+	}
90
+	config := composetypes.ServiceVolumeConfig{
91
+		Type:     "volume",
92
+		Source:   "normal",
93
+		Target:   "/foo",
94
+		ReadOnly: true,
95
+		Volume: &composetypes.ServiceVolumeVolume{
96
+			NoCopy: true,
87 97
 		},
88 98
 	}
89
-	mount, err := convertVolumeToMount("normal:/foo:ro", stackVolumes, namespace)
99
+	mount, err := convertVolumeToMount(config, stackVolumes, namespace)
90 100
 	assert.NilError(t, err)
91 101
 	assert.DeepEqual(t, mount, expected)
92 102
 }
... ...
@@ -109,7 +116,12 @@ func TestConvertVolumeToMountNamedVolumeExternal(t *testing.T) {
109 109
 			NoCopy: false,
110 110
 		},
111 111
 	}
112
-	mount, err := convertVolumeToMount("outside:/foo", stackVolumes, namespace)
112
+	config := composetypes.ServiceVolumeConfig{
113
+		Type:   "volume",
114
+		Source: "outside",
115
+		Target: "/foo",
116
+	}
117
+	mount, err := convertVolumeToMount(config, stackVolumes, namespace)
113 118
 	assert.NilError(t, err)
114 119
 	assert.DeepEqual(t, mount, expected)
115 120
 }
... ...
@@ -132,7 +144,15 @@ func TestConvertVolumeToMountNamedVolumeExternalNoCopy(t *testing.T) {
132 132
 			NoCopy: true,
133 133
 		},
134 134
 	}
135
-	mount, err := convertVolumeToMount("outside:/foo:nocopy", stackVolumes, namespace)
135
+	config := composetypes.ServiceVolumeConfig{
136
+		Type:   "volume",
137
+		Source: "outside",
138
+		Target: "/foo",
139
+		Volume: &composetypes.ServiceVolumeVolume{
140
+			NoCopy: true,
141
+		},
142
+	}
143
+	mount, err := convertVolumeToMount(config, stackVolumes, namespace)
136 144
 	assert.NilError(t, err)
137 145
 	assert.DeepEqual(t, mount, expected)
138 146
 }
... ...
@@ -147,13 +167,26 @@ func TestConvertVolumeToMountBind(t *testing.T) {
147 147
 		ReadOnly:    true,
148 148
 		BindOptions: &mount.BindOptions{Propagation: mount.PropagationShared},
149 149
 	}
150
-	mount, err := convertVolumeToMount("/bar:/foo:ro,shared", stackVolumes, namespace)
150
+	config := composetypes.ServiceVolumeConfig{
151
+		Type:     "bind",
152
+		Source:   "/bar",
153
+		Target:   "/foo",
154
+		ReadOnly: true,
155
+		Bind:     &composetypes.ServiceVolumeBind{Propagation: "shared"},
156
+	}
157
+	mount, err := convertVolumeToMount(config, stackVolumes, namespace)
151 158
 	assert.NilError(t, err)
152 159
 	assert.DeepEqual(t, mount, expected)
153 160
 }
154 161
 
155 162
 func TestConvertVolumeToMountVolumeDoesNotExist(t *testing.T) {
156 163
 	namespace := NewNamespace("foo")
157
-	_, err := convertVolumeToMount("unknown:/foo:ro", volumes{}, namespace)
164
+	config := composetypes.ServiceVolumeConfig{
165
+		Type:     "volume",
166
+		Source:   "unknown",
167
+		Target:   "/foo",
168
+		ReadOnly: true,
169
+	}
170
+	_, err := convertVolumeToMount(config, volumes{}, namespace)
158 171
 	assert.Error(t, err, "undefined volume: unknown")
159 172
 }
... ...
@@ -251,6 +251,8 @@ func transformHook(
251 251
 		return transformMappingOrList(data, "="), nil
252 252
 	case reflect.TypeOf(types.MappingWithColon{}):
253 253
 		return transformMappingOrList(data, ":"), nil
254
+	case reflect.TypeOf(types.ServiceVolumeConfig{}):
255
+		return transformServiceVolumeConfig(data)
254 256
 	}
255 257
 	return data, nil
256 258
 }
... ...
@@ -333,10 +335,7 @@ func LoadService(name string, serviceDict types.Dict, workingDir string) (*types
333 333
 		return nil, err
334 334
 	}
335 335
 
336
-	if err := resolveVolumePaths(serviceConfig.Volumes, workingDir); err != nil {
337
-		return nil, err
338
-	}
339
-
336
+	resolveVolumePaths(serviceConfig.Volumes, workingDir)
340 337
 	return serviceConfig, nil
341 338
 }
342 339
 
... ...
@@ -369,22 +368,15 @@ func resolveEnvironment(serviceConfig *types.ServiceConfig, workingDir string) e
369 369
 	return nil
370 370
 }
371 371
 
372
-func resolveVolumePaths(volumes []string, workingDir string) error {
373
-	for i, mapping := range volumes {
374
-		parts := strings.SplitN(mapping, ":", 2)
375
-		if len(parts) == 1 {
372
+func resolveVolumePaths(volumes []types.ServiceVolumeConfig, workingDir string) {
373
+	for i, volume := range volumes {
374
+		if volume.Type != "bind" {
376 375
 			continue
377 376
 		}
378 377
 
379
-		if strings.HasPrefix(parts[0], ".") {
380
-			parts[0] = absPath(workingDir, parts[0])
381
-		}
382
-		parts[0] = expandUser(parts[0])
383
-
384
-		volumes[i] = strings.Join(parts, ":")
378
+		volume.Source = absPath(workingDir, expandUser(volume.Source))
379
+		volumes[i] = volume
385 380
 	}
386
-
387
-	return nil
388 381
 }
389 382
 
390 383
 // TODO: make this more robust
... ...
@@ -555,6 +547,20 @@ func transformServiceSecret(data interface{}) (interface{}, error) {
555 555
 	}
556 556
 }
557 557
 
558
+func transformServiceVolumeConfig(data interface{}) (interface{}, error) {
559
+	switch value := data.(type) {
560
+	case string:
561
+		return parseVolume(value)
562
+	case types.Dict:
563
+		return data, nil
564
+	case map[string]interface{}:
565
+		return data, nil
566
+	default:
567
+		return data, fmt.Errorf("invalid type %T for service volume", value)
568
+	}
569
+
570
+}
571
+
558 572
 func transformServiceNetworkMap(value interface{}) (interface{}, error) {
559 573
 	if list, ok := value.([]interface{}); ok {
560 574
 		mapValue := map[interface{}]interface{}{}
... ...
@@ -881,13 +881,13 @@ func TestFullExample(t *testing.T) {
881 881
 			},
882 882
 		},
883 883
 		User: "someone",
884
-		Volumes: []string{
885
-			"/var/lib/mysql",
886
-			"/opt/data:/var/lib/mysql",
887
-			fmt.Sprintf("%s:/code", workingDir),
888
-			fmt.Sprintf("%s/static:/var/www/html", workingDir),
889
-			fmt.Sprintf("%s/configs:/etc/configs/:ro", homeDir),
890
-			"datavolume:/var/lib/mysql",
884
+		Volumes: []types.ServiceVolumeConfig{
885
+			{Target: "/var/lib/mysql", Type: "volume"},
886
+			{Source: "/opt/data", Target: "/var/lib/mysql", Type: "bind"},
887
+			{Source: workingDir, Target: "/code", Type: "bind"},
888
+			{Source: workingDir + "/static", Target: "/var/www/html", Type: "bind"},
889
+			{Source: homeDir + "/configs", Target: "/etc/configs/", Type: "bind", ReadOnly: true},
890
+			{Source: "datavolume", Target: "/var/lib/mysql", Type: "volume"},
891 891
 		},
892 892
 		WorkingDir: "/code",
893 893
 	}
... ...
@@ -1085,3 +1085,31 @@ services:
1085 1085
 	assert.Equal(t, 1, len(config.Services))
1086 1086
 	assert.Equal(t, expected, config.Services[0].Ports)
1087 1087
 }
1088
+
1089
+func TestLoadExpandedMountFormat(t *testing.T) {
1090
+	config, err := loadYAML(`
1091
+version: "3.1"
1092
+services:
1093
+  web:
1094
+    image: busybox
1095
+    volumes:
1096
+      - type: volume
1097
+        source: foo
1098
+        target: /target
1099
+        read_only: true
1100
+volumes:
1101
+  foo: {}
1102
+`)
1103
+	assert.NoError(t, err)
1104
+
1105
+	expected := types.ServiceVolumeConfig{
1106
+		Type:     "volume",
1107
+		Source:   "foo",
1108
+		Target:   "/target",
1109
+		ReadOnly: true,
1110
+	}
1111
+
1112
+	assert.Equal(t, 1, len(config.Services))
1113
+	assert.Equal(t, 1, len(config.Services[0].Volumes))
1114
+	assert.Equal(t, expected, config.Services[0].Volumes[0])
1115
+}
1088 1116
new file mode 100644
... ...
@@ -0,0 +1,119 @@
0
+package loader
1
+
2
+import (
3
+	"strings"
4
+	"unicode"
5
+	"unicode/utf8"
6
+
7
+	"github.com/docker/docker/api/types/mount"
8
+	"github.com/docker/docker/cli/compose/types"
9
+	"github.com/pkg/errors"
10
+)
11
+
12
+func parseVolume(spec string) (types.ServiceVolumeConfig, error) {
13
+	volume := types.ServiceVolumeConfig{}
14
+
15
+	switch len(spec) {
16
+	case 0:
17
+		return volume, errors.New("invalid empty volume spec")
18
+	case 1, 2:
19
+		volume.Target = spec
20
+		volume.Type = string(mount.TypeVolume)
21
+		return volume, nil
22
+	}
23
+
24
+	buffer := []rune{}
25
+	for _, char := range spec {
26
+		switch {
27
+		case isWindowsDrive(char, buffer, volume):
28
+			buffer = append(buffer, char)
29
+		case char == ':':
30
+			if err := populateFieldFromBuffer(char, buffer, &volume); err != nil {
31
+				return volume, errors.Wrapf(err, "invalid spec: %s", spec)
32
+			}
33
+			buffer = []rune{}
34
+		default:
35
+			buffer = append(buffer, char)
36
+		}
37
+	}
38
+
39
+	if err := populateFieldFromBuffer(rune(0), buffer, &volume); err != nil {
40
+		return volume, errors.Wrapf(err, "invalid spec: %s", spec)
41
+	}
42
+	populateType(&volume)
43
+	return volume, nil
44
+}
45
+
46
+func isWindowsDrive(char rune, buffer []rune, volume types.ServiceVolumeConfig) bool {
47
+	return char == ':' && len(buffer) == 1 && unicode.IsLetter(buffer[0])
48
+}
49
+
50
+func populateFieldFromBuffer(char rune, buffer []rune, volume *types.ServiceVolumeConfig) error {
51
+	strBuffer := string(buffer)
52
+	switch {
53
+	case len(buffer) == 0:
54
+		return errors.New("empty section between colons")
55
+	// Anonymous volume
56
+	case volume.Source == "" && char == rune(0):
57
+		volume.Target = strBuffer
58
+		return nil
59
+	case volume.Source == "":
60
+		volume.Source = strBuffer
61
+		return nil
62
+	case volume.Target == "":
63
+		volume.Target = strBuffer
64
+		return nil
65
+	case char == ':':
66
+		return errors.New("too many colons")
67
+	}
68
+	for _, option := range strings.Split(strBuffer, ",") {
69
+		switch option {
70
+		case "ro":
71
+			volume.ReadOnly = true
72
+		case "nocopy":
73
+			volume.Volume = &types.ServiceVolumeVolume{NoCopy: true}
74
+		default:
75
+			if isBindOption(option) {
76
+				volume.Bind = &types.ServiceVolumeBind{Propagation: option}
77
+			} else {
78
+				return errors.Errorf("unknown option: %s", option)
79
+			}
80
+		}
81
+	}
82
+	return nil
83
+}
84
+
85
+func isBindOption(option string) bool {
86
+	for _, propagation := range mount.Propagations {
87
+		if mount.Propagation(option) == propagation {
88
+			return true
89
+		}
90
+	}
91
+	return false
92
+}
93
+
94
+func populateType(volume *types.ServiceVolumeConfig) {
95
+	switch {
96
+	// Anonymous volume
97
+	case volume.Source == "":
98
+		volume.Type = string(mount.TypeVolume)
99
+	case isFilePath(volume.Source):
100
+		volume.Type = string(mount.TypeBind)
101
+	default:
102
+		volume.Type = string(mount.TypeVolume)
103
+	}
104
+}
105
+
106
+func isFilePath(source string) bool {
107
+	switch source[0] {
108
+	case '.', '/', '~':
109
+		return true
110
+	}
111
+
112
+	// Windows absolute path
113
+	first, next := utf8.DecodeRuneInString(source)
114
+	if unicode.IsLetter(first) && source[next] == ':' {
115
+		return true
116
+	}
117
+	return false
118
+}
0 119
new file mode 100644
... ...
@@ -0,0 +1,134 @@
0
+package loader
1
+
2
+import (
3
+	"testing"
4
+
5
+	"github.com/docker/docker/cli/compose/types"
6
+	"github.com/docker/docker/pkg/testutil/assert"
7
+)
8
+
9
+func TestParseVolumeAnonymousVolume(t *testing.T) {
10
+	for _, path := range []string{"/path", "/path/foo"} {
11
+		volume, err := parseVolume(path)
12
+		expected := types.ServiceVolumeConfig{Type: "volume", Target: path}
13
+		assert.NilError(t, err)
14
+		assert.DeepEqual(t, volume, expected)
15
+	}
16
+}
17
+
18
+func TestParseVolumeAnonymousVolumeWindows(t *testing.T) {
19
+	for _, path := range []string{"C:\\path", "Z:\\path\\foo"} {
20
+		volume, err := parseVolume(path)
21
+		expected := types.ServiceVolumeConfig{Type: "volume", Target: path}
22
+		assert.NilError(t, err)
23
+		assert.DeepEqual(t, volume, expected)
24
+	}
25
+}
26
+
27
+func TestParseVolumeTooManyColons(t *testing.T) {
28
+	_, err := parseVolume("/foo:/foo:ro:foo")
29
+	assert.Error(t, err, "too many colons")
30
+}
31
+
32
+func TestParseVolumeShortVolumes(t *testing.T) {
33
+	for _, path := range []string{".", "/a"} {
34
+		volume, err := parseVolume(path)
35
+		expected := types.ServiceVolumeConfig{Type: "volume", Target: path}
36
+		assert.NilError(t, err)
37
+		assert.DeepEqual(t, volume, expected)
38
+	}
39
+}
40
+
41
+func TestParseVolumeMissingSource(t *testing.T) {
42
+	for _, spec := range []string{":foo", "/foo::ro"} {
43
+		_, err := parseVolume(spec)
44
+		assert.Error(t, err, "empty section between colons")
45
+	}
46
+}
47
+
48
+func TestParseVolumeBindMount(t *testing.T) {
49
+	for _, path := range []string{"./foo", "~/thing", "../other", "/foo", "/home/user"} {
50
+		volume, err := parseVolume(path + ":/target")
51
+		expected := types.ServiceVolumeConfig{
52
+			Type:   "bind",
53
+			Source: path,
54
+			Target: "/target",
55
+		}
56
+		assert.NilError(t, err)
57
+		assert.DeepEqual(t, volume, expected)
58
+	}
59
+}
60
+
61
+func TestParseVolumeRelativeBindMountWindows(t *testing.T) {
62
+	for _, path := range []string{
63
+		"./foo",
64
+		"~/thing",
65
+		"../other",
66
+		"D:\\path", "/home/user",
67
+	} {
68
+		volume, err := parseVolume(path + ":d:\\target")
69
+		expected := types.ServiceVolumeConfig{
70
+			Type:   "bind",
71
+			Source: path,
72
+			Target: "d:\\target",
73
+		}
74
+		assert.NilError(t, err)
75
+		assert.DeepEqual(t, volume, expected)
76
+	}
77
+}
78
+
79
+func TestParseVolumeWithBindOptions(t *testing.T) {
80
+	volume, err := parseVolume("/source:/target:slave")
81
+	expected := types.ServiceVolumeConfig{
82
+		Type:   "bind",
83
+		Source: "/source",
84
+		Target: "/target",
85
+		Bind:   &types.ServiceVolumeBind{Propagation: "slave"},
86
+	}
87
+	assert.NilError(t, err)
88
+	assert.DeepEqual(t, volume, expected)
89
+}
90
+
91
+func TestParseVolumeWithBindOptionsWindows(t *testing.T) {
92
+	volume, err := parseVolume("C:\\source\\foo:D:\\target:ro,rprivate")
93
+	expected := types.ServiceVolumeConfig{
94
+		Type:     "bind",
95
+		Source:   "C:\\source\\foo",
96
+		Target:   "D:\\target",
97
+		ReadOnly: true,
98
+		Bind:     &types.ServiceVolumeBind{Propagation: "rprivate"},
99
+	}
100
+	assert.NilError(t, err)
101
+	assert.DeepEqual(t, volume, expected)
102
+}
103
+
104
+func TestParseVolumeWithInvalidVolumeOptions(t *testing.T) {
105
+	_, err := parseVolume("name:/target:bogus")
106
+	assert.Error(t, err, "invalid spec: name:/target:bogus: unknown option: bogus")
107
+}
108
+
109
+func TestParseVolumeWithVolumeOptions(t *testing.T) {
110
+	volume, err := parseVolume("name:/target:nocopy")
111
+	expected := types.ServiceVolumeConfig{
112
+		Type:   "volume",
113
+		Source: "name",
114
+		Target: "/target",
115
+		Volume: &types.ServiceVolumeVolume{NoCopy: true},
116
+	}
117
+	assert.NilError(t, err)
118
+	assert.DeepEqual(t, volume, expected)
119
+}
120
+
121
+func TestParseVolumeWithReadOnly(t *testing.T) {
122
+	for _, path := range []string{"./foo", "/home/user"} {
123
+		volume, err := parseVolume(path + ":/target:ro")
124
+		expected := types.ServiceVolumeConfig{
125
+			Type:     "bind",
126
+			Source:   path,
127
+			Target:   "/target",
128
+			ReadOnly: true,
129
+		}
130
+		assert.NilError(t, err)
131
+		assert.DeepEqual(t, volume, expected)
132
+	}
133
+}
... ...
@@ -89,7 +89,7 @@ func dataConfig_schema_v30Json() (*asset, error) {
89 89
 	return a, nil
90 90
 }
91 91
 
92
-var _dataConfig_schema_v31Json = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xec\x5a\xcd\x8f\xdc\x28\x16\xbf\xd7\x5f\x61\x39\xb9\xa5\x3f\xb2\xda\x68\xa5\xcd\x6d\x8f\x7b\x9a\x39\x4f\xcb\xb1\x28\xfb\x95\x8b\x34\x06\x02\xb8\xd2\x95\xa8\xff\xf7\x11\xfe\x2a\x8c\xc1\xe0\x2e\xf7\x74\x34\x9a\x53\x77\x99\xdf\x03\xde\xf7\xe3\xc1\xcf\x5d\x92\xa4\xef\x65\x71\x84\x1a\xa5\x9f\x93\xf4\xa8\x14\xff\x7c\x7f\xff\x55\x32\x7a\xdb\x7d\xbd\x63\xa2\xba\x2f\x05\x3a\xa8\xdb\x8f\x9f\xee\xbb\x6f\xef\xd2\x1b\x4d\x87\x4b\x4d\x52\x30\x7a\xc0\x55\xde\x8d\xe4\xa7\x7f\xdf\xfd\xeb\x4e\x93\x77\x10\x75\xe6\xa0\x41\x6c\xff\x15\x0a\xd5\x7d\x13\xf0\xad\xc1\x02\x34\xf1\x43\x7a\x02\x21\x31\xa3\x69\x76\xb3\xd3\x63\x5c\x30\x0e\x42\x61\x90\xe9\xe7\x44\x6f\x2e\x49\x46\xc8\xf0\xc1\x98\x56\x2a\x81\x69\x95\xb6\x9f\x9f\xdb\x19\x92\x24\x95\x20\x4e\xb8\x30\x66\x18\xb7\xfa\xee\xfe\x32\xff\xfd\x08\xbb\xb1\x67\x35\x36\xdb\x7e\xe7\x48\x29\x10\xf4\xf7\xf9\xde\xda\xe1\x2f\x0f\xe8\xf6\xc7\xff\x6e\xff\xf8\x78\xfb\xdf\xbb\xfc\x36\xfb\xf0\x7e\x32\xac\xe5\x2b\xe0\xd0\x2d\x5f\xc2\x01\x53\xac\x30\xa3\xe3\xfa\xe9\x88\x7c\xee\xff\x7b\x1e\x17\x46\x65\xd9\x82\x11\x99\xac\x7d\x40\x44\xc2\x94\x67\x0a\xea\x3b\x13\x8f\x21\x9e\x47\xd8\x1b\xf1\xdc\xaf\xef\xe0\x79\xca\xce\x89\x91\xa6\x0e\x6a\x70\x40\xbd\x11\x33\xdd\xf2\xdb\xe8\x4f\x42\x21\x40\x85\x4d\xb6\x43\xbd\x99\xc5\xea\xe5\xaf\x63\x78\x37\x30\xbd\x88\xed\x10\xc6\xda\xed\x06\x27\xee\xed\x12\x95\xcb\xbd\xfc\xb2\x1a\x85\xe5\x91\x52\x09\x9c\xb0\xb3\xfe\xe6\x91\x47\x07\xa8\x81\xaa\x74\x14\x41\x92\xa4\xfb\x06\x93\xd2\x96\x28\xa3\xf0\x9b\x9e\xe2\xc1\xf8\x98\x24\x3f\xed\x48\x66\xcc\xd3\x8e\x4f\x7e\xf9\x15\x3e\x8e\x7b\x78\x19\xc7\x0b\x46\x15\x3c\xa9\x96\xa9\xe5\xa5\x3b\x11\xb0\xe2\x11\xc4\x01\x13\x88\xa5\x40\xa2\x92\x0b\x22\x23\x58\xaa\x9c\x89\xbc\xc4\x85\x4a\x9f\x2d\xf2\xd9\x7c\x61\x7b\x1a\x49\x8d\x5f\xd9\xce\x31\x61\x5a\x20\x9e\xa3\xb2\x9c\xf0\x81\x84\x40\xe7\xf4\x26\x49\xb1\x82\x5a\xba\x59\x4c\xd2\x86\xe2\x6f\x0d\xfc\xbf\x87\x28\xd1\x80\x3d\x6f\x29\x18\xdf\x7e\xe2\x4a\xb0\x86\xe7\x1c\x09\x6d\x60\xcb\xe2\x4f\x0b\x56\xd7\x88\x6e\x65\x75\x6b\xf8\x88\x90\x3c\xa3\x0a\x61\x0a\x22\xa7\xa8\x0e\x19\x92\xf6\x3a\xa0\xa5\xcc\xbb\x84\xbf\x68\x46\x87\xbc\xa3\x97\xd6\x04\x63\xf6\xdf\x54\x1f\x25\x5d\x32\xec\x6e\x1a\x6d\xda\x7a\x6f\xa9\x45\x98\x4b\x40\xa2\x38\xbe\x90\x9e\xd5\x08\xd3\x18\xd9\x01\x55\xe2\xcc\x19\xee\xec\xe5\x97\x33\x04\xa0\xa7\x7c\x8c\x25\xab\xc5\x00\xf4\x84\x05\xa3\xf5\xe0\x0d\x31\x01\x66\x0c\xf2\x9a\xfe\x89\x33\x09\xb6\x60\x2c\x06\xcd\xa1\x91\xd5\x89\x4c\x06\x8a\x87\x81\xf1\x9b\x24\xa5\x4d\xbd\x07\xa1\x6b\xd8\x09\xf2\xc0\x44\x8d\xf4\x66\x87\xb5\x8d\xe1\x89\xa4\x1d\x96\x67\x0a\xd0\xe4\x41\xa7\x75\x44\x72\x82\xe9\xe3\xf6\x26\x0e\x4f\x4a\xa0\xfc\xc8\xa4\x8a\x8f\xe1\x06\xf9\x11\x10\x51\xc7\xe2\x08\xc5\xe3\x02\xb9\x89\x9a\x50\x33\xa9\x62\x8c\x1c\xd7\xa8\x0a\x83\x78\x11\x82\x10\xb4\x07\xf2\x22\x3e\x37\x15\xbe\x31\x2d\xab\x2a\x0d\xf5\x59\xdc\xac\x72\xe9\x87\x43\x39\xbf\x14\xf8\x04\x22\x36\x81\x33\x7e\x29\xb8\xec\xc1\x70\x01\x92\x84\xab\xcf\x09\xf4\xcb\x5d\x57\x7c\x2e\x78\x55\xfb\x1f\x21\x69\x66\x97\x0b\x89\x95\xf7\x5d\x5f\x2c\x0e\xe3\x0a\x8a\x89\x56\x6a\x54\xe8\xba\x41\x80\xf4\xe8\xf5\x02\xed\x4f\x37\x79\xcd\x4a\x9f\x81\xce\xc0\xb6\x6c\xbc\x91\x7a\x75\x22\x4c\x5e\x54\x3f\x46\xa9\x2e\x78\x80\x08\x70\xe3\xdb\x5e\xec\x36\x2f\xdb\x0d\x9b\x58\x8b\x43\x04\x23\x09\x61\x67\xf7\x0a\x72\x32\x1b\xe6\xa7\x4f\x91\x36\xe1\xa2\xfd\xcf\x22\xad\x87\xd4\x3b\x67\x7c\x8d\x1c\x98\xea\xb2\x95\xd6\xdd\x5c\x1b\xc9\x02\xde\xf6\xca\x25\x3c\xc7\xa5\x3f\x56\xb4\x11\xc2\x74\x30\xce\x84\x9a\x79\xd7\xfa\x74\xef\xb3\x60\x53\x5c\x43\x9c\xba\x24\xfc\x6e\xf1\x99\x34\x66\xea\x8e\x22\x9a\xfb\x5f\xd0\x3f\xc2\x9e\x91\x2e\x44\x29\x07\x5a\x21\x51\xc1\xf4\x18\x82\xa9\x82\x0a\x84\x87\x80\x37\x7b\x82\xe5\x11\xca\x35\x34\x82\x29\x56\x30\x12\xe7\x18\xce\xe3\x67\xbc\x33\x4c\x27\xcc\xae\xae\xcd\xb8\xc0\x27\x4c\xa0\xb2\x38\xde\x33\x46\x00\xd1\x49\xa2\x10\x80\xca\x9c\x51\x72\x8e\x40\x4a\x85\x44\xf0\xf8\x27\xa1\x68\x04\x56\xe7\x9c\x71\xb5\x79\x55\x28\x8f\x75\x2e\xf1\x0f\x98\xfa\xde\xc5\xea\xfb\x89\x32\x6b\x43\x56\x3f\x2b\x79\x2d\xf7\xf3\x99\xed\x2b\xb9\x8d\x64\x8d\x28\xae\x73\x9c\x45\x7c\x33\x0d\x72\xcb\xe0\x6a\x0d\x78\xe6\xf0\xbd\x0a\x43\x35\xd4\xa2\xab\x38\x03\xb5\x3c\xcb\x42\xbd\xac\xb6\x96\xaa\xc4\x34\x67\x1c\x68\xd0\x37\xa4\x62\x3c\xaf\x04\x2a\x20\xe7\x20\x30\x73\x8a\x62\x12\x60\xcb\x46\x20\xbd\xfe\x7c\x1a\x89\x2b\x8a\xdc\x71\xc7\x80\xaa\x9a\x1f\x5e\xd8\x04\x50\x2a\xec\xec\x0d\xc1\x35\xf6\x3b\x8d\xc3\x6a\x23\xea\xb5\xae\x56\x73\x97\x68\x0b\xe5\x59\x54\xc8\x5e\x38\x21\x2c\x1f\x10\x22\x4e\x06\x47\x24\x56\xa4\x8e\xd6\x31\x0f\x9e\xfc\xe4\x3a\x37\x38\xf7\x35\xb9\x99\x6a\xe7\xbb\xe9\x37\x92\x39\xf1\xab\x4a\x2f\x7b\x1b\x99\xb7\xfa\x71\x3b\x55\x23\x83\x87\xb8\x16\x43\xe5\xd2\x01\x64\x84\x1a\x57\x2c\x9b\x66\x0b\x7d\xa8\xd1\x4e\x50\x62\xf7\x6e\x77\x16\x67\x2b\x2e\x49\xac\xfe\xc2\x30\x81\xab\xfb\x6f\x42\x83\xb7\x25\xcb\x37\x11\x3d\xc8\x7b\x4b\x80\x25\xda\x5b\xfd\x71\x97\x73\x6b\x6b\x14\xa7\x70\x8c\x11\xa0\x04\xb6\xf4\x32\x04\x6a\x33\x9e\x80\xfc\x35\x9b\x7c\x0a\xd7\xc0\x1a\x77\xc2\xdb\x99\xf6\xdd\x13\xa5\xc6\x2d\x4a\x40\xa9\x06\xd2\xd6\xe9\xc3\xa8\xd4\xe1\x2c\x10\x54\x5c\x8c\x93\x08\xe0\x04\x17\x48\x86\x02\xd1\x15\xcd\xa4\x86\x97\x48\x41\xde\xdd\xa2\xaf\x0a\xfd\x0b\x31\x9f\x23\x81\x08\x01\x82\x65\x1d\x13\x43\xd3\x12\x08\x3a\xbf\x28\x7d\xb6\xe4\x07\x84\x49\x23\x20\x47\x85\xea\x2f\xea\x03\x36\x97\xd6\x8c\x62\xc5\x9c\x11\x22\x6e\xc9\x1a\x3d\xe5\xc3\xb2\x2d\x24\x54\xd9\x4c\x8b\xfa\xd8\x3e\x90\x61\x09\x5d\xe1\xb7\x2e\x3b\x2f\xa8\xe8\x92\xeb\x3d\x16\x33\xac\x38\x63\x5d\x80\xd4\x91\x64\x6c\xd3\x05\xe9\x83\xa9\xa5\x3f\x65\xe4\x9c\x11\x5c\x9c\xb7\xe2\xb0\x60\xb4\x13\x72\x8c\x41\x5c\x69\x81\xda\x1c\x74\x29\x54\x73\x15\x74\xd6\x96\xe0\x3b\xa6\x25\xfb\xbe\x62\xc1\xed\x4c\x89\x13\x54\x80\x15\xef\xae\x15\xb4\x54\x02\x61\xaa\x56\xa7\xf3\x6b\xd9\xba\x22\x9b\x8f\xf6\x19\x88\xfa\x23\x2e\xfc\xea\xc1\x13\xe9\x0b\xde\x04\x7b\xb7\x35\xd4\x4c\x38\x0d\x70\x83\x67\x39\x21\x16\x07\xd8\x06\x59\x2d\xaa\xd9\xdf\xa3\x72\xc6\xb7\x3f\x6d\x84\x1b\xfa\x59\x38\x20\x61\x8e\xea\xad\xbc\x23\xfa\xfa\x23\x75\xe6\xe0\x64\xb9\x6f\x91\xf8\x7b\x17\xa1\x5d\x87\xf7\xde\x23\x64\xb3\xa7\x9e\x16\xc2\xfc\x94\xb1\x65\x53\x6c\xc3\xa0\x37\xdc\x5c\x7a\xb4\xfa\x30\xd6\xcc\x37\xa3\xac\xb2\x68\x15\x7b\xaf\x0d\xb7\xdb\x7f\x5b\xbe\xdb\x2d\x02\x57\x9d\x8f\x94\x42\xc5\x31\xea\x48\xb0\xb2\x68\xbc\x22\x0e\xf5\x4f\xd5\x02\x61\xa8\x47\xfd\x13\x85\xfe\x26\x36\xfb\xd7\xd9\x57\xff\x32\x30\xf8\x24\xaf\x45\xbd\x38\x8f\x47\xbc\x43\xfb\x05\x74\xf6\xd6\xaa\x98\xf6\x20\x0d\x95\xcc\xdb\x03\x4b\x92\x8c\xbe\x28\xed\x29\xb2\xe9\x36\x6c\x98\xe3\xf1\xf6\x34\x99\x2e\xf5\x9c\x06\x88\xe7\x2a\xc6\x5a\xb4\x17\xe2\x32\xe7\x1b\x06\x9b\xbb\x0f\x0b\x25\xc3\xd2\x83\x86\x57\xca\xb5\x1b\xf4\xf3\xdc\x3a\xb5\xce\x19\x83\x74\xe7\x0f\x72\x3d\xfe\x6f\xd0\xcf\x9e\xe7\x6a\x3e\xe9\x79\xd6\xbe\xfa\x39\xed\xc9\x76\x4f\x6b\xb3\x89\x7c\x2c\x48\xf7\x3c\xc8\x88\xee\x99\x79\xf4\xf2\xa9\xd1\xf9\x68\xd7\xee\x08\x0f\x8f\x67\x3d\x17\x20\x3b\xf3\x6f\xfb\xd0\x79\xf7\xbc\xfb\x33\x00\x00\xff\xff\xfa\xcc\x57\x15\x61\x31\x00\x00")
92
+var _dataConfig_schema_v31Json = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xec\x5b\xcd\x73\xdc\x28\x16\xbf\xf7\x5f\xa1\x52\x72\x8b\x3f\xb2\xb5\xa9\xad\xda\xdc\xf6\xb8\xa7\x99\xf3\xb8\x3a\x2a\x5a\x7a\xad\x26\x46\x40\x00\xb5\xdd\x49\xf9\x7f\x9f\xd2\x67\x03\x02\x81\xba\xe5\x38\x33\x35\x27\xdb\xe2\xf7\x80\xf7\xfd\x1e\xe0\x1f\x9b\x24\x49\xdf\xcb\xfc\x00\x15\x4a\x3f\x27\xe9\x41\x29\xfe\xf9\xfe\xfe\xab\x64\xf4\xb6\xfb\x7a\xc7\x44\x79\x5f\x08\xb4\x57\xb7\x1f\x3f\xdd\x77\xdf\xde\xa5\x37\x0d\x1d\x2e\x1a\x92\x9c\xd1\x3d\x2e\xb3\x6e\x24\x3b\xfe\xfb\xee\x5f\x77\x0d\x79\x07\x51\x27\x0e\x0d\x88\xed\xbe\x42\xae\xba\x6f\x02\xbe\xd5\x58\x40\x43\xfc\x90\x1e\x41\x48\xcc\x68\xba\xbd\xd9\x34\x63\x5c\x30\x0e\x42\x61\x90\xe9\xe7\xa4\xd9\x5c\x92\x8c\x90\xe1\x83\x36\xad\x54\x02\xd3\x32\x6d\x3f\xbf\xb4\x33\x24\x49\x2a\x41\x1c\x71\xae\xcd\x30\x6e\xf5\xdd\xfd\x79\xfe\xfb\x11\x76\x63\xcf\xaa\x6d\xb6\xfd\xce\x91\x52\x20\xe8\xef\xd3\xbd\xb5\xc3\x5f\x1e\xd0\xed\xf7\xff\xdd\xfe\xf1\xf1\xf6\xbf\x77\xd9\xed\xf6\xc3\x7b\x63\xb8\x91\xaf\x80\x7d\xb7\x7c\x01\x7b\x4c\xb1\xc2\x8c\x8e\xeb\xa7\x23\xf2\xa5\xff\xed\x65\x5c\x18\x15\x45\x0b\x46\xc4\x58\x7b\x8f\x88\x04\x93\x67\x0a\xea\x89\x89\xc7\x10\xcf\x23\xec\x8d\x78\xee\xd7\x77\xf0\x6c\xb2\x73\x64\xa4\xae\x82\x1a\x1c\x50\x6f\xc4\x4c\xb7\xfc\x3a\xfa\x93\x90\x0b\x50\x61\x93\xed\x50\x6f\x66\xb1\xcd\xf2\xd7\x31\xbc\x19\x98\x9e\xc5\x76\x08\x6d\xed\x76\x83\x86\x7b\xbb\x44\xe5\x72\x2f\xbf\xac\x46\x61\x79\xa4\x54\x00\x27\xec\xd4\x7c\xf3\xc8\xa3\x03\x54\x40\x55\x3a\x8a\x20\x49\xd2\x5d\x8d\x49\x61\x4b\x94\x51\xf8\xad\x99\xe2\x41\xfb\x98\x24\x3f\xec\x48\xa6\xcd\xd3\x8e\x1b\x7f\xf9\x15\x3e\x8e\x7b\x78\x19\xc7\x73\x46\x15\x3c\xab\x96\xa9\xf9\xa5\x3b\x11\xb0\xfc\x11\xc4\x1e\x13\x88\xa5\x40\xa2\x94\x33\x22\x23\x58\xaa\x8c\x89\xac\xc0\xb9\x4a\x5f\x2c\xf2\xc9\x7c\x61\x7b\x1a\x49\xb5\xbf\xb6\x1b\xc7\x84\x69\x8e\x78\x86\x8a\xc2\xe0\x03\x09\x81\x4e\xe9\x4d\x92\x62\x05\x95\x74\xb3\x98\xa4\x35\xc5\xdf\x6a\xf8\x7f\x0f\x51\xa2\x06\x7b\xde\x42\x30\xbe\xfe\xc4\xa5\x60\x35\xcf\x38\x12\x8d\x81\xcd\x8b\x3f\xcd\x59\x55\x21\xba\x96\xd5\x2d\xe1\x23\x42\xf2\x8c\x2a\x84\x29\x88\x8c\xa2\x2a\x64\x48\x8d\xd7\x01\x2d\x64\xd6\x25\xfc\x59\x33\xda\x67\x1d\xbd\xb4\x26\x18\xb3\xff\xaa\xfa\x28\xe8\x9c\x61\x77\xd3\x34\xa6\xdd\xec\x2d\xb5\x08\x33\x09\x48\xe4\x87\x0b\xe9\x59\x85\x30\x8d\x91\x1d\x50\x25\x4e\x9c\xe1\xce\x5e\x7e\x39\x43\x00\x7a\xcc\xc6\x58\xb2\x58\x0c\x40\x8f\x58\x30\x5a\x0d\xde\x10\x13\x60\xc6\x20\xdf\xd0\x3f\x73\x26\xc1\x16\x8c\xc5\xa0\x3e\x34\xb2\x6a\xc8\x64\xa0\x78\x18\x18\xbf\x49\x52\x5a\x57\x3b\x10\x4d\x0d\x6b\x20\xf7\x4c\x54\xa8\xd9\xec\xb0\xb6\x36\x6c\x48\xda\x61\x79\xba\x00\x75\x1e\x9a\xb4\x8e\x48\x46\x30\x7d\x5c\xdf\xc4\xe1\x59\x09\x94\x1d\x98\x54\xf1\x31\x5c\x23\x3f\x00\x22\xea\x90\x1f\x20\x7f\x9c\x21\xd7\x51\x06\x35\x93\x2a\xc6\xc8\x71\x85\xca\x30\x88\xe7\x21\x08\x41\x3b\x20\x17\xf1\xb9\xaa\xf0\xb5\x69\x59\x59\x36\x50\x9f\xc5\x4d\x2a\x97\x7e\x38\x94\xf3\x0b\x81\x8f\x20\x62\x13\x38\xe3\xe7\x82\xcb\x1e\x0c\x17\x20\x49\xb8\xfa\x34\xa0\x5f\xee\xba\xe2\x73\xc6\xab\xda\xdf\x08\x49\xb7\x76\xb9\x90\x58\x79\xdf\xf5\xc5\xe2\x30\xae\xa0\x30\xb4\x52\xa1\xbc\xa9\x1b\x04\x48\x8f\x5e\xcf\xd0\xbe\xbb\xc9\x2a\x56\xf8\x0c\x74\x02\xb6\x65\xe3\x8d\xd4\x8b\x13\x61\x72\x51\xfd\x18\xa5\xba\x60\x03\x11\xe0\xc6\xb7\xbd\xd8\x6d\x9e\xb7\x1b\x36\xb1\x16\x87\x08\x46\x12\xc2\xce\xee\x15\xa4\x31\x1b\xe6\xc7\x4f\x91\x36\xe1\xa2\xfd\xcf\x2c\xad\x87\xd4\x3b\x67\x7c\x8d\x1c\x98\xea\xbc\x95\xd6\xdd\x5c\x1b\xd9\x06\xbc\xed\x95\x4b\x78\x8e\x0b\x7f\xac\x68\x23\x84\xee\x60\x9c\x09\x35\xf1\xae\xe5\xe9\xde\x67\xc1\xba\xb8\x86\x38\x75\x4e\xf8\xdd\xe2\x13\x69\x4c\xd4\x1d\x45\x34\xf5\xbf\xa0\x7f\x84\x3d\x23\x9d\x89\x52\x0e\xb4\x42\xa2\x04\xb3\x0d\xc1\x54\x41\x09\xc2\x43\xc0\xeb\x1d\xc1\xf2\x00\xc5\x12\x1a\xc1\x14\xcb\x19\x89\x73\x0c\x67\xfb\x19\xef\x0c\xe6\x84\xdb\xab\x6b\x33\x2e\xf0\x11\x13\x28\x2d\x8e\x77\x8c\x11\x40\xd4\x48\x14\x02\x50\x91\x31\x4a\x4e\x11\x48\xa9\x90\x08\xb6\x7f\x12\xf2\x5a\x60\x75\xca\x18\x57\xab\x57\x85\xf2\x50\x65\x12\x7f\x07\xd3\xf7\xce\x56\xdf\x4f\xb4\xb5\x36\x64\x9d\x67\x25\xaf\xe5\x7e\x3e\xb3\x7d\x25\xb7\x91\xac\x16\xf9\x75\x8e\x33\x8b\xaf\xcd\x20\x37\x0f\x2e\x97\x80\x27\x0e\xdf\xab\x30\x54\x43\xcd\xba\x8a\x33\x50\xcb\x93\xcc\xd5\x65\xb5\xb5\x54\x05\xa6\x19\xe3\x40\x83\xbe\x21\x15\xe3\x59\x29\x50\x0e\x19\x07\x81\x99\x53\x14\x46\x80\x2d\x6a\x81\x9a\xf5\xa7\xd3\x48\x5c\x52\xe4\x8e\x3b\x1a\x54\x55\x7c\x7f\xe1\x21\x80\x52\x61\x67\xaf\x09\xae\xb0\xdf\x69\x1c\x56\x1b\x51\xaf\x75\xb5\x9a\xbb\x44\x9b\x29\xcf\xa2\x42\xf6\x4c\x87\x30\xdf\x20\x44\x74\x06\x07\x24\x16\xa4\x8e\xd6\x31\xf7\x9e\xfc\xe4\xea\x1b\x9c\xfb\x32\x6e\xa6\xda\xf9\x6e\xfa\x8d\x6c\x9d\xf8\x45\xa5\x97\xbd\x8d\xad\xb7\xfa\x71\x3b\x55\x2d\x83\x4d\x5c\x8b\xa1\x72\xae\x01\x19\xa1\xd3\x2b\x96\xe4\x2f\x11\xa1\x0d\x1d\xb5\x70\x87\x6e\x22\xe2\x78\xbf\x52\x64\xec\x7c\xed\xa8\x1f\x5d\x11\x68\x34\x3b\x3c\x39\xf0\x5d\x22\xc9\x38\x39\x8d\x28\x54\x76\xa1\x33\xba\x67\x89\x77\xbb\xfe\x22\xed\xa7\xb0\x42\x59\xce\xb8\x47\xca\xf1\x6c\x2c\xcd\x98\xd6\x29\xc4\x4c\x49\xe9\xf3\xfe\x27\x26\x1e\x9b\xdc\x52\x60\x77\x10\xd8\x58\x24\x0b\xee\x1e\xad\x63\xbb\x61\x02\xd7\xa5\x9a\x0e\x0d\x5e\x42\xce\x5f\xf0\xf5\x20\xef\xe5\x1b\x96\x68\x67\x5d\x3b\xb9\x72\x66\x13\xe4\xc5\x31\x9c\xba\x05\x28\x81\xad\x5b\x81\xa1\xfe\xd1\xd3\x34\xc8\x5f\xf3\xec\x5c\xe1\x0a\x58\xed\x8e\x28\x1b\xdd\x70\x7a\xa2\x54\xbb\x9c\x0c\x28\x55\x43\xda\x3a\x7d\x18\x95\x3a\xb4\xd8\x41\xc5\xc5\xe4\x1e\x01\x9c\xe0\x1c\xc9\x50\x7e\xbf\xe2\x8c\xb6\xe6\x05\x52\x90\x75\x8f\x53\x16\x55\x54\x33\xa5\x14\x47\x02\x11\x02\x04\xcb\x2a\xa6\x34\x49\x0b\x20\xe8\x74\x51\x55\xda\x92\xef\x11\x26\xb5\x80\x0c\xe5\xde\xc8\x6b\x51\x54\x8c\x62\xc5\x9c\x11\x22\x6e\xc9\x0a\x3d\x67\xc3\xb2\x2d\x24\xd4\x30\x98\xbd\x72\xec\xf1\xaa\x66\x09\x5d\x66\x5d\x56\xf4\xce\xa8\xe8\x5c\x42\x7b\x2c\x66\x58\x71\xc2\xba\x00\xd9\x44\x92\xf1\xf4\x3b\x48\x1f\x8c\xd9\x7d\xf3\x9e\x71\x46\x70\x7e\x5a\x8b\xc3\x9c\xd1\x4e\xc8\x31\x06\x71\xa5\x05\x36\xe6\xd0\x74\x18\x15\x57\x41\x67\x6d\x09\x9e\x30\x2d\xd8\xd3\x82\x05\xd7\x33\x25\x4e\x50\x0e\x56\xbc\xbb\x56\xd0\x52\x09\x84\xa9\x5a\x7c\xd9\x73\x2d\x5b\x57\x64\xf3\xd1\x3e\x03\x51\x7f\xc4\x05\xf3\xb8\x2f\xd2\xe7\xbc\x0e\x5e\x89\x54\x50\x31\xe1\x34\xc0\x15\x5e\xbb\x85\x58\x1c\x60\x2b\x64\xb5\xa8\x3b\xb4\x1e\x95\x31\xbe\x7e\x13\x1f\xbe\x27\xdb\x86\x03\x12\xe6\xa8\x5a\xcb\x3b\xa2\x6f\x15\x53\x67\x0e\x4e\xe6\x9b\xcd\xc4\xdf\x70\x86\x76\x1d\xde\x7b\x8f\x90\xf5\x8e\x7a\x7a\xb4\x69\x7d\xbf\xe6\x59\xf3\x8a\x41\x6f\x78\x10\xe0\xd1\xea\xc3\x58\x33\xdf\x8c\xb2\xda\x46\xab\xd8\x7b\x1b\xbf\xde\xfe\xdb\xf2\xdd\x3e\x79\x73\xd5\xf9\x48\x29\x94\x1f\xa2\x5a\x82\x85\x45\xe3\x15\x71\x68\xd2\xb8\x3a\xc3\x50\x8f\xfa\x27\x0a\xfd\x4d\x6c\xf6\xe7\xd9\x57\xff\xe0\x36\xf8\xd2\xb5\x45\x5d\x9c\xc7\x23\x9e\x77\xfe\x02\x3a\x7b\x6b\x55\x98\x47\xfb\x9a\x4a\xa6\xc7\x03\x73\x92\x8c\x7e\x7f\xd0\x53\x6c\xcd\x6d\xd8\x30\xc7\xff\x44\x98\xc9\x74\xee\xe2\x6f\x80\x78\x8e\xa3\xac\x45\x7b\x21\xce\x73\xbe\x62\xb0\xb9\xfb\x30\x53\x32\xcc\xbd\x13\x7a\xa5\x5c\xbb\xc2\xa5\xaa\x5b\xa7\x56\x9f\x31\x48\x77\xfa\xce\xdd\xe3\xff\x1a\xfd\xe4\xd5\x7b\xc3\x27\x3d\x4d\x8e\xaf\x7e\x98\xc7\xe8\xdd\x8b\xf5\xad\x21\x1f\x0b\xd2\xbd\xba\xd3\xa2\xfb\x56\x6f\xbd\x7c\x6a\x74\xbe\x85\xb7\x0f\xf1\x87\x37\xe9\x9e\x7b\xc5\x8d\xfe\xb3\xfd\xff\x81\xcd\xcb\xe6\xcf\x00\x00\x00\xff\xff\xea\x87\x24\xae\xb8\x34\x00\x00")
93 93
 
94 94
 func dataConfig_schema_v31JsonBytes() ([]byte, error) {
95 95
 	return bindataRead(
... ...
@@ -235,7 +235,37 @@
235 235
         },
236 236
         "user": {"type": "string"},
237 237
         "userns_mode": {"type": "string"},
238
-        "volumes": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
238
+        "volumes": {
239
+          "type": "array",
240
+          "items": {
241
+            "oneOf": [
242
+              {"type": "string"},
243
+              {
244
+                "type": "object",
245
+                "required": ["type"],
246
+                "properties": {
247
+                  "type": {"type": "string"},
248
+                  "source": {"type": "string"},
249
+                  "target": {"type": "string"},
250
+                  "read_only": {"type": "boolean"},
251
+                  "bind": {
252
+                    "type": "object",
253
+                    "properties": {
254
+                      "propagation": {"type": "string"}
255
+                    }
256
+                  },
257
+                  "volume": {
258
+                    "type": "object",
259
+                    "properties": {
260
+                      "nocopy": {"type": "boolean"}
261
+                    }
262
+                  }
263
+                }
264
+              }
265
+            ],
266
+            "uniqueItems": true
267
+          }
268
+        },
239 269
         "working_dir": {"type": "string"}
240 270
       },
241 271
       "additionalProperties": false
... ...
@@ -78,18 +78,27 @@ func Validate(config map[string]interface{}, version string) error {
78 78
 
79 79
 func toError(result *gojsonschema.Result) error {
80 80
 	err := getMostSpecificError(result.Errors())
81
-	description := getDescription(err)
82
-	return fmt.Errorf("%s %s", err.Field(), description)
81
+	return err
83 82
 }
84 83
 
85
-func getDescription(err gojsonschema.ResultError) string {
86
-	if err.Type() == "invalid_type" {
87
-		if expectedType, ok := err.Details()["expected"].(string); ok {
84
+const (
85
+	jsonschemaOneOf = "number_one_of"
86
+	jsonschemaAnyOf = "number_any_of"
87
+)
88
+
89
+func getDescription(err validationError) string {
90
+	switch err.parent.Type() {
91
+	case "invalid_type":
92
+		if expectedType, ok := err.parent.Details()["expected"].(string); ok {
88 93
 			return fmt.Sprintf("must be a %s", humanReadableType(expectedType))
89 94
 		}
95
+	case jsonschemaOneOf, jsonschemaAnyOf:
96
+		if err.child == nil {
97
+			return err.parent.Description()
98
+		}
99
+		return err.child.Description()
90 100
 	}
91
-
92
-	return err.Description()
101
+	return err.parent.Description()
93 102
 }
94 103
 
95 104
 func humanReadableType(definition string) string {
... ...
@@ -113,23 +122,45 @@ func humanReadableType(definition string) string {
113 113
 	return definition
114 114
 }
115 115
 
116
-func getMostSpecificError(errors []gojsonschema.ResultError) gojsonschema.ResultError {
117
-	var mostSpecificError gojsonschema.ResultError
116
+type validationError struct {
117
+	parent gojsonschema.ResultError
118
+	child  gojsonschema.ResultError
119
+}
118 120
 
119
-	for _, err := range errors {
120
-		if mostSpecificError == nil {
121
-			mostSpecificError = err
122
-		} else if specificity(err) > specificity(mostSpecificError) {
123
-			mostSpecificError = err
124
-		} else if specificity(err) == specificity(mostSpecificError) {
121
+func (err validationError) Error() string {
122
+	description := getDescription(err)
123
+	return fmt.Sprintf("%s %s", err.parent.Field(), description)
124
+}
125
+
126
+func getMostSpecificError(errors []gojsonschema.ResultError) validationError {
127
+	mostSpecificError := 0
128
+	for i, err := range errors {
129
+		if specificity(err) > specificity(errors[mostSpecificError]) {
130
+			mostSpecificError = i
131
+			continue
132
+		}
133
+
134
+		if specificity(err) == specificity(errors[mostSpecificError]) {
125 135
 			// Invalid type errors win in a tie-breaker for most specific field name
126
-			if err.Type() == "invalid_type" && mostSpecificError.Type() != "invalid_type" {
127
-				mostSpecificError = err
136
+			if err.Type() == "invalid_type" && errors[mostSpecificError].Type() != "invalid_type" {
137
+				mostSpecificError = i
128 138
 			}
129 139
 		}
130 140
 	}
131 141
 
132
-	return mostSpecificError
142
+	if mostSpecificError+1 == len(errors) {
143
+		return validationError{parent: errors[mostSpecificError]}
144
+	}
145
+
146
+	switch errors[mostSpecificError].Type() {
147
+	case "number_one_of", "number_any_of":
148
+		return validationError{
149
+			parent: errors[mostSpecificError],
150
+			child:  errors[mostSpecificError+1],
151
+		}
152
+	default:
153
+		return validationError{parent: errors[mostSpecificError]}
154
+	}
133 155
 }
134 156
 
135 157
 func specificity(err gojsonschema.ResultError) int {
... ...
@@ -119,7 +119,7 @@ type ServiceConfig struct {
119 119
 	Tty             bool `mapstructure:"tty"`
120 120
 	Ulimits         map[string]*UlimitsConfig
121 121
 	User            string
122
-	Volumes         []string
122
+	Volumes         []ServiceVolumeConfig
123 123
 	WorkingDir      string `mapstructure:"working_dir"`
124 124
 }
125 125
 
... ...
@@ -223,6 +223,26 @@ type ServicePortConfig struct {
223 223
 	Protocol  string
224 224
 }
225 225
 
226
+// ServiceVolumeConfig are references to a volume used by a service
227
+type ServiceVolumeConfig struct {
228
+	Type     string
229
+	Source   string
230
+	Target   string
231
+	ReadOnly bool `mapstructure:"read_only"`
232
+	Bind     *ServiceVolumeBind
233
+	Volume   *ServiceVolumeVolume
234
+}
235
+
236
+// ServiceVolumeBind are options for a service volume of type bind
237
+type ServiceVolumeBind struct {
238
+	Propagation string
239
+}
240
+
241
+// ServiceVolumeVolume are options for a service volume of type volume
242
+type ServiceVolumeVolume struct {
243
+	NoCopy bool `mapstructure:"nocopy"`
244
+}
245
+
226 246
 // ServiceSecretConfig is the secret configuration for a service
227 247
 type ServiceSecretConfig struct {
228 248
 	Source string
... ...
@@ -102,7 +102,7 @@ func (m *MountOpt) Set(value string) error {
102 102
 		case "volume-nocopy":
103 103
 			volumeOptions().NoCopy, err = strconv.ParseBool(value)
104 104
 			if err != nil {
105
-				return fmt.Errorf("invalid value for populate: %s", value)
105
+				return fmt.Errorf("invalid value for volume-nocopy: %s", value)
106 106
 			}
107 107
 		case "volume-label":
108 108
 			setValueOnMap(volumeOptions().Labels, value)