瀏覽代碼

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 9 年之前
父節點
當前提交
12a00e6017

+ 70 - 0
api/client/idresolver/idresolver.go

@@ -0,0 +1,70 @@
+package idresolver
+
+import (
+	"fmt"
+
+	"golang.org/x/net/context"
+
+	"github.com/docker/engine-api/client"
+	"github.com/docker/engine-api/types/swarm"
+)
+
+// IDResolver provides ID to Name resolution.
+type IDResolver struct {
+	client    client.APIClient
+	noResolve bool
+	cache     map[string]string
+}
+
+// New creates a new IDResolver.
+func New(client client.APIClient, noResolve bool) *IDResolver {
+	return &IDResolver{
+		client:    client,
+		noResolve: noResolve,
+		cache:     make(map[string]string),
+	}
+}
+
+func (r *IDResolver) get(ctx context.Context, t interface{}, id string) (string, error) {
+	switch t.(type) {
+	case swarm.Node:
+		node, err := r.client.NodeInspect(ctx, id)
+		if err != nil {
+			return id, nil
+		}
+		if node.Spec.Annotations.Name != "" {
+			return node.Spec.Annotations.Name, nil
+		}
+		if node.Description.Hostname != "" {
+			return node.Description.Hostname, nil
+		}
+		return id, nil
+	case swarm.Service:
+		service, err := r.client.ServiceInspect(ctx, id)
+		if err != nil {
+			return id, nil
+		}
+		return service.Spec.Annotations.Name, nil
+	default:
+		return "", fmt.Errorf("unsupported type")
+	}
+
+}
+
+// Resolve will attempt to resolve an ID to a Name by querying the manager.
+// Results are stored into a cache.
+// If the `-n` flag is used in the command-line, resolution is disabled.
+func (r *IDResolver) Resolve(ctx context.Context, t interface{}, id string) (string, error) {
+	if r.noResolve {
+		return id, nil
+	}
+	if name, ok := r.cache[id]; ok {
+		return name, nil
+	}
+	name, err := r.get(ctx, t, id)
+	if err != nil {
+		return "", err
+	}
+	r.cache[id] = name
+	return name, nil
+}

+ 16 - 0
api/client/info.go

@@ -10,6 +10,7 @@ import (
 	"github.com/docker/docker/pkg/ioutils"
 	flag "github.com/docker/docker/pkg/mflag"
 	"github.com/docker/docker/utils"
+	"github.com/docker/engine-api/types/swarm"
 	"github.com/docker/go-units"
 )
 
@@ -68,6 +69,21 @@ func (cli *DockerCli) CmdInfo(args ...string) error {
 		fmt.Fprintf(cli.out, "\n")
 	}
 
+	fmt.Fprintf(cli.out, "Swarm: %v\n", info.Swarm.LocalNodeState)
+	if info.Swarm.LocalNodeState != swarm.LocalNodeStateInactive {
+		fmt.Fprintf(cli.out, " NodeID: %s\n", info.Swarm.NodeID)
+		if info.Swarm.Error != "" {
+			fmt.Fprintf(cli.out, " Error: %v\n", info.Swarm.Error)
+		}
+		if info.Swarm.ControlAvailable {
+			fmt.Fprintf(cli.out, " IsManager: Yes\n")
+			fmt.Fprintf(cli.out, " Managers: %d\n", info.Swarm.Managers)
+			fmt.Fprintf(cli.out, " Nodes: %d\n", info.Swarm.Nodes)
+			ioutils.FprintfIfNotEmpty(cli.out, " CACertHash: %s\n", info.Swarm.CACertHash)
+		} else {
+			fmt.Fprintf(cli.out, " IsManager: No\n")
+		}
+	}
 	ioutils.FprintfIfNotEmpty(cli.out, "Kernel Version: %s\n", info.KernelVersion)
 	ioutils.FprintfIfNotEmpty(cli.out, "Operating System: %s\n", info.OperatingSystem)
 	ioutils.FprintfIfNotEmpty(cli.out, "OSType: %s\n", info.OSType)

+ 25 - 6
api/client/inspect.go

@@ -11,19 +11,19 @@ import (
 	"github.com/docker/engine-api/client"
 )
 
-// CmdInspect displays low-level information on one or more containers or images.
+// CmdInspect displays low-level information on one or more containers, images or tasks.
 //
-// Usage: docker inspect [OPTIONS] CONTAINER|IMAGE [CONTAINER|IMAGE...]
+// Usage: docker inspect [OPTIONS] CONTAINER|IMAGE|TASK [CONTAINER|IMAGE|TASK...]
 func (cli *DockerCli) CmdInspect(args ...string) error {
-	cmd := Cli.Subcmd("inspect", []string{"CONTAINER|IMAGE [CONTAINER|IMAGE...]"}, Cli.DockerCommands["inspect"].Description, true)
+	cmd := Cli.Subcmd("inspect", []string{"CONTAINER|IMAGE|TASK [CONTAINER|IMAGE|TASK...]"}, Cli.DockerCommands["inspect"].Description, true)
 	tmplStr := cmd.String([]string{"f", "-format"}, "", "Format the output using the given go template")
-	inspectType := cmd.String([]string{"-type"}, "", "Return JSON for specified type, (e.g image or container)")
+	inspectType := cmd.String([]string{"-type"}, "", "Return JSON for specified type, (e.g image, container or task)")
 	size := cmd.Bool([]string{"s", "-size"}, false, "Display total file sizes if the type is container")
 	cmd.Require(flag.Min, 1)
 
 	cmd.ParseFlags(args, true)
 
-	if *inspectType != "" && *inspectType != "container" && *inspectType != "image" {
+	if *inspectType != "" && *inspectType != "container" && *inspectType != "image" && *inspectType != "task" {
 		return fmt.Errorf("%q is not a valid value for --type", *inspectType)
 	}
 
@@ -35,6 +35,11 @@ func (cli *DockerCli) CmdInspect(args ...string) error {
 		elementSearcher = cli.inspectContainers(ctx, *size)
 	case "image":
 		elementSearcher = cli.inspectImages(ctx, *size)
+	case "task":
+		if *size {
+			fmt.Fprintln(cli.err, "WARNING: --size ignored for tasks")
+		}
+		elementSearcher = cli.inspectTasks(ctx)
 	default:
 		elementSearcher = cli.inspectAll(ctx, *size)
 	}
@@ -54,6 +59,12 @@ func (cli *DockerCli) inspectImages(ctx context.Context, getSize bool) inspect.G
 	}
 }
 
+func (cli *DockerCli) inspectTasks(ctx context.Context) inspect.GetRefFunc {
+	return func(ref string) (interface{}, []byte, error) {
+		return cli.client.TaskInspectWithRaw(ctx, ref)
+	}
+}
+
 func (cli *DockerCli) inspectAll(ctx context.Context, getSize bool) inspect.GetRefFunc {
 	return func(ref string) (interface{}, []byte, error) {
 		c, rawContainer, err := cli.client.ContainerInspectWithRaw(ctx, ref, getSize)
@@ -63,7 +74,15 @@ func (cli *DockerCli) inspectAll(ctx context.Context, getSize bool) inspect.GetR
 				i, rawImage, err := cli.client.ImageInspectWithRaw(ctx, ref, getSize)
 				if err != nil {
 					if client.IsErrImageNotFound(err) {
-						return nil, nil, fmt.Errorf("Error: No such image or container: %s", ref)
+						// Search for task with that id if an image doesn't exists.
+						t, rawTask, err := cli.client.TaskInspectWithRaw(ctx, ref)
+						if err != nil {
+							return nil, nil, fmt.Errorf("Error: No such image, container or task: %s", ref)
+						}
+						if getSize {
+							fmt.Fprintln(cli.err, "WARNING: --size ignored for tasks")
+						}
+						return t, rawTask, nil
 					}
 					return nil, nil, err
 				}

+ 6 - 4
api/client/network/list.go

@@ -71,7 +71,7 @@ func runList(dockerCli *client.DockerCli, opts listOptions) error {
 
 	w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0)
 	if !opts.quiet {
-		fmt.Fprintf(w, "NETWORK ID\tNAME\tDRIVER")
+		fmt.Fprintf(w, "NETWORK ID\tNAME\tDRIVER\tSCOPE")
 		fmt.Fprintf(w, "\n")
 	}
 
@@ -79,6 +79,8 @@ func runList(dockerCli *client.DockerCli, opts listOptions) error {
 	for _, networkResource := range networkResources {
 		ID := networkResource.ID
 		netName := networkResource.Name
+		driver := networkResource.Driver
+		scope := networkResource.Scope
 		if !opts.noTrunc {
 			ID = stringid.TruncateID(ID)
 		}
@@ -86,11 +88,11 @@ func runList(dockerCli *client.DockerCli, opts listOptions) error {
 			fmt.Fprintln(w, ID)
 			continue
 		}
-		driver := networkResource.Driver
-		fmt.Fprintf(w, "%s\t%s\t%s\t",
+		fmt.Fprintf(w, "%s\t%s\t%s\t%s\t",
 			ID,
 			netName,
-			driver)
+			driver,
+			scope)
 		fmt.Fprint(w, "\n")
 	}
 	w.Flush()

+ 40 - 0
api/client/node/accept.go

@@ -0,0 +1,40 @@
+package node
+
+import (
+	"fmt"
+
+	"github.com/docker/docker/api/client"
+	"github.com/docker/docker/cli"
+	"github.com/docker/engine-api/types/swarm"
+	"github.com/spf13/cobra"
+	"github.com/spf13/pflag"
+)
+
+func newAcceptCommand(dockerCli *client.DockerCli) *cobra.Command {
+	var flags *pflag.FlagSet
+
+	cmd := &cobra.Command{
+		Use:   "accept NODE [NODE...]",
+		Short: "Accept a node in the swarm",
+		Args:  cli.RequiresMinArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runAccept(dockerCli, flags, args)
+		},
+	}
+
+	flags = cmd.Flags()
+	return cmd
+}
+
+func runAccept(dockerCli *client.DockerCli, flags *pflag.FlagSet, args []string) error {
+	for _, id := range args {
+		if err := runUpdate(dockerCli, id, func(node *swarm.Node) {
+			node.Spec.Membership = swarm.NodeMembershipAccepted
+		}); err != nil {
+			return err
+		}
+		fmt.Println(id, "attempting to accept a node in the swarm.")
+	}
+
+	return nil
+}

+ 49 - 0
api/client/node/cmd.go

@@ -0,0 +1,49 @@
+package node
+
+import (
+	"fmt"
+
+	"golang.org/x/net/context"
+
+	"github.com/spf13/cobra"
+
+	"github.com/docker/docker/api/client"
+	"github.com/docker/docker/cli"
+	apiclient "github.com/docker/engine-api/client"
+)
+
+// NewNodeCommand returns a cobra command for `node` subcommands
+func NewNodeCommand(dockerCli *client.DockerCli) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "node",
+		Short: "Manage docker swarm nodes",
+		Args:  cli.NoArgs,
+		Run: func(cmd *cobra.Command, args []string) {
+			fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
+		},
+	}
+	cmd.AddCommand(
+		newAcceptCommand(dockerCli),
+		newDemoteCommand(dockerCli),
+		newInspectCommand(dockerCli),
+		newListCommand(dockerCli),
+		newPromoteCommand(dockerCli),
+		newRemoveCommand(dockerCli),
+		newTasksCommand(dockerCli),
+		newUpdateCommand(dockerCli),
+	)
+	return cmd
+}
+
+func nodeReference(client apiclient.APIClient, ctx context.Context, ref string) (string, error) {
+	// The special value "self" for a node reference is mapped to the current
+	// node, hence the node ID is retrieved using the `/info` endpoint.
+	if ref == "self" {
+		info, err := client.Info(ctx)
+		if err != nil {
+			return "", err
+		}
+		return info.Swarm.NodeID, nil
+	}
+	return ref, nil
+}

