Browse code

Convert new compose volume type to swarm mount type

Signed-off-by: Daniel Nephin <dnephin@docker.com>

Daniel Nephin authored on 2017/01/28 06:56:45
Showing 3 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
 }
... ...
@@ -81,13 +81,18 @@ func toError(result *gojsonschema.Result) error {
81 81
 	return err
82 82
 }
83 83
 
84
+const (
85
+	jsonschemaOneOf = "number_one_of"
86
+	jsonschemaAnyOf = "number_any_of"
87
+)
88
+
84 89
 func getDescription(err validationError) string {
85 90
 	switch err.parent.Type() {
86 91
 	case "invalid_type":
87 92
 		if expectedType, ok := err.parent.Details()["expected"].(string); ok {
88 93
 			return fmt.Sprintf("must be a %s", humanReadableType(expectedType))
89 94
 		}
90
-	case "number_one_of", "number_any_of":
95
+	case jsonschemaOneOf, jsonschemaAnyOf:
91 96
 		if err.child == nil {
92 97
 			return err.parent.Description()
93 98
 		}