Procházet zdrojové kódy

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 před 9 roky
rodič
revize
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) {