+ 40 - 0
api/client/node/demote.go

@@ -0,0 +1,40 @@
+package node
+
+import (
+	"fmt"
+
+	"github.com/docker/docker/api/client"
+	"github.com/docker/docker/cli"
+	"github.com/docker/engine-api/types/swarm"
+	"github.com/spf13/cobra"
+	"github.com/spf13/pflag"
+)
+
+func newDemoteCommand(dockerCli *client.DockerCli) *cobra.Command {
+	var flags *pflag.FlagSet
+
+	cmd := &cobra.Command{
+		Use:   "demote NODE [NODE...]",
+		Short: "Demote a node from manager in the swarm",
+		Args:  cli.RequiresMinArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runDemote(dockerCli, flags, args)
+		},
+	}
+
+	flags = cmd.Flags()
+	return cmd
+}
+
+func runDemote(dockerCli *client.DockerCli, flags *pflag.FlagSet, args []string) error {
+	for _, id := range args {
+		if err := runUpdate(dockerCli, id, func(node *swarm.Node) {
+			node.Spec.Role = swarm.NodeRoleWorker
+		}); err != nil {
+			return err
+		}
+		fmt.Println(id, "attempting to demote a manager in the swarm.")
+	}
+
+	return nil
+}

+ 141 - 0
api/client/node/inspect.go

@@ -0,0 +1,141 @@
+package node
+
+import (
+	"fmt"
+	"io"
+	"sort"
+	"strings"
+
+	"github.com/docker/docker/api/client"
+	"github.com/docker/docker/api/client/inspect"
+	"github.com/docker/docker/cli"
+	"github.com/docker/docker/pkg/ioutils"
+	"github.com/docker/engine-api/types/swarm"
+	"github.com/docker/go-units"
+	"github.com/spf13/cobra"
+	"golang.org/x/net/context"
+)
+
+type inspectOptions struct {
+	nodeIds []string
+	format  string
+	pretty  bool
+}
+
+func newInspectCommand(dockerCli *client.DockerCli) *cobra.Command {
+	var opts inspectOptions
+
+	cmd := &cobra.Command{
+		Use:   "inspect [OPTIONS] self|NODE [NODE...]",
+		Short: "Inspect a node in the swarm",
+		Args:  cli.RequiresMinArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			opts.nodeIds = args
+			return runInspect(dockerCli, opts)
+		},
+	}
+
+	flags := cmd.Flags()
+	flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template")
+	flags.BoolVarP(&opts.pretty, "pretty", "p", false, "Print the information in a human friendly format.")
+	return cmd
+}
+
+func runInspect(dockerCli *client.DockerCli, opts inspectOptions) error {
+	client := dockerCli.Client()
+	ctx := context.Background()
+	getRef := func(ref string) (interface{}, []byte, error) {
+		nodeRef, err := nodeReference(client, ctx, ref)
+		if err != nil {
+			return nil, nil, err
+		}
+		node, err := client.NodeInspect(ctx, nodeRef)
+		return node, nil, err
+	}
+
+	if !opts.pretty {
+		return inspect.Inspect(dockerCli.Out(), opts.nodeIds, opts.format, getRef)
+	}
+	return printHumanFriendly(dockerCli.Out(), opts.nodeIds, getRef)
+}
+
+func printHumanFriendly(out io.Writer, refs []string, getRef inspect.GetRefFunc) error {
+	for idx, ref := range refs {
+		obj, _, err := getRef(ref)
+		if err != nil {
+			return err
+		}
+		printNode(out, obj.(swarm.Node))
+
+		// TODO: better way to do this?
+		// print extra space between objects, but not after the last one
+		if idx+1 != len(refs) {
+			fmt.Fprintf(out, "\n\n")
+		}
+	}
+	return nil
+}
+
+// TODO: use a template
+func printNode(out io.Writer, node swarm.Node) {
+	fmt.Fprintf(out, "ID:\t\t\t%s\n", node.ID)
+	ioutils.FprintfIfNotEmpty(out, "Name:\t\t\t%s\n", node.Spec.Name)
+	if node.Spec.Labels != nil {
+		fmt.Fprintln(out, "Labels:")
+		for k, v := range node.Spec.Labels {
+			fmt.Fprintf(out, " - %s = %s\n", k, v)
+		}
+	}
+
+	ioutils.FprintfIfNotEmpty(out, "Hostname:\t\t%s\n", node.Description.Hostname)
+	fmt.Fprintln(out, "Status:")
+	fmt.Fprintf(out, " State:\t\t\t%s\n", client.PrettyPrint(node.Status.State))
+	ioutils.FprintfIfNotEmpty(out, " Message:\t\t%s\n", client.PrettyPrint(node.Status.Message))
+	fmt.Fprintf(out, " Availability:\t\t%s\n", client.PrettyPrint(node.Spec.Availability))
+
+	if node.ManagerStatus != nil {
+		fmt.Fprintln(out, "Manager Status:")
+		fmt.Fprintf(out, " Address:\t\t%s\n", node.ManagerStatus.Addr)
+		fmt.Fprintf(out, " Raft status:\t\t%s\n", client.PrettyPrint(node.ManagerStatus.Reachability))
+		leader := "No"
+		if node.ManagerStatus.Leader {
+			leader = "Yes"
+		}
+		fmt.Fprintf(out, " Leader:\t\t%s\n", leader)
+	}
+
+	fmt.Fprintln(out, "Platform:")
+	fmt.Fprintf(out, " Operating System:\t%s\n", node.Description.Platform.OS)
+	fmt.Fprintf(out, " Architecture:\t\t%s\n", node.Description.Platform.Architecture)
+
+	fmt.Fprintln(out, "Resources:")
+	fmt.Fprintf(out, " CPUs:\t\t\t%d\n", node.Description.Resources.NanoCPUs/1e9)
+	fmt.Fprintf(out, " Memory:\t\t%s\n", units.BytesSize(float64(node.Description.Resources.MemoryBytes)))
+
+	var pluginTypes []string
+	pluginNamesByType := map[string][]string{}
+	for _, p := range node.Description.Engine.Plugins {
+		// append to pluginTypes only if not done previously
+		if _, ok := pluginNamesByType[p.Type]; !ok {
+			pluginTypes = append(pluginTypes, p.Type)
+		}
+		pluginNamesByType[p.Type] = append(pluginNamesByType[p.Type], p.Name)
+	}
+
+	if len(pluginTypes) > 0 {
+		fmt.Fprintln(out, "Plugins:")
+		sort.Strings(pluginTypes) // ensure stable output
+		for _, pluginType := range pluginTypes {
+			fmt.Fprintf(out, "  %s:\t\t%s\n", pluginType, strings.Join(pluginNamesByType[pluginType], ", "))
+		}
+	}
+	fmt.Fprintf(out, "Engine Version:\t\t%s\n", node.Description.Engine.EngineVersion)
+
+	if len(node.Description.Engine.Labels) != 0 {
+		fmt.Fprintln(out, "Engine Labels:")
+		for k, v := range node.Description.Engine.Labels {
+			fmt.Fprintf(out, " - %s = %s", k, v)
+		}
+	}
+
+}

+ 119 - 0
api/client/node/list.go

@@ -0,0 +1,119 @@
+package node
+
+import (
+	"fmt"
+	"io"
+	"text/tabwriter"
+
+	"golang.org/x/net/context"
+
+	"github.com/docker/docker/api/client"
+	"github.com/docker/docker/cli"
+	"github.com/docker/docker/opts"
+	"github.com/docker/engine-api/types"
+	"github.com/docker/engine-api/types/swarm"
+	"github.com/spf13/cobra"
+)
+
+const (
+	listItemFmt = "%s\t%s\t%s\t%s\t%s\t%s\t%s\n"
+)
+
+type listOptions struct {
+	quiet  bool
+	filter opts.FilterOpt
+}
+
+func newListCommand(dockerCli *client.DockerCli) *cobra.Command {
+	opts := listOptions{filter: opts.NewFilterOpt()}
+
+	cmd := &cobra.Command{
+		Use:     "ls",
+		Aliases: []string{"list"},
+		Short:   "List nodes in the swarm",
+		Args:    cli.NoArgs,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runList(dockerCli, opts)
+		},
+	}
+	flags := cmd.Flags()
+	flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs")
+	flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided")
+
+	return cmd
+}
+
+func runList(dockerCli *client.DockerCli, opts listOptions) error {
+	client := dockerCli.Client()
+	ctx := context.Background()
+
+	nodes, err := client.NodeList(
+		ctx,
+		types.NodeListOptions{Filter: opts.filter.Value()})
+	if err != nil {
+		return err
+	}
+
+	info, err := client.Info(ctx)
+	if err != nil {
+		return err
+	}
+
+	out := dockerCli.Out()
+	if opts.quiet {
+		printQuiet(out, nodes)
+	} else {
+		printTable(out, nodes, info)
+	}
+	return nil
+}
+
+func printTable(out io.Writer, nodes []swarm.Node, info types.Info) {
+	writer := tabwriter.NewWriter(out, 0, 4, 2, ' ', 0)
+
+	// Ignore flushing errors
+	defer writer.Flush()
+
+	fmt.Fprintf(writer, listItemFmt, "ID", "NAME", "MEMBERSHIP", "STATUS", "AVAILABILITY", "MANAGER STATUS", "LEADER")
+	for _, node := range nodes {
+		name := node.Spec.Name
+		availability := string(node.Spec.Availability)
+		membership := string(node.Spec.Membership)
+
+		if name == "" {
+			name = node.Description.Hostname
+		}
+
+		leader := ""
+		if node.ManagerStatus != nil && node.ManagerStatus.Leader {
+			leader = "Yes"
+		}
+
+		reachability := ""
+		if node.ManagerStatus != nil {
+			reachability = string(node.ManagerStatus.Reachability)
+		}
+
+		ID := node.ID
+		if node.ID == info.Swarm.NodeID {
+			ID = ID + " *"
+		}
+
+		fmt.Fprintf(
+			writer,
+			listItemFmt,
+			ID,
+			name,
+			client.PrettyPrint(membership),
+			client.PrettyPrint(string(node.Status.State)),
+			client.PrettyPrint(availability),
+			client.PrettyPrint(reachability),
+			leader)
+	}
+}
+
+func printQuiet(out io.Writer, nodes []swarm.Node) {
+	for _, node := range nodes {
+		fmt.Fprintln(out, node.ID)
+	}
+}

+ 50 - 0
api/client/node/opts.go

@@ -0,0 +1,50 @@
+package node
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/docker/engine-api/types/swarm"
+)
+
+type nodeOptions struct {
+	role         string
+	membership   string
+	availability string
+}
+
+func (opts *nodeOptions) ToNodeSpec() (swarm.NodeSpec, error) {
+	var spec swarm.NodeSpec
+
+	switch swarm.NodeRole(strings.ToLower(opts.role)) {
+	case swarm.NodeRoleWorker:
+		spec.Role = swarm.NodeRoleWorker
+	case swarm.NodeRoleManager:
+		spec.Role = swarm.NodeRoleManager
+	case "":
+	default:
+		return swarm.NodeSpec{}, fmt.Errorf("invalid role %q, only worker and manager are supported", opts.role)
+	}
+
+	switch swarm.NodeMembership(strings.ToLower(opts.membership)) {
+	case swarm.NodeMembershipAccepted:
+		spec.Membership = swarm.NodeMembershipAccepted
+	case "":
+	default:
+		return swarm.NodeSpec{}, fmt.Errorf("invalid membership %q, only accepted is supported", opts.membership)
+	}
+
+	switch swarm.NodeAvailability(strings.ToLower(opts.availability)) {
+	case swarm.NodeAvailabilityActive:
+		spec.Availability = swarm.NodeAvailabilityActive
+	case swarm.NodeAvailabilityPause:
+		spec.Availability = swarm.NodeAvailabilityPause
+	case swarm.NodeAvailabilityDrain:
+		spec.Availability = swarm.NodeAvailabilityDrain
+	case "":
+	default:
+		return swarm.NodeSpec{}, fmt.Errorf("invalid availability %q, only active, pause and drain are supported", opts.availability)
+	}
+
+	return spec, nil
+}

