Browse code

Add Swarm management CLI commands

As described in our ROADMAP.md, introduce new Swarm management commands
to call to the corresponding API endpoints.

This PR is fully backward compatible (joining a Swarm is an optional
feature of the Engine, and existing commands are not impacted).

Signed-off-by: Daniel Nephin <dnephin@docker.com>
Signed-off-by: Victor Vieux <vieux@docker.com>
Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>

Daniel Nephin authored on 2016/06/14 11:56:23
Showing 36 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,70 @@
0
+package idresolver
1
+
2
+import (
3
+	"fmt"
4
+
5
+	"golang.org/x/net/context"
6
+
7
+	"github.com/docker/engine-api/client"
8
+	"github.com/docker/engine-api/types/swarm"
9
+)
10
+
11
+// IDResolver provides ID to Name resolution.
12
+type IDResolver struct {
13
+	client    client.APIClient
14
+	noResolve bool
15
+	cache     map[string]string
16
+}
17
+
18
+// New creates a new IDResolver.
19
+func New(client client.APIClient, noResolve bool) *IDResolver {
20
+	return &IDResolver{
21
+		client:    client,
22
+		noResolve: noResolve,
23
+		cache:     make(map[string]string),
24
+	}
25
+}
26
+
27
+func (r *IDResolver) get(ctx context.Context, t interface{}, id string) (string, error) {
28
+	switch t.(type) {
29
+	case swarm.Node:
30
+		node, err := r.client.NodeInspect(ctx, id)
31
+		if err != nil {
32
+			return id, nil
33
+		}
34
+		if node.Spec.Annotations.Name != "" {
35
+			return node.Spec.Annotations.Name, nil
36
+		}
37
+		if node.Description.Hostname != "" {
38
+			return node.Description.Hostname, nil
39
+		}
40
+		return id, nil
41
+	case swarm.Service:
42
+		service, err := r.client.ServiceInspect(ctx, id)
43
+		if err != nil {
44
+			return id, nil
45
+		}
46
+		return service.Spec.Annotations.Name, nil
47
+	default:
48
+		return "", fmt.Errorf("unsupported type")
49
+	}
50
+
51
+}
52
+
53
+// Resolve will attempt to resolve an ID to a Name by querying the manager.
54
+// Results are stored into a cache.
55
+// If the `-n` flag is used in the command-line, resolution is disabled.
56
+func (r *IDResolver) Resolve(ctx context.Context, t interface{}, id string) (string, error) {
57
+	if r.noResolve {
58
+		return id, nil
59
+	}
60
+	if name, ok := r.cache[id]; ok {
61
+		return name, nil
62
+	}
63
+	name, err := r.get(ctx, t, id)
64
+	if err != nil {
65
+		return "", err
66
+	}
67
+	r.cache[id] = name
68
+	return name, nil
69
+}
... ...
@@ -10,6 +10,7 @@ import (
10 10
 	"github.com/docker/docker/pkg/ioutils"
11 11
 	flag "github.com/docker/docker/pkg/mflag"
12 12
 	"github.com/docker/docker/utils"
13
+	"github.com/docker/engine-api/types/swarm"
13 14
 	"github.com/docker/go-units"
14 15
 )
15 16
 
... ...
@@ -68,6 +69,21 @@ func (cli *DockerCli) CmdInfo(args ...string) error {
68 68
 		fmt.Fprintf(cli.out, "\n")
69 69
 	}
70 70
 
71
+	fmt.Fprintf(cli.out, "Swarm: %v\n", info.Swarm.LocalNodeState)
72
+	if info.Swarm.LocalNodeState != swarm.LocalNodeStateInactive {
73
+		fmt.Fprintf(cli.out, " NodeID: %s\n", info.Swarm.NodeID)
74
+		if info.Swarm.Error != "" {
75
+			fmt.Fprintf(cli.out, " Error: %v\n", info.Swarm.Error)
76
+		}
77
+		if info.Swarm.ControlAvailable {
78
+			fmt.Fprintf(cli.out, " IsManager: Yes\n")
79
+			fmt.Fprintf(cli.out, " Managers: %d\n", info.Swarm.Managers)
80
+			fmt.Fprintf(cli.out, " Nodes: %d\n", info.Swarm.Nodes)
81
+			ioutils.FprintfIfNotEmpty(cli.out, " CACertHash: %s\n", info.Swarm.CACertHash)
82
+		} else {
83
+			fmt.Fprintf(cli.out, " IsManager: No\n")
84
+		}
85
+	}
71 86
 	ioutils.FprintfIfNotEmpty(cli.out, "Kernel Version: %s\n", info.KernelVersion)
72 87
 	ioutils.FprintfIfNotEmpty(cli.out, "Operating System: %s\n", info.OperatingSystem)
73 88
 	ioutils.FprintfIfNotEmpty(cli.out, "OSType: %s\n", info.OSType)
... ...
@@ -11,19 +11,19 @@ import (
11 11
 	"github.com/docker/engine-api/client"
12 12
 )
13 13
 
14
-// CmdInspect displays low-level information on one or more containers or images.
14
+// CmdInspect displays low-level information on one or more containers, images or tasks.
15 15
 //
16
-// Usage: docker inspect [OPTIONS] CONTAINER|IMAGE [CONTAINER|IMAGE...]
16
+// Usage: docker inspect [OPTIONS] CONTAINER|IMAGE|TASK [CONTAINER|IMAGE|TASK...]
17 17
 func (cli *DockerCli) CmdInspect(args ...string) error {
18
-	cmd := Cli.Subcmd("inspect", []string{"CONTAINER|IMAGE [CONTAINER|IMAGE...]"}, Cli.DockerCommands["inspect"].Description, true)
18
+	cmd := Cli.Subcmd("inspect", []string{"CONTAINER|IMAGE|TASK [CONTAINER|IMAGE|TASK...]"}, Cli.DockerCommands["inspect"].Description, true)
19 19
 	tmplStr := cmd.String([]string{"f", "-format"}, "", "Format the output using the given go template")
20
-	inspectType := cmd.String([]string{"-type"}, "", "Return JSON for specified type, (e.g image or container)")
20
+	inspectType := cmd.String([]string{"-type"}, "", "Return JSON for specified type, (e.g image, container or task)")
21 21
 	size := cmd.Bool([]string{"s", "-size"}, false, "Display total file sizes if the type is container")
22 22
 	cmd.Require(flag.Min, 1)
23 23
 
24 24
 	cmd.ParseFlags(args, true)
25 25
 
26
-	if *inspectType != "" && *inspectType != "container" && *inspectType != "image" {
26
+	if *inspectType != "" && *inspectType != "container" && *inspectType != "image" && *inspectType != "task" {
27 27
 		return fmt.Errorf("%q is not a valid value for --type", *inspectType)
28 28
 	}
29 29
 
... ...
@@ -35,6 +35,11 @@ func (cli *DockerCli) CmdInspect(args ...string) error {
35 35
 		elementSearcher = cli.inspectContainers(ctx, *size)
36 36
 	case "image":
37 37
 		elementSearcher = cli.inspectImages(ctx, *size)
38
+	case "task":
39
+		if *size {
40
+			fmt.Fprintln(cli.err, "WARNING: --size ignored for tasks")
41
+		}
42
+		elementSearcher = cli.inspectTasks(ctx)
38 43
 	default:
39 44
 		elementSearcher = cli.inspectAll(ctx, *size)
40 45
 	}
... ...
@@ -54,6 +59,12 @@ func (cli *DockerCli) inspectImages(ctx context.Context, getSize bool) inspect.G
54 54
 	}
55 55
 }
56 56
 
