Browse code

Move ConvertVolumes to composetransform package.

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

Daniel Nephin authored on 2016/12/01 06:34:29
Showing 7 changed files
... ...
@@ -50,6 +50,16 @@ const (
50 50
 	PropagationSlave Propagation = "slave"
51 51
 )
52 52
 
53
+// Propagations is the list of all valid mount propagations
54
+var Propagations = []Propagation{
55
+	PropagationRPrivate,
56
+	PropagationPrivate,
57
+	PropagationRShared,
58
+	PropagationShared,
59
+	PropagationRSlave,
60
+	PropagationSlave,
61
+}
62
+
53 63
 // BindOptions defines options specific to mounts of type "bind".
54 64
 type BindOptions struct {
55 65
 	Propagation Propagation `json:",omitempty"`
... ...
@@ -7,11 +7,12 @@ import (
7 7
 	"github.com/docker/docker/api/types/filters"
8 8
 	"github.com/docker/docker/api/types/swarm"
9 9
 	"github.com/docker/docker/client"
10
+	"github.com/docker/docker/pkg/composetransform"
10 11
 )
11 12
 
12 13
 func getStackFilter(namespace string) filters.Args {
13 14
 	filter := filters.NewArgs()
14
-	filter.Add("label", labelNamespace+"="+namespace)
15
+	filter.Add("label", composetransform.LabelNamespace+"="+namespace)
15 16
 	return filter
16 17
 }
17 18
 
... ...
@@ -16,13 +16,12 @@ import (
16 16
 	composetypes "github.com/aanand/compose-file/types"
17 17
 	"github.com/docker/docker/api/types"
18 18
 	"github.com/docker/docker/api/types/container"
19
-	"github.com/docker/docker/api/types/mount"
20
-	networktypes "github.com/docker/docker/api/types/network"
21 19
 	"github.com/docker/docker/api/types/swarm"
22 20
 	"github.com/docker/docker/cli"
23 21
 	"github.com/docker/docker/cli/command"
24 22
 	dockerclient "github.com/docker/docker/client"
25 23
 	"github.com/docker/docker/opts"
24
+	"github.com/docker/docker/pkg/composetransform"
26 25
 	runconfigopts "github.com/docker/docker/runconfig/opts"
27 26
 	"github.com/docker/go-connections/nat"
28 27
 )
... ...
@@ -121,9 +120,9 @@ func deployCompose(ctx context.Context, dockerCli *command.DockerCli, opts deplo
121 121
 		return err
122 122
 	}
123 123
 
124
-	namespace := namespace{name: opts.namespace}
124
+	namespace := composetransform.NewNamespace(opts.namespace)
125 125
 
126
-	networks, externalNetworks := convertNetworks(namespace, config.Networks)
126
+	networks, externalNetworks := composetransform.ConvertNetworks(namespace, config.Networks)
127 127
 	if err := validateExternalNetworks(ctx, dockerCli, externalNetworks); err != nil {
128 128
 		return err
129 129
 	}
... ...
@@ -204,12 +203,12 @@ func validateExternalNetworks(
204 204
 func createNetworks(
205 205
 	ctx context.Context,
206 206
 	dockerCli *command.DockerCli,
207
-	namespace namespace,
207
+	namespace composetransform.Namespace,
208 208
 	networks map[string]types.NetworkCreate,
209 209
 ) error {
210 210
 	client := dockerCli.Client()
211 211
 
212
-	existingNetworks, err := getStackNetworks(ctx, client, namespace.name)
212
+	existingNetworks, err := getStackNetworks(ctx, client, namespace.Name())
213 213
 	if err != nil {
214 214
 		return err
215 215
 	}
... ...
@@ -220,7 +219,7 @@ func createNetworks(
220 220
 	}
221 221
 
222 222
 	for internalName, createOpts := range networks {
223
-		name := namespace.scope(internalName)
223
+		name := namespace.Scope(internalName)
224 224
 		if _, exists := existingNetworkMap[name]; exists {
225 225
 			continue
226 226
 		}
... ...
@@ -241,7 +240,7 @@ func createNetworks(
241 241
 func convertServiceNetworks(
242 242
 	networks map[string]*composetypes.ServiceNetworkConfig,
243 243
 	networkConfigs map[string]composetypes.NetworkConfig,
244
-	namespace namespace,
244
+	namespace composetransform.Namespace,
245 245
 	name string,
246 246
 ) ([]swarm.NetworkAttachmentConfig, error) {
247 247
 	if len(networks) == 0 {
... ...
@@ -275,116 +274,6 @@ func convertServiceNetworks(
275 275
 	return nets, nil
276 276
 }
277 277
 
278
-func convertVolumes(
279
-	serviceVolumes []string,
280
-	stackVolumes map[string]composetypes.VolumeConfig,
281
-	namespace namespace,
282
-) ([]mount.Mount, error) {
283
-	var mounts []mount.Mount
284
-
285
-	for _, volumeSpec := range serviceVolumes {
286
-		mount, err := convertVolumeToMount(volumeSpec, stackVolumes, namespace)
287
-		if err != nil {
288
-			return nil, err
289
-		}
290
-		mounts = append(mounts, mount)
291
-	}
292
-	return mounts, nil
293
-}
294
-
295
-func convertVolumeToMount(
296
-	volumeSpec string,
297
-	stackVolumes map[string]composetypes.VolumeConfig,
298
-	namespace namespace,
299
-) (mount.Mount, error) {
300
-	var source, target string
301
-	var mode []string
302
-
303
-	// TODO: split Windows path mappings properly
304
-	parts := strings.SplitN(volumeSpec, ":", 3)
305
-
306
-	switch len(parts) {
307
-	case 3:
308
-		source = parts[0]
309
-		target = parts[1]
310
-		mode = strings.Split(parts[2], ",")
311
-	case 2:
312
-		source = parts[0]
313
-		target = parts[1]
314
-	case 1:
315
-		target = parts[0]
316
-	default:
317
-		return mount.Mount{}, fmt.Errorf("invald volume: %s", volumeSpec)
318
-	}
319
-
320
-	// TODO: catch Windows paths here
321
-	if strings.HasPrefix(source, "/") {
322
-		return mount.Mount{
323
-			Type:        mount.TypeBind,
324
-			Source:      source,
325
-			Target:      target,
326
-			ReadOnly:    isReadOnly(mode),
327
-			BindOptions: getBindOptions(mode),
328
-		}, nil
329
-	}
330
-
331
-	stackVolume, exists := stackVolumes[source]
332
-	if !exists {
333
-		return mount.Mount{}, fmt.Errorf("undefined volume: %s", source)
334
-	}
335
-
336
-	var volumeOptions *mount.VolumeOptions
337
-	if stackVolume.External.Name != "" {
338
-		source = stackVolume.External.Name
339
-	} else {
340
-		volumeOptions = &mount.VolumeOptions{
341
-			Labels: getStackLabels(namespace.name, stackVolume.Labels),
342
-			NoCopy: isNoCopy(mode),
343
-		}
344
-
345
-		if stackVolume.Driver != "" {
346
-			volumeOptions.DriverConfig = &mount.Driver{
347
-				Name:    stackVolume.Driver,
348
-				Options: stackVolume.DriverOpts,
349
-			}
350
-		}
351
-		source = namespace.scope(source)
352
-	}
353
-	return mount.Mount{
354
-		Type:          mount.TypeVolume,
355
-		Source:        source,
356
-		Target:        target,
357
-		ReadOnly:      isReadOnly(mode),
358
-		VolumeOptions: volumeOptions,
359
-	}, nil
360
-}
361
-
362
-func modeHas(mode []string, field string) bool {
363
-	for _, item := range mode {
364
-		if item == field {
365
-			return true
366
-		}
367
-	}
368
-	return false
369
-}
370
-
371
-func isReadOnly(mode []string) bool {
372
-	return modeHas(mode, "ro")
373
-}
374
-
375
-func isNoCopy(mode []string) bool {
376
-	return modeHas(mode, "nocopy")
377
-}
378
-
379
-func getBindOptions(mode []string) *mount.BindOptions {
380
-	for _, item := range mode {
381
-		if strings.Contains(item, "private") || strings.Contains(item, "shared") || strings.Contains(item, "slave") {
382
-			return &mount.BindOptions{Propagation: mount.Propagation(item)}
383
-		}
384
-	}
385
-	return nil
386
-}
387
-
388 278
 func deployServices(
389 279
 	ctx context.Context,
390 280
 	dockerCli *command.DockerCli,
... ...
@@ -494,7 +383,7 @@ func convertService(
494 494
 		return swarm.ServiceSpec{}, err
495 495
 	}
496 496
 
497
-	mounts, err := convertVolumes(service.Volumes, volumes, namespace)
497
+	mounts, err := composetransform.ConvertVolumes(service.Volumes, volumes, namespace)
498 498
 	if err != nil {
499 499
 		// TODO: better error message (include service name)
500 500
 		return swarm.ServiceSpec{}, err
... ...
@@ -7,7 +7,8 @@ import (
7 7
 )
8 8
 
9 9
 const (
10
-	labelNamespace = "com.docker.stack.namespace"
10
+	// LabelNamespace is the label used to track stack resources
11
+	LabelNamespace = "com.docker.stack.namespace"
11 12
 )
12 13
 
13 14
 // Namespace mangles names by prepending the name
... ...
@@ -20,12 +21,22 @@ func (n Namespace) Scope(name string) string {
20 20
 	return n.name + "_" + name
21 21
 }
22 22
 
23
+// Name returns the name of the namespace
24
+func (n Namespace) Name() string {
25
+	return n.name
26
+}
27
+
28
+// NewNamespace returns a new Namespace for scoping of names
29
+func NewNamespace(name string) Namespace {
30
+	return Namespace{name: name}
31
+}
32
+
23 33
 // AddStackLabel returns labels with the namespace label added
24 34
 func AddStackLabel(namespace Namespace, labels map[string]string) map[string]string {
25 35
 	if labels == nil {
26 36
 		labels = make(map[string]string)
27 37
 	}
28
-	labels[labelNamespace] = namespace.name
38
+	labels[LabelNamespace] = namespace.name
29 39
 	return labels
30 40
 }
31 41
 
... ...
@@ -21,7 +21,7 @@ func TestAddStackLabel(t *testing.T) {
21 21
 	actual := AddStackLabel(Namespace{name: "foo"}, labels)
22 22
 	expected := map[string]string{
23 23
 		"something":    "labeled",
24
-		labelNamespace: "foo",
24
+		LabelNamespace: "foo",
25 25
 	}
26 26
 	assert.DeepEqual(t, actual, expected)
27 27
 }
... ...
@@ -56,7 +56,7 @@ func TestConvertNetworks(t *testing.T) {
56 56
 	expected := map[string]types.NetworkCreate{
57 57
 		"default": {
58 58
 			Labels: map[string]string{
59
-				labelNamespace: "foo",
59
+				LabelNamespace: "foo",
60 60
 			},
61 61
 		},
62 62
 		"normal": {
... ...
@@ -73,7 +73,7 @@ func TestConvertNetworks(t *testing.T) {
73 73
 				"opt": "value",
74 74
 			},
75 75
 			Labels: map[string]string{
76
-				labelNamespace: "foo",
76
+				LabelNamespace: "foo",
77 77
 				"something":    "labeled",
78 78
 			},
79 79
 		},
80 80
new file mode 100644
... ...
@@ -0,0 +1,116 @@
0
+package composetransform
1
+
2
+import (
3
+	"fmt"
4
+	"strings"
5
+
6
+	composetypes "github.com/aanand/compose-file/types"
7
+	"github.com/docker/docker/api/types/mount"
8
+)
9
+
10
+type volumes map[string]composetypes.VolumeConfig
11
+
12
+// ConvertVolumes from compose-file types to engine api types
13
+func ConvertVolumes(serviceVolumes []string, stackVolumes volumes, namespace Namespace) ([]mount.Mount, error) {
14
+	var mounts []mount.Mount
15
+
16
+	for _, volumeSpec := range serviceVolumes {
17
+		mount, err := convertVolumeToMount(volumeSpec, stackVolumes, namespace)
18
+		if err != nil {
19
+			return nil, err
20
+		}
21
+		mounts = append(mounts, mount)
22
+	}
23
+	return mounts, nil
24
+}
25
+
26
+func convertVolumeToMount(volumeSpec string, stackVolumes volumes, namespace Namespace) (mount.Mount, error) {
27
+	var source, target string
28
+	var mode []string
29
+
30
+	// TODO: split Windows path mappings properly
31
+	parts := strings.SplitN(volumeSpec, ":", 3)
32
+
33
+	switch len(parts) {
34
+	case 3:
35
+		source = parts[0]
36
+		target = parts[1]
37
+		mode = strings.Split(parts[2], ",")
38
+	case 2:
39
+		source = parts[0]
40
+		target = parts[1]
41
+	case 1:
42
+		target = parts[0]
43
+	default:
44
+		return mount.Mount{}, fmt.Errorf("invald volume: %s", volumeSpec)
45
+	}
46
+
47
+	// TODO: catch Windows paths here
48
+	if strings.HasPrefix(source, "/") {
49
+		return mount.Mount{
50
+			Type:        mount.TypeBind,
51
+			Source:      source,
52
+			Target:      target,
53
+			ReadOnly:    isReadOnly(mode),
54
+			BindOptions: getBindOptions(mode),
55
+		}, nil
56
+	}
57
+
58
+	stackVolume, exists := stackVolumes[source]
59
+	if !exists {
60
+		return mount.Mount{}, fmt.Errorf("undefined volume: %s", source)
61
+	}
62
+
63
+	var volumeOptions *mount.VolumeOptions
64
+	if stackVolume.External.Name != "" {
65
+		source = stackVolume.External.Name
66
+	} else {
67
+		volumeOptions = &mount.VolumeOptions{
68
+			Labels: AddStackLabel(namespace, stackVolume.Labels),
69
+			NoCopy: isNoCopy(mode),
70
+		}
71
+
72
+		if stackVolume.Driver != "" {
73
+			volumeOptions.DriverConfig = &mount.Driver{
74
+				Name:    stackVolume.Driver,
75
+				Options: stackVolume.DriverOpts,
76
+			}
77
+		}
78
+		source = namespace.Scope(source)
79
+	}
80
+	return mount.Mount{
81
+		Type:          mount.TypeVolume,
82
+		Source:        source,
83
+		Target:        target,
84
+		ReadOnly:      isReadOnly(mode),
85
+		VolumeOptions: volumeOptions,
86
+	}, nil
87
+}
88
+
89
+func modeHas(mode []string, field string) bool {
90
+	for _, item := range mode {
91
+		if item == field {
92
+			return true
93
+		}
94
+	}
95
+	return false
96
+}
97
+
98
+func isReadOnly(mode []string) bool {
99
+	return modeHas(mode, "ro")
100
+}
101
+
102
+func isNoCopy(mode []string) bool {
103
+	return modeHas(mode, "nocopy")
104
+}
105
+
106
+func getBindOptions(mode []string) *mount.BindOptions {
107
+	for _, item := range mode {
108
+		for _, propagation := range mount.Propagations {
109
+			if mount.Propagation(item) == propagation {
110
+				return &mount.BindOptions{Propagation: mount.Propagation(item)}
111
+			}
112
+		}
113
+	}
114
+	return nil
115
+}
0 116
new file mode 100644
... ...
@@ -0,0 +1,112 @@
0
+package composetransform
1
+
2
+import (
3
+	"testing"
4
+
5
+	composetypes "github.com/aanand/compose-file/types"
6
+	"github.com/docker/docker/api/types/mount"
7
+	"github.com/docker/docker/pkg/testutil/assert"
8
+)
9
+
10
+func TestIsReadOnly(t *testing.T) {
11
+	assert.Equal(t, isReadOnly([]string{"foo", "bar", "ro"}), true)
12
+	assert.Equal(t, isReadOnly([]string{"ro"}), true)
13
+	assert.Equal(t, isReadOnly([]string{}), false)
14
+	assert.Equal(t, isReadOnly([]string{"foo", "rw"}), false)
15
+	assert.Equal(t, isReadOnly([]string{"foo"}), false)
16
+}
17
+
18
+func TestIsNoCopy(t *testing.T) {
19
+	assert.Equal(t, isNoCopy([]string{"foo", "bar", "nocopy"}), true)
20
+	assert.Equal(t, isNoCopy([]string{"nocopy"}), true)
21
+	assert.Equal(t, isNoCopy([]string{}), false)
22
+	assert.Equal(t, isNoCopy([]string{"foo", "rw"}), false)
23
+}
24
+
25
+func TesTGetBindOptions(t *testing.T) {
26
+	opts := getBindOptions([]string{"slave"})
27
+	expected := &mount.BindOptions{Propagation: mount.PropagationSlave}
28
+	assert.Equal(t, opts, expected)
29
+}
30
+
31
+func TesTGetBindOptionsNone(t *testing.T) {
32
+	opts := getBindOptions([]string{"ro"})
33
+	assert.Equal(t, opts, nil)
34
+}
35
+
36
+func TestConvertVolumeToMountNamedVolume(t *testing.T) {
37
+	stackVolumes := volumes{
38
+		"normal": composetypes.VolumeConfig{
39
+			Driver: "glusterfs",
40
+			DriverOpts: map[string]string{
41
+				"opt": "value",
42
+			},
43
+			Labels: map[string]string{
44
+				"something": "labeled",
45
+			},
46
+		},
47
+	}
48
+	namespace := NewNamespace("foo")
49
+	expected := mount.Mount{
50
+		Type:     mount.TypeVolume,
51
+		Source:   "foo_normal",
52
+		Target:   "/foo",
53
+		ReadOnly: true,
54
+		VolumeOptions: &mount.VolumeOptions{
55
+			Labels: map[string]string{
56
+				LabelNamespace: "foo",
57
+				"something":    "labeled",
58
+			},
59
+			DriverConfig: &mount.Driver{
60
+				Name: "glusterfs",
61
+				Options: map[string]string{
62
+					"opt": "value",
63
+				},
64
+			},
65
+		},
66
+	}
67
+	mount, err := convertVolumeToMount("normal:/foo:ro", stackVolumes, namespace)
68
+	assert.NilError(t, err)
69
+	assert.DeepEqual(t, mount, expected)
70
+}
71
+
72
+func TestConvertVolumeToMountNamedVolumeExternal(t *testing.T) {
73
+	stackVolumes := volumes{
74
+		"outside": composetypes.VolumeConfig{
75
+			External: composetypes.External{
76
+				External: true,
77
+				Name:     "special",
78
+			},
79
+		},
80
+	}
81
+	namespace := NewNamespace("foo")
82
+	expected := mount.Mount{
83
+		Type:   mount.TypeVolume,
84
+		Source: "special",
85
+		Target: "/foo",
86
+	}
87
+	mount, err := convertVolumeToMount("outside:/foo", stackVolumes, namespace)
88
+	assert.NilError(t, err)
89
+	assert.DeepEqual(t, mount, expected)
90
+}
91
+
92
+func TestConvertVolumeToMountBind(t *testing.T) {
93
+	stackVolumes := volumes{}
94
+	namespace := NewNamespace("foo")
95
+	expected := mount.Mount{
96
+		Type:        mount.TypeBind,
97
+		Source:      "/bar",
98
+		Target:      "/foo",
99
+		ReadOnly:    true,
100
+		BindOptions: &mount.BindOptions{Propagation: mount.PropagationShared},
101
+	}
102
+	mount, err := convertVolumeToMount("/bar:/foo:ro,shared", stackVolumes, namespace)
103
+	assert.NilError(t, err)
104
+	assert.DeepEqual(t, mount, expected)
105
+}
106
+
107
+func TestConvertVolumeToMountVolumeDoesNotExist(t *testing.T) {
108
+	namespace := NewNamespace("foo")
109
+	_, err := convertVolumeToMount("unknown:/foo:ro", volumes{}, namespace)
110
+	assert.Error(t, err, "undefined volume: unknown")
111
+}