+ 40 - 0
api/client/node/promote.go

@@ -0,0 +1,40 @@
+package node
+
+import (
+	"fmt"
+
+	"github.com/docker/docker/api/client"
+	"github.com/docker/docker/cli"
+	"github.com/docker/engine-api/types/swarm"
+	"github.com/spf13/cobra"
+	"github.com/spf13/pflag"
+)
+
+func newPromoteCommand(dockerCli *client.DockerCli) *cobra.Command {
+	var flags *pflag.FlagSet
+
+	cmd := &cobra.Command{
+		Use:   "promote NODE [NODE...]",
+		Short: "Promote a node to a manager in the swarm",
+		Args:  cli.RequiresMinArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runPromote(dockerCli, flags, args)
+		},
+	}
+
+	flags = cmd.Flags()
+	return cmd
+}
+
+func runPromote(dockerCli *client.DockerCli, flags *pflag.FlagSet, args []string) error {
+	for _, id := range args {
+		if err := runUpdate(dockerCli, id, func(node *swarm.Node) {
+			node.Spec.Role = swarm.NodeRoleManager
+		}); err != nil {
+			return err
+		}
+		fmt.Println(id, "attempting to promote a node to a manager in the swarm.")
+	}
+
+	return nil
+}

+ 36 - 0
api/client/node/remove.go

@@ -0,0 +1,36 @@
+package node
+
+import (
+	"fmt"
+
+	"golang.org/x/net/context"
+
+	"github.com/docker/docker/api/client"
+	"github.com/docker/docker/cli"
+	"github.com/spf13/cobra"
+)
+
+func newRemoveCommand(dockerCli *client.DockerCli) *cobra.Command {
+	return &cobra.Command{
+		Use:     "rm NODE [NODE...]",
+		Aliases: []string{"remove"},
+		Short:   "Remove a node from the swarm",
+		Args:    cli.RequiresMinArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runRemove(dockerCli, args)
+		},
+	}
+}
+
+func runRemove(dockerCli *client.DockerCli, args []string) error {
+	client := dockerCli.Client()
+	ctx := context.Background()
+	for _, nodeID := range args {
+		err := client.NodeRemove(ctx, nodeID)
+		if err != nil {
+			return err
+		}
+		fmt.Fprintf(dockerCli.Out(), "%s\n", nodeID)
+	}
+	return nil
+}

+ 72 - 0
api/client/node/tasks.go

@@ -0,0 +1,72 @@
+package node
+
+import (
+	"golang.org/x/net/context"
+
+	"github.com/docker/docker/api/client"
+	"github.com/docker/docker/api/client/idresolver"
+	"github.com/docker/docker/api/client/task"
+	"github.com/docker/docker/cli"
+	"github.com/docker/docker/opts"
+	"github.com/docker/engine-api/types"
+	"github.com/docker/engine-api/types/swarm"
+	"github.com/spf13/cobra"
+)
+
+type tasksOptions struct {
+	nodeID    string
+	all       bool
+	noResolve bool
+	filter    opts.FilterOpt
+}
+
+func newTasksCommand(dockerCli *client.DockerCli) *cobra.Command {
+	opts := tasksOptions{filter: opts.NewFilterOpt()}
+
+	cmd := &cobra.Command{
+		Use:   "tasks [OPTIONS] self|NODE",
+		Short: "List tasks running on a node",
+		Args:  cli.ExactArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			opts.nodeID = args[0]
+			return runTasks(dockerCli, opts)
+		},
+	}
+	flags := cmd.Flags()
+	flags.BoolVarP(&opts.all, "all", "a", false, "Display all instances")
+	flags.BoolVarP(&opts.noResolve, "no-resolve", "n", false, "Do not map IDs to Names")
+	flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided")
+
+	return cmd
+}
+
+func runTasks(dockerCli *client.DockerCli, opts tasksOptions) error {
+	client := dockerCli.Client()
+	ctx := context.Background()
+
+	nodeRef, err := nodeReference(client, ctx, opts.nodeID)
+	if err != nil {
+		return nil
+	}
+	node, err := client.NodeInspect(ctx, nodeRef)
+	if err != nil {
+		return err
+	}
+
+	filter := opts.filter.Value()
+	filter.Add("node", node.ID)
+	if !opts.all {
+		filter.Add("desired_state", string(swarm.TaskStateRunning))
+		filter.Add("desired_state", string(swarm.TaskStateAccepted))
+
+	}
+
+	tasks, err := client.TaskList(
+		ctx,
+		types.TaskListOptions{Filter: filter})
+	if err != nil {
+		return err
+	}
+
+	return task.Print(dockerCli, ctx, tasks, idresolver.New(client, opts.noResolve))
+}

+ 100 - 0
api/client/node/update.go

@@ -0,0 +1,100 @@
+package node
+
+import (
+	"fmt"
+
+	"github.com/docker/docker/api/client"
+	"github.com/docker/docker/cli"
+	runconfigopts "github.com/docker/docker/runconfig/opts"
+	"github.com/docker/engine-api/types/swarm"
+	"github.com/spf13/cobra"
+	"github.com/spf13/pflag"
+	"golang.org/x/net/context"
+)
+
+func newUpdateCommand(dockerCli *client.DockerCli) *cobra.Command {
+	var opts nodeOptions
+	var flags *pflag.FlagSet
+
+	cmd := &cobra.Command{
+		Use:   "update [OPTIONS] NODE",
+		Short: "Update a node",
+		Args:  cli.ExactArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runUpdate(dockerCli, args[0], mergeNodeUpdate(flags))
+		},
+	}
+
+	flags = cmd.Flags()
+	flags.StringVar(&opts.role, "role", "", "Role of the node (worker/manager)")
+	flags.StringVar(&opts.membership, "membership", "", "Membership of the node (accepted/rejected)")
+	flags.StringVar(&opts.availability, "availability", "", "Availability of the node (active/pause/drain)")
+	return cmd
+}
+
+func runUpdate(dockerCli *client.DockerCli, nodeID string, mergeNode func(node *swarm.Node)) error {
+	client := dockerCli.Client()
+	ctx := context.Background()
+
+	node, err := client.NodeInspect(ctx, nodeID)
+	if err != nil {
+		return err
+	}
+
+	mergeNode(&node)
+	err = client.NodeUpdate(ctx, nodeID, node.Version, node.Spec)
+	if err != nil {
+		return err
+	}
+
+	fmt.Fprintf(dockerCli.Out(), "%s\n", nodeID)
+	return nil
+}
+
+func mergeNodeUpdate(flags *pflag.FlagSet) func(*swarm.Node) {
+	return func(node *swarm.Node) {
+		mergeString := func(flag string, field *string) {
+			if flags.Changed(flag) {
+				*field, _ = flags.GetString(flag)
+			}
+		}
+
+		mergeRole := func(flag string, field *swarm.NodeRole) {
+			if flags.Changed(flag) {
+				str, _ := flags.GetString(flag)
+				*field = swarm.NodeRole(str)
+			}
+		}
+
+		mergeMembership := func(flag string, field *swarm.NodeMembership) {
+			if flags.Changed(flag) {
+				str, _ := flags.GetString(flag)
+				*field = swarm.NodeMembership(str)
+			}
+		}
+
+		mergeAvailability := func(flag string, field *swarm.NodeAvailability) {
+			if flags.Changed(flag) {
+				str, _ := flags.GetString(flag)
+				*field = swarm.NodeAvailability(str)
+			}
+		}
+
+		mergeLabels := func(flag string, field *map[string]string) {
+			if flags.Changed(flag) {
+				values, _ := flags.GetStringSlice(flag)
+				for key, value := range runconfigopts.ConvertKVStringsToMap(values) {
+					(*field)[key] = value
+				}
+			}
+		}
+
+		spec := &node.Spec
+		mergeString("name", &spec.Name)
+		// TODO: setting labels is not working
+		mergeLabels("label", &spec.Labels)
+		mergeRole("role", &spec.Role)
+		mergeMembership("membership", &spec.Membership)
+		mergeAvailability("availability", &spec.Availability)
+	}
+}

+ 32 - 0
api/client/service/cmd.go

@@ -0,0 +1,32 @@
+package service
+
+import (
+	"fmt"
+
+	"github.com/spf13/cobra"
+
+	"github.com/docker/docker/api/client"
+	"github.com/docker/docker/cli"
+)
+
+// NewServiceCommand returns a cobra command for `service` subcommands
+func NewServiceCommand(dockerCli *client.DockerCli) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "service",
+		Short: "Manage docker services",
+		Args:  cli.NoArgs,
+		Run: func(cmd *cobra.Command, args []string) {
+			fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
+		},
+	}
+	cmd.AddCommand(
+		newCreateCommand(dockerCli),
+		newInspectCommand(dockerCli),
+		newTasksCommand(dockerCli),
+		newListCommand(dockerCli),
+		newRemoveCommand(dockerCli),
+		newScaleCommand(dockerCli),
+		newUpdateCommand(dockerCli),
+	)
+	return cmd
+}

+ 47 - 0
api/client/service/create.go

@@ -0,0 +1,47 @@
+package service
+
+import (
+	"fmt"
+
+	"github.com/docker/docker/api/client"
+	"github.com/docker/docker/cli"
+	"github.com/spf13/cobra"
+	"golang.org/x/net/context"
+)
+
+func newCreateCommand(dockerCli *client.DockerCli) *cobra.Command {
+	opts := newServiceOptions()
+
+	cmd := &cobra.Command{
+		Use:   "create [OPTIONS] IMAGE [COMMAND] [ARG...]",
+		Short: "Create a new service",
+		Args:  cli.RequiresMinArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			opts.image = args[0]
+			if len(args) > 1 {
+				opts.args = args[1:]
+			}
+			return runCreate(dockerCli, opts)
+		},
+	}
+	addServiceFlags(cmd, opts)
+	cmd.Flags().SetInterspersed(false)
+	return cmd
+}
+
+func runCreate(dockerCli *client.DockerCli, opts *serviceOptions) error {
+	client := dockerCli.Client()
+
+	service, err := opts.ToService()
+	if err != nil {
+		return err
+	}
+
+	response, err := client.ServiceCreate(context.Background(), service)
+	if err != nil {
+		return err
+	}
+
+	fmt.Fprintf(dockerCli.Out(), "%s\n", response.ID)
+	return nil
+}

+ 127 - 0
api/client/service/inspect.go

