Browse code

Add experimental docker stack commands

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

Daniel Nephin authored on 2016/06/09 02:47:46
Showing 13 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,62 @@
0
+// +build experimental
1
+
2
+package bundlefile
3
+
4
+import (
5
+	"encoding/json"
6
+	"io"
7
+	"os"
8
+)
9
+
10
+// Bundlefile stores the contents of a bundlefile
11
+type Bundlefile struct {
12
+	Version  string
13
+	Services map[string]Service
14
+}
15
+
16
+// Service is a service from a bundlefile
17
+type Service struct {
18
+	Image      string
19
+	Command    []string          `json:",omitempty"`
20
+	Args       []string          `json:",omitempty"`
21
+	Env        []string          `json:",omitempty"`
22
+	Labels     map[string]string `json:",omitempty"`
23
+	Ports      []Port            `json:",omitempty"`
24
+	WorkingDir *string           `json:",omitempty"`
25
+	User       *string           `json:",omitempty"`
26
+	Networks   []string          `json:",omitempty"`
27
+}
28
+
29
+// Port is a port as defined in a bundlefile
30
+type Port struct {
31
+	Protocol string
32
+	Port     uint32
33
+}
34
+
35
+// LoadFile loads a bundlefile from a path to the file
36
+func LoadFile(path string) (*Bundlefile, error) {
37
+	reader, err := os.Open(path)
38
+	if err != nil {
39
+		return nil, err
40
+	}
41
+
42
+	bundlefile := &Bundlefile{}
43
+
44
+	if err := json.NewDecoder(reader).Decode(bundlefile); err != nil {
45
+		return nil, err
46
+	}
47
+
48
+	return bundlefile, err
49
+}
50
+
51
+// Print writes the contents of the bundlefile to the output writer
52
+// as human readable json
53
+func Print(out io.Writer, bundle *Bundlefile) error {
54
+	bytes, err := json.MarshalIndent(*bundle, "", "    ")
55
+	if err != nil {
56
+		return err
57
+	}
58
+
59
+	_, err = out.Write(bytes)
60
+	return err
61
+}
... ...
@@ -16,7 +16,7 @@ import (
16 16
 func NewNodeCommand(dockerCli *client.DockerCli) *cobra.Command {
17 17
 	cmd := &cobra.Command{
18 18
 		Use:   "node",
19
-		Short: "Manage docker swarm nodes",
19
+		Short: "Manage Docker Swarm nodes",
20 20
 		Args:  cli.NoArgs,
21 21
 		Run: func(cmd *cobra.Command, args []string) {
22 22
 			fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
... ...
@@ -13,7 +13,7 @@ import (
13 13
 func NewServiceCommand(dockerCli *client.DockerCli) *cobra.Command {
14 14
 	cmd := &cobra.Command{
15 15
 		Use:   "service",
16
-		Short: "Manage docker services",
16
+		Short: "Manage Docker services",
17 17
 		Args:  cli.NoArgs,
18 18
 		Run: func(cmd *cobra.Command, args []string) {
19 19
 			fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
20 20
new file mode 100644
... ...
@@ -0,0 +1,38 @@
0
+// +build experimental
1
+
2
+package stack
3
+
4
+import (
5
+	"fmt"
6
+
7
+	"github.com/docker/docker/api/client"
8
+	"github.com/docker/docker/cli"
9
+	"github.com/spf13/cobra"
10
+)
11
+
12
+// NewStackCommand returns a cobra command for `stack` subcommands
13
+func NewStackCommand(dockerCli *client.DockerCli) *cobra.Command {
14
+	cmd := &cobra.Command{
15
+		Use:   "stack",
16
+		Short: "Manage Docker stacks",
17
+		Args:  cli.NoArgs,
18
+		Run: func(cmd *cobra.Command, args []string) {
19
+			fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
20
+		},
21
+	}
22
+	cmd.AddCommand(
23
+		newConfigCommand(dockerCli),
24
+		newDeployCommand(dockerCli),
25
+		newRemoveCommand(dockerCli),
26
+		newTasksCommand(dockerCli),
27
+	)
28
+	return cmd
29
+}
30
+
31
+// NewTopLevelDeployCommand return a command for `docker deploy`
32
+func NewTopLevelDeployCommand(dockerCli *client.DockerCli) *cobra.Command {
33
+	cmd := newDeployCommand(dockerCli)
34
+	// Remove the aliases at the top level
35
+	cmd.Aliases = []string{}
36
+	return cmd
37
+}
0 38
new file mode 100644
... ...
@@ -0,0 +1,18 @@
0
+// +build !experimental
1
+
2
+package stack
3
+
4
+import (
5
+	"github.com/docker/docker/api/client"
6
+	"github.com/spf13/cobra"
7
+)
8
+
9
+// NewStackCommand returns nocommand
10
+func NewStackCommand(dockerCli *client.DockerCli) *cobra.Command {
11
+	return &cobra.Command{}
12
+}
13
+
14
+// NewTopLevelDeployCommand return no command
15
+func NewTopLevelDeployCommand(dockerCli *client.DockerCli) *cobra.Command {
16
+	return &cobra.Command{}
17
+}
0 18
new file mode 100644
... ...
@@ -0,0 +1,50 @@
0
+// +build experimental
1
+
2
+package stack
3
+
4
+import (
5
+	"golang.org/x/net/context"
6
+
7
+	"github.com/docker/engine-api/client"
8
+	"github.com/docker/engine-api/types"
9
+	"github.com/docker/engine-api/types/filters"
10
+	"github.com/docker/engine-api/types/swarm"
11
+)
12
+
13
+const (
14
+	labelNamespace = "com.docker.stack.namespace"
15
+)
16
+
17
+func getStackLabels(namespace string, labels map[string]string) map[string]string {
18
+	if labels == nil {
19
+		labels = make(map[string]string)
20
+	}
21
+	labels[labelNamespace] = namespace
22
+	return labels
23
+}
24
+
25
+func getStackFilter(namespace string) filters.Args {
26
+	filter := filters.NewArgs()
27
+	filter.Add("label", labelNamespace+"="+namespace)
28
+	return filter
29
+}
30
+
31
+func getServices(
32
+	ctx context.Context,
33
+	apiclient client.APIClient,
34
+	namespace string,
35
+) ([]swarm.Service, error) {
36
+	return apiclient.ServiceList(
37
+		ctx,
38
+		types.ServiceListOptions{Filter: getStackFilter(namespace)})
39
+}
40
+
41
+func getNetworks(
42
+	ctx context.Context,
43
+	apiclient client.APIClient,
44
+	namespace string,
45
+) ([]types.NetworkResource, error) {
46
+	return apiclient.NetworkList(
47
+		ctx,
48
+		types.NetworkListOptions{Filters: getStackFilter(namespace)})
49
+}
0 50
new file mode 100644
... ...
@@ -0,0 +1,41 @@
0
+// +build experimental
1
+
2
+package stack
3
+
4
+import (
5
+	"github.com/docker/docker/api/client"
6
+	"github.com/docker/docker/api/client/bundlefile"
7
+	"github.com/docker/docker/cli"
8
+	"github.com/spf13/cobra"
9
+)
10
+
11
+type configOptions struct {
12
+	bundlefile string
13
+	namespace  string
14
+}
15
+
16
+func newConfigCommand(dockerCli *client.DockerCli) *cobra.Command {
17
+	var opts configOptions
18
+
19
+	cmd := &cobra.Command{
20
+		Use:   "config [OPTIONS] STACK",
21
+		Short: "Print the stack configuration",
22
+		Args:  cli.ExactArgs(1),
23
+		RunE: func(cmd *cobra.Command, args []string) error {
24
+			opts.namespace = args[0]
25
+			return runConfig(dockerCli, opts)
26
+		},
27
+	}
28
+
29
+	flags := cmd.Flags()
30
+	addBundlefileFlag(&opts.bundlefile, flags)
31
+	return cmd
32
+}
33
+
34
+func runConfig(dockerCli *client.DockerCli, opts configOptions) error {
35
+	bundle, err := loadBundlefile(dockerCli.Err(), opts.namespace, opts.bundlefile)
36
+	if err != nil {
37
+		return err
38
+	}
39
+	return bundlefile.Print(dockerCli.Out(), bundle)
40
+}
0 41
new file mode 100644
... ...
@@ -0,0 +1,205 @@
0
+// +build experimental
1
+
2
+package stack
3
+
4
+import (
5
+	"fmt"
6
+
7
+	"github.com/spf13/cobra"
8
+	"golang.org/x/net/context"
9
+
10
+	"github.com/docker/docker/api/client"
11
+	"github.com/docker/docker/api/client/bundlefile"
12
+	"github.com/docker/docker/cli"
13
+	"github.com/docker/engine-api/types"
14
+	"github.com/docker/engine-api/types/network"
15
+	"github.com/docker/engine-api/types/swarm"
16
+)
17
+
18
+const (
19
+	defaultNetworkDriver = "overlay"
20
+)
21
+
22
+type deployOptions struct {
23
+	bundlefile string
24
+	namespace  string
25
+}
26
+
27
+func newDeployCommand(dockerCli *client.DockerCli) *cobra.Command {
28
+	var opts deployOptions
29
+
30
+	cmd := &cobra.Command{
31
+		Use:     "deploy [OPTIONS] STACK",
32
+		Aliases: []string{"up"},
33
+		Short:   "Create and update a stack",
34
+		Args:    cli.ExactArgs(1),
35
+		RunE: func(cmd *cobra.Command, args []string) error {
36
+			opts.namespace = args[0]
37
+			return runDeploy(dockerCli, opts)
38
+		},
39
+	}
40
+
41
+	flags := cmd.Flags()
42
+	addBundlefileFlag(&opts.bundlefile, flags)
43
+	return cmd
44
+}
45
+
46
+func runDeploy(dockerCli *client.DockerCli, opts deployOptions) error {
47
+	bundle, err := loadBundlefile(dockerCli.Err(), opts.namespace, opts.bundlefile)
48
+	if err != nil {
49
+		return err
50
+	}
51
+
52
+	networks := getUniqueNetworkNames(bundle.Services)
53
+	ctx := context.Background()
54
+
55
+	if err := updateNetworks(ctx, dockerCli, networks, opts.namespace); err != nil {
56
+		return err
57
+	}
58
+	return deployServices(ctx, dockerCli, bundle.Services, opts.namespace)
59
+}
60
+
61
+func getUniqueNetworkNames(services map[string]bundlefile.Service) []string {
62
+	networkSet := make(map[string]bool)
63
+	for _, service := range services {
64
+		for _, network := range service.Networks {
65
+			networkSet[network] = true
66
+		}
67
+	}
68
+
69
+	networks := []string{}
70
+	for network := range networkSet {
71
+		networks = append(networks, network)
72
+	}
73
+	return networks
74
+}
75
+
76
+func updateNetworks(
77
+	ctx context.Context,
78
+	dockerCli *client.DockerCli,
79
+	networks []string,
80
+	namespace string,
81
+) error {
82
+	client := dockerCli.Client()
83
+
84
+	existingNetworks, err := getNetworks(ctx, client, namespace)
85
+	if err != nil {
86
+		return err
87
+	}
88
+
89
+	existingNetworkMap := make(map[string]types.NetworkResource)
90
+	for _, network := range existingNetworks {
91
+		existingNetworkMap[network.Name] = network
92
+	}
93
+
94
+	createOpts := types.NetworkCreate{
95
+		Labels: getStackLabels(namespace, nil),
96
+		Driver: defaultNetworkDriver,
97
+		// TODO: remove when engine-api uses omitempty for IPAM
98
+		IPAM: network.IPAM{Driver: "default"},
99
+	}
100
+
101
+	for _, internalName := range networks {
102
+		name := fmt.Sprintf("%s_%s", namespace, internalName)
103
+
104
+		if _, exists := existingNetworkMap[name]; exists {
105
+			continue
106
+		}
107
+		fmt.Fprintf(dockerCli.Out(), "Creating network %s\n", name)
108
+		if _, err := client.NetworkCreate(ctx, name, createOpts); err != nil {
109
+			return err
110
+		}
111
+	}
112
+	return nil
113
+}
114
+
115
+func convertNetworks(networks []string, namespace string, name string) []swarm.NetworkAttachmentConfig {
116
+	nets := []swarm.NetworkAttachmentConfig{}
117
+	for _, network := range networks {
118
+		nets = append(nets, swarm.NetworkAttachmentConfig{
119
+			Target:  namespace + "_" + network,
120
+			Aliases: []string{name},
121
+		})
122
+	}
123
+	return nets
124
+}
125
+
126
+func deployServices(
127
+	ctx context.Context,
128
+	dockerCli *client.DockerCli,
129
+	services map[string]bundlefile.Service,
130
+	namespace string,
131
+) error {
132
+	apiClient := dockerCli.Client()
133
+	out := dockerCli.Out()
134
+
135
+	existingServices, err := getServices(ctx, apiClient, namespace)
136
+	if err != nil {
137
+		return err
138
+	}
139
+
140
+	existingServiceMap := make(map[string]swarm.Service)
141
+	for _, service := range existingServices {
142
+		existingServiceMap[service.Spec.Name] = service
143
+	}
144
+
145
+	for internalName, service := range services {
146
+		name := fmt.Sprintf("%s_%s", namespace, internalName)
147
+
148
+		var ports []swarm.PortConfig
149
+		for _, portSpec := range service.Ports {
150
+			ports = append(ports, swarm.PortConfig{
151
+				Protocol:   swarm.PortConfigProtocol(portSpec.Protocol),
152
+				TargetPort: portSpec.Port,
153
+			})
154
+		}
155
+
156
+		serviceSpec := swarm.ServiceSpec{
157
+			Annotations: swarm.Annotations{
158
+				Name:   name,
159
+				Labels: getStackLabels(namespace, service.Labels),
160
+			},
161
+			TaskTemplate: swarm.TaskSpec{
162
+				ContainerSpec: swarm.ContainerSpec{
163
+					Image:   service.Image,
164
+					Command: service.Command,
165
+					Args:    service.Args,
166
+					Env:     service.Env,
167
+				},
168
+			},
169
+			EndpointSpec: &swarm.EndpointSpec{
170
+				Ports: ports,
171
+			},
172
+			Networks: convertNetworks(service.Networks, namespace, internalName),
173
+		}
174
+
175
+		cspec := &serviceSpec.TaskTemplate.ContainerSpec
176
+		if service.WorkingDir != nil {
177
+			cspec.Dir = *service.WorkingDir
178
+		}
179
+		if service.User != nil {
180
+			cspec.User = *service.User
181
+		}
182
+
183
+		if service, exists := existingServiceMap[name]; exists {
184
+			fmt.Fprintf(out, "Updating service %s (id: %s)\n", name, service.ID)
185
+
186
+			if err := apiClient.ServiceUpdate(
187
+				ctx,
188
+				service.ID,
189
+				service.Version,
190
+				serviceSpec,
191
+			); err != nil {
192
+				return err
193
+			}
194
+		} else {
195
+			fmt.Fprintf(out, "Creating service %s\n", name)
196
+
197
+			if _, err := apiClient.ServiceCreate(ctx, serviceSpec); err != nil {
198
+				return err
199
+			}
200
+		}
201
+	}
202
+
203
+	return nil
204
+}
0 205
new file mode 100644
... ...
@@ -0,0 +1,39 @@
0
+// +build experimental
1
+
2
+package stack
3
+
4
+import (
5
+	"fmt"
6
+	"io"
7
+	"os"
8
+
9
+	"github.com/docker/docker/api/client/bundlefile"
10
+	"github.com/spf13/pflag"
11
+)
12
+
13
+func addBundlefileFlag(opt *string, flags *pflag.FlagSet) {
14
+	flags.StringVarP(
15
+		opt,
16
+		"bundle", "f", "",
17
+		"Path to a bundle (Default: STACK.dsb)")
18
+}
19
+
20
+func loadBundlefile(stderr io.Writer, namespace string, path string) (*bundlefile.Bundlefile, error) {
21
+	defaultPath := fmt.Sprintf("%s.dsb", namespace)
22
+
23
+	if path == "" {
24
+		path = defaultPath
25
+	}
26
+	if _, err := os.Stat(path); err != nil {
27
+		return nil, fmt.Errorf(
28
+			"Bundle %s not found. Specify the path with -f or --bundle",
29
+			path)
30
+	}
31
+
32
+	fmt.Fprintf(stderr, "Loading bundle from %s\n", path)
33
+	bundle, err := bundlefile.LoadFile(path)
34
+	if err != nil {
35
+		return nil, fmt.Errorf("Error reading %s: %v\n", path, err)
36
+	}
37
+	return bundle, err
38
+}
0 39
new file mode 100644
... ...
@@ -0,0 +1,70 @@
0
+// +build experimental
1
+
2
+package stack
3
+
4
+import (
5
+	"fmt"
6
+
7
+	"golang.org/x/net/context"
8
+
9
+	"github.com/docker/docker/api/client"
10
+	"github.com/docker/docker/cli"
11
+	"github.com/spf13/cobra"
12
+)
13
+
14
+type removeOptions struct {
15
+	namespace string
16
+}
17
+
18
+func newRemoveCommand(dockerCli *client.DockerCli) *cobra.Command {
19
+	var opts removeOptions
20
+
21
+	cmd := &cobra.Command{
22
+		Use:     "rm STACK",
23
+		Aliases: []string{"remove", "down"},
24
+		Short:   "Remove the stack",
25
+		Args:    cli.ExactArgs(1),
26
+		RunE: func(cmd *cobra.Command, args []string) error {
27
+			opts.namespace = args[0]
28
+			return runRemove(dockerCli, opts)
29
+		},
30
+	}
31
+	return cmd
32
+}
33
+
34
+func runRemove(dockerCli *client.DockerCli, opts removeOptions) error {
35
+	namespace := opts.namespace
36
+	client := dockerCli.Client()
37
+	stderr := dockerCli.Err()
38
+	ctx := context.Background()
39
+	hasError := false
40
+
41
+	services, err := getServices(ctx, client, namespace)
42
+	if err != nil {
43
+		return err
44
+	}
45
+	for _, service := range services {
46
+		fmt.Fprintf(stderr, "Removing service %s\n", service.Spec.Name)
47
+		if err := client.ServiceRemove(ctx, service.ID); err != nil {
48
+			hasError = true
49
+			fmt.Fprintf(stderr, "Failed to remove service %s: %s", service.ID, err)
50
+		}
51
+	}
52
+
53
+	networks, err := getNetworks(ctx, client, namespace)
54
+	if err != nil {
55
+		return err
56
+	}
57
+	for _, network := range networks {
58
+		fmt.Fprintf(stderr, "Removing network %s\n", network.Name)
59
+		if err := client.NetworkRemove(ctx, network.ID); err != nil {
60
+			hasError = true
61
+			fmt.Fprintf(stderr, "Failed to remove network %s: %s", network.ID, err)
62
+		}
63
+	}
64
+
65
+	if hasError {
66
+		return fmt.Errorf("Failed to remove some resources")
67
+	}
68
+	return nil
69
+}
0 70
new file mode 100644
... ...
@@ -0,0 +1,62 @@
0
+// +build experimental
1
+
2
+package stack
3
+
4
+import (
5
+	"golang.org/x/net/context"
6
+
7
+	"github.com/docker/docker/api/client"
8
+	"github.com/docker/docker/api/client/idresolver"
9
+	"github.com/docker/docker/api/client/task"
10
+	"github.com/docker/docker/cli"
11
+	"github.com/docker/docker/opts"
12
+	"github.com/docker/engine-api/types"
13
+	"github.com/docker/engine-api/types/swarm"
14
+	"github.com/spf13/cobra"
15
+)
16
+
17
+type tasksOptions struct {
18
+	all       bool
19
+	filter    opts.FilterOpt
20
+	namespace string
21
+	noResolve bool
22
+}
23
+
24
+func newTasksCommand(dockerCli *client.DockerCli) *cobra.Command {
25
+	opts := tasksOptions{filter: opts.NewFilterOpt()}
26
+
27
+	cmd := &cobra.Command{
28
+		Use:   "tasks [OPTIONS] STACK",
29
+		Short: "List the tasks in the stack",
30
+		Args:  cli.ExactArgs(1),
31
+		RunE: func(cmd *cobra.Command, args []string) error {
32
+			opts.namespace = args[0]
33
+			return runTasks(dockerCli, opts)
34
+		},
35
+	}
36
+	flags := cmd.Flags()
37
+	flags.BoolVarP(&opts.all, "all", "a", false, "Display all tasks")
38
+	flags.BoolVarP(&opts.noResolve, "no-resolve", "n", false, "Do not map IDs to Names")
39
+	flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided")
40
+
41
+	return cmd
42
+}
43
+
44
+func runTasks(dockerCli *client.DockerCli, opts tasksOptions) error {
45
+	client := dockerCli.Client()
46
+	ctx := context.Background()
47
+
48
+	filter := opts.filter.Value()
49
+	filter.Add("label", labelNamespace+"="+opts.namespace)
50
+	if !opts.all && !filter.Include("desired_state") {
51
+		filter.Add("desired_state", string(swarm.TaskStateRunning))
52
+		filter.Add("desired_state", string(swarm.TaskStateAccepted))
53
+	}
54
+
55
+	tasks, err := client.TaskList(ctx, types.TaskListOptions{Filter: filter})
56
+	if err != nil {
57
+		return err
58
+	}
59
+
60
+	return task.Print(dockerCli, ctx, tasks, idresolver.New(client, opts.noResolve))
61
+}
... ...
@@ -13,7 +13,7 @@ import (
13 13
 func NewSwarmCommand(dockerCli *client.DockerCli) *cobra.Command {
14 14
 	cmd := &cobra.Command{
15 15
 		Use:   "swarm",
16
-		Short: "Manage docker swarm",
16
+		Short: "Manage Docker Swarm",
17 17
 		Args:  cli.NoArgs,
18 18
 		Run: func(cmd *cobra.Command, args []string) {
19 19
 			fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
... ...
@@ -8,6 +8,7 @@ import (
8 8
 	"github.com/docker/docker/api/client/node"
9 9
 	"github.com/docker/docker/api/client/registry"
10 10
 	"github.com/docker/docker/api/client/service"
11
+	"github.com/docker/docker/api/client/stack"
11 12
 	"github.com/docker/docker/api/client/swarm"
12 13
 	"github.com/docker/docker/api/client/system"
13 14
 	"github.com/docker/docker/api/client/volume"
... ...
@@ -41,6 +42,8 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor {
41 41
 	rootCmd.AddCommand(
42 42
 		node.NewNodeCommand(dockerCli),
43 43
 		service.NewServiceCommand(dockerCli),
44
+		stack.NewStackCommand(dockerCli),
45
+		stack.NewTopLevelDeployCommand(dockerCli),
44 46
 		swarm.NewSwarmCommand(dockerCli),
45 47
 		container.NewAttachCommand(dockerCli),
46 48
 		container.NewCommitCommand(dockerCli),
... ...
@@ -96,7 +99,9 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor {
96 96
 func (c CobraAdaptor) Usage() []cli.Command {
97 97
 	cmds := []cli.Command{}
98 98
 	for _, cmd := range c.rootCmd.Commands() {
99
-		cmds = append(cmds, cli.Command{Name: cmd.Name(), Description: cmd.Short})
99
+		if cmd.Name() != "" {
100
+			cmds = append(cmds, cli.Command{Name: cmd.Name(), Description: cmd.Short})
101
+		}
100 102
 	}
101 103
 	return cmds
102 104
 }