Browse code

Convert deploy to use a compose-file.

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

Daniel Nephin authored on 2016/11/03 03:57:40
Showing 6 changed files
... ...
@@ -297,7 +297,7 @@ func (e *endpointOptions) ToEndpointSpec() *swarm.EndpointSpec {
297 297
 	ports, portBindings, _ := nat.ParsePortSpecs(e.ports.GetAll())
298 298
 
299 299
 	for port := range ports {
300
-		portConfigs = append(portConfigs, convertPortToPortConfig(port, portBindings)...)
300
+		portConfigs = append(portConfigs, ConvertPortToPortConfig(port, portBindings)...)
301 301
 	}
302 302
 
303 303
 	return &swarm.EndpointSpec{
... ...
@@ -306,7 +306,8 @@ func (e *endpointOptions) ToEndpointSpec() *swarm.EndpointSpec {
306 306
 	}
307 307
 }
308 308
 
309
-func convertPortToPortConfig(
309
+// ConvertPortToPortConfig converts ports to the swarm type
310
+func ConvertPortToPortConfig(
310 311
 	port nat.Port,
311 312
 	portBindings map[nat.Port][]nat.PortBinding,
312 313
 ) []swarm.PortConfig {
... ...
@@ -631,7 +631,7 @@ func updatePorts(flags *pflag.FlagSet, portConfig *[]swarm.PortConfig) error {
631 631
 		ports, portBindings, _ := nat.ParsePortSpecs(values)
632 632
 
633 633
 		for port := range ports {
634
-			newConfigs := convertPortToPortConfig(port, portBindings)
634
+			newConfigs := ConvertPortToPortConfig(port, portBindings)
635 635
 			for _, entry := range newConfigs {
636 636
 				if v, ok := portSet[portConfigToString(&entry)]; ok && v != entry {
637 637
 					return fmt.Errorf("conflicting port mapping between %v:%v/%s and %v:%v/%s", entry.PublishedPort, entry.TargetPort, entry.Protocol, v.PublishedPort, v.TargetPort, v.Protocol)
... ...
@@ -19,7 +19,6 @@ func NewStackCommand(dockerCli *command.DockerCli) *cobra.Command {
19 19
 		Tags: map[string]string{"experimental": "", "version": "1.25"},
20 20
 	}
21 21
 	cmd.AddCommand(
22
-		newConfigCommand(dockerCli),
23 22
 		newDeployCommand(dockerCli),
24 23
 		newListCommand(dockerCli),
25 24
 		newRemoveCommand(dockerCli),
26 25
deleted file mode 100644
... ...
@@ -1,39 +0,0 @@
1
-package stack
2
-
3
-import (
4
-	"github.com/docker/docker/cli"
5
-	"github.com/docker/docker/cli/command"
6
-	"github.com/docker/docker/cli/command/bundlefile"
7
-	"github.com/spf13/cobra"
8
-)
9
-
10
-type configOptions struct {
11
-	bundlefile string
12
-	namespace  string
13
-}
14
-
15
-func newConfigCommand(dockerCli *command.DockerCli) *cobra.Command {
16
-	var opts configOptions
17
-
18
-	cmd := &cobra.Command{
19
-		Use:   "config [OPTIONS] STACK",
20
-		Short: "Print the stack configuration",
21
-		Args:  cli.ExactArgs(1),
22
-		RunE: func(cmd *cobra.Command, args []string) error {
23
-			opts.namespace = args[0]
24
-			return runConfig(dockerCli, opts)
25
-		},
26
-	}
27
-
28
-	flags := cmd.Flags()
29
-	addBundlefileFlag(&opts.bundlefile, flags)
30
-	return cmd
31
-}
32
-
33
-func runConfig(dockerCli *command.DockerCli, opts configOptions) error {
34
-	bundle, err := loadBundlefile(dockerCli.Err(), opts.namespace, opts.bundlefile)
35
-	if err != nil {
36
-		return err
37
-	}
38
-	return bundlefile.Print(dockerCli.Out(), bundle)
39
-}
... ...
@@ -2,16 +2,22 @@ package stack
2 2
 
3 3
 import (
4 4
 	"fmt"
5
-	"strings"
5
+	"io/ioutil"
6
+	"os"
7
+	"time"
6 8
 
7 9
 	"github.com/spf13/cobra"
8 10
 	"golang.org/x/net/context"
9 11
 
12
+	"github.com/aanand/compose-file/loader"
13
+	composetypes "github.com/aanand/compose-file/types"
10 14
 	"github.com/docker/docker/api/types"
15
+	networktypes "github.com/docker/docker/api/types/network"
11 16
 	"github.com/docker/docker/api/types/swarm"
12 17
 	"github.com/docker/docker/cli"
13 18
 	"github.com/docker/docker/cli/command"
14
-	"github.com/docker/docker/cli/command/bundlefile"
19
+	servicecmd "github.com/docker/docker/cli/command/service"
20
+	"github.com/docker/go-connections/nat"
15 21
 )
16 22
 
17 23
 const (
... ...
@@ -19,7 +25,7 @@ const (
19 19
 )
20 20
 
21 21
 type deployOptions struct {
22
-	bundlefile       string
22
+	composefile      string
23 23
 	namespace        string
24 24
 	sendRegistryAuth bool
25 25
 }
... ...
@@ -30,63 +36,69 @@ func newDeployCommand(dockerCli *command.DockerCli) *cobra.Command {
30 30
 	cmd := &cobra.Command{
31 31
 		Use:     "deploy [OPTIONS] STACK",
32 32
 		Aliases: []string{"up"},
33
-		Short:   "Create and update a stack from a Distributed Application Bundle (DAB)",
33
+		Short:   "Deploy a new stack or update an existing stack",
34 34
 		Args:    cli.ExactArgs(1),
35 35
 		RunE: func(cmd *cobra.Command, args []string) error {
36
-			opts.namespace = strings.TrimSuffix(args[0], ".dab")
36
+			opts.namespace = args[0]
37 37
 			return runDeploy(dockerCli, opts)
38 38
 		},
39 39
 		Tags: map[string]string{"experimental": "", "version": "1.25"},
40 40
 	}
41 41
 
42 42
 	flags := cmd.Flags()
43
-	addBundlefileFlag(&opts.bundlefile, flags)
43
+	addComposefileFlag(&opts.composefile, flags)
44 44
 	addRegistryAuthFlag(&opts.sendRegistryAuth, flags)
45 45
 	return cmd
46 46
 }
47 47
 
48 48
 func runDeploy(dockerCli *command.DockerCli, opts deployOptions) error {
49
-	bundle, err := loadBundlefile(dockerCli.Err(), opts.namespace, opts.bundlefile)
49
+	configDetails, err := getConfigDetails(opts)
50 50
 	if err != nil {
51 51
 		return err
52 52
 	}
53 53
 
54
-	info, err := dockerCli.Client().Info(context.Background())
54
+	config, err := loader.Load(configDetails)
55 55
 	if err != nil {
56 56
 		return err
57 57
 	}
58
-	if !info.Swarm.ControlAvailable {
59
-		return fmt.Errorf("This node is not a swarm manager. Use \"docker swarm init\" or \"docker swarm join\" to connect this node to swarm and try again.")
60
-	}
61 58
 
62
-	networks := getUniqueNetworkNames(bundle.Services)
63 59
 	ctx := context.Background()
64
-
65
-	if err := updateNetworks(ctx, dockerCli, networks, opts.namespace); err != nil {
60
+	if err := createNetworks(ctx, dockerCli, config.Networks, opts.namespace); err != nil {
66 61
 		return err
67 62
 	}
68
-	return deployServices(ctx, dockerCli, bundle.Services, opts.namespace, opts.sendRegistryAuth)
63
+	return deployServices(ctx, dockerCli, config, opts.namespace, opts.sendRegistryAuth)
69 64
 }
70 65
 
71
-func getUniqueNetworkNames(services map[string]bundlefile.Service) []string {
72
-	networkSet := make(map[string]bool)
73
-	for _, service := range services {
74
-		for _, network := range service.Networks {
75
-			networkSet[network] = true
76
-		}
66
+func getConfigDetails(opts deployOptions) (composetypes.ConfigDetails, error) {
67
+	var details composetypes.ConfigDetails
68
+	var err error
69
+
70
+	details.WorkingDir, err = os.Getwd()
71
+	if err != nil {
72
+		return details, err
73
+	}
74
+
75
+	configFile, err := getConfigFile(opts.composefile)
76
+	if err != nil {
77
+		return details, err
77 78
 	}
79
+	// TODO: support multiple files
80
+	details.ConfigFiles = []composetypes.ConfigFile{*configFile}
81
+	return details, nil
82
+}
78 83
 
79
-	networks := []string{}
80
-	for network := range networkSet {
81
-		networks = append(networks, network)
84
+func getConfigFile(filename string) (*composetypes.ConfigFile, error) {
85
+	bytes, err := ioutil.ReadFile(filename)
86
+	if err != nil {
87
+		return nil, err
82 88
 	}
83
-	return networks
89
+	return loader.ParseYAML(bytes, filename)
84 90
 }
85 91
 
86
-func updateNetworks(
92
+func createNetworks(
87 93
 	ctx context.Context,
88 94
 	dockerCli *command.DockerCli,
89
-	networks []string,
95
+	networks map[string]composetypes.NetworkConfig,
90 96
 	namespace string,
91 97
 ) error {
92 98
 	client := dockerCli.Client()
... ...
@@ -101,17 +113,34 @@ func updateNetworks(
101 101
 		existingNetworkMap[network.Name] = network
102 102
 	}
103 103
 
104
-	createOpts := types.NetworkCreate{
105
-		Labels: getStackLabels(namespace, nil),
106
-		Driver: defaultNetworkDriver,
107
-	}
104
+	for internalName, network := range networks {
105
+		if network.ExternalName != "" {
106
+			continue
107
+		}
108 108
 
109
-	for _, internalName := range networks {
110 109
 		name := fmt.Sprintf("%s_%s", namespace, internalName)
111
-
112 110
 		if _, exists := existingNetworkMap[name]; exists {
113 111
 			continue
114 112
 		}
113
+
114
+		createOpts := types.NetworkCreate{
115
+			// TODO: support network labels from compose file
116
+			Labels:  getStackLabels(namespace, nil),
117
+			Driver:  network.Driver,
118
+			Options: network.DriverOpts,
119
+		}
120
+
121
+		if network.Ipam.Driver != "" {
122
+			createOpts.IPAM = &networktypes.IPAM{
123
+				Driver: network.Ipam.Driver,
124
+			}
125
+		}
126
+		// TODO: IPAMConfig.Config
127
+
128
+		if createOpts.Driver == "" {
129
+			createOpts.Driver = defaultNetworkDriver
130
+		}
131
+
115 132
 		fmt.Fprintf(dockerCli.Out(), "Creating network %s\n", name)
116 133
 		if _, err := client.NetworkCreate(ctx, name, createOpts); err != nil {
117 134
 			return err
... ...
@@ -120,12 +149,17 @@ func updateNetworks(
120 120
 	return nil
121 121
 }
122 122
 
123
-func convertNetworks(networks []string, namespace string, name string) []swarm.NetworkAttachmentConfig {
123
+func convertNetworks(
124
+	networks map[string]*composetypes.ServiceNetworkConfig,
125
+	namespace string,
126
+	name string,
127
+) []swarm.NetworkAttachmentConfig {
124 128
 	nets := []swarm.NetworkAttachmentConfig{}
125
-	for _, network := range networks {
129
+	for networkName, network := range networks {
126 130
 		nets = append(nets, swarm.NetworkAttachmentConfig{
127
-			Target:  namespace + "_" + network,
128
-			Aliases: []string{name},
131
+			// TODO: only do this name mangling in one function
132
+			Target:  namespace + "_" + networkName,
133
+			Aliases: append(network.Aliases, name),
129 134
 		})
130 135
 	}
131 136
 	return nets
... ...
@@ -134,12 +168,14 @@ func convertNetworks(networks []string, namespace string, name string) []swarm.N
134 134
 func deployServices(
135 135
 	ctx context.Context,
136 136
 	dockerCli *command.DockerCli,
137
-	services map[string]bundlefile.Service,
137
+	config *composetypes.Config,
138 138
 	namespace string,
139 139
 	sendAuth bool,
140 140
 ) error {
141 141
 	apiClient := dockerCli.Client()
142 142
 	out := dockerCli.Out()
143
+	services := config.Services
144
+	volumes := config.Volumes
143 145
 
144 146
 	existingServices, err := getServices(ctx, apiClient, namespace)
145 147
 	if err != nil {
... ...
@@ -151,46 +187,12 @@ func deployServices(
151 151
 		existingServiceMap[service.Spec.Name] = service
152 152
 	}
153 153
 
154
-	for internalName, service := range services {
155
-		name := fmt.Sprintf("%s_%s", namespace, internalName)
156
-
157
-		var ports []swarm.PortConfig
158
-		for _, portSpec := range service.Ports {
159
-			ports = append(ports, swarm.PortConfig{
160
-				Protocol:   swarm.PortConfigProtocol(portSpec.Protocol),
161
-				TargetPort: portSpec.Port,
162
-			})
163
-		}
164
-
165
-		serviceSpec := swarm.ServiceSpec{
166
-			Annotations: swarm.Annotations{
167
-				Name:   name,
168
-				Labels: getStackLabels(namespace, service.Labels),
169
-			},
170
-			TaskTemplate: swarm.TaskSpec{
171
-				ContainerSpec: swarm.ContainerSpec{
172
-					Image:   service.Image,
173
-					Command: service.Command,
174
-					Args:    service.Args,
175
-					Env:     service.Env,
176
-					// Service Labels will not be copied to Containers
177
-					// automatically during the deployment so we apply
178
-					// it here.
179
-					Labels: getStackLabels(namespace, nil),
180
-				},
181
-			},
182
-			EndpointSpec: &swarm.EndpointSpec{
183
-				Ports: ports,
184
-			},
185
-			Networks: convertNetworks(service.Networks, namespace, internalName),
186
-		}
154
+	for _, service := range services {
155
+		name := fmt.Sprintf("%s_%s", namespace, service.Name)
187 156
 
188
-		cspec := &serviceSpec.TaskTemplate.ContainerSpec
189
-		if service.WorkingDir != nil {
190
-			cspec.Dir = *service.WorkingDir
191
-		}
192
-		if service.User != nil {
193
-			cspec.User = *service.User
157
+		serviceSpec, err := convertService(namespace, service, volumes)
158
+		if err != nil {
159
+			return err
194 160
 		}
195 161
 
196 162
 		encodedAuth := ""
... ...
@@ -234,3 +236,100 @@ func deployServices(
234 234
 
235 235
 	return nil
236 236
 }
237
+
238
+func convertService(
239
+	namespace string,
240
+	service composetypes.ServiceConfig,
241
+	volumes map[string]composetypes.VolumeConfig,
242
+) (swarm.ServiceSpec, error) {
243
+	// TODO: remove this duplication
244
+	name := fmt.Sprintf("%s_%s", namespace, service.Name)
245
+
246
+	endpoint, err := convertEndpointSpec(service.Ports)
247
+	if err != nil {
248
+		return swarm.ServiceSpec{}, err
249
+	}
250
+
251
+	mode, err := convertDeployMode(service.Deploy.Mode, service.Deploy.Replicas)
252
+	if err != nil {
253
+		return swarm.ServiceSpec{}, err
254
+	}
255
+
256
+	serviceSpec := swarm.ServiceSpec{
257
+		Annotations: swarm.Annotations{
258
+			Name:   name,
259
+			Labels: getStackLabels(namespace, service.Labels),
260
+		},
261
+		TaskTemplate: swarm.TaskSpec{
262
+			ContainerSpec: swarm.ContainerSpec{
263
+				Image:   service.Image,
264
+				Command: service.Entrypoint,
265
+				Args:    service.Command,
266
+				Env:     convertEnvironment(service.Environment),
267
+				Labels:  getStackLabels(namespace, service.Deploy.Labels),
268
+				Dir:     service.WorkingDir,
269
+				User:    service.User,
270
+			},
271
+			Placement: &swarm.Placement{
272
+				Constraints: service.Deploy.Placement.Constraints,
273
+			},
274
+		},
275
+		EndpointSpec: endpoint,
276
+		Mode:         mode,
277
+		Networks:     convertNetworks(service.Networks, namespace, service.Name),
278
+	}
279
+
280
+	if service.StopGracePeriod != nil {
281
+		stopGrace, err := time.ParseDuration(*service.StopGracePeriod)
282
+		if err != nil {
283
+			return swarm.ServiceSpec{}, err
284
+		}
285
+		serviceSpec.TaskTemplate.ContainerSpec.StopGracePeriod = &stopGrace
286
+	}
287
+
288
+	// TODO: convert mounts
289
+	return serviceSpec, nil
290
+}
291
+
292
+func convertEndpointSpec(source []string) (*swarm.EndpointSpec, error) {
293
+	portConfigs := []swarm.PortConfig{}
294
+	ports, portBindings, err := nat.ParsePortSpecs(source)
295
+	if err != nil {
296
+		return nil, err
297
+	}
298
+
299
+	for port := range ports {
300
+		portConfigs = append(
301
+			portConfigs,
302
+			servicecmd.ConvertPortToPortConfig(port, portBindings)...)
303
+	}
304
+
305
+	return &swarm.EndpointSpec{Ports: portConfigs}, nil
306
+}
307
+
308
+func convertEnvironment(source map[string]string) []string {
309
+	var output []string
310
+
311
+	for name, value := range source {
312
+		output = append(output, fmt.Sprintf("%s=%s", name, value))
313
+	}
314
+
315
+	return output
316
+}
317
+
318
+func convertDeployMode(mode string, replicas uint64) (swarm.ServiceMode, error) {
319
+	serviceMode := swarm.ServiceMode{}
320
+
321
+	switch mode {
322
+	case "global":
323
+		if replicas != 0 {
324
+			return serviceMode, fmt.Errorf("replicas can only be used with replicated mode")
325
+		}
326
+		serviceMode.Global = &swarm.GlobalService{}
327
+	case "replicated":
328
+		serviceMode.Replicated = &swarm.ReplicatedService{Replicas: &replicas}
329
+	default:
330
+		return serviceMode, fmt.Errorf("Unknown mode: %s", mode)
331
+	}
332
+	return serviceMode, nil
333
+}
... ...
@@ -9,11 +9,12 @@ import (
9 9
 	"github.com/spf13/pflag"
10 10
 )
11 11
 
12
+func addComposefileFlag(opt *string, flags *pflag.FlagSet) {
13
+	flags.StringVar(opt, "compose-file", "", "Path to a Compose file")
14
+}
15
+
12 16
 func addBundlefileFlag(opt *string, flags *pflag.FlagSet) {
13
-	flags.StringVar(
14
-		opt,
15
-		"file", "",
16
-		"Path to a Distributed Application Bundle file (Default: STACK.dab)")
17
+	flags.StringVar(opt, "bundle-file", "", "Path to a Distributed Application Bundle file")
17 18
 }
18 19
 
19 20
 func addRegistryAuthFlag(opt *bool, flags *pflag.FlagSet) {