@@ -0,0 +1,127 @@
+package service
+
+import (
+	"fmt"
+	"io"
+	"strings"
+
+	"golang.org/x/net/context"
+
+	"github.com/docker/docker/api/client"
+	"github.com/docker/docker/api/client/inspect"
+	"github.com/docker/docker/cli"
+	"github.com/docker/docker/pkg/ioutils"
+	apiclient "github.com/docker/engine-api/client"
+	"github.com/docker/engine-api/types/swarm"
+	"github.com/spf13/cobra"
+)
+
+type inspectOptions struct {
+	refs   []string
+	format string
+	pretty bool
+}
+
+func newInspectCommand(dockerCli *client.DockerCli) *cobra.Command {
+	var opts inspectOptions
+
+	cmd := &cobra.Command{
+		Use:   "inspect [OPTIONS] SERVICE [SERVICE...]",
+		Short: "Inspect a service",
+		Args:  cli.RequiresMinArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			opts.refs = args
+
+			if opts.pretty && len(opts.format) > 0 {
+				return fmt.Errorf("--format is incompatible with human friendly format")
+			}
+			return runInspect(dockerCli, opts)
+		},
+	}
+
+	flags := cmd.Flags()
+	flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template")
+	flags.BoolVarP(&opts.pretty, "pretty", "p", false, "Print the information in a human friendly format.")
+	return cmd
+}
+
+func runInspect(dockerCli *client.DockerCli, opts inspectOptions) error {
+	client := dockerCli.Client()
+	ctx := context.Background()
+
+	getRef := func(ref string) (interface{}, []byte, error) {
+		service, err := client.ServiceInspect(ctx, ref)
+		if err == nil || !apiclient.IsErrServiceNotFound(err) {
+			return service, nil, err
+		}
+		return nil, nil, fmt.Errorf("Error: no such service: %s", ref)
+	}
+
+	if !opts.pretty {
+		return inspect.Inspect(dockerCli.Out(), opts.refs, opts.format, getRef)
+	}
+
+	return printHumanFriendly(dockerCli.Out(), opts.refs, getRef)
+}
+
+func printHumanFriendly(out io.Writer, refs []string, getRef inspect.GetRefFunc) error {
+	for idx, ref := range refs {
+		obj, _, err := getRef(ref)
+		if err != nil {
+			return err
+		}
+		printService(out, obj.(swarm.Service))
+
+		// TODO: better way to do this?
+		// print extra space between objects, but not after the last one
+		if idx+1 != len(refs) {
+			fmt.Fprintf(out, "\n\n")
+		}
+	}
+	return nil
+}
+
+// TODO: use a template
+func printService(out io.Writer, service swarm.Service) {
+	fmt.Fprintf(out, "ID:\t\t%s\n", service.ID)
+	fmt.Fprintf(out, "Name:\t\t%s\n", service.Spec.Name)
+	if service.Spec.Labels != nil {
+		fmt.Fprintln(out, "Labels:")
+		for k, v := range service.Spec.Labels {
+			fmt.Fprintf(out, " - %s=%s\n", k, v)
+		}
+	}
+
+	if service.Spec.Mode.Global != nil {
+		fmt.Fprintln(out, "Mode:\t\tGLOBAL")
+	} else {
+		fmt.Fprintln(out, "Mode:\t\tREPLICATED")
+		if service.Spec.Mode.Replicated.Replicas != nil {
+			fmt.Fprintf(out, " Replicas:\t\t%d\n", *service.Spec.Mode.Replicated.Replicas)
+		}
+	}
+	fmt.Fprintln(out, "Placement:")
+	fmt.Fprintln(out, " Strategy:\tSPREAD")
+	fmt.Fprintf(out, "UpateConfig:\n")
+	fmt.Fprintf(out, " Parallelism:\t%d\n", service.Spec.UpdateConfig.Parallelism)
+	if service.Spec.UpdateConfig.Delay.Nanoseconds() > 0 {
+		fmt.Fprintf(out, " Delay:\t\t%s\n", service.Spec.UpdateConfig.Delay)
+	}
+	fmt.Fprintf(out, "ContainerSpec:\n")
+	printContainerSpec(out, service.Spec.TaskTemplate.ContainerSpec)
+}
+
+func printContainerSpec(out io.Writer, containerSpec swarm.ContainerSpec) {
+	fmt.Fprintf(out, " Image:\t\t%s\n", containerSpec.Image)
+	if len(containerSpec.Command) > 0 {
+		fmt.Fprintf(out, " Command:\t%s\n", strings.Join(containerSpec.Command, " "))
+	}
+	if len(containerSpec.Args) > 0 {
+		fmt.Fprintf(out, " Args:\t%s\n", strings.Join(containerSpec.Args, " "))
+	}
+	if len(containerSpec.Env) > 0 {
+		fmt.Fprintf(out, " Env:\t\t%s\n", strings.Join(containerSpec.Env, " "))
+	}
+	ioutils.FprintfIfNotEmpty(out, " Dir\t\t%s\n", containerSpec.Dir)
+	ioutils.FprintfIfNotEmpty(out, " User\t\t%s\n", containerSpec.User)
+}

+ 97 - 0
api/client/service/list.go

@@ -0,0 +1,97 @@
+package service
+
+import (
+	"fmt"
+	"io"
+	"strings"
+	"text/tabwriter"
+
+	"golang.org/x/net/context"
+
+	"github.com/docker/docker/api/client"
+	"github.com/docker/docker/cli"
+	"github.com/docker/docker/opts"
+	"github.com/docker/docker/pkg/stringid"
+	"github.com/docker/engine-api/types"
+	"github.com/docker/engine-api/types/swarm"
+	"github.com/spf13/cobra"
+)
+
+const (
+	listItemFmt = "%s\t%s\t%s\t%s\t%s\n"
+)
+
+type listOptions struct {
+	quiet  bool
+	filter opts.FilterOpt
+}
+
+func newListCommand(dockerCli *client.DockerCli) *cobra.Command {
+	opts := listOptions{filter: opts.NewFilterOpt()}
+
+	cmd := &cobra.Command{
+		Use:     "ls",
+		Aliases: []string{"list"},
+		Short:   "List services",
+		Args:    cli.NoArgs,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runList(dockerCli, opts)
+		},
+	}
+
+	flags := cmd.Flags()
+	flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs")
+	flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided")
+
+	return cmd
+}
+
+func runList(dockerCli *client.DockerCli, opts listOptions) error {
+	client := dockerCli.Client()
+
+	services, err := client.ServiceList(
+		context.Background(),
+		types.ServiceListOptions{Filter: opts.filter.Value()})
+	if err != nil {
+		return err
+	}
+
+	out := dockerCli.Out()
+	if opts.quiet {
+		printQuiet(out, services)
+	} else {
+		printTable(out, services)
+	}
+	return nil
+}
+
+func printTable(out io.Writer, services []swarm.Service) {
+	writer := tabwriter.NewWriter(out, 0, 4, 2, ' ', 0)
+
+	// Ignore flushing errors
+	defer writer.Flush()
+
+	fmt.Fprintf(writer, listItemFmt, "ID", "NAME", "SCALE", "IMAGE", "COMMAND")
+	for _, service := range services {
+		scale := ""
+		if service.Spec.Mode.Replicated != nil && service.Spec.Mode.Replicated.Replicas != nil {
+			scale = fmt.Sprintf("%d", *service.Spec.Mode.Replicated.Replicas)
+		} else if service.Spec.Mode.Global != nil {
+			scale = "global"
+		}
+		fmt.Fprintf(
+			writer,
+			listItemFmt,
+			stringid.TruncateID(service.ID),
+			service.Spec.Name,
+			scale,
+			service.Spec.TaskTemplate.ContainerSpec.Image,
+			strings.Join(service.Spec.TaskTemplate.ContainerSpec.Args, " "))
+	}
+}
+
+func printQuiet(out io.Writer, services []swarm.Service) {
+	for _, service := range services {
+		fmt.Fprintln(out, service.ID)
+	}
+}

+ 462 - 0
api/client/service/opts.go

