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
 
... ...
@@ -17,8 +17,8 @@ import (
17 17
 	"github.com/docker/docker/api/types/swarm"
18 18
 	"github.com/docker/docker/cli"
19 19
 	"github.com/docker/docker/cli/command"
20
+	"github.com/docker/docker/cli/compose/convert"
20 21
 	dockerclient "github.com/docker/docker/client"
21
-	"github.com/docker/docker/pkg/composetransform"
22 22
 )
23 23
 
24 24
 const (
... ...
@@ -115,16 +115,16 @@ func deployCompose(ctx context.Context, dockerCli *command.DockerCli, opts deplo
115 115
 		return err
116 116
 	}
117 117
 
118
-	namespace := composetransform.NewNamespace(opts.namespace)
118
+	namespace := convert.NewNamespace(opts.namespace)
119 119
 
120
-	networks, externalNetworks := composetransform.ConvertNetworks(namespace, config.Networks)
120
+	networks, externalNetworks := convert.Networks(namespace, config.Networks)
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
 	}
... ...
@@ -198,7 +198,7 @@ func validateExternalNetworks(
198 198
 func createNetworks(
199 199
 	ctx context.Context,
200 200
 	dockerCli *command.DockerCli,
201
-	namespace composetransform.Namespace,
201
+	namespace convert.Namespace,
202 202
 	networks map[string]types.NetworkCreate,
203 203
 ) error {
204 204
 	client := dockerCli.Client()
... ...
@@ -236,7 +236,7 @@ func deployServices(
236 236
 	ctx context.Context,
237 237
 	dockerCli *command.DockerCli,
238 238
 	services map[string]swarm.ServiceSpec,
239
-	namespace composetransform.Namespace,
239
+	namespace convert.Namespace,
240 240
 	sendAuth bool,
241 241
 ) error {
242 242
 	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 from the compose-file type to the engine API type
45
+func Networks(namespace Namespace, networks networkMap) (map[string]types.NetworkCreate, []string) {
46
+	if networks == nil {
47
+		networks = make(map[string]composetypes.NetworkConfig)
48
+	}
49
+
50
+	// TODO: only add default network if it's used
51
+	if _, ok := networks["default"]; !ok {
52
+		networks["default"] = composetypes.NetworkConfig{}
53
+	}
54
+
55
+	externalNetworks := []string{}
56
+	result := make(map[string]types.NetworkCreate)
57
+
58
+	for internalName, network := range networks {
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,85 @@
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
+	networks, externals := Networks(namespace, source)
82
+	assert.DeepEqual(t, networks, expected)
83
+	assert.DeepEqual(t, externals, []string{"special"})
84
+}
0 85
new file mode 100644
... ...
@@ -0,0 +1,330 @@
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
+	if source.Limits != nil {
266
+		cpus, err := opts.ParseCPUs(source.Limits.NanoCPUs)
267
+		if err != nil {
268
+			return nil, err
269
+		}
270
+		resources.Limits = &swarm.Resources{
271
+			NanoCPUs:    cpus,
272
+			MemoryBytes: int64(source.Limits.MemoryBytes),
273
+		}
274
+	}
275
+	if source.Reservations != nil {
276
+		cpus, err := opts.ParseCPUs(source.Reservations.NanoCPUs)
277
+		if err != nil {
278
+			return nil, err
279
+		}
280
+		resources.Reservations = &swarm.Resources{
281
+			NanoCPUs:    cpus,
282
+			MemoryBytes: int64(source.Reservations.MemoryBytes),
283
+		}
284
+	}
285
+	return resources, nil
286
+}
287
+
288
+func convertEndpointSpec(source []string) (*swarm.EndpointSpec, error) {
289
+	portConfigs := []swarm.PortConfig{}
290
+	ports, portBindings, err := nat.ParsePortSpecs(source)
291
+	if err != nil {
292
+		return nil, err
293
+	}
294
+
295
+	for port := range ports {
296
+		portConfigs = append(
297
+			portConfigs,
298
+			opts.ConvertPortToPortConfig(port, portBindings)...)
299
+	}
300
+
301
+	return &swarm.EndpointSpec{Ports: portConfigs}, nil
302
+}
303
+
304
+func convertEnvironment(source map[string]string) []string {
305
+	var output []string
306
+
307
+	for name, value := range source {
308
+		output = append(output, fmt.Sprintf("%s=%s", name, value))
309
+	}
310
+
311
+	return output
312
+}
313
+
314
+func convertDeployMode(mode string, replicas *uint64) (swarm.ServiceMode, error) {
315
+	serviceMode := swarm.ServiceMode{}
316
+
317
+	switch mode {
318
+	case "global":
319
+		if replicas != nil {
320
+			return serviceMode, fmt.Errorf("replicas can only be used with replicated mode")
321
+		}
322
+		serviceMode.Global = &swarm.GlobalService{}
323
+	case "replicated", "":
324
+		serviceMode.Replicated = &swarm.ReplicatedService{Replicas: replicas}
325
+	default:
326
+		return serviceMode, fmt.Errorf("Unknown mode: %s", mode)
327
+	}
328
+	return serviceMode, nil
329
+}
0 330
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,116 @@
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
+	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 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(namespace Namespace, networks networkMap) (map[string]types.NetworkCreate, []string) {
47
-	if networks == nil {
48
-		networks = make(map[string]composetypes.NetworkConfig)
49
-	}
50
-
51
-	// TODO: only add default network if it's used
52
-	if _, ok := networks["default"]; !ok {
53
-		networks["default"] = composetypes.NetworkConfig{}
54
-	}
55
-
56
-	externalNetworks := []string{}
57
-	result := make(map[string]types.NetworkCreate)
58
-
59
-	for internalName, network := range networks {
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,85 +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
-	networks, externals := ConvertNetworks(namespace, source)
83
-	assert.DeepEqual(t, networks, expected)
84
-	assert.DeepEqual(t, externals, []string{"special"})
85
-}
86 1
deleted file mode 100644
... ...
@@ -1,330 +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
-	if source.Limits != nil {
267
-		cpus, err := opts.ParseCPUs(source.Limits.NanoCPUs)
268
-		if err != nil {
269
-			return nil, err
270
-		}
271
-		resources.Limits = &swarm.Resources{
272
-			NanoCPUs:    cpus,
273
-			MemoryBytes: int64(source.Limits.MemoryBytes),
274
-		}
275
-	}
276
-	if source.Reservations != nil {
277
-		cpus, err := opts.ParseCPUs(source.Reservations.NanoCPUs)
278
-		if err != nil {
279
-			return nil, err
280
-		}
281
-		resources.Reservations = &swarm.Resources{
282
-			NanoCPUs:    cpus,
283
-			MemoryBytes: int64(source.Reservations.MemoryBytes),
284
-		}
285
-	}
286
-	return resources, nil
287
-}
288
-
289
-func convertEndpointSpec(source []string) (*swarm.EndpointSpec, error) {
290
-	portConfigs := []swarm.PortConfig{}
291
-	ports, portBindings, err := nat.ParsePortSpecs(source)
292
-	if err != nil {
293
-		return nil, err
294
-	}
295
-
296
-	for port := range ports {
297
-		portConfigs = append(
298
-			portConfigs,
299
-			opts.ConvertPortToPortConfig(port, portBindings)...)
300
-	}
301
-
302
-	return &swarm.EndpointSpec{Ports: portConfigs}, nil
303
-}
304
-
305
-func convertEnvironment(source map[string]string) []string {
306
-	var output []string
307
-
308
-	for name, value := range source {
309
-		output = append(output, fmt.Sprintf("%s=%s", name, value))
310
-	}
311
-
312
-	return output
313
-}
314
-
315
-func convertDeployMode(mode string, replicas *uint64) (swarm.ServiceMode, error) {
316
-	serviceMode := swarm.ServiceMode{}
317
-
318
-	switch mode {
319
-	case "global":
320
-		if replicas != nil {
321
-			return serviceMode, fmt.Errorf("replicas can only be used with replicated mode")
322
-		}
323
-		serviceMode.Global = &swarm.GlobalService{}
324
-	case "replicated", "":
325
-		serviceMode.Replicated = &swarm.ReplicatedService{Replicas: replicas}
326
-	default:
327
-		return serviceMode, fmt.Errorf("Unknown mode: %s", mode)
328
-	}
329
-	return serviceMode, nil
330
-}
331 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,116 +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
-	switch len(parts) {
35
-	case 3:
36
-		source = parts[0]
37
-		target = parts[1]
38
-		mode = strings.Split(parts[2], ",")
39
-	case 2:
40
-		source = parts[0]
41
-		target = parts[1]
42
-	case 1:
43
-		target = parts[0]
44
-	default:
45
-		return mount.Mount{}, fmt.Errorf("invald volume: %s", volumeSpec)
46
-	}
47
-
48
-	// TODO: catch Windows paths here
49
-	if strings.HasPrefix(source, "/") {
50
-		return mount.Mount{
51
-			Type:        mount.TypeBind,
52
-			Source:      source,
53
-			Target:      target,
54
-			ReadOnly:    isReadOnly(mode),
55
-			BindOptions: getBindOptions(mode),
56
-		}, nil
57
-	}
58
-
59
-	stackVolume, exists := stackVolumes[source]
60
-	if !exists {
61
-		return mount.Mount{}, fmt.Errorf("undefined volume: %s", source)
62
-	}
63
-
64
-	var volumeOptions *mount.VolumeOptions
65
-	if stackVolume.External.Name != "" {
66
-		source = stackVolume.External.Name
67
-	} else {
68
-		volumeOptions = &mount.VolumeOptions{
69
-			Labels: AddStackLabel(namespace, stackVolume.Labels),
70
-			NoCopy: isNoCopy(mode),
71
-		}
72
-
73
-		if stackVolume.Driver != "" {
74
-			volumeOptions.DriverConfig = &mount.Driver{
75
-				Name:    stackVolume.Driver,
76
-				Options: stackVolume.DriverOpts,
77
-			}
78
-		}
79
-		source = namespace.Scope(source)
80
-	}
81
-	return mount.Mount{
82
-		Type:          mount.TypeVolume,
83
-		Source:        source,
84
-		Target:        target,
85
-		ReadOnly:      isReadOnly(mode),
86
-		VolumeOptions: volumeOptions,
87
-	}, nil
88
-}
89
-
90
-func modeHas(mode []string, field string) bool {
91
-	for _, item := range mode {
92
-		if item == field {
93
-			return true
94
-		}
95
-	}
96
-	return false
97
-}
98
-
99
-func isReadOnly(mode []string) bool {
100
-	return modeHas(mode, "ro")
101
-}
102
-
103
-func isNoCopy(mode []string) bool {
104
-	return modeHas(mode, "nocopy")
105
-}
106
-
107
-func getBindOptions(mode []string) *mount.BindOptions {
108
-	for _, item := range mode {
109
-		for _, propagation := range mount.Propagations {
110
-			if mount.Propagation(item) == propagation {
111
-				return &mount.BindOptions{Propagation: mount.Propagation(item)}
112
-			}
113
-		}
114
-	}
115
-	return nil
116
-}
117 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
-}