Browse code

Move pkg to cli/compose/convert

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

Daniel Nephin authored on 2016/12/06 06:14:08
Showing 16 changed files
... ...
@@ -6,26 +6,26 @@ import (
6 6
 	"github.com/docker/docker/api/types"
7 7
 	"github.com/docker/docker/api/types/filters"
8 8
 	"github.com/docker/docker/api/types/swarm"
9
+	"github.com/docker/docker/cli/compose/convert"
9 10
 	"github.com/docker/docker/client"
10 11
 	"github.com/docker/docker/opts"
11
-	"github.com/docker/docker/pkg/composetransform"
12 12
 )
13 13
 
14 14
 func getStackFilter(namespace string) filters.Args {
15 15
 	filter := filters.NewArgs()
16
-	filter.Add("label", composetransform.LabelNamespace+"="+namespace)
16
+	filter.Add("label", convert.LabelNamespace+"="+namespace)
17 17
 	return filter
18 18
 }
19 19
 
20 20
 func getStackFilterFromOpt(namespace string, opt opts.FilterOpt) filters.Args {
21 21
 	filter := opt.Value()
22
-	filter.Add("label", composetransform.LabelNamespace+"="+namespace)
22
+	filter.Add("label", convert.LabelNamespace+"="+namespace)
23 23
 	return filter
24 24
 }
25 25
 
26 26
 func getAllStacksFilter() filters.Args {
27 27
 	filter := filters.NewArgs()
28
-	filter.Add("label", composetransform.LabelNamespace)
28
+	filter.Add("label", convert.LabelNamespace)
29 29
 	return filter
30 30
 }
31 31
 
... ...
@@ -14,8 +14,8 @@ import (
14 14
 	"github.com/docker/docker/api/types/swarm"
15 15
 	"github.com/docker/docker/cli"
16 16
 	"github.com/docker/docker/cli/command"
17
+	"github.com/docker/docker/cli/compose/convert"
17 18
 	dockerclient "github.com/docker/docker/client"
18
-	"github.com/docker/docker/pkg/composetransform"
19 19
 	"github.com/spf13/cobra"
20 20
 	"golang.org/x/net/context"
21 21
 )
... ...
@@ -114,17 +114,17 @@ func deployCompose(ctx context.Context, dockerCli *command.DockerCli, opts deplo
114 114
 		return err
115 115
 	}
116 116
 
117
-	namespace := composetransform.NewNamespace(opts.namespace)
117
+	namespace := convert.NewNamespace(opts.namespace)
118 118
 
119 119
 	serviceNetworks := getServicesDeclaredNetworks(config.Services)
120
-	networks, externalNetworks := composetransform.ConvertNetworks(namespace, config.Networks, serviceNetworks)
120
+	networks, externalNetworks := convert.Networks(namespace, config.Networks, serviceNetworks)
121 121
 	if err := validateExternalNetworks(ctx, dockerCli, externalNetworks); err != nil {
122 122
 		return err
123 123
 	}
124 124
 	if err := createNetworks(ctx, dockerCli, namespace, networks); err != nil {
125 125
 		return err
126 126
 	}
127
-	services, err := composetransform.ConvertServices(namespace, config)
127
+	services, err := convert.Services(namespace, config)
128 128
 	if err != nil {
129 129
 		return err
130 130
 	}