@@ -0,0 +1,462 @@
+package service
+
+import (
+	"encoding/csv"
+	"fmt"
+	"math/big"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/docker/docker/opts"
+	runconfigopts "github.com/docker/docker/runconfig/opts"
+	"github.com/docker/engine-api/types/swarm"
+	"github.com/docker/go-connections/nat"
+	units "github.com/docker/go-units"
+	"github.com/spf13/cobra"
+)
+
+var (
+	// DefaultReplicas is the default replicas to use for a replicated service
+	DefaultReplicas uint64 = 1
+)
+
+type int64Value interface {
+	Value() int64
+}
+
+type memBytes int64
+
+func (m *memBytes) String() string {
+	return strconv.FormatInt(m.Value(), 10)
+}
+
+func (m *memBytes) Set(value string) error {
+	val, err := units.RAMInBytes(value)
+	*m = memBytes(val)
+	return err
+}
+
+func (m *memBytes) Type() string {
+	return "MemoryBytes"
+}
+
+func (m *memBytes) Value() int64 {
+	return int64(*m)
+}
+
+type nanoCPUs int64
+
+func (c *nanoCPUs) String() string {
+	return strconv.FormatInt(c.Value(), 10)
+}
+
+func (c *nanoCPUs) Set(value string) error {
+	cpu, ok := new(big.Rat).SetString(value)
+	if !ok {
+		return fmt.Errorf("Failed to parse %v as a rational number", value)
+	}
+	nano := cpu.Mul(cpu, big.NewRat(1e9, 1))
+	if !nano.IsInt() {
+		return fmt.Errorf("value is too precise")
+	}
+	*c = nanoCPUs(nano.Num().Int64())
+	return nil
+}
+
+func (c *nanoCPUs) Type() string {
+	return "NanoCPUs"
+}
+
+func (c *nanoCPUs) Value() int64 {
+	return int64(*c)
+}
+
+// DurationOpt is an option type for time.Duration that uses a pointer. This
+// allows us to get nil values outside, instead of defaulting to 0
+type DurationOpt struct {
+	value *time.Duration
+}
+
+// Set a new value on the option
+func (d *DurationOpt) Set(s string) error {
+	v, err := time.ParseDuration(s)
+	d.value = &v
+	return err
+}
+
+// Type returns the type of this option
+func (d *DurationOpt) Type() string {
+	return "duration-ptr"
+}
+
+// String returns a string repr of this option
+func (d *DurationOpt) String() string {
+	if d.value != nil {
+		return d.value.String()
+	}
+	return "none"
+}
+
+// Value returns the time.Duration
+func (d *DurationOpt) Value() *time.Duration {
+	return d.value
+}
+
+// Uint64Opt represents a uint64.
+type Uint64Opt struct {
+	value *uint64
+}
+
+// Set a new value on the option
+func (i *Uint64Opt) Set(s string) error {
+	v, err := strconv.ParseUint(s, 0, 64)
+	i.value = &v
+	return err
+}
+
+// Type returns the type of this option
+func (i *Uint64Opt) Type() string {
+	return "uint64-ptr"
+}
+
+// String returns a string repr of this option
+func (i *Uint64Opt) String() string {
+	if i.value != nil {
+		return fmt.Sprintf("%v", *i.value)
+	}
+	return "none"
+}
+
+// Value returns the uint64
+func (i *Uint64Opt) Value() *uint64 {
+	return i.value
+}
+
+// MountOpt is a Value type for parsing mounts
+type MountOpt struct {
+	values []swarm.Mount
+}
+
+// Set a new mount value
+func (m *MountOpt) Set(value string) error {
+	csvReader := csv.NewReader(strings.NewReader(value))
+	fields, err := csvReader.Read()
+	if err != nil {
+		return err
+	}
+
+	mount := swarm.Mount{}
+
+	volumeOptions := func() *swarm.VolumeOptions {
+		if mount.VolumeOptions == nil {
+			mount.VolumeOptions = &swarm.VolumeOptions{
+				Labels: make(map[string]string),
+			}
+		}
+		return mount.VolumeOptions
+	}
+
+	setValueOnMap := func(target map[string]string, value string) {
+		parts := strings.SplitN(value, "=", 2)
+		if len(parts) == 1 {
+			target[value] = ""
+		} else {
+			target[parts[0]] = parts[1]
+		}
+	}
+
+	for _, field := range fields {
+		parts := strings.SplitN(field, "=", 2)
+		if len(parts) == 1 && strings.ToLower(parts[0]) == "writable" {
+			mount.Writable = true
+			continue
+		}
+
+		if len(parts) != 2 {
+			return fmt.Errorf("invald field '%s' must be a key=value pair", field)
+		}
+
+		key, value := parts[0], parts[1]
+		switch strings.ToLower(key) {
+		case "type":
+			mount.Type = swarm.MountType(strings.ToUpper(value))
+		case "source":
+			mount.Source = value
+		case "target":
+			mount.Target = value
+		case "writable":
+			mount.Writable, err = strconv.ParseBool(value)
+			if err != nil {
+				return fmt.Errorf("invald value for writable: %s", err.Error())
+			}
+		case "bind-propagation":
+			mount.BindOptions.Propagation = swarm.MountPropagation(strings.ToUpper(value))
+		case "volume-populate":
+			volumeOptions().Populate, err = strconv.ParseBool(value)
+			if err != nil {
+				return fmt.Errorf("invald value for populate: %s", err.Error())
+			}
+		case "volume-label":
+			setValueOnMap(volumeOptions().Labels, value)
+		case "volume-driver":
+			volumeOptions().DriverConfig.Name = value
+		case "volume-driver-opt":
+			if volumeOptions().DriverConfig.Options == nil {
+				volumeOptions().DriverConfig.Options = make(map[string]string)
+			}
+			setValueOnMap(volumeOptions().DriverConfig.Options, value)
+		default:
+			return fmt.Errorf("unexpected key '%s' in '%s'", key, value)
+		}
+	}
+
+	if mount.Type == "" {
+		return fmt.Errorf("type is required")
+	}
+
+	if mount.Target == "" {
+		return fmt.Errorf("target is required")
+	}
+
+	m.values = append(m.values, mount)
+	return nil
+}
+
+// Type returns the type of this option
+func (m *MountOpt) Type() string {
+	return "mount"
+}
+
+// String returns a string repr of this option
+func (m *MountOpt) String() string {
+	mounts := []string{}
+	for _, mount := range m.values {
+		mounts = append(mounts, fmt.Sprintf("%v", mount))
+	}
+	return strings.Join(mounts, ", ")
+}
+
+// Value returns the mounts
+func (m *MountOpt) Value() []swarm.Mount {
+	return m.values
+}
+
+type updateOptions struct {
+	parallelism uint64
+	delay       time.Duration
+}
+
+type resourceOptions struct {
+	limitCPU      nanoCPUs
+	limitMemBytes memBytes
+	resCPU        nanoCPUs
+	resMemBytes   memBytes
+}
+
+func (r *resourceOptions) ToResourceRequirements() *swarm.ResourceRequirements {
+	return &swarm.ResourceRequirements{
+		Limits: &swarm.Resources{
+			NanoCPUs:    r.limitCPU.Value(),
+			MemoryBytes: r.limitMemBytes.Value(),
+		},
+		Reservations: &swarm.Resources{
+			NanoCPUs:    r.resCPU.Value(),
+			MemoryBytes: r.resMemBytes.Value(),
+		},
+	}
+}
+
+type restartPolicyOptions struct {
+	condition   string
+	delay       DurationOpt
+	maxAttempts Uint64Opt
+	window      DurationOpt
+}
+
+func (r *restartPolicyOptions) ToRestartPolicy() *swarm.RestartPolicy {
+	return &swarm.RestartPolicy{
+		Condition:   swarm.RestartPolicyCondition(r.condition),
+		Delay:       r.delay.Value(),
+		MaxAttempts: r.maxAttempts.Value(),
+		Window:      r.window.Value(),
+	}
+}
+
+func convertNetworks(networks []string) []swarm.NetworkAttachmentConfig {
+	nets := []swarm.NetworkAttachmentConfig{}
+	for _, network := range networks {
+		nets = append(nets, swarm.NetworkAttachmentConfig{Target: network})
+	}
+	return nets
+}
+
+type endpointOptions struct {
+	mode  string
+	ports opts.ListOpts
+}
+
+func (e *endpointOptions) ToEndpointSpec() *swarm.EndpointSpec {
+	portConfigs := []swarm.PortConfig{}
+	// We can ignore errors because the format was already validated by ValidatePort
+	ports, portBindings, _ := nat.ParsePortSpecs(e.ports.GetAll())
+
+	for port := range ports {
+		portConfigs = append(portConfigs, convertPortToPortConfig(port, portBindings)...)
+	}
+
+	return &swarm.EndpointSpec{
+		Mode:  swarm.ResolutionMode(e.mode),
+		Ports: portConfigs,
+	}
+}
+
+func convertPortToPortConfig(
+	port nat.Port,
+	portBindings map[nat.Port][]nat.PortBinding,
+) []swarm.PortConfig {
+	ports := []swarm.PortConfig{}
+
+	for _, binding := range portBindings[port] {
+		hostPort, _ := strconv.ParseUint(binding.HostPort, 10, 16)
+		ports = append(ports, swarm.PortConfig{
+			//TODO Name: ?
+			Protocol:      swarm.PortConfigProtocol(strings.ToLower(port.Proto())),
+			TargetPort:    uint32(port.Int()),
+			PublishedPort: uint32(hostPort),
+		})
+	}
+	return ports
+}
+
+// ValidatePort validates a string is in the expected format for a port definition
+func ValidatePort(value string) (string, error) {
+	portMappings, err := nat.ParsePortSpec(value)
+	for _, portMapping := range portMappings {
+		if portMapping.Binding.HostIP != "" {
+			return "", fmt.Errorf("HostIP is not supported by a service.")
+		}
+	}
+	return value, err
+}
+
+type serviceOptions struct {
+	name    string
+	labels  opts.ListOpts
+	image   string
+	command []string
+	args    []string
+	env     opts.ListOpts
+	workdir string
+	user    string
+	mounts  MountOpt
+
+	resources resourceOptions
+	stopGrace DurationOpt
+
+	replicas Uint64Opt
+	mode     string
+
+	restartPolicy restartPolicyOptions
+	constraints   []string
+	update        updateOptions
+	networks      []string
+	endpoint      endpointOptions
+}
+
+func newServiceOptions() *serviceOptions {
+	return &serviceOptions{
+		labels: opts.NewListOpts(runconfigopts.ValidateEnv),
+		env:    opts.NewListOpts(runconfigopts.ValidateEnv),
+		endpoint: endpointOptions{
+			ports: opts.NewListOpts(ValidatePort),
+		},
+	}
+}
+
+func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) {
+	var service swarm.ServiceSpec
+
+	service = swarm.ServiceSpec{
+		Annotations: swarm.Annotations{
+			Name:   opts.name,
+			Labels: runconfigopts.ConvertKVStringsToMap(opts.labels.GetAll()),
+		},
+		TaskTemplate: swarm.TaskSpec{
+			ContainerSpec: swarm.ContainerSpec{
+				Image:           opts.image,
+				Command:         opts.command,
+				Args:            opts.args,
+				Env:             opts.env.GetAll(),
+				Dir:             opts.workdir,
+				User:            opts.user,
+				Mounts:          opts.mounts.Value(),
+				StopGracePeriod: opts.stopGrace.Value(),
+			},
+			Resources:     opts.resources.ToResourceRequirements(),
+			RestartPolicy: opts.restartPolicy.ToRestartPolicy(),
+			Placement: &swarm.Placement{
+				Constraints: opts.constraints,
+			},
+		},
+		Mode: swarm.ServiceMode{},
+		UpdateConfig: &swarm.UpdateConfig{
+			Parallelism: opts.update.parallelism,
+			Delay:       opts.update.delay,
+		},
+		Networks:     convertNetworks(opts.networks),
+		EndpointSpec: opts.endpoint.ToEndpointSpec(),
+	}
+
+	switch opts.mode {
+	case "global":
+		if opts.replicas.Value() != nil {
+			return service, fmt.Errorf("replicas can only be used with replicated mode")
+		}
+
+		service.Mode.Global = &swarm.GlobalService{}
+	case "replicated":
+		service.Mode.Replicated = &swarm.ReplicatedService{
+			Replicas: opts.replicas.Value(),
+		}
+	default:
+		return service, fmt.Errorf("Unknown mode: %s", opts.mode)
+	}
+	return service, nil
+}
+
+// addServiceFlags adds all flags that are common to both `create` and `update.
+// Any flags that are not common are added separately in the individual command
+func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) {
+	flags := cmd.Flags()
+	flags.StringVar(&opts.name, "name", "", "Service name")
+	flags.VarP(&opts.labels, "label", "l", "Service labels")
+
+	flags.VarP(&opts.env, "env", "e", "Set environment variables")
+	flags.StringVarP(&opts.workdir, "workdir", "w", "", "Working directory inside the container")
+	flags.StringVarP(&opts.user, "user", "u", "", "Username or UID")
+	flags.VarP(&opts.mounts, "mount", "m", "Attach a mount to the service")
+
+	flags.Var(&opts.resources.limitCPU, "limit-cpu", "Limit CPUs")
+	flags.Var(&opts.resources.limitMemBytes, "limit-memory", "Limit Memory")
+	flags.Var(&opts.resources.resCPU, "reserve-cpu", "Reserve CPUs")
+	flags.Var(&opts.resources.resMemBytes, "reserve-memory", "Reserve Memory")
+	flags.Var(&opts.stopGrace, "stop-grace-period", "Time to wait before force killing a container")
+
+	flags.StringVar(&opts.mode, "mode", "replicated", "Service mode (replicated or global)")
+	flags.Var(&opts.replicas, "replicas", "Number of tasks")
+
+	flags.StringVar(&opts.restartPolicy.condition, "restart-condition", "", "Restart when condition is met (none, on_failure, or any)")
+	flags.Var(&opts.restartPolicy.delay, "restart-delay", "Delay between restart attempts")
+	flags.Var(&opts.restartPolicy.maxAttempts, "restart-max-attempts", "Maximum number of restarts before giving up")
+	flags.Var(&opts.restartPolicy.window, "restart-window", "Window used to evalulate the restart policy")
+
+	flags.StringSliceVar(&opts.constraints, "constraint", []string{}, "Placement constraints")
+
+	flags.Uint64Var(&opts.update.parallelism, "update-parallelism", 1, "Maximum number of tasks updated simultaneously")
+	flags.DurationVar(&opts.update.delay, "update-delay", time.Duration(0), "Delay between updates")
+
+	flags.StringSliceVar(&opts.networks, "network", []string{}, "Network attachments")
+	flags.StringVar(&opts.endpoint.mode, "endpoint-mode", "", "Endpoint mode(Valid values: VIP, DNSRR)")
+	flags.VarP(&opts.endpoint.ports, "publish", "p", "Publish a port as a node port")
+}

+ 47 - 0
api/client/service/remove.go

@@ -0,0 +1,47 @@
+package service
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/docker/docker/api/client"
+	"github.com/docker/docker/cli"
+	"github.com/spf13/cobra"
+	"golang.org/x/net/context"
+)
+
+func newRemoveCommand(dockerCli *client.DockerCli) *cobra.Command {
+
+	cmd := &cobra.Command{
+		Use:     "rm [OPTIONS] SERVICE",
+		Aliases: []string{"remove"},
+		Short:   "Remove a service",
+		Args:    cli.RequiresMinArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runRemove(dockerCli, args)
+		},
+	}
+	cmd.Flags()
+
+	return cmd
+}
+
+func runRemove(dockerCli *client.DockerCli, sids []string) error {
+	client := dockerCli.Client()
+
+	ctx := context.Background()
+
+	var errs []string
+	for _, sid := range sids {
+		err := client.ServiceRemove(ctx, sid)
+		if err != nil {
+			errs = append(errs, err.Error())
+			continue
+		}
+		fmt.Fprintf(dockerCli.Out(), "%s\n", sid)
+	}
+	if len(errs) > 0 {
+		return fmt.Errorf(strings.Join(errs, "\n"))
+	}
+	return nil
+}

+ 86 - 0
api/client/service/scale.go