57
+func (cli *DockerCli) inspectTasks(ctx context.Context) inspect.GetRefFunc {
58
+	return func(ref string) (interface{}, []byte, error) {
59
+		return cli.client.TaskInspectWithRaw(ctx, ref)
60
+	}
61
+}
62
+
57 63
 func (cli *DockerCli) inspectAll(ctx context.Context, getSize bool) inspect.GetRefFunc {
58 64
 	return func(ref string) (interface{}, []byte, error) {
59 65
 		c, rawContainer, err := cli.client.ContainerInspectWithRaw(ctx, ref, getSize)
... ...
@@ -63,7 +74,15 @@ func (cli *DockerCli) inspectAll(ctx context.Context, getSize bool) inspect.GetR
63 63
 				i, rawImage, err := cli.client.ImageInspectWithRaw(ctx, ref, getSize)
64 64
 				if err != nil {
65 65
 					if client.IsErrImageNotFound(err) {
66
-						return nil, nil, fmt.Errorf("Error: No such image or container: %s", ref)
66
+						// Search for task with that id if an image doesn't exists.
67
+						t, rawTask, err := cli.client.TaskInspectWithRaw(ctx, ref)
68
+						if err != nil {
69
+							return nil, nil, fmt.Errorf("Error: No such image, container or task: %s", ref)
70
+						}
71
+						if getSize {
72
+							fmt.Fprintln(cli.err, "WARNING: --size ignored for tasks")
73
+						}
74
+						return t, rawTask, nil
67 75
 					}
68 76
 					return nil, nil, err
69 77
 				}
... ...
@@ -71,7 +71,7 @@ func runList(dockerCli *client.DockerCli, opts listOptions) error {
71 71
 
72 72
 	w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0)
73 73
 	if !opts.quiet {
74
-		fmt.Fprintf(w, "NETWORK ID\tNAME\tDRIVER")
74
+		fmt.Fprintf(w, "NETWORK ID\tNAME\tDRIVER\tSCOPE")
75 75
 		fmt.Fprintf(w, "\n")
76 76
 	}
77 77
 
... ...
@@ -79,6 +79,8 @@ func runList(dockerCli *client.DockerCli, opts listOptions) error {
79 79
 	for _, networkResource := range networkResources {
80 80
 		ID := networkResource.ID
81 81
 		netName := networkResource.Name
82
+		driver := networkResource.Driver
83
+		scope := networkResource.Scope
82 84
 		if !opts.noTrunc {
83 85
 			ID = stringid.TruncateID(ID)
84 86
 		}
... ...
@@ -86,11 +88,11 @@ func runList(dockerCli *client.DockerCli, opts listOptions) error {
86 86
 			fmt.Fprintln(w, ID)
87 87
 			continue
88 88
 		}
89
-		driver := networkResource.Driver
90
-		fmt.Fprintf(w, "%s\t%s\t%s\t",
89
+		fmt.Fprintf(w, "%s\t%s\t%s\t%s\t",
91 90
 			ID,
92 91
 			netName,
93
-			driver)
92
+			driver,
93
+			scope)
94 94
 		fmt.Fprint(w, "\n")
95 95
 	}
96 96
 	w.Flush()
97 97
new file mode 100644
... ...
@@ -0,0 +1,40 @@
0
+package node
1
+
2
+import (
3
+	"fmt"
4
+
5
+	"github.com/docker/docker/api/client"
6
+	"github.com/docker/docker/cli"
7
+	"github.com/docker/engine-api/types/swarm"
8
+	"github.com/spf13/cobra"
9
+	"github.com/spf13/pflag"
10
+)
11
+
12
+func newAcceptCommand(dockerCli *client.DockerCli) *cobra.Command {
13
+	var flags *pflag.FlagSet
14
+
15
+	cmd := &cobra.Command{
16
+		Use:   "accept NODE [NODE...]",
17
+		Short: "Accept a node in the swarm",
18
+		Args:  cli.RequiresMinArgs(1),
19
+		RunE: func(cmd *cobra.Command, args []string) error {
20
+			return runAccept(dockerCli, flags, args)
21
+		},
22
+	}
23
+
24
+	flags = cmd.Flags()
25
+	return cmd
26
+}
27
+
28
+func runAccept(dockerCli *client.DockerCli, flags *pflag.FlagSet, args []string) error {
29
+	for _, id := range args {
30
+		if err := runUpdate(dockerCli, id, func(node *swarm.Node) {
31
+			node.Spec.Membership = swarm.NodeMembershipAccepted
32
+		}); err != nil {
33
+			return err
34
+		}
35
+		fmt.Println(id, "attempting to accept a node in the swarm.")
36
+	}
37
+
38
+	return nil
39
+}
0 40
new file mode 100644
... ...
@@ -0,0 +1,49 @@
0
+package node
1
+
2
+import (
3
+	"fmt"
4
+
5
+	"golang.org/x/net/context"
6
+
7
+	"github.com/spf13/cobra"
8
+
9
+	"github.com/docker/docker/api/client"
10
+	"github.com/docker/docker/cli"
11
+	apiclient "github.com/docker/engine-api/client"
12
+)
13
+
14
+// NewNodeCommand returns a cobra command for `node` subcommands
15
+func NewNodeCommand(dockerCli *client.DockerCli) *cobra.Command {
16
+	cmd := &cobra.Command{
17
+		Use:   "node",
18
+		Short: "Manage docker swarm nodes",
19
+		Args:  cli.NoArgs,
20
+		Run: func(cmd *cobra.Command, args []string) {
21
+			fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
22
+		},
23
+	}
24
+	cmd.AddCommand(
25
+		newAcceptCommand(dockerCli),
26
+		newDemoteCommand(dockerCli),
27
+		newInspectCommand(dockerCli),
28
+		newListCommand(dockerCli),
29
+		newPromoteCommand(dockerCli),
30
+		newRemoveCommand(dockerCli),
31
+		newTasksCommand(dockerCli),
32
+		newUpdateCommand(dockerCli),
33
+	)
34
+	return cmd
35
+}
36
+
37
+func nodeReference(client apiclient.APIClient, ctx context.Context, ref string) (string, error) {
38
+	// The special value "self" for a node reference is mapped to the current
39
+	// node, hence the node ID is retrieved using the `/info` endpoint.
40
+	if ref == "self" {
41
+		info, err := client.Info(ctx)
42
+		if err != nil {
43
+			return "", err
44
+		}
45
+		return info.Swarm.NodeID, nil
46
+	}
47
+	return ref, nil
48
+}
0 49
new file mode 100644
... ...
@@ -0,0 +1,40 @@
0
+package node
1
+
2
+import (
3
+	"fmt"
4
+
5
+	"github.com/docker/docker/api/client"
6
+	"github.com/docker/docker/cli"
7
+	"github.com/docker/engine-api/types/swarm"
8
+	"github.com/spf13/cobra"
9
+	"github.com/spf13/pflag"
10
+)
11
+
12
+func newDemoteCommand(dockerCli *client.DockerCli) *cobra.Command {
13
+	var flags *pflag.FlagSet
14
+
15
+	cmd := &cobra.Command{
16
+		Use:   "demote NODE [NODE...]",
17
+		Short: "Demote a node from manager in the swarm",
18
+		Args:  cli.RequiresMinArgs(1),
19
+		RunE: func(cmd *cobra.Command, args []string) error {
20
+			return runDemote(dockerCli, flags, args)
21
+		},
22
+	}
23
+
24
+	flags = cmd.Flags()
25
+	return cmd
26
+}
27
+
28
+func runDemote(dockerCli *client.DockerCli, flags *pflag.FlagSet, args []string) error {
29
+	for _, id := range args {
30
+		if err := runUpdate(dockerCli, id, func(node *swarm.Node) {
31
+			node.Spec.Role = swarm.NodeRoleWorker
32
+		}); err != nil {
33
+			return err
34
+		}
35
+		fmt.Println(id, "attempting to demote a manager in the swarm.")
36
+	}
37
+
38
+	return nil
39
+}
0 40
new file mode 100644
... ...
@@ -0,0 +1,141 @@
0
+package node
1
+
2
+import (
3
+	"fmt"
4
+	"io"
5
+	"sort"
6
+	"strings"
7
+
8
+	"github.com/docker/docker/api/client"
9
+	"github.com/docker/docker/api/client/inspect"
10
+	"github.com/docker/docker/cli"
11
+	"github.com/docker/docker/pkg/ioutils"
12
+	"github.com/docker/engine-api/types/swarm"
13
+	"github.com/docker/go-units"
14
+	"github.com/spf13/cobra"
15
+	"golang.org/x/net/context"
16
+)
17
+
18
+type inspectOptions struct {
19
+	nodeIds []string
20
+	format  string
21
+	pretty  bool
22
+}
23
+
24
+func newInspectCommand(dockerCli *client.DockerCli) *cobra.Command {
25
+	var opts inspectOptions
26
+
27
+	cmd := &cobra.Command{
28
+		Use:   "inspect [OPTIONS] self|NODE [NODE...]",
29
+		Short: "Inspect a node in the swarm",
30
+		Args:  cli.RequiresMinArgs(1),
31
+		RunE: func(cmd *cobra.Command, args []string) error {
32
+			opts.nodeIds = args
33
+			return runInspect(dockerCli, opts)
34
+		},
35
+	}
36
+
37
+	flags := cmd.Flags()
38
+	flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template")
39
+	flags.BoolVarP(&opts.pretty, "pretty", "p", false, "Print the information in a human friendly format.")
40
+	return cmd
41
+}
42
+
43
+func runInspect(dockerCli *client.DockerCli, opts inspectOptions) error {
44
+	client := dockerCli.Client()
45
+	ctx := context.Background()
46
+	getRef := func(ref string) (interface{}, []byte, error) {
47
+		nodeRef, err := nodeReference(client, ctx, ref)
48
+		if err != nil {
49
+			return nil, nil, err
50
+		}
51
+		node, err := client.NodeInspect(ctx, nodeRef)
52
+		return node, nil, err
53
+	}
54
+
55
+	if !opts.pretty {
56
+		return inspect.Inspect(dockerCli.Out(), opts.nodeIds, opts.format, getRef)
57
+	}
58
+	return printHumanFriendly(dockerCli.Out(), opts.nodeIds, getRef)
59
+}
60
+
61
+func printHumanFriendly(out io.Writer, refs []string, getRef inspect.GetRefFunc) error {
62
+	for idx, ref := range refs {
63
+		obj, _, err := getRef(ref)
64
+		if err != nil {
65
+			return err
66
+		}
67
+		printNode(out, obj.(swarm.Node))
68
+
69
+		// TODO: better way to do this?
70
+		// print extra space between objects, but not after the last one
71
+		if idx+1 != len(refs) {
72
+			fmt.Fprintf(out, "\n\n")
73
+		}
74
+	}
75
+	return nil
76
+}
77
+
78
+// TODO: use a template
79
+func printNode(out io.Writer, node swarm.Node) {
80
+	fmt.Fprintf(out, "ID:\t\t\t%s\n", node.ID)
81
+	ioutils.FprintfIfNotEmpty(out, "Name:\t\t\t%s\n", node.Spec.Name)
82
+	if node.Spec.Labels != nil {
83
+		fmt.Fprintln(out, "Labels:")
84
+		for k, v := range node.Spec.Labels {
85
+			fmt.Fprintf(out, " - %s = %s\n", k, v)
86
+		}
87
+	}
88
+
89
+	ioutils.FprintfIfNotEmpty(out, "Hostname:\t\t%s\n", node.Description.Hostname)
90
+	fmt.Fprintln(out, "Status:")
91
+	fmt.Fprintf(out, " State:\t\t\t%s\n", client.PrettyPrint(node.Status.State))
92
+	ioutils.FprintfIfNotEmpty(out, " Message:\t\t%s\n", client.PrettyPrint(node.Status.Message))
93
+	fmt.Fprintf(out, " Availability:\t\t%s\n", client.PrettyPrint(node.Spec.Availability))
94
+
95
+	if node.ManagerStatus != nil {
96
+		fmt.Fprintln(out, "Manager Status:")
97
+		fmt.Fprintf(out, " Address:\t\t%s\n", node.ManagerStatus.Addr)
98
+		fmt.Fprintf(out, " Raft status:\t\t%s\n", client.PrettyPrint(node.ManagerStatus.Reachability))
99
+		leader := "No"
100
+		if node.ManagerStatus.Leader {
101
+			leader = "Yes"
102
+		}
103
+		fmt.Fprintf(out, " Leader:\t\t%s\n", leader)
104
+	}
105
+
106
+	fmt.Fprintln(out, "Platform:")
107
+	fmt.Fprintf(out, " Operating System:\t%s\n", node.Description.Platform.OS)
108
+	fmt.Fprintf(out, " Architecture:\t\t%s\n", node.Description.Platform.Architecture)
109
+
110
+	fmt.Fprintln(out, "Resources:")
111
+	fmt.Fprintf(out, " CPUs:\t\t\t%d\n", node.Description.Resources.NanoCPUs/1e9)
112
+	fmt.Fprintf(out, " Memory:\t\t%s\n", units.BytesSize(float64(node.Description.Resources.MemoryBytes)))
113
+
114
+	var pluginTypes []string
115
+	pluginNamesByType := map[string][]string{}
116
+	for _, p := range node.Description.Engine.Plugins {
117
+		// append to pluginTypes only if not done previously
118
+		if _, ok := pluginNamesByType[p.Type]; !ok {
119
+			pluginTypes = append(pluginTypes, p.Type)
120
+		}
121
+		pluginNamesByType[p.Type] = append(pluginNamesByType[p.Type], p.Name)
122
+	}
123
+
124
+	if len(pluginTypes) > 0 {
125
+		fmt.Fprintln(out, "Plugins:")
126
+		sort.Strings(pluginTypes) // ensure stable output
127
+		for _, pluginType := range pluginTypes {
128
+			fmt.Fprintf(out, "  %s:\t\t%s\n", pluginType, strings.Join(pluginNamesByType[pluginType], ", "))
129
+		}
130
+	}
131
+	fmt.Fprintf(out, "Engine Version:\t\t%s\n", node.Description.Engine.EngineVersion)
132
+
133
+	if len(node.Description.Engine.Labels) != 0 {
134
+		fmt.Fprintln(out, "Engine Labels:")
135
+		for k, v := range node.Description.Engine.Labels {
136
+			fmt.Fprintf(out, " - %s = %s", k, v)
137
+		}
138
+	}
139
+
140
+}
0 141
new file mode 100644
... ...
@@ -0,0 +1,119 @@
0
+package node
1
+
2
+import (
3
+	"fmt"
4
+	"io"
5
+	"text/tabwriter"
6
+
7
+	"golang.org/x/net/context"
8
+
9
+	"github.com/docker/docker/api/client"
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
+const (
18
+	listItemFmt = "%s\t%s\t%s\t%s\t%s\t%s\t%s\n"
19
+)
20
+
21
+type listOptions struct {
22
+	quiet  bool
23
+	filter opts.FilterOpt
24
+}
25
+
26
+func newListCommand(dockerCli *client.DockerCli) *cobra.Command {
27
+	opts := listOptions{filter: opts.NewFilterOpt()}
28
+
29
+	cmd := &cobra.Command{
30
+		Use:     "ls",
31
+		Aliases: []string{"list"},
32
+		Short:   "List nodes in the swarm",
33
+		Args:    cli.NoArgs,
34
+		RunE: func(cmd *cobra.Command, args []string) error {
35
+			return runList(dockerCli, opts)
36
+		},
37
+	}
38
+	flags := cmd.Flags()
39
+	flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs")
40
+	flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided")
41
+
42
+	return cmd
43
+}
44
+
45
+func runList(dockerCli *client.DockerCli, opts listOptions) error {
46
+	client := dockerCli.Client()
47
+	ctx := context.Background()
48
+
49
+	nodes, err := client.NodeList(
50
+		ctx,
51
+		types.NodeListOptions{Filter: opts.filter.Value()})
52
+	if err != nil {
53
+		return err
54
+	}
55
+
56
+	info, err := client.Info(ctx)
57
+	if err != nil {
58
+		return err
59
+	}
60
+
61
+	out := dockerCli.Out()
62
+	if opts.quiet {
63
+		printQuiet(out, nodes)
64
+	} else {
65
+		printTable(out, nodes, info)
66
+	}
67
+	return nil
68
+}
69
+
70
+func printTable(out io.Writer, nodes []swarm.Node, info types.Info) {
71
+	writer := tabwriter.NewWriter(out, 0, 4, 2, ' ', 0)
72
+
73
+	// Ignore flushing errors
74
+	defer writer.Flush()
75
+
76
+	fmt.Fprintf(writer, listItemFmt, "ID", "NAME", "MEMBERSHIP", "STATUS", "AVAILABILITY", "MANAGER STATUS", "LEADER")
77
+	for _, node := range nodes {
78
+		name := node.Spec.Name
79
+		availability := string(node.Spec.Availability)
80
+		membership := string(node.Spec.Membership)
81
+
82
+		if name == "" {
83
+			name = node.Description.Hostname
84
+		}
85
+
86
+		leader := ""
87
+		if node.ManagerStatus != nil && node.ManagerStatus.Leader {
88
+			leader = "Yes"
89
+		}
90
+
91
+		reachability := ""
92
+		if node.ManagerStatus != nil {
93
+			reachability = string(node.ManagerStatus.Reachability)
94
+		}
95
+
96
+		ID := node.ID
97
+		if node.ID == info.Swarm.NodeID {
98
+			ID = ID + " *"
99
+		}
100
+
101
+		fmt.Fprintf(
102
+			writer,
103
+			listItemFmt,
104
+			ID,
105
+			name,
106
+			client.PrettyPrint(membership),
107
+			client.PrettyPrint(string(node.Status.State)),
108
+			client.PrettyPrint(availability),
109
+			client.PrettyPrint(reachability),
110
+			leader)
111
+	}
112
+}
113
+
114
+func printQuiet(out io.Writer, nodes []swarm.Node) {
115
+	for _, node := range nodes {
116
+		fmt.Fprintln(out, node.ID)
117
+	}
118
+}
0 119
new file mode 100644
... ...
@@ -0,0 +1,50 @@
0
+package node
1
+
2
+import (
3
+	"fmt"
4
+	"strings"
5
+
6
+	"github.com/docker/engine-api/types/swarm"
7
+)
8
+
9
+type nodeOptions struct {
10
+	role         string
11
+	membership   string
12
+	availability string
13
+}
14
+
15
+func (opts *nodeOptions) ToNodeSpec() (swarm.NodeSpec, error) {
16
+	var spec swarm.NodeSpec
17
+
18
+	switch swarm.NodeRole(strings.ToLower(opts.role)) {
19
+	case swarm.NodeRoleWorker:
20
+		spec.Role = swarm.NodeRoleWorker
21
+	case swarm.NodeRoleManager:
22
+		spec.Role = swarm.NodeRoleManager
23
+	case "":
24
+	default:
25
+		return swarm.NodeSpec{}, fmt.Errorf("invalid role %q, only worker and manager are supported", opts.role)
26
+	}
27
+
28
+	switch swarm.NodeMembership(strings.ToLower(opts.membership)) {
29
+	case swarm.NodeMembershipAccepted:
30
+		spec.Membership = swarm.NodeMembershipAccepted
31
+	case "":
32
+	default:
33
+		return swarm.NodeSpec{}, fmt.Errorf("invalid membership %q, only accepted is supported", opts.membership)
34
+	}
35
+
36
+	switch swarm.NodeAvailability(strings.ToLower(opts.availability)) {
37
+	case swarm.NodeAvailabilityActive:
38
+		spec.Availability = swarm.NodeAvailabilityActive
39
+	case swarm.NodeAvailabilityPause:
40
+		spec.Availability = swarm.NodeAvailabilityPause
41
+	case swarm.NodeAvailabilityDrain:
42
+		spec.Availability = swarm.NodeAvailabilityDrain
43
+	case "":
44
+	default:
45
+		return swarm.NodeSpec{}, fmt.Errorf("invalid availability %q, only active, pause and drain are supported", opts.availability)
46
+	}
47
+
48
+	return spec, nil
49
+}
0 50
new file mode 100644
... ...
@@ -0,0 +1,40 @@
0
+package node
1
+
2
+import (
3
+	"fmt"
4
+
5
+	"github.com/docker/docker/api/client"
6
+	"github.com/docker/docker/cli"
7
+	"github.com/docker/engine-api/types/swarm"
8
+	"github.com/spf13/cobra"
9
+	"github.com/spf13/pflag"
10
+)
11
+
12
+func newPromoteCommand(dockerCli *client.DockerCli) *cobra.Command {
13
+	var flags *pflag.FlagSet
14
+
15
+	cmd := &cobra.Command{
16
+		Use:   "promote NODE [NODE...]",
17
+		Short: "Promote a node to a manager in the swarm",
18
+		Args:  cli.RequiresMinArgs(1),
19
+		RunE: func(cmd *cobra.Command, args []string) error {
20
+			return runPromote(dockerCli, flags, args)
21
+		},
22
+	}
23
+
24
+	flags = cmd.Flags()
25
+	return cmd
26
+}
27
+
28
+func runPromote(dockerCli *client.DockerCli, flags *pflag.FlagSet, args []string) error {
29
+	for _, id := range args {
30
+		if err := runUpdate(dockerCli, id, func(node *swarm.Node) {
31
+			node.Spec.Role = swarm.NodeRoleManager
32
+		}); err != nil {
33
+			return err
34
+		}
35
+		fmt.Println(id, "attempting to promote a node to a manager in the swarm.")
36
+	}
37
+
38
+	return nil
39
+}
0 40
new file mode 100644
... ...
@@ -0,0 +1,36 @@
0
+package node
1
+
2
+import (
3
+	"fmt"
4
+
5
+	"golang.org/x/net/context"
6
+
7
+	"github.com/docker/docker/api/client"
8
+	"github.com/docker/docker/cli"
9
+	"github.com/spf13/cobra"
10
+)
11
+
12
+func newRemoveCommand(dockerCli *client.DockerCli) *cobra.Command {
13
+	return &cobra.Command{
14
+		Use:     "rm NODE [NODE...]",
15
+		Aliases: []string{"remove"},
16
+		Short:   "Remove a node from the swarm",
17
+		Args:    cli.RequiresMinArgs(1),
18
+		RunE: func(cmd *cobra.Command, args []string) error {
19
+			return runRemove(dockerCli, args)
20
+		},
21
+	}
22
+}
23
+
24
+func runRemove(dockerCli *client.DockerCli, args []string) error {
25
+	client := dockerCli.Client()
26
+	ctx := context.Background()
27
+	for _, nodeID := range args {
28
+		err := client.NodeRemove(ctx, nodeID)
29
+		if err != nil {
30
+			return err
31
+		}
32
+		fmt.Fprintf(dockerCli.Out(), "%s\n", nodeID)
33
+	}
34
+	return nil
35
+}
0 36
new file mode 100644
... ...
@@ -0,0 +1,72 @@
0
+package node
1
+
2
+import (
3
+	"golang.org/x/net/context"
4
+
5
+	"github.com/docker/docker/api/client"
6
+	"github.com/docker/docker/api/client/idresolver"
7
+	"github.com/docker/docker/api/client/task"
8
+	"github.com/docker/docker/cli"
9
+	"github.com/docker/docker/opts"
10
+	"github.com/docker/engine-api/types"
11
+	"github.com/docker/engine-api/types/swarm"
12
+	"github.com/spf13/cobra"
13
+)
14
+
15
+type tasksOptions struct {
16
+	nodeID    string
17
+	all       bool
18
+	noResolve bool
19
+	filter    opts.FilterOpt
20
+}
21
+
22
+func newTasksCommand(dockerCli *client.DockerCli) *cobra.Command {
23
+	opts := tasksOptions{filter: opts.NewFilterOpt()}
24
+
25
+	cmd := &cobra.Command{
26
+		Use:   "tasks [OPTIONS] self|NODE",
27
+		Short: "List tasks running on a node",
28
+		Args:  cli.ExactArgs(1),
29
+		RunE: func(cmd *cobra.Command, args []string) error {
30
+			opts.nodeID = args[0]
31
+			return runTasks(dockerCli, opts)
32
+		},
33
+	}
34
+	flags := cmd.Flags()
35
+	flags.BoolVarP(&opts.all, "all", "a", false, "Display all instances")
36
+	flags.BoolVarP(&opts.noResolve, "no-resolve", "n", false, "Do not map IDs to Names")
37
+	flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided")
38
+
39
+	return cmd
40
+}
41
+
42
+func runTasks(dockerCli *client.DockerCli, opts tasksOptions) error {
43
+	client := dockerCli.Client()
44
+	ctx := context.Background()
45
+
46
+	nodeRef, err := nodeReference(client, ctx, opts.nodeID)
47
+	if err != nil {
48
+		return nil
49
+	}
50
+	node, err := client.NodeInspect(ctx, nodeRef)
51
+	if err != nil {
52
+		return err
53
+	}
54
+
55
+	filter := opts.filter.Value()
56
+	filter.Add("node", node.ID)
57
+	if !opts.all {
58
+		filter.Add("desired_state", string(swarm.TaskStateRunning))
59
+		filter.Add("desired_state", string(swarm.TaskStateAccepted))
60
+
61
+	}
62
+
63
+	tasks, err := client.TaskList(
64
+		ctx,
65
+		types.TaskListOptions{Filter: filter})
66
+	if err != nil {
67
+		return err
68
+	}
69
+
70
+	return task.Print(dockerCli, ctx, tasks, idresolver.New(client, opts.noResolve))
71
+}
0 72
new file mode 100644
... ...
@@ -0,0 +1,100 @@
0
+package node
1
+
2
+import (
3
+	"fmt"
4
+
5
+	"github.com/docker/docker/api/client"
6
+	"github.com/docker/docker/cli"
7
+	runconfigopts "github.com/docker/docker/runconfig/opts"
8
+	"github.com/docker/engine-api/types/swarm"
9
+	"github.com/spf13/cobra"
10
+	"github.com/spf13/pflag"
11
+	"golang.org/x/net/context"
12
+)
13
+
14
+func newUpdateCommand(dockerCli *client.DockerCli) *cobra.Command {
15
+	var opts nodeOptions
16
+	var flags *pflag.FlagSet
17
+
18
+	cmd := &cobra.Command{
19
+		Use:   "update [OPTIONS] NODE",
20
+		Short: "Update a node",
21
+		Args:  cli.ExactArgs(1),
22
+		RunE: func(cmd *cobra.Command, args []string) error {
23
+			return runUpdate(dockerCli, args[0], mergeNodeUpdate(flags))
24
+		},
25
+	}
26
+
27
+	flags = cmd.Flags()
28
+	flags.StringVar(&opts.role, "role", "", "Role of the node (worker/manager)")
29
+	flags.StringVar(&opts.membership, "membership", "", "Membership of the node (accepted/rejected)")
30
+	flags.StringVar(&opts.availability, "availability", "", "Availability of the node (active/pause/drain)")
31
+	return cmd
32
+}
33
+
34
+func runUpdate(dockerCli *client.DockerCli, nodeID string, mergeNode func(node *swarm.Node)) error {
35
+	client := dockerCli.Client()
36
+	ctx := context.Background()
37
+
38
+	node, err := client.NodeInspect(ctx, nodeID)
39
+	if err != nil {
40
+		return err
41
+	}
42
+
43
+	mergeNode(&node)
44
+	err = client.NodeUpdate(ctx, nodeID, node.Version, node.Spec)
45
+	if err != nil {
46
+		return err
47
+	}
48
+
49
+	fmt.Fprintf(dockerCli.Out(), "%s\n", nodeID)
50
+	return nil
51
+}
52
+
53
+func mergeNodeUpdate(flags *pflag.FlagSet) func(*swarm.Node) {
54
+	return func(node *swarm.Node) {
55
+		mergeString := func(flag string, field *string) {
56
+			if flags.Changed(flag) {
57
+				*field, _ = flags.GetString(flag)
58
+			}
59
+		}
60
+
61
+		mergeRole := func(flag string, field *swarm.NodeRole) {
62
+			if flags.Changed(flag) {
63
+				str, _ := flags.GetString(flag)
64
+				*field = swarm.NodeRole(str)
65
+			}
66
+		}
67
+
68
+		mergeMembership := func(flag string, field *swarm.NodeMembership) {
69
+			if flags.Changed(flag) {
70
+				str, _ := flags.GetString(flag)
71
+				*field = swarm.NodeMembership(str)
72
+			}
73
+		}
74
+
75
+		mergeAvailability := func(flag string, field *swarm.NodeAvailability) {
76
+			if flags.Changed(flag) {
77
+				str, _ := flags.GetString(flag)
78
+				*field = swarm.NodeAvailability(str)
79
+			}
80
+		}
81
+
82
+		mergeLabels := func(flag string, field *map[string]string) {
83
+			if flags.Changed(flag) {
84
+				values, _ := flags.GetStringSlice(flag)
85
+				for key, value := range runconfigopts.ConvertKVStringsToMap(values) {
86
+					(*field)[key] = value
87
+				}
88
+			}
89
+		}
90
+
91
+		spec := &node.Spec
92
+		mergeString("name", &spec.Name)
93
+		// TODO: setting labels is not working
94
+		mergeLabels("label", &spec.Labels)
95
+		mergeRole("role", &spec.Role)
96
+		mergeMembership("membership", &spec.Membership)
97
+		mergeAvailability("availability", &spec.Availability)
98
+	}
99
+}
0 100
new file mode 100644
... ...
@@ -0,0 +1,32 @@
0
+package service
1
+
2
+import (
3
+	"fmt"
4
+
5
+	"github.com/spf13/cobra"
6
+
7
+	"github.com/docker/docker/api/client"
8
+	"github.com/docker/docker/cli"
9
+)
10
+
11
+// NewServiceCommand returns a cobra command for `service` subcommands
12
+func NewServiceCommand(dockerCli *client.DockerCli) *cobra.Command {
13
+	cmd := &cobra.Command{
14
+		Use:   "service",
15
+		Short: "Manage docker services",
16
+		Args:  cli.NoArgs,
17
+		Run: func(cmd *cobra.Command, args []string) {
18
+			fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
19
+		},
20
+	}
21
+	cmd.AddCommand(
22
+		newCreateCommand(dockerCli),
23
+		newInspectCommand(dockerCli),
24
+		newTasksCommand(dockerCli),
25
+		newListCommand(dockerCli),
26
+		newRemoveCommand(dockerCli),
27
+		newScaleCommand(dockerCli),
28
+		newUpdateCommand(dockerCli),
29
+	)
30
+	return cmd
31
+}
0 32
new file mode 100644
... ...
@@ -0,0 +1,47 @@
0
+package service
1
+
2
+import (
3
+	"fmt"
4
+
5
+	"github.com/docker/docker/api/client"
6
+	"github.com/docker/docker/cli"
7
+	"github.com/spf13/cobra"
8
+	"golang.org/x/net/context"
9
+)
10
+
11
+func newCreateCommand(dockerCli *client.DockerCli) *cobra.Command {
12
+	opts := newServiceOptions()
13
+
14
+	cmd := &cobra.Command{
15
+		Use:   "create [OPTIONS] IMAGE [COMMAND] [ARG...]",
16
+		Short: "Create a new service",
17
+		Args:  cli.RequiresMinArgs(1),
18
+		RunE: func(cmd *cobra.Command, args []string) error {
19
+			opts.image = args[0]
20
+			if len(args) > 1 {
21
+				opts.args = args[1:]
22
+			}
23
+			return runCreate(dockerCli, opts)
24
+		},
25
+	}
26
+	addServiceFlags(cmd, opts)
27
+	cmd.Flags().SetInterspersed(false)
28
+	return cmd
29
+}
30
+
31
+func runCreate(dockerCli *client.DockerCli, opts *serviceOptions) error {
32
+	client := dockerCli.Client()
33
+
34
+	service, err := opts.ToService()
35
+	if err != nil {
36
+		return err
37
+	}
38
+
39
+	response, err := client.ServiceCreate(context.Background(), service)
40
+	if err != nil {
41
+		return err
42
+	}
43
+
44
+	fmt.Fprintf(dockerCli.Out(), "%s\n", response.ID)
45
+	return nil
46
+}
0 47
new file mode 100644
... ...
@@ -0,0 +1,127 @@
0
+package service
1
+
2
+import (
3
+	"fmt"
4
+	"io"
5
+	"strings"
6
+
7
+	"golang.org/x/net/context"
8
+
9
+	"github.com/docker/docker/api/client"
10
+	"github.com/docker/docker/api/client/inspect"
11
+	"github.com/docker/docker/cli"
12
+	"github.com/docker/docker/pkg/ioutils"
13
+	apiclient "github.com/docker/engine-api/client"
14
+	"github.com/docker/engine-api/types/swarm"
15
+	"github.com/spf13/cobra"
16
+)
17
+
18
+type inspectOptions struct {
19
+	refs   []string
20
+	format string
21
+	pretty bool
22
+}
23
+
24
+func newInspectCommand(dockerCli *client.DockerCli) *cobra.Command {
25
+	var opts inspectOptions
26
+
27
+	cmd := &cobra.Command{
28
+		Use:   "inspect [OPTIONS] SERVICE [SERVICE...]",
29
+		Short: "Inspect a service",
30
+		Args:  cli.RequiresMinArgs(1),
31
+		RunE: func(cmd *cobra.Command, args []string) error {
32
+			opts.refs = args
33
+
34
+			if opts.pretty && len(opts.format) > 0 {
35
+				return fmt.Errorf("--format is incompatible with human friendly format")
36
+			}
37
+			return runInspect(dockerCli, opts)
38
+		},
39
+	}
40
+
41
+	flags := cmd.Flags()
42
+	flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template")
43
+	flags.BoolVarP(&opts.pretty, "pretty", "p", false, "Print the information in a human friendly format.")
44
+	return cmd
45
+}
46
+
47
+func runInspect(dockerCli *client.DockerCli, opts inspectOptions) error {
48
+	client := dockerCli.Client()
49
+	ctx := context.Background()
50
+
51
+	getRef := func(ref string) (interface{}, []byte, error) {
52
+		service, err := client.ServiceInspect(ctx, ref)
53
+		if err == nil || !apiclient.IsErrServiceNotFound(err) {
54
+			return service, nil, err
55
+		}
56
+		return nil, nil, fmt.Errorf("Error: no such service: %s", ref)
57
+	}
58
+
59
+	if !opts.pretty {
60
+		return inspect.Inspect(dockerCli.Out(), opts.refs, opts.format, getRef)
61
+	}
62
+
63
+	return printHumanFriendly(dockerCli.Out(), opts.refs, getRef)
64
+}
65
+
66
+func printHumanFriendly(out io.Writer, refs []string, getRef inspect.GetRefFunc) error {
67
+	for idx, ref := range refs {
68
+		obj, _, err := getRef(ref)
69
+		if err != nil {
70
+			return err
71
+		}
72
+		printService(out, obj.(swarm.Service))
73
+
74
+		// TODO: better way to do this?
75
+		// print extra space between objects, but not after the last one
76
+		if idx+1 != len(refs) {
77
+			fmt.Fprintf(out, "\n\n")
78
+		}
79
+	}
80
+	return nil
81
+}
82
+
83
+// TODO: use a template
84
+func printService(out io.Writer, service swarm.Service) {
85
+	fmt.Fprintf(out, "ID:\t\t%s\n", service.ID)
86
+	fmt.Fprintf(out, "Name:\t\t%s\n", service.Spec.Name)
87
+	if service.Spec.Labels != nil {
88
+		fmt.Fprintln(out, "Labels:")
89
+		for k, v := range service.Spec.Labels {
90
+			fmt.Fprintf(out, " - %s=%s\n", k, v)
91
+		}
92
+	}
93
+
94
+	if service.Spec.Mode.Global != nil {
95
+		fmt.Fprintln(out, "Mode:\t\tGLOBAL")
96
+	} else {
97
+		fmt.Fprintln(out, "Mode:\t\tREPLICATED")
98
+		if service.Spec.Mode.Replicated.Replicas != nil {
99
+			fmt.Fprintf(out, " Replicas:\t\t%d\n", *service.Spec.Mode.Replicated.Replicas)
100
+		}
101
+	}
102
+	fmt.Fprintln(out, "Placement:")
103
+	fmt.Fprintln(out, " Strategy:\tSPREAD")
104
+	fmt.Fprintf(out, "UpateConfig:\n")
105
+	fmt.Fprintf(out, " Parallelism:\t%d\n", service.Spec.UpdateConfig.Parallelism)
106
+	if service.Spec.UpdateConfig.Delay.Nanoseconds() > 0 {
107
+		fmt.Fprintf(out, " Delay:\t\t%s\n", service.Spec.UpdateConfig.Delay)
108
+	}
109
+	fmt.Fprintf(out, "ContainerSpec:\n")
110
+	printContainerSpec(out, service.Spec.TaskTemplate.ContainerSpec)
111
+}
112
+
113
+func printContainerSpec(out io.Writer, containerSpec swarm.ContainerSpec) {
114
+	fmt.Fprintf(out, " Image:\t\t%s\n", containerSpec.Image)
115
+	if len(containerSpec.Command) > 0 {
116
+		fmt.Fprintf(out, " Command:\t%s\n", strings.Join(containerSpec.Command, " "))
117
+	}
118
+	if len(containerSpec.Args) > 0 {
119
+		fmt.Fprintf(out, " Args:\t%s\n", strings.Join(containerSpec.Args, " "))
120
+	}
121
+	if len(containerSpec.Env) > 0 {
122
+		fmt.Fprintf(out, " Env:\t\t%s\n", strings.Join(containerSpec.Env, " "))
123
+	}
124
+	ioutils.FprintfIfNotEmpty(out, " Dir\t\t%s\n", containerSpec.Dir)
125
+	ioutils.FprintfIfNotEmpty(out, " User\t\t%s\n", containerSpec.User)
126
+}
0 127
new file mode 100644
... ...
@@ -0,0 +1,97 @@
0
+package service
1
+
2
+import (
3
+	"fmt"
4
+	"io"
5
+	"strings"
6
+	"text/tabwriter"
7
+
8
+	"golang.org/x/net/context"
9
+
10
+	"github.com/docker/docker/api/client"
11
+	"github.com/docker/docker/cli"
12
+	"github.com/docker/docker/opts"
13
+	"github.com/docker/docker/pkg/stringid"
14
+	"github.com/docker/engine-api/types"
15
+	"github.com/docker/engine-api/types/swarm"
16
+	"github.com/spf13/cobra"
17
+)
18
+
19
+const (
20
+	listItemFmt = "%s\t%s\t%s\t%s\t%s\n"
21
+)
22
+
23
+type listOptions struct {
24
+	quiet  bool
25
+	filter opts.FilterOpt
26
+}
27
+
28
+func newListCommand(dockerCli *client.DockerCli) *cobra.Command {
29
+	opts := listOptions{filter: opts.NewFilterOpt()}
30
+
31
+	cmd := &cobra.Command{
32
+		Use:     "ls",
33
+		Aliases: []string{"list"},
34
+		Short:   "List services",
35
+		Args:    cli.NoArgs,
36
+		RunE: func(cmd *cobra.Command, args []string) error {
37
+			return runList(dockerCli, opts)
38
+		},
39
+	}
40
+
41
+	flags := cmd.Flags()
42
+	flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs")
43
+	flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided")
44
+
45
+	return cmd
46
+}
47
+
48
+func runList(dockerCli *client.DockerCli, opts listOptions) error {
49
+	client := dockerCli.Client()
50
+
51
+	services, err := client.ServiceList(
52
+		context.Background(),
53
+		types.ServiceListOptions{Filter: opts.filter.Value()})
54
+	if err != nil {
55
+		return err
56
+	}
57
+
58
+	out := dockerCli.Out()
59
+	if opts.quiet {
60
+		printQuiet(out, services)
61
+	} else {
62
+		printTable(out, services)
63
+	}
64
+	return nil
65
+}
66
+
67
+func printTable(out io.Writer, services []swarm.Service) {
68
+	writer := tabwriter.NewWriter(out, 0, 4, 2, ' ', 0)
69
+
70
+	// Ignore flushing errors
71
+	defer writer.Flush()
72
+
73
+	fmt.Fprintf(writer, listItemFmt, "ID", "NAME", "SCALE", "IMAGE", "COMMAND")
74
+	for _, service := range services {
75
+		scale := ""
76
+		if service.Spec.Mode.Replicated != nil && service.Spec.Mode.Replicated.Replicas != nil {
77
+			scale = fmt.Sprintf("%d", *service.Spec.Mode.Replicated.Replicas)
78
+		} else if service.Spec.Mode.Global != nil {
79
+			scale = "global"
80
+		}
81
+		fmt.Fprintf(
82
+			writer,
83
+			listItemFmt,
84
+			stringid.TruncateID(service.ID),
85
+			service.Spec.Name,
86
+			scale,
87
+			service.Spec.TaskTemplate.ContainerSpec.Image,
88
+			strings.Join(service.Spec.TaskTemplate.ContainerSpec.Args, " "))
89
+	}
90
+}
91
+
92
+func printQuiet(out io.Writer, services []swarm.Service) {
93
+	for _, service := range services {
94
+		fmt.Fprintln(out, service.ID)
95
+	}
96
+}
0 97
new file mode 100644
... ...
@@ -0,0 +1,462 @@
0
+package service
1
+
2
+import (
3
+	"encoding/csv"
4
+	"fmt"
5
+	"math/big"
6
+	"strconv"
7
+	"strings"
8
+	"time"
9
+
10
+	"github.com/docker/docker/opts"
11
+	runconfigopts "github.com/docker/docker/runconfig/opts"
12
+	"github.com/docker/engine-api/types/swarm"
13
+	"github.com/docker/go-connections/nat"
14
+	units "github.com/docker/go-units"
15
+	"github.com/spf13/cobra"
16
+)
17
+
18
+var (
19
+	// DefaultReplicas is the default replicas to use for a replicated service
20
+	DefaultReplicas uint64 = 1
21
+)
22
+
23
+type int64Value interface {
24
+	Value() int64
25
+}
26
+
27
+type memBytes int64
28
+
29
+func (m *memBytes) String() string {
30
+	return strconv.FormatInt(m.Value(), 10)
31
+}
32
+
33
+func (m *memBytes) Set(value string) error {
34
+	val, err := units.RAMInBytes(value)
35
+	*m = memBytes(val)
36
+	return err
37
+}
38
+
39
+func (m *memBytes) Type() string {
40
+	return "MemoryBytes"
41
+}
42
+
43
+func (m *memBytes) Value() int64 {
44
+	return int64(*m)
45
+}
46
+
47
+type nanoCPUs int64
48
+
49
+func (c *nanoCPUs) String() string {
50
+	return strconv.FormatInt(c.Value(), 10)
51
+}
52
+
53
+func (c *nanoCPUs) Set(value string) error {
54
+	cpu, ok := new(big.Rat).SetString(value)
55
+	if !ok {
56
+		return fmt.Errorf("Failed to parse %v as a rational number", value)
57
+	}
58
+	nano := cpu.Mul(cpu, big.NewRat(1e9, 1))
59
+	if !nano.IsInt() {
60
+		return fmt.Errorf("value is too precise")
61
+	}
62
+	*c = nanoCPUs(nano.Num().Int64())
63
+	return nil
64
+}
65
+
66
+func (c *nanoCPUs) Type() string {
67
+	return "NanoCPUs"
68
+}
69
+
70
+func (c *nanoCPUs) Value() int64 {
71
+	return int64(*c)
72
+}
73
+
74
+// DurationOpt is an option type for time.Duration that uses a pointer. This
75
+// allows us to get nil values outside, instead of defaulting to 0
76
+type DurationOpt struct {
77
+	value *time.Duration
78
+}
79
+
80
+// Set a new value on the option
81
+func (d *DurationOpt) Set(s string) error {
82
+	v, err := time.ParseDuration(s)
83
+	d.value = &v
84
+	return err
85
+}
86
+
87
+// Type returns the type of this option
88
+func (d *DurationOpt) Type() string {
89
+	return "duration-ptr"
90
+}
91
+
92
+// String returns a string repr of this option
93
+func (d *DurationOpt) String() string {
94
+	if d.value != nil {
95
+		return d.value.String()
96
+	}
97
+	return "none"
98
+}
99
+
100
+// Value returns the time.Duration
101
+func (d *DurationOpt) Value() *time.Duration {
102
+	return d.value
103
+}
104
+
105
+// Uint64Opt represents a uint64.
106
+type Uint64Opt struct {
107
+	value *uint64
108
+}
109
+
110
+// Set a new value on the option
111
+func (i *Uint64Opt) Set(s string) error {
112
+	v, err := strconv.ParseUint(s, 0, 64)
113
+	i.value = &v
114
+	return err
115
+}
116
+
117
+// Type returns the type of this option
118
+func (i *Uint64Opt) Type() string {
119
+	return "uint64-ptr"
120
+}
121
+
122
+// String returns a string repr of this option
123
+func (i *Uint64Opt) String() string {
124
+	if i.value != nil {
125
+		return fmt.Sprintf("%v", *i.value)
126
+	}
127
+	return "none"
128
+}
129
+
130
+// Value returns the uint64
131
+func (i *Uint64Opt) Value() *uint64 {
132
+	return i.value
133
+}
134
+
135
+// MountOpt is a Value type for parsing mounts
136
+type MountOpt struct {
137
+	values []swarm.Mount
138
+}
139
+
140
+// Set a new mount value
141
+func (m *MountOpt) Set(value string) error {
142
+	csvReader := csv.NewReader(strings.NewReader(value))
143
+	fields, err := csvReader.Read()
144
+	if err != nil {
145
+		return err
146
+	}
147
+
148
+	mount := swarm.Mount{}
149
+
150
+	volumeOptions := func() *swarm.VolumeOptions {
151
+		if mount.VolumeOptions == nil {
152
+			mount.VolumeOptions = &swarm.VolumeOptions{
153
+				Labels: make(map[string]string),
154
+			}
155
+		}
156
+		return mount.VolumeOptions
157
+	}
158
+
159
+	setValueOnMap := func(target map[string]string, value string) {
160
+		parts := strings.SplitN(value, "=", 2)
161
+		if len(parts) == 1 {
162
+			target[value] = ""
163
+		} else {
164
+			target[parts[0]] = parts[1]
165
+		}
166
+	}
167
+
168
+	for _, field := range fields {
169
+		parts := strings.SplitN(field, "=", 2)
170
+		if len(parts) == 1 && strings.ToLower(parts[0]) == "writable" {
171
+			mount.Writable = true
172
+			continue
173
+		}
174
+
175
+		if len(parts) != 2 {
176
+			return fmt.Errorf("invald field '%s' must be a key=value pair", field)
177
+		}
178
+
179
+		key, value := parts[0], parts[1]
180
+		switch strings.ToLower(key) {
181
+		case "type":
182
+			mount.Type = swarm.MountType(strings.ToUpper(value))
183
+		case "source":
184
+			mount.Source = value
185
+		case "target":
186
+			mount.Target = value
187
+		case "writable":
188
+			mount.Writable, err = strconv.ParseBool(value)
189
+			if err != nil {
190
+				return fmt.Errorf("invald value for writable: %s", err.Error())
191
+			}
192
+		case "bind-propagation":
193
+			mount.BindOptions.Propagation = swarm.MountPropagation(strings.ToUpper(value))
194
+		case "volume-populate":
195
+			volumeOptions().Populate, err = strconv.ParseBool(value)
196
+			if err != nil {
197
+				return fmt.Errorf("invald value for populate: %s", err.Error())
198
+			}
199
+		case "volume-label":
200
+			setValueOnMap(volumeOptions().Labels, value)
201
+		case "volume-driver":
202
+			volumeOptions().DriverConfig.Name = value
203
+		case "volume-driver-opt":
204
+			if volumeOptions().DriverConfig.Options == nil {
205
+				volumeOptions().DriverConfig.Options = make(map[string]string)
206
+			}
207
+			setValueOnMap(volumeOptions().DriverConfig.Options, value)
208
+		default:
209
+			return fmt.Errorf("unexpected key '%s' in '%s'", key, value)
210
+		}
211
+	}
212
+
213
+	if mount.Type == "" {
214
+		return fmt.Errorf("type is required")
215
+	}
216
+
217
+	if mount.Target == "" {
218
+		return fmt.Errorf("target is required")
219
+	}
220
+
221
+	m.values = append(m.values, mount)
222
+	return nil
223
+}
224
+
225
+// Type returns the type of this option
226
+func (m *MountOpt) Type() string {
227
+	return "mount"
228
+}
229
+
230
+// String returns a string repr of this option
231
+func (m *MountOpt) String() string {
232
+	mounts := []string{}
233
+	for _, mount := range m.values {
234
+		mounts = append(mounts, fmt.Sprintf("%v", mount))
235
+	}
236
+	return strings.Join(mounts, ", ")
237
+}
238
+
239
+// Value returns the mounts
240
+func (m *MountOpt) Value() []swarm.Mount {
241
+	return m.values
242
+}
243
+
244
+type updateOptions struct {
245
+	parallelism uint64
246
+	delay       time.Duration
247
+}
248
+
249
+type resourceOptions struct {
250
+	limitCPU      nanoCPUs
251
+	limitMemBytes memBytes
252
+	resCPU        nanoCPUs
253
+	resMemBytes   memBytes
254
+}
255
+
256
+func (r *resourceOptions) ToResourceRequirements() *swarm.ResourceRequirements {
257
+	return &swarm.ResourceRequirements{
258
+		Limits: &swarm.Resources{
259
+			NanoCPUs:    r.limitCPU.Value(),
260
+			MemoryBytes: r.limitMemBytes.Value(),
261
+		},
262
+		Reservations: &swarm.Resources{
263
+			NanoCPUs:    r.resCPU.Value(),
264
+			MemoryBytes: r.resMemBytes.Value(),
265
+		},
266
+	}
267
+}
268
+
269
+type restartPolicyOptions struct {
270
+	condition   string
271
+	delay       DurationOpt
272
+	maxAttempts Uint64Opt
273
+	window      DurationOpt
274
+}
275
+
276
+func (r *restartPolicyOptions) ToRestartPolicy() *swarm.RestartPolicy {
277
+	return &swarm.RestartPolicy{
278
+		Condition:   swarm.RestartPolicyCondition(r.condition),
279
+		Delay:       r.delay.Value(),
280
+		MaxAttempts: r.maxAttempts.Value(),
281
+		Window:      r.window.Value(),
282
+	}
283
+}
284
+
285
+func convertNetworks(networks []string) []swarm.NetworkAttachmentConfig {
286
+	nets := []swarm.NetworkAttachmentConfig{}
287
+	for _, network := range networks {
288
+		nets = append(nets, swarm.NetworkAttachmentConfig{Target: network})
289
+	}
290
+	return nets
291
+}
292
+
293
+type endpointOptions struct {
294
+	mode  string
295
+	ports opts.ListOpts
296
+}
297
+
298
+func (e *endpointOptions) ToEndpointSpec() *swarm.EndpointSpec {
299
+	portConfigs := []swarm.PortConfig{}
300
+	// We can ignore errors because the format was already validated by ValidatePort
301
+	ports, portBindings, _ := nat.ParsePortSpecs(e.ports.GetAll())
302
+
303
+	for port := range ports {
304
+		portConfigs = append(portConfigs, convertPortToPortConfig(port, portBindings)...)
305
+	}
306
+
307
+	return &swarm.EndpointSpec{
308
+		Mode:  swarm.ResolutionMode(e.mode),
309
+		Ports: portConfigs,
310
+	}
311
+}
312
+
313
+func convertPortToPortConfig(
314
+	port nat.Port,
315
+	portBindings map[nat.Port][]nat.PortBinding,
316
+) []swarm.PortConfig {
317
+	ports := []swarm.PortConfig{}
318
+
319
+	for _, binding := range portBindings[port] {
320
+		hostPort, _ := strconv.ParseUint(binding.HostPort, 10, 16)
321
+		ports = append(ports, swarm.PortConfig{
322
+			//TODO Name: ?
323
+			Protocol:      swarm.PortConfigProtocol(strings.ToLower(port.Proto())),
324
+			TargetPort:    uint32(port.Int()),
325
+			PublishedPort: uint32(hostPort),
326
+		})
327
+	}
328
+	return ports
329
+}
330
+
331
+// ValidatePort validates a string is in the expected format for a port definition
332
+func ValidatePort(value string) (string, error) {
333
+	portMappings, err := nat.ParsePortSpec(value)
334
+	for _, portMapping := range portMappings {
335
+		if portMapping.Binding.HostIP != "" {
336
+			return "", fmt.Errorf("HostIP is not supported by a service.")
337
+		}
338
+	}
339
+	return value, err
340
+}
341
+
342
+type serviceOptions struct {
343
+	name    string
344
+	labels  opts.ListOpts
345
+	image   string
346
+	command []string
347
+	args    []string
348
+	env     opts.ListOpts
349
+	workdir string
350
+	user    string
351
+	mounts  MountOpt
352
+
353
+	resources resourceOptions
354
+	stopGrace DurationOpt
355
+
356
+	replicas Uint64Opt
357
+	mode     string
358
+
359
+	restartPolicy restartPolicyOptions
360
+	constraints   []string
361
+	update        updateOptions
362
+	networks      []string
363
+	endpoint      endpointOptions
364
+}
365
+
366
+func newServiceOptions() *serviceOptions {
367
+	return &serviceOptions{
368
+		labels: opts.NewListOpts(runconfigopts.ValidateEnv),
369
+		env:    opts.NewListOpts(runconfigopts.ValidateEnv),
370
+		endpoint: endpointOptions{
371
+			ports: opts.NewListOpts(ValidatePort),
372
+		},
373
+	}
374
+}
375
+
376
+func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) {
377
+	var service swarm.ServiceSpec
378
+
379
+	service = swarm.ServiceSpec{
380
+		Annotations: swarm.Annotations{
381
+			Name:   opts.name,
382
+			Labels: runconfigopts.ConvertKVStringsToMap(opts.labels.GetAll()),
383
+		},
384
+		TaskTemplate: swarm.TaskSpec{
385
+			ContainerSpec: swarm.ContainerSpec{
386
+				Image:           opts.image,
387
+				Command:         opts.command,
388
+				Args:            opts.args,
389
+				Env:             opts.env.GetAll(),
390
+				Dir:             opts.workdir,
391
+				User:            opts.user,
392
+				Mounts:          opts.mounts.Value(),
393
+				StopGracePeriod: opts.stopGrace.Value(),
394
+			},
395
+			Resources:     opts.resources.ToResourceRequirements(),
396
+			RestartPolicy: opts.restartPolicy.ToRestartPolicy(),
397
+			Placement: &swarm.Placement{
398
+				Constraints: opts.constraints,
399
+			},
400
+		},
401
+		Mode: swarm.ServiceMode{},
402
+		UpdateConfig: &swarm.UpdateConfig{
403
+			Parallelism: opts.update.parallelism,
404
+			Delay:       opts.update.delay,
405
+		},
406
+		Networks:     convertNetworks(opts.networks),
407
+		EndpointSpec: opts.endpoint.ToEndpointSpec(),
408
+	}
409
+
410
+	switch opts.mode {
411
+	case "global":
412
+		if opts.replicas.Value() != nil {
413
+			return service, fmt.Errorf("replicas can only be used with replicated mode")
414
+		}
415
+
416
+		service.Mode.Global = &swarm.GlobalService{}
417
+	case "replicated":
418
+		service.Mode.Replicated = &swarm.ReplicatedService{
419
+			Replicas: opts.replicas.Value(),
420
+		}
421
+	default:
422
+		return service, fmt.Errorf("Unknown mode: %s", opts.mode)
423
+	}
424
+	return service, nil
425
+}
426
+
427
+// addServiceFlags adds all flags that are common to both `create` and `update.
428
+// Any flags that are not common are added separately in the individual command
429
+func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) {
430
+	flags := cmd.Flags()
431
+	flags.StringVar(&opts.name, "name", "", "Service name")
432
+	flags.VarP(&opts.labels, "label", "l", "Service labels")
433
+
434
+	flags.VarP(&opts.env, "env", "e", "Set environment variables")
435
+	flags.StringVarP(&opts.workdir, "workdir", "w", "", "Working directory inside the container")
436
+	flags.StringVarP(&opts.user, "user", "u", "", "Username or UID")
437
+	flags.VarP(&opts.mounts, "mount", "m", "Attach a mount to the service")
438
+
439
+	flags.Var(&opts.resources.limitCPU, "limit-cpu", "Limit CPUs")
440
+	flags.Var(&opts.resources.limitMemBytes, "limit-memory", "Limit Memory")
441
+	flags.Var(&opts.resources.resCPU, "reserve-cpu", "Reserve CPUs")
442
+	flags.Var(&opts.resources.resMemBytes, "reserve-memory", "Reserve Memory")
443
+	flags.Var(&opts.stopGrace, "stop-grace-period", "Time to wait before force killing a container")
444
+
445
+	flags.StringVar(&opts.mode, "mode", "replicated", "Service mode (replicated or global)")
446
+	flags.Var(&opts.replicas, "replicas", "Number of tasks")
447
+
448
+	flags.StringVar(&opts.restartPolicy.condition, "restart-condition", "", "Restart when condition is met (none, on_failure, or any)")
449
+	flags.Var(&opts.restartPolicy.delay, "restart-delay", "Delay between restart attempts")
450
+	flags.Var(&opts.restartPolicy.maxAttempts, "restart-max-attempts", "Maximum number of restarts before giving up")
451
+	flags.Var(&opts.restartPolicy.window, "restart-window", "Window used to evalulate the restart policy")
452
+
453
+	flags.StringSliceVar(&opts.constraints, "constraint", []string{}, "Placement constraints")
454
+
455
+	flags.Uint64Var(&opts.update.parallelism, "update-parallelism", 1, "Maximum number of tasks updated simultaneously")
456
+	flags.DurationVar(&opts.update.delay, "update-delay", time.Duration(0), "Delay between updates")
457
+
458
+	flags.StringSliceVar(&opts.networks, "network", []string{}, "Network attachments")
459
+	flags.StringVar(&opts.endpoint.mode, "endpoint-mode", "", "Endpoint mode(Valid values: VIP, DNSRR)")
460
+	flags.VarP(&opts.endpoint.ports, "publish", "p", "Publish a port as a node port")
461
+}
0 462
new file mode 100644
... ...
@@ -0,0 +1,47 @@
0
+package service
1
+
2
+import (
3
+	"fmt"
4
+	"strings"
5
+
6
+	"github.com/docker/docker/api/client"
7
+	"github.com/docker/docker/cli"
8
+	"github.com/spf13/cobra"
9
+	"golang.org/x/net/context"
10
+)
11
+
12
+func newRemoveCommand(dockerCli *client.DockerCli) *cobra.Command {
13
+
14
+	cmd := &cobra.Command{
15
+		Use:     "rm [OPTIONS] SERVICE",
16
+		Aliases: []string{"remove"},
17
+		Short:   "Remove a service",
18
+		Args:    cli.RequiresMinArgs(1),
19
+		RunE: func(cmd *cobra.Command, args []string) error {
20
+			return runRemove(dockerCli, args)
21
+		},
22
+	}
23
+	cmd.Flags()
24
+
25
+	return cmd
26
+}
27
+
28
+func runRemove(dockerCli *client.DockerCli, sids []string) error {
29
+	client := dockerCli.Client()
30
+
31
+	ctx := context.Background()
32
+
33
+	var errs []string
34
+	for _, sid := range sids {
35
+		err := client.ServiceRemove(ctx, sid)
36
+		if err != nil {
37
+			errs = append(errs, err.Error())
38
+			continue
39
+		}
40
+		fmt.Fprintf(dockerCli.Out(), "%s\n", sid)
41
+	}
42
+	if len(errs) > 0 {
43
+		return fmt.Errorf(strings.Join(errs, "\n"))
44
+	}
45
+	return nil
46
+}
0 47
new file mode 100644
... ...
@@ -0,0 +1,86 @@
0
+package service
1
+
2
+import (
3
+	"fmt"
4
+	"strconv"
5
+	"strings"
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
+func newScaleCommand(dockerCli *client.DockerCli) *cobra.Command {
15
+	return &cobra.Command{
16
+		Use:   "scale SERVICE=SCALE [SERVICE=SCALE...]",
17
+		Short: "Scale one or multiple services",
18
+		Args:  scaleArgs,
19
+		RunE: func(cmd *cobra.Command, args []string) error {
20
+			return runScale(dockerCli, args)
21
+		},
22
+	}
23
+}
24
+
25
+func scaleArgs(cmd *cobra.Command, args []string) error {
26
+	if err := cli.RequiresMinArgs(1)(cmd, args); err != nil {
27
+		return err
28
+	}
29
+	for _, arg := range args {
30
+		if parts := strings.SplitN(arg, "=", 2); len(parts) != 2 {
31
+			return fmt.Errorf(
32
+				"Invalid scale specifier '%s'.\nSee '%s --help'.\n\nUsage:  %s\n\n%s",
33
+				arg,
34
+				cmd.CommandPath(),
35
+				cmd.UseLine(),
36
+				cmd.Short,
37
+			)
38
+		}
39
+	}
40
+	return nil
41
+}
42
+
43
+func runScale(dockerCli *client.DockerCli, args []string) error {
44
+	var errors []string
45
+	for _, arg := range args {
46
+		parts := strings.SplitN(arg, "=", 2)
47
+		serviceID, scale := parts[0], parts[1]
48
+		if err := runServiceScale(dockerCli, serviceID, scale); err != nil {
49
+			errors = append(errors, fmt.Sprintf("%s: %s", serviceID, err.Error()))
50
+		}
51
+	}
52
+
53
+	if len(errors) == 0 {
54
+		return nil
55
+	}
56
+	return fmt.Errorf(strings.Join(errors, "\n"))
57
+}
58
+
59
+func runServiceScale(dockerCli *client.DockerCli, serviceID string, scale string) error {
60
+	client := dockerCli.Client()
61
+	ctx := context.Background()
62
+
63
+	service, err := client.ServiceInspect(ctx, serviceID)
64
+	if err != nil {
65
+		return err
66
+	}
67
+
68
+	serviceMode := &service.Spec.Mode
69
+	if serviceMode.Replicated == nil {
70
+		return fmt.Errorf("scale can only be used with replicated mode")
71
+	}
72
+	uintScale, err := strconv.ParseUint(scale, 10, 64)
73
+	if err != nil {
74
+		return fmt.Errorf("invalid replicas value %s: %s", scale, err.Error())
75
+	}
76
+	serviceMode.Replicated.Replicas = &uintScale
77
+
78
+	err = client.ServiceUpdate(ctx, service.ID, service.Version, service.Spec)
79
+	if err != nil {
80
+		return err
81
+	}
82
+
83
+	fmt.Fprintf(dockerCli.Out(), "%s scaled to %s\n", serviceID, scale)
84
+	return nil
85
+}
0 86
new file mode 100644
... ...
@@ -0,0 +1,65 @@
0
+package service
1
+
2
+import (
3
+	"golang.org/x/net/context"
4
+
5
+	"github.com/docker/docker/api/client"
6
+	"github.com/docker/docker/api/client/idresolver"
7
+	"github.com/docker/docker/api/client/task"
8
+	"github.com/docker/docker/cli"
9
+	"github.com/docker/docker/opts"
10
+	"github.com/docker/engine-api/types"
11
+	"github.com/docker/engine-api/types/swarm"
12
+	"github.com/spf13/cobra"
13
+)
14
+
15
+type tasksOptions struct {
16
+	serviceID string
17
+	all       bool
18
+	noResolve bool
19
+	filter    opts.FilterOpt
20
+}
21
+
22
+func newTasksCommand(dockerCli *client.DockerCli) *cobra.Command {
23
+	opts := tasksOptions{filter: opts.NewFilterOpt()}
24
+
25
+	cmd := &cobra.Command{
26
+		Use:   "tasks [OPTIONS] SERVICE",
27
+		Short: "List the tasks of a service",
28
+		Args:  cli.ExactArgs(1),
29
+		RunE: func(cmd *cobra.Command, args []string) error {
30
+			opts.serviceID = args[0]
31
+			return runTasks(dockerCli, opts)
32
+		},
33
+	}
34
+	flags := cmd.Flags()
35
+	flags.BoolVarP(&opts.all, "all", "a", false, "Display all tasks")
36
+	flags.BoolVarP(&opts.noResolve, "no-resolve", "n", false, "Do not map IDs to Names")
37
+	flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided")
38
+
39
+	return cmd
40
+}
41
+
42
+func runTasks(dockerCli *client.DockerCli, opts tasksOptions) error {
43
+	client := dockerCli.Client()
44
+	ctx := context.Background()
45
+
46
+	service, err := client.ServiceInspect(ctx, opts.serviceID)
47
+	if err != nil {
48
+		return err
49
+	}
50
+
51
+	filter := opts.filter.Value()
52
+	filter.Add("service", service.ID)
53
+	if !opts.all && !filter.Include("desired_state") {
54
+		filter.Add("desired_state", string(swarm.TaskStateRunning))
55
+		filter.Add("desired_state", string(swarm.TaskStateAccepted))
56
+	}
57
+
58
+	tasks, err := client.TaskList(ctx, types.TaskListOptions{Filter: filter})
59
+	if err != nil {
60
+		return err
61
+	}
62
+
63
+	return task.Print(dockerCli, ctx, tasks, idresolver.New(client, opts.noResolve))
64
+}
0 65
new file mode 100644
... ...
@@ -0,0 +1,244 @@
0
+package service
1
+
2
+import (
3
+	"fmt"
4
+	"time"
5
+
6
+	"golang.org/x/net/context"
7
+
8
+	"github.com/docker/docker/api/client"
9
+	"github.com/docker/docker/cli"
10
+	"github.com/docker/docker/opts"
11
+	runconfigopts "github.com/docker/docker/runconfig/opts"
12
+	"github.com/docker/engine-api/types/swarm"
13
+	"github.com/docker/go-connections/nat"
14
+	"github.com/spf13/cobra"
15
+	"github.com/spf13/pflag"
16
+)
17
+
18
+func newUpdateCommand(dockerCli *client.DockerCli) *cobra.Command {
19
+	opts := newServiceOptions()
20
+	var flags *pflag.FlagSet
21
+
22
+	cmd := &cobra.Command{
23
+		Use:   "update [OPTIONS] SERVICE",
24
+		Short: "Update a service",
25
+		Args:  cli.ExactArgs(1),
26
+		RunE: func(cmd *cobra.Command, args []string) error {
27
+			return runUpdate(dockerCli, flags, args[0])
28
+		},
29
+	}
30
+
31
+	flags = cmd.Flags()
32
+	flags.String("image", "", "Service image tag")
33
+	flags.StringSlice("command", []string{}, "Service command")
34
+	flags.StringSlice("arg", []string{}, "Service command args")
35
+	addServiceFlags(cmd, opts)
36
+	return cmd
37
+}
38
+
39
+func runUpdate(dockerCli *client.DockerCli, flags *pflag.FlagSet, serviceID string) error {
40
+	client := dockerCli.Client()
41
+	ctx := context.Background()
42
+
43
+	service, err := client.ServiceInspect(ctx, serviceID)
44
+	if err != nil {
45
+		return err
46
+	}
47
+
48
+	err = mergeService(&service.Spec, flags)
49
+	if err != nil {
50
+		return err
51
+	}
52
+	err = client.ServiceUpdate(ctx, service.ID, service.Version, service.Spec)
53
+	if err != nil {
54
+		return err
55
+	}
56
+
57
+	fmt.Fprintf(dockerCli.Out(), "%s\n", serviceID)
58
+	return nil
59
+}
60
+
61
+func mergeService(spec *swarm.ServiceSpec, flags *pflag.FlagSet) error {
62
+
63
+	mergeString := func(flag string, field *string) {
64
+		if flags.Changed(flag) {
65
+			*field, _ = flags.GetString(flag)
66
+		}
67
+	}
68
+
69
+	mergeListOpts := func(flag string, field *[]string) {
70
+		if flags.Changed(flag) {
71
+			value := flags.Lookup(flag).Value.(*opts.ListOpts)
72
+			*field = value.GetAll()
73
+		}
74
+	}
75
+
76
+	mergeSlice := func(flag string, field *[]string) {
77
+		if flags.Changed(flag) {
78
+			*field, _ = flags.GetStringSlice(flag)
79
+		}
80
+	}
81
+
82
+	mergeInt64Value := func(flag string, field *int64) {
83
+		if flags.Changed(flag) {
84
+			*field = flags.Lookup(flag).Value.(int64Value).Value()
85
+		}
86
+	}
87
+
88
+	mergeDuration := func(flag string, field *time.Duration) {
89
+		if flags.Changed(flag) {
90
+			*field, _ = flags.GetDuration(flag)
91
+		}
92
+	}
93
+
94
+	mergeDurationOpt := func(flag string, field *time.Duration) {
95
+		if flags.Changed(flag) {
96
+			*field = *flags.Lookup(flag).Value.(*DurationOpt).Value()
97
+		}
98
+	}
99
+
100
+	mergeUint64 := func(flag string, field *uint64) {
101
+		if flags.Changed(flag) {
102
+			*field, _ = flags.GetUint64(flag)
103
+		}
104
+	}
105
+
106
+	mergeUint64Opt := func(flag string, field *uint64) {
107
+		if flags.Changed(flag) {
108
+			*field = *flags.Lookup(flag).Value.(*Uint64Opt).Value()
109
+		}
110
+	}
111
+
112
+	cspec := &spec.TaskTemplate.ContainerSpec
113
+	task := &spec.TaskTemplate
114
+	mergeString("name", &spec.Name)
115
+	mergeLabels(flags, &spec.Labels)
116
+	mergeString("image", &cspec.Image)
117
+	mergeSlice("command", &cspec.Command)
118
+	mergeSlice("arg", &cspec.Command)
119
+	mergeListOpts("env", &cspec.Env)
120
+	mergeString("workdir", &cspec.Dir)
121
+	mergeString("user", &cspec.User)
122
+	mergeMounts(flags, &cspec.Mounts)
123
+
124
+	mergeInt64Value("limit-cpu", &task.Resources.Limits.NanoCPUs)
125
+	mergeInt64Value("limit-memory", &task.Resources.Limits.MemoryBytes)
126
+	mergeInt64Value("reserve-cpu", &task.Resources.Reservations.NanoCPUs)
127
+	mergeInt64Value("reserve-memory", &task.Resources.Reservations.MemoryBytes)
128
+
129
+	mergeDurationOpt("stop-grace-period", cspec.StopGracePeriod)
130
+
131
+	if flags.Changed("restart-policy-condition") {
132
+		value, _ := flags.GetString("restart-policy-condition")
133
+		task.RestartPolicy.Condition = swarm.RestartPolicyCondition(value)
134
+	}
135
+	mergeDurationOpt("restart-policy-delay", task.RestartPolicy.Delay)
136
+	mergeUint64Opt("restart-policy-max-attempts", task.RestartPolicy.MaxAttempts)
137
+	mergeDurationOpt("restart-policy-window", task.RestartPolicy.Window)
138
+	mergeSlice("constraint", &task.Placement.Constraints)
139
+
140
+	if err := mergeMode(flags, &spec.Mode); err != nil {
141
+		return err
142
+	}
143
+
144
+	mergeUint64("updateconfig-parallelism", &spec.UpdateConfig.Parallelism)
145
+	mergeDuration("updateconfig-delay", &spec.UpdateConfig.Delay)
146
+
147
+	mergeNetworks(flags, &spec.Networks)
148
+	if flags.Changed("endpoint-mode") {
149
+		value, _ := flags.GetString("endpoint-mode")
150
+		spec.EndpointSpec.Mode = swarm.ResolutionMode(value)
151
+	}
152
+
153
+	mergePorts(flags, &spec.EndpointSpec.Ports)
154
+
155
+	return nil
156
+}
157
+
158
+func mergeLabels(flags *pflag.FlagSet, field *map[string]string) {
159
+	if !flags.Changed("label") {
160
+		return
161
+	}
162
+
163
+	if *field == nil {
164
+		*field = make(map[string]string)
165
+	}
166
+
167
+	values := flags.Lookup("label").Value.(*opts.ListOpts).GetAll()
168
+	for key, value := range runconfigopts.ConvertKVStringsToMap(values) {
169
+		(*field)[key] = value
170
+	}
171
+}
172
+
173
+// TODO: should this override by destination path, or does swarm handle that?
174
+func mergeMounts(flags *pflag.FlagSet, mounts *[]swarm.Mount) {
175
+	if !flags.Changed("mount") {
176
+		return
177
+	}
178
+
179
+	values := flags.Lookup("mount").Value.(*MountOpt).Value()
180
+	*mounts = append(*mounts, values...)
181
+}
182
+
183
+// TODO: should this override by name, or does swarm handle that?
184
+func mergePorts(flags *pflag.FlagSet, portConfig *[]swarm.PortConfig) {
185
+	if !flags.Changed("ports") {
186
+		return
187
+	}
188
+
189
+	values := flags.Lookup("ports").Value.(*opts.ListOpts).GetAll()
190
+	ports, portBindings, _ := nat.ParsePortSpecs(values)
191
+
192
+	for port := range ports {
193
+		*portConfig = append(*portConfig, convertPortToPortConfig(port, portBindings)...)
194
+	}
195
+}
196
+
197
+func mergeNetworks(flags *pflag.FlagSet, attachments *[]swarm.NetworkAttachmentConfig) {
198
+	if !flags.Changed("network") {
199
+		return
200
+	}
201
+	networks, _ := flags.GetStringSlice("network")
202
+	for _, network := range networks {
203
+		*attachments = append(*attachments, swarm.NetworkAttachmentConfig{Target: network})
204
+	}
205
+}
206
+
207
+func mergeMode(flags *pflag.FlagSet, serviceMode *swarm.ServiceMode) error {
208
+	if !flags.Changed("mode") && !flags.Changed("scale") {
209
+		return nil
210
+	}
211
+
212
+	var mode string
213
+	if flags.Changed("mode") {
214
+		mode, _ = flags.GetString("mode")
215
+	}
216
+
217
+	if !(mode == "replicated" || serviceMode.Replicated != nil) && flags.Changed("replicas") {
218
+		return fmt.Errorf("replicas can only be used with replicated mode")
219
+	}
220
+
221
+	if mode == "global" {
222
+		serviceMode.Replicated = nil
223
+		serviceMode.Global = &swarm.GlobalService{}
224
+		return nil
225
+	}
226
+
227
+	if flags.Changed("replicas") {
228
+		replicas := flags.Lookup("replicas").Value.(*Uint64Opt).Value()
229
+		serviceMode.Replicated = &swarm.ReplicatedService{Replicas: replicas}
230
+		serviceMode.Global = nil
231
+		return nil
232
+	}
233
+
234
+	if mode == "replicated" {
235
+		if serviceMode.Replicated != nil {
236
+			return nil
237
+		}
238
+		serviceMode.Replicated = &swarm.ReplicatedService{Replicas: &DefaultReplicas}
239
+		serviceMode.Global = nil
240
+	}
241
+
242
+	return nil
243
+}
0 244
new file mode 100644
... ...
@@ -0,0 +1,30 @@
0
+package swarm
1
+
2
+import (
3
+	"fmt"
4
+
5
+	"github.com/spf13/cobra"
6
+
7
+	"github.com/docker/docker/api/client"
8
+	"github.com/docker/docker/cli"
9
+)
10
+
11
+// NewSwarmCommand returns a cobra command for `swarm` subcommands
12
+func NewSwarmCommand(dockerCli *client.DockerCli) *cobra.Command {
13
+	cmd := &cobra.Command{
14
+		Use:   "swarm",
15
+		Short: "Manage docker swarm",
16
+		Args:  cli.NoArgs,
17
+		Run: func(cmd *cobra.Command, args []string) {
18
+			fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
19
+		},
20
+	}
21
+	cmd.AddCommand(
22
+		newInitCommand(dockerCli),
23
+		newJoinCommand(dockerCli),
24
+		newUpdateCommand(dockerCli),
25
+		newLeaveCommand(dockerCli),
26
+		newInspectCommand(dockerCli),
27
+	)
28
+	return cmd
29
+}
0 30
new file mode 100644
... ...
@@ -0,0 +1,61 @@
0
+package swarm
1
+
2
+import (
3
+	"fmt"
4
+
5
+	"golang.org/x/net/context"
6
+
7
+	"github.com/docker/docker/api/client"
8
+	"github.com/docker/docker/cli"
9
+	"github.com/docker/engine-api/types/swarm"
10
+	"github.com/spf13/cobra"
11
+)
12
+
13
+type initOptions struct {
14
+	listenAddr      NodeAddrOption
15
+	autoAccept      AutoAcceptOption
16
+	forceNewCluster bool
17
+	secret          string
18
+}
19
+
20
+func newInitCommand(dockerCli *client.DockerCli) *cobra.Command {
21
+	opts := initOptions{
22
+		listenAddr: NewNodeAddrOption(),
23
+		autoAccept: NewAutoAcceptOption(),
24
+	}
25
+
26
+	cmd := &cobra.Command{
27
+		Use:   "init",
28
+		Short: "Initialize a Swarm.",
29
+		Args:  cli.NoArgs,
30
+		RunE: func(cmd *cobra.Command, args []string) error {
31
+			return runInit(dockerCli, opts)
32
+		},
33
+	}
34
+
35
+	flags := cmd.Flags()
36
+	flags.Var(&opts.listenAddr, "listen-addr", "Listen address")
37
+	flags.Var(&opts.autoAccept, "auto-accept", "Auto acceptance policy (worker, manager, or none)")
38
+	flags.StringVar(&opts.secret, "secret", "", "Set secret value needed to accept nodes into cluster")
39
+	flags.BoolVar(&opts.forceNewCluster, "force-new-cluster", false, "Force create a new cluster from current state.")
40
+	return cmd
41
+}
42
+
43
+func runInit(dockerCli *client.DockerCli, opts initOptions) error {
44
+	client := dockerCli.Client()
45
+	ctx := context.Background()
46
+
47
+	req := swarm.InitRequest{
48
+		ListenAddr:      opts.listenAddr.String(),
49
+		ForceNewCluster: opts.forceNewCluster,
50
+	}
51
+
52
+	req.Spec.AcceptancePolicy.Policies = opts.autoAccept.Policies(opts.secret)
53
+
54
+	nodeID, err := client.SwarmInit(ctx, req)
55
+	if err != nil {
56
+		return err
57
+	}
58
+	fmt.Printf("Swarm initialized: current node (%s) is now a manager.\n", nodeID)
59
+	return nil
60
+}
0 61
new file mode 100644
... ...
@@ -0,0 +1,56 @@
0
+package swarm
1
+
2
+import (
3
+	"golang.org/x/net/context"
4
+
5
+	"github.com/docker/docker/api/client"
6
+	"github.com/docker/docker/api/client/inspect"
7
+	"github.com/docker/docker/cli"
8
+	"github.com/spf13/cobra"
9
+)
10
+
11
+type inspectOptions struct {
12
+	format string
13
+	//	pretty  bool
14
+}
15
+
16
+func newInspectCommand(dockerCli *client.DockerCli) *cobra.Command {
17
+	var opts inspectOptions
18
+
19
+	cmd := &cobra.Command{
20
+		Use:   "inspect [OPTIONS]",
21
+		Short: "Inspect the Swarm",
22
+		Args:  cli.NoArgs,
23
+		RunE: func(cmd *cobra.Command, args []string) error {
24
+			// if opts.pretty && len(opts.format) > 0 {
25
+			//	return fmt.Errorf("--format is incompatible with human friendly format")
26
+			// }
27
+			return runInspect(dockerCli, opts)
28
+		},
29
+	}
30
+
31
+	flags := cmd.Flags()
32
+	flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template")
33
+	//flags.BoolVarP(&opts.pretty, "pretty", "h", false, "Print the information in a human friendly format.")
34
+	return cmd
35
+}
36
+
37
+func runInspect(dockerCli *client.DockerCli, opts inspectOptions) error {
38
+	client := dockerCli.Client()
39
+	ctx := context.Background()
40
+
41
+	swarm, err := client.SwarmInspect(ctx)
42
+	if err != nil {
43
+		return err
44
+	}
45
+
46
+	getRef := func(_ string) (interface{}, []byte, error) {
47
+		return swarm, nil, nil
48
+	}
49
+
50
+	//	if !opts.pretty {
51
+	return inspect.Inspect(dockerCli.Out(), []string{""}, opts.format, getRef)
52
+	//	}
53
+
54
+	//return printHumanFriendly(dockerCli.Out(), opts.refs, getRef)
55
+}
0 56
new file mode 100644
... ...
@@ -0,0 +1,65 @@
0
+package swarm
1
+
2
+import (
3
+	"fmt"
4
+
5
+	"github.com/docker/docker/api/client"
6
+	"github.com/docker/docker/cli"
7
+	"github.com/docker/engine-api/types/swarm"
8
+	"github.com/spf13/cobra"
9
+	"golang.org/x/net/context"
10
+)
11
+
12
+type joinOptions struct {
13
+	remote     string
14
+	listenAddr NodeAddrOption
15
+	manager    bool
16
+	secret     string
17
+	CACertHash string
18
+}
19
+
20
+func newJoinCommand(dockerCli *client.DockerCli) *cobra.Command {
21
+	opts := joinOptions{
22
+		listenAddr: NodeAddrOption{addr: defaultListenAddr},
23
+	}
24
+
25
+	cmd := &cobra.Command{
26
+		Use:   "join [OPTIONS] HOST:PORT",
27
+		Short: "Join a Swarm as a node and/or manager.",
28
+		Args:  cli.ExactArgs(1),
29
+		RunE: func(cmd *cobra.Command, args []string) error {
30
+			opts.remote = args[0]
31
+			return runJoin(dockerCli, opts)
32
+		},
33
+	}
34
+
35
+	flags := cmd.Flags()
36
+	flags.Var(&opts.listenAddr, "listen-addr", "Listen address")
37
+	flags.BoolVar(&opts.manager, "manager", false, "Try joining as a manager.")
38
+	flags.StringVar(&opts.secret, "secret", "", "Secret for node acceptance")
39
+	flags.StringVar(&opts.CACertHash, "ca-hash", "", "Hash of the Root Certificate Authority certificate used for trusted join")
40
+	return cmd
41
+}
42
+
43
+func runJoin(dockerCli *client.DockerCli, opts joinOptions) error {
44
+	client := dockerCli.Client()
45
+	ctx := context.Background()
46
+
47
+	req := swarm.JoinRequest{
48
+		Manager:     opts.manager,
49
+		Secret:      opts.secret,
50
+		ListenAddr:  opts.listenAddr.String(),
51
+		RemoteAddrs: []string{opts.remote},
52
+		CACertHash:  opts.CACertHash,
53
+	}
54
+	err := client.SwarmJoin(ctx, req)
55
+	if err != nil {
56
+		return err
57
+	}
58
+	if opts.manager {
59
+		fmt.Fprintln(dockerCli.Out(), "This node joined a Swarm as a manager.")
60
+	} else {
61
+		fmt.Fprintln(dockerCli.Out(), "This node joined a Swarm as a worker.")
62
+	}
63
+	return nil
64
+}
0 65
new file mode 100644
... ...
@@ -0,0 +1,44 @@
0
+package swarm
1
+
2
+import (
3
+	"fmt"
4
+
5
+	"golang.org/x/net/context"
6
+
7
+	"github.com/docker/docker/api/client"
8
+	"github.com/docker/docker/cli"
9
+	"github.com/spf13/cobra"
10
+)
11
+
12
+type leaveOptions struct {
13
+	force bool
14
+}
15
+
16
+func newLeaveCommand(dockerCli *client.DockerCli) *cobra.Command {
17
+	opts := leaveOptions{}
18
+
19
+	cmd := &cobra.Command{
20
+		Use:   "leave",
21
+		Short: "Leave a Swarm.",
22
+		Args:  cli.NoArgs,
23
+		RunE: func(cmd *cobra.Command, args []string) error {
24
+			return runLeave(dockerCli, opts)
25
+		},
26
+	}
27
+
28
+	flags := cmd.Flags()
29
+	flags.BoolVar(&opts.force, "force", false, "Force leave ignoring warnings.")
30
+	return cmd
31
+}
32
+
33
+func runLeave(dockerCli *client.DockerCli, opts leaveOptions) error {
34
+	client := dockerCli.Client()
35
+	ctx := context.Background()
36
+
37
+	if err := client.SwarmLeave(ctx, opts.force); err != nil {
38
+		return err
39
+	}
40
+
41
+	fmt.Fprintln(dockerCli.Out(), "Node left the default swarm.")
42
+	return nil
43
+}
0 44
new file mode 100644
... ...
@@ -0,0 +1,120 @@
0
+package swarm
1
+
2
+import (
3
+	"fmt"
4
+	"strings"
5
+
6
+	"github.com/docker/engine-api/types/swarm"
7
+)
8
+
9
+const (
10
+	defaultListenAddr = "0.0.0.0:2377"
11
+	// WORKER constant for worker name
12
+	WORKER = "WORKER"
13
+	// MANAGER constant for manager name
14
+	MANAGER = "MANAGER"
15
+)
16
+
17
+var (
18
+	defaultPolicies = []swarm.Policy{
19
+		{Role: WORKER, Autoaccept: true},
20
+		{Role: MANAGER, Autoaccept: false},
21
+	}
22
+)
23
+
24
+// NodeAddrOption is a pflag.Value for listen and remote addresses
25
+type NodeAddrOption struct {
26
+	addr string
27
+}
28
+
29
+// String prints the representation of this flag
30
+func (a *NodeAddrOption) String() string {
31
+	return a.addr
32
+}
33
+
34
+// Set the value for this flag
35
+func (a *NodeAddrOption) Set(value string) error {
36
+	if !strings.Contains(value, ":") {
37
+		return fmt.Errorf("Invalud url, a host and port are required")
38
+	}
39
+
40
+	parts := strings.Split(value, ":")
41
+	if len(parts) != 2 {
42
+		return fmt.Errorf("Invalud url, too many colons")
43
+	}
44
+
45
+	a.addr = value
46
+	return nil
47
+}
48
+
49
+// Type returns the type of this flag
50
+func (a *NodeAddrOption) Type() string {
51
+	return "node-addr"
52
+}
53
+
54
+// NewNodeAddrOption returns a new node address option
55
+func NewNodeAddrOption() NodeAddrOption {
56
+	return NodeAddrOption{addr: defaultListenAddr}
57
+}
58
+
59
+// AutoAcceptOption is a value type for auto-accept policy
60
+type AutoAcceptOption struct {
61
+	values map[string]bool
62
+}
63
+
64
+// String prints a string representation of this option
65
+func (o *AutoAcceptOption) String() string {
66
+	keys := []string{}
67
+	for key := range o.values {
68
+		keys = append(keys, key)
69
+	}
70
+	return strings.Join(keys, " ")
71
+}
72
+
73
+// Set sets a new value on this option
74
+func (o *AutoAcceptOption) Set(value string) error {
75
+	value = strings.ToUpper(value)
76
+	switch value {
77
+	case "", "NONE":
78
+		if accept, ok := o.values[WORKER]; ok && accept {
79
+			return fmt.Errorf("value NONE is incompatible with %s", WORKER)
80
+		}
81
+		if accept, ok := o.values[MANAGER]; ok && accept {
82
+			return fmt.Errorf("value NONE is incompatible with %s", MANAGER)
83
+		}
84
+		o.values[WORKER] = false
85
+		o.values[MANAGER] = false
86
+	case WORKER, MANAGER:
87
+		if accept, ok := o.values[value]; ok && !accept {
88
+			return fmt.Errorf("value NONE is incompatible with %s", value)
89
+		}
90
+		o.values[value] = true
91
+	default:
92
+		return fmt.Errorf("must be one of %s, %s, NONE", WORKER, MANAGER)
93
+	}
94
+
95
+	return nil
96
+}
97
+
98
+// Type returns the type of this option
99
+func (o *AutoAcceptOption) Type() string {
100
+	return "auto-accept"
101
+}
102
+
103
+// Policies returns a representation of this option for the api
104
+func (o *AutoAcceptOption) Policies(secret string) []swarm.Policy {
105
+	policies := []swarm.Policy{}
106
+	for _, p := range defaultPolicies {
107
+		if len(o.values) != 0 {
108
+			p.Autoaccept = o.values[string(p.Role)]
109
+		}
110
+		p.Secret = secret
111
+		policies = append(policies, p)
112
+	}
113
+	return policies
114
+}
115
+
116
+// NewAutoAcceptOption returns a new auto-accept option
117
+func NewAutoAcceptOption() AutoAcceptOption {
118
+	return AutoAcceptOption{values: make(map[string]bool)}
119
+}
0 120
new file mode 100644
... ...
@@ -0,0 +1,93 @@
0
+package swarm
1
+
2
+import (
3
+	"fmt"
4
+
5
+	"golang.org/x/net/context"
6
+
7
+	"github.com/docker/docker/api/client"
8
+	"github.com/docker/docker/cli"
9
+	"github.com/docker/engine-api/types/swarm"
10
+	"github.com/spf13/cobra"
11
+	"github.com/spf13/pflag"
12
+)
13
+
14
+type updateOptions struct {
15
+	autoAccept       AutoAcceptOption
16
+	secret           string
17
+	taskHistoryLimit int64
18
+	heartbeatPeriod  uint64
19
+}
20
+
21
+func newUpdateCommand(dockerCli *client.DockerCli) *cobra.Command {
22
+	opts := updateOptions{autoAccept: NewAutoAcceptOption()}
23
+	var flags *pflag.FlagSet
24
+
25
+	cmd := &cobra.Command{
26
+		Use:   "update",
27
+		Short: "update the Swarm.",
28
+		Args:  cli.NoArgs,
29
+		RunE: func(cmd *cobra.Command, args []string) error {
30
+			return runUpdate(dockerCli, flags, opts)
31
+		},
32
+	}
33
+
34
+	flags = cmd.Flags()
35
+	flags.Var(&opts.autoAccept, "auto-accept", "Auto acceptance policy (worker, manager or none)")
36
+	flags.StringVar(&opts.secret, "secret", "", "Set secret value needed to accept nodes into cluster")
37
+	flags.Int64Var(&opts.taskHistoryLimit, "task-history-limit", 10, "Task history retention limit")
38
+	flags.Uint64Var(&opts.heartbeatPeriod, "dispatcher-heartbeat-period", 5000000000, "Dispatcher heartbeat period")
39
+	return cmd
40
+}
41
+
42
+func runUpdate(dockerCli *client.DockerCli, flags *pflag.FlagSet, opts updateOptions) error {
43
+	client := dockerCli.Client()
44
+	ctx := context.Background()
45
+
46
+	swarm, err := client.SwarmInspect(ctx)
47
+	if err != nil {
48
+		return err
49
+	}
50
+
51
+	err = mergeSwarm(&swarm, flags)
52
+	if err != nil {
53
+		return err
54
+	}
55
+	err = client.SwarmUpdate(ctx, swarm.Version, swarm.Spec)
56
+	if err != nil {
57
+		return err
58
+	}
59
+
60
+	fmt.Println("Swarm updated.")
61
+	return nil
62
+}
63
+
64
+func mergeSwarm(swarm *swarm.Swarm, flags *pflag.FlagSet) error {
65
+	spec := &swarm.Spec
66
+
67
+	if flags.Changed("auto-accept") {
68
+		value := flags.Lookup("auto-accept").Value.(*AutoAcceptOption)
69
+		if len(spec.AcceptancePolicy.Policies) > 0 {
70
+			spec.AcceptancePolicy.Policies = value.Policies(spec.AcceptancePolicy.Policies[0].Secret)
71
+		} else {
72
+			spec.AcceptancePolicy.Policies = value.Policies("")
73
+		}
74
+	}
75
+
76
+	if flags.Changed("secret") {
77
+		secret, _ := flags.GetString("secret")
78
+		for _, policy := range spec.AcceptancePolicy.Policies {
79
+			policy.Secret = secret
80
+		}
81
+	}
82
+
83
+	if flags.Changed("task-history-limit") {
84
+		spec.Orchestration.TaskHistoryRetentionLimit, _ = flags.GetInt64("task-history-limit")
85
+	}
86
+
87
+	if flags.Changed("dispatcher-heartbeat-period") {
88
+		spec.Dispatcher.HeartbeatPeriod, _ = flags.GetUint64("dispatcher-heartbeat-period")
89
+	}
90
+
91
+	return nil
92
+}
0 93
new file mode 100644
... ...
@@ -0,0 +1,20 @@
0
+package client
1
+
2
+import (
3
+	"golang.org/x/net/context"
4
+
5
+	Cli "github.com/docker/docker/cli"
6
+	flag "github.com/docker/docker/pkg/mflag"
7
+)
8
+
9
+// CmdTag tags an image into a repository.
10
+//
11
+// Usage: docker tag [OPTIONS] IMAGE[:TAG] [REGISTRYHOST/][USERNAME/]NAME[:TAG]
12
+func (cli *DockerCli) CmdTag(args ...string) error {
13
+	cmd := Cli.Subcmd("tag", []string{"IMAGE[:TAG] [REGISTRYHOST/][USERNAME/]NAME[:TAG]"}, Cli.DockerCommands["tag"].Description, true)
14
+	cmd.Require(flag.Exact, 2)
15
+
16
+	cmd.ParseFlags(args, true)
17
+
18
+	return cli.client.ImageTag(context.Background(), cmd.Arg(0), cmd.Arg(1))
19
+}
0 20
new file mode 100644
... ...
@@ -0,0 +1,79 @@
0
+package task
1
+
2
+import (
3
+	"fmt"
4
+	"sort"
5
+	"strings"
6
+	"text/tabwriter"
7
+	"time"
8
+
9
+	"golang.org/x/net/context"
10
+
11
+	"github.com/docker/docker/api/client"
12
+	"github.com/docker/docker/api/client/idresolver"
13
+	"github.com/docker/engine-api/types/swarm"
14
+	"github.com/docker/go-units"
15
+)
16
+
17
+const (
18
+	psTaskItemFmt = "%s\t%s\t%s\t%s\t%s %s\t%s\t%s\n"
19
+)
20
+
21
+type tasksBySlot []swarm.Task
22
+
23
+func (t tasksBySlot) Len() int {
24
+	return len(t)
25
+}
26
+
27
+func (t tasksBySlot) Swap(i, j int) {
28
+	t[i], t[j] = t[j], t[i]
29
+}
30
+
31
+func (t tasksBySlot) Less(i, j int) bool {
32
+	// Sort by slot.
33
+	if t[i].Slot != t[j].Slot {
34
+		return t[i].Slot < t[j].Slot
35
+	}
36
+
37
+	// If same slot, sort by most recent.
38
+	return t[j].Meta.CreatedAt.Before(t[i].CreatedAt)
39
+}
40
+
41
+// Print task information in a table format
42
+func Print(dockerCli *client.DockerCli, ctx context.Context, tasks []swarm.Task, resolver *idresolver.IDResolver) error {
43
+	sort.Stable(tasksBySlot(tasks))
44
+
45
+	writer := tabwriter.NewWriter(dockerCli.Out(), 0, 4, 2, ' ', 0)
46
+
47
+	// Ignore flushing errors
48
+	defer writer.Flush()
49
+	fmt.Fprintln(writer, strings.Join([]string{"ID", "NAME", "SERVICE", "IMAGE", "LAST STATE", "DESIRED STATE", "NODE"}, "\t"))
50
+	for _, task := range tasks {
51
+		serviceValue, err := resolver.Resolve(ctx, swarm.Service{}, task.ServiceID)
52
+		if err != nil {
53
+			return err
54
+		}
55
+		nodeValue, err := resolver.Resolve(ctx, swarm.Node{}, task.NodeID)
56
+		if err != nil {
57
+			return err
58
+		}
59
+		name := serviceValue
60
+		if task.Slot > 0 {
61
+			name = fmt.Sprintf("%s.%d", name, task.Slot)
62
+		}
63
+		fmt.Fprintf(
64
+			writer,
65
+			psTaskItemFmt,
66
+			task.ID,
67
+			name,
68
+			serviceValue,
69
+			task.Spec.ContainerSpec.Image,
70
+			client.PrettyPrint(task.Status.State),
71
+			units.HumanDuration(time.Since(task.Status.Timestamp)),
72
+			client.PrettyPrint(task.DesiredState),
73
+			nodeValue,
74
+		)
75
+	}
76
+
77
+	return nil
78
+}
... ...
@@ -8,6 +8,7 @@ import (
8 8
 	gosignal "os/signal"
9 9
 	"path/filepath"
10 10
 	"runtime"
11
+	"strings"
11 12
 	"time"
12 13
 
13 14
 	"golang.org/x/net/context"
... ...
@@ -163,3 +164,27 @@ func (cli *DockerCli) ForwardAllSignals(ctx context.Context, cid string) chan os
163 163
 	}()
164 164
 	return sigc
165 165
 }
166
+
167
+// capitalizeFirst capitalizes the first character of string
168
+func capitalizeFirst(s string) string {
169
+	switch l := len(s); l {
170
+	case 0:
171
+		return s
172
+	case 1:
173
+		return strings.ToLower(s)
174
+	default:
175
+		return strings.ToUpper(string(s[0])) + strings.ToLower(s[1:])
176
+	}
177
+}
178
+
179
+// PrettyPrint outputs arbitrary data for human formatted output by uppercasing the first letter.
180
+func PrettyPrint(i interface{}) string {
181
+	switch t := i.(type) {
182
+	case nil:
183
+		return "None"
184
+	case string:
185
+		return capitalizeFirst(t)
186
+	default:
187
+		return capitalizeFirst(fmt.Sprintf("%s", t))
188
+	}
189
+}
... ...
@@ -5,7 +5,10 @@ import (
5 5
 	"github.com/docker/docker/api/client/container"
6 6
 	"github.com/docker/docker/api/client/image"
7 7
 	"github.com/docker/docker/api/client/network"
8
+	"github.com/docker/docker/api/client/node"
8 9
 	"github.com/docker/docker/api/client/registry"
10
+	"github.com/docker/docker/api/client/service"
11
+	"github.com/docker/docker/api/client/swarm"
9 12
 	"github.com/docker/docker/api/client/system"
10 13
 	"github.com/docker/docker/api/client/volume"
11 14
 	"github.com/docker/docker/cli"
... ...
@@ -36,6 +39,9 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor {
36 36
 	rootCmd.SetFlagErrorFunc(cli.FlagErrorFunc)
37 37
 	rootCmd.SetOutput(stdout)
38 38
 	rootCmd.AddCommand(
39
+		node.NewNodeCommand(dockerCli),
40
+		service.NewServiceCommand(dockerCli),
41
+		swarm.NewSwarmCommand(dockerCli),
39 42
 		container.NewAttachCommand(dockerCli),
40 43
 		container.NewCommitCommand(dockerCli),
41 44
 		container.NewCreateCommand(dockerCli),
... ...
@@ -11,7 +11,7 @@ var DockerCommandUsage = []Command{
11 11
 	{"cp", "Copy files/folders between a container and the local filesystem"},
12 12
 	{"exec", "Run a command in a running container"},
13 13
 	{"info", "Display system-wide information"},
14
-	{"inspect", "Return low-level information on a container or image"},
14
+	{"inspect", "Return low-level information on a container, image or task"},
15 15
 	{"update", "Update configuration of one or more containers"},
16 16
 }
17 17
 
... ...
@@ -63,7 +63,7 @@ func (s *DockerSuite) TestRenameCheckNames(c *check.C) {
63 63
 
64 64
 	name, err := inspectFieldWithError("first_name", "Name")
65 65
 	c.Assert(err, checker.NotNil, check.Commentf(name))
66
-	c.Assert(err.Error(), checker.Contains, "No such image or container: first_name")
66
+	c.Assert(err.Error(), checker.Contains, "No such image, container or task: first_name")
67 67
 }
68 68
 
69 69
 func (s *DockerSuite) TestRenameInvalidName(c *check.C) {