... ...
@@ -211,7 +211,7 @@ func validateExternalNetworks(
211 211
 func createNetworks(
212 212
 	ctx context.Context,
213 213
 	dockerCli *command.DockerCli,
214
-	namespace composetransform.Namespace,
214
+	namespace convert.Namespace,
215 215
 	networks map[string]types.NetworkCreate,
216 216
 ) error {
217 217
 	client := dockerCli.Client()
... ...
@@ -249,7 +249,7 @@ func deployServices(
249 249
 	ctx context.Context,
250 250
 	dockerCli *command.DockerCli,
251 251
 	services map[string]swarm.ServiceSpec,
252
-	namespace composetransform.Namespace,
252
+	namespace convert.Namespace,
253 253
 	sendAuth bool,
254 254
 ) error {
255 255
 	apiClient := dockerCli.Client()
... ...
@@ -6,7 +6,7 @@ import (
6 6
 	"github.com/docker/docker/api/types"
7 7
 	"github.com/docker/docker/api/types/swarm"
8 8
 	"github.com/docker/docker/cli/command"
9
-	"github.com/docker/docker/pkg/composetransform"
9
+	"github.com/docker/docker/cli/compose/convert"
10 10
 )
11 11
 
12 12
 func deployBundle(ctx context.Context, dockerCli *command.DockerCli, opts deployOptions) error {
... ...
@@ -19,13 +19,13 @@ func deployBundle(ctx context.Context, dockerCli *command.DockerCli, opts deploy
19 19
 		return err
20 20
 	}
21 21
 
22
-	namespace := composetransform.NewNamespace(opts.namespace)
22
+	namespace := convert.NewNamespace(opts.namespace)
23 23
 
24 24
 	networks := make(map[string]types.NetworkCreate)
25 25
 	for _, service := range bundle.Services {
26 26
 		for _, networkName := range service.Networks {
27 27
 			networks[networkName] = types.NetworkCreate{
28
-				Labels: composetransform.AddStackLabel(namespace, nil),
28
+				Labels: convert.AddStackLabel(namespace, nil),
29 29
 			}
30 30
 		}
31 31
 	}
... ...
@@ -53,7 +53,7 @@ func deployBundle(ctx context.Context, dockerCli *command.DockerCli, opts deploy
53 53
 		serviceSpec := swarm.ServiceSpec{
54 54
 			Annotations: swarm.Annotations{
55 55
 				Name:   name,
56
-				Labels: composetransform.AddStackLabel(namespace, service.Labels),
56
+				Labels: convert.AddStackLabel(namespace, service.Labels),
57 57
 			},
58 58
 			TaskTemplate: swarm.TaskSpec{
59 59
 				ContainerSpec: swarm.ContainerSpec{
... ...
@@ -64,7 +64,7 @@ func deployBundle(ctx context.Context, dockerCli *command.DockerCli, opts deploy
64 64
 					// Service Labels will not be copied to Containers
65 65
 					// automatically during the deployment so we apply
66 66
 					// it here.
67
-					Labels: composetransform.AddStackLabel(namespace, nil),
67
+					Labels: convert.AddStackLabel(namespace, nil),
68 68
 				},
69 69
 			},
70 70
 			EndpointSpec: &swarm.EndpointSpec{
... ...
@@ -11,8 +11,8 @@ import (
11 11
 	"github.com/docker/docker/api/types"
12 12
 	"github.com/docker/docker/cli"
13 13
 	"github.com/docker/docker/cli/command"
14
+	"github.com/docker/docker/cli/compose/convert"
14 15
 	"github.com/docker/docker/client"
15
-	"github.com/docker/docker/pkg/composetransform"
16 16
 	"github.com/spf13/cobra"
17 17
 )
18 18
 
... ...
@@ -90,10 +90,10 @@ func getStacks(
90 90
 	m := make(map[string]*stack, 0)
91 91
 	for _, service := range services {
92 92
 		labels := service.Spec.Labels
93
-		name, ok := labels[composetransform.LabelNamespace]
93
+		name, ok := labels[convert.LabelNamespace]
94 94
 		if !ok {
95 95
 			return nil, fmt.Errorf("cannot get label %s for service %s",
96
-				composetransform.LabelNamespace, service.ID)
96
+				convert.LabelNamespace, service.ID)
97 97
 		}
98 98
 		ztack, ok := m[name]
99 99
 		if !ok {
100 100
new file mode 100644
... ...
@@ -0,0 +1,88 @@
0
+package convert
1
+
2
+import (
3
+	composetypes "github.com/aanand/compose-file/types"
4
+	"github.com/docker/docker/api/types"
5
+	networktypes "github.com/docker/docker/api/types/network"
6
+)
7
+
8
+const (
9
+	// LabelNamespace is the label used to track stack resources
10
+	LabelNamespace = "com.docker.stack.namespace"
11
+)
12
+
13
+// Namespace mangles names by prepending the name
14
+type Namespace struct {
15
+	name string
16
+}
17
+
18
+// Scope prepends the namespace to a name
19
+func (n Namespace) Scope(name string) string {
20
+	return n.name + "_" + name
21
+}
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
+
33
+// AddStackLabel returns labels with the namespace label added
34
+func AddStackLabel(namespace Namespace, labels map[string]string) map[string]string {
35
+	if labels == nil {
36
+		labels = make(map[string]string)
37
+	}
38
+	labels[LabelNamespace] = namespace.name
39
+	return labels
40
+}
41
+
42
+type networkMap map[string]composetypes.NetworkConfig
43
+
44
+// Networks converts networks from the compose-file type to the engine API type
45
+func Networks(
46
+	namespace Namespace,
47
+	networks networkMap,
48
+	servicesNetworks map[string]struct{},
49
+) (map[string]types.NetworkCreate, []string) {
50
+	if networks == nil {
51
+		networks = make(map[string]composetypes.NetworkConfig)
52
+	}
53
+
54
+	externalNetworks := []string{}
55
+	result := make(map[string]types.NetworkCreate)
56
+
57
+	for internalName := range servicesNetworks {
58
+		network := networks[internalName]
59
+		if network.External.External {
60
+			externalNetworks = append(externalNetworks, network.External.Name)
61
+			continue
62
+		}
63
+
64
+		createOpts := types.NetworkCreate{
65
+			Labels:  AddStackLabel(namespace, network.Labels),
66
+			Driver:  network.Driver,
67
+			Options: network.DriverOpts,
68
+		}
69
+
70
+		if network.Ipam.Driver != "" || len(network.Ipam.Config) > 0 {
71
+			createOpts.IPAM = &networktypes.IPAM{}
72
+		}
73
+
74
+		if network.Ipam.Driver != "" {
75
+			createOpts.IPAM.Driver = network.Ipam.Driver
76
+		}
77
+		for _, ipamConfig := range network.Ipam.Config {
78
+			config := networktypes.IPAMConfig{
79
+				Subnet: ipamConfig.Subnet,
80
+			}
81
+			createOpts.IPAM.Config = append(createOpts.IPAM.Config, config)
82
+		}
83
+		result[internalName] = createOpts
84
+	}
85
+
86
+	return result, externalNetworks
87
+}
0 88
new file mode 100644
... ...
@@ -0,0 +1,90 @@
0
+package convert
1
+
2
+import (
3
+	"testing"
4
+
5
+	composetypes "github.com/aanand/compose-file/types"
6
+	"github.com/docker/docker/api/types"
7
+	"github.com/docker/docker/api/types/network"
8
+	"github.com/docker/docker/pkg/testutil/assert"
9
+)
10
+
11
+func TestNamespaceScope(t *testing.T) {
12
+	scoped := Namespace{name: "foo"}.Scope("bar")
13
+	assert.Equal(t, scoped, "foo_bar")
14
+}
15
+
16
+func TestAddStackLabel(t *testing.T) {
17
+	labels := map[string]string{
18
+		"something": "labeled",
19
+	}
20
+	actual := AddStackLabel(Namespace{name: "foo"}, labels)
21
+	expected := map[string]string{
22
+		"something":    "labeled",
23
+		LabelNamespace: "foo",
24
+	}
25
+	assert.DeepEqual(t, actual, expected)
26
+}
27
+
28
+func TestNetworks(t *testing.T) {
29
+	namespace := Namespace{name: "foo"}
30
+	source := networkMap{
31
+		"normal": composetypes.NetworkConfig{
32
+			Driver: "overlay",
33
+			DriverOpts: map[string]string{
34
+				"opt": "value",
35
+			},
36
+			Ipam: composetypes.IPAMConfig{
37
+				Driver: "driver",
38
+				Config: []*composetypes.IPAMPool{
39
+					{
40
+						Subnet: "10.0.0.0",
41
+					},
42
+				},
43
+			},
44
+			Labels: map[string]string{
45
+				"something": "labeled",
46
+			},
47
+		},
48
+		"outside": composetypes.NetworkConfig{
49
+			External: composetypes.External{
50
+				External: true,
51
+				Name:     "special",
52
+			},
53
+		},
54
+	}
55
+	expected := map[string]types.NetworkCreate{
56
+		"default": {
57
+			Labels: map[string]string{
58
+				LabelNamespace: "foo",
59
+			},
60
+		},
61
+		"normal": {
62
+			Driver: "overlay",
63
+			IPAM: &network.IPAM{
64
+				Driver: "driver",
65
+				Config: []network.IPAMConfig{
66
+					{
67
+						Subnet: "10.0.0.0",
68
+					},
69
+				},
70
+			},
71
+			Options: map[string]string{
72
+				"opt": "value",
73
+			},
74
+			Labels: map[string]string{
75
+				LabelNamespace: "foo",
76
+				"something":    "labeled",
77
+			},
78
+		},
79
+	}
80
+
81
+	serviceNetworks := map[string]struct{}{
82
+		"default": {},
83
+		"normal":  {},
84
+		"outside": {},
85
+	}
86
+	networks, externals := Networks(namespace, source, serviceNetworks)
87
+	assert.DeepEqual(t, networks, expected)
88
+	assert.DeepEqual(t, externals, []string{"special"})
89
+}
0 90
new file mode 100644
... ...
@@ -0,0 +1,338 @@
0
+package convert
1
+
2
+import (
3
+	"fmt"
4
+	"time"
5
+
6
+	composetypes "github.com/aanand/compose-file/types"
7
+	"github.com/docker/docker/api/types/container"
8
+	"github.com/docker/docker/api/types/swarm"
9
+	"github.com/docker/docker/opts"
10
+	runconfigopts "github.com/docker/docker/runconfig/opts"
11
+	"github.com/docker/go-connections/nat"
12
+)
13
+
14
+// Services from compose-file types to engine API types
15
+func Services(
16
+	namespace Namespace,
17
+	config *composetypes.Config,
18
+) (map[string]swarm.ServiceSpec, error) {
19
+	result := make(map[string]swarm.ServiceSpec)
20
+
21
+	services := config.Services
22
+	volumes := config.Volumes
23
+	networks := config.Networks
24
+
25
+	for _, service := range services {
26
+		serviceSpec, err := convertService(namespace, service, networks, volumes)
27
+		if err != nil {
28
+			return nil, err
29
+		}
30
+		result[service.Name] = serviceSpec
31
+	}
32
+
33
+	return result, nil
34
+}
35
+
36
+func convertService(
37
+	namespace Namespace,
38
+	service composetypes.ServiceConfig,
39
+	networkConfigs map[string]composetypes.NetworkConfig,
40
+	volumes map[string]composetypes.VolumeConfig,
41
+) (swarm.ServiceSpec, error) {
42
+	name := namespace.Scope(service.Name)
43
+
44
+	endpoint, err := convertEndpointSpec(service.Ports)
45
+	if err != nil {
46
+		return swarm.ServiceSpec{}, err
47
+	}
48
+
49
+	mode, err := convertDeployMode(service.Deploy.Mode, service.Deploy.Replicas)
50
+	if err != nil {
51
+		return swarm.ServiceSpec{}, err
52
+	}
53
+
54
+	mounts, err := Volumes(service.Volumes, volumes, namespace)
55
+	if err != nil {
56
+		// TODO: better error message (include service name)
57
+		return swarm.ServiceSpec{}, err
58
+	}
59
+
60
+	resources, err := convertResources(service.Deploy.Resources)
61
+	if err != nil {
62
+		return swarm.ServiceSpec{}, err
63
+	}
64
+
65
+	restartPolicy, err := convertRestartPolicy(
66
+		service.Restart, service.Deploy.RestartPolicy)
67
+	if err != nil {
68
+		return swarm.ServiceSpec{}, err
69
+	}
70
+
71
+	healthcheck, err := convertHealthcheck(service.HealthCheck)
72
+	if err != nil {
73
+		return swarm.ServiceSpec{}, err
74
+	}
75
+
76
+	networks, err := convertServiceNetworks(service.Networks, networkConfigs, namespace, service.Name)
77
+	if err != nil {
78
+		return swarm.ServiceSpec{}, err
79
+	}
80
+
81
+	var logDriver *swarm.Driver
82
+	if service.Logging != nil {
83
+		logDriver = &swarm.Driver{
84
+			Name:    service.Logging.Driver,
85
+			Options: service.Logging.Options,
86
+		}
87
+	}
88
+
89
+	serviceSpec := swarm.ServiceSpec{
90
+		Annotations: swarm.Annotations{
91
+			Name:   name,
92
+			Labels: AddStackLabel(namespace, service.Deploy.Labels),
93
+		},
94
+		TaskTemplate: swarm.TaskSpec{
95
+			ContainerSpec: swarm.ContainerSpec{
96
+				Image:           service.Image,
97
+				Command:         service.Entrypoint,
98
+				Args:            service.Command,
99
+				Hostname:        service.Hostname,
100
+				Hosts:           convertExtraHosts(service.ExtraHosts),
101
+				Healthcheck:     healthcheck,
102
+				Env:             convertEnvironment(service.Environment),
103
+				Labels:          AddStackLabel(namespace, service.Labels),
104
+				Dir:             service.WorkingDir,
105
+				User:            service.User,
106
+				Mounts:          mounts,
107
+				StopGracePeriod: service.StopGracePeriod,
108
+				TTY:             service.Tty,
109
+				OpenStdin:       service.StdinOpen,
110
+			},
111
+			LogDriver:     logDriver,
112
+			Resources:     resources,
113
+			RestartPolicy: restartPolicy,
114
+			Placement: &swarm.Placement{
115
+				Constraints: service.Deploy.Placement.Constraints,
116
+			},
117
+		},
118
+		EndpointSpec: endpoint,
119
+		Mode:         mode,
120
+		Networks:     networks,
121
+		UpdateConfig: convertUpdateConfig(service.Deploy.UpdateConfig),
122
+	}
123
+
124
+	return serviceSpec, nil
125
+}
126
+
127
+func convertServiceNetworks(
128
+	networks map[string]*composetypes.ServiceNetworkConfig,
129
+	networkConfigs networkMap,
130
+	namespace Namespace,
131
+	name string,
132
+) ([]swarm.NetworkAttachmentConfig, error) {
133
+	if len(networks) == 0 {
134
+		return []swarm.NetworkAttachmentConfig{
135
+			{
136
+				Target:  namespace.Scope("default"),
137
+				Aliases: []string{name},
138
+			},
139
+		}, nil
140
+	}
141
+
142
+	nets := []swarm.NetworkAttachmentConfig{}
143
+	for networkName, network := range networks {
144
+		networkConfig, ok := networkConfigs[networkName]
145
+		if !ok {
146
+			return []swarm.NetworkAttachmentConfig{}, fmt.Errorf(
147
+				"service %q references network %q, which is not declared", name, networkName)
148
+		}
149
+		var aliases []string
150
+		if network != nil {
151
+			aliases = network.Aliases
152
+		}
153
+		target := namespace.Scope(networkName)
154
+		if networkConfig.External.External {
155
+			target = networkConfig.External.Name
156
+		}
157
+		nets = append(nets, swarm.NetworkAttachmentConfig{
158
+			Target:  target,
159
+			Aliases: append(aliases, name),
160
+		})
161
+	}
162
+	return nets, nil
163
+}
164
+
165
+func convertExtraHosts(extraHosts map[string]string) []string {
166
+	hosts := []string{}
167
+	for host, ip := range extraHosts {
168
+		hosts = append(hosts, fmt.Sprintf("%s %s", ip, host))
169
+	}
170
+	return hosts
171
+}
172
+
173
+func convertHealthcheck(healthcheck *composetypes.HealthCheckConfig) (*container.HealthConfig, error) {
174
+	if healthcheck == nil {
175
+		return nil, nil
176
+	}
177
+	var (
178
+		err               error
179
+		timeout, interval time.Duration
180
+		retries           int
181
+	)
182
+	if healthcheck.Disable {
183
+		if len(healthcheck.Test) != 0 {
184
+			return nil, fmt.Errorf("test and disable can't be set at the same time")
185
+		}
186
+		return &container.HealthConfig{
187
+			Test: []string{"NONE"},
188
+		}, nil
189
+
190
+	}
191
+	if healthcheck.Timeout != "" {
192
+		timeout, err = time.ParseDuration(healthcheck.Timeout)
193
+		if err != nil {
194
+			return nil, err
195
+		}
196
+	}
197
+	if healthcheck.Interval != "" {
198
+		interval, err = time.ParseDuration(healthcheck.Interval)
199
+		if err != nil {
200
+			return nil, err
201
+		}
202
+	}
203
+	if healthcheck.Retries != nil {
204
+		retries = int(*healthcheck.Retries)
205
+	}
206
+	return &container.HealthConfig{
207
+		Test:     healthcheck.Test,
208
+		Timeout:  timeout,
209
+		Interval: interval,
210
+		Retries:  retries,
211
+	}, nil
212
+}
213
+
214
+func convertRestartPolicy(restart string, source *composetypes.RestartPolicy) (*swarm.RestartPolicy, error) {
215
+	// TODO: log if restart is being ignored
216
+	if source == nil {
217
+		policy, err := runconfigopts.ParseRestartPolicy(restart)
218
+		if err != nil {
219
+			return nil, err
220
+		}
221
+		switch {
222
+		case policy.IsNone():
223
+			return nil, nil
224
+		case policy.IsAlways(), policy.IsUnlessStopped():
225
+			return &swarm.RestartPolicy{
226
+				Condition: swarm.RestartPolicyConditionAny,
227
+			}, nil
228
+		case policy.IsOnFailure():
229
+			attempts := uint64(policy.MaximumRetryCount)
230
+			return &swarm.RestartPolicy{
231
+				Condition:   swarm.RestartPolicyConditionOnFailure,
232
+				MaxAttempts: &attempts,
233
+			}, nil
234
+		default:
235
+			return nil, fmt.Errorf("unknown restart policy: %s", restart)
236
+		}
237
+	}
238
+	return &swarm.RestartPolicy{
239
+		Condition:   swarm.RestartPolicyCondition(source.Condition),
240
+		Delay:       source.Delay,
241
+		MaxAttempts: source.MaxAttempts,
242
+		Window:      source.Window,
243
+	}, nil
244
+}
245
+
246
+func convertUpdateConfig(source *composetypes.UpdateConfig) *swarm.UpdateConfig {
247
+	if source == nil {
248
+		return nil
249
+	}
250
+	parallel := uint64(1)
251
+	if source.Parallelism != nil {
252
+		parallel = *source.Parallelism
253
+	}
254
+	return &swarm.UpdateConfig{
255
+		Parallelism:     parallel,
256
+		Delay:           source.Delay,
257
+		FailureAction:   source.FailureAction,
258
+		Monitor:         source.Monitor,
259
+		MaxFailureRatio: source.MaxFailureRatio,
260
+	}
261
+}
262
+
263
+func convertResources(source composetypes.Resources) (*swarm.ResourceRequirements, error) {
264
+	resources := &swarm.ResourceRequirements{}
265
+	var err error
266
+	if source.Limits != nil {
267
+		var cpus int64
268
+		if source.Limits.NanoCPUs != "" {
269
+			cpus, err = opts.ParseCPUs(source.Limits.NanoCPUs)
270
+			if err != nil {
271
+				return nil, err
272
+			}
273
+		}
274
+		resources.Limits = &swarm.Resources{
275
+			NanoCPUs:    cpus,
276
+			MemoryBytes: int64(source.Limits.MemoryBytes),
277
+		}
278
+	}
279
+	if source.Reservations != nil {
280
+		var cpus int64
281
+		if source.Reservations.NanoCPUs != "" {
282
+			cpus, err = opts.ParseCPUs(source.Reservations.NanoCPUs)
283
+			if err != nil {
284
+				return nil, err
285
+			}
286
+		}
287
+		resources.Reservations = &swarm.Resources{
288
+			NanoCPUs:    cpus,
289
+			MemoryBytes: int64(source.Reservations.MemoryBytes),
290
+		}
291
+	}
292
+	return resources, nil
293
+
294
+}
295
+
296
+func convertEndpointSpec(source []string) (*swarm.EndpointSpec, error) {
297
+	portConfigs := []swarm.PortConfig{}
298
+	ports, portBindings, err := nat.ParsePortSpecs(source)
299
+	if err != nil {
300
+		return nil, err
301
+	}
302
+
303
+	for port := range ports {
304
+		portConfigs = append(
305
+			portConfigs,
306
+			opts.ConvertPortToPortConfig(port, portBindings)...)
307
+	}
308
+
309
+	return &swarm.EndpointSpec{Ports: portConfigs}, nil
310
+}
311
+
312
+func convertEnvironment(source map[string]string) []string {
313
+	var output []string
314
+
315
+	for name, value := range source {
316
+		output = append(output, fmt.Sprintf("%s=%s", name, value))
317
+	}
318
+
319
+	return output
320
+}
321
+
322
+func convertDeployMode(mode string, replicas *uint64) (swarm.ServiceMode, error) {
323
+	serviceMode := swarm.ServiceMode{}
324
+
325
+	switch mode {
326
+	case "global":
327
+		if replicas != nil {
328
+			return serviceMode, fmt.Errorf("replicas can only be used with replicated mode")
329
+		}
330
+		serviceMode.Global = &swarm.GlobalService{}
331
+	case "replicated", "":
332
+		serviceMode.Replicated = &swarm.ReplicatedService{Replicas: replicas}
333
+	default:
334
+		return serviceMode, fmt.Errorf("Unknown mode: %s", mode)
335
+	}
336
+	return serviceMode, nil
337
+}
0 338
new file mode 100644
... ...
@@ -0,0 +1,193 @@
0
+package convert
1
+
2
+import (
3
+	"sort"
4
+	"strings"
5
+	"testing"
6
+	"time"
7
+
8
+	composetypes "github.com/aanand/compose-file/types"
9
+	"github.com/docker/docker/api/types/container"
10
+	"github.com/docker/docker/api/types/swarm"
11
+	"github.com/docker/docker/pkg/testutil/assert"
12
+)
13
+
14
+func TestConvertRestartPolicyFromNone(t *testing.T) {
15
+	policy, err := convertRestartPolicy("no", nil)
16
+	assert.NilError(t, err)
17
+	assert.Equal(t, policy, (*swarm.RestartPolicy)(nil))
18
+}
19
+
20
+func TestConvertRestartPolicyFromUnknown(t *testing.T) {
21
+	_, err := convertRestartPolicy("unknown", nil)
22
+	assert.Error(t, err, "unknown restart policy: unknown")
23
+}
24
+
25
+func TestConvertRestartPolicyFromAlways(t *testing.T) {
26
+	policy, err := convertRestartPolicy("always", nil)
27
+	expected := &swarm.RestartPolicy{
28
+		Condition: swarm.RestartPolicyConditionAny,
29
+	}
30
+	assert.NilError(t, err)
31
+	assert.DeepEqual(t, policy, expected)
32
+}
33
+
34
+func TestConvertRestartPolicyFromFailure(t *testing.T) {
35
+	policy, err := convertRestartPolicy("on-failure:4", nil)
36
+	attempts := uint64(4)
37
+	expected := &swarm.RestartPolicy{
38
+		Condition:   swarm.RestartPolicyConditionOnFailure,
39
+		MaxAttempts: &attempts,
40
+	}
41
+	assert.NilError(t, err)
42
+	assert.DeepEqual(t, policy, expected)
43
+}
44
+
45
+func TestConvertEnvironment(t *testing.T) {
46
+	source := map[string]string{
47
+		"foo": "bar",
48
+		"key": "value",
49
+	}
50
+	env := convertEnvironment(source)
51
+	sort.Strings(env)
52
+	assert.DeepEqual(t, env, []string{"foo=bar", "key=value"})
53
+}
54
+
55
+func TestConvertResourcesFull(t *testing.T) {
56
+	source := composetypes.Resources{
57
+		Limits: &composetypes.Resource{
58
+			NanoCPUs:    "0.003",
59
+			MemoryBytes: composetypes.UnitBytes(300000000),
60
+		},
61
+		Reservations: &composetypes.Resource{
62
+			NanoCPUs:    "0.002",
63
+			MemoryBytes: composetypes.UnitBytes(200000000),
64
+		},
65
+	}
66
+	resources, err := convertResources(source)
67
+	assert.NilError(t, err)
68
+
69
+	expected := &swarm.ResourceRequirements{
70
+		Limits: &swarm.Resources{
71
+			NanoCPUs:    3000000,
72
+			MemoryBytes: 300000000,
73
+		},
74
+		Reservations: &swarm.Resources{
75
+			NanoCPUs:    2000000,
76
+			MemoryBytes: 200000000,
77
+		},
78
+	}
79
+	assert.DeepEqual(t, resources, expected)
80
+}
81
+
82
+func TestConvertHealthcheck(t *testing.T) {
83
+	retries := uint64(10)
84
+	source := &composetypes.HealthCheckConfig{
85
+		Test:     []string{"EXEC", "touch", "/foo"},
86
+		Timeout:  "30s",
87
+		Interval: "2ms",
88
+		Retries:  &retries,
89
+	}
90
+	expected := &container.HealthConfig{
91
+		Test:     source.Test,
92
+		Timeout:  30 * time.Second,
93
+		Interval: 2 * time.Millisecond,
94
+		Retries:  10,
95
+	}
96
+
97
+	healthcheck, err := convertHealthcheck(source)
98
+	assert.NilError(t, err)
99
+	assert.DeepEqual(t, healthcheck, expected)
100
+}
101
+
102
+func TestConvertHealthcheckDisable(t *testing.T) {
103
+	source := &composetypes.HealthCheckConfig{Disable: true}
104
+	expected := &container.HealthConfig{
105
+		Test: []string{"NONE"},
106
+	}
107
+
108
+	healthcheck, err := convertHealthcheck(source)
109
+	assert.NilError(t, err)
110
+	assert.DeepEqual(t, healthcheck, expected)
111
+}
112
+
113
+func TestConvertHealthcheckDisableWithTest(t *testing.T) {
114
+	source := &composetypes.HealthCheckConfig{
115
+		Disable: true,
116
+		Test:    []string{"EXEC", "touch"},
117
+	}
118
+	_, err := convertHealthcheck(source)
119
+	assert.Error(t, err, "test and disable can't be set")
120
+}
121
+
122
+func TestConvertServiceNetworksOnlyDefault(t *testing.T) {
123
+	networkConfigs := networkMap{}
124
+	networks := map[string]*composetypes.ServiceNetworkConfig{}
125
+
126
+	configs, err := convertServiceNetworks(
127
+		networks, networkConfigs, NewNamespace("foo"), "service")
128
+
129
+	expected := []swarm.NetworkAttachmentConfig{
130
+		{
131
+			Target:  "foo_default",
132
+			Aliases: []string{"service"},
133
+		},
134
+	}
135
+
136
+	assert.NilError(t, err)
137
+	assert.DeepEqual(t, configs, expected)
138
+}
139
+
140
+func TestConvertServiceNetworks(t *testing.T) {
141
+	networkConfigs := networkMap{
142
+		"front": composetypes.NetworkConfig{
143
+			External: composetypes.External{
144
+				External: true,
145
+				Name:     "fronttier",
146
+			},
147
+		},
148
+		"back": composetypes.NetworkConfig{},
149
+	}
150
+	networks := map[string]*composetypes.ServiceNetworkConfig{
151
+		"front": {
152
+			Aliases: []string{"something"},
153
+		},
154
+		"back": {
155
+			Aliases: []string{"other"},
156
+		},
157
+	}
158
+
159
+	configs, err := convertServiceNetworks(
160
+		networks, networkConfigs, NewNamespace("foo"), "service")
161
+
162
+	expected := []swarm.NetworkAttachmentConfig{
163
+		{
164
+			Target:  "foo_back",
165
+			Aliases: []string{"other", "service"},
166
+		},
167
+		{
168
+			Target:  "fronttier",
169
+			Aliases: []string{"something", "service"},
170
+		},
171
+	}
172
+
173
+	sortedConfigs := byTargetSort(configs)
174
+	sort.Sort(&sortedConfigs)
175
+
176
+	assert.NilError(t, err)
177
+	assert.DeepEqual(t, []swarm.NetworkAttachmentConfig(sortedConfigs), expected)
178
+}
179
+
180
+type byTargetSort []swarm.NetworkAttachmentConfig
181
+
182
+func (s byTargetSort) Len() int {
183
+	return len(s)
184
+}
185
+
186
+func (s byTargetSort) Less(i, j int) bool {
187
+	return strings.Compare(s[i].Target, s[j].Target) < 0
188
+}
189
+
190
+func (s byTargetSort) Swap(i, j int) {
191
+	s[i], s[j] = s[j], s[i]
192
+}
0 193
new file mode 100644
... ...
@@ -0,0 +1,120 @@
0
+package convert
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
+// Volumes from compose-file types to engine api types
13
+func Volumes(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
+	for _, part := range parts {
34
+		if strings.TrimSpace(part) == "" {
35
+			return mount.Mount{}, fmt.Errorf("invalid volume: %s", volumeSpec)
36
+		}
37
+	}
38
+
39
+	switch len(parts) {
40
+	case 3:
41
+		source = parts[0]
42
+		target = parts[1]
43
+		mode = strings.Split(parts[2], ",")
44
+	case 2:
45
+		source = parts[0]
46
+		target = parts[1]
47
+	case 1:
48
+		target = parts[0]
49
+	}
50
+
51
+	// TODO: catch Windows paths here
52
+	if strings.HasPrefix(source, "/") {
53
+		return mount.Mount{
54
+			Type:        mount.TypeBind,
55
+			Source:      source,
56
+			Target:      target,
57
+			ReadOnly:    isReadOnly(mode),
58
+			BindOptions: getBindOptions(mode),
59
+		}, nil
60
+	}
61
+
62
+	stackVolume, exists := stackVolumes[source]
63
+	if !exists {
64
+		return mount.Mount{}, fmt.Errorf("undefined volume: %s", source)
65
+	}
66
+
67
+	var volumeOptions *mount.VolumeOptions
68
+	if stackVolume.External.Name != "" {
69
+		source = stackVolume.External.Name
70
+	} else {
71
+		volumeOptions = &mount.VolumeOptions{
72
+			Labels: AddStackLabel(namespace, stackVolume.Labels),
73
+			NoCopy: isNoCopy(mode),
74
+		}
75
+
76
+		if stackVolume.Driver != "" {
77
+			volumeOptions.DriverConfig = &mount.Driver{
78
+				Name:    stackVolume.Driver,
79
+				Options: stackVolume.DriverOpts,
80
+			}
81
+		}
82
+		source = namespace.Scope(source)
83
+	}
84
+	return mount.Mount{
85
+		Type:          mount.TypeVolume,
86
+		Source:        source,
87
+		Target:        target,
88
+		ReadOnly:      isReadOnly(mode),
89
+		VolumeOptions: volumeOptions,
90
+	}, nil
91
+}
92
+
93
+func modeHas(mode []string, field string) bool {
94
+	for _, item := range mode {
95
+		if item == field {
96
+			return true
97
+		}
98
+	}
99
+	return false
100
+}
101
+
102
+func isReadOnly(mode []string) bool {
103
+	return modeHas(mode, "ro")
104
+}
105
+
106
+func isNoCopy(mode []string) bool {
107
+	return modeHas(mode, "nocopy")
108
+}
109
+
110
+func getBindOptions(mode []string) *mount.BindOptions {
111
+	for _, item := range mode {
112
+		for _, propagation := range mount.Propagations {
113
+			if mount.Propagation(item) == propagation {
114
+				return &mount.BindOptions{Propagation: mount.Propagation(item)}
115
+			}
116
+		}
117
+	}
118
+	return nil
119
+}
0 120
new file mode 100644
... ...
@@ -0,0 +1,112 @@
0
+package convert
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, (*mount.BindOptions)(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
+}
0 112
deleted file mode 100644
... ...
@@ -1,88 +0,0 @@
1
-package composetransform
2
-
3
-import (
4
-	composetypes "github.com/aanand/compose-file/types"
5
-	"github.com/docker/docker/api/types"
6
-	networktypes "github.com/docker/docker/api/types/network"
7
-)
8
-
9
-const (
10
-	// LabelNamespace is the label used to track stack resources
11
-	LabelNamespace = "com.docker.stack.namespace"
12
-)
13
-
14
-// Namespace mangles names by prepending the name
15
-type Namespace struct {
16
-	name string
17
-}
18
-
19
-// Scope prepends the namespace to a name
20
-func (n Namespace) Scope(name string) string {
21
-	return n.name + "_" + name
22
-}
23
-
24
-// Name returns the name of the namespace
25
-func (n Namespace) Name() string {
26
-	return n.name
27
-}
28
-
29
-// NewNamespace returns a new Namespace for scoping of names
30
-func NewNamespace(name string) Namespace {
31
-	return Namespace{name: name}
32
-}
33
-
34
-// AddStackLabel returns labels with the namespace label added
35
-func AddStackLabel(namespace Namespace, labels map[string]string) map[string]string {
36
-	if labels == nil {
37
-		labels = make(map[string]string)
38
-	}
39
-	labels[LabelNamespace] = namespace.name
40
-	return labels
41
-}
42
-
43
-type networkMap map[string]composetypes.NetworkConfig
44
-
45
-// ConvertNetworks from the compose-file type to the engine API type
46
-func ConvertNetworks(
47
-	namespace Namespace,
48
-	networks networkMap,
49
-	servicesNetworks map[string]struct{},
50
-) (map[string]types.NetworkCreate, []string) {
51
-	if networks == nil {
52
-		networks = make(map[string]composetypes.NetworkConfig)
53
-	}
54
-
55
-	externalNetworks := []string{}
56
-	result := make(map[string]types.NetworkCreate)
57
-
58
-	for internalName := range servicesNetworks {
59
-		network := networks[internalName]
60
-		if network.External.External {
61
-			externalNetworks = append(externalNetworks, network.External.Name)
62
-			continue
63
-		}
64
-
65
-		createOpts := types.NetworkCreate{
66
-			Labels:  AddStackLabel(namespace, network.Labels),
67
-			Driver:  network.Driver,
68
-			Options: network.DriverOpts,
69
-		}
70
-
71
-		if network.Ipam.Driver != "" || len(network.Ipam.Config) > 0 {
72
-			createOpts.IPAM = &networktypes.IPAM{}
73
-		}
74
-
75
-		if network.Ipam.Driver != "" {
76
-			createOpts.IPAM.Driver = network.Ipam.Driver
77
-		}
78
-		for _, ipamConfig := range network.Ipam.Config {
79
-			config := networktypes.IPAMConfig{
80
-				Subnet: ipamConfig.Subnet,
81
-			}
82
-			createOpts.IPAM.Config = append(createOpts.IPAM.Config, config)
83
-		}
84
-		result[internalName] = createOpts
85
-	}
86
-
87
-	return result, externalNetworks
88
-}
89 1
deleted file mode 100644
... ...
@@ -1,90 +0,0 @@
1
-package composetransform
2
-
3
-import (
4
-	"testing"
5
-
6
-	composetypes "github.com/aanand/compose-file/types"
7
-	"github.com/docker/docker/api/types"
8
-	"github.com/docker/docker/api/types/network"
9
-	"github.com/docker/docker/pkg/testutil/assert"
10
-)
11
-
12
-func TestNamespaceScope(t *testing.T) {
13
-	scoped := Namespace{name: "foo"}.Scope("bar")
14
-	assert.Equal(t, scoped, "foo_bar")
15
-}
16
-
17
-func TestAddStackLabel(t *testing.T) {
18
-	labels := map[string]string{
19
-		"something": "labeled",
20
-	}
21
-	actual := AddStackLabel(Namespace{name: "foo"}, labels)
22
-	expected := map[string]string{
23
-		"something":    "labeled",
24
-		LabelNamespace: "foo",
25
-	}
26
-	assert.DeepEqual(t, actual, expected)
27
-}
28
-
29
-func TestConvertNetworks(t *testing.T) {
30
-	namespace := Namespace{name: "foo"}
31
-	source := networkMap{
32
-		"normal": composetypes.NetworkConfig{
33
-			Driver: "overlay",
34
-			DriverOpts: map[string]string{
35
-				"opt": "value",
36
-			},
37
-			Ipam: composetypes.IPAMConfig{
38
-				Driver: "driver",
39
-				Config: []*composetypes.IPAMPool{
40
-					{
41
-						Subnet: "10.0.0.0",
42
-					},
43
-				},
44
-			},
45
-			Labels: map[string]string{
46
-				"something": "labeled",
47
-			},
48
-		},
49
-		"outside": composetypes.NetworkConfig{
50
-			External: composetypes.External{
51
-				External: true,
52
-				Name:     "special",
53
-			},
54
-		},
55
-	}
56
-	expected := map[string]types.NetworkCreate{
57
-		"default": {
58
-			Labels: map[string]string{
59
-				LabelNamespace: "foo",
60
-			},
61
-		},
62
-		"normal": {
63
-			Driver: "overlay",
64
-			IPAM: &network.IPAM{
65
-				Driver: "driver",
66
-				Config: []network.IPAMConfig{
67
-					{
68
-						Subnet: "10.0.0.0",
69
-					},
70
-				},
71
-			},
72
-			Options: map[string]string{
73
-				"opt": "value",
74
-			},
75
-			Labels: map[string]string{
76
-				LabelNamespace: "foo",
77
-				"something":    "labeled",
78
-			},
79
-		},
80
-	}
81
-
82
-	serviceNetworks := map[string]struct{}{
83
-		"default": {},
84
-		"normal":  {},
85
-		"outside": {},
86
-	}
87
-	networks, externals := ConvertNetworks(namespace, source, serviceNetworks)
88
-	assert.DeepEqual(t, networks, expected)
89
-	assert.DeepEqual(t, externals, []string{"special"})
90
-}
91 1
deleted file mode 100644
... ...
@@ -1,338 +0,0 @@
1
-package composetransform
2
-
3
-import (
4
-	"fmt"
5
-	"time"
6
-
7
-	composetypes "github.com/aanand/compose-file/types"
8
-	"github.com/docker/docker/api/types/container"
9
-	"github.com/docker/docker/api/types/swarm"
10
-	"github.com/docker/docker/opts"
11
-	runconfigopts "github.com/docker/docker/runconfig/opts"
12
-	"github.com/docker/go-connections/nat"
13
-)
14
-
15
-// ConvertServices from compose-file types to engine API types
16
-func ConvertServices(
17
-	namespace Namespace,
18
-	config *composetypes.Config,
19
-) (map[string]swarm.ServiceSpec, error) {
20
-	result := make(map[string]swarm.ServiceSpec)
21
-
22
-	services := config.Services
23
-	volumes := config.Volumes
24
-	networks := config.Networks
25
-
26
-	for _, service := range services {
27
-		serviceSpec, err := convertService(namespace, service, networks, volumes)
28
-		if err != nil {
29
-			return nil, err
30
-		}
31
-		result[service.Name] = serviceSpec
32
-	}
33
-
34
-	return result, nil
35
-}
36
-
37
-func convertService(
38
-	namespace Namespace,
39
-	service composetypes.ServiceConfig,
40
-	networkConfigs map[string]composetypes.NetworkConfig,
41
-	volumes map[string]composetypes.VolumeConfig,
42
-) (swarm.ServiceSpec, error) {
43
-	name := namespace.Scope(service.Name)
44
-
45
-	endpoint, err := convertEndpointSpec(service.Ports)
46
-	if err != nil {
47
-		return swarm.ServiceSpec{}, err
48
-	}
49
-
50
-	mode, err := convertDeployMode(service.Deploy.Mode, service.Deploy.Replicas)
51
-	if err != nil {
52
-		return swarm.ServiceSpec{}, err
53
-	}
54
-
55
-	mounts, err := ConvertVolumes(service.Volumes, volumes, namespace)
56
-	if err != nil {
57
-		// TODO: better error message (include service name)
58
-		return swarm.ServiceSpec{}, err
59
-	}
60
-
61
-	resources, err := convertResources(service.Deploy.Resources)
62
-	if err != nil {
63
-		return swarm.ServiceSpec{}, err
64
-	}
65
-
66
-	restartPolicy, err := convertRestartPolicy(
67
-		service.Restart, service.Deploy.RestartPolicy)
68
-	if err != nil {
69
-		return swarm.ServiceSpec{}, err
70
-	}
71
-
72
-	healthcheck, err := convertHealthcheck(service.HealthCheck)
73
-	if err != nil {
74
-		return swarm.ServiceSpec{}, err
75
-	}
76
-
77
-	networks, err := convertServiceNetworks(service.Networks, networkConfigs, namespace, service.Name)
78
-	if err != nil {
79
-		return swarm.ServiceSpec{}, err
80
-	}
81
-
82
-	var logDriver *swarm.Driver
83
-	if service.Logging != nil {
84
-		logDriver = &swarm.Driver{
85
-			Name:    service.Logging.Driver,
86
-			Options: service.Logging.Options,
87
-		}
88
-	}
89
-
90
-	serviceSpec := swarm.ServiceSpec{
91
-		Annotations: swarm.Annotations{
92
-			Name:   name,
93
-			Labels: AddStackLabel(namespace, service.Deploy.Labels),
94
-		},
95
-		TaskTemplate: swarm.TaskSpec{
96
-			ContainerSpec: swarm.ContainerSpec{
97
-				Image:           service.Image,
98
-				Command:         service.Entrypoint,
99
-				Args:            service.Command,
100
-				Hostname:        service.Hostname,
101
-				Hosts:           convertExtraHosts(service.ExtraHosts),
102
-				Healthcheck:     healthcheck,
103
-				Env:             convertEnvironment(service.Environment),
104
-				Labels:          AddStackLabel(namespace, service.Labels),
105
-				Dir:             service.WorkingDir,
106
-				User:            service.User,
107
-				Mounts:          mounts,
108
-				StopGracePeriod: service.StopGracePeriod,
109
-				TTY:             service.Tty,
110
-				OpenStdin:       service.StdinOpen,
111
-			},
112
-			LogDriver:     logDriver,
113
-			Resources:     resources,
114
-			RestartPolicy: restartPolicy,
115
-			Placement: &swarm.Placement{
116
-				Constraints: service.Deploy.Placement.Constraints,
117
-			},
118
-		},
119
-		EndpointSpec: endpoint,
120
-		Mode:         mode,
121
-		Networks:     networks,
122
-		UpdateConfig: convertUpdateConfig(service.Deploy.UpdateConfig),
123
-	}
124
-
125
-	return serviceSpec, nil
126
-}
127
-
128
-func convertServiceNetworks(
129
-	networks map[string]*composetypes.ServiceNetworkConfig,
130
-	networkConfigs networkMap,
131
-	namespace Namespace,
132
-	name string,
133
-) ([]swarm.NetworkAttachmentConfig, error) {
134
-	if len(networks) == 0 {
135
-		return []swarm.NetworkAttachmentConfig{
136
-			{
137
-				Target:  namespace.Scope("default"),
138
-				Aliases: []string{name},
139
-			},
140
-		}, nil
141
-	}
142
-
143
-	nets := []swarm.NetworkAttachmentConfig{}
144
-	for networkName, network := range networks {
145
-		networkConfig, ok := networkConfigs[networkName]
146
-		if !ok {
147
-			return []swarm.NetworkAttachmentConfig{}, fmt.Errorf(
148
-				"service %q references network %q, which is not declared", name, networkName)
149
-		}
150
-		var aliases []string
151
-		if network != nil {
152
-			aliases = network.Aliases
153
-		}
154
-		target := namespace.Scope(networkName)
155
-		if networkConfig.External.External {
156
-			target = networkConfig.External.Name
157
-		}
158
-		nets = append(nets, swarm.NetworkAttachmentConfig{
159
-			Target:  target,
160
-			Aliases: append(aliases, name),
161
-		})
162
-	}
163
-	return nets, nil
164
-}
165
-
166
-func convertExtraHosts(extraHosts map[string]string) []string {
167
-	hosts := []string{}
168
-	for host, ip := range extraHosts {
169
-		hosts = append(hosts, fmt.Sprintf("%s %s", ip, host))
170
-	}
171
-	return hosts
172
-}
173
-
174
-func convertHealthcheck(healthcheck *composetypes.HealthCheckConfig) (*container.HealthConfig, error) {
175
-	if healthcheck == nil {
176
-		return nil, nil
177
-	}
178
-	var (
179
-		err               error
180
-		timeout, interval time.Duration
181
-		retries           int
182
-	)
183
-	if healthcheck.Disable {
184
-		if len(healthcheck.Test) != 0 {
185
-			return nil, fmt.Errorf("test and disable can't be set at the same time")
186
-		}
187
-		return &container.HealthConfig{
188
-			Test: []string{"NONE"},
189
-		}, nil
190
-
191
-	}
192
-	if healthcheck.Timeout != "" {
193
-		timeout, err = time.ParseDuration(healthcheck.Timeout)
194
-		if err != nil {
195
-			return nil, err
196
-		}
197
-	}
198
-	if healthcheck.Interval != "" {
199
-		interval, err = time.ParseDuration(healthcheck.Interval)
200
-		if err != nil {
201
-			return nil, err
202
-		}
203
-	}
204
-	if healthcheck.Retries != nil {
205
-		retries = int(*healthcheck.Retries)
206
-	}
207
-	return &container.HealthConfig{
208
-		Test:     healthcheck.Test,
209
-		Timeout:  timeout,
210
-		Interval: interval,
211
-		Retries:  retries,
212
-	}, nil
213
-}
214
-
215
-func convertRestartPolicy(restart string, source *composetypes.RestartPolicy) (*swarm.RestartPolicy, error) {
216
-	// TODO: log if restart is being ignored
217
-	if source == nil {
218
-		policy, err := runconfigopts.ParseRestartPolicy(restart)
219
-		if err != nil {
220
-			return nil, err
221
-		}
222
-		switch {
223
-		case policy.IsNone():
224
-			return nil, nil
225
-		case policy.IsAlways(), policy.IsUnlessStopped():
226
-			return &swarm.RestartPolicy{
227
-				Condition: swarm.RestartPolicyConditionAny,
228
-			}, nil
229
-		case policy.IsOnFailure():
230
-			attempts := uint64(policy.MaximumRetryCount)
231
-			return &swarm.RestartPolicy{
232
-				Condition:   swarm.RestartPolicyConditionOnFailure,
233
-				MaxAttempts: &attempts,
234
-			}, nil
235
-		default:
236
-			return nil, fmt.Errorf("unknown restart policy: %s", restart)
237
-		}
238
-	}
239
-	return &swarm.RestartPolicy{
240
-		Condition:   swarm.RestartPolicyCondition(source.Condition),
241
-		Delay:       source.Delay,
242
-		MaxAttempts: source.MaxAttempts,
243
-		Window:      source.Window,
244
-	}, nil
245
-}
246
-
247
-func convertUpdateConfig(source *composetypes.UpdateConfig) *swarm.UpdateConfig {
248
-	if source == nil {
249
-		return nil
250
-	}
251
-	parallel := uint64(1)
252
-	if source.Parallelism != nil {
253
-		parallel = *source.Parallelism
254
-	}
255
-	return &swarm.UpdateConfig{
256
-		Parallelism:     parallel,
257
-		Delay:           source.Delay,
258
-		FailureAction:   source.FailureAction,
259
-		Monitor:         source.Monitor,
260
-		MaxFailureRatio: source.MaxFailureRatio,
261
-	}
262
-}
263
-
264
-func convertResources(source composetypes.Resources) (*swarm.ResourceRequirements, error) {
265
-	resources := &swarm.ResourceRequirements{}
266
-	var err error
267
-	if source.Limits != nil {
268
-		var cpus int64
269
-		if source.Limits.NanoCPUs != "" {
270
-			cpus, err = opts.ParseCPUs(source.Limits.NanoCPUs)
271
-			if err != nil {
272
-				return nil, err
273
-			}
274
-		}
275
-		resources.Limits = &swarm.Resources{
276
-			NanoCPUs:    cpus,
277
-			MemoryBytes: int64(source.Limits.MemoryBytes),
278
-		}
279
-	}
280
-	if source.Reservations != nil {
281
-		var cpus int64
282
-		if source.Reservations.NanoCPUs != "" {
283
-			cpus, err = opts.ParseCPUs(source.Reservations.NanoCPUs)
284
-			if err != nil {
285
-				return nil, err
286
-			}
287
-		}
288
-		resources.Reservations = &swarm.Resources{
289
-			NanoCPUs:    cpus,
290
-			MemoryBytes: int64(source.Reservations.MemoryBytes),
291
-		}
292
-	}
293
-	return resources, nil
294
-
295
-}
296
-
297
-func convertEndpointSpec(source []string) (*swarm.EndpointSpec, error) {
298
-	portConfigs := []swarm.PortConfig{}
299
-	ports, portBindings, err := nat.ParsePortSpecs(source)
300
-	if err != nil {
301
-		return nil, err
302
-	}
303
-
304
-	for port := range ports {
305
-		portConfigs = append(
306
-			portConfigs,
307
-			opts.ConvertPortToPortConfig(port, portBindings)...)
308
-	}
309
-
310
-	return &swarm.EndpointSpec{Ports: portConfigs}, nil
311
-}
312
-
313
-func convertEnvironment(source map[string]string) []string {
314
-	var output []string
315
-
316
-	for name, value := range source {
317
-		output = append(output, fmt.Sprintf("%s=%s", name, value))
318
-	}
319
-
320
-	return output
321
-}
322
-
323
-func convertDeployMode(mode string, replicas *uint64) (swarm.ServiceMode, error) {
324
-	serviceMode := swarm.ServiceMode{}
325
-
326
-	switch mode {
327
-	case "global":
328
-		if replicas != nil {
329
-			return serviceMode, fmt.Errorf("replicas can only be used with replicated mode")
330
-		}
331
-		serviceMode.Global = &swarm.GlobalService{}
332
-	case "replicated", "":
333
-		serviceMode.Replicated = &swarm.ReplicatedService{Replicas: replicas}
334
-	default:
335
-		return serviceMode, fmt.Errorf("Unknown mode: %s", mode)
336
-	}
337
-	return serviceMode, nil
338
-}
339 1
deleted file mode 100644
... ...
@@ -1,193 +0,0 @@
1
-package composetransform
2
-
3
-import (
4
-	"sort"
5
-	"strings"
6
-	"testing"
7
-	"time"
8
-
9
-	composetypes "github.com/aanand/compose-file/types"
10
-	"github.com/docker/docker/api/types/container"
11
-	"github.com/docker/docker/api/types/swarm"
12
-	"github.com/docker/docker/pkg/testutil/assert"
13
-)
14
-
15
-func TestConvertRestartPolicyFromNone(t *testing.T) {
16
-	policy, err := convertRestartPolicy("no", nil)
17
-	assert.NilError(t, err)
18
-	assert.Equal(t, policy, (*swarm.RestartPolicy)(nil))
19
-}
20
-
21
-func TestConvertRestartPolicyFromUnknown(t *testing.T) {
22
-	_, err := convertRestartPolicy("unknown", nil)
23
-	assert.Error(t, err, "unknown restart policy: unknown")
24
-}
25
-
26
-func TestConvertRestartPolicyFromAlways(t *testing.T) {
27
-	policy, err := convertRestartPolicy("always", nil)
28
-	expected := &swarm.RestartPolicy{
29
-		Condition: swarm.RestartPolicyConditionAny,
30
-	}
31
-	assert.NilError(t, err)
32
-	assert.DeepEqual(t, policy, expected)
33
-}
34
-
35
-func TestConvertRestartPolicyFromFailure(t *testing.T) {
36
-	policy, err := convertRestartPolicy("on-failure:4", nil)
37
-	attempts := uint64(4)
38
-	expected := &swarm.RestartPolicy{
39
-		Condition:   swarm.RestartPolicyConditionOnFailure,
40
-		MaxAttempts: &attempts,
41
-	}
42
-	assert.NilError(t, err)
43
-	assert.DeepEqual(t, policy, expected)
44
-}
45
-
46
-func TestConvertEnvironment(t *testing.T) {
47
-	source := map[string]string{
48
-		"foo": "bar",
49
-		"key": "value",
50
-	}
51
-	env := convertEnvironment(source)
52
-	sort.Strings(env)
53
-	assert.DeepEqual(t, env, []string{"foo=bar", "key=value"})
54
-}
55
-
56
-func TestConvertResourcesFull(t *testing.T) {
57
-	source := composetypes.Resources{
58
-		Limits: &composetypes.Resource{
59
-			NanoCPUs:    "0.003",
60
-			MemoryBytes: composetypes.UnitBytes(300000000),
61
-		},
62
-		Reservations: &composetypes.Resource{
63
-			NanoCPUs:    "0.002",
64
-			MemoryBytes: composetypes.UnitBytes(200000000),
65
-		},
66
-	}
67
-	resources, err := convertResources(source)
68
-	assert.NilError(t, err)
69
-
70
-	expected := &swarm.ResourceRequirements{
71
-		Limits: &swarm.Resources{
72
-			NanoCPUs:    3000000,
73
-			MemoryBytes: 300000000,
74
-		},
75
-		Reservations: &swarm.Resources{
76
-			NanoCPUs:    2000000,
77
-			MemoryBytes: 200000000,
78
-		},
79
-	}
80
-	assert.DeepEqual(t, resources, expected)
81
-}
82
-
83
-func TestConvertHealthcheck(t *testing.T) {
84
-	retries := uint64(10)
85
-	source := &composetypes.HealthCheckConfig{
86
-		Test:     []string{"EXEC", "touch", "/foo"},
87
-		Timeout:  "30s",
88
-		Interval: "2ms",
89
-		Retries:  &retries,
90
-	}
91
-	expected := &container.HealthConfig{
92
-		Test:     source.Test,
93
-		Timeout:  30 * time.Second,
94
-		Interval: 2 * time.Millisecond,
95
-		Retries:  10,
96
-	}
97
-
98
-	healthcheck, err := convertHealthcheck(source)
99
-	assert.NilError(t, err)
100
-	assert.DeepEqual(t, healthcheck, expected)
101
-}
102
-
103
-func TestConvertHealthcheckDisable(t *testing.T) {
104
-	source := &composetypes.HealthCheckConfig{Disable: true}
105
-	expected := &container.HealthConfig{
106
-		Test: []string{"NONE"},
107
-	}
108
-
109
-	healthcheck, err := convertHealthcheck(source)
110
-	assert.NilError(t, err)
111
-	assert.DeepEqual(t, healthcheck, expected)
112
-}
113
-
114
-func TestConvertHealthcheckDisableWithTest(t *testing.T) {
115
-	source := &composetypes.HealthCheckConfig{
116
-		Disable: true,
117
-		Test:    []string{"EXEC", "touch"},
118
-	}
119
-	_, err := convertHealthcheck(source)
120
-	assert.Error(t, err, "test and disable can't be set")
121
-}
122
-
123
-func TestConvertServiceNetworksOnlyDefault(t *testing.T) {
124
-	networkConfigs := networkMap{}
125
-	networks := map[string]*composetypes.ServiceNetworkConfig{}
126
-
127
-	configs, err := convertServiceNetworks(
128
-		networks, networkConfigs, NewNamespace("foo"), "service")
129
-
130
-	expected := []swarm.NetworkAttachmentConfig{
131
-		{
132
-			Target:  "foo_default",
133
-			Aliases: []string{"service"},
134
-		},
135
-	}
136
-
137
-	assert.NilError(t, err)
138
-	assert.DeepEqual(t, configs, expected)
139
-}
140
-
141
-func TestConvertServiceNetworks(t *testing.T) {
142
-	networkConfigs := networkMap{
143
-		"front": composetypes.NetworkConfig{
144
-			External: composetypes.External{
145
-				External: true,
146
-				Name:     "fronttier",
147
-			},
148
-		},
149
-		"back": composetypes.NetworkConfig{},
150
-	}
151
-	networks := map[string]*composetypes.ServiceNetworkConfig{
152
-		"front": {
153
-			Aliases: []string{"something"},
154
-		},
155
-		"back": {
156
-			Aliases: []string{"other"},
157
-		},
158
-	}
159
-
160
-	configs, err := convertServiceNetworks(
161
-		networks, networkConfigs, NewNamespace("foo"), "service")
162
-
163
-	expected := []swarm.NetworkAttachmentConfig{
164
-		{
165
-			Target:  "foo_back",
166
-			Aliases: []string{"other", "service"},
167
-		},
168
-		{
169
-			Target:  "fronttier",
170
-			Aliases: []string{"something", "service"},
171
-		},
172
-	}
173
-
174
-	sortedConfigs := byTargetSort(configs)
175
-	sort.Sort(&sortedConfigs)
176
-
177
-	assert.NilError(t, err)
178
-	assert.DeepEqual(t, []swarm.NetworkAttachmentConfig(sortedConfigs), expected)
179
-}
180
-
181
-type byTargetSort []swarm.NetworkAttachmentConfig
182
-
183
-func (s byTargetSort) Len() int {
184
-	return len(s)
185
-}
186
-
187
-func (s byTargetSort) Less(i, j int) bool {
188
-	return strings.Compare(s[i].Target, s[j].Target) < 0
189
-}
190
-
191
-func (s byTargetSort) Swap(i, j int) {
192
-	s[i], s[j] = s[j], s[i]
193
-}
194 1
deleted file mode 100644
... ...
@@ -1,120 +0,0 @@
1
-package composetransform
2
-
3
-import (
4
-	"fmt"
5
-	"strings"
6
-
7
-	composetypes "github.com/aanand/compose-file/types"
8
-	"github.com/docker/docker/api/types/mount"
9
-)
10
-
11
-type volumes map[string]composetypes.VolumeConfig
12
-
13
-// ConvertVolumes from compose-file types to engine api types
14
-func ConvertVolumes(serviceVolumes []string, stackVolumes volumes, namespace Namespace) ([]mount.Mount, error) {
15
-	var mounts []mount.Mount
16
-
17
-	for _, volumeSpec := range serviceVolumes {
18
-		mount, err := convertVolumeToMount(volumeSpec, stackVolumes, namespace)
19
-		if err != nil {
20
-			return nil, err
21
-		}
22
-		mounts = append(mounts, mount)
23
-	}
24
-	return mounts, nil
25
-}
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
-		}
38
-	}
39
-
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]
50
-	}
51
-
52
-	// TODO: catch Windows paths here
53
-	if strings.HasPrefix(source, "/") {
54
-		return mount.Mount{
55
-			Type:        mount.TypeBind,
56
-			Source:      source,
57
-			Target:      target,
58
-			ReadOnly:    isReadOnly(mode),
59
-			BindOptions: getBindOptions(mode),
60
-		}, nil
61
-	}
62
-
63
-	stackVolume, exists := stackVolumes[source]
64
-	if !exists {
65
-		return mount.Mount{}, fmt.Errorf("undefined volume: %s", source)
66
-	}
67
-
68
-	var volumeOptions *mount.VolumeOptions
69
-	if stackVolume.External.Name != "" {
70
-		source = stackVolume.External.Name
71
-	} else {
72
-		volumeOptions = &mount.VolumeOptions{
73
-			Labels: AddStackLabel(namespace, stackVolume.Labels),
74
-			NoCopy: isNoCopy(mode),
75
-		}
76
-
77
-		if stackVolume.Driver != "" {
78
-			volumeOptions.DriverConfig = &mount.Driver{
79
-				Name:    stackVolume.Driver,
80
-				Options: stackVolume.DriverOpts,
81
-			}
82
-		}
83
-		source = namespace.Scope(source)
84
-	}
85
-	return mount.Mount{
86
-		Type:          mount.TypeVolume,
87
-		Source:        source,
88
-		Target:        target,
89
-		ReadOnly:      isReadOnly(mode),
90
-		VolumeOptions: volumeOptions,
91
-	}, nil
92
-}
93
-
94
-func modeHas(mode []string, field string) bool {
95
-	for _, item := range mode {
96
-		if item == field {
97
-			return true
98
-		}
99
-	}
100
-	return false
101
-}
102
-
103
-func isReadOnly(mode []string) bool {
104
-	return modeHas(mode, "ro")
105
-}
106
-
107
-func isNoCopy(mode []string) bool {
108
-	return modeHas(mode, "nocopy")
109
-}
110
-
111
-func getBindOptions(mode []string) *mount.BindOptions {
112
-	for _, item := range mode {
113
-		for _, propagation := range mount.Propagations {
114
-			if mount.Propagation(item) == propagation {
115
-				return &mount.BindOptions{Propagation: mount.Propagation(item)}
116
-			}
117
-		}
118
-	}
119
-	return nil
120
-}
121 1
deleted file mode 100644
... ...
@@ -1,112 +0,0 @@
1
-package composetransform
2
-
3
-import (
4
-	"testing"
5
-
6
-	composetypes "github.com/aanand/compose-file/types"
7
-	"github.com/docker/docker/api/types/mount"
8
-	"github.com/docker/docker/pkg/testutil/assert"
9
-)
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
-func TestConvertVolumeToMountNamedVolume(t *testing.T) {
38
-	stackVolumes := volumes{
39
-		"normal": composetypes.VolumeConfig{
40
-			Driver: "glusterfs",
41
-			DriverOpts: map[string]string{
42
-				"opt": "value",
43
-			},
44
-			Labels: map[string]string{
45
-				"something": "labeled",
46
-			},
47
-		},
48
-	}
49
-	namespace := NewNamespace("foo")
50
-	expected := mount.Mount{
51
-		Type:     mount.TypeVolume,
52
-		Source:   "foo_normal",
53
-		Target:   "/foo",
54
-		ReadOnly: true,
55
-		VolumeOptions: &mount.VolumeOptions{
56
-			Labels: map[string]string{
57
-				LabelNamespace: "foo",
58
-				"something":    "labeled",
59
-			},
60
-			DriverConfig: &mount.Driver{
61
-				Name: "glusterfs",
62
-				Options: map[string]string{
63
-					"opt": "value",
64
-				},
65
-			},
66
-		},
67
-	}
68
-	mount, err := convertVolumeToMount("normal:/foo:ro", stackVolumes, namespace)
69
-	assert.NilError(t, err)
70
-	assert.DeepEqual(t, mount, expected)
71
-}
72
-
73
-func TestConvertVolumeToMountNamedVolumeExternal(t *testing.T) {
74
-	stackVolumes := volumes{
75
-		"outside": composetypes.VolumeConfig{
76
-			External: composetypes.External{
77
-				External: true,
78
-				Name:     "special",
79
-			},
80
-		},
81
-	}
82
-	namespace := NewNamespace("foo")
83
-	expected := mount.Mount{
84
-		Type:   mount.TypeVolume,
85
-		Source: "special",
86
-		Target: "/foo",
87
-	}
88
-	mount, err := convertVolumeToMount("outside:/foo", stackVolumes, namespace)
89
-	assert.NilError(t, err)
90
-	assert.DeepEqual(t, mount, expected)
91
-}
92
-
93
-func TestConvertVolumeToMountBind(t *testing.T) {
94
-	stackVolumes := volumes{}
95
-	namespace := NewNamespace("foo")
96
-	expected := mount.Mount{
97
-		Type:        mount.TypeBind,
98
-		Source:      "/bar",
99
-		Target:      "/foo",
100
-		ReadOnly:    true,
101
-		BindOptions: &mount.BindOptions{Propagation: mount.PropagationShared},
102
-	}
103
-	mount, err := convertVolumeToMount("/bar:/foo:ro,shared", stackVolumes, namespace)
104
-	assert.NilError(t, err)
105
-	assert.DeepEqual(t, mount, expected)
106
-}
107
-
108
-func TestConvertVolumeToMountVolumeDoesNotExist(t *testing.T) {
109
-	namespace := NewNamespace("foo")
110
-	_, err := convertVolumeToMount("unknown:/foo:ro", volumes{}, namespace)
111
-	assert.Error(t, err, "undefined volume: unknown")
112
-}