@@ -0,0 +1,86 @@
+package service
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+
+	"golang.org/x/net/context"
+
+	"github.com/docker/docker/api/client"
+	"github.com/docker/docker/cli"
+	"github.com/spf13/cobra"
+)
+
+func newScaleCommand(dockerCli *client.DockerCli) *cobra.Command {
+	return &cobra.Command{
+		Use:   "scale SERVICE=SCALE [SERVICE=SCALE...]",
+		Short: "Scale one or multiple services",
+		Args:  scaleArgs,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runScale(dockerCli, args)
+		},
+	}
+}
+
+func scaleArgs(cmd *cobra.Command, args []string) error {
+	if err := cli.RequiresMinArgs(1)(cmd, args); err != nil {
+		return err
+	}
+	for _, arg := range args {
+		if parts := strings.SplitN(arg, "=", 2); len(parts) != 2 {
+			return fmt.Errorf(
+				"Invalid scale specifier '%s'.\nSee '%s --help'.\n\nUsage:  %s\n\n%s",
+				arg,
+				cmd.CommandPath(),
+				cmd.UseLine(),
+				cmd.Short,
+			)
+		}
+	}
+	return nil
+}
+
+func runScale(dockerCli *client.DockerCli, args []string) error {
+	var errors []string
+	for _, arg := range args {
+		parts := strings.SplitN(arg, "=", 2)
+		serviceID, scale := parts[0], parts[1]
+		if err := runServiceScale(dockerCli, serviceID, scale); err != nil {
+			errors = append(errors, fmt.Sprintf("%s: %s", serviceID, err.Error()))
+		}
+	}
+
+	if len(errors) == 0 {
+		return nil
+	}
+	return fmt.Errorf(strings.Join(errors, "\n"))
+}
+
+func runServiceScale(dockerCli *client.DockerCli, serviceID string, scale string) error {
+	client := dockerCli.Client()
+	ctx := context.Background()
+
+	service, err := client.ServiceInspect(ctx, serviceID)
+	if err != nil {
+		return err
+	}
+
+	serviceMode := &service.Spec.Mode
+	if serviceMode.Replicated == nil {
+		return fmt.Errorf("scale can only be used with replicated mode")
+	}
+	uintScale, err := strconv.ParseUint(scale, 10, 64)
+	if err != nil {
+		return fmt.Errorf("invalid replicas value %s: %s", scale, err.Error())
+	}
+	serviceMode.Replicated.Replicas = &uintScale
+
+	err = client.ServiceUpdate(ctx, service.ID, service.Version, service.Spec)
+	if err != nil {
+		return err
+	}
+
+	fmt.Fprintf(dockerCli.Out(), "%s scaled to %s\n", serviceID, scale)
+	return nil
+}

+ 65 - 0
api/client/service/tasks.go

@@ -0,0 +1,65 @@
+package service
+
+import (
+	"golang.org/x/net/context"
+
+	"github.com/docker/docker/api/client"
+	"github.com/docker/docker/api/client/idresolver"
+	"github.com/docker/docker/api/client/task"
+	"github.com/docker/docker/cli"
+	"github.com/docker/docker/opts"
+	"github.com/docker/engine-api/types"
+	"github.com/docker/engine-api/types/swarm"
+	"github.com/spf13/cobra"
+)
+
+type tasksOptions struct {
+	serviceID string
+	all       bool
+	noResolve bool
+	filter    opts.FilterOpt
+}
+
+func newTasksCommand(dockerCli *client.DockerCli) *cobra.Command {
+	opts := tasksOptions{filter: opts.NewFilterOpt()}
+
+	cmd := &cobra.Command{
+		Use:   "tasks [OPTIONS] SERVICE",
+		Short: "List the tasks of a service",
+		Args:  cli.ExactArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			opts.serviceID = args[0]
+			return runTasks(dockerCli, opts)
+		},
+	}
+	flags := cmd.Flags()
+	flags.BoolVarP(&opts.all, "all", "a", false, "Display all tasks")
+	flags.BoolVarP(&opts.noResolve, "no-resolve", "n", false, "Do not map IDs to Names")
+	flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided")
+
+	return cmd
+}
+
+func runTasks(dockerCli *client.DockerCli, opts tasksOptions) error {
+	client := dockerCli.Client()
+	ctx := context.Background()
+
+	service, err := client.ServiceInspect(ctx, opts.serviceID)
+	if err != nil {
+		return err
+	}
+
+	filter := opts.filter.Value()
+	filter.Add("service", service.ID)
+	if !opts.all && !filter.Include("desired_state") {
+		filter.Add("desired_state", string(swarm.TaskStateRunning))
+		filter.Add("desired_state", string(swarm.TaskStateAccepted))
+	}
+
+	tasks, err := client.TaskList(ctx, types.TaskListOptions{Filter: filter})
+	if err != nil {
+		return err
+	}
+
+	return task.Print(dockerCli, ctx, tasks, idresolver.New(client, opts.noResolve))
+}

+ 244 - 0
api/client/service/update.go

@@ -0,0 +1,244 @@
+package service
+
+import (
+	"fmt"
+	"time"
+
+	"golang.org/x/net/context"
+
+	"github.com/docker/docker/api/client"
+	"github.com/docker/docker/cli"
+	"github.com/docker/docker/opts"
+	runconfigopts "github.com/docker/docker/runconfig/opts"
+	"github.com/docker/engine-api/types/swarm"
+	"github.com/docker/go-connections/nat"
+	"github.com/spf13/cobra"
+	"github.com/spf13/pflag"
+)
+
+func newUpdateCommand(dockerCli *client.DockerCli) *cobra.Command {
+	opts := newServiceOptions()
+	var flags *pflag.FlagSet
+
+	cmd := &cobra.Command{
+		Use:   "update [OPTIONS] SERVICE",
+		Short: "Update a service",
+		Args:  cli.ExactArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runUpdate(dockerCli, flags, args[0])
+		},
+	}
+
+	flags = cmd.Flags()
+	flags.String("image", "", "Service image tag")
+	flags.StringSlice("command", []string{}, "Service command")
+	flags.StringSlice("arg", []string{}, "Service command args")
+	addServiceFlags(cmd, opts)
+	return cmd
+}
+
+func runUpdate(dockerCli *client.DockerCli, flags *pflag.FlagSet, serviceID string) error {
+	client := dockerCli.Client()
+	ctx := context.Background()
+
+	service, err := client.ServiceInspect(ctx, serviceID)
+	if err != nil {
+		return err
+	}
+
+	err = mergeService(&service.Spec, flags)
+	if err != nil {
+		return err
+	}
+	err = client.ServiceUpdate(ctx, service.ID, service.Version, service.Spec)
+	if err != nil {
+		return err
+	}
+
+	fmt.Fprintf(dockerCli.Out(), "%s\n", serviceID)
+	return nil
+}
+
+func mergeService(spec *swarm.ServiceSpec, flags *pflag.FlagSet) error {
+
+	mergeString := func(flag string, field *string) {
+		if flags.Changed(flag) {
+			*field, _ = flags.GetString(flag)
+		}
+	}
+
+	mergeListOpts := func(flag string, field *[]string) {
+		if flags.Changed(flag) {
+			value := flags.Lookup(flag).Value.(*opts.ListOpts)
+			*field = value.GetAll()
+		}
+	}
+
+	mergeSlice := func(flag string, field *[]string) {
+		if flags.Changed(flag) {
+			*field, _ = flags.GetStringSlice(flag)
+		}
+	}
+
+	mergeInt64Value := func(flag string, field *int64) {
+		if flags.Changed(flag) {
+			*field = flags.Lookup(flag).Value.(int64Value).Value()
+		}
+	}
+
+	mergeDuration := func(flag string, field *time.Duration) {
+		if flags.Changed(flag) {
+			*field, _ = flags.GetDuration(flag)
+		}
+	}
+
+	mergeDurationOpt := func(flag string, field *time.Duration) {
+		if flags.Changed(flag) {
+			*field = *flags.Lookup(flag).Value.(*DurationOpt).Value()
+		}
+	}
+
+	mergeUint64 := func(flag string, field *uint64) {
+		if flags.Changed(flag) {
+			*field, _ = flags.GetUint64(flag)
+		}
+	}
+
+	mergeUint64Opt := func(flag string, field *uint64) {
+		if flags.Changed(flag) {
+			*field = *flags.Lookup(flag).Value.(*Uint64Opt).Value()
+		}
+	}
+
+	cspec := &spec.TaskTemplate.ContainerSpec
+	task := &spec.TaskTemplate
+	mergeString("name", &spec.Name)
+	mergeLabels(flags, &spec.Labels)
+	mergeString("image", &cspec.Image)
+	mergeSlice("command", &cspec.Command)
+	mergeSlice("arg", &cspec.Command)
+	mergeListOpts("env", &cspec.Env)
+	mergeString("workdir", &cspec.Dir)
+	mergeString("user", &cspec.User)
+	mergeMounts(flags, &cspec.Mounts)
+
+	mergeInt64Value("limit-cpu", &task.Resources.Limits.NanoCPUs)
+	mergeInt64Value("limit-memory", &task.Resources.Limits.MemoryBytes)
+	mergeInt64Value("reserve-cpu", &task.Resources.Reservations.NanoCPUs)
+	mergeInt64Value("reserve-memory", &task.Resources.Reservations.MemoryBytes)
+
+	mergeDurationOpt("stop-grace-period", cspec.StopGracePeriod)
+
+	if flags.Changed("restart-policy-condition") {
+		value, _ := flags.GetString("restart-policy-condition")
+		task.RestartPolicy.Condition = swarm.RestartPolicyCondition(value)
+	}
+	mergeDurationOpt("restart-policy-delay", task.RestartPolicy.Delay)
+	mergeUint64Opt("restart-policy-max-attempts", task.RestartPolicy.MaxAttempts)
+	mergeDurationOpt("restart-policy-window", task.RestartPolicy.Window)
+	mergeSlice("constraint", &task.Placement.Constraints)
+
+	if err := mergeMode(flags, &spec.Mode); err != nil {
+		return err
+	}
+
+	mergeUint64("updateconfig-parallelism", &spec.UpdateConfig.Parallelism)
+	mergeDuration("updateconfig-delay", &spec.UpdateConfig.Delay)
+
+	mergeNetworks(flags, &spec.Networks)
+	if flags.Changed("endpoint-mode") {
+		value, _ := flags.GetString("endpoint-mode")
+		spec.EndpointSpec.Mode = swarm.ResolutionMode(value)
+	}
+
+	mergePorts(flags, &spec.EndpointSpec.Ports)
+
+	return nil
+}
+
+func mergeLabels(flags *pflag.FlagSet, field *map[string]string) {
+	if !flags.Changed("label") {
+		return
+	}
+
+	if *field == nil {
+		*field = make(map[string]string)
+	}
+
+	values := flags.Lookup("label").Value.(*opts.ListOpts).GetAll()
+	for key, value := range runconfigopts.ConvertKVStringsToMap(values) {
+		(*field)[key] = value
+	}
+}
+
+// TODO: should this override by destination path, or does swarm handle that?
+func mergeMounts(flags *pflag.FlagSet, mounts *[]swarm.Mount) {
+	if !flags.Changed("mount") {
+		return
+	}
+
+	values := flags.Lookup("mount").Value.(*MountOpt).Value()
+	*mounts = append(*mounts, values...)
+}
+
+// TODO: should this override by name, or does swarm handle that?
+func mergePorts(flags *pflag.FlagSet, portConfig *[]swarm.PortConfig) {
+	if !flags.Changed("ports") {
+		return
+	}
+
+	values := flags.Lookup("ports").Value.(*opts.ListOpts).GetAll()
+	ports, portBindings, _ := nat.ParsePortSpecs(values)
+
+	for port := range ports {
+		*portConfig = append(*portConfig, convertPortToPortConfig(port, portBindings)...)
+	}
+}
+
+func mergeNetworks(flags *pflag.FlagSet, attachments *[]swarm.NetworkAttachmentConfig) {
+	if !flags.Changed("network") {
+		return
+	}
+	networks, _ := flags.GetStringSlice("network")
+	for _, network := range networks {
+		*attachments = append(*attachments, swarm.NetworkAttachmentConfig{Target: network})
+	}
+}
+
+func mergeMode(flags *pflag.FlagSet, serviceMode *swarm.ServiceMode) error {
+	if !flags.Changed("mode") && !flags.Changed("scale") {
+		return nil
+	}
+
+	var mode string
+	if flags.Changed("mode") {
+		mode, _ = flags.GetString("mode")
+	}
+
+	if !(mode == "replicated" || serviceMode.Replicated != nil) && flags.Changed("replicas") {
+		return fmt.Errorf("replicas can only be used with replicated mode")
+	}
+
+	if mode == "global" {
+		serviceMode.Replicated = nil
+		serviceMode.Global = &swarm.GlobalService{}
+		return nil
+	}
+
+	if flags.Changed("replicas") {
+		replicas := flags.Lookup("replicas").Value.(*Uint64Opt).Value()
+		serviceMode.Replicated = &swarm.ReplicatedService{Replicas: replicas}
+		serviceMode.Global = nil
+		return nil
+	}
+
+	if mode == "replicated" {
+		if serviceMode.Replicated != nil {
+			return nil
+		}
+		serviceMode.Replicated = &swarm.ReplicatedService{Replicas: &DefaultReplicas}
+		serviceMode.Global = nil
+	}
+
+	return nil
+}

+ 30 - 0
api/client/swarm/cmd.go

@@ -0,0 +1,30 @@
+package swarm
+
+import (
+	"fmt"
+
+	"github.com/spf13/cobra"
+
+	"github.com/docker/docker/api/client"
+	"github.com/docker/docker/cli"
+)
+
+// NewSwarmCommand returns a cobra command for `swarm` subcommands
+func NewSwarmCommand(dockerCli *client.DockerCli) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "swarm",
+		Short: "Manage docker swarm",
+		Args:  cli.NoArgs,
+		Run: func(cmd *cobra.Command, args []string) {
+			fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
+		},
+	}
+	cmd.AddCommand(
+		newInitCommand(dockerCli),
+		newJoinCommand(dockerCli),
+		newUpdateCommand(dockerCli),
+		newLeaveCommand(dockerCli),
+		newInspectCommand(dockerCli),
+	)
+	return cmd
+}

+ 61 - 0
api/client/swarm/init.go

@@ -0,0 +1,61 @@
+package swarm
+
+import (
+	"fmt"
+
+	"golang.org/x/net/context"
+
+	"github.com/docker/docker/api/client"
+	"github.com/docker/docker/cli"
+	"github.com/docker/engine-api/types/swarm"
+	"github.com/spf13/cobra"
+)
+
+type initOptions struct {
+	listenAddr      NodeAddrOption
+	autoAccept      AutoAcceptOption
+	forceNewCluster bool
+	secret          string
+}
+
+func newInitCommand(dockerCli *client.DockerCli) *cobra.Command {
+	opts := initOptions{
+		listenAddr: NewNodeAddrOption(),
+		autoAccept: NewAutoAcceptOption(),
+	}
+
+	cmd := &cobra.Command{
+		Use:   "init",
+		Short: "Initialize a Swarm.",
+		Args:  cli.NoArgs,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runInit(dockerCli, opts)
+		},
+	}
+
+	flags := cmd.Flags()
+	flags.Var(&opts.listenAddr, "listen-addr", "Listen address")
+	flags.Var(&opts.autoAccept, "auto-accept", "Auto acceptance policy (worker, manager, or none)")
+	flags.StringVar(&opts.secret, "secret", "", "Set secret value needed to accept nodes into cluster")
+	flags.BoolVar(&opts.forceNewCluster, "force-new-cluster", false, "Force create a new cluster from current state.")
+	return cmd
+}
+
+func runInit(dockerCli *client.DockerCli, opts initOptions) error {
+	client := dockerCli.Client()
+	ctx := context.Background()
+
+	req := swarm.InitRequest{
+		ListenAddr:      opts.listenAddr.String(),
+		ForceNewCluster: opts.forceNewCluster,
+	}
+
+	req.Spec.AcceptancePolicy.Policies = opts.autoAccept.Policies(opts.secret)
+
+	nodeID, err := client.SwarmInit(ctx, req)
+	if err != nil {
+		return err
+	}
+	fmt.Printf("Swarm initialized: current node (%s) is now a manager.\n", nodeID)
+	return nil
+}

+ 56 - 0
api/client/swarm/inspect.go

@@ -0,0 +1,56 @@
+package swarm
+
+import (
+	"golang.org/x/net/context"
+
+	"github.com/docker/docker/api/client"
+	"github.com/docker/docker/api/client/inspect"
+	"github.com/docker/docker/cli"
+	"github.com/spf13/cobra"
+)
+
+type inspectOptions struct {
+	format string
+	//	pretty  bool
+}
+
+func newInspectCommand(dockerCli *client.DockerCli) *cobra.Command {
+	var opts inspectOptions
+
+	cmd := &cobra.Command{
+		Use:   "inspect [OPTIONS]",
+		Short: "Inspect the Swarm",
+		Args:  cli.NoArgs,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			// if opts.pretty && len(opts.format) > 0 {
+			//	return fmt.Errorf("--format is incompatible with human friendly format")
+			// }
+			return runInspect(dockerCli, opts)
+		},
+	}
+
+	flags := cmd.Flags()
+	flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template")
+	//flags.BoolVarP(&opts.pretty, "pretty", "h", false, "Print the information in a human friendly format.")
+	return cmd
+}
+
+func runInspect(dockerCli *client.DockerCli, opts inspectOptions) error {
+	client := dockerCli.Client()
+	ctx := context.Background()
+
+	swarm, err := client.SwarmInspect(ctx)
+	if err != nil {
+		return err
+	}
+
+	getRef := func(_ string) (interface{}, []byte, error) {
+		return swarm, nil, nil
+	}
+
+	//	if !opts.pretty {
+	return inspect.Inspect(dockerCli.Out(), []string{""}, opts.format, getRef)
+	//	}
+
+	//return printHumanFriendly(dockerCli.Out(), opts.refs, getRef)
+}

+ 65 - 0
api/client/swarm/join.go

@@ -0,0 +1,65 @@
+package swarm
+
+import (
+	"fmt"
+
+	"github.com/docker/docker/api/client"
+	"github.com/docker/docker/cli"
+	"github.com/docker/engine-api/types/swarm"
+	"github.com/spf13/cobra"
+	"golang.org/x/net/context"
+)
+
+type joinOptions struct {
+	remote     string
+	listenAddr NodeAddrOption
+	manager    bool
+	secret     string
+	CACertHash string
+}
+
+func newJoinCommand(dockerCli *client.DockerCli) *cobra.Command {
+	opts := joinOptions{
+		listenAddr: NodeAddrOption{addr: defaultListenAddr},
+	}
+
+	cmd := &cobra.Command{
+		Use:   "join [OPTIONS] HOST:PORT",
+		Short: "Join a Swarm as a node and/or manager.",
+		Args:  cli.ExactArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			opts.remote = args[0]
+			return runJoin(dockerCli, opts)
+		},
+	}
+
+	flags := cmd.Flags()
+	flags.Var(&opts.listenAddr, "listen-addr", "Listen address")
+	flags.BoolVar(&opts.manager, "manager", false, "Try joining as a manager.")
+	flags.StringVar(&opts.secret, "secret", "", "Secret for node acceptance")
+	flags.StringVar(&opts.CACertHash, "ca-hash", "", "Hash of the Root Certificate Authority certificate used for trusted join")
+	return cmd
+}
+
+func runJoin(dockerCli *client.DockerCli, opts joinOptions) error {
+	client := dockerCli.Client()
+	ctx := context.Background()
+
+	req := swarm.JoinRequest{
+		Manager:     opts.manager,
+		Secret:      opts.secret,
+		ListenAddr:  opts.listenAddr.String(),
+		RemoteAddrs: []string{opts.remote},
+		CACertHash:  opts.CACertHash,
+	}
+	err := client.SwarmJoin(ctx, req)
+	if err != nil {
+		return err
+	}
+	if opts.manager {
+		fmt.Fprintln(dockerCli.Out(), "This node joined a Swarm as a manager.")
+	} else {
+		fmt.Fprintln(dockerCli.Out(), "This node joined a Swarm as a worker.")
+	}
+	return nil
+}

+ 44 - 0
api/client/swarm/leave.go

@@ -0,0 +1,44 @@
+package swarm
+
+import (
+	"fmt"
+
+	"golang.org/x/net/context"
+
+	"github.com/docker/docker/api/client"
+	"github.com/docker/docker/cli"
+	"github.com/spf13/cobra"
+)
+
+type leaveOptions struct {
+	force bool
+}
+
+func newLeaveCommand(dockerCli *client.DockerCli) *cobra.Command {
+	opts := leaveOptions{}
+
+	cmd := &cobra.Command{
+		Use:   "leave",
+		Short: "Leave a Swarm.",
+		Args:  cli.NoArgs,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runLeave(dockerCli, opts)
+		},
+	}
+
+	flags := cmd.Flags()
+	flags.BoolVar(&opts.force, "force", false, "Force leave ignoring warnings.")
+	return cmd
+}
+
+func runLeave(dockerCli *client.DockerCli, opts leaveOptions) error {
+	client := dockerCli.Client()
+	ctx := context.Background()
+
+	if err := client.SwarmLeave(ctx, opts.force); err != nil {
+		return err
+	}
+
+	fmt.Fprintln(dockerCli.Out(), "Node left the default swarm.")
+	return nil
+}

+ 120 - 0
api/client/swarm/opts.go

@@ -0,0 +1,120 @@
+package swarm
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/docker/engine-api/types/swarm"
+)
+
+const (
+	defaultListenAddr = "0.0.0.0:2377"
+	// WORKER constant for worker name
+	WORKER = "WORKER"
+	// MANAGER constant for manager name
+	MANAGER = "MANAGER"
+)
+
+var (
+	defaultPolicies = []swarm.Policy{
+		{Role: WORKER, Autoaccept: true},
+		{Role: MANAGER, Autoaccept: false},
+	}
+)
+
+// NodeAddrOption is a pflag.Value for listen and remote addresses
+type NodeAddrOption struct {
+	addr string
+}
+
+// String prints the representation of this flag
+func (a *NodeAddrOption) String() string {
+	return a.addr
+}
+
+// Set the value for this flag
+func (a *NodeAddrOption) Set(value string) error {
+	if !strings.Contains(value, ":") {
+		return fmt.Errorf("Invalud url, a host and port are required")
+	}
+
+	parts := strings.Split(value, ":")
+	if len(parts) != 2 {
+		return fmt.Errorf("Invalud url, too many colons")
+	}
+
+	a.addr = value
+	return nil
+}
+
+// Type returns the type of this flag
+func (a *NodeAddrOption) Type() string {
+	return "node-addr"
+}
+
+// NewNodeAddrOption returns a new node address option
+func NewNodeAddrOption() NodeAddrOption {
+	return NodeAddrOption{addr: defaultListenAddr}
+}
+
+// AutoAcceptOption is a value type for auto-accept policy
+type AutoAcceptOption struct {
+	values map[string]bool
+}
+
+// String prints a string representation of this option
+func (o *AutoAcceptOption) String() string {
+	keys := []string{}
+	for key := range o.values {
+		keys = append(keys, key)
+	}
+	return strings.Join(keys, " ")
+}
+
+// Set sets a new value on this option
+func (o *AutoAcceptOption) Set(value string) error {
+	value = strings.ToUpper(value)
+	switch value {
+	case "", "NONE":
+		if accept, ok := o.values[WORKER]; ok && accept {
+			return fmt.Errorf("value NONE is incompatible with %s", WORKER)
+		}
+		if accept, ok := o.values[MANAGER]; ok && accept {
+			return fmt.Errorf("value NONE is incompatible with %s", MANAGER)
+		}
+		o.values[WORKER] = false
+		o.values[MANAGER] = false
+	case WORKER, MANAGER:
+		if accept, ok := o.values[value]; ok && !accept {
+			return fmt.Errorf("value NONE is incompatible with %s", value)
+		}
+		o.values[value] = true
+	default:
+		return fmt.Errorf("must be one of %s, %s, NONE", WORKER, MANAGER)
+	}
+
+	return nil
+}
+
+// Type returns the type of this option
+func (o *AutoAcceptOption) Type() string {
+	return "auto-accept"
+}
+
+// Policies returns a representation of this option for the api
+func (o *AutoAcceptOption) Policies(secret string) []swarm.Policy {
+	policies := []swarm.Policy{}
+	for _, p := range defaultPolicies {
+		if len(o.values) != 0 {
+			p.Autoaccept = o.values[string(p.Role)]
+		}
+		p.Secret = secret
+		policies = append(policies, p)
+	}
+	return policies
+}
+
+// NewAutoAcceptOption returns a new auto-accept option
+func NewAutoAcceptOption() AutoAcceptOption {
+	return AutoAcceptOption{values: make(map[string]bool)}
+}

+ 93 - 0
api/client/swarm/update.go

@@ -0,0 +1,93 @@
+package swarm
+
+import (
+	"fmt"
+
+	"golang.org/x/net/context"
+
+	"github.com/docker/docker/api/client"
+	"github.com/docker/docker/cli"
+	"github.com/docker/engine-api/types/swarm"
+	"github.com/spf13/cobra"
+	"github.com/spf13/pflag"
+)
+
+type updateOptions struct {
+	autoAccept       AutoAcceptOption
+	secret           string
+	taskHistoryLimit int64
+	heartbeatPeriod  uint64
+}
+
+func newUpdateCommand(dockerCli *client.DockerCli) *cobra.Command {
+	opts := updateOptions{autoAccept: NewAutoAcceptOption()}
+	var flags *pflag.FlagSet
+
+	cmd := &cobra.Command{
+		Use:   "update",
+		Short: "update the Swarm.",
+		Args:  cli.NoArgs,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runUpdate(dockerCli, flags, opts)
+		},
+	}
+
+	flags = cmd.Flags()
+	flags.Var(&opts.autoAccept, "auto-accept", "Auto acceptance policy (worker, manager or none)")
+	flags.StringVar(&opts.secret, "secret", "", "Set secret value needed to accept nodes into cluster")
+	flags.Int64Var(&opts.taskHistoryLimit, "task-history-limit", 10, "Task history retention limit")
+	flags.Uint64Var(&opts.heartbeatPeriod, "dispatcher-heartbeat-period", 5000000000, "Dispatcher heartbeat period")
+	return cmd
+}
+
+func runUpdate(dockerCli *client.DockerCli, flags *pflag.FlagSet, opts updateOptions) error {
+	client := dockerCli.Client()
+	ctx := context.Background()
+
+	swarm, err := client.SwarmInspect(ctx)
+	if err != nil {
+		return err
+	}
+
+	err = mergeSwarm(&swarm, flags)
+	if err != nil {
+		return err
+	}
+	err = client.SwarmUpdate(ctx, swarm.Version, swarm.Spec)
+	if err != nil {
+		return err
+	}
+
+	fmt.Println("Swarm updated.")
+	return nil
+}
+
+func mergeSwarm(swarm *swarm.Swarm, flags *pflag.FlagSet) error {
+	spec := &swarm.Spec
+
+	if flags.Changed("auto-accept") {
+		value := flags.Lookup("auto-accept").Value.(*AutoAcceptOption)
+		if len(spec.AcceptancePolicy.Policies) > 0 {
+			spec.AcceptancePolicy.Policies = value.Policies(spec.AcceptancePolicy.Policies[0].Secret)
+		} else {
+			spec.AcceptancePolicy.Policies = value.Policies("")
+		}
+	}
+
+	if flags.Changed("secret") {
+		secret, _ := flags.GetString("secret")
+		for _, policy := range spec.AcceptancePolicy.Policies {
+			policy.Secret = secret
+		}
+	}
+
+	if flags.Changed("task-history-limit") {
+		spec.Orchestration.TaskHistoryRetentionLimit, _ = flags.GetInt64("task-history-limit")
+	}
+
+	if flags.Changed("dispatcher-heartbeat-period") {
+		spec.Dispatcher.HeartbeatPeriod, _ = flags.GetUint64("dispatcher-heartbeat-period")
+	}
+
+	return nil
+}

+ 20 - 0
api/client/tag.go

@@ -0,0 +1,20 @@
+package client
+
+import (
+	"golang.org/x/net/context"
+
+	Cli "github.com/docker/docker/cli"
+	flag "github.com/docker/docker/pkg/mflag"
+)
+
+// CmdTag tags an image into a repository.
+//
+// Usage: docker tag [OPTIONS] IMAGE[:TAG] [REGISTRYHOST/][USERNAME/]NAME[:TAG]
+func (cli *DockerCli) CmdTag(args ...string) error {
+	cmd := Cli.Subcmd("tag", []string{"IMAGE[:TAG] [REGISTRYHOST/][USERNAME/]NAME[:TAG]"}, Cli.DockerCommands["tag"].Description, true)
+	cmd.Require(flag.Exact, 2)
+
+	cmd.ParseFlags(args, true)
+
+	return cli.client.ImageTag(context.Background(), cmd.Arg(0), cmd.Arg(1))
+}

+ 79 - 0
api/client/task/print.go

@@ -0,0 +1,79 @@
+package task
+
+import (
+	"fmt"
+	"sort"
+	"strings"
+	"text/tabwriter"
+	"time"
+
+	"golang.org/x/net/context"
+
+	"github.com/docker/docker/api/client"
+	"github.com/docker/docker/api/client/idresolver"
+	"github.com/docker/engine-api/types/swarm"
+	"github.com/docker/go-units"
+)
+
+const (
+	psTaskItemFmt = "%s\t%s\t%s\t%s\t%s %s\t%s\t%s\n"
+)
+
+type tasksBySlot []swarm.Task
+
+func (t tasksBySlot) Len() int {
+	return len(t)
+}
+
+func (t tasksBySlot) Swap(i, j int) {
+	t[i], t[j] = t[j], t[i]
+}
+
+func (t tasksBySlot) Less(i, j int) bool {
+	// Sort by slot.
+	if t[i].Slot != t[j].Slot {
+		return t[i].Slot < t[j].Slot
+	}
+
+	// If same slot, sort by most recent.
+	return t[j].Meta.CreatedAt.Before(t[i].CreatedAt)
+}
+
+// Print task information in a table format
+func Print(dockerCli *client.DockerCli, ctx context.Context, tasks []swarm.Task, resolver *idresolver.IDResolver) error {
+	sort.Stable(tasksBySlot(tasks))
+
+	writer := tabwriter.NewWriter(dockerCli.Out(), 0, 4, 2, ' ', 0)
+
+	// Ignore flushing errors
+	defer writer.Flush()
+	fmt.Fprintln(writer, strings.Join([]string{"ID", "NAME", "SERVICE", "IMAGE", "LAST STATE", "DESIRED STATE", "NODE"}, "\t"))
+	for _, task := range tasks {
+		serviceValue, err := resolver.Resolve(ctx, swarm.Service{}, task.ServiceID)
+		if err != nil {
+			return err
+		}
+		nodeValue, err := resolver.Resolve(ctx, swarm.Node{}, task.NodeID)
+		if err != nil {
+			return err
+		}
+		name := serviceValue
+		if task.Slot > 0 {
+			name = fmt.Sprintf("%s.%d", name, task.Slot)
+		}
+		fmt.Fprintf(
+			writer,
+			psTaskItemFmt,
+			task.ID,
+			name,
+			serviceValue,
+			task.Spec.ContainerSpec.Image,
+			client.PrettyPrint(task.Status.State),
+			units.HumanDuration(time.Since(task.Status.Timestamp)),
+			client.PrettyPrint(task.DesiredState),
+			nodeValue,
+		)
+	}
+
+	return nil
+}

+ 25 - 0
api/client/utils.go

@@ -8,6 +8,7 @@ import (
 	gosignal "os/signal"
 	"path/filepath"
 	"runtime"
+	"strings"
 	"time"
 
 	"golang.org/x/net/context"
@@ -163,3 +164,27 @@ func (cli *DockerCli) ForwardAllSignals(ctx context.Context, cid string) chan os
 	}()
 	return sigc
 }
+
+// capitalizeFirst capitalizes the first character of string
+func capitalizeFirst(s string) string {
+	switch l := len(s); l {
+	case 0:
+		return s
+	case 1:
+		return strings.ToLower(s)
+	default:
+		return strings.ToUpper(string(s[0])) + strings.ToLower(s[1:])
+	}
+}
+
+// PrettyPrint outputs arbitrary data for human formatted output by uppercasing the first letter.
+func PrettyPrint(i interface{}) string {
+	switch t := i.(type) {
+	case nil:
+		return "None"
+	case string:
+		return capitalizeFirst(t)
+	default:
+		return capitalizeFirst(fmt.Sprintf("%s", t))
+	}
+}

+ 6 - 0
cli/cobraadaptor/adaptor.go

@@ -5,7 +5,10 @@ import (
 	"github.com/docker/docker/api/client/container"
 	"github.com/docker/docker/api/client/image"
 	"github.com/docker/docker/api/client/network"
+	"github.com/docker/docker/api/client/node"
 	"github.com/docker/docker/api/client/registry"
+	"github.com/docker/docker/api/client/service"
+	"github.com/docker/docker/api/client/swarm"
 	"github.com/docker/docker/api/client/system"
 	"github.com/docker/docker/api/client/volume"
 	"github.com/docker/docker/cli"
@@ -36,6 +39,9 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor {
 	rootCmd.SetFlagErrorFunc(cli.FlagErrorFunc)
 	rootCmd.SetOutput(stdout)
 	rootCmd.AddCommand(
+		node.NewNodeCommand(dockerCli),
+		service.NewServiceCommand(dockerCli),
+		swarm.NewSwarmCommand(dockerCli),
 		container.NewAttachCommand(dockerCli),
 		container.NewCommitCommand(dockerCli),
 		container.NewCreateCommand(dockerCli),

+ 1 - 1
cli/usage.go

@@ -11,7 +11,7 @@ var DockerCommandUsage = []Command{
 	{"cp", "Copy files/folders between a container and the local filesystem"},
 	{"exec", "Run a command in a running container"},
 	{"info", "Display system-wide information"},
-	{"inspect", "Return low-level information on a container or image"},
+	{"inspect", "Return low-level information on a container, image or task"},
 	{"update", "Update configuration of one or more containers"},
 }
 

+ 1 - 1
integration-cli/docker_cli_rename_test.go

@@ -63,7 +63,7 @@ func (s *DockerSuite) TestRenameCheckNames(c *check.C) {
 
 	name, err := inspectFieldWithError("first_name", "Name")
 	c.Assert(err, checker.NotNil, check.Commentf(name))
-	c.Assert(err.Error(), checker.Contains, "No such image or container: first_name")
+	c.Assert(err.Error(), checker.Contains, "No such image, container or task: first_name")
 }
 
 func (s *DockerSuite) TestRenameInvalidName(c *check.C) {