Переглянути джерело

Merge pull request #23361 from docker/swarm

Add dependency to docker/swarmkit
Tibor Vass 9 роки тому
батько
коміт
a1e319e847
100 змінених файлів з 9086 додано та 71 видалено
  1. 70 0
      api/client/idresolver/idresolver.go
  2. 16 0
      api/client/info.go
  3. 25 6
      api/client/inspect.go
  4. 6 4
      api/client/network/list.go
  5. 40 0
      api/client/node/accept.go
  6. 49 0
      api/client/node/cmd.go
  7. 40 0
      api/client/node/demote.go
  8. 141 0
      api/client/node/inspect.go
  9. 119 0
      api/client/node/list.go
  10. 50 0
      api/client/node/opts.go
  11. 40 0
      api/client/node/promote.go
  12. 36 0
      api/client/node/remove.go
  13. 72 0
      api/client/node/tasks.go
  14. 100 0
      api/client/node/update.go
  15. 32 0
      api/client/service/cmd.go
  16. 47 0
      api/client/service/create.go
  17. 127 0
      api/client/service/inspect.go
  18. 97 0
      api/client/service/list.go
  19. 462 0
      api/client/service/opts.go
  20. 47 0
      api/client/service/remove.go
  21. 86 0
      api/client/service/scale.go
  22. 65 0
      api/client/service/tasks.go
  23. 244 0
      api/client/service/update.go
  24. 30 0
      api/client/swarm/cmd.go
  25. 61 0
      api/client/swarm/init.go
  26. 56 0
      api/client/swarm/inspect.go
  27. 65 0
      api/client/swarm/join.go
  28. 44 0
      api/client/swarm/leave.go
  29. 120 0
      api/client/swarm/opts.go
  30. 93 0
      api/client/swarm/update.go
  31. 20 0
      api/client/tag.go
  32. 79 0
      api/client/task/print.go
  33. 25 0
      api/client/utils.go
  34. 3 1
      api/server/httputils/errors.go
  35. 1 2
      api/server/router/network/backend.go
  36. 98 0
      api/server/router/network/filter.go
  37. 10 5
      api/server/router/network/network.go
  38. 42 9
      api/server/router/network/network_routes.go
  39. 26 0
      api/server/router/swarm/backend.go
  40. 44 0
      api/server/router/swarm/cluster.go
  41. 229 0
      api/server/router/swarm/cluster_routes.go
  42. 10 5
      api/server/router/system/system.go
  43. 3 0
      api/server/router/system/system_routes.go
  44. 6 0
      cli/cobraadaptor/adaptor.go
  45. 1 1
      cli/usage.go
  46. 20 4
      cmd/dockerd/daemon.go
  47. 23 1
      container/container.go
  48. 28 0
      container/state.go
  49. 1056 0
      daemon/cluster/cluster.go
  50. 116 0
      daemon/cluster/convert/container.go
  51. 194 0
      daemon/cluster/convert/network.go
  52. 95 0
      daemon/cluster/convert/node.go
  53. 252 0
      daemon/cluster/convert/service.go
  54. 116 0
      daemon/cluster/convert/swarm.go
  55. 53 0
      daemon/cluster/convert/task.go
  56. 35 0
      daemon/cluster/executor/backend.go
  57. 229 0
      daemon/cluster/executor/container/adapter.go
  58. 415 0
      daemon/cluster/executor/container/container.go
  59. 305 0
      daemon/cluster/executor/container/controller.go
  60. 12 0
      daemon/cluster/executor/container/errors.go
  61. 139 0
      daemon/cluster/executor/container/executor.go
  62. 93 0
      daemon/cluster/filters.go
  63. 108 0
      daemon/cluster/helpers.go
  64. 36 0
      daemon/cluster/provider/network.go
  65. 2 1
      daemon/container.go
  66. 7 0
      daemon/container_operations.go
  67. 13 4
      daemon/create.go
  68. 12 0
      daemon/daemon.go
  69. 4 2
      daemon/inspect.go
  70. 1 1
      daemon/inspect_windows.go
  71. 11 0
      daemon/list.go
  72. 137 15
      daemon/network.go
  73. 2 0
      daemon/network/settings.go
  74. 16 1
      daemon/wait.go
  75. 1113 1
      docs/reference/api/docker_remote_api_v1.24.md
  76. 20 0
      docs/reference/commandline/index.md
  77. 6 1
      docs/reference/commandline/info.md
  78. 11 7
      docs/reference/commandline/inspect.md
  79. 28 0
      docs/reference/commandline/node_accept.md
  80. 28 0
      docs/reference/commandline/node_demote.md
  81. 108 0
      docs/reference/commandline/node_inspect.md
  82. 89 0
      docs/reference/commandline/node_ls.md
  83. 28 0
      docs/reference/commandline/node_promote.md
  84. 28 0
      docs/reference/commandline/node_reject.md
  85. 38 0
      docs/reference/commandline/node_rm.md
  86. 94 0
      docs/reference/commandline/node_tasks.md
  87. 26 0
      docs/reference/commandline/node_update.md
  88. 69 0
      docs/reference/commandline/swarm_init.md
  89. 68 0
      docs/reference/commandline/swarm_join.md
  90. 52 0
      docs/reference/commandline/swarm_leave.md
  91. 37 0
      docs/reference/commandline/swarm_update.md
  92. 79 0
      docs/swarm/index.md
  93. 85 0
      docs/swarm/key-concepts.md
  94. 21 0
      docs/swarm/menu.md
  95. 64 0
      docs/swarm/swarm-tutorial/add-nodes.md
  96. 77 0
      docs/swarm/swarm-tutorial/create-swarm.md
  97. 44 0
      docs/swarm/swarm-tutorial/delete-service.md
  98. 50 0
      docs/swarm/swarm-tutorial/deploy-service.md
  99. 129 0
      docs/swarm/swarm-tutorial/drain-node.md
  100. 87 0
      docs/swarm/swarm-tutorial/index.md

+ 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"
 	"github.com/docker/docker/pkg/ioutils"
 	flag "github.com/docker/docker/pkg/mflag"
 	flag "github.com/docker/docker/pkg/mflag"
 	"github.com/docker/docker/utils"
 	"github.com/docker/docker/utils"
+	"github.com/docker/engine-api/types/swarm"
 	"github.com/docker/go-units"
 	"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, "\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, "Kernel Version: %s\n", info.KernelVersion)
 	ioutils.FprintfIfNotEmpty(cli.out, "Operating System: %s\n", info.OperatingSystem)
 	ioutils.FprintfIfNotEmpty(cli.out, "Operating System: %s\n", info.OperatingSystem)
 	ioutils.FprintfIfNotEmpty(cli.out, "OSType: %s\n", info.OSType)
 	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"
 	"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 {
 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")
 	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")
 	size := cmd.Bool([]string{"s", "-size"}, false, "Display total file sizes if the type is container")
 	cmd.Require(flag.Min, 1)
 	cmd.Require(flag.Min, 1)
 
 
 	cmd.ParseFlags(args, true)
 	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)
 		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)
 		elementSearcher = cli.inspectContainers(ctx, *size)
 	case "image":
 	case "image":
 		elementSearcher = cli.inspectImages(ctx, *size)
 		elementSearcher = cli.inspectImages(ctx, *size)
+	case "task":
+		if *size {
+			fmt.Fprintln(cli.err, "WARNING: --size ignored for tasks")
+		}
+		elementSearcher = cli.inspectTasks(ctx)
 	default:
 	default:
 		elementSearcher = cli.inspectAll(ctx, *size)
 		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 {
 func (cli *DockerCli) inspectAll(ctx context.Context, getSize bool) inspect.GetRefFunc {
 	return func(ref string) (interface{}, []byte, error) {
 	return func(ref string) (interface{}, []byte, error) {
 		c, rawContainer, err := cli.client.ContainerInspectWithRaw(ctx, ref, getSize)
 		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)
 				i, rawImage, err := cli.client.ImageInspectWithRaw(ctx, ref, getSize)
 				if err != nil {
 				if err != nil {
 					if client.IsErrImageNotFound(err) {
 					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
 					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)
 	w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0)
 	if !opts.quiet {
 	if !opts.quiet {
-		fmt.Fprintf(w, "NETWORK ID\tNAME\tDRIVER")
+		fmt.Fprintf(w, "NETWORK ID\tNAME\tDRIVER\tSCOPE")
 		fmt.Fprintf(w, "\n")
 		fmt.Fprintf(w, "\n")
 	}
 	}
 
 
@@ -79,6 +79,8 @@ func runList(dockerCli *client.DockerCli, opts listOptions) error {
 	for _, networkResource := range networkResources {
 	for _, networkResource := range networkResources {
 		ID := networkResource.ID
 		ID := networkResource.ID
 		netName := networkResource.Name
 		netName := networkResource.Name
+		driver := networkResource.Driver
+		scope := networkResource.Scope
 		if !opts.noTrunc {
 		if !opts.noTrunc {
 			ID = stringid.TruncateID(ID)
 			ID = stringid.TruncateID(ID)
 		}
 		}
@@ -86,11 +88,11 @@ func runList(dockerCli *client.DockerCli, opts listOptions) error {
 			fmt.Fprintln(w, ID)
 			fmt.Fprintln(w, ID)
 			continue
 			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,
 			ID,
 			netName,
 			netName,
-			driver)
+			driver,
+			scope)
 		fmt.Fprint(w, "\n")
 		fmt.Fprint(w, "\n")
 	}
 	}
 	w.Flush()
 	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"
 	gosignal "os/signal"
 	"path/filepath"
 	"path/filepath"
 	"runtime"
 	"runtime"
+	"strings"
 	"time"
 	"time"
 
 
 	"golang.org/x/net/context"
 	"golang.org/x/net/context"
@@ -163,3 +164,27 @@ func (cli *DockerCli) ForwardAllSignals(ctx context.Context, cid string) chan os
 	}()
 	}()
 	return sigc
 	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))
+	}
+}

+ 3 - 1
api/server/httputils/errors.go

@@ -8,6 +8,7 @@ import (
 	"github.com/docker/engine-api/types"
 	"github.com/docker/engine-api/types"
 	"github.com/docker/engine-api/types/versions"
 	"github.com/docker/engine-api/types/versions"
 	"github.com/gorilla/mux"
 	"github.com/gorilla/mux"
+	"google.golang.org/grpc"
 )
 )
 
 
 // httpStatusError is an interface
 // httpStatusError is an interface
@@ -58,6 +59,7 @@ func GetHTTPErrorStatusCode(err error) int {
 			"wrong login/password":  http.StatusUnauthorized,
 			"wrong login/password":  http.StatusUnauthorized,
 			"unauthorized":          http.StatusUnauthorized,
 			"unauthorized":          http.StatusUnauthorized,
 			"hasn't been activated": http.StatusForbidden,
 			"hasn't been activated": http.StatusForbidden,
+			"this node":             http.StatusNotAcceptable,
 		} {
 		} {
 			if strings.Contains(errStr, keyword) {
 			if strings.Contains(errStr, keyword) {
 				statusCode = status
 				statusCode = status
@@ -85,7 +87,7 @@ func MakeErrorHandler(err error) http.HandlerFunc {
 			}
 			}
 			WriteJSON(w, statusCode, response)
 			WriteJSON(w, statusCode, response)
 		} else {
 		} else {
-			http.Error(w, err.Error(), statusCode)
+			http.Error(w, grpc.ErrorDesc(err), statusCode)
 		}
 		}
 	}
 	}
 }
 }

+ 1 - 2
api/server/router/network/backend.go

@@ -2,7 +2,6 @@ package network
 
 
 import (
 import (
 	"github.com/docker/engine-api/types"
 	"github.com/docker/engine-api/types"
-	"github.com/docker/engine-api/types/filters"
 	"github.com/docker/engine-api/types/network"
 	"github.com/docker/engine-api/types/network"
 	"github.com/docker/libnetwork"
 	"github.com/docker/libnetwork"
 )
 )
@@ -13,7 +12,7 @@ type Backend interface {
 	FindNetwork(idName string) (libnetwork.Network, error)
 	FindNetwork(idName string) (libnetwork.Network, error)
 	GetNetworkByName(idName string) (libnetwork.Network, error)
 	GetNetworkByName(idName string) (libnetwork.Network, error)
 	GetNetworksByID(partialID string) []libnetwork.Network
 	GetNetworksByID(partialID string) []libnetwork.Network
-	FilterNetworks(netFilters filters.Args) ([]libnetwork.Network, error)
+	GetNetworks() []libnetwork.Network
 	CreateNetwork(nc types.NetworkCreateRequest) (*types.NetworkCreateResponse, error)
 	CreateNetwork(nc types.NetworkCreateRequest) (*types.NetworkCreateResponse, error)
 	ConnectContainerToNetwork(containerName, networkName string, endpointConfig *network.EndpointSettings) error
 	ConnectContainerToNetwork(containerName, networkName string, endpointConfig *network.EndpointSettings) error
 	DisconnectContainerFromNetwork(containerName string, network libnetwork.Network, force bool) error
 	DisconnectContainerFromNetwork(containerName string, network libnetwork.Network, force bool) error

+ 98 - 0
api/server/router/network/filter.go

@@ -0,0 +1,98 @@
+package network
+
+import (
+	"fmt"
+
+	"github.com/docker/docker/runconfig"
+	"github.com/docker/engine-api/types"
+	"github.com/docker/engine-api/types/filters"
+)
+
+type filterHandler func([]types.NetworkResource, string) ([]types.NetworkResource, error)
+
+var (
+	// AcceptedFilters is an acceptable filters for validation
+	AcceptedFilters = map[string]bool{
+		"driver": true,
+		"type":   true,
+		"name":   true,
+		"id":     true,
+		"label":  true,
+	}
+)
+
+func filterNetworkByType(nws []types.NetworkResource, netType string) (retNws []types.NetworkResource, err error) {
+	switch netType {
+	case "builtin":
+		for _, nw := range nws {
+			if runconfig.IsPreDefinedNetwork(nw.Name) {
+				retNws = append(retNws, nw)
+			}
+		}
+	case "custom":
+		for _, nw := range nws {
+			if !runconfig.IsPreDefinedNetwork(nw.Name) {
+				retNws = append(retNws, nw)
+			}
+		}
+	default:
+		return nil, fmt.Errorf("Invalid filter: 'type'='%s'", netType)
+	}
+	return retNws, nil
+}
+
+// filterNetworks filters network list according to user specified filter
+// and returns user chosen networks
+func filterNetworks(nws []types.NetworkResource, filter filters.Args) ([]types.NetworkResource, error) {
+	// if filter is empty, return original network list
+	if filter.Len() == 0 {
+		return nws, nil
+	}
+
+	if err := filter.Validate(AcceptedFilters); err != nil {
+		return nil, err
+	}
+
+	var displayNet []types.NetworkResource
+	for _, nw := range nws {
+		if filter.Include("driver") {
+			if !filter.ExactMatch("driver", nw.Driver) {
+				continue
+			}
+		}
+		if filter.Include("name") {
+			if !filter.Match("name", nw.Name) {
+				continue
+			}
+		}
+		if filter.Include("id") {
+			if !filter.Match("id", nw.ID) {
+				continue
+			}
+		}
+		if filter.Include("label") {
+			if !filter.MatchKVList("label", nw.Labels) {
+				continue
+			}
+		}
+		displayNet = append(displayNet, nw)
+	}
+
+	if filter.Include("type") {
+		var typeNet []types.NetworkResource
+		errFilter := filter.WalkValues("type", func(fval string) error {
+			passList, err := filterNetworkByType(displayNet, fval)
+			if err != nil {
+				return err
+			}
+			typeNet = append(typeNet, passList...)
+			return nil
+		})
+		if errFilter != nil {
+			return nil, errFilter
+		}
+		displayNet = typeNet
+	}
+
+	return displayNet, nil
+}

+ 10 - 5
api/server/router/network/network.go

@@ -1,17 +1,22 @@
 package network
 package network
 
 
-import "github.com/docker/docker/api/server/router"
+import (
+	"github.com/docker/docker/api/server/router"
+	"github.com/docker/docker/daemon/cluster"
+)
 
 
 // networkRouter is a router to talk with the network controller
 // networkRouter is a router to talk with the network controller
 type networkRouter struct {
 type networkRouter struct {
-	backend Backend
-	routes  []router.Route
+	backend         Backend
+	clusterProvider *cluster.Cluster
+	routes          []router.Route
 }
 }
 
 
 // NewRouter initializes a new network router
 // NewRouter initializes a new network router
-func NewRouter(b Backend) router.Router {
+func NewRouter(b Backend, c *cluster.Cluster) router.Router {
 	r := &networkRouter{
 	r := &networkRouter{
-		backend: b,
+		backend:         b,
+		clusterProvider: c,
 	}
 	}
 	r.initRoutes()
 	r.initRoutes()
 	return r
 	return r

+ 42 - 9
api/server/router/network/network_routes.go

@@ -24,17 +24,30 @@ func (n *networkRouter) getNetworksList(ctx context.Context, w http.ResponseWrit
 		return err
 		return err
 	}
 	}
 
 
-	list := []*types.NetworkResource{}
+	list := []types.NetworkResource{}
 
 
-	nwList, err := n.backend.FilterNetworks(netFilters)
-	if err != nil {
-		return err
+	if nr, err := n.clusterProvider.GetNetworks(); err == nil {
+		for _, nw := range nr {
+			list = append(list, nw)
+		}
 	}
 	}
 
 
-	for _, nw := range nwList {
-		list = append(list, buildNetworkResource(nw))
+	// Combine the network list returned by Docker daemon if it is not already
+	// returned by the cluster manager
+SKIP:
+	for _, nw := range n.backend.GetNetworks() {
+		for _, nl := range list {
+			if nl.ID == nw.ID() {
+				continue SKIP
+			}
+		}
+		list = append(list, *n.buildNetworkResource(nw))
 	}
 	}
 
 
+	list, err = filterNetworks(list, netFilters)
+	if err != nil {
+		return err
+	}
 	return httputils.WriteJSON(w, http.StatusOK, list)
 	return httputils.WriteJSON(w, http.StatusOK, list)
 }
 }
 
 
@@ -45,9 +58,12 @@ func (n *networkRouter) getNetwork(ctx context.Context, w http.ResponseWriter, r
 
 
 	nw, err := n.backend.FindNetwork(vars["id"])
 	nw, err := n.backend.FindNetwork(vars["id"])
 	if err != nil {
 	if err != nil {
+		if nr, err := n.clusterProvider.GetNetwork(vars["id"]); err == nil {
+			return httputils.WriteJSON(w, http.StatusOK, nr)
+		}
 		return err
 		return err
 	}
 	}
-	return httputils.WriteJSON(w, http.StatusOK, buildNetworkResource(nw))
+	return httputils.WriteJSON(w, http.StatusOK, n.buildNetworkResource(nw))
 }
 }
 
 
 func (n *networkRouter) postNetworkCreate(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
 func (n *networkRouter) postNetworkCreate(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
@@ -67,7 +83,14 @@ func (n *networkRouter) postNetworkCreate(ctx context.Context, w http.ResponseWr
 
 
 	nw, err := n.backend.CreateNetwork(create)
 	nw, err := n.backend.CreateNetwork(create)
 	if err != nil {
 	if err != nil {
-		return err
+		if _, ok := err.(libnetwork.ManagerRedirectError); !ok {
+			return err
+		}
+		id, err := n.clusterProvider.CreateNetwork(create)
+		if err != nil {
+			return err
+		}
+		nw = &types.NetworkCreateResponse{ID: id}
 	}
 	}
 
 
 	return httputils.WriteJSON(w, http.StatusCreated, nw)
 	return httputils.WriteJSON(w, http.StatusCreated, nw)
@@ -121,6 +144,9 @@ func (n *networkRouter) deleteNetwork(ctx context.Context, w http.ResponseWriter
 	if err := httputils.ParseForm(r); err != nil {
 	if err := httputils.ParseForm(r); err != nil {
 		return err
 		return err
 	}
 	}
+	if _, err := n.clusterProvider.GetNetwork(vars["id"]); err == nil {
+		return n.clusterProvider.RemoveNetwork(vars["id"])
+	}
 	if err := n.backend.DeleteNetwork(vars["id"]); err != nil {
 	if err := n.backend.DeleteNetwork(vars["id"]); err != nil {
 		return err
 		return err
 	}
 	}
@@ -128,7 +154,7 @@ func (n *networkRouter) deleteNetwork(ctx context.Context, w http.ResponseWriter
 	return nil
 	return nil
 }
 }
 
 
-func buildNetworkResource(nw libnetwork.Network) *types.NetworkResource {
+func (n *networkRouter) buildNetworkResource(nw libnetwork.Network) *types.NetworkResource {
 	r := &types.NetworkResource{}
 	r := &types.NetworkResource{}
 	if nw == nil {
 	if nw == nil {
 		return r
 		return r
@@ -138,6 +164,13 @@ func buildNetworkResource(nw libnetwork.Network) *types.NetworkResource {
 	r.Name = nw.Name()
 	r.Name = nw.Name()
 	r.ID = nw.ID()
 	r.ID = nw.ID()
 	r.Scope = info.Scope()
 	r.Scope = info.Scope()
+	if n.clusterProvider.IsManager() {
+		if _, err := n.clusterProvider.GetNetwork(nw.Name()); err == nil {
+			r.Scope = "swarm"
+		}
+	} else if info.Dynamic() {
+		r.Scope = "swarm"
+	}
 	r.Driver = nw.Type()
 	r.Driver = nw.Type()
 	r.EnableIPv6 = info.IPv6Enabled()
 	r.EnableIPv6 = info.IPv6Enabled()
 	r.Internal = info.Internal()
 	r.Internal = info.Internal()

+ 26 - 0
api/server/router/swarm/backend.go

@@ -0,0 +1,26 @@
+package swarm
+
+import (
+	basictypes "github.com/docker/engine-api/types"
+	types "github.com/docker/engine-api/types/swarm"
+)
+
+// Backend abstracts an swarm commands manager.
+type Backend interface {
+	Init(req types.InitRequest) (string, error)
+	Join(req types.JoinRequest) error
+	Leave(force bool) error
+	Inspect() (types.Swarm, error)
+	Update(uint64, types.Spec) error
+	GetServices(basictypes.ServiceListOptions) ([]types.Service, error)
+	GetService(string) (types.Service, error)
+	CreateService(types.ServiceSpec) (string, error)
+	UpdateService(string, uint64, types.ServiceSpec) error
+	RemoveService(string) error
+	GetNodes(basictypes.NodeListOptions) ([]types.Node, error)
+	GetNode(string) (types.Node, error)
+	UpdateNode(string, uint64, types.NodeSpec) error
+	RemoveNode(string) error
+	GetTasks(basictypes.TaskListOptions) ([]types.Task, error)
+	GetTask(string) (types.Task, error)
+}

+ 44 - 0
api/server/router/swarm/cluster.go

@@ -0,0 +1,44 @@
+package swarm
+
+import "github.com/docker/docker/api/server/router"
+
+// buildRouter is a router to talk with the build controller
+type swarmRouter struct {
+	backend Backend
+	routes  []router.Route
+}
+
+// NewRouter initializes a new build router
+func NewRouter(b Backend) router.Router {
+	r := &swarmRouter{
+		backend: b,
+	}
+	r.initRoutes()
+	return r
+}
+
+// Routes returns the available routers to the swarm controller
+func (sr *swarmRouter) Routes() []router.Route {
+	return sr.routes
+}
+
+func (sr *swarmRouter) initRoutes() {
+	sr.routes = []router.Route{
+		router.NewPostRoute("/swarm/init", sr.initCluster),
+		router.NewPostRoute("/swarm/join", sr.joinCluster),
+		router.NewPostRoute("/swarm/leave", sr.leaveCluster),
+		router.NewGetRoute("/swarm", sr.inspectCluster),
+		router.NewPostRoute("/swarm/update", sr.updateCluster),
+		router.NewGetRoute("/services", sr.getServices),
+		router.NewGetRoute("/services/{id:.*}", sr.getService),
+		router.NewPostRoute("/services/create", sr.createService),
+		router.NewPostRoute("/services/{id:.*}/update", sr.updateService),
+		router.NewDeleteRoute("/services/{id:.*}", sr.removeService),
+		router.NewGetRoute("/nodes", sr.getNodes),
+		router.NewGetRoute("/nodes/{id:.*}", sr.getNode),
+		router.NewDeleteRoute("/nodes/{id:.*}", sr.removeNode),
+		router.NewPostRoute("/nodes/{id:.*}/update", sr.updateNode),
+		router.NewGetRoute("/tasks", sr.getTasks),
+		router.NewGetRoute("/tasks/{id:.*}", sr.getTask),
+	}
+}

+ 229 - 0
api/server/router/swarm/cluster_routes.go

@@ -0,0 +1,229 @@
+package swarm
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"strconv"
+
+	"github.com/Sirupsen/logrus"
+	"github.com/docker/docker/api/server/httputils"
+	basictypes "github.com/docker/engine-api/types"
+	"github.com/docker/engine-api/types/filters"
+	types "github.com/docker/engine-api/types/swarm"
+	"golang.org/x/net/context"
+)
+
+func (sr *swarmRouter) initCluster(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
+	var req types.InitRequest
+	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+		return err
+	}
+	nodeID, err := sr.backend.Init(req)
+	if err != nil {
+		logrus.Errorf("Error initializing swarm: %v", err)
+		return err
+	}
+	return httputils.WriteJSON(w, http.StatusOK, nodeID)
+}
+
+func (sr *swarmRouter) joinCluster(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
+	var req types.JoinRequest
+	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+		return err
+	}
+	return sr.backend.Join(req)
+}
+
+func (sr *swarmRouter) leaveCluster(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
+	if err := httputils.ParseForm(r); err != nil {
+		return err
+	}
+
+	force := httputils.BoolValue(r, "force")
+	return sr.backend.Leave(force)
+}
+
+func (sr *swarmRouter) inspectCluster(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
+	swarm, err := sr.backend.Inspect()
+	if err != nil {
+		logrus.Errorf("Error getting swarm: %v", err)
+		return err
+	}
+
+	return httputils.WriteJSON(w, http.StatusOK, swarm)
+}
+
+func (sr *swarmRouter) updateCluster(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
+	var swarm types.Spec
+	if err := json.NewDecoder(r.Body).Decode(&swarm); err != nil {
+		return err
+	}
+
+	rawVersion := r.URL.Query().Get("version")
+	version, err := strconv.ParseUint(rawVersion, 10, 64)
+	if err != nil {
+		return fmt.Errorf("Invalid swarm version '%s': %s", rawVersion, err.Error())
+	}
+
+	if err := sr.backend.Update(version, swarm); err != nil {
+		logrus.Errorf("Error configuring swarm: %v", err)
+		return err
+	}
+	return nil
+}
+
+func (sr *swarmRouter) getServices(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
+	if err := httputils.ParseForm(r); err != nil {
+		return err
+	}
+	filter, err := filters.FromParam(r.Form.Get("filters"))
+	if err != nil {
+		return err
+	}
+
+	services, err := sr.backend.GetServices(basictypes.ServiceListOptions{Filter: filter})
+	if err != nil {
+		logrus.Errorf("Error getting services: %v", err)
+		return err
+	}
+
+	return httputils.WriteJSON(w, http.StatusOK, services)
+}
+
+func (sr *swarmRouter) getService(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
+	service, err := sr.backend.GetService(vars["id"])
+	if err != nil {
+		logrus.Errorf("Error getting service %s: %v", vars["id"], err)
+		return err
+	}
+
+	return httputils.WriteJSON(w, http.StatusOK, service)
+}
+
+func (sr *swarmRouter) createService(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
+	var service types.ServiceSpec
+	if err := json.NewDecoder(r.Body).Decode(&service); err != nil {
+		return err
+	}
+
+	id, err := sr.backend.CreateService(service)
+	if err != nil {
+		logrus.Errorf("Error reating service %s: %v", id, err)
+		return err
+	}
+
+	return httputils.WriteJSON(w, http.StatusCreated, &basictypes.ServiceCreateResponse{
+		ID: id,
+	})
+}
+
+func (sr *swarmRouter) updateService(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
+	var service types.ServiceSpec
+	if err := json.NewDecoder(r.Body).Decode(&service); err != nil {
+		return err
+	}
+
+	rawVersion := r.URL.Query().Get("version")
+	version, err := strconv.ParseUint(rawVersion, 10, 64)
+	if err != nil {
+		return fmt.Errorf("Invalid service version '%s': %s", rawVersion, err.Error())
+	}
+
+	if err := sr.backend.UpdateService(vars["id"], version, service); err != nil {
+		logrus.Errorf("Error updating service %s: %v", vars["id"], err)
+		return err
+	}
+	return nil
+}
+
+func (sr *swarmRouter) removeService(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
+	if err := sr.backend.RemoveService(vars["id"]); err != nil {
+		logrus.Errorf("Error removing service %s: %v", vars["id"], err)
+		return err
+	}
+	return nil
+}
+
+func (sr *swarmRouter) getNodes(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
+	if err := httputils.ParseForm(r); err != nil {
+		return err
+	}
+	filter, err := filters.FromParam(r.Form.Get("filters"))
+	if err != nil {
+		return err
+	}
+
+	nodes, err := sr.backend.GetNodes(basictypes.NodeListOptions{Filter: filter})
+	if err != nil {
+		logrus.Errorf("Error getting nodes: %v", err)
+		return err
+	}
+
+	return httputils.WriteJSON(w, http.StatusOK, nodes)
+}
+
+func (sr *swarmRouter) getNode(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
+	node, err := sr.backend.GetNode(vars["id"])
+	if err != nil {
+		logrus.Errorf("Error getting node %s: %v", vars["id"], err)
+		return err
+	}
+
+	return httputils.WriteJSON(w, http.StatusOK, node)
+}
+
+func (sr *swarmRouter) updateNode(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
+	var node types.NodeSpec
+	if err := json.NewDecoder(r.Body).Decode(&node); err != nil {
+		return err
+	}
+
+	rawVersion := r.URL.Query().Get("version")
+	version, err := strconv.ParseUint(rawVersion, 10, 64)
+	if err != nil {
+		return fmt.Errorf("Invalid node version '%s': %s", rawVersion, err.Error())
+	}
+
+	if err := sr.backend.UpdateNode(vars["id"], version, node); err != nil {
+		logrus.Errorf("Error updating node %s: %v", vars["id"], err)
+		return err
+	}
+	return nil
+}
+
+func (sr *swarmRouter) removeNode(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
+	if err := sr.backend.RemoveNode(vars["id"]); err != nil {
+		logrus.Errorf("Error removing node %s: %v", vars["id"], err)
+		return err
+	}
+	return nil
+}
+
+func (sr *swarmRouter) getTasks(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
+	if err := httputils.ParseForm(r); err != nil {
+		return err
+	}
+	filter, err := filters.FromParam(r.Form.Get("filters"))
+	if err != nil {
+		return err
+	}
+
+	tasks, err := sr.backend.GetTasks(basictypes.TaskListOptions{Filter: filter})
+	if err != nil {
+		logrus.Errorf("Error getting tasks: %v", err)
+		return err
+	}
+
+	return httputils.WriteJSON(w, http.StatusOK, tasks)
+}
+
+func (sr *swarmRouter) getTask(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
+	task, err := sr.backend.GetTask(vars["id"])
+	if err != nil {
+		logrus.Errorf("Error getting task %s: %v", vars["id"], err)
+		return err
+	}
+
+	return httputils.WriteJSON(w, http.StatusOK, task)
+}

+ 10 - 5
api/server/router/system/system.go

@@ -1,18 +1,23 @@
 package system
 package system
 
 
-import "github.com/docker/docker/api/server/router"
+import (
+	"github.com/docker/docker/api/server/router"
+	"github.com/docker/docker/daemon/cluster"
+)
 
 
 // systemRouter provides information about the Docker system overall.
 // systemRouter provides information about the Docker system overall.
 // It gathers information about host, daemon and container events.
 // It gathers information about host, daemon and container events.
 type systemRouter struct {
 type systemRouter struct {
-	backend Backend
-	routes  []router.Route
+	backend         Backend
+	clusterProvider *cluster.Cluster
+	routes          []router.Route
 }
 }
 
 
 // NewRouter initializes a new system router
 // NewRouter initializes a new system router
-func NewRouter(b Backend) router.Router {
+func NewRouter(b Backend, c *cluster.Cluster) router.Router {
 	r := &systemRouter{
 	r := &systemRouter{
-		backend: b,
+		backend:         b,
+		clusterProvider: c,
 	}
 	}
 
 
 	r.routes = []router.Route{
 	r.routes = []router.Route{

+ 3 - 0
api/server/router/system/system_routes.go

@@ -33,6 +33,9 @@ func (s *systemRouter) getInfo(ctx context.Context, w http.ResponseWriter, r *ht
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
+	if s.clusterProvider != nil {
+		info.Swarm = s.clusterProvider.Info()
+	}
 
 
 	return httputils.WriteJSON(w, http.StatusOK, info)
 	return httputils.WriteJSON(w, http.StatusOK, info)
 }
 }

+ 6 - 0
cli/cobraadaptor/adaptor.go

@@ -5,7 +5,10 @@ import (
 	"github.com/docker/docker/api/client/container"
 	"github.com/docker/docker/api/client/container"
 	"github.com/docker/docker/api/client/image"
 	"github.com/docker/docker/api/client/image"
 	"github.com/docker/docker/api/client/network"
 	"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/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/system"
 	"github.com/docker/docker/api/client/volume"
 	"github.com/docker/docker/api/client/volume"
 	"github.com/docker/docker/cli"
 	"github.com/docker/docker/cli"
@@ -36,6 +39,9 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor {
 	rootCmd.SetFlagErrorFunc(cli.FlagErrorFunc)
 	rootCmd.SetFlagErrorFunc(cli.FlagErrorFunc)
 	rootCmd.SetOutput(stdout)
 	rootCmd.SetOutput(stdout)
 	rootCmd.AddCommand(
 	rootCmd.AddCommand(
+		node.NewNodeCommand(dockerCli),
+		service.NewServiceCommand(dockerCli),
+		swarm.NewSwarmCommand(dockerCli),
 		container.NewAttachCommand(dockerCli),
 		container.NewAttachCommand(dockerCli),
 		container.NewCommitCommand(dockerCli),
 		container.NewCommitCommand(dockerCli),
 		container.NewCreateCommand(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"},
 	{"cp", "Copy files/folders between a container and the local filesystem"},
 	{"exec", "Run a command in a running container"},
 	{"exec", "Run a command in a running container"},
 	{"info", "Display system-wide information"},
 	{"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"},
 	{"update", "Update configuration of one or more containers"},
 }
 }
 
 

+ 20 - 4
cmd/dockerd/daemon.go

@@ -20,12 +20,14 @@ import (
 	"github.com/docker/docker/api/server/router/container"
 	"github.com/docker/docker/api/server/router/container"
 	"github.com/docker/docker/api/server/router/image"
 	"github.com/docker/docker/api/server/router/image"
 	"github.com/docker/docker/api/server/router/network"
 	"github.com/docker/docker/api/server/router/network"
+	swarmrouter "github.com/docker/docker/api/server/router/swarm"
 	systemrouter "github.com/docker/docker/api/server/router/system"
 	systemrouter "github.com/docker/docker/api/server/router/system"
 	"github.com/docker/docker/api/server/router/volume"
 	"github.com/docker/docker/api/server/router/volume"
 	"github.com/docker/docker/builder/dockerfile"
 	"github.com/docker/docker/builder/dockerfile"
 	cliflags "github.com/docker/docker/cli/flags"
 	cliflags "github.com/docker/docker/cli/flags"
 	"github.com/docker/docker/cliconfig"
 	"github.com/docker/docker/cliconfig"
 	"github.com/docker/docker/daemon"
 	"github.com/docker/docker/daemon"
+	"github.com/docker/docker/daemon/cluster"
 	"github.com/docker/docker/daemon/logger"
 	"github.com/docker/docker/daemon/logger"
 	"github.com/docker/docker/dockerversion"
 	"github.com/docker/docker/dockerversion"
 	"github.com/docker/docker/libcontainerd"
 	"github.com/docker/docker/libcontainerd"
@@ -208,6 +210,7 @@ func (cli *DaemonCli) start() (err error) {
 	}
 	}
 
 
 	api := apiserver.New(serverConfig)
 	api := apiserver.New(serverConfig)
+	cli.api = api
 
 
 	for i := 0; i < len(cli.Config.Hosts); i++ {
 	for i := 0; i < len(cli.Config.Hosts); i++ {
 		var err error
 		var err error
@@ -264,6 +267,17 @@ func (cli *DaemonCli) start() (err error) {
 		return fmt.Errorf("Error starting daemon: %v", err)
 		return fmt.Errorf("Error starting daemon: %v", err)
 	}
 	}
 
 
+	name, _ := os.Hostname()
+
+	c, err := cluster.New(cluster.Config{
+		Root:    cli.Config.Root,
+		Name:    name,
+		Backend: d,
+	})
+	if err != nil {
+		logrus.Fatalf("Error creating cluster component: %v", err)
+	}
+
 	logrus.Info("Daemon has completed initialization")
 	logrus.Info("Daemon has completed initialization")
 
 
 	logrus.WithFields(logrus.Fields{
 	logrus.WithFields(logrus.Fields{
@@ -273,7 +287,7 @@ func (cli *DaemonCli) start() (err error) {
 	}).Info("Docker daemon")
 	}).Info("Docker daemon")
 
 
 	cli.initMiddlewares(api, serverConfig)
 	cli.initMiddlewares(api, serverConfig)
-	initRouter(api, d)
+	initRouter(api, d, c)
 
 
 	cli.d = d
 	cli.d = d
 	cli.setupConfigReloadTrap()
 	cli.setupConfigReloadTrap()
@@ -290,6 +304,7 @@ func (cli *DaemonCli) start() (err error) {
 	// Daemon is fully initialized and handling API traffic
 	// Daemon is fully initialized and handling API traffic
 	// Wait for serve API to complete
 	// Wait for serve API to complete
 	errAPI := <-serveAPIWait
 	errAPI := <-serveAPIWait
+	c.Cleanup()
 	shutdownDaemon(d, 15)
 	shutdownDaemon(d, 15)
 	containerdRemote.Cleanup()
 	containerdRemote.Cleanup()
 	if errAPI != nil {
 	if errAPI != nil {
@@ -385,18 +400,19 @@ func loadDaemonCliConfig(config *daemon.Config, flags *flag.FlagSet, commonConfi
 	return config, nil
 	return config, nil
 }
 }
 
 
-func initRouter(s *apiserver.Server, d *daemon.Daemon) {
+func initRouter(s *apiserver.Server, d *daemon.Daemon, c *cluster.Cluster) {
 	decoder := runconfig.ContainerDecoder{}
 	decoder := runconfig.ContainerDecoder{}
 
 
 	routers := []router.Router{
 	routers := []router.Router{
 		container.NewRouter(d, decoder),
 		container.NewRouter(d, decoder),
 		image.NewRouter(d, decoder),
 		image.NewRouter(d, decoder),
-		systemrouter.NewRouter(d),
+		systemrouter.NewRouter(d, c),
 		volume.NewRouter(d),
 		volume.NewRouter(d),
 		build.NewRouter(dockerfile.NewBuildManager(d)),
 		build.NewRouter(dockerfile.NewBuildManager(d)),
+		swarmrouter.NewRouter(c),
 	}
 	}
 	if d.NetworkControllerEnabled() {
 	if d.NetworkControllerEnabled() {
-		routers = append(routers, network.NewRouter(d))
+		routers = append(routers, network.NewRouter(d, c))
 	}
 	}
 
 
 	s.InitRouter(utils.IsDebugEnabled(), routers...)
 	s.InitRouter(utils.IsDebugEnabled(), routers...)

+ 23 - 1
container/container.go

@@ -66,6 +66,7 @@ type CommonContainer struct {
 	RWLayer         layer.RWLayer  `json:"-"`
 	RWLayer         layer.RWLayer  `json:"-"`
 	ID              string
 	ID              string
 	Created         time.Time
 	Created         time.Time
+	Managed         bool
 	Path            string
 	Path            string
 	Args            []string
 	Args            []string
 	Config          *containertypes.Config
 	Config          *containertypes.Config
@@ -790,7 +791,7 @@ func (container *Container) BuildCreateEndpointOptions(n libnetwork.Network, epC
 		ipam := epConfig.IPAMConfig
 		ipam := epConfig.IPAMConfig
 		if ipam != nil && (ipam.IPv4Address != "" || ipam.IPv6Address != "") {
 		if ipam != nil && (ipam.IPv4Address != "" || ipam.IPv6Address != "") {
 			createOptions = append(createOptions,
 			createOptions = append(createOptions,
-				libnetwork.CreateOptionIpam(net.ParseIP(ipam.IPv4Address), net.ParseIP(ipam.IPv6Address), nil))
+				libnetwork.CreateOptionIpam(net.ParseIP(ipam.IPv4Address), net.ParseIP(ipam.IPv6Address), nil, nil))
 		}
 		}
 
 
 		for _, alias := range epConfig.Aliases {
 		for _, alias := range epConfig.Aliases {
@@ -798,6 +799,27 @@ func (container *Container) BuildCreateEndpointOptions(n libnetwork.Network, epC
 		}
 		}
 	}
 	}
 
 
+	if container.NetworkSettings.Service != nil {
+		svcCfg := container.NetworkSettings.Service
+
+		var vip string
+		if svcCfg.VirtualAddresses[n.ID()] != nil {
+			vip = svcCfg.VirtualAddresses[n.ID()].IPv4
+		}
+
+		var portConfigs []*libnetwork.PortConfig
+		for _, portConfig := range svcCfg.ExposedPorts {
+			portConfigs = append(portConfigs, &libnetwork.PortConfig{
+				Name:          portConfig.Name,
+				Protocol:      libnetwork.PortConfig_Protocol(portConfig.Protocol),
+				TargetPort:    portConfig.TargetPort,
+				PublishedPort: portConfig.PublishedPort,
+			})
+		}
+
+		createOptions = append(createOptions, libnetwork.CreateOptionService(svcCfg.Name, svcCfg.ID, net.ParseIP(vip), portConfigs))
+	}
+
 	if !containertypes.NetworkMode(n.Name()).IsUserDefined() {
 	if !containertypes.NetworkMode(n.Name()).IsUserDefined() {
 		createOptions = append(createOptions, libnetwork.CreateOptionDisableResolution())
 		createOptions = append(createOptions, libnetwork.CreateOptionDisableResolution())
 	}
 	}

+ 28 - 0
container/state.go

@@ -5,6 +5,8 @@ import (
 	"sync"
 	"sync"
 	"time"
 	"time"
 
 
+	"golang.org/x/net/context"
+
 	"github.com/docker/go-units"
 	"github.com/docker/go-units"
 )
 )
 
 
@@ -139,6 +141,32 @@ func (s *State) WaitStop(timeout time.Duration) (int, error) {
 	return s.getExitCode(), nil
 	return s.getExitCode(), nil
 }
 }
 
 
+// WaitWithContext waits for the container to stop. Optional context can be
+// passed for canceling the request.
+func (s *State) WaitWithContext(ctx context.Context) <-chan int {
+	// todo(tonistiigi): make other wait functions use this
+	c := make(chan int)
+	go func() {
+		s.Lock()
+		if !s.Running {
+			exitCode := s.ExitCode
+			s.Unlock()
+			c <- exitCode
+			close(c)
+			return
+		}
+		waitChan := s.waitChan
+		s.Unlock()
+		select {
+		case <-waitChan:
+			c <- s.getExitCode()
+		case <-ctx.Done():
+		}
+		close(c)
+	}()
+	return c
+}
+
 // IsRunning returns whether the running flag is set. Used by Container to check whether a container is running.
 // IsRunning returns whether the running flag is set. Used by Container to check whether a container is running.
 func (s *State) IsRunning() bool {
 func (s *State) IsRunning() bool {
 	s.Lock()
 	s.Lock()

+ 1056 - 0
daemon/cluster/cluster.go

@@ -0,0 +1,1056 @@
+package cluster
+
+import (
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+	"sync"
+	"time"
+
+	"google.golang.org/grpc"
+
+	"github.com/Sirupsen/logrus"
+	"github.com/docker/docker/daemon/cluster/convert"
+	executorpkg "github.com/docker/docker/daemon/cluster/executor"
+	"github.com/docker/docker/daemon/cluster/executor/container"
+	"github.com/docker/docker/errors"
+	"github.com/docker/docker/pkg/ioutils"
+	"github.com/docker/docker/runconfig"
+	apitypes "github.com/docker/engine-api/types"
+	types "github.com/docker/engine-api/types/swarm"
+	swarmagent "github.com/docker/swarmkit/agent"
+	swarmapi "github.com/docker/swarmkit/api"
+	"golang.org/x/net/context"
+)
+
+const swarmDirName = "swarm"
+const controlSocket = "control.sock"
+const swarmConnectTimeout = 5 * time.Second
+const stateFile = "docker-state.json"
+
+const (
+	initialReconnectDelay = 100 * time.Millisecond
+	maxReconnectDelay     = 10 * time.Second
+)
+
+// ErrNoManager is returned then a manager-only function is called on non-manager
+var ErrNoManager = fmt.Errorf("this node is not participating as a Swarm manager")
+
+// ErrNoSwarm is returned on leaving a cluster that was never initialized
+var ErrNoSwarm = fmt.Errorf("this node is not part of Swarm")
+
+// ErrSwarmExists is returned on initialize or join request for a cluster that has already been activated
+var ErrSwarmExists = fmt.Errorf("this node is already part of a Swarm")
+
+// ErrSwarmJoinTimeoutReached is returned when cluster join could not complete before timeout was reached.
+var ErrSwarmJoinTimeoutReached = fmt.Errorf("timeout reached before node was joined")
+
+type state struct {
+	ListenAddr string
+}
+
+// Config provides values for Cluster.
+type Config struct {
+	Root    string
+	Name    string
+	Backend executorpkg.Backend
+}
+
+// Cluster provides capabilities to pariticipate in a cluster as worker or a
+// manager and a worker.
+type Cluster struct {
+	sync.RWMutex
+	root           string
+	config         Config
+	configEvent    chan struct{} // todo: make this array and goroutine safe
+	node           *swarmagent.Node
+	conn           *grpc.ClientConn
+	client         swarmapi.ControlClient
+	ready          bool
+	listenAddr     string
+	err            error
+	reconnectDelay time.Duration
+	stop           bool
+	cancelDelay    func()
+}
+
+// New creates a new Cluster instance using provided config.
+func New(config Config) (*Cluster, error) {
+	root := filepath.Join(config.Root, swarmDirName)
+	if err := os.MkdirAll(root, 0700); err != nil {
+		return nil, err
+	}
+	c := &Cluster{
+		root:           root,
+		config:         config,
+		configEvent:    make(chan struct{}, 10),
+		reconnectDelay: initialReconnectDelay,
+	}
+
+	dt, err := ioutil.ReadFile(filepath.Join(root, stateFile))
+	if err != nil {
+		if os.IsNotExist(err) {
+			return c, nil
+		}
+		return nil, err
+	}
+
+	var st state
+	if err := json.Unmarshal(dt, &st); err != nil {
+		return nil, err
+	}
+
+	n, ctx, err := c.startNewNode(false, st.ListenAddr, "", "", "", false)
+	if err != nil {
+		return nil, err
+	}
+
+	select {
+	case <-time.After(swarmConnectTimeout):
+		logrus.Errorf("swarm component could not be started before timeout was reached")
+	case <-n.Ready(context.Background()):
+	case <-ctx.Done():
+	}
+	if ctx.Err() != nil {
+		return nil, fmt.Errorf("swarm component could not be started")
+	}
+	go c.reconnectOnFailure(ctx)
+	return c, nil
+}
+
+func (c *Cluster) checkCompatibility() error {
+	info, _ := c.config.Backend.SystemInfo()
+	if info != nil && (info.ClusterStore != "" || info.ClusterAdvertise != "") {
+		return fmt.Errorf("swarm mode is incompatible with `--cluster-store` and `--cluster-advertise daemon configuration")
+	}
+	return nil
+}
+
+func (c *Cluster) saveState() error {
+	dt, err := json.Marshal(state{ListenAddr: c.listenAddr})
+	if err != nil {
+		return err
+	}
+	return ioutils.AtomicWriteFile(filepath.Join(c.root, stateFile), dt, 0600)
+}
+
+func (c *Cluster) reconnectOnFailure(ctx context.Context) {
+	for {
+		<-ctx.Done()
+		c.Lock()
+		if c.stop || c.node != nil {
+			c.Unlock()
+			return
+		}
+		c.reconnectDelay *= 2
+		if c.reconnectDelay > maxReconnectDelay {
+			c.reconnectDelay = maxReconnectDelay
+		}
+		logrus.Warnf("Restarting swarm in %.2f seconds", c.reconnectDelay.Seconds())
+		delayCtx, cancel := context.WithTimeout(context.Background(), c.reconnectDelay)
+		c.cancelDelay = cancel
+		c.Unlock()
+		<-delayCtx.Done()
+		if delayCtx.Err() != context.DeadlineExceeded {
+			return
+		}
+		c.Lock()
+		if c.node != nil {
+			c.Unlock()
+			return
+		}
+		var err error
+		_, ctx, err = c.startNewNode(false, c.listenAddr, c.getRemoteAddress(), "", "", false)
+		if err != nil {
+			c.err = err
+			ctx = delayCtx
+		}
+		c.Unlock()
+	}
+}
+
+func (c *Cluster) startNewNode(forceNewCluster bool, listenAddr, joinAddr, secret, cahash string, ismanager bool) (*swarmagent.Node, context.Context, error) {
+	if err := c.checkCompatibility(); err != nil {
+		return nil, nil, err
+	}
+	c.node = nil
+	c.cancelDelay = nil
+	node, err := swarmagent.NewNode(&swarmagent.NodeConfig{
+		Hostname:         c.config.Name,
+		ForceNewCluster:  forceNewCluster,
+		ListenControlAPI: filepath.Join(c.root, controlSocket),
+		ListenRemoteAPI:  listenAddr,
+		JoinAddr:         joinAddr,
+		StateDir:         c.root,
+		CAHash:           cahash,
+		Secret:           secret,
+		Executor:         container.NewExecutor(c.config.Backend),
+		HeartbeatTick:    1,
+		ElectionTick:     3,
+		IsManager:        ismanager,
+	})
+	if err != nil {
+		return nil, nil, err
+	}
+	ctx, cancel := context.WithCancel(context.Background())
+	if err := node.Start(ctx); err != nil {
+		return nil, nil, err
+	}
+
+	c.node = node
+	c.listenAddr = listenAddr
+	c.saveState()
+	c.config.Backend.SetClusterProvider(c)
+	go func() {
+		err := node.Err(ctx)
+		if err != nil {
+			logrus.Errorf("cluster exited with error: %v", err)
+		}
+		c.Lock()
+		c.conn = nil
+		c.client = nil
+		c.node = nil
+		c.ready = false
+		c.err = err
+		c.Unlock()
+		cancel()
+	}()
+
+	go func() {
+		select {
+		case <-node.Ready(context.Background()):
+			c.Lock()
+			c.reconnectDelay = initialReconnectDelay
+			c.Unlock()
+		case <-ctx.Done():
+		}
+		if ctx.Err() == nil {
+			c.Lock()
+			c.ready = true
+			c.err = nil
+			c.Unlock()
+		}
+		c.configEvent <- struct{}{}
+	}()
+
+	go func() {
+		for conn := range node.ListenControlSocket(ctx) {
+			c.Lock()
+			if c.conn != conn {
+				c.client = swarmapi.NewControlClient(conn)
+			}
+			if c.conn != nil {
+				c.client = nil
+			}
+			c.conn = conn
+			c.Unlock()
+			c.configEvent <- struct{}{}
+		}
+	}()
+
+	return node, ctx, nil
+}
+
+// Init initializes new cluster from user provided request.
+func (c *Cluster) Init(req types.InitRequest) (string, error) {
+	c.Lock()
+	if c.node != nil {
+		c.Unlock()
+		if !req.ForceNewCluster {
+			return "", ErrSwarmExists
+		}
+		ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
+		defer cancel()
+		if err := c.node.Stop(ctx); err != nil && !strings.Contains(err.Error(), "context canceled") {
+			return "", err
+		}
+		c.Lock()
+		c.node = nil
+		c.conn = nil
+		c.ready = false
+	}
+	// todo: check current state existing
+	n, ctx, err := c.startNewNode(req.ForceNewCluster, req.ListenAddr, "", "", "", false)
+	if err != nil {
+		c.Unlock()
+		return "", err
+	}
+	c.Unlock()
+
+	select {
+	case <-n.Ready(context.Background()):
+		if err := initAcceptancePolicy(n, req.Spec.AcceptancePolicy); err != nil {
+			return "", err
+		}
+		go c.reconnectOnFailure(ctx)
+		return n.NodeID(), nil
+	case <-ctx.Done():
+		c.RLock()
+		defer c.RUnlock()
+		if c.err != nil {
+			if !req.ForceNewCluster { // if failure on first attempt don't keep state
+				if err := c.clearState(); err != nil {
+					return "", err
+				}
+			}
+			return "", c.err
+		}
+		return "", ctx.Err()
+	}
+}
+
+// Join makes current Cluster part of an existing swarm cluster.
+func (c *Cluster) Join(req types.JoinRequest) error {
+	c.Lock()
+	if c.node != nil {
+		c.Unlock()
+		return ErrSwarmExists
+	}
+	// todo: check current state existing
+	if len(req.RemoteAddrs) == 0 {
+		return fmt.Errorf("at least 1 RemoteAddr is required to join")
+	}
+	n, ctx, err := c.startNewNode(false, req.ListenAddr, req.RemoteAddrs[0], req.Secret, req.CACertHash, req.Manager)
+	if err != nil {
+		c.Unlock()
+		return err
+	}
+	c.Unlock()
+
+	select {
+	case <-time.After(swarmConnectTimeout):
+		go c.reconnectOnFailure(ctx)
+		if nodeid := n.NodeID(); nodeid != "" {
+			return fmt.Errorf("Timeout reached before node was joined. Your cluster settings may be preventing this node from automatically joining. To accept this node into cluster run `docker node accept %v` in an existing cluster manager", nodeid)
+		}
+		return ErrSwarmJoinTimeoutReached
+	case <-n.Ready(context.Background()):
+		go c.reconnectOnFailure(ctx)
+		return nil
+	case <-ctx.Done():
+		c.RLock()
+		defer c.RUnlock()
+		if c.err != nil {
+			return c.err
+		}
+		return ctx.Err()
+	}
+}
+
+func (c *Cluster) cancelReconnect() {
+	c.stop = true
+	if c.cancelDelay != nil {
+		c.cancelDelay()
+		c.cancelDelay = nil
+	}
+}
+
+// Leave shuts down Cluster and removes current state.
+func (c *Cluster) Leave(force bool) error {
+	c.Lock()
+	node := c.node
+	if node == nil {
+		c.Unlock()
+		return ErrNoSwarm
+	}
+
+	if node.Manager() != nil && !force {
+		msg := "You are attempting to leave cluster on a node that is participating as a manager. "
+		if c.isActiveManager() {
+			active, reachable, unreachable, err := c.managerStats()
+			if err == nil {
+				if active && reachable-2 <= unreachable {
+					if reachable == 1 && unreachable == 0 {
+						msg += "Leaving last manager will remove all current state of the cluster. Use `--force` to ignore this message. "
+						c.Unlock()
+						return fmt.Errorf(msg)
+					}
+					msg += fmt.Sprintf("Leaving cluster will leave you with  %v managers out of %v. This means Raft quorum will be lost and your cluster will become inaccessible. ", reachable-1, reachable+unreachable)
+				}
+			}
+		} else {
+			msg += "Doing so may lose the consenus of your cluster. "
+		}
+
+		msg += "Only way to restore a cluster that has lost consensus is to reinitialize it with `--force-new-cluster`. Use `--force` to ignore this message."
+		c.Unlock()
+		return fmt.Errorf(msg)
+	}
+	c.cancelReconnect()
+	c.Unlock()
+
+	ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
+	defer cancel()
+	if err := node.Stop(ctx); err != nil && !strings.Contains(err.Error(), "context canceled") {
+		return err
+	}
+	nodeID := node.NodeID()
+	for _, id := range c.config.Backend.ListContainersForNode(nodeID) {
+		if err := c.config.Backend.ContainerRm(id, &apitypes.ContainerRmConfig{ForceRemove: true}); err != nil {
+			logrus.Errorf("error removing %v: %v", id, err)
+		}
+	}
+	c.Lock()
+	defer c.Unlock()
+	c.node = nil
+	c.conn = nil
+	c.ready = false
+	c.configEvent <- struct{}{}
+	// todo: cleanup optional?
+	if err := c.clearState(); err != nil {
+		return err
+	}
+	return nil
+}
+
+func (c *Cluster) clearState() error {
+	if err := os.RemoveAll(c.root); err != nil {
+		return err
+	}
+	if err := os.MkdirAll(c.root, 0700); err != nil {
+		return err
+	}
+	c.config.Backend.SetClusterProvider(nil)
+	return nil
+}
+
+func (c *Cluster) getRequestContext() context.Context { // TODO: not needed when requests don't block on qourum lost
+	ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
+	return ctx
+}
+
+// Inspect retrives the confuguration properties of managed swarm cluster.
+func (c *Cluster) Inspect() (types.Swarm, error) {
+	c.RLock()
+	defer c.RUnlock()
+
+	if !c.isActiveManager() {
+		return types.Swarm{}, ErrNoManager
+	}
+
+	swarm, err := getSwarm(c.getRequestContext(), c.client)
+	if err != nil {
+		return types.Swarm{}, err
+	}
+
+	if err != nil {
+		return types.Swarm{}, err
+	}
+
+	return convert.SwarmFromGRPC(*swarm), nil
+}
+
+// Update updates configuration of a managed swarm cluster.
+func (c *Cluster) Update(version uint64, spec types.Spec) error {
+	c.RLock()
+	defer c.RUnlock()
+
+	if !c.isActiveManager() {
+		return ErrNoManager
+	}
+
+	swarmSpec, err := convert.SwarmSpecToGRPC(spec)
+	if err != nil {
+		return err
+	}
+
+	swarm, err := getSwarm(c.getRequestContext(), c.client)
+	if err != nil {
+		return err
+	}
+
+	_, err = c.client.UpdateCluster(
+		c.getRequestContext(),
+		&swarmapi.UpdateClusterRequest{
+			ClusterID: swarm.ID,
+			Spec:      &swarmSpec,
+			ClusterVersion: &swarmapi.Version{
+				Index: version,
+			},
+		},
+	)
+	return err
+}
+
+// IsManager returns true is Cluster is participating as a manager.
+func (c *Cluster) IsManager() bool {
+	c.RLock()
+	defer c.RUnlock()
+	return c.isActiveManager()
+}
+
+// IsAgent returns true is Cluster is participating as a worker/agent.
+func (c *Cluster) IsAgent() bool {
+	c.RLock()
+	defer c.RUnlock()
+	return c.ready
+}
+
+// GetListenAddress returns the listening address for current maanger's
+// consensus and dispatcher APIs.
+func (c *Cluster) GetListenAddress() string {
+	c.RLock()
+	defer c.RUnlock()
+	if c.conn != nil {
+		return c.listenAddr
+	}
+	return ""
+}
+
+// GetRemoteAddress returns a known advertise address of a remote maanger if
+// available.
+// todo: change to array/connect with info
+func (c *Cluster) GetRemoteAddress() string {
+	c.RLock()
+	defer c.RUnlock()
+	return c.getRemoteAddress()
+}
+
+func (c *Cluster) getRemoteAddress() string {
+	if c.node == nil {
+		return ""
+	}
+	nodeID := c.node.NodeID()
+	for _, r := range c.node.Remotes() {
+		if r.NodeID != nodeID {
+			return r.Addr
+		}
+	}
+	return ""
+}
+
+// ListenClusterEvents returns a channel that receives messages on cluster
+// participation changes.
+// todo: make cancelable and accessible to multiple callers
+func (c *Cluster) ListenClusterEvents() <-chan struct{} {
+	return c.configEvent
+}
+
+// Info returns information about the current cluster state.
+func (c *Cluster) Info() types.Info {
+	var info types.Info
+	c.RLock()
+	defer c.RUnlock()
+
+	if c.node == nil {
+		info.LocalNodeState = types.LocalNodeStateInactive
+		if c.cancelDelay != nil {
+			info.LocalNodeState = types.LocalNodeStateError
+		}
+	} else {
+		info.LocalNodeState = types.LocalNodeStatePending
+		if c.ready == true {
+			info.LocalNodeState = types.LocalNodeStateActive
+		}
+	}
+	if c.err != nil {
+		info.Error = c.err.Error()
+	}
+
+	if c.isActiveManager() {
+		info.ControlAvailable = true
+		if r, err := c.client.ListNodes(c.getRequestContext(), &swarmapi.ListNodesRequest{}); err == nil {
+			info.Nodes = len(r.Nodes)
+			for _, n := range r.Nodes {
+				if n.ManagerStatus != nil {
+					info.Managers = info.Managers + 1
+				}
+			}
+		}
+
+		if swarm, err := getSwarm(c.getRequestContext(), c.client); err == nil && swarm != nil {
+			info.CACertHash = swarm.RootCA.CACertHash
+		}
+	}
+
+	if c.node != nil {
+		for _, r := range c.node.Remotes() {
+			info.RemoteManagers = append(info.RemoteManagers, types.Peer{NodeID: r.NodeID, Addr: r.Addr})
+		}
+		info.NodeID = c.node.NodeID()
+	}
+
+	return info
+}
+
+// isActiveManager should not be called without a read lock
+func (c *Cluster) isActiveManager() bool {
+	return c.conn != nil
+}
+
+// GetServices returns all services of a managed swarm cluster.
+func (c *Cluster) GetServices(options apitypes.ServiceListOptions) ([]types.Service, error) {
+	c.RLock()
+	defer c.RUnlock()
+
+	if !c.isActiveManager() {
+		return nil, ErrNoManager
+	}
+
+	filters, err := newListServicesFilters(options.Filter)
+	if err != nil {
+		return nil, err
+	}
+	r, err := c.client.ListServices(
+		c.getRequestContext(),
+		&swarmapi.ListServicesRequest{Filters: filters})
+	if err != nil {
+		return nil, err
+	}
+
+	var services []types.Service
+
+	for _, service := range r.Services {
+		services = append(services, convert.ServiceFromGRPC(*service))
+	}
+
+	return services, nil
+}
+
+// CreateService creates a new service in a managed swarm cluster.
+func (c *Cluster) CreateService(s types.ServiceSpec) (string, error) {
+	c.RLock()
+	defer c.RUnlock()
+
+	if !c.isActiveManager() {
+		return "", ErrNoManager
+	}
+
+	ctx := c.getRequestContext()
+
+	err := populateNetworkID(ctx, c.client, &s)
+	if err != nil {
+		return "", err
+	}
+
+	serviceSpec, err := convert.ServiceSpecToGRPC(s)
+	if err != nil {
+		return "", err
+	}
+	r, err := c.client.CreateService(ctx, &swarmapi.CreateServiceRequest{Spec: &serviceSpec})
+	if err != nil {
+		return "", err
+	}
+
+	return r.Service.ID, nil
+}
+
+// GetService returns a service based on a ID or name.
+func (c *Cluster) GetService(input string) (types.Service, error) {
+	c.RLock()
+	defer c.RUnlock()
+
+	if !c.isActiveManager() {
+		return types.Service{}, ErrNoManager
+	}
+
+	service, err := getService(c.getRequestContext(), c.client, input)
+	if err != nil {
+		return types.Service{}, err
+	}
+	return convert.ServiceFromGRPC(*service), nil
+}
+
+// UpdateService updates existing service to match new properties.
+func (c *Cluster) UpdateService(serviceID string, version uint64, spec types.ServiceSpec) error {
+	c.RLock()
+	defer c.RUnlock()
+
+	if !c.isActiveManager() {
+		return ErrNoManager
+	}
+
+	serviceSpec, err := convert.ServiceSpecToGRPC(spec)
+	if err != nil {
+		return err
+	}
+
+	_, err = c.client.UpdateService(
+		c.getRequestContext(),
+		&swarmapi.UpdateServiceRequest{
+			ServiceID: serviceID,
+			Spec:      &serviceSpec,
+			ServiceVersion: &swarmapi.Version{
+				Index: version,
+			},
+		},
+	)
+	return err
+}
+
+// RemoveService removes a service from a managed swarm cluster.
+func (c *Cluster) RemoveService(input string) error {
+	c.RLock()
+	defer c.RUnlock()
+
+	if !c.isActiveManager() {
+		return ErrNoManager
+	}
+
+	service, err := getService(c.getRequestContext(), c.client, input)
+	if err != nil {
+		return err
+	}
+
+	if _, err := c.client.RemoveService(c.getRequestContext(), &swarmapi.RemoveServiceRequest{ServiceID: service.ID}); err != nil {
+		return err
+	}
+	return nil
+}
+
+// GetNodes returns a list of all nodes known to a cluster.
+func (c *Cluster) GetNodes(options apitypes.NodeListOptions) ([]types.Node, error) {
+	c.RLock()
+	defer c.RUnlock()
+
+	if !c.isActiveManager() {
+		return nil, ErrNoManager
+	}
+
+	filters, err := newListNodesFilters(options.Filter)
+	if err != nil {
+		return nil, err
+	}
+	r, err := c.client.ListNodes(
+		c.getRequestContext(),
+		&swarmapi.ListNodesRequest{Filters: filters})
+	if err != nil {
+		return nil, err
+	}
+
+	nodes := []types.Node{}
+
+	for _, node := range r.Nodes {
+		nodes = append(nodes, convert.NodeFromGRPC(*node))
+	}
+	return nodes, nil
+}
+
+// GetNode returns a node based on a ID or name.
+func (c *Cluster) GetNode(input string) (types.Node, error) {
+	c.RLock()
+	defer c.RUnlock()
+
+	if !c.isActiveManager() {
+		return types.Node{}, ErrNoManager
+	}
+
+	node, err := getNode(c.getRequestContext(), c.client, input)
+	if err != nil {
+		return types.Node{}, err
+	}
+	return convert.NodeFromGRPC(*node), nil
+}
+
+// UpdateNode updates existing nodes properties.
+func (c *Cluster) UpdateNode(nodeID string, version uint64, spec types.NodeSpec) error {
+	c.RLock()
+	defer c.RUnlock()
+
+	if !c.isActiveManager() {
+		return ErrNoManager
+	}
+
+	nodeSpec, err := convert.NodeSpecToGRPC(spec)
+	if err != nil {
+		return err
+	}
+
+	_, err = c.client.UpdateNode(
+		c.getRequestContext(),
+		&swarmapi.UpdateNodeRequest{
+			NodeID: nodeID,
+			Spec:   &nodeSpec,
+			NodeVersion: &swarmapi.Version{
+				Index: version,
+			},
+		},
+	)
+	return err
+}
+
+// RemoveNode removes a node from a cluster
+func (c *Cluster) RemoveNode(input string) error {
+	c.RLock()
+	defer c.RUnlock()
+
+	if !c.isActiveManager() {
+		return ErrNoManager
+	}
+
+	ctx := c.getRequestContext()
+
+	node, err := getNode(ctx, c.client, input)
+	if err != nil {
+		return err
+	}
+
+	if _, err := c.client.RemoveNode(ctx, &swarmapi.RemoveNodeRequest{NodeID: node.ID}); err != nil {
+		return err
+	}
+	return nil
+}
+
+// GetTasks returns a list of tasks matching the filter options.
+func (c *Cluster) GetTasks(options apitypes.TaskListOptions) ([]types.Task, error) {
+	c.RLock()
+	defer c.RUnlock()
+
+	if !c.isActiveManager() {
+		return nil, ErrNoManager
+	}
+
+	filters, err := newListTasksFilters(options.Filter)
+	if err != nil {
+		return nil, err
+	}
+	r, err := c.client.ListTasks(
+		c.getRequestContext(),
+		&swarmapi.ListTasksRequest{Filters: filters})
+	if err != nil {
+		return nil, err
+	}
+
+	tasks := []types.Task{}
+
+	for _, task := range r.Tasks {
+		tasks = append(tasks, convert.TaskFromGRPC(*task))
+	}
+	return tasks, nil
+}
+
+// GetTask returns a task by an ID.
+func (c *Cluster) GetTask(input string) (types.Task, error) {
+	c.RLock()
+	defer c.RUnlock()
+
+	if !c.isActiveManager() {
+		return types.Task{}, ErrNoManager
+	}
+
+	task, err := getTask(c.getRequestContext(), c.client, input)
+	if err != nil {
+		return types.Task{}, err
+	}
+	return convert.TaskFromGRPC(*task), nil
+}
+
+// GetNetwork returns a cluster network by ID.
+func (c *Cluster) GetNetwork(input string) (apitypes.NetworkResource, error) {
+	c.RLock()
+	defer c.RUnlock()
+
+	if !c.isActiveManager() {
+		return apitypes.NetworkResource{}, ErrNoManager
+	}
+
+	network, err := getNetwork(c.getRequestContext(), c.client, input)
+	if err != nil {
+		return apitypes.NetworkResource{}, err
+	}
+	return convert.BasicNetworkFromGRPC(*network), nil
+}
+
+// GetNetworks returns all current cluster managed networks.
+func (c *Cluster) GetNetworks() ([]apitypes.NetworkResource, error) {
+	c.RLock()
+	defer c.RUnlock()
+
+	if !c.isActiveManager() {
+		return nil, ErrNoManager
+	}
+
+	r, err := c.client.ListNetworks(c.getRequestContext(), &swarmapi.ListNetworksRequest{})
+	if err != nil {
+		return nil, err
+	}
+
+	var networks []apitypes.NetworkResource
+
+	for _, network := range r.Networks {
+		networks = append(networks, convert.BasicNetworkFromGRPC(*network))
+	}
+
+	return networks, nil
+}
+
+// CreateNetwork creates a new cluster managed network.
+func (c *Cluster) CreateNetwork(s apitypes.NetworkCreateRequest) (string, error) {
+	c.RLock()
+	defer c.RUnlock()
+
+	if !c.isActiveManager() {
+		return "", ErrNoManager
+	}
+
+	if runconfig.IsPreDefinedNetwork(s.Name) {
+		err := fmt.Errorf("%s is a pre-defined network and cannot be created", s.Name)
+		return "", errors.NewRequestForbiddenError(err)
+	}
+
+	networkSpec := convert.BasicNetworkCreateToGRPC(s)
+	r, err := c.client.CreateNetwork(c.getRequestContext(), &swarmapi.CreateNetworkRequest{Spec: &networkSpec})
+	if err != nil {
+		return "", err
+	}
+
+	return r.Network.ID, nil
+}
+
+// RemoveNetwork removes a cluster network.
+func (c *Cluster) RemoveNetwork(input string) error {
+	c.RLock()
+	defer c.RUnlock()
+
+	if !c.isActiveManager() {
+		return ErrNoManager
+	}
+
+	network, err := getNetwork(c.getRequestContext(), c.client, input)
+	if err != nil {
+		return err
+	}
+
+	if _, err := c.client.RemoveNetwork(c.getRequestContext(), &swarmapi.RemoveNetworkRequest{NetworkID: network.ID}); err != nil {
+		return err
+	}
+	return nil
+}
+
+func populateNetworkID(ctx context.Context, c swarmapi.ControlClient, s *types.ServiceSpec) error {
+	for i, n := range s.Networks {
+		apiNetwork, err := getNetwork(ctx, c, n.Target)
+		if err != nil {
+			return err
+		}
+		s.Networks[i] = types.NetworkAttachmentConfig{Target: apiNetwork.ID}
+	}
+	return nil
+}
+
+func getNetwork(ctx context.Context, c swarmapi.ControlClient, input string) (*swarmapi.Network, error) {
+	// GetNetwork to match via full ID.
+	rg, err := c.GetNetwork(ctx, &swarmapi.GetNetworkRequest{NetworkID: input})
+	if err != nil {
+		// If any error (including NotFound), ListNetworks to match via ID prefix and full name.
+		rl, err := c.ListNetworks(ctx, &swarmapi.ListNetworksRequest{Filters: &swarmapi.ListNetworksRequest_Filters{Names: []string{input}}})
+		if err != nil || len(rl.Networks) == 0 {
+			rl, err = c.ListNetworks(ctx, &swarmapi.ListNetworksRequest{Filters: &swarmapi.ListNetworksRequest_Filters{IDPrefixes: []string{input}}})
+		}
+
+		if err != nil {
+			return nil, err
+		}
+
+		if len(rl.Networks) == 0 {
+			return nil, fmt.Errorf("network %s not found", input)
+		}
+
+		if l := len(rl.Networks); l > 1 {
+			return nil, fmt.Errorf("network %s is ambigious (%d matches found)", input, l)
+		}
+
+		return rl.Networks[0], nil
+	}
+	return rg.Network, nil
+}
+
+// Cleanup stops active swarm node. This is run before daemon shutdown.
+func (c *Cluster) Cleanup() {
+	c.Lock()
+	node := c.node
+	if node == nil {
+		c.Unlock()
+		return
+	}
+
+	if c.isActiveManager() {
+		active, reachable, unreachable, err := c.managerStats()
+		if err == nil {
+			singlenode := active && reachable == 1 && unreachable == 0
+			if active && !singlenode && reachable-2 <= unreachable {
+				logrus.Errorf("Leaving cluster with %v managers left out of %v. Raft quorum will be lost.", reachable-1, reachable+unreachable)
+			}
+		}
+	}
+	c.cancelReconnect()
+	c.Unlock()
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	defer cancel()
+	if err := node.Stop(ctx); err != nil {
+		logrus.Errorf("error cleaning up cluster: %v", err)
+	}
+	c.Lock()
+	c.node = nil
+	c.ready = false
+	c.conn = nil
+	c.Unlock()
+}
+
+func (c *Cluster) managerStats() (current bool, reachable int, unreachable int, err error) {
+	ctx, _ := context.WithTimeout(context.Background(), 3*time.Second)
+	nodes, err := c.client.ListNodes(ctx, &swarmapi.ListNodesRequest{})
+	if err != nil {
+		return false, 0, 0, err
+	}
+	for _, n := range nodes.Nodes {
+		if n.ManagerStatus != nil {
+			if n.ManagerStatus.Raft.Status.Reachability == swarmapi.RaftMemberStatus_REACHABLE {
+				reachable++
+				if n.ID == c.node.NodeID() {
+					current = true
+				}
+			}
+			if n.ManagerStatus.Raft.Status.Reachability == swarmapi.RaftMemberStatus_UNREACHABLE {
+				unreachable++
+			}
+		}
+	}
+	return
+}
+
+func initAcceptancePolicy(node *swarmagent.Node, acceptancePolicy types.AcceptancePolicy) error {
+	ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
+	for conn := range node.ListenControlSocket(ctx) {
+		if ctx.Err() != nil {
+			return ctx.Err()
+		}
+		if conn != nil {
+			client := swarmapi.NewControlClient(conn)
+			var cluster *swarmapi.Cluster
+			for i := 0; ; i++ {
+				lcr, err := client.ListClusters(ctx, &swarmapi.ListClustersRequest{})
+				if err != nil {
+					return fmt.Errorf("error on listing clusters: %v", err)
+				}
+				if len(lcr.Clusters) == 0 {
+					if i < 10 {
+						time.Sleep(200 * time.Millisecond)
+						continue
+					}
+					return fmt.Errorf("empty list of clusters was returned")
+				}
+				cluster = lcr.Clusters[0]
+				break
+			}
+			spec := &cluster.Spec
+
+			if err := convert.SwarmSpecUpdateAcceptancePolicy(spec, acceptancePolicy); err != nil {
+				return fmt.Errorf("error updating cluster settings: %v", err)
+			}
+			_, err := client.UpdateCluster(ctx, &swarmapi.UpdateClusterRequest{
+				ClusterID:      cluster.ID,
+				ClusterVersion: &cluster.Meta.Version,
+				Spec:           spec,
+			})
+			if err != nil {
+				return fmt.Errorf("error updating cluster settings: %v", err)
+			}
+			return nil
+		}
+	}
+	return ctx.Err()
+}

+ 116 - 0
daemon/cluster/convert/container.go

@@ -0,0 +1,116 @@
+package convert
+
+import (
+	"fmt"
+	"strings"
+
+	types "github.com/docker/engine-api/types/swarm"
+	swarmapi "github.com/docker/swarmkit/api"
+	"github.com/docker/swarmkit/protobuf/ptypes"
+)
+
+func containerSpecFromGRPC(c *swarmapi.ContainerSpec) types.ContainerSpec {
+	containerSpec := types.ContainerSpec{
+		Image:   c.Image,
+		Labels:  c.Labels,
+		Command: c.Command,
+		Args:    c.Args,
+		Env:     c.Env,
+		Dir:     c.Dir,
+		User:    c.User,
+	}
+
+	// Mounts
+	for _, m := range c.Mounts {
+		mount := types.Mount{
+			Target:   m.Target,
+			Source:   m.Source,
+			Type:     types.MountType(strings.ToLower(swarmapi.Mount_MountType_name[int32(m.Type)])),
+			Writable: m.Writable,
+		}
+
+		if m.BindOptions != nil {
+			mount.BindOptions = &types.BindOptions{
+				Propagation: types.MountPropagation(strings.ToLower(swarmapi.Mount_BindOptions_MountPropagation_name[int32(m.BindOptions.Propagation)])),
+			}
+		}
+
+		if m.VolumeOptions != nil {
+			mount.VolumeOptions = &types.VolumeOptions{
+				Populate: m.VolumeOptions.Populate,
+				Labels:   m.VolumeOptions.Labels,
+			}
+			if m.VolumeOptions.DriverConfig != nil {
+				mount.VolumeOptions.DriverConfig = &types.Driver{
+					Name:    m.VolumeOptions.DriverConfig.Name,
+					Options: m.VolumeOptions.DriverConfig.Options,
+				}
+			}
+		}
+		containerSpec.Mounts = append(containerSpec.Mounts, mount)
+	}
+
+	if c.StopGracePeriod != nil {
+		grace, _ := ptypes.Duration(c.StopGracePeriod)
+		containerSpec.StopGracePeriod = &grace
+	}
+	return containerSpec
+}
+
+func containerToGRPC(c types.ContainerSpec) (*swarmapi.ContainerSpec, error) {
+	containerSpec := &swarmapi.ContainerSpec{
+		Image:   c.Image,
+		Labels:  c.Labels,
+		Command: c.Command,
+		Args:    c.Args,
+		Env:     c.Env,
+		Dir:     c.Dir,
+		User:    c.User,
+	}
+
+	if c.StopGracePeriod != nil {
+		containerSpec.StopGracePeriod = ptypes.DurationProto(*c.StopGracePeriod)
+	}
+
+	// Mounts
+	for _, m := range c.Mounts {
+		mount := swarmapi.Mount{
+			Target:   m.Target,
+			Source:   m.Source,
+			Writable: m.Writable,
+		}
+
+		if mountType, ok := swarmapi.Mount_MountType_value[strings.ToUpper(string(m.Type))]; ok {
+			mount.Type = swarmapi.Mount_MountType(mountType)
+		} else if string(m.Type) != "" {
+			return nil, fmt.Errorf("invalid MountType: %q", m.Type)
+		}
+
+		if m.BindOptions != nil {
+			if mountPropagation, ok := swarmapi.Mount_BindOptions_MountPropagation_value[strings.ToUpper(string(m.BindOptions.Propagation))]; ok {
+				mount.BindOptions = &swarmapi.Mount_BindOptions{Propagation: swarmapi.Mount_BindOptions_MountPropagation(mountPropagation)}
+			} else if string(m.BindOptions.Propagation) != "" {
+				return nil, fmt.Errorf("invalid MountPropagation: %q", m.BindOptions.Propagation)
+
+			}
+
+		}
+
+		if m.VolumeOptions != nil {
+			mount.VolumeOptions = &swarmapi.Mount_VolumeOptions{
+				Populate: m.VolumeOptions.Populate,
+				Labels:   m.VolumeOptions.Labels,
+			}
+			if m.VolumeOptions.DriverConfig != nil {
+				mount.VolumeOptions.DriverConfig = &swarmapi.Driver{
+					Name:    m.VolumeOptions.DriverConfig.Name,
+					Options: m.VolumeOptions.DriverConfig.Options,
+				}
+			}
+		}
+
+		containerSpec.Mounts = append(containerSpec.Mounts, mount)
+	}
+
+	return containerSpec, nil
+}

+ 194 - 0
daemon/cluster/convert/network.go

@@ -0,0 +1,194 @@
+package convert
+
+import (
+	"strings"
+
+	basictypes "github.com/docker/engine-api/types"
+	networktypes "github.com/docker/engine-api/types/network"
+	types "github.com/docker/engine-api/types/swarm"
+	swarmapi "github.com/docker/swarmkit/api"
+	"github.com/docker/swarmkit/protobuf/ptypes"
+)
+
+func networkAttachementFromGRPC(na *swarmapi.NetworkAttachment) types.NetworkAttachment {
+	if na != nil {
+		return types.NetworkAttachment{
+			Network:   networkFromGRPC(na.Network),
+			Addresses: na.Addresses,
+		}
+	}
+	return types.NetworkAttachment{}
+}
+
+func networkFromGRPC(n *swarmapi.Network) types.Network {
+	if n != nil {
+		network := types.Network{
+			ID: n.ID,
+			Spec: types.NetworkSpec{
+				IPv6Enabled: n.Spec.Ipv6Enabled,
+				Internal:    n.Spec.Internal,
+				IPAMOptions: ipamFromGRPC(n.Spec.IPAM),
+			},
+			IPAMOptions: ipamFromGRPC(n.IPAM),
+		}
+
+		// Meta
+		network.Version.Index = n.Meta.Version.Index
+		network.CreatedAt, _ = ptypes.Timestamp(n.Meta.CreatedAt)
+		network.UpdatedAt, _ = ptypes.Timestamp(n.Meta.UpdatedAt)
+
+		//Annotations
+		network.Spec.Name = n.Spec.Annotations.Name
+		network.Spec.Labels = n.Spec.Annotations.Labels
+
+		//DriverConfiguration
+		if n.Spec.DriverConfig != nil {
+			network.Spec.DriverConfiguration = &types.Driver{
+				Name:    n.Spec.DriverConfig.Name,
+				Options: n.Spec.DriverConfig.Options,
+			}
+		}
+
+		//DriverState
+		if n.DriverState != nil {
+			network.DriverState = types.Driver{
+				Name:    n.DriverState.Name,
+				Options: n.DriverState.Options,
+			}
+		}
+
+		return network
+	}
+	return types.Network{}
+}
+
+func ipamFromGRPC(i *swarmapi.IPAMOptions) *types.IPAMOptions {
+	var ipam *types.IPAMOptions
+	if i != nil {
+		ipam = &types.IPAMOptions{}
+		if i.Driver != nil {
+			ipam.Driver.Name = i.Driver.Name
+			ipam.Driver.Options = i.Driver.Options
+		}
+
+		for _, config := range i.Configs {
+			ipam.Configs = append(ipam.Configs, types.IPAMConfig{
+				Subnet:  config.Subnet,
+				Range:   config.Range,
+				Gateway: config.Gateway,
+			})
+		}
+	}
+	return ipam
+}
+
+func endpointSpecFromGRPC(es *swarmapi.EndpointSpec) *types.EndpointSpec {
+	var endpointSpec *types.EndpointSpec
+	if es != nil {
+		endpointSpec = &types.EndpointSpec{}
+		endpointSpec.Mode = types.ResolutionMode(strings.ToLower(es.Mode.String()))
+
+		for _, portState := range es.Ports {
+			endpointSpec.Ports = append(endpointSpec.Ports, types.PortConfig{
+				Name:          portState.Name,
+				Protocol:      types.PortConfigProtocol(strings.ToLower(swarmapi.PortConfig_Protocol_name[int32(portState.Protocol)])),
+				TargetPort:    portState.TargetPort,
+				PublishedPort: portState.PublishedPort,
+			})
+		}
+	}
+	return endpointSpec
+}
+
+func endpointFromGRPC(e *swarmapi.Endpoint) types.Endpoint {
+	endpoint := types.Endpoint{}
+	if e != nil {
+		if espec := endpointSpecFromGRPC(e.Spec); espec != nil {
+			endpoint.Spec = *espec
+		}
+
+		for _, portState := range e.Ports {
+			endpoint.Ports = append(endpoint.Ports, types.PortConfig{
+				Name:          portState.Name,
+				Protocol:      types.PortConfigProtocol(strings.ToLower(swarmapi.PortConfig_Protocol_name[int32(portState.Protocol)])),
+				TargetPort:    portState.TargetPort,
+				PublishedPort: portState.PublishedPort,
+			})
+		}
+
+		for _, v := range e.VirtualIPs {
+			endpoint.VirtualIPs = append(endpoint.VirtualIPs, types.EndpointVirtualIP{
+				NetworkID: v.NetworkID,
+				Addr:      v.Addr})
+		}
+
+	}
+
+	return endpoint
+}
+
+// BasicNetworkFromGRPC converts a grpc Network to a NetworkResource.
+func BasicNetworkFromGRPC(n swarmapi.Network) basictypes.NetworkResource {
+	spec := n.Spec
+	var ipam networktypes.IPAM
+	if spec.IPAM != nil {
+		if spec.IPAM.Driver != nil {
+			ipam.Driver = spec.IPAM.Driver.Name
+			ipam.Options = spec.IPAM.Driver.Options
+		}
+		ipam.Config = make([]networktypes.IPAMConfig, 0, len(spec.IPAM.Configs))
+		for _, ic := range spec.IPAM.Configs {
+			ipamConfig := networktypes.IPAMConfig{
+				Subnet:     ic.Subnet,
+				IPRange:    ic.Range,
+				Gateway:    ic.Gateway,
+				AuxAddress: ic.Reserved,
+			}
+			ipam.Config = append(ipam.Config, ipamConfig)
+		}
+	}
+
+	return basictypes.NetworkResource{
+		ID:         n.ID,
+		Name:       n.Spec.Annotations.Name,
+		Scope:      "swarm",
+		Driver:     n.DriverState.Name,
+		EnableIPv6: spec.Ipv6Enabled,
+		IPAM:       ipam,
+		Internal:   spec.Internal,
+		Options:    n.DriverState.Options,
+		Labels:     n.Spec.Annotations.Labels,
+	}
+}
+
+// BasicNetworkCreateToGRPC converts a NetworkCreateRequest to a grpc NetworkSpec.
+func BasicNetworkCreateToGRPC(create basictypes.NetworkCreateRequest) swarmapi.NetworkSpec {
+	ns := swarmapi.NetworkSpec{
+		Annotations: swarmapi.Annotations{
+			Name:   create.Name,
+			Labels: create.Labels,
+		},
+		DriverConfig: &swarmapi.Driver{
+			Name:    create.Driver,
+			Options: create.Options,
+		},
+		Ipv6Enabled: create.EnableIPv6,
+		Internal:    create.Internal,
+		IPAM: &swarmapi.IPAMOptions{
+			Driver: &swarmapi.Driver{
+				Name:    create.IPAM.Driver,
+				Options: create.IPAM.Options,
+			},
+		},
+	}
+	ipamSpec := make([]*swarmapi.IPAMConfig, 0, len(create.IPAM.Config))
+	for _, ipamConfig := range create.IPAM.Config {
+		ipamSpec = append(ipamSpec, &swarmapi.IPAMConfig{
+			Subnet:  ipamConfig.Subnet,
+			Range:   ipamConfig.IPRange,
+			Gateway: ipamConfig.Gateway,
+		})
+	}
+	ns.IPAM.Configs = ipamSpec
+	return ns
+}

+ 95 - 0
daemon/cluster/convert/node.go

@@ -0,0 +1,95 @@
+package convert
+
+import (
+	"fmt"
+	"strings"
+
+	types "github.com/docker/engine-api/types/swarm"
+	swarmapi "github.com/docker/swarmkit/api"
+	"github.com/docker/swarmkit/protobuf/ptypes"
+)
+
+// NodeFromGRPC converts a grpc Node to a Node.
+func NodeFromGRPC(n swarmapi.Node) types.Node {
+	node := types.Node{
+		ID: n.ID,
+		Spec: types.NodeSpec{
+			Role:         types.NodeRole(strings.ToLower(n.Spec.Role.String())),
+			Membership:   types.NodeMembership(strings.ToLower(n.Spec.Membership.String())),
+			Availability: types.NodeAvailability(strings.ToLower(n.Spec.Availability.String())),
+		},
+		Status: types.NodeStatus{
+			State:   types.NodeState(strings.ToLower(n.Status.State.String())),
+			Message: n.Status.Message,
+		},
+	}
+
+	// Meta
+	node.Version.Index = n.Meta.Version.Index
+	node.CreatedAt, _ = ptypes.Timestamp(n.Meta.CreatedAt)
+	node.UpdatedAt, _ = ptypes.Timestamp(n.Meta.UpdatedAt)
+
+	//Annotations
+	node.Spec.Name = n.Spec.Annotations.Name
+	node.Spec.Labels = n.Spec.Annotations.Labels
+
+	//Description
+	if n.Description != nil {
+		node.Description.Hostname = n.Description.Hostname
+		if n.Description.Platform != nil {
+			node.Description.Platform.Architecture = n.Description.Platform.Architecture
+			node.Description.Platform.OS = n.Description.Platform.OS
+		}
+		if n.Description.Resources != nil {
+			node.Description.Resources.NanoCPUs = n.Description.Resources.NanoCPUs
+			node.Description.Resources.MemoryBytes = n.Description.Resources.MemoryBytes
+		}
+		if n.Description.Engine != nil {
+			node.Description.Engine.EngineVersion = n.Description.Engine.EngineVersion
+			node.Description.Engine.Labels = n.Description.Engine.Labels
+			for _, plugin := range n.Description.Engine.Plugins {
+				node.Description.Engine.Plugins = append(node.Description.Engine.Plugins, types.PluginDescription{Type: plugin.Type, Name: plugin.Name})
+			}
+		}
+	}
+
+	//Manager
+	if n.ManagerStatus != nil {
+		node.ManagerStatus = &types.ManagerStatus{
+			Leader:       n.ManagerStatus.Raft.Status.Leader,
+			Reachability: types.Reachability(strings.ToLower(n.ManagerStatus.Raft.Status.Reachability.String())),
+			Addr:         n.ManagerStatus.Raft.Addr,
+		}
+	}
+
+	return node
+}
+
+// NodeSpecToGRPC converts a NodeSpec to a grpc NodeSpec.
+func NodeSpecToGRPC(s types.NodeSpec) (swarmapi.NodeSpec, error) {
+	spec := swarmapi.NodeSpec{
+		Annotations: swarmapi.Annotations{
+			Name:   s.Name,
+			Labels: s.Labels,
+		},
+	}
+	if role, ok := swarmapi.NodeRole_value[strings.ToUpper(string(s.Role))]; ok {
+		spec.Role = swarmapi.NodeRole(role)
+	} else {
+		return swarmapi.NodeSpec{}, fmt.Errorf("invalid Role: %q", s.Role)
+	}
+
+	if membership, ok := swarmapi.NodeSpec_Membership_value[strings.ToUpper(string(s.Membership))]; ok {
+		spec.Membership = swarmapi.NodeSpec_Membership(membership)
+	} else {
+		return swarmapi.NodeSpec{}, fmt.Errorf("invalid Membership: %q", s.Membership)
+	}
+
+	if availability, ok := swarmapi.NodeSpec_Availability_value[strings.ToUpper(string(s.Availability))]; ok {
+		spec.Availability = swarmapi.NodeSpec_Availability(availability)
+	} else {
+		return swarmapi.NodeSpec{}, fmt.Errorf("invalid Availability: %q", s.Availability)
+	}
+
+	return spec, nil
+}

+ 252 - 0
daemon/cluster/convert/service.go

@@ -0,0 +1,252 @@
+package convert
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/docker/docker/pkg/namesgenerator"
+	types "github.com/docker/engine-api/types/swarm"
+	swarmapi "github.com/docker/swarmkit/api"
+	"github.com/docker/swarmkit/protobuf/ptypes"
+)
+
+// ServiceFromGRPC converts a grpc Service to a Service.
+func ServiceFromGRPC(s swarmapi.Service) types.Service {
+	spec := s.Spec
+	containerConfig := spec.Task.Runtime.(*swarmapi.TaskSpec_Container).Container
+
+	networks := make([]types.NetworkAttachmentConfig, 0, len(spec.Networks))
+	for _, n := range spec.Networks {
+		networks = append(networks, types.NetworkAttachmentConfig{Target: n.Target, Aliases: n.Aliases})
+	}
+	service := types.Service{
+		ID: s.ID,
+
+		Spec: types.ServiceSpec{
+			TaskTemplate: types.TaskSpec{
+				ContainerSpec: containerSpecFromGRPC(containerConfig),
+				Resources:     resourcesFromGRPC(s.Spec.Task.Resources),
+				RestartPolicy: restartPolicyFromGRPC(s.Spec.Task.Restart),
+				Placement:     placementFromGRPC(s.Spec.Task.Placement),
+			},
+
+			Networks:     networks,
+			EndpointSpec: endpointSpecFromGRPC(s.Spec.Endpoint),
+		},
+		Endpoint: endpointFromGRPC(s.Endpoint),
+	}
+
+	// Meta
+	service.Version.Index = s.Meta.Version.Index
+	service.CreatedAt, _ = ptypes.Timestamp(s.Meta.CreatedAt)
+	service.UpdatedAt, _ = ptypes.Timestamp(s.Meta.UpdatedAt)
+
+	// Annotations
+	service.Spec.Name = s.Spec.Annotations.Name
+	service.Spec.Labels = s.Spec.Annotations.Labels
+
+	// UpdateConfig
+	if s.Spec.Update != nil {
+		service.Spec.UpdateConfig = &types.UpdateConfig{
+			Parallelism: s.Spec.Update.Parallelism,
+		}
+
+		service.Spec.UpdateConfig.Delay, _ = ptypes.Duration(&s.Spec.Update.Delay)
+	}
+
+	//Mode
+	switch t := s.Spec.GetMode().(type) {
+	case *swarmapi.ServiceSpec_Global:
+		service.Spec.Mode.Global = &types.GlobalService{}
+	case *swarmapi.ServiceSpec_Replicated:
+		service.Spec.Mode.Replicated = &types.ReplicatedService{
+			Replicas: &t.Replicated.Replicas,
+		}
+	}
+
+	return service
+}
+
+// ServiceSpecToGRPC converts a ServiceSpec to a grpc ServiceSpec.
+func ServiceSpecToGRPC(s types.ServiceSpec) (swarmapi.ServiceSpec, error) {
+	name := s.Name
+	if name == "" {
+		name = namesgenerator.GetRandomName(0)
+	}
+
+	networks := make([]*swarmapi.ServiceSpec_NetworkAttachmentConfig, 0, len(s.Networks))
+	for _, n := range s.Networks {
+		networks = append(networks, &swarmapi.ServiceSpec_NetworkAttachmentConfig{Target: n.Target, Aliases: n.Aliases})
+	}
+
+	spec := swarmapi.ServiceSpec{
+		Annotations: swarmapi.Annotations{
+			Name:   name,
+			Labels: s.Labels,
+		},
+		Task: swarmapi.TaskSpec{
+			Resources: resourcesToGRPC(s.TaskTemplate.Resources),
+		},
+		Networks: networks,
+	}
+
+	containerSpec, err := containerToGRPC(s.TaskTemplate.ContainerSpec)
+	if err != nil {
+		return swarmapi.ServiceSpec{}, err
+	}
+	spec.Task.Runtime = &swarmapi.TaskSpec_Container{Container: containerSpec}
+
+	restartPolicy, err := restartPolicyToGRPC(s.TaskTemplate.RestartPolicy)
+	if err != nil {
+		return swarmapi.ServiceSpec{}, err
+	}
+	spec.Task.Restart = restartPolicy
+
+	if s.TaskTemplate.Placement != nil {
+		spec.Task.Placement = &swarmapi.Placement{
+			Constraints: s.TaskTemplate.Placement.Constraints,
+		}
+	}
+
+	if s.UpdateConfig != nil {
+		spec.Update = &swarmapi.UpdateConfig{
+			Parallelism: s.UpdateConfig.Parallelism,
+			Delay:       *ptypes.DurationProto(s.UpdateConfig.Delay),
+		}
+	}
+
+	if s.EndpointSpec != nil {
+		if s.EndpointSpec.Mode != "" &&
+			s.EndpointSpec.Mode != types.ResolutionModeVIP &&
+			s.EndpointSpec.Mode != types.ResolutionModeDNSRR {
+			return swarmapi.ServiceSpec{}, fmt.Errorf("invalid resolution mode: %q", s.EndpointSpec.Mode)
+		}
+
+		spec.Endpoint = &swarmapi.EndpointSpec{}
+
+		spec.Endpoint.Mode = swarmapi.EndpointSpec_ResolutionMode(swarmapi.EndpointSpec_ResolutionMode_value[strings.ToUpper(string(s.EndpointSpec.Mode))])
+
+		for _, portConfig := range s.EndpointSpec.Ports {
+			spec.Endpoint.Ports = append(spec.Endpoint.Ports, &swarmapi.PortConfig{
+				Name:          portConfig.Name,
+				Protocol:      swarmapi.PortConfig_Protocol(swarmapi.PortConfig_Protocol_value[strings.ToUpper(string(portConfig.Protocol))]),
+				TargetPort:    portConfig.TargetPort,
+				PublishedPort: portConfig.PublishedPort,
+			})
+		}
+	}
+
+	//Mode
+	if s.Mode.Global != nil {
+		spec.Mode = &swarmapi.ServiceSpec_Global{
+			Global: &swarmapi.GlobalService{},
+		}
+	} else if s.Mode.Replicated != nil && s.Mode.Replicated.Replicas != nil {
+		spec.Mode = &swarmapi.ServiceSpec_Replicated{
+			Replicated: &swarmapi.ReplicatedService{Replicas: *s.Mode.Replicated.Replicas},
+		}
+	} else {
+		spec.Mode = &swarmapi.ServiceSpec_Replicated{
+			Replicated: &swarmapi.ReplicatedService{Replicas: 1},
+		}
+	}
+
+	return spec, nil
+}
+
+func resourcesFromGRPC(res *swarmapi.ResourceRequirements) *types.ResourceRequirements {
+	var resources *types.ResourceRequirements
+	if res != nil {
+		resources = &types.ResourceRequirements{}
+		if res.Limits != nil {
+			resources.Limits = &types.Resources{
+				NanoCPUs:    res.Limits.NanoCPUs,
+				MemoryBytes: res.Limits.MemoryBytes,
+			}
+		}
+		if res.Reservations != nil {
+			resources.Reservations = &types.Resources{
+				NanoCPUs:    res.Reservations.NanoCPUs,
+				MemoryBytes: res.Reservations.MemoryBytes,
+			}
+		}
+	}
+
+	return resources
+}
+
+func resourcesToGRPC(res *types.ResourceRequirements) *swarmapi.ResourceRequirements {
+	var reqs *swarmapi.ResourceRequirements
+	if res != nil {
+		reqs = &swarmapi.ResourceRequirements{}
+		if res.Limits != nil {
+			reqs.Limits = &swarmapi.Resources{
+				NanoCPUs:    res.Limits.NanoCPUs,
+				MemoryBytes: res.Limits.MemoryBytes,
+			}
+		}
+		if res.Reservations != nil {
+			reqs.Reservations = &swarmapi.Resources{
+				NanoCPUs:    res.Reservations.NanoCPUs,
+				MemoryBytes: res.Reservations.MemoryBytes,
+			}
+
+		}
+	}
+	return reqs
+}
+
+func restartPolicyFromGRPC(p *swarmapi.RestartPolicy) *types.RestartPolicy {
+	var rp *types.RestartPolicy
+	if p != nil {
+		rp = &types.RestartPolicy{}
+		rp.Condition = types.RestartPolicyCondition(strings.ToLower(p.Condition.String()))
+		if p.Delay != nil {
+			delay, _ := ptypes.Duration(p.Delay)
+			rp.Delay = &delay
+		}
+		if p.Window != nil {
+			window, _ := ptypes.Duration(p.Window)
+			rp.Window = &window
+		}
+
+		rp.MaxAttempts = &p.MaxAttempts
+	}
+	return rp
+}
+
+func restartPolicyToGRPC(p *types.RestartPolicy) (*swarmapi.RestartPolicy, error) {
+	var rp *swarmapi.RestartPolicy
+	if p != nil {
+		rp = &swarmapi.RestartPolicy{}
+		if condition, ok := swarmapi.RestartPolicy_RestartCondition_value[strings.ToUpper(string(p.Condition))]; ok {
+			rp.Condition = swarmapi.RestartPolicy_RestartCondition(condition)
+		} else if string(p.Condition) == "" {
+			rp.Condition = swarmapi.RestartOnAny
+		} else {
+			return nil, fmt.Errorf("invalid RestartCondition: %q", p.Condition)
+		}
+
+		if p.Delay != nil {
+			rp.Delay = ptypes.DurationProto(*p.Delay)
+		}
+		if p.Window != nil {
+			rp.Window = ptypes.DurationProto(*p.Window)
+		}
+		if p.MaxAttempts != nil {
+			rp.MaxAttempts = *p.MaxAttempts
+
+		}
+	}
+	return rp, nil
+}
+
+func placementFromGRPC(p *swarmapi.Placement) *types.Placement {
+	var r *types.Placement
+	if p != nil {
+		r = &types.Placement{}
+		r.Constraints = p.Constraints
+	}
+
+	return r
+}

+ 116 - 0
daemon/cluster/convert/swarm.go

@@ -0,0 +1,116 @@
+package convert
+
+import (
+	"fmt"
+	"strings"
+
+	"golang.org/x/crypto/bcrypt"
+
+	types "github.com/docker/engine-api/types/swarm"
+	swarmapi "github.com/docker/swarmkit/api"
+	"github.com/docker/swarmkit/protobuf/ptypes"
+)
+
+// SwarmFromGRPC converts a grpc Cluster to a Swarm.
+func SwarmFromGRPC(c swarmapi.Cluster) types.Swarm {
+	swarm := types.Swarm{
+		ID: c.ID,
+		Spec: types.Spec{
+			Orchestration: types.OrchestrationConfig{
+				TaskHistoryRetentionLimit: c.Spec.Orchestration.TaskHistoryRetentionLimit,
+			},
+			Raft: types.RaftConfig{
+				SnapshotInterval:           c.Spec.Raft.SnapshotInterval,
+				KeepOldSnapshots:           c.Spec.Raft.KeepOldSnapshots,
+				LogEntriesForSlowFollowers: c.Spec.Raft.LogEntriesForSlowFollowers,
+				HeartbeatTick:              c.Spec.Raft.HeartbeatTick,
+				ElectionTick:               c.Spec.Raft.ElectionTick,
+			},
+			Dispatcher: types.DispatcherConfig{
+				HeartbeatPeriod: c.Spec.Dispatcher.HeartbeatPeriod,
+			},
+		},
+	}
+
+	swarm.Spec.CAConfig.NodeCertExpiry, _ = ptypes.Duration(c.Spec.CAConfig.NodeCertExpiry)
+
+	// Meta
+	swarm.Version.Index = c.Meta.Version.Index
+	swarm.CreatedAt, _ = ptypes.Timestamp(c.Meta.CreatedAt)
+	swarm.UpdatedAt, _ = ptypes.Timestamp(c.Meta.UpdatedAt)
+
+	// Annotations
+	swarm.Spec.Name = c.Spec.Annotations.Name
+	swarm.Spec.Labels = c.Spec.Annotations.Labels
+
+	for _, policy := range c.Spec.AcceptancePolicy.Policies {
+		p := types.Policy{
+			Role:       types.NodeRole(strings.ToLower(policy.Role.String())),
+			Autoaccept: policy.Autoaccept,
+		}
+		if policy.Secret != nil {
+			p.Secret = string(policy.Secret.Data)
+		}
+		swarm.Spec.AcceptancePolicy.Policies = append(swarm.Spec.AcceptancePolicy.Policies, p)
+	}
+
+	return swarm
+}
+
+// SwarmSpecToGRPC converts a Spec to a grpc ClusterSpec.
+func SwarmSpecToGRPC(s types.Spec) (swarmapi.ClusterSpec, error) {
+	spec := swarmapi.ClusterSpec{
+		Annotations: swarmapi.Annotations{
+			Name:   s.Name,
+			Labels: s.Labels,
+		},
+		Orchestration: swarmapi.OrchestrationConfig{
+			TaskHistoryRetentionLimit: s.Orchestration.TaskHistoryRetentionLimit,
+		},
+		Raft: swarmapi.RaftConfig{
+			SnapshotInterval:           s.Raft.SnapshotInterval,
+			KeepOldSnapshots:           s.Raft.KeepOldSnapshots,
+			LogEntriesForSlowFollowers: s.Raft.LogEntriesForSlowFollowers,
+			HeartbeatTick:              s.Raft.HeartbeatTick,
+			ElectionTick:               s.Raft.ElectionTick,
+		},
+		Dispatcher: swarmapi.DispatcherConfig{
+			HeartbeatPeriod: s.Dispatcher.HeartbeatPeriod,
+		},
+		CAConfig: swarmapi.CAConfig{
+			NodeCertExpiry: ptypes.DurationProto(s.CAConfig.NodeCertExpiry),
+		},
+	}
+
+	if err := SwarmSpecUpdateAcceptancePolicy(&spec, s.AcceptancePolicy); err != nil {
+		return swarmapi.ClusterSpec{}, err
+	}
+	return spec, nil
+}
+
+// SwarmSpecUpdateAcceptancePolicy updates a grpc ClusterSpec using AcceptancePolicy.
+func SwarmSpecUpdateAcceptancePolicy(spec *swarmapi.ClusterSpec, acceptancePolicy types.AcceptancePolicy) error {
+	spec.AcceptancePolicy.Policies = nil
+	for _, p := range acceptancePolicy.Policies {
+		role, ok := swarmapi.NodeRole_value[strings.ToUpper(string(p.Role))]
+		if !ok {
+			return fmt.Errorf("invalid Role: %q", p.Role)
+		}
+
+		policy := &swarmapi.AcceptancePolicy_RoleAdmissionPolicy{
+			Role:       swarmapi.NodeRole(role),
+			Autoaccept: p.Autoaccept,
+		}
+
+		if p.Secret != "" {
+			hashPwd, _ := bcrypt.GenerateFromPassword([]byte(p.Secret), 0)
+			policy.Secret = &swarmapi.AcceptancePolicy_RoleAdmissionPolicy_HashedSecret{
+				Data: hashPwd,
+				Alg:  "bcrypt",
+			}
+		}
+
+		spec.AcceptancePolicy.Policies = append(spec.AcceptancePolicy.Policies, policy)
+	}
+	return nil
+}

+ 53 - 0
daemon/cluster/convert/task.go

@@ -0,0 +1,53 @@
+package convert
+
+import (
+	"strings"
+
+	types "github.com/docker/engine-api/types/swarm"
+	swarmapi "github.com/docker/swarmkit/api"
+	"github.com/docker/swarmkit/protobuf/ptypes"
+)
+
+// TaskFromGRPC converts a grpc Task to a Task.
+func TaskFromGRPC(t swarmapi.Task) types.Task {
+	containerConfig := t.Spec.Runtime.(*swarmapi.TaskSpec_Container).Container
+	containerStatus := t.Status.GetContainer()
+	task := types.Task{
+		ID:        t.ID,
+		ServiceID: t.ServiceID,
+		Slot:      int(t.Slot),
+		NodeID:    t.NodeID,
+		Spec: types.TaskSpec{
+			ContainerSpec: containerSpecFromGRPC(containerConfig),
+			Resources:     resourcesFromGRPC(t.Spec.Resources),
+			RestartPolicy: restartPolicyFromGRPC(t.Spec.Restart),
+			Placement:     placementFromGRPC(t.Spec.Placement),
+		},
+		Status: types.TaskStatus{
+			State:   types.TaskState(strings.ToLower(t.Status.State.String())),
+			Message: t.Status.Message,
+			Err:     t.Status.Err,
+		},
+		DesiredState: types.TaskState(strings.ToLower(t.DesiredState.String())),
+	}
+
+	// Meta
+	task.Version.Index = t.Meta.Version.Index
+	task.CreatedAt, _ = ptypes.Timestamp(t.Meta.CreatedAt)
+	task.UpdatedAt, _ = ptypes.Timestamp(t.Meta.UpdatedAt)
+
+	task.Status.Timestamp, _ = ptypes.Timestamp(t.Status.Timestamp)
+
+	if containerStatus != nil {
+		task.Status.ContainerStatus.ContainerID = containerStatus.ContainerID
+		task.Status.ContainerStatus.PID = int(containerStatus.PID)
+		task.Status.ContainerStatus.ExitCode = int(containerStatus.ExitCode)
+	}
+
+	// NetworksAttachments
+	for _, na := range t.Networks {
+		task.NetworksAttachments = append(task.NetworksAttachments, networkAttachementFromGRPC(na))
+	}
+
+	return task
+}

+ 35 - 0
daemon/cluster/executor/backend.go

@@ -0,0 +1,35 @@
+package executor
+
+import (
+	"io"
+
+	clustertypes "github.com/docker/docker/daemon/cluster/provider"
+	"github.com/docker/engine-api/types"
+	"github.com/docker/engine-api/types/container"
+	"github.com/docker/engine-api/types/network"
+	"github.com/docker/libnetwork/cluster"
+	networktypes "github.com/docker/libnetwork/types"
+	"golang.org/x/net/context"
+)
+
+// Backend defines the executor component for a swarm agent.
+type Backend interface {
+	CreateManagedNetwork(clustertypes.NetworkCreateRequest) error
+	DeleteManagedNetwork(name string) error
+	SetupIngress(req clustertypes.NetworkCreateRequest, nodeIP string) error
+	PullImage(ctx context.Context, image, tag string, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error
+	CreateManagedContainer(types.ContainerCreateConfig) (types.ContainerCreateResponse, error)
+	ContainerStart(name string, hostConfig *container.HostConfig) error
+	ContainerStop(name string, seconds int) error
+	ConnectContainerToNetwork(containerName, networkName string, endpointConfig *network.EndpointSettings) error
+	UpdateContainerServiceConfig(containerName string, serviceConfig *clustertypes.ServiceConfig) error
+	ContainerInspectCurrent(name string, size bool) (*types.ContainerJSON, error)
+	ContainerWaitWithContext(ctx context.Context, name string) (<-chan int, error)
+	ContainerRm(name string, config *types.ContainerRmConfig) error
+	ContainerKill(name string, sig uint64) error
+	SystemInfo() (*types.Info, error)
+	VolumeCreate(name, driverName string, opts, labels map[string]string) (*types.Volume, error)
+	ListContainersForNode(nodeID string) []string
+	SetNetworkBootstrapKeys([]*networktypes.EncryptionKey) error
+	SetClusterProvider(provider cluster.Provider)
+}

+ 229 - 0
daemon/cluster/executor/container/adapter.go

@@ -0,0 +1,229 @@
+package container
+
+import (
+	"encoding/base64"
+	"encoding/json"
+	"fmt"
+	"io"
+	"strings"
+	"syscall"
+
+	"github.com/Sirupsen/logrus"
+	executorpkg "github.com/docker/docker/daemon/cluster/executor"
+	"github.com/docker/engine-api/types"
+	"github.com/docker/libnetwork"
+	"github.com/docker/swarmkit/api"
+	"github.com/docker/swarmkit/log"
+	"golang.org/x/net/context"
+)
+
+// containerAdapter conducts remote operations for a container. All calls
+// are mostly naked calls to the client API, seeded with information from
+// containerConfig.
+type containerAdapter struct {
+	backend   executorpkg.Backend
+	container *containerConfig
+}
+
+func newContainerAdapter(b executorpkg.Backend, task *api.Task) (*containerAdapter, error) {
+	ctnr, err := newContainerConfig(task)
+	if err != nil {
+		return nil, err
+	}
+
+	return &containerAdapter{
+		container: ctnr,
+		backend:   b,
+	}, nil
+}
+
+func (c *containerAdapter) pullImage(ctx context.Context) error {
+	// if the image needs to be pulled, the auth config will be retrieved and updated
+	encodedAuthConfig := c.container.task.ServiceAnnotations.Labels[fmt.Sprintf("%v.registryauth", systemLabelPrefix)]
+
+	authConfig := &types.AuthConfig{}
+	if encodedAuthConfig != "" {
+		if err := json.NewDecoder(base64.NewDecoder(base64.URLEncoding, strings.NewReader(encodedAuthConfig))).Decode(authConfig); err != nil {
+			logrus.Warnf("invalid authconfig: %v", err)
+		}
+	}
+
+	pr, pw := io.Pipe()
+	metaHeaders := map[string][]string{}
+	go func() {
+		err := c.backend.PullImage(ctx, c.container.image(), "", metaHeaders, authConfig, pw)
+		pw.CloseWithError(err)
+	}()
+
+	dec := json.NewDecoder(pr)
+	m := map[string]interface{}{}
+	for {
+		if err := dec.Decode(&m); err != nil {
+			if err == io.EOF {
+				break
+			}
+			return err
+		}
+		// TOOD(stevvooe): Report this status somewhere.
+		logrus.Debugln("pull progress", m)
+	}
+	// if the final stream object contained an error, return it
+	if errMsg, ok := m["error"]; ok {
+		return fmt.Errorf("%v", errMsg)
+	}
+	return nil
+}
+
+func (c *containerAdapter) createNetworks(ctx context.Context) error {
+	for _, network := range c.container.networks() {
+		ncr, err := c.container.networkCreateRequest(network)
+		if err != nil {
+			return err
+		}
+
+		if err := c.backend.CreateManagedNetwork(ncr); err != nil { // todo name missing
+			if _, ok := err.(libnetwork.NetworkNameError); ok {
+				continue
+			}
+
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (c *containerAdapter) removeNetworks(ctx context.Context) error {
+	for _, nid := range c.container.networks() {
+		if err := c.backend.DeleteManagedNetwork(nid); err != nil {
+			if _, ok := err.(*libnetwork.ActiveEndpointsError); ok {
+				continue
+			}
+			log.G(ctx).Errorf("network %s remove failed: %v", nid, err)
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (c *containerAdapter) create(ctx context.Context, backend executorpkg.Backend) error {
+	var cr types.ContainerCreateResponse
+	var err error
+	if cr, err = backend.CreateManagedContainer(types.ContainerCreateConfig{
+		Name:       c.container.name(),
+		Config:     c.container.config(),
+		HostConfig: c.container.hostConfig(),
+		// Use the first network in container create
+		NetworkingConfig: c.container.createNetworkingConfig(),
+	}); err != nil {
+		return err
+	}
+
+	// Docker daemon currently doesnt support multiple networks in container create
+	// Connect to all other networks
+	nc := c.container.connectNetworkingConfig()
+
+	if nc != nil {
+		for n, ep := range nc.EndpointsConfig {
+			logrus.Errorf("CONNECT %s : %v", n, ep.IPAMConfig.IPv4Address)
+			if err := backend.ConnectContainerToNetwork(cr.ID, n, ep); err != nil {
+				return err
+			}
+		}
+	}
+
+	if err := backend.UpdateContainerServiceConfig(cr.ID, c.container.serviceConfig()); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (c *containerAdapter) start(ctx context.Context) error {
+	return c.backend.ContainerStart(c.container.name(), nil)
+}
+
+func (c *containerAdapter) inspect(ctx context.Context) (types.ContainerJSON, error) {
+	cs, err := c.backend.ContainerInspectCurrent(c.container.name(), false)
+	if ctx.Err() != nil {
+		return types.ContainerJSON{}, ctx.Err()
+	}
+	if err != nil {
+		return types.ContainerJSON{}, err
+	}
+	return *cs, nil
+}
+
+// events issues a call to the events API and returns a channel with all
+// events. The stream of events can be shutdown by cancelling the context.
+//
+// A chan struct{} is returned that will be closed if the event procressing
+// fails and needs to be restarted.
+func (c *containerAdapter) wait(ctx context.Context) (<-chan int, error) {
+	return c.backend.ContainerWaitWithContext(ctx, c.container.name())
+}
+
+func (c *containerAdapter) shutdown(ctx context.Context) error {
+	// Default stop grace period to 10s.
+	stopgrace := 10
+	spec := c.container.spec()
+	if spec.StopGracePeriod != nil {
+		stopgrace = int(spec.StopGracePeriod.Seconds)
+	}
+	return c.backend.ContainerStop(c.container.name(), stopgrace)
+}
+
+func (c *containerAdapter) terminate(ctx context.Context) error {
+	return c.backend.ContainerKill(c.container.name(), uint64(syscall.SIGKILL))
+}
+
+func (c *containerAdapter) remove(ctx context.Context) error {
+	return c.backend.ContainerRm(c.container.name(), &types.ContainerRmConfig{
+		RemoveVolume: true,
+		ForceRemove:  true,
+	})
+}
+
+func (c *containerAdapter) createVolumes(ctx context.Context, backend executorpkg.Backend) error {
+	// Create plugin volumes that are embedded inside a Mount
+	for _, mount := range c.container.task.Spec.GetContainer().Mounts {
+		if mount.Type != api.MountTypeVolume {
+			continue
+		}
+
+		if mount.VolumeOptions != nil {
+			continue
+		}
+
+		if mount.VolumeOptions.DriverConfig == nil {
+			continue
+		}
+
+		req := c.container.volumeCreateRequest(&mount)
+
+		// Check if this volume exists on the engine
+		if _, err := backend.VolumeCreate(req.Name, req.Driver, req.DriverOpts, req.Labels); err != nil {
+			// TODO(amitshukla): Today, volume create through the engine api does not return an error
+			// when the named volume with the same parameters already exists.
+			// It returns an error if the driver name is different - that is a valid error
+			return err
+		}
+
+	}
+
+	return nil
+}
+
+// todo: typed/wrapped errors
+func isContainerCreateNameConflict(err error) bool {
+	return strings.Contains(err.Error(), "Conflict. The name")
+}
+
+func isUnknownContainer(err error) bool {
+	return strings.Contains(err.Error(), "No such container:")
+}
+
+func isStoppedContainer(err error) bool {
+	return strings.Contains(err.Error(), "is already stopped")
+}

+ 415 - 0
daemon/cluster/executor/container/container.go

@@ -0,0 +1,415 @@
+package container
+
+import (
+	"errors"
+	"fmt"
+	"log"
+	"net"
+	"strings"
+	"time"
+
+	clustertypes "github.com/docker/docker/daemon/cluster/provider"
+	"github.com/docker/docker/reference"
+	"github.com/docker/engine-api/types"
+	enginecontainer "github.com/docker/engine-api/types/container"
+	"github.com/docker/engine-api/types/network"
+	"github.com/docker/swarmkit/agent/exec"
+	"github.com/docker/swarmkit/api"
+)
+
+const (
+	// Explictly use the kernel's default setting for CPU quota of 100ms.
+	// https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt
+	cpuQuotaPeriod = 100 * time.Millisecond
+
+	// systemLabelPrefix represents the reserved namespace for system labels.
+	systemLabelPrefix = "com.docker.swarm"
+)
+
+// containerConfig converts task properties into docker container compatible
+// components.
+type containerConfig struct {
+	task                *api.Task
+	networksAttachments map[string]*api.NetworkAttachment
+}
+
+// newContainerConfig returns a validated container config. No methods should
+// return an error if this function returns without error.
+func newContainerConfig(t *api.Task) (*containerConfig, error) {
+	var c containerConfig
+	return &c, c.setTask(t)
+}
+
+func (c *containerConfig) setTask(t *api.Task) error {
+	container := t.Spec.GetContainer()
+	if container == nil {
+		return exec.ErrRuntimeUnsupported
+	}
+
+	if container.Image == "" {
+		return ErrImageRequired
+	}
+
+	// index the networks by name
+	c.networksAttachments = make(map[string]*api.NetworkAttachment, len(t.Networks))
+	for _, attachment := range t.Networks {
+		c.networksAttachments[attachment.Network.Spec.Annotations.Name] = attachment
+	}
+
+	c.task = t
+	return nil
+}
+
+func (c *containerConfig) endpoint() *api.Endpoint {
+	return c.task.Endpoint
+}
+
+func (c *containerConfig) spec() *api.ContainerSpec {
+	return c.task.Spec.GetContainer()
+}
+
+func (c *containerConfig) name() string {
+	if c.task.Annotations.Name != "" {
+		// if set, use the container Annotations.Name field, set in the orchestrator.
+		return c.task.Annotations.Name
+	}
+
+	// fallback to service.slot.id.
+	return strings.Join([]string{c.task.ServiceAnnotations.Name, fmt.Sprint(c.task.Slot), c.task.ID}, ".")
+}
+
+func (c *containerConfig) image() string {
+	raw := c.spec().Image
+	ref, err := reference.ParseNamed(raw)
+	if err != nil {
+		return raw
+	}
+	return reference.WithDefaultTag(ref).String()
+}
+
+func (c *containerConfig) volumes() map[string]struct{} {
+	r := make(map[string]struct{})
+
+	for _, mount := range c.spec().Mounts {
+		// pick off all the volume mounts.
+		if mount.Type != api.MountTypeVolume {
+			continue
+		}
+
+		r[fmt.Sprintf("%s:%s", mount.Target, getMountMask(&mount))] = struct{}{}
+	}
+
+	return r
+}
+
+func (c *containerConfig) config() *enginecontainer.Config {
+	config := &enginecontainer.Config{
+		Labels:     c.labels(),
+		User:       c.spec().User,
+		Env:        c.spec().Env,
+		WorkingDir: c.spec().Dir,
+		Image:      c.image(),
+		Volumes:    c.volumes(),
+	}
+
+	if len(c.spec().Command) > 0 {
+		// If Command is provided, we replace the whole invocation with Command
+		// by replacing Entrypoint and specifying Cmd. Args is ignored in this
+		// case.
+		config.Entrypoint = append(config.Entrypoint, c.spec().Command[0])
+		config.Cmd = append(config.Cmd, c.spec().Command[1:]...)
+	} else if len(c.spec().Args) > 0 {
+		// In this case, we assume the image has an Entrypoint and Args
+		// specifies the arguments for that entrypoint.
+		config.Cmd = c.spec().Args
+	}
+
+	return config
+}
+
+func (c *containerConfig) labels() map[string]string {
+	var (
+		system = map[string]string{
+			"task":         "", // mark as cluster task
+			"task.id":      c.task.ID,
+			"task.name":    fmt.Sprintf("%v.%v", c.task.ServiceAnnotations.Name, c.task.Slot),
+			"node.id":      c.task.NodeID,
+			"service.id":   c.task.ServiceID,
+			"service.name": c.task.ServiceAnnotations.Name,
+		}
+		labels = make(map[string]string)
+	)
+
+	// base labels are those defined in the spec.
+	for k, v := range c.spec().Labels {
+		labels[k] = v
+	}
+
+	// we then apply the overrides from the task, which may be set via the
+	// orchestrator.
+	for k, v := range c.task.Annotations.Labels {
+		labels[k] = v
+	}
+
+	// finally, we apply the system labels, which override all labels.
+	for k, v := range system {
+		labels[strings.Join([]string{systemLabelPrefix, k}, ".")] = v
+	}
+
+	return labels
+}
+
+func (c *containerConfig) bindMounts() []string {
+	var r []string
+
+	for _, val := range c.spec().Mounts {
+		mask := getMountMask(&val)
+		if val.Type == api.MountTypeBind {
+			r = append(r, fmt.Sprintf("%s:%s:%s", val.Source, val.Target, mask))
+		}
+	}
+
+	return r
+}
+
+func getMountMask(m *api.Mount) string {
+	maskOpts := []string{"ro"}
+	if m.Writable {
+		maskOpts[0] = "rw"
+	}
+
+	if m.BindOptions != nil {
+		switch m.BindOptions.Propagation {
+		case api.MountPropagationPrivate:
+			maskOpts = append(maskOpts, "private")
+		case api.MountPropagationRPrivate:
+			maskOpts = append(maskOpts, "rprivate")
+		case api.MountPropagationShared:
+			maskOpts = append(maskOpts, "shared")
+		case api.MountPropagationRShared:
+			maskOpts = append(maskOpts, "rshared")
+		case api.MountPropagationSlave:
+			maskOpts = append(maskOpts, "slave")
+		case api.MountPropagationRSlave:
+			maskOpts = append(maskOpts, "rslave")
+		}
+	}
+
+	if m.VolumeOptions != nil {
+		if !m.VolumeOptions.Populate {
+			maskOpts = append(maskOpts, "nocopy")
+		}
+	}
+	return strings.Join(maskOpts, ",")
+}
+
+func (c *containerConfig) hostConfig() *enginecontainer.HostConfig {
+	return &enginecontainer.HostConfig{
+		Resources: c.resources(),
+		Binds:     c.bindMounts(),
+	}
+}
+
+// This handles the case of volumes that are defined inside a service Mount
+func (c *containerConfig) volumeCreateRequest(mount *api.Mount) *types.VolumeCreateRequest {
+	var (
+		driverName string
+		driverOpts map[string]string
+		labels     map[string]string
+	)
+
+	if mount.VolumeOptions != nil && mount.VolumeOptions.DriverConfig != nil {
+		driverName = mount.VolumeOptions.DriverConfig.Name
+		driverOpts = mount.VolumeOptions.DriverConfig.Options
+		labels = mount.VolumeOptions.Labels
+	}
+
+	if mount.VolumeOptions != nil {
+		return &types.VolumeCreateRequest{
+			Name:       mount.Source,
+			Driver:     driverName,
+			DriverOpts: driverOpts,
+			Labels:     labels,
+		}
+	}
+	return nil
+}
+
+func (c *containerConfig) resources() enginecontainer.Resources {
+	resources := enginecontainer.Resources{}
+
+	// If no limits are specified let the engine use its defaults.
+	//
+	// TODO(aluzzardi): We might want to set some limits anyway otherwise
+	// "unlimited" tasks will step over the reservation of other tasks.
+	r := c.task.Spec.Resources
+	if r == nil || r.Limits == nil {
+		return resources
+	}
+
+	if r.Limits.MemoryBytes > 0 {
+		resources.Memory = r.Limits.MemoryBytes
+	}
+
+	if r.Limits.NanoCPUs > 0 {
+		// CPU Period must be set in microseconds.
+		resources.CPUPeriod = int64(cpuQuotaPeriod / time.Microsecond)
+		resources.CPUQuota = r.Limits.NanoCPUs * resources.CPUPeriod / 1e9
+	}
+
+	return resources
+}
+
+// Docker daemon supports just 1 network during container create.
+func (c *containerConfig) createNetworkingConfig() *network.NetworkingConfig {
+	var networks []*api.NetworkAttachment
+	if c.task.Spec.GetContainer() != nil {
+		networks = c.task.Networks
+	}
+
+	epConfig := make(map[string]*network.EndpointSettings)
+	if len(networks) > 0 {
+		epConfig[networks[0].Network.Spec.Annotations.Name] = getEndpointConfig(networks[0])
+	}
+
+	return &network.NetworkingConfig{EndpointsConfig: epConfig}
+}
+
+// TODO: Merge this function with createNetworkingConfig after daemon supports multiple networks in container create
+func (c *containerConfig) connectNetworkingConfig() *network.NetworkingConfig {
+	var networks []*api.NetworkAttachment
+	if c.task.Spec.GetContainer() != nil {
+		networks = c.task.Networks
+	}
+
+	// First network is used during container create. Other networks are used in "docker network connect"
+	if len(networks) < 2 {
+		return nil
+	}
+
+	epConfig := make(map[string]*network.EndpointSettings)
+	for _, na := range networks[1:] {
+		epConfig[na.Network.Spec.Annotations.Name] = getEndpointConfig(na)
+	}
+	return &network.NetworkingConfig{EndpointsConfig: epConfig}
+}
+
+func getEndpointConfig(na *api.NetworkAttachment) *network.EndpointSettings {
+	var ipv4, ipv6 string
+	for _, addr := range na.Addresses {
+		ip, _, err := net.ParseCIDR(addr)
+		if err != nil {
+			continue
+		}
+
+		if ip.To4() != nil {
+			ipv4 = ip.String()
+			continue
+		}
+
+		if ip.To16() != nil {
+			ipv6 = ip.String()
+		}
+	}
+
+	return &network.EndpointSettings{
+		IPAMConfig: &network.EndpointIPAMConfig{
+			IPv4Address: ipv4,
+			IPv6Address: ipv6,
+		},
+	}
+}
+
+func (c *containerConfig) virtualIP(networkID string) string {
+	if c.task.Endpoint == nil {
+		return ""
+	}
+
+	for _, eVip := range c.task.Endpoint.VirtualIPs {
+		// We only support IPv4 VIPs for now.
+		if eVip.NetworkID == networkID {
+			vip, _, err := net.ParseCIDR(eVip.Addr)
+			if err != nil {
+				return ""
+			}
+
+			return vip.String()
+		}
+	}
+
+	return ""
+}
+
+func (c *containerConfig) serviceConfig() *clustertypes.ServiceConfig {
+	if len(c.task.Networks) == 0 {
+		return nil
+	}
+
+	log.Printf("Creating service config in agent for t = %+v", c.task)
+	svcCfg := &clustertypes.ServiceConfig{
+		Name:             c.task.ServiceAnnotations.Name,
+		ID:               c.task.ServiceID,
+		VirtualAddresses: make(map[string]*clustertypes.VirtualAddress),
+	}
+
+	for _, na := range c.task.Networks {
+		svcCfg.VirtualAddresses[na.Network.ID] = &clustertypes.VirtualAddress{
+			// We support only IPv4 virtual IP for now.
+			IPv4: c.virtualIP(na.Network.ID),
+		}
+	}
+
+	if c.task.Endpoint != nil {
+		for _, ePort := range c.task.Endpoint.Ports {
+			svcCfg.ExposedPorts = append(svcCfg.ExposedPorts, &clustertypes.PortConfig{
+				Name:          ePort.Name,
+				Protocol:      int32(ePort.Protocol),
+				TargetPort:    ePort.TargetPort,
+				PublishedPort: ePort.PublishedPort,
+			})
+		}
+	}
+
+	return svcCfg
+}
+
+// networks returns a list of network names attached to the container. The
+// returned name can be used to lookup the corresponding network create
+// options.
+func (c *containerConfig) networks() []string {
+	var networks []string
+
+	for name := range c.networksAttachments {
+		networks = append(networks, name)
+	}
+
+	return networks
+}
+
+func (c *containerConfig) networkCreateRequest(name string) (clustertypes.NetworkCreateRequest, error) {
+	na, ok := c.networksAttachments[name]
+	if !ok {
+		return clustertypes.NetworkCreateRequest{}, errors.New("container: unknown network referenced")
+	}
+
+	options := types.NetworkCreate{
+		// ID:     na.Network.ID,
+		Driver: na.Network.DriverState.Name,
+		IPAM: network.IPAM{
+			Driver: na.Network.IPAM.Driver.Name,
+		},
+		Options:        na.Network.DriverState.Options,
+		CheckDuplicate: true,
+	}
+
+	for _, ic := range na.Network.IPAM.Configs {
+		c := network.IPAMConfig{
+			Subnet:  ic.Subnet,
+			IPRange: ic.Range,
+			Gateway: ic.Gateway,
+		}
+		options.IPAM.Config = append(options.IPAM.Config, c)
+	}
+
+	return clustertypes.NetworkCreateRequest{na.Network.ID, types.NetworkCreateRequest{Name: name, NetworkCreate: options}}, nil
+}

+ 305 - 0
daemon/cluster/executor/container/controller.go

@@ -0,0 +1,305 @@
+package container
+
+import (
+	"errors"
+	"fmt"
+	"strings"
+
+	executorpkg "github.com/docker/docker/daemon/cluster/executor"
+	"github.com/docker/engine-api/types"
+	"github.com/docker/swarmkit/agent/exec"
+	"github.com/docker/swarmkit/api"
+	"github.com/docker/swarmkit/log"
+	"golang.org/x/net/context"
+)
+
+// controller implements agent.Controller against docker's API.
+//
+// Most operations against docker's API are done through the container name,
+// which is unique to the task.
+type controller struct {
+	backend executorpkg.Backend
+	task    *api.Task
+	adapter *containerAdapter
+	closed  chan struct{}
+	err     error
+}
+
+var _ exec.Controller = &controller{}
+
+// NewController returns a dockerexec runner for the provided task.
+func newController(b executorpkg.Backend, task *api.Task) (*controller, error) {
+	adapter, err := newContainerAdapter(b, task)
+	if err != nil {
+		return nil, err
+	}
+
+	return &controller{
+		backend: b,
+		task:    task,
+		adapter: adapter,
+		closed:  make(chan struct{}),
+	}, nil
+}
+
+func (r *controller) Task() (*api.Task, error) {
+	return r.task, nil
+}
+
+// ContainerStatus returns the container-specific status for the task.
+func (r *controller) ContainerStatus(ctx context.Context) (*api.ContainerStatus, error) {
+	ctnr, err := r.adapter.inspect(ctx)
+	if err != nil {
+		if isUnknownContainer(err) {
+			return nil, nil
+		}
+		return nil, err
+	}
+	return parseContainerStatus(ctnr)
+}
+
+// Update tasks a recent task update and applies it to the container.
+func (r *controller) Update(ctx context.Context, t *api.Task) error {
+	log.G(ctx).Warnf("task updates not yet supported")
+	// TODO(stevvooe): While assignment of tasks is idempotent, we do allow
+	// updates of metadata, such as labelling, as well as any other properties
+	// that make sense.
+	return nil
+}
+
+// Prepare creates a container and ensures the image is pulled.
+//
+// If the container has already be created, exec.ErrTaskPrepared is returned.
+func (r *controller) Prepare(ctx context.Context) error {
+	if err := r.checkClosed(); err != nil {
+		return err
+	}
+
+	// Make sure all the networks that the task needs are created.
+	if err := r.adapter.createNetworks(ctx); err != nil {
+		return err
+	}
+
+	// Make sure all the volumes that the task needs are created.
+	if err := r.adapter.createVolumes(ctx, r.backend); err != nil {
+		return err
+	}
+
+	for {
+		if err := r.checkClosed(); err != nil {
+			return err
+		}
+		if err := r.adapter.create(ctx, r.backend); err != nil {
+			if isContainerCreateNameConflict(err) {
+				if _, err := r.adapter.inspect(ctx); err != nil {
+					return err
+				}
+
+				// container is already created. success!
+				return exec.ErrTaskPrepared
+			}
+
+			if !strings.Contains(err.Error(), "No such image") { // todo: better error detection
+				return err
+			}
+			if err := r.adapter.pullImage(ctx); err != nil {
+				return err
+			}
+
+			continue // retry to create the container
+		}
+
+		break
+	}
+
+	return nil
+}
+
+// Start the container. An error will be returned if the container is already started.
+func (r *controller) Start(ctx context.Context) error {
+	if err := r.checkClosed(); err != nil {
+		return err
+	}
+
+	ctnr, err := r.adapter.inspect(ctx)
+	if err != nil {
+		return err
+	}
+
+	// Detect whether the container has *ever* been started. If so, we don't
+	// issue the start.
+	//
+	// TODO(stevvooe): This is very racy. While reading inspect, another could
+	// start the process and we could end up starting it twice.
+	if ctnr.State.Status != "created" {
+		return exec.ErrTaskStarted
+	}
+
+	if err := r.adapter.start(ctx); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// Wait on the container to exit.
+func (r *controller) Wait(pctx context.Context) error {
+	if err := r.checkClosed(); err != nil {
+		return err
+	}
+
+	ctx, cancel := context.WithCancel(pctx)
+	defer cancel()
+
+	c, err := r.adapter.wait(ctx)
+	if err != nil {
+		return err
+	}
+
+	<-c
+	if ctx.Err() != nil {
+		return ctx.Err()
+	}
+	ctnr, err := r.adapter.inspect(ctx)
+	if err != nil {
+		// TODO(stevvooe): Need to handle missing container here. It is likely
+		// that a Wait call with a not found error should result in no waiting
+		// and no error at all.
+		return err
+	}
+
+	if ctnr.State.ExitCode != 0 {
+		var cause error
+		if ctnr.State.Error != "" {
+			cause = errors.New(ctnr.State.Error)
+		}
+		cstatus, _ := parseContainerStatus(ctnr)
+		return &exitError{
+			code:            ctnr.State.ExitCode,
+			cause:           cause,
+			containerStatus: cstatus,
+		}
+	}
+	return nil
+}
+
+// Shutdown the container cleanly.
+func (r *controller) Shutdown(ctx context.Context) error {
+	if err := r.checkClosed(); err != nil {
+		return err
+	}
+
+	if err := r.adapter.shutdown(ctx); err != nil {
+		if isUnknownContainer(err) || isStoppedContainer(err) {
+			return nil
+		}
+
+		return err
+	}
+
+	return nil
+}
+
+// Terminate the container, with force.
+func (r *controller) Terminate(ctx context.Context) error {
+	if err := r.checkClosed(); err != nil {
+		return err
+	}
+
+	if err := r.adapter.terminate(ctx); err != nil {
+		if isUnknownContainer(err) {
+			return nil
+		}
+
+		return err
+	}
+
+	return nil
+}
+
+// Remove the container and its resources.
+func (r *controller) Remove(ctx context.Context) error {
+	if err := r.checkClosed(); err != nil {
+		return err
+	}
+
+	// It may be necessary to shut down the task before removing it.
+	if err := r.Shutdown(ctx); err != nil {
+		if isUnknownContainer(err) {
+			return nil
+		}
+		// This may fail if the task was already shut down.
+		log.G(ctx).WithError(err).Debug("shutdown failed on removal")
+	}
+
+	// Try removing networks referenced in this task in case this
+	// task is the last one referencing it
+	if err := r.adapter.removeNetworks(ctx); err != nil {
+		if isUnknownContainer(err) {
+			return nil
+		}
+		return err
+	}
+
+	if err := r.adapter.remove(ctx); err != nil {
+		if isUnknownContainer(err) {
+			return nil
+		}
+
+		return err
+	}
+	return nil
+}
+
+// Close the runner and clean up any ephemeral resources.
+func (r *controller) Close() error {
+	select {
+	case <-r.closed:
+		return r.err
+	default:
+		r.err = exec.ErrControllerClosed
+		close(r.closed)
+	}
+	return nil
+}
+
+func (r *controller) checkClosed() error {
+	select {
+	case <-r.closed:
+		return r.err
+	default:
+		return nil
+	}
+}
+
+func parseContainerStatus(ctnr types.ContainerJSON) (*api.ContainerStatus, error) {
+	status := &api.ContainerStatus{
+		ContainerID: ctnr.ID,
+		PID:         int32(ctnr.State.Pid),
+		ExitCode:    int32(ctnr.State.ExitCode),
+	}
+
+	return status, nil
+}
+
+type exitError struct {
+	code            int
+	cause           error
+	containerStatus *api.ContainerStatus
+}
+
+func (e *exitError) Error() string {
+	if e.cause != nil {
+		return fmt.Sprintf("task: non-zero exit (%v): %v", e.code, e.cause)
+	}
+
+	return fmt.Sprintf("task: non-zero exit (%v)", e.code)
+}
+
+func (e *exitError) ExitCode() int {
+	return int(e.containerStatus.ExitCode)
+}
+
+func (e *exitError) Cause() error {
+	return e.cause
+}

+ 12 - 0
daemon/cluster/executor/container/errors.go

@@ -0,0 +1,12 @@
+package container
+
+import "fmt"
+
+var (
+	// ErrImageRequired returned if a task is missing the image definition.
+	ErrImageRequired = fmt.Errorf("dockerexec: image required")
+
+	// ErrContainerDestroyed returned when a container is prematurely destroyed
+	// during a wait call.
+	ErrContainerDestroyed = fmt.Errorf("dockerexec: container destroyed")
+)

+ 139 - 0
daemon/cluster/executor/container/executor.go

@@ -0,0 +1,139 @@
+package container
+
+import (
+	"strings"
+
+	executorpkg "github.com/docker/docker/daemon/cluster/executor"
+	clustertypes "github.com/docker/docker/daemon/cluster/provider"
+	"github.com/docker/engine-api/types"
+	"github.com/docker/engine-api/types/network"
+	networktypes "github.com/docker/libnetwork/types"
+	"github.com/docker/swarmkit/agent/exec"
+	"github.com/docker/swarmkit/api"
+	"golang.org/x/net/context"
+)
+
+type executor struct {
+	backend executorpkg.Backend
+}
+
+// NewExecutor returns an executor from the docker client.
+func NewExecutor(b executorpkg.Backend) exec.Executor {
+	return &executor{
+		backend: b,
+	}
+}
+
+// Describe returns the underlying node description from the docker client.
+func (e *executor) Describe(ctx context.Context) (*api.NodeDescription, error) {
+	info, err := e.backend.SystemInfo()
+	if err != nil {
+		return nil, err
+	}
+
+	var plugins []api.PluginDescription
+	addPlugins := func(typ string, names []string) {
+		for _, name := range names {
+			plugins = append(plugins, api.PluginDescription{
+				Type: typ,
+				Name: name,
+			})
+		}
+	}
+
+	addPlugins("Volume", info.Plugins.Volume)
+	// Add builtin driver "overlay" (the only builtin multi-host driver) to
+	// the plugin list by default.
+	addPlugins("Network", append([]string{"overlay"}, info.Plugins.Network...))
+	addPlugins("Authorization", info.Plugins.Authorization)
+
+	// parse []string labels into a map[string]string
+	labels := map[string]string{}
+	for _, l := range info.Labels {
+		stringSlice := strings.SplitN(l, "=", 2)
+		// this will take the last value in the list for a given key
+		// ideally, one shouldn't assign multiple values to the same key
+		if len(stringSlice) > 1 {
+			labels[stringSlice[0]] = stringSlice[1]
+		}
+	}
+
+	description := &api.NodeDescription{
+		Hostname: info.Name,
+		Platform: &api.Platform{
+			Architecture: info.Architecture,
+			OS:           info.OSType,
+		},
+		Engine: &api.EngineDescription{
+			EngineVersion: info.ServerVersion,
+			Labels:        labels,
+			Plugins:       plugins,
+		},
+		Resources: &api.Resources{
+			NanoCPUs:    int64(info.NCPU) * 1e9,
+			MemoryBytes: info.MemTotal,
+		},
+	}
+
+	return description, nil
+}
+
+func (e *executor) Configure(ctx context.Context, node *api.Node) error {
+	na := node.Attachment
+	if na == nil {
+		return nil
+	}
+
+	options := types.NetworkCreate{
+		Driver: na.Network.DriverState.Name,
+		IPAM: network.IPAM{
+			Driver: na.Network.IPAM.Driver.Name,
+		},
+		Options:        na.Network.DriverState.Options,
+		CheckDuplicate: true,
+	}
+
+	for _, ic := range na.Network.IPAM.Configs {
+		c := network.IPAMConfig{
+			Subnet:  ic.Subnet,
+			IPRange: ic.Range,
+			Gateway: ic.Gateway,
+		}
+		options.IPAM.Config = append(options.IPAM.Config, c)
+	}
+
+	return e.backend.SetupIngress(clustertypes.NetworkCreateRequest{
+		na.Network.ID,
+		types.NetworkCreateRequest{
+			Name:          na.Network.Spec.Annotations.Name,
+			NetworkCreate: options,
+		},
+	}, na.Addresses[0])
+}
+
+// Controller returns a docker container runner.
+func (e *executor) Controller(t *api.Task) (exec.Controller, error) {
+	ctlr, err := newController(e.backend, t)
+	if err != nil {
+		return nil, err
+	}
+
+	return ctlr, nil
+}
+
+func (e *executor) SetNetworkBootstrapKeys(keys []*api.EncryptionKey) error {
+	nwKeys := []*networktypes.EncryptionKey{}
+	for _, key := range keys {
+		nwKey := &networktypes.EncryptionKey{
+			Subsystem:   key.Subsystem,
+			Algorithm:   int32(key.Algorithm),
+			Key:         make([]byte, len(key.Key)),
+			LamportTime: key.LamportTime,
+		}
+		copy(nwKey.Key, key.Key)
+		nwKeys = append(nwKeys, nwKey)
+	}
+	e.backend.SetNetworkBootstrapKeys(nwKeys)
+
+	return nil
+}

+ 93 - 0
daemon/cluster/filters.go

@@ -0,0 +1,93 @@
+package cluster
+
+import (
+	"fmt"
+	"strings"
+
+	runconfigopts "github.com/docker/docker/runconfig/opts"
+	"github.com/docker/engine-api/types/filters"
+	swarmapi "github.com/docker/swarmkit/api"
+)
+
+func newListNodesFilters(filter filters.Args) (*swarmapi.ListNodesRequest_Filters, error) {
+	accepted := map[string]bool{
+		"name":       true,
+		"id":         true,
+		"label":      true,
+		"role":       true,
+		"membership": true,
+	}
+	if err := filter.Validate(accepted); err != nil {
+		return nil, err
+	}
+	f := &swarmapi.ListNodesRequest_Filters{
+		Names:      filter.Get("name"),
+		IDPrefixes: filter.Get("id"),
+		Labels:     runconfigopts.ConvertKVStringsToMap(filter.Get("label")),
+	}
+
+	for _, r := range filter.Get("role") {
+		if role, ok := swarmapi.NodeRole_value[strings.ToUpper(r)]; ok {
+			f.Roles = append(f.Roles, swarmapi.NodeRole(role))
+		} else if r != "" {
+			return nil, fmt.Errorf("Invalid role filter: '%s'", r)
+		}
+	}
+
+	for _, a := range filter.Get("membership") {
+		if membership, ok := swarmapi.NodeSpec_Membership_value[strings.ToUpper(a)]; ok {
+			f.Memberships = append(f.Memberships, swarmapi.NodeSpec_Membership(membership))
+		} else if a != "" {
+			return nil, fmt.Errorf("Invalid membership filter: '%s'", a)
+		}
+	}
+
+	return f, nil
+}
+
+func newListServicesFilters(filter filters.Args) (*swarmapi.ListServicesRequest_Filters, error) {
+	accepted := map[string]bool{
+		"name":  true,
+		"id":    true,
+		"label": true,
+	}
+	if err := filter.Validate(accepted); err != nil {
+		return nil, err
+	}
+	return &swarmapi.ListServicesRequest_Filters{
+		Names:      filter.Get("name"),
+		IDPrefixes: filter.Get("id"),
+		Labels:     runconfigopts.ConvertKVStringsToMap(filter.Get("label")),
+	}, nil
+}
+
+func newListTasksFilters(filter filters.Args) (*swarmapi.ListTasksRequest_Filters, error) {
+	accepted := map[string]bool{
+		"name":          true,
+		"id":            true,
+		"label":         true,
+		"service":       true,
+		"node":          true,
+		"desired_state": true,
+	}
+	if err := filter.Validate(accepted); err != nil {
+		return nil, err
+	}
+	f := &swarmapi.ListTasksRequest_Filters{
+		Names:      filter.Get("name"),
+		IDPrefixes: filter.Get("id"),
+		Labels:     runconfigopts.ConvertKVStringsToMap(filter.Get("label")),
+		ServiceIDs: filter.Get("service"),
+		NodeIDs:    filter.Get("node"),
+	}
+
+	for _, s := range filter.Get("desired_state") {
+		if state, ok := swarmapi.TaskState_value[strings.ToUpper(s)]; ok {
+			f.DesiredStates = append(f.DesiredStates, swarmapi.TaskState(state))
+		} else if s != "" {
+			return nil, fmt.Errorf("Invalid desired_state filter: '%s'", s)
+		}
+	}
+
+	return f, nil
+}

+ 108 - 0
daemon/cluster/helpers.go

@@ -0,0 +1,108 @@
+package cluster
+
+import (
+	"fmt"
+
+	swarmapi "github.com/docker/swarmkit/api"
+	"golang.org/x/net/context"
+)
+
+func getSwarm(ctx context.Context, c swarmapi.ControlClient) (*swarmapi.Cluster, error) {
+	rl, err := c.ListClusters(ctx, &swarmapi.ListClustersRequest{})
+	if err != nil {
+		return nil, err
+	}
+
+	if len(rl.Clusters) == 0 {
+		return nil, fmt.Errorf("swarm not found")
+	}
+
+	// TODO: assume one cluster only
+	return rl.Clusters[0], nil
+}
+
+func getNode(ctx context.Context, c swarmapi.ControlClient, input string) (*swarmapi.Node, error) {
+	// GetNode to match via full ID.
+	rg, err := c.GetNode(ctx, &swarmapi.GetNodeRequest{NodeID: input})
+	if err != nil {
+		// If any error (including NotFound), ListNodes to match via full name.
+		rl, err := c.ListNodes(ctx, &swarmapi.ListNodesRequest{Filters: &swarmapi.ListNodesRequest_Filters{Names: []string{input}}})
+
+		if err != nil || len(rl.Nodes) == 0 {
+			// If any error or 0 result, ListNodes to match via ID prefix.
+			rl, err = c.ListNodes(ctx, &swarmapi.ListNodesRequest{Filters: &swarmapi.ListNodesRequest_Filters{IDPrefixes: []string{input}}})
+		}
+
+		if err != nil {
+			return nil, err
+		}
+
+		if len(rl.Nodes) == 0 {
+			return nil, fmt.Errorf("node %s not found", input)
+		}
+
+		if l := len(rl.Nodes); l > 1 {
+			return nil, fmt.Errorf("node %s is ambigious (%d matches found)", input, l)
+		}
+
+		return rl.Nodes[0], nil
+	}
+	return rg.Node, nil
+}
+
+func getService(ctx context.Context, c swarmapi.ControlClient, input string) (*swarmapi.Service, error) {
+	// GetService to match via full ID.
+	rg, err := c.GetService(ctx, &swarmapi.GetServiceRequest{ServiceID: input})
+	if err != nil {
+		// If any error (including NotFound), ListServices to match via full name.
+		rl, err := c.ListServices(ctx, &swarmapi.ListServicesRequest{Filters: &swarmapi.ListServicesRequest_Filters{Names: []string{input}}})
+		if err != nil || len(rl.Services) == 0 {
+			// If any error or 0 result, ListServices to match via ID prefix.
+			rl, err = c.ListServices(ctx, &swarmapi.ListServicesRequest{Filters: &swarmapi.ListServicesRequest_Filters{IDPrefixes: []string{input}}})
+		}
+
+		if err != nil {
+			return nil, err
+		}
+
+		if len(rl.Services) == 0 {
+			return nil, fmt.Errorf("service %s not found", input)
+		}
+
+		if l := len(rl.Services); l > 1 {
+			return nil, fmt.Errorf("service %s is ambigious (%d matches found)", input, l)
+		}
+
+		return rl.Services[0], nil
+	}
+	return rg.Service, nil
+}
+
+func getTask(ctx context.Context, c swarmapi.ControlClient, input string) (*swarmapi.Task, error) {
+	// GetTask to match via full ID.
+	rg, err := c.GetTask(ctx, &swarmapi.GetTaskRequest{TaskID: input})
+	if err != nil {
+		// If any error (including NotFound), ListTasks to match via full name.
+		rl, err := c.ListTasks(ctx, &swarmapi.ListTasksRequest{Filters: &swarmapi.ListTasksRequest_Filters{Names: []string{input}}})
+
+		if err != nil || len(rl.Tasks) == 0 {
+			// If any error or 0 result, ListTasks to match via ID prefix.
+			rl, err = c.ListTasks(ctx, &swarmapi.ListTasksRequest{Filters: &swarmapi.ListTasksRequest_Filters{IDPrefixes: []string{input}}})
+		}
+
+		if err != nil {
+			return nil, err
+		}
+
+		if len(rl.Tasks) == 0 {
+			return nil, fmt.Errorf("task %s not found", input)
+		}
+
+		if l := len(rl.Tasks); l > 1 {
+			return nil, fmt.Errorf("task %s is ambigious (%d matches found)", input, l)
+		}
+
+		return rl.Tasks[0], nil
+	}
+	return rg.Task, nil
+}

+ 36 - 0
daemon/cluster/provider/network.go

@@ -0,0 +1,36 @@
+package provider
+
+import "github.com/docker/engine-api/types"
+
+// NetworkCreateRequest is a request when creating a network.
+type NetworkCreateRequest struct {
+	ID string
+	types.NetworkCreateRequest
+}
+
+// NetworkCreateResponse is a response when creating a network.
+type NetworkCreateResponse struct {
+	ID string `json:"Id"`
+}
+
+// VirtualAddress represents a virtual adress.
+type VirtualAddress struct {
+	IPv4 string
+	IPv6 string
+}
+
+// PortConfig represents a port configuration.
+type PortConfig struct {
+	Name          string
+	Protocol      int32
+	TargetPort    uint32
+	PublishedPort uint32
+}
+
+// ServiceConfig represents a service configuration.
+type ServiceConfig struct {
+	ID               string
+	Name             string
+	VirtualAddresses map[string]*VirtualAddress
+	ExposedPorts     []*PortConfig
+}

+ 2 - 1
daemon/container.go

@@ -101,7 +101,7 @@ func (daemon *Daemon) Register(c *container.Container) error {
 	return nil
 	return nil
 }
 }
 
 
-func (daemon *Daemon) newContainer(name string, config *containertypes.Config, imgID image.ID) (*container.Container, error) {
+func (daemon *Daemon) newContainer(name string, config *containertypes.Config, imgID image.ID, managed bool) (*container.Container, error) {
 	var (
 	var (
 		id             string
 		id             string
 		err            error
 		err            error
@@ -117,6 +117,7 @@ func (daemon *Daemon) newContainer(name string, config *containertypes.Config, i
 
 
 	base := daemon.newBaseContainer(id)
 	base := daemon.newBaseContainer(id)
 	base.Created = time.Now().UTC()
 	base.Created = time.Now().UTC()
+	base.Managed = managed
 	base.Path = entrypoint
 	base.Path = entrypoint
 	base.Args = args //FIXME: de-duplicate from config
 	base.Args = args //FIXME: de-duplicate from config
 	base.Config = config
 	base.Config = config

+ 7 - 0
daemon/container_operations.go

@@ -324,6 +324,10 @@ func (daemon *Daemon) updateNetwork(container *container.Container) error {
 	return nil
 	return nil
 }
 }
 
 
+func errClusterNetworkOnRun(n string) error {
+	return fmt.Errorf("swarm-scoped network (%s) is not compatible with `docker create` or `docker run`. This network can be only used docker service", n)
+}
+
 // updateContainerNetworkSettings update the network settings
 // updateContainerNetworkSettings update the network settings
 func (daemon *Daemon) updateContainerNetworkSettings(container *container.Container, endpointsConfig map[string]*networktypes.EndpointSettings) error {
 func (daemon *Daemon) updateContainerNetworkSettings(container *container.Container, endpointsConfig map[string]*networktypes.EndpointSettings) error {
 	var (
 	var (
@@ -345,6 +349,9 @@ func (daemon *Daemon) updateContainerNetworkSettings(container *container.Contai
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
+		if !container.Managed && n.Info().Dynamic() {
+			return errClusterNetworkOnRun(networkName)
+		}
 		networkName = n.Name()
 		networkName = n.Name()
 	}
 	}
 	if container.NetworkSettings == nil {
 	if container.NetworkSettings == nil {

+ 13 - 4
daemon/create.go

@@ -19,8 +19,17 @@ import (
 	"github.com/opencontainers/runc/libcontainer/label"
 	"github.com/opencontainers/runc/libcontainer/label"
 )
 )
 
 
-// ContainerCreate creates a container.
+// CreateManagedContainer creates a container that is managed by a Service
+func (daemon *Daemon) CreateManagedContainer(params types.ContainerCreateConfig) (types.ContainerCreateResponse, error) {
+	return daemon.containerCreate(params, true)
+}
+
+// ContainerCreate creates a regular container
 func (daemon *Daemon) ContainerCreate(params types.ContainerCreateConfig) (types.ContainerCreateResponse, error) {
 func (daemon *Daemon) ContainerCreate(params types.ContainerCreateConfig) (types.ContainerCreateResponse, error) {
+	return daemon.containerCreate(params, false)
+}
+
+func (daemon *Daemon) containerCreate(params types.ContainerCreateConfig, managed bool) (types.ContainerCreateResponse, error) {
 	if params.Config == nil {
 	if params.Config == nil {
 		return types.ContainerCreateResponse{}, fmt.Errorf("Config cannot be empty in order to create a container")
 		return types.ContainerCreateResponse{}, fmt.Errorf("Config cannot be empty in order to create a container")
 	}
 	}
@@ -43,7 +52,7 @@ func (daemon *Daemon) ContainerCreate(params types.ContainerCreateConfig) (types
 		return types.ContainerCreateResponse{Warnings: warnings}, err
 		return types.ContainerCreateResponse{Warnings: warnings}, err
 	}
 	}
 
 
-	container, err := daemon.create(params)
+	container, err := daemon.create(params, managed)
 	if err != nil {
 	if err != nil {
 		return types.ContainerCreateResponse{Warnings: warnings}, daemon.imageNotExistToErrcode(err)
 		return types.ContainerCreateResponse{Warnings: warnings}, daemon.imageNotExistToErrcode(err)
 	}
 	}
@@ -52,7 +61,7 @@ func (daemon *Daemon) ContainerCreate(params types.ContainerCreateConfig) (types
 }
 }
 
 
 // Create creates a new container from the given configuration with a given name.
 // Create creates a new container from the given configuration with a given name.
-func (daemon *Daemon) create(params types.ContainerCreateConfig) (retC *container.Container, retErr error) {
+func (daemon *Daemon) create(params types.ContainerCreateConfig, managed bool) (retC *container.Container, retErr error) {
 	var (
 	var (
 		container *container.Container
 		container *container.Container
 		img       *image.Image
 		img       *image.Image
@@ -76,7 +85,7 @@ func (daemon *Daemon) create(params types.ContainerCreateConfig) (retC *containe
 		return nil, err
 		return nil, err
 	}
 	}
 
 
-	if container, err = daemon.newContainer(params.Name, params.Config, imgID); err != nil {
+	if container, err = daemon.newContainer(params.Name, params.Config, imgID, managed); err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 	defer func() {
 	defer func() {

+ 12 - 0
daemon/daemon.go

@@ -28,6 +28,7 @@ import (
 	"github.com/docker/docker/daemon/exec"
 	"github.com/docker/docker/daemon/exec"
 	"github.com/docker/engine-api/types"
 	"github.com/docker/engine-api/types"
 	containertypes "github.com/docker/engine-api/types/container"
 	containertypes "github.com/docker/engine-api/types/container"
+	"github.com/docker/libnetwork/cluster"
 	// register graph drivers
 	// register graph drivers
 	_ "github.com/docker/docker/daemon/graphdriver/register"
 	_ "github.com/docker/docker/daemon/graphdriver/register"
 	dmetadata "github.com/docker/docker/distribution/metadata"
 	dmetadata "github.com/docker/docker/distribution/metadata"
@@ -94,6 +95,7 @@ type Daemon struct {
 	containerd                libcontainerd.Client
 	containerd                libcontainerd.Client
 	containerdRemote          libcontainerd.Remote
 	containerdRemote          libcontainerd.Remote
 	defaultIsolation          containertypes.Isolation // Default isolation mode on Windows
 	defaultIsolation          containertypes.Isolation // Default isolation mode on Windows
+	clusterProvider           cluster.Provider
 }
 }
 
 
 func (daemon *Daemon) restore() error {
 func (daemon *Daemon) restore() error {
@@ -344,6 +346,12 @@ func (daemon *Daemon) registerLink(parent, child *container.Container, alias str
 	return nil
 	return nil
 }
 }
 
 
+// SetClusterProvider sets a component for quering the current cluster state.
+func (daemon *Daemon) SetClusterProvider(clusterProvider cluster.Provider) {
+	daemon.clusterProvider = clusterProvider
+	daemon.netController.SetClusterProvider(clusterProvider)
+}
+
 // NewDaemon sets up everything for the daemon to be able to service
 // NewDaemon sets up everything for the daemon to be able to service
 // requests from the webserver.
 // requests from the webserver.
 func NewDaemon(config *Config, registryService registry.Service, containerdRemote libcontainerd.Remote) (daemon *Daemon, err error) {
 func NewDaemon(config *Config, registryService registry.Service, containerdRemote libcontainerd.Remote) (daemon *Daemon, err error) {
@@ -893,6 +901,10 @@ func (daemon *Daemon) reloadClusterDiscovery(config *Config) error {
 		return nil
 		return nil
 	}
 	}
 
 
+	if daemon.clusterProvider != nil {
+		return fmt.Errorf("--cluster-store and --cluster-advertise daemon configurations are incompatible with swarm mode")
+	}
+
 	// enable discovery for the first time if it was not previously enabled
 	// enable discovery for the first time if it was not previously enabled
 	if daemon.discoveryWatcher == nil {
 	if daemon.discoveryWatcher == nil {
 		discoveryWatcher, err := initDiscovery(newClusterStore, newAdvertise, config.ClusterOpts)
 		discoveryWatcher, err := initDiscovery(newClusterStore, newAdvertise, config.ClusterOpts)

+ 4 - 2
daemon/inspect.go

@@ -23,10 +23,12 @@ func (daemon *Daemon) ContainerInspect(name string, size bool, version string) (
 	case versions.Equal(version, "1.20"):
 	case versions.Equal(version, "1.20"):
 		return daemon.containerInspect120(name)
 		return daemon.containerInspect120(name)
 	}
 	}
-	return daemon.containerInspectCurrent(name, size)
+	return daemon.ContainerInspectCurrent(name, size)
 }
 }
 
 
-func (daemon *Daemon) containerInspectCurrent(name string, size bool) (*types.ContainerJSON, error) {
+// ContainerInspectCurrent returns low-level information about a
+// container in a most recent api version.
+func (daemon *Daemon) ContainerInspectCurrent(name string, size bool) (*types.ContainerJSON, error) {
 	container, err := daemon.GetContainer(name)
 	container, err := daemon.GetContainer(name)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err

+ 1 - 1
daemon/inspect_windows.go

@@ -28,7 +28,7 @@ func addMountPoints(container *container.Container) []types.MountPoint {
 
 
 // containerInspectPre120 get containers for pre 1.20 APIs.
 // containerInspectPre120 get containers for pre 1.20 APIs.
 func (daemon *Daemon) containerInspectPre120(name string) (*types.ContainerJSON, error) {
 func (daemon *Daemon) containerInspectPre120(name string) (*types.ContainerJSON, error) {
-	return daemon.containerInspectCurrent(name, false)
+	return daemon.ContainerInspectCurrent(name, false)
 }
 }
 
 
 func inspectExecProcessConfig(e *exec.Config) *backend.ExecProcessConfig {
 func inspectExecProcessConfig(e *exec.Config) *backend.ExecProcessConfig {

+ 11 - 0
daemon/list.go

@@ -91,6 +91,17 @@ func (daemon *Daemon) Containers(config *types.ContainerListOptions) ([]*types.C
 	return daemon.reduceContainers(config, daemon.transformContainer)
 	return daemon.reduceContainers(config, daemon.transformContainer)
 }
 }
 
 
+// ListContainersForNode returns all containerID that match the specified nodeID
+func (daemon *Daemon) ListContainersForNode(nodeID string) []string {
+	var ids []string
+	for _, c := range daemon.List() {
+		if c.Config.Labels["com.docker.swarm.node.id"] == nodeID {
+			ids = append(ids, c.ID)
+		}
+	}
+	return ids
+}
+
 func (daemon *Daemon) filterByNameIDMatches(ctx *listContext) []*container.Container {
 func (daemon *Daemon) filterByNameIDMatches(ctx *listContext) []*container.Container {
 	idSearch := false
 	idSearch := false
 	names := ctx.filters.Get("name")
 	names := ctx.filters.Get("name")

+ 137 - 15
daemon/network.go

@@ -5,13 +5,14 @@ import (
 	"net"
 	"net"
 	"strings"
 	"strings"
 
 
-	netsettings "github.com/docker/docker/daemon/network"
+	"github.com/Sirupsen/logrus"
+	clustertypes "github.com/docker/docker/daemon/cluster/provider"
 	"github.com/docker/docker/errors"
 	"github.com/docker/docker/errors"
 	"github.com/docker/docker/runconfig"
 	"github.com/docker/docker/runconfig"
 	"github.com/docker/engine-api/types"
 	"github.com/docker/engine-api/types"
-	"github.com/docker/engine-api/types/filters"
 	"github.com/docker/engine-api/types/network"
 	"github.com/docker/engine-api/types/network"
 	"github.com/docker/libnetwork"
 	"github.com/docker/libnetwork"
+	networktypes "github.com/docker/libnetwork/types"
 )
 )
 
 
 // NetworkControllerEnabled checks if the networking stack is enabled.
 // NetworkControllerEnabled checks if the networking stack is enabled.
@@ -92,9 +93,106 @@ func (daemon *Daemon) getAllNetworks() []libnetwork.Network {
 	return list
 	return list
 }
 }
 
 
+func isIngressNetwork(name string) bool {
+	return name == "ingress"
+}
+
+var ingressChan = make(chan struct{}, 1)
+
+func ingressWait() func() {
+	ingressChan <- struct{}{}
+	return func() { <-ingressChan }
+}
+
+// SetupIngress setups ingress networking.
+func (daemon *Daemon) SetupIngress(create clustertypes.NetworkCreateRequest, nodeIP string) error {
+	ip, _, err := net.ParseCIDR(nodeIP)
+	if err != nil {
+		return err
+	}
+
+	go func() {
+		controller := daemon.netController
+		controller.AgentInitWait()
+
+		if n, err := daemon.GetNetworkByName(create.Name); err == nil && n != nil && n.ID() != create.ID {
+			if err := controller.SandboxDestroy("ingress-sbox"); err != nil {
+				logrus.Errorf("Failed to delete stale ingress sandbox: %v", err)
+				return
+			}
+
+			if err := n.Delete(); err != nil {
+				logrus.Errorf("Failed to delete stale ingress network %s: %v", n.ID(), err)
+				return
+			}
+		}
+
+		if _, err := daemon.createNetwork(create.NetworkCreateRequest, create.ID, true); err != nil {
+			// If it is any other error other than already
+			// exists error log error and return.
+			if _, ok := err.(libnetwork.NetworkNameError); !ok {
+				logrus.Errorf("Failed creating ingress network: %v", err)
+				return
+			}
+
+			// Otherwise continue down the call to create or recreate sandbox.
+		}
+
+		n, err := daemon.GetNetworkByID(create.ID)
+		if err != nil {
+			logrus.Errorf("Failed getting ingress network by id after creating: %v", err)
+			return
+		}
+
+		sb, err := controller.NewSandbox("ingress-sbox", libnetwork.OptionIngress())
+		if err != nil {
+			logrus.Errorf("Failed creating ingress sanbox: %v", err)
+			return
+		}
+
+		ep, err := n.CreateEndpoint("ingress-endpoint", libnetwork.CreateOptionIpam(ip, nil, nil, nil))
+		if err != nil {
+			logrus.Errorf("Failed creating ingress endpoint: %v", err)
+			return
+		}
+
+		if err := ep.Join(sb, nil); err != nil {
+			logrus.Errorf("Failed joining ingress sandbox to ingress endpoint: %v", err)
+		}
+	}()
+
+	return nil
+}
+
+// SetNetworkBootstrapKeys sets the bootstrap keys.
+func (daemon *Daemon) SetNetworkBootstrapKeys(keys []*networktypes.EncryptionKey) error {
+	return daemon.netController.SetKeys(keys)
+}
+
+// CreateManagedNetwork creates an agent network.
+func (daemon *Daemon) CreateManagedNetwork(create clustertypes.NetworkCreateRequest) error {
+	_, err := daemon.createNetwork(create.NetworkCreateRequest, create.ID, true)
+	return err
+}
+
 // CreateNetwork creates a network with the given name, driver and other optional parameters
 // CreateNetwork creates a network with the given name, driver and other optional parameters
 func (daemon *Daemon) CreateNetwork(create types.NetworkCreateRequest) (*types.NetworkCreateResponse, error) {
 func (daemon *Daemon) CreateNetwork(create types.NetworkCreateRequest) (*types.NetworkCreateResponse, error) {
-	if runconfig.IsPreDefinedNetwork(create.Name) {
+	resp, err := daemon.createNetwork(create, "", false)
+	if err != nil {
+		return nil, err
+	}
+	return resp, err
+}
+
+func (daemon *Daemon) createNetwork(create types.NetworkCreateRequest, id string, agent bool) (*types.NetworkCreateResponse, error) {
+	// If there is a pending ingress network creation wait here
+	// since ingress network creation can happen via node download
+	// from manager or task download.
+	if isIngressNetwork(create.Name) {
+		defer ingressWait()()
+	}
+
+	if runconfig.IsPreDefinedNetwork(create.Name) && !agent {
 		err := fmt.Errorf("%s is a pre-defined network and cannot be created", create.Name)
 		err := fmt.Errorf("%s is a pre-defined network and cannot be created", create.Name)
 		return nil, errors.NewRequestForbiddenError(err)
 		return nil, errors.NewRequestForbiddenError(err)
 	}
 	}
@@ -134,7 +232,16 @@ func (daemon *Daemon) CreateNetwork(create types.NetworkCreateRequest) (*types.N
 	if create.Internal {
 	if create.Internal {
 		nwOptions = append(nwOptions, libnetwork.NetworkOptionInternalNetwork())
 		nwOptions = append(nwOptions, libnetwork.NetworkOptionInternalNetwork())
 	}
 	}
-	n, err := c.NewNetwork(driver, create.Name, "", nwOptions...)
+	if agent {
+		nwOptions = append(nwOptions, libnetwork.NetworkOptionDynamic())
+		nwOptions = append(nwOptions, libnetwork.NetworkOptionPersist(false))
+	}
+
+	if isIngressNetwork(create.Name) {
+		nwOptions = append(nwOptions, libnetwork.NetworkOptionIngress())
+	}
+
+	n, err := c.NewNetwork(driver, create.Name, id, nwOptions...)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
@@ -168,6 +275,17 @@ func getIpamConfig(data []network.IPAMConfig) ([]*libnetwork.IpamConf, []*libnet
 	return ipamV4Cfg, ipamV6Cfg, nil
 	return ipamV4Cfg, ipamV6Cfg, nil
 }
 }
 
 
+// UpdateContainerServiceConfig updates a service configuration.
+func (daemon *Daemon) UpdateContainerServiceConfig(containerName string, serviceConfig *clustertypes.ServiceConfig) error {
+	container, err := daemon.GetContainer(containerName)
+	if err != nil {
+		return err
+	}
+
+	container.NetworkSettings.Service = serviceConfig
+	return nil
+}
+
 // ConnectContainerToNetwork connects the given container to the given
 // ConnectContainerToNetwork connects the given container to the given
 // network. If either cannot be found, an err is returned. If the
 // network. If either cannot be found, an err is returned. If the
 // network cannot be set up, an err is returned.
 // network cannot be set up, an err is returned.
@@ -207,18 +325,29 @@ func (daemon *Daemon) GetNetworkDriverList() map[string]bool {
 		driver := network.Type()
 		driver := network.Type()
 		pluginList[driver] = true
 		pluginList[driver] = true
 	}
 	}
+	// TODO : Replace this with proper libnetwork API
+	pluginList["overlay"] = true
 
 
 	return pluginList
 	return pluginList
 }
 }
 
 
+// DeleteManagedNetwork deletes an agent network.
+func (daemon *Daemon) DeleteManagedNetwork(networkID string) error {
+	return daemon.deleteNetwork(networkID, true)
+}
+
 // DeleteNetwork destroys a network unless it's one of docker's predefined networks.
 // DeleteNetwork destroys a network unless it's one of docker's predefined networks.
 func (daemon *Daemon) DeleteNetwork(networkID string) error {
 func (daemon *Daemon) DeleteNetwork(networkID string) error {
+	return daemon.deleteNetwork(networkID, false)
+}
+
+func (daemon *Daemon) deleteNetwork(networkID string, dynamic bool) error {
 	nw, err := daemon.FindNetwork(networkID)
 	nw, err := daemon.FindNetwork(networkID)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 
 
-	if runconfig.IsPreDefinedNetwork(nw.Name()) {
+	if runconfig.IsPreDefinedNetwork(nw.Name()) && !dynamic {
 		err := fmt.Errorf("%s is a pre-defined network and cannot be removed", nw.Name())
 		err := fmt.Errorf("%s is a pre-defined network and cannot be removed", nw.Name())
 		return errors.NewRequestForbiddenError(err)
 		return errors.NewRequestForbiddenError(err)
 	}
 	}
@@ -230,14 +359,7 @@ func (daemon *Daemon) DeleteNetwork(networkID string) error {
 	return nil
 	return nil
 }
 }
 
 
-// FilterNetworks returns a list of networks filtered by the given arguments.
-// It returns an error if the filters are not included in the list of accepted filters.
-func (daemon *Daemon) FilterNetworks(netFilters filters.Args) ([]libnetwork.Network, error) {
-	if netFilters.Len() != 0 {
-		if err := netFilters.Validate(netsettings.AcceptedFilters); err != nil {
-			return nil, err
-		}
-	}
-	nwList := daemon.getAllNetworks()
-	return netsettings.FilterNetworks(nwList, netFilters)
+// GetNetworks returns a list of all networks
+func (daemon *Daemon) GetNetworks() []libnetwork.Network {
+	return daemon.getAllNetworks()
 }
 }

+ 2 - 0
daemon/network/settings.go

@@ -1,6 +1,7 @@
 package network
 package network
 
 
 import (
 import (
+	clustertypes "github.com/docker/docker/daemon/cluster/provider"
 	networktypes "github.com/docker/engine-api/types/network"
 	networktypes "github.com/docker/engine-api/types/network"
 	"github.com/docker/go-connections/nat"
 	"github.com/docker/go-connections/nat"
 )
 )
@@ -14,6 +15,7 @@ type Settings struct {
 	LinkLocalIPv6Address   string
 	LinkLocalIPv6Address   string
 	LinkLocalIPv6PrefixLen int
 	LinkLocalIPv6PrefixLen int
 	Networks               map[string]*networktypes.EndpointSettings
 	Networks               map[string]*networktypes.EndpointSettings
+	Service                *clustertypes.ServiceConfig
 	Ports                  nat.PortMap
 	Ports                  nat.PortMap
 	SandboxKey             string
 	SandboxKey             string
 	SecondaryIPAddresses   []networktypes.Address
 	SecondaryIPAddresses   []networktypes.Address

+ 16 - 1
daemon/wait.go

@@ -1,6 +1,10 @@
 package daemon
 package daemon
 
 
-import "time"
+import (
+	"time"
+
+	"golang.org/x/net/context"
+)
 
 
 // ContainerWait stops processing until the given container is
 // ContainerWait stops processing until the given container is
 // stopped. If the container is not found, an error is returned. On a
 // stopped. If the container is not found, an error is returned. On a
@@ -15,3 +19,14 @@ func (daemon *Daemon) ContainerWait(name string, timeout time.Duration) (int, er
 
 
 	return container.WaitStop(timeout)
 	return container.WaitStop(timeout)
 }
 }
+
+// ContainerWaitWithContext returns a channel where exit code is sent
+// when container stops. Channel can be cancelled with a context.
+func (daemon *Daemon) ContainerWaitWithContext(ctx context.Context, name string) (<-chan int, error) {
+	container, err := daemon.GetContainer(name)
+	if err != nil {
+		return nil, err
+	}
+
+	return container.WaitWithContext(ctx), nil
+}

+ 1113 - 1
docs/reference/api/docker_remote_api_v1.24.md

@@ -492,7 +492,6 @@ Status Codes:
 
 
 Return low-level information on the container `id`
 Return low-level information on the container `id`
 
 
-
 **Example request**:
 **Example request**:
 
 
       GET /containers/4fa6e0f0c678/json HTTP/1.1
       GET /containers/4fa6e0f0c678/json HTTP/1.1
@@ -3306,6 +3305,1119 @@ Status Codes
 -   **404** - no such network
 -   **404** - no such network
 -   **500** - server error
 -   **500** - server error
 
 
+## 3.6 Nodes
+
+**Note:** Nodes operations require to first be part of a Swarm.
+
+### List nodes
+
+**Warning:** this endpoint is part of the Swarm management feature introduced in Docker 1.12, and
+might be subject to non backward-compatible changes.
+
+`GET /nodes`
+
+List nodes
+
+**Example request**:
+
+    GET /nodes HTTP/1.1
+
+**Example response**:
+
+    HTTP/1.1 200 OK
+    Content-Type: application/json
+
+    [
+      {
+        "ID": "24ifsmvkjbyhk",
+        "Version": {
+          "Index": 8
+        },
+        "CreatedAt": "2016-06-07T20:31:11.853781916Z",
+        "UpdatedAt": "2016-06-07T20:31:11.999868824Z",
+        "Spec": {
+          "Role": "MANAGER",
+          "Membership": "ACCEPTED",
+          "Availability": "ACTIVE"
+        },
+        "Description": {
+          "Hostname": "bf3067039e47",
+          "Platform": {
+            "Architecture": "x86_64",
+            "OS": "linux"
+          },
+          "Resources": {
+            "NanoCPUs": 4000000000,
+            "MemoryBytes": 8272408576
+          },
+          "Engine": {
+            "EngineVersion": "1.12.0-dev",
+            "Plugins": [
+              {
+                "Type": "Volume",
+                "Name": "local"
+              },
+              {
+                "Type": "Network",
+                "Name": "overlay"
+              }
+            ]
+          }
+        },
+        "Status": {
+          "State": "READY"
+        },
+        "Manager": {
+          "Raft": {
+            "RaftID": 10070664527094528000,
+            "Addr": "172.17.0.2:4500",
+            "Status": {
+              "Leader": true,
+              "Reachability": "REACHABLE"
+            }
+          }
+        },
+        "Attachment": {
+          "Network": {
+            "ID": "4qvuz4ko70xaltuqbt8956gd1",
+            "Version": {
+              "Index": 6
+            },
+            "CreatedAt": "2016-06-07T20:31:11.912919752Z",
+            "UpdatedAt": "2016-06-07T20:31:11.921784144Z",
+            "Spec": {
+              "Name": "ingress",
+              "Labels": {
+                "com.docker.swarm.internal": "true"
+              },
+              "DriverConfiguration": {},
+              "IPAM": {
+                "Driver": {},
+                "Configs": [
+                  {
+                    "Family": "UNKNOWN",
+                    "Subnet": "10.255.0.0/16"
+                  }
+                ]
+              }
+            },
+            "DriverState": {
+              "Name": "overlay",
+              "Options": {
+                "com.docker.network.driver.overlay.vxlanid_list": "256"
+              }
+            },
+            "IPAM": {
+              "Driver": {
+                "Name": "default"
+              },
+              "Configs": [
+                {
+                  "Family": "UNKNOWN",
+                  "Subnet": "10.255.0.0/16"
+                }
+              ]
+            }
+          },
+          "Addresses": [
+            "10.255.0.2/16"
+          ]
+        }
+      }
+    ]
+
+Query Parameters:
+
+- **filters** – a JSON encoded value of the filters (a `map[string][]string`) to process on the
+  nodes list. Available filters:
+  - `id=<node id>`
+  - `name=<node name>`
+  - `membership=`(`pending`|`accepted`|`rejected`)`
+  - `role=`(`worker`|`manager`)`
+
+Status Codes:
+
+- **200** – no error
+- **500** – server error
+
+### Inspect a node
+
+**Warning:** this endpoint is part of the Swarm management feature introduced in Docker 1.12, and
+might be subject to non backward-compatible changes.
+
+`GET /nodes/<id>`
+
+Return low-level information on the node `id`
+
+**Example request**:
+
+      GET /node/24ifsmvkjbyhk HTTP/1.1
+
+**Example response**:
+
+    HTTP/1.1 200 OK
+    Content-Type: application/json
+
+    {
+      "ID": "24ifsmvkjbyhk",
+      "Version": {
+        "Index": 8
+      },
+      "CreatedAt": "2016-06-07T20:31:11.853781916Z",
+      "UpdatedAt": "2016-06-07T20:31:11.999868824Z",
+      "Spec": {
+        "Role": "MANAGER",
+        "Membership": "ACCEPTED",
+        "Availability": "ACTIVE"
+      },
+      "Description": {
+        "Hostname": "bf3067039e47",
+        "Platform": {
+          "Architecture": "x86_64",
+          "OS": "linux"
+        },
+        "Resources": {
+          "NanoCPUs": 4000000000,
+          "MemoryBytes": 8272408576
+        },
+        "Engine": {
+          "EngineVersion": "1.12.0-dev",
+          "Plugins": [
+            {
+              "Type": "Volume",
+              "Name": "local"
+            },
+            {
+              "Type": "Network",
+              "Name": "overlay"
+            }
+          ]
+        }
+      },
+      "Status": {
+        "State": "READY"
+      },
+      "Manager": {
+        "Raft": {
+          "RaftID": 10070664527094528000,
+          "Addr": "172.17.0.2:4500",
+          "Status": {
+            "Leader": true,
+            "Reachability": "REACHABLE"
+          }
+        }
+      },
+      "Attachment": {
+        "Network": {
+          "ID": "4qvuz4ko70xaltuqbt8956gd1",
+          "Version": {
+            "Index": 6
+          },
+          "CreatedAt": "2016-06-07T20:31:11.912919752Z",
+          "UpdatedAt": "2016-06-07T20:31:11.921784144Z",
+          "Spec": {
+            "Name": "ingress",
+            "Labels": {
+              "com.docker.swarm.internal": "true"
+            },
+            "DriverConfiguration": {},
+            "IPAM": {
+              "Driver": {},
+              "Configs": [
+                {
+                  "Family": "UNKNOWN",
+                  "Subnet": "10.255.0.0/16"
+                }
+              ]
+            }
+          },
+          "DriverState": {
+            "Name": "overlay",
+            "Options": {
+              "com.docker.network.driver.overlay.vxlanid_list": "256"
+            }
+          },
+          "IPAM": {
+            "Driver": {
+              "Name": "default"
+            },
+            "Configs": [
+              {
+                "Family": "UNKNOWN",
+                "Subnet": "10.255.0.0/16"
+              }
+            ]
+          }
+        },
+        "Addresses": [
+          "10.255.0.2/16"
+        ]
+      }
+    }
+
+Status Codes:
+
+-   **200** – no error
+-   **404** – no such node
+-   **500** – server error
+
+## 3.7 Swarm
+
+### Initialize a new Swarm
+
+**Warning:** this endpoint is part of the Swarm management feature introduced in Docker 1.12, and
+might be subject to non backward-compatible changes.
+
+`POST /swarm/init`
+
+Initialize a new Swarm
+
+**Example request**:
+
+    POST /swarm/init HTTP/1.1
+    Content-Type: application/json
+
+    {
+      "ListenAddr": "0.0.0.0:4500",
+      "ForceNewCluster": false,
+      "Spec": {
+        "AcceptancePolicy": {
+          "Policies": [
+            {
+              "Role": "MANAGER",
+              "Autoaccept": false
+            },
+            {
+              "Role": "WORKER",
+              "Autoaccept": true
+            }
+          ]
+        },
+        "Orchestration": {},
+        "Raft": {},
+        "Dispatcher": {},
+        "CAConfig": {}
+      }
+    }
+
+**Example response**:
+
+    HTTP/1.1 200 OK
+    Content-Length: 0
+    Content-Type: text/plain; charset=utf-8
+
+Status Codes:
+
+- **200** – no error
+- **400** – bad parameter
+- **500** – server error or node is already part of a Swarm
+
+JSON Parameters:
+
+- **ListenAddr** – Listen address used for inter-manager communication, as well as determining.
+  the networking interface used for the VXLAN Tunnel Endpoint (VTEP).
+- **ForceNewCluster** – Force creating a new Swarm even if already part of one.
+- **Spec** – Configuration settings of the new Swarm.
+    - **Policies** – An array of acceptance policies.
+        - **Role** – The role that policy applies to (`MANAGER` or `WORKER`)
+        - **Autoaccept** – A boolean indicating whether nodes joining for that role should be
+          automatically accepted in the Swarm.
+        - **Secret** – An optional secret to provide for nodes to join the Swarm.
+    - **Orchestration** – Configuration settings for the orchestration aspects of the Swarm.
+        - **TaskHistoryRetentionLimit** – Maximum number of tasks history stored.
+    - **RaftConfig** – Raft related configuration.
+        - **SnapshotInterval** – (TODO)
+        - **KeepOldSnapshots** – (TODO)
+        - **LogEntriesForSlowFollowers** – (TODO)
+        - **HeartbeatTick** – (TODO)
+        - **ElectionTick** – (TODO)
+    - **DispatcherConfig** – Configuration settings for the task dispatcher.
+        - **HeartbeatPeriod** – (TODO)
+    - **CAConfig** – CA configuration.
+        - **NodeCertExpiry** – Automatic expiry for nodes certificates.
+
+### Join an existing Swarm
+
+**Warning:** this endpoint is part of the Swarm management feature introduced in Docker 1.12, and
+might be subject to non backward-compatible changes.
+
+`POST /swarm/join`
+
+Join an existing new Swarm
+
+**Example request**:
+
+    POST /swarm/join HTTP/1.1
+    Content-Type: application/json
+
+    {
+      "ListenAddr": "0.0.0.0:4500",
+      "RemoteAddr": "node1:4500",
+      "Secret": "",
+      "CAHash": "",
+      "Manager": false
+    }
+
+**Example response**:
+
+    HTTP/1.1 200 OK
+    Content-Length: 0
+    Content-Type: text/plain; charset=utf-8
+
+Status Codes:
+
+- **200** – no error
+- **400** – bad parameter
+- **500** – server error or node is already part of a Swarm
+
+JSON Parameters:
+
+- **ListenAddr** – Listen address used for inter-manager communication if the node gets promoted to
+  manager, as well as determining the networking interface used for the VXLAN Tunnel Endpoint (VTEP).
+- **RemoteAddr** – Address of any manager node already participating in the Swarm to join.
+- **Secret** – Secret token for joining this Swarm.
+- **CAHash** – Optional hash of the root CA to avoid relying on trust on first use.
+- **Manager** – Directly join as a manager (only for a Swarm configured to autoaccept managers).
+
+### Leave a Swarm
+
+**Warning:** this endpoint is part of the Swarm management feature introduced in Docker 1.12, and
+might be subject to non backward-compatible changes.
+
+`POST /swarm/leave`
+
+Leave a Swarm
+
+**Example request**:
+
+    POST /swarm/leave HTTP/1.1
+
+**Example response**:
+
+    HTTP/1.1 200 OK
+    Content-Length: 0
+    Content-Type: text/plain; charset=utf-8
+
+Status Codes:
+
+- **200** – no error
+- **500** – server error or node is not part of a Swarm
+
+### Update a Swarm
+
+**Warning:** this endpoint is part of the Swarm management feature introduced in Docker 1.12, and
+might be subject to non backward-compatible changes.
+
+`POST /swarm/update`
+
+Update a Swarm
+
+**Example request**:
+
+    POST /swarm/update HTTP/1.1
+
+    (TODO)
+
+**Example response**:
+
+    HTTP/1.1 200 OK
+    Content-Length: 0
+    Content-Type: text/plain; charset=utf-8
+
+    (TODO)
+
+Status Codes:
+
+- **200** – no error
+- **400** – bad parameter
+- **500** – server error or node is not part of a Swarm
+
+## 3.8 Services
+
+**Note:** Service operations require to first be part of a Swarm.
+
+### List services
+
+**Warning:** this endpoint is part of the Swarm management feature introduced in Docker 1.12, and
+might be subject to non backward-compatible changes.
+
+`GET /services`
+
+List services
+
+**Example request**:
+
+    GET /services HTTP/1.1
+
+**Example response**:
+
+    HTTP/1.1 200 OK
+    Content-Type: application/json
+
+    [
+      {
+        "ID": "9mnpnzenvg8p8tdbtq4wvbkcz",
+        "Version": {
+          "Index": 19
+        },
+        "CreatedAt": "2016-06-07T21:05:51.880065305Z",
+        "UpdatedAt": "2016-06-07T21:07:29.962229872Z",
+        "Spec": {
+          "Name": "hopeful_cori",
+          "Task": {
+            "ContainerSpec": {
+              "Image": "redis"
+            },
+            "Resources": {
+              "Limits": {},
+              "Reservations": {}
+            },
+            "RestartPolicy": {
+              "Condition": "ANY"
+            },
+            "Placement": {}
+          },
+          "Mode": {
+            "Replicated": {
+              "Instances": 1
+            }
+          },
+          "UpdateConfig": {
+            "Parallelism": 1
+          },
+          "EndpointSpec": {
+            "Mode": "VIP",
+            "Ingress": "PUBLICPORT",
+            "ExposedPorts": [
+              {
+                "Protocol": "tcp",
+                "Port": 6379
+              }
+            ]
+          }
+        },
+        "Endpoint": {
+          "Spec": {},
+          "ExposedPorts": [
+            {
+              "Protocol": "tcp",
+              "Port": 6379,
+              "PublicPort": 30000
+            }
+          ],
+          "VirtualIPs": [
+            {
+              "NetworkID": "4qvuz4ko70xaltuqbt8956gd1",
+              "Addr": "10.255.0.2/16"
+            },
+            {
+              "NetworkID": "4qvuz4ko70xaltuqbt8956gd1",
+              "Addr": "10.255.0.3/16"
+            }
+          ]
+        }
+      }
+    ]
+
+Query Parameters:
+
+- **filters** – a JSON encoded value of the filters (a `map[string][]string`) to process on the
+  services list. Available filters:
+  - `id=<node id>`
+  - `name=<node name>`
+
+Status Codes:
+
+- **200** – no error
+- **500** – server error
+
+### Create a service
+
+**Warning:** this endpoint is part of the Swarm management feature introduced in Docker 1.12, and
+might be subject to non backward-compatible changes.
+
+`POST /services/create`
+
+Create a service
+
+**Example request**:
+
+    POST /service/create HTTP/1.1
+    Content-Type: application/json
+
+    {
+      "Name": "redis",
+      "Task": {
+        "ContainerSpec": {
+          "Image": "redis"
+        },
+        "Resources": {
+          "Limits": {},
+          "Reservations": {}
+        },
+        "RestartPolicy": {},
+        "Placement": {}
+      },
+      "Mode": {
+        "Replicated": {
+          "Instances": 1
+        }
+      },
+      "UpdateConfig": {
+        "Parallelism": 1
+      },
+      "EndpointSpec": {
+        "ExposedPorts": [
+          {
+            "Protocol": "tcp",
+            "Port": 6379
+          }
+        ]
+      }
+    }
+
+**Example response**:
+
+    HTTP/1.1 201 Created
+    Content-Type: application/json
+
+    {
+      "Id":"ak7w3gjqoa3kuz8xcpnyy0pvl"
+    }
+
+Status Codes:
+
+- **201** – no error
+- **500** – server error or node is not part of a Swarm
+
+JSON Parameters:
+
+- **Annotations** – Optional medata to associate with the service.
+    - **Name** – User-defined name for the service.
+    - **Labels** – A map of labels to associate with the service (e.g.,
+      `{"key":"value"[,"key2":"value2"]}`).
+- **Task** – Specification of the tasks to start as part of the new service.
+    - **ContainerSpec** - Container settings for containers started as part of this task.
+        - **Image** – A string specifying the image name to use for the container.
+        - **Command** – The command to be run in the image.
+        - **Args** – Arguments to the command.
+        - **Env** – A list of environment variables in the form of `["VAR=value"[,"VAR2=value2"]]`.
+        - **Dir** – A string specifying the working directory for commands to run in.
+        - **User** – A string value specifying the user inside the container.
+        - **Labels** – A map of labels to associate with the service (e.g.,
+          `{"key":"value"[,"key2":"value2"]}`).
+        - **Mounts** – Specification for mounts to be added to containers created as part of the new
+          service.
+            - **Target** – Container path.
+            - **Source** – Optional host path to be mounted in the target.
+            - **Type** – The mount type (`bind`, `epheremal`, or `volume`).
+            - **VolumeName** – A name for the volume.
+            - **Populate** – A boolean indicating if volume should be populated with the data form the
+              target (defaults to false).
+            - **Propagation** – A propagation mode with the value `[r]private`, `[r]shared`, or
+              `[r]slave` (`bind` type mounts only).
+            - **MCSAccessMode** – MCS label for sharing mode (`bind` type mounts only).
+            - **Writable** – A boolean indicating whether the mount should be writable.
+            - **VolumeTemplate** – Optional configuration for the volume.
+                - **Annotations** – User-defined name and labels for the volume.
+                - **Driver** – Name of the driver to be used and driver-specific options.
+        - **StopGracePeriod** – Amount of time to wait for the container to terminate before
+          forcefully killing it.
+    - **Resources** – Resource requirements which apply to each individual container created as part
+      of the service.
+        - **Limits** – Define resources limits.
+            - **CPU** – CPU limit
+            - **Memory** – Memory limit
+        - **Reservation** – Define resources reservation.
+            - **CPU** – CPU reservation
+            - **Memory** – Memory reservation
+    - **RestartPolicy** – Specification for the restart policy which applies to containers created
+      as part of this service.
+        - **Condition** – Condition for restart (`none`, `on_failure`, or `any`).
+        - **Delay** – Delay between restart attempts.
+        - **Attempts** – Maximum attempts to restart a given container before giving up (default value
+          is 0, which is ignored).
+        - **Window** – Windows is the time window used to evaluate the restart policy (default value is
+          0, which is unbounded).
+    - **Placement** – An array of constraints.
+- **Mode** – Scheduling mode for the service (`replicated` or `global`, defaults to `replicated`).
+- **UpdateConfig** – Specification for the update strategy of the service.
+    - **Parallelism** – Maximum number of tasks to be updated in one iteration (0 means unlimited
+      parallelism).
+    - **Delay** – Amount of time between updates.
+- **Networks** – Array of network names or IDs to attach the service to.
+- **EndpointSpec** – (TODO)
+    - **EndpointSpecStrategy** – `network` or `disabled` (TODO)
+    - **ExposedPorts** – An object mapping ports to an empty object in the form of:
+      `"ExposedPorts": { "<port>/<tcp|udp>: {}" }`
+
+### Remove a service
+
+**Warning:** this endpoint is part of the Swarm management feature introduced in Docker 1.12, and
+might be subject to non backward-compatible changes.
+
+`DELETE /service/(id or name)`
+
+Stop and remove the service `id`
+
+**Example request**:
+
+    DELETE /service/16253994b7c4 HTTP/1.1
+
+**Example response**:
+
+    HTTP/1.1 204 No Content
+
+Status Codes:
+
+-   **204** – no error
+-   **404** – no such service
+-   **500** – server error
+
+### Inspect a service
+
+**Warning:** this endpoint is part of the Swarm management feature introduced in Docker 1.12, and
+might be subject to non backward-compatible changes.
+
+`GET /service/(id or name)`
+
+Return information on the service `id`.
+
+**Example request**:
+
+    GET /service/1cb4dnqcyx6m66g2t538x3rxha HTTP/1.1
+
+**Example response**:
+
+    {
+      "ID": "ak7w3gjqoa3kuz8xcpnyy0pvl",
+      "Version": {
+        "Index": 95
+      },
+      "CreatedAt": "2016-06-07T21:10:20.269723157Z",
+      "UpdatedAt": "2016-06-07T21:10:20.276301259Z",
+      "Spec": {
+        "Name": "redis",
+        "Task": {
+          "ContainerSpec": {
+            "Image": "redis"
+          },
+          "Resources": {
+            "Limits": {},
+            "Reservations": {}
+          },
+          "RestartPolicy": {
+            "Condition": "ANY"
+          },
+          "Placement": {}
+        },
+        "Mode": {
+          "Replicated": {
+            "Instances": 1
+          }
+        },
+        "UpdateConfig": {
+          "Parallelism": 1
+        },
+        "EndpointSpec": {
+          "Mode": "VIP",
+          "Ingress": "PUBLICPORT",
+          "ExposedPorts": [
+            {
+              "Protocol": "tcp",
+              "Port": 6379
+            }
+          ]
+        }
+      },
+      "Endpoint": {
+        "Spec": {},
+        "ExposedPorts": [
+          {
+            "Protocol": "tcp",
+            "Port": 6379,
+            "PublicPort": 30001
+          }
+        ],
+        "VirtualIPs": [
+          {
+            "NetworkID": "4qvuz4ko70xaltuqbt8956gd1",
+            "Addr": "10.255.0.4/16"
+          }
+        ]
+      }
+    }
+
+Status Codes:
+
+-   **200** – no error
+-   **404** – no such service
+-   **500** – server error
+
+### Update a service
+
+(TODO)
+
+## 3.9 Tasks
+
+**Note:** Tasks operations require to first be part of a Swarm.
+
+### List tasks
+
+**Warning:** this endpoint is part of the Swarm management feature introduced in Docker 1.12, and
+might be subject to non backward-compatible changes.
+
+`GET /tasks`
+
+List tasks
+
+**Example request**:
+
+    GET /tasks HTTP/1.1
+
+**Example response**:
+
+    [
+      {
+        "ID": "0kzzo1i0y4jz6027t0k7aezc7",
+        "Version": {
+          "Index": 71
+        },
+        "CreatedAt": "2016-06-07T21:07:31.171892745Z",
+        "UpdatedAt": "2016-06-07T21:07:31.376370513Z",
+        "Name": "hopeful_cori",
+        "Spec": {
+          "ContainerSpec": {
+            "Image": "redis"
+          },
+          "Resources": {
+            "Limits": {},
+            "Reservations": {}
+          },
+          "RestartPolicy": {
+            "Condition": "ANY"
+          },
+          "Placement": {}
+        },
+        "ServiceID": "9mnpnzenvg8p8tdbtq4wvbkcz",
+        "Instance": 1,
+        "NodeID": "24ifsmvkjbyhk",
+        "ServiceAnnotations": {},
+        "Status": {
+          "Timestamp": "2016-06-07T21:07:31.290032978Z",
+          "State": "FAILED",
+          "Message": "execution failed",
+          "ContainerStatus": {}
+        },
+        "DesiredState": "SHUTDOWN",
+        "NetworksAttachments": [
+          {
+            "Network": {
+              "ID": "4qvuz4ko70xaltuqbt8956gd1",
+              "Version": {
+                "Index": 18
+              },
+              "CreatedAt": "2016-06-07T20:31:11.912919752Z",
+              "UpdatedAt": "2016-06-07T21:07:29.955277358Z",
+              "Spec": {
+                "Name": "ingress",
+                "Labels": {
+                  "com.docker.swarm.internal": "true"
+                },
+                "DriverConfiguration": {},
+                "IPAM": {
+                  "Driver": {},
+                  "Configs": [
+                    {
+                      "Family": "UNKNOWN",
+                      "Subnet": "10.255.0.0/16"
+                    }
+                  ]
+                }
+              },
+              "DriverState": {
+                "Name": "overlay",
+                "Options": {
+                  "com.docker.network.driver.overlay.vxlanid_list": "256"
+                }
+              },
+              "IPAM": {
+                "Driver": {
+                  "Name": "default"
+                },
+                "Configs": [
+                  {
+                    "Family": "UNKNOWN",
+                    "Subnet": "10.255.0.0/16"
+                  }
+                ]
+              }
+            },
+            "Addresses": [
+              "10.255.0.10/16"
+            ]
+          }
+        ],
+        "Endpoint": {
+          "Spec": {},
+          "ExposedPorts": [
+            {
+              "Protocol": "tcp",
+              "Port": 6379,
+              "PublicPort": 30000
+            }
+          ],
+          "VirtualIPs": [
+            {
+              "NetworkID": "4qvuz4ko70xaltuqbt8956gd1",
+              "Addr": "10.255.0.2/16"
+            },
+            {
+              "NetworkID": "4qvuz4ko70xaltuqbt8956gd1",
+              "Addr": "10.255.0.3/16"
+            }
+          ]
+        }
+      },
+      {
+        "ID": "1yljwbmlr8er2waf8orvqpwms",
+        "Version": {
+          "Index": 30
+        },
+        "CreatedAt": "2016-06-07T21:07:30.019104782Z",
+        "UpdatedAt": "2016-06-07T21:07:30.231958098Z",
+        "Name": "hopeful_cori",
+        "Spec": {
+          "ContainerSpec": {
+            "Image": "redis"
+          },
+          "Resources": {
+            "Limits": {},
+            "Reservations": {}
+          },
+          "RestartPolicy": {
+            "Condition": "ANY"
+          },
+          "Placement": {}
+        },
+        "ServiceID": "9mnpnzenvg8p8tdbtq4wvbkcz",
+        "Instance": 1,
+        "NodeID": "24ifsmvkjbyhk",
+        "ServiceAnnotations": {},
+        "Status": {
+          "Timestamp": "2016-06-07T21:07:30.202183143Z",
+          "State": "FAILED",
+          "Message": "execution failed",
+          "ContainerStatus": {}
+        },
+        "DesiredState": "SHUTDOWN",
+        "NetworksAttachments": [
+          {
+            "Network": {
+              "ID": "4qvuz4ko70xaltuqbt8956gd1",
+              "Version": {
+                "Index": 18
+              },
+              "CreatedAt": "2016-06-07T20:31:11.912919752Z",
+              "UpdatedAt": "2016-06-07T21:07:29.955277358Z",
+              "Spec": {
+                "Name": "ingress",
+                "Labels": {
+                  "com.docker.swarm.internal": "true"
+                },
+                "DriverConfiguration": {},
+                "IPAM": {
+                  "Driver": {},
+                  "Configs": [
+                    {
+                      "Family": "UNKNOWN",
+                      "Subnet": "10.255.0.0/16"
+                    }
+                  ]
+                }
+              },
+              "DriverState": {
+                "Name": "overlay",
+                "Options": {
+                  "com.docker.network.driver.overlay.vxlanid_list": "256"
+                }
+              },
+              "IPAM": {
+                "Driver": {
+                  "Name": "default"
+                },
+                "Configs": [
+                  {
+                    "Family": "UNKNOWN",
+                    "Subnet": "10.255.0.0/16"
+                  }
+                ]
+              }
+            },
+            "Addresses": [
+              "10.255.0.5/16"
+            ]
+          }
+        ],
+        "Endpoint": {
+          "Spec": {},
+          "ExposedPorts": [
+            {
+              "Protocol": "tcp",
+              "Port": 6379,
+              "PublicPort": 30000
+            }
+          ],
+          "VirtualIPs": [
+            {
+              "NetworkID": "4qvuz4ko70xaltuqbt8956gd1",
+              "Addr": "10.255.0.2/16"
+            },
+            {
+              "NetworkID": "4qvuz4ko70xaltuqbt8956gd1",
+              "Addr": "10.255.0.3/16"
+            }
+          ]
+        }
+      }
+    ]
+
+Query Parameters:
+
+- **filters** – a JSON encoded value of the filters (a `map[string][]string`) to process on the
+  services list. Available filters:
+  - `id=<task id>`
+  - `name=<task name>`
+  - `service=<service name>`
+
+Status Codes:
+
+- **200** – no error
+- **500** – server error
+
+### Inspect a task
+
+**Warning:** this endpoint is part of the Swarm management feature introduced in Docker 1.12, and
+might be subject to non backward-compatible changes.
+
+`GET /tasks/(task id)`
+
+Get details on a task
+
+**Example request**:
+
+    GET /tasks/0kzzo1i0y4jz6027t0k7aezc7 HTTP/1.1
+
+**Example response**:
+
+    {
+      "ID": "0kzzo1i0y4jz6027t0k7aezc7",
+      "Version": {
+        "Index": 71
+      },
+      "CreatedAt": "2016-06-07T21:07:31.171892745Z",
+      "UpdatedAt": "2016-06-07T21:07:31.376370513Z",
+      "Name": "hopeful_cori",
+      "Spec": {
+        "ContainerSpec": {
+          "Image": "redis"
+        },
+        "Resources": {
+          "Limits": {},
+          "Reservations": {}
+        },
+        "RestartPolicy": {
+          "Condition": "ANY"
+        },
+        "Placement": {}
+      },
+      "ServiceID": "9mnpnzenvg8p8tdbtq4wvbkcz",
+      "Instance": 1,
+      "NodeID": "24ifsmvkjbyhk",
+      "ServiceAnnotations": {},
+      "Status": {
+        "Timestamp": "2016-06-07T21:07:31.290032978Z",
+        "State": "FAILED",
+        "Message": "execution failed",
+        "ContainerStatus": {}
+      },
+      "DesiredState": "SHUTDOWN",
+      "NetworksAttachments": [
+        {
+          "Network": {
+            "ID": "4qvuz4ko70xaltuqbt8956gd1",
+            "Version": {
+              "Index": 18
+            },
+            "CreatedAt": "2016-06-07T20:31:11.912919752Z",
+            "UpdatedAt": "2016-06-07T21:07:29.955277358Z",
+            "Spec": {
+              "Name": "ingress",
+              "Labels": {
+                "com.docker.swarm.internal": "true"
+              },
+              "DriverConfiguration": {},
+              "IPAM": {
+                "Driver": {},
+                "Configs": [
+                  {
+                    "Family": "UNKNOWN",
+                    "Subnet": "10.255.0.0/16"
+                  }
+                ]
+              }
+            },
+            "DriverState": {
+              "Name": "overlay",
+              "Options": {
+                "com.docker.network.driver.overlay.vxlanid_list": "256"
+              }
+            },
+            "IPAM": {
+              "Driver": {
+                "Name": "default"
+              },
+              "Configs": [
+                {
+                  "Family": "UNKNOWN",
+                  "Subnet": "10.255.0.0/16"
+                }
+              ]
+            }
+          },
+          "Addresses": [
+            "10.255.0.10/16"
+          ]
+        }
+      ],
+      "Endpoint": {
+        "Spec": {},
+        "ExposedPorts": [
+          {
+            "Protocol": "tcp",
+            "Port": 6379,
+            "PublicPort": 30000
+          }
+        ],
+        "VirtualIPs": [
+          {
+            "NetworkID": "4qvuz4ko70xaltuqbt8956gd1",
+            "Addr": "10.255.0.2/16"
+          },
+          {
+            "NetworkID": "4qvuz4ko70xaltuqbt8956gd1",
+            "Addr": "10.255.0.3/16"
+          }
+        ]
+      }
+    }
+
+Status Codes:
+
+- **200** – no error
+- **404** – unknown task
+- **500** – server error
+
 # 4. Going further
 # 4. Going further
 
 
 ## 4.1 Inside `docker run`
 ## 4.1 Inside `docker run`

+ 20 - 0
docs/reference/commandline/index.md

@@ -86,3 +86,23 @@ You start the Docker daemon with the command line. How you start the daemon affe
 * [volume_inspect](volume_inspect.md)
 * [volume_inspect](volume_inspect.md)
 * [volume_ls](volume_ls.md)
 * [volume_ls](volume_ls.md)
 * [volume_rm](volume_rm.md)
 * [volume_rm](volume_rm.md)
+
+### Swarm node commands
+
+* [node_accept](node_accept.md)
+* [node_reject](node_reject.md)
+* [node_promote](node_promote.md)
+* [node_demote](node_demote.md)
+* [node_inspect](node_inspect.md)
+* [node_update](node_update.md)
+* [node_tasks](node_tasks.md)
+* [node_ls](node_ls.md)
+* [node_rm](node_rm.md)
+
+### Swarm swarm commands
+
+* [swarm init](swarm_init.md)
+* [swarm join](swarm_join.md)
+* [swarm leave](swarm_leave.md)
+* [swarm update](swarm_update.md)
+

+ 6 - 1
docs/reference/commandline/info.md

@@ -37,7 +37,7 @@ available on the volume where `/var/lib/docker` is mounted.
 ## Display Docker system information
 ## Display Docker system information
 
 
 Here is a sample output for a daemon running on Ubuntu, using the overlay
 Here is a sample output for a daemon running on Ubuntu, using the overlay
-storage driver:
+storage driver and a node that is part of a 2 node Swarm cluster:
 
 
     $ docker -D info
     $ docker -D info
     Containers: 14
     Containers: 14
@@ -53,6 +53,11 @@ storage driver:
     Plugins:
     Plugins:
      Volume: local
      Volume: local
      Network: bridge null host
      Network: bridge null host
+    Swarm: 
+     NodeID: 0gac67oclbxq7
+     IsManager: YES
+     Managers: 2
+     Nodes: 2
     Kernel Version: 4.4.0-21-generic
     Kernel Version: 4.4.0-21-generic
     Operating System: Ubuntu 16.04 LTS
     Operating System: Ubuntu 16.04 LTS
     OSType: linux
     OSType: linux

+ 11 - 7
docs/reference/commandline/inspect.md

@@ -10,15 +10,15 @@ parent = "smn_cli"
 
 
 # inspect
 # inspect
 
 
-    Usage: docker inspect [OPTIONS] CONTAINER|IMAGE [CONTAINER|IMAGE...]
+    Usage: docker inspect [OPTIONS] CONTAINER|IMAGE|TASK [CONTAINER|IMAGE|TASK...]
 
 
-    Return low-level information on a container or image
+    Return low-level information on a container or image or task
 
 
-      -f, --format=""         Format the output using the given go template
-      --help                  Print usage
-      --type=container|image  Return JSON for specified type, permissible
-                              values are "image" or "container"
-      -s, --size              Display total file sizes if the type is container
+      -f, --format=""              Format the output using the given go template
+      --help                       Print usage
+      --type=container|image|task  Return JSON for specified type, permissible
+                                   values are "image" or "container" or "task"
+      -s, --size                   Display total file sizes if the type is container
 
 
 By default, this will render all results in a JSON array. If the container and
 By default, this will render all results in a JSON array. If the container and
 image have the same name, this will return container JSON for unspecified type.
 image have the same name, this will return container JSON for unspecified type.
@@ -47,6 +47,10 @@ straightforward manner.
 
 
     $ docker inspect --format='{{.LogPath}}' $INSTANCE_ID
     $ docker inspect --format='{{.LogPath}}' $INSTANCE_ID
 
 
+**Get a Task's image name:**
+
+    $ docker inspect --format='{{.Container.Spec.Image}}' $INSTANCE_ID
+
 **List All Port Bindings:**
 **List All Port Bindings:**
 
 
 One can loop over arrays and maps in the results to produce simple text
 One can loop over arrays and maps in the results to produce simple text

+ 28 - 0
docs/reference/commandline/node_accept.md

@@ -0,0 +1,28 @@
+<!--[metadata]>
++++
+title = "node accept"
+description = "The node accept command description and usage"
+keywords = ["node, accept"]
+[menu.main]
+parent = "smn_cli"
++++
+<![end-metadata]-->
+
+# node accept
+
+    Usage:  docker node accept NODE [NODE...]
+
+    Accept a node in the swarm
+
+Accept a node into the swarm. This command targets a docker engine that is a manager in the swarm cluster.
+
+
+```bash
+$ docker node accept <node name>
+```
+
+## Related information
+
+* [node reject](node_reject.md)
+* [node promote](node_promote.md)
+* [node demote](node_demote.md)

+ 28 - 0
docs/reference/commandline/node_demote.md

@@ -0,0 +1,28 @@
+<!--[metadata]>
++++
+title = "node demote"
+description = "The node demote command description and usage"
+keywords = ["node, demote"]
+[menu.main]
+parent = "smn_cli"
++++
+<![end-metadata]-->
+
+# node demote
+
+    Usage:  docker node demote NODE [NODE...]
+
+    Demote a node as manager in the swarm
+
+Demotes an existing Manager so that it is no longer a manager. This command targets a docker engine that is a manager in the swarm cluster.
+
+
+```bash
+$ docker node demote <node name>
+```
+
+## Related information
+
+* [node accept](node_accept.md)
+* [node reject](node_reject.md)
+* [node promote](node_promote.md)

+ 108 - 0
docs/reference/commandline/node_inspect.md

@@ -0,0 +1,108 @@
+<!--[metadata]>
++++
+title = "node inspect"
+description = "The node inspect command description and usage"
+keywords = ["node, inspect"]
+[menu.main]
+parent = "smn_cli"
++++
+<![end-metadata]-->
+
+**Warning:** this command is part of the Swarm management feature introduced in Docker 1.12, and might be subject to non backward-compatible changes.
+
+# node inspect
+
+    Usage: docker node inspect [OPTIONS] self|NODE [NODE...]
+
+    Return low-level information on a volume
+
+      -f, --format=       Format the output using the given go template.
+      --help              Print usage
+      -p, --pretty        Print the information in a human friendly format.
+
+Returns information about a node. By default, this command renders all results
+in a JSON array. You can specify an alternate format to execute a
+given template for each result. Go's
+[text/template](http://golang.org/pkg/text/template/) package describes all the
+details of the format.
+
+Example output:
+
+    $ docker node inspect swarm-manager
+    [
+      {
+        "ID": "0gac67oclbxq7",
+        "Version": {
+            "Index": 2028
+        },
+        "CreatedAt": "2016-06-06T20:49:32.720047494Z",
+        "UpdatedAt": "2016-06-07T00:23:31.207632893Z",
+        "Spec": {
+            "Role": "MANAGER",
+            "Membership": "ACCEPTED",
+            "Availability": "ACTIVE"
+        },
+        "Description": {
+            "Hostname": "swarm-manager",
+            "Platform": {
+                "Architecture": "x86_64",
+                "OS": "linux"
+            },
+            "Resources": {
+                "NanoCPUs": 1000000000,
+                "MemoryBytes": 1044250624
+            },
+            "Engine": {
+                "EngineVersion": "1.12.0",
+                "Labels": {
+                    "provider": "virtualbox"
+                }
+            }
+        },
+        "Status": {
+            "State": "READY"
+        },
+        "Manager": {
+            "Raft": {
+                "RaftID": 2143745093569717375,
+                "Addr": "192.168.99.118:4500",
+                "Status": {
+                    "Leader": true,
+                    "Reachability": "REACHABLE"
+                }
+            }
+        },
+        "Attachment": {},
+      }
+    ]
+
+    $ docker node inspect --format '{{ .Manager.Raft.Status.Leader }}' self
+    false
+
+    $ docker node inspect --pretty self
+    ID:                     2otfhz83efcc7
+    Hostname:               ad960a848573
+    Status:
+     State:                 Ready
+     Availability:          Active
+    Manager Status:
+     Address:               172.17.0.2:2377
+     Raft status:           Reachable
+     Leader:                Yes
+    Platform:
+     Operating System:      linux
+     Architecture:          x86_64
+    Resources:
+     CPUs:                  4
+     Memory:                7.704 GiB
+    Plugins:
+      Network:              overlay, bridge, null, host, overlay
+      Volume:               local
+    Engine Version:         1.12.0
+
+## Related information
+
+* [node update](node_update.md)
+* [node tasks](node_tasks.md)
+* [node ls](node_ls.md)
+* [node rm](node_rm.md)

+ 89 - 0
docs/reference/commandline/node_ls.md

@@ -0,0 +1,89 @@
+<!--[metadata]>
++++
+title = "node ls"
+description = "The node ls command description and usage"
+keywords = ["node, list"]
+[menu.main]
+parent = "smn_cli"
++++
+<![end-metadata]-->
+
+**Warning:** this command is part of the Swarm management feature introduced in Docker 1.12, and might be subject to non backward-compatible changes.
+
+# node ls
+
+    Usage:  docker node ls [OPTIONS]
+
+    List nodes in the swarm
+
+    Aliases:
+      ls, list
+
+    Options:
+      -f, --filter value   Filter output based on conditions provided
+          --help           Print usage
+      -q, --quiet          Only display IDs
+
+Lists all the nodes that the Docker Swarm manager knows about. You can filter using the `-f` or `--filter` flag. Refer to the [filtering](#filtering) section for more information about available filter options.
+
+Example output:
+
+    $ docker node ls
+    ID              NAME           STATUS  AVAILABILITY     MANAGER STATUS  LEADER
+    0gac67oclbxq    swarm-master   Ready   Active           Reachable       Yes
+    0pwvm3ve66q7    swarm-node-02  Ready   Active
+    15xwihgw71aw *  swarm-node-01  Ready   Active           Reachable
+
+
+## Filtering
+
+The filtering flag (`-f` or `--filter`) format is of "key=value". If there is more
+than one filter, then pass multiple flags (e.g., `--filter "foo=bar" --filter "bif=baz"`)
+
+The currently supported filters are:
+
+* name
+* id
+* label
+* desired_state
+
+### name
+
+The `name` filter matches on all or part of a tasks's name.
+
+The following filter matches the node with a name equal to `swarm-master` string.
+
+    $ docker node ls -f name=swarm-master
+    ID              NAME          STATUS  AVAILABILITY      MANAGER STATUS  LEADER
+    0gac67oclbxq *  swarm-master  Ready   Active            Reachable       Yes
+
+### id
+
+The `id` filter matches all or part of a node's id.
+
+    $ docker node ls -f id=0
+    ID              NAME           STATUS  AVAILABILITY     MANAGER STATUS  LEADER
+    0gac67oclbxq *  swarm-master   Ready   Active           Reachable       Yes
+    0pwvm3ve66q7    swarm-node-02  Ready   Active
+
+
+#### label
+
+The `label` filter matches tasks based on the presence of a `label` alone or a `label` and a
+value.
+
+The following filter matches nodes with the `usage` label regardless of its value.
+
+```bash
+$ docker node ls -f "label=foo"
+ID              NAME           STATUS  AVAILABILITY     MANAGER STATUS  LEADER
+15xwihgw71aw *  swarm-node-01  Ready   Active           Reachable
+```
+
+
+## Related information
+
+* [node inspect](node_inspect.md)
+* [node update](node_update.md)
+* [node tasks](node_tasks.md)
+* [node rm](node_rm.md)

+ 28 - 0
docs/reference/commandline/node_promote.md

@@ -0,0 +1,28 @@
+<!--[metadata]>
++++
+title = "node promote"
+description = "The node promote command description and usage"
+keywords = ["node, promote"]
+[menu.main]
+parent = "smn_cli"
++++
+<![end-metadata]-->
+
+# node promote
+
+    Usage:  docker node promote NODE [NODE...]
+
+    Promote a node as manager in the swarm
+
+Promotes a node that is pending a promotion to manager. This command targets a docker engine that is a manager in the swarm cluster.
+
+
+```bash
+$ docker node promote <node name>
+```
+
+## Related information
+
+* [node accept](node_accept.md)
+* [node reject](node_reject.md)
+* [node demote](node_demote.md)

+ 28 - 0
docs/reference/commandline/node_reject.md

@@ -0,0 +1,28 @@
+<!--[metadata]>
++++
+title = "node reject"
+description = "The node reject command description and usage"
+keywords = ["node, reject"]
+[menu.main]
+parent = "smn_cli"
++++
+<![end-metadata]-->
+
+# node reject
+
+    Usage:  docker node reject NODE [NODE...]
+
+    Reject a node from the swarm
+
+Reject a node from joining the swarm. This command targets a docker engine that is a manager in the swarm cluster.
+
+
+```bash
+$ docker node reject <node name>
+```
+
+## Related information
+
+* [node accept](node_accept.md)
+* [node promote](node_promote.md)
+* [node demote](node_demote.md)

+ 38 - 0
docs/reference/commandline/node_rm.md

@@ -0,0 +1,38 @@
+<!--[metadata]>
++++
+title = "node rm"
+description = "The node rm command description and usage"
+keywords = ["node, remove"]
+[menu.main]
+parent = "smn_cli"
++++
+<![end-metadata]-->
+
+**Warning:** this command is part of the Swarm management feature introduced in Docker 1.12, and might be subject to non backward-compatible changes.
+
+# node rm
+
+	Usage:	docker node rm NODE [NODE...]
+
+	Remove a node from the swarm
+
+	Aliases:
+	  rm, remove
+
+	Options:
+	      --help   Print usage
+
+Removes nodes that are specified. 
+
+Example output:
+
+    $ docker node rm swarm-node-02
+    Node swarm-node-02 removed from Swarm
+
+
+## Related information
+
+* [node inspect](node_inspect.md)
+* [node update](node_update.md)
+* [node tasks](node_tasks.md)
+* [node ls](node_ls.md)

+ 94 - 0
docs/reference/commandline/node_tasks.md

@@ -0,0 +1,94 @@
+<!--[metadata]>
++++
+title = "node tasks"
+description = "The node tasks command description and usage"
+keywords = ["node, tasks"]
+[menu.main]
+parent = "smn_cli"
++++
+<![end-metadata]-->
+
+**Warning:** this command is part of the Swarm management feature introduced in Docker 1.12, and might be subject to non backward-compatible changes.
+
+# node tasks
+
+    Usage:  docker node tasks [OPTIONS] NODE
+
+    List tasks running on a node
+
+    Options:
+      -a, --all            Display all instances
+      -f, --filter value   Filter output based on conditions provided
+      --help           Print usage
+      -n, --no-resolve     Do not map IDs to Names
+
+Lists all the tasks on a Node that Docker knows about. You can filter using the `-f` or `--filter` flag. Refer to the [filtering](#filtering) section for more information about available filter options.
+
+Example output:
+
+    $ docker node tasks swarm-master
+    ID                         NAME     SERVICE  IMAGE        DESIRED STATE  LAST STATE       NODE
+    dx2g0fe3zsdb6y6q453f8dqw2  redis.1  redis    redis:3.0.6  RUNNING        RUNNING 2 hours  swarm-master
+    f33pcf8lwhs4c1t4kq8szwzta  redis.4  redis    redis:3.0.6  RUNNING        RUNNING 2 hours  swarm-master
+    5v26yzixl3one3ptjyqqbd0ro  redis.5  redis    redis:3.0.6  RUNNING        RUNNING 2 hours  swarm-master
+    adcaphlhsfr30d47lby6walg6  redis.8  redis    redis:3.0.6  RUNNING        RUNNING 2 hours  swarm-master
+    chancjvk9tex6768uzzacslq2  redis.9  redis    redis:3.0.6  RUNNING        RUNNING 2 hours  swarm-master
+
+
+## Filtering
+
+The filtering flag (`-f` or `--filter`) format is of "key=value". If there is more
+than one filter, then pass multiple flags (e.g., `--filter "foo=bar" --filter "bif=baz"`)
+
+The currently supported filters are:
+
+* name
+* id
+* label
+* desired_state
+
+### name
+
+The `name` filter matches on all or part of a task's name.
+
+The following filter matches all tasks with a name containing the `redis` string.
+
+    $ docker node tasks -f name=redis swarm-master
+    ID                         NAME     SERVICE  IMAGE        DESIRED STATE  LAST STATE       NODE
+    dx2g0fe3zsdb6y6q453f8dqw2  redis.1  redis    redis:3.0.6  RUNNING        RUNNING 2 hours  swarm-master
+    f33pcf8lwhs4c1t4kq8szwzta  redis.4  redis    redis:3.0.6  RUNNING        RUNNING 2 hours  swarm-master
+    5v26yzixl3one3ptjyqqbd0ro  redis.5  redis    redis:3.0.6  RUNNING        RUNNING 2 hours  swarm-master
+    adcaphlhsfr30d47lby6walg6  redis.8  redis    redis:3.0.6  RUNNING        RUNNING 2 hours  swarm-master
+    chancjvk9tex6768uzzacslq2  redis.9  redis    redis:3.0.6  RUNNING        RUNNING 2 hours  swarm-master
+
+
+### id
+
+The `id` filter matches a task's id.
+
+    $ docker node tasks -f id=f33pcf8lwhs4c1t4kq8szwzta swarm-master
+    ID                         NAME     SERVICE  IMAGE        DESIRED STATE  LAST STATE       NODE
+    f33pcf8lwhs4c1t4kq8szwzta  redis.4  redis    redis:3.0.6  RUNNING        RUNNING 2 hours  swarm-master
+
+
+#### label
+
+The `label` filter matches tasks based on the presence of a `label` alone or a `label` and a
+value.
+
+The following filter matches tasks with the `usage` label regardless of its value.
+
+```bash
+$ docker node tasks -f "label=usage"
+ID                         NAME     SERVICE  IMAGE        DESIRED STATE  LAST STATE       NODE
+dx2g0fe3zsdb6y6q453f8dqw2  redis.1  redis    redis:3.0.6  RUNNING        RUNNING 2 hours  swarm-master
+f33pcf8lwhs4c1t4kq8szwzta  redis.4  redis    redis:3.0.6  RUNNING        RUNNING 2 hours  swarm-master
+```
+
+
+## Related information
+
+* [node inspect](node_inspect.md)
+* [node update](node_update.md)
+* [node ls](node_ls.md)
+* [node rm](node_rm.md)

+ 26 - 0
docs/reference/commandline/node_update.md

@@ -0,0 +1,26 @@
+<!--[metadata]>
++++
+title = "node update"
+description = "The node update command description and usage"
+keywords = ["resources, update, dynamically"]
+[menu.main]
+parent = "smn_cli"
++++
+<![end-metadata]-->
+
+**Warning:** this command is part of the Swarm management feature introduced in Docker 1.12, and might be subject to non backward-compatible changes.
+
+## update
+
+    Usage: docker node update [OPTIONS] Node
+
+    Update a node
+
+
+
+## Related information
+
+* [node inspect](node_inspect.md)
+* [node tasks](node_tasks.md)
+* [node ls](node_ls.md)
+* [node rm](node_rm.md)

+ 69 - 0
docs/reference/commandline/swarm_init.md

@@ -0,0 +1,69 @@
+<!--[metadata]>
++++
+title = "swarm init"
+description = "The swarm init command description and usage"
+keywords = ["swarm, init"]
+[menu.main]
+parent = "smn_cli"
++++
+<![end-metadata]-->
+
+**Warning:** this command is part of the Swarm management feature introduced in Docker 1.12, and might be subject to non backward-compatible changes.
+
+# swarm init
+
+	Usage:	docker swarm init [OPTIONS]
+
+	Initialize a Swarm.
+
+	Options:
+	      --auto-accept value   Acceptance policy (default [worker,manager])
+	      --force-new-cluster   Force create a new cluster from current state.
+	      --help                Print usage
+	      --listen-addr value   Listen address (default 0.0.0.0:2377)
+	      --secret string       Set secret value needed to accept nodes into cluster
+
+Initialize a Swarm cluster. The docker engine targeted by this command becomes a manager
+in the newly created one node Swarm cluster.
+
+
+```bash
+$ docker swarm init --listen-addr 192.168.99.121:2377
+Initializing a new swarm
+$ docker node ls
+ID              NAME          STATUS  AVAILABILITY/MEMBERSHIP  MANAGER STATUS  LEADER
+3l1f6uzcuoa3 *  swarm-master  READY   ACTIVE                   REACHABLE       Yes
+```
+
+###	--auto-accept value
+
+This flag controls node acceptance into the cluster. By default, both `worker` and `manager`
+nodes are auto accepted by the cluster. This can be changed by specifing what kinds of nodes
+can be auto-accepted into the cluster. If auto-accept is not turned on, then 
+[node accept](node_accept.md) can be used to explicitly accept a node into the cluster.
+
+For example, the following initializes a cluster with auto-acceptance of workers, but not managers
+
+
+```bash
+$ docker swarm init --listen-addr 192.168.99.121:2377 --auto-accept worker
+Initializing a new swarm
+```
+
+### `--force-new-cluster`
+
+This flag forces an existing node that was part of a quorum that was lost to restart as a single node Manager without losing its data
+
+### `--listen-addr value`
+
+The node listens for inbound Swarm manager traffic on this IP:PORT
+
+### `--secret string`
+
+Secret value needed to accept nodes into the Swarm
+
+## Related information
+
+* [swarm join](swarm_join.md)
+* [swarm leave](swarm_leave.md)
+* [swarm update](swarm_update.md)

+ 68 - 0
docs/reference/commandline/swarm_join.md

@@ -0,0 +1,68 @@
+<!--[metadata]>
++++
+title = "swarm join"
+description = "The swarm join command description and usage"
+keywords = ["swarm, join"]
+[menu.main]
+parent = "smn_cli"
++++
+<![end-metadata]-->
+
+**Warning:** this command is part of the Swarm management feature introduced in Docker 1.12, and might be subject to non backward-compatible changes.
+
+# swarm join
+
+	Usage:	docker swarm join [OPTIONS] HOST:PORT
+
+	Join a Swarm as a node and/or manager.
+
+	Options:
+	      --help                Print usage
+	      --listen-addr value   Listen address (default 0.0.0.0:2377)
+	      --manager             Try joining as a manager.
+	      --secret string       Secret for node acceptance
+
+Join a node to a Swarm cluster. If the `--manager` flag is specified, the docker engine
+targeted by this command becomes a `manager`. If it is not specified, it becomes a `worker`.
+
+### Join a node to swarm as a manager
+
+```bash
+$ docker swarm join --manager --listen-addr 192.168.99.122:2377 192.168.99.121:2377
+This node is attempting to join a Swarm as a manager.
+$ docker node ls
+ID              NAME           STATUS  AVAILABILITY/MEMBERSHIP  MANAGER STATUS  LEADER
+2fg70txcrde2    swarm-node-01  READY   ACTIVE                   REACHABLE       
+3l1f6uzcuoa3 *  swarm-master   READY   ACTIVE                   REACHABLE       Yes
+```
+
+### Join a node to swarm as a worker
+
+```bash
+$ docker swarm join --listen-addr 192.168.99.123:2377 192.168.99.121:2377
+This node is attempting to join a Swarm.
+$ docker node ls
+ID              NAME           STATUS  AVAILABILITY/MEMBERSHIP  MANAGER STATUS  LEADER
+04zm7ue1fd1q    swarm-node-02  READY   ACTIVE                                   
+2fg70txcrde2    swarm-node-01  READY   ACTIVE                   REACHABLE       
+3l1f6uzcuoa3 *  swarm-master   READY   ACTIVE                   REACHABLE       Yes
+```
+
+### `--manager`
+
+Joins the node as a manager
+
+### `--listen-addr value`
+
+The node listens for inbound Swarm manager traffic on this IP:PORT
+
+### `--secret string`
+
+Secret value required for nodes to join the swarm
+
+
+## Related information
+
+* [swarm init](swarm_init.md)
+* [swarm leave](swarm_leave.md)
+* [swarm update](swarm_update.md)

+ 52 - 0
docs/reference/commandline/swarm_leave.md

@@ -0,0 +1,52 @@
+<!--[metadata]>
++++
+title = "swarm leave"
+description = "The swarm leave command description and usage"
+keywords = ["swarm, leave"]
+[menu.main]
+parent = "smn_cli"
++++
+<![end-metadata]-->
+
+**Warning:** this command is part of the Swarm management feature introduced in Docker 1.12, and might be subject to non backward-compatible changes.
+
+# swarm leave
+
+	Usage:	docker swarm leave
+
+	Leave a Swarm swarm.
+
+	Options:
+	      --help   Print usage
+
+This command causes the node to leave the swarm.
+
+On a manager node:
+```bash
+$ docker node ls
+ID              NAME           STATUS  AVAILABILITY/MEMBERSHIP  MANAGER STATUS  LEADER
+04zm7ue1fd1q    swarm-node-02  READY   ACTIVE                                   
+2fg70txcrde2    swarm-node-01  READY   ACTIVE                   REACHABLE       
+3l1f6uzcuoa3 *  swarm-master   READY   ACTIVE                   REACHABLE       Yes
+```
+
+On a worker node:
+```bash
+$ docker swarm leave
+Node left the default swarm.
+```
+
+On a manager node:
+```bash
+$ docker node ls
+ID              NAME           STATUS  AVAILABILITY/MEMBERSHIP  MANAGER STATUS  LEADER
+04zm7ue1fd1q    swarm-node-02  DOWN    ACTIVE                                   
+2fg70txcrde2    swarm-node-01  READY   ACTIVE                   REACHABLE       
+3l1f6uzcuoa3 *  swarm-master   READY   ACTIVE                   REACHABLE       Yes
+```
+
+## Related information
+
+* [swarm init](swarm_init.md)
+* [swarm join](swarm_join.md)
+* [swarm update](swarm_update.md)

+ 37 - 0
docs/reference/commandline/swarm_update.md

@@ -0,0 +1,37 @@
+<!--[metadata]>
++++
+title = "swarm update"
+description = "The swarm update command description and usage"
+keywords = ["swarm, update"]
+[menu.main]
+parent = "smn_cli"
++++
+<![end-metadata]-->
+
+**Warning:** this command is part of the Swarm management feature introduced in Docker 1.12, and might be subject to non backward-compatible changes.
+
+# swarm update
+
+	Usage:    docker swarm update [OPTIONS]
+
+	update the Swarm.
+
+	Options:
+	      --auto-accept value   Acceptance policy (default [worker,manager])
+	      --help                Print usage
+	      --secret string       Set secret value needed to accept nodes into cluster
+
+
+Updates a Swarm cluster with new parameter values. This command must target a manager node.
+
+
+```bash
+$ docker swarm update --auto-accept manager
+```
+
+## Related information
+
+* [swarm init](swarm_init.md)
+* [swarm join](swarm_join.md)
+* [swarm leave](swarm_leave.md)
+

+ 79 - 0
docs/swarm/index.md

@@ -0,0 +1,79 @@
+<!--[metadata]>
++++
+title = "Swarm overview"
+description = "Docker Swarm overview"
+keywords = ["docker, container, cluster, swarm"]
+[menu.main]
+identifier="swarm_overview"
+parent="engine_swarm"
+weight="1"
+advisory = "rc"
++++
+<![end-metadata]-->
+# Docker Swarm overview
+
+To use this version of Swarm, install the Docker Engine `v1.12.0-rc1` or later
+from the [Docker releases GitHub
+repository](https://github.com/docker/docker/releases). Alternatively, install
+the latest Docker for Mac or Docker for Windows Beta.
+
+Docker Engine 1.12 includes Docker Swarm for natively managing a cluster of
+Docker Engines called a Swarm. Use the Docker CLI to create a Swarm, deploy
+application services to the Swarm, and manage the Swarm behavior.
+
+
+If you’re using a Docker version prior to `v1.12.0-rc1`, see [Docker
+Swarm](https://docs.docker.com/swarm).
+
+## Feature highlights
+
+* **Cluster management integrated with Docker Engine:** Use the Docker Engine
+CLI to create a Swarm of Docker Engines where you can deploy application
+services. You don't need additional orchestration software to create or manage
+a Swarm.
+
+* **Decentralized design:** Instead of handling differentiation between node
+roles at deployment time, Swarm handles any specialization at runtime. You can
+deploy both kinds of nodes, managers and workers, using the Docker Engine.
+This means you can build an entire Swarm from a single disk image.
+
+* **Declarative service model:** Swarm uses a declarative syntax to let you
+define the desired state of the various services in your application stack.
+For example, you might describe an application comprised of a web front end
+service with message queueing services and a database backend.
+
+* **Desired state reconciliation:** Swarm constantly monitors the cluster state
+and reconciles any differences between the actual state your expressed desired
+state.
+
+* **Multi-host networking:** You can specify an overlay network for your
+application. Swarm automatically assigns addresses to the containers on the
+overlay network when it initializes or updates the application.
+
+* **Service discovery:** Swarm assigns each service a unique DNS name and load
+balances running containers. Each Swarm has an internal DNS server that can
+query every container in the cluster using DNS.
+
+* **Load balancing:** Using Swarm, you can expose the ports for services to an
+external load balancer. Internally, Swarm lets you specify how to distribute
+service containers between nodes.
+
+* **Secure by default:** Each node in the Swarm enforces TLS mutual
+authentication and encryption to secure communications between itself and all
+other nodes. You have the option to use self-signed root certificates or
+certificates from a custom root CA.
+
+* **Scaling:** For each service, you can declare the number of instances you
+want to run. When you scale up or down, Swarm automatically adapts by adding
+or removing instances of the service to maintain the desired state.
+
+* **Rolling updates:** At rollout time you can apply service updates to nodes
+incrementally. Swarm lets you control the delay between service deployment to
+different sets of nodes. If anything goes wrong, you can roll-back an instance
+of a service.
+
+## What's next?
+* Learn Swarm [key concepts](key-concepts.md).
+* Get started with the [Swarm tutorial](swarm-tutorial/index.md).
+
+<p style="margin-bottom:300px">&nbsp;</p>

+ 85 - 0
docs/swarm/key-concepts.md

@@ -0,0 +1,85 @@
+<!--[metadata]>
++++
+title = "Swarm key concepts"
+description = "Introducing key concepts for Docker Swarm"
+keywords = ["docker, container, cluster, swarm"]
+[menu.main]
+identifier="swarm-concepts"
+parent="engine_swarm"
+weight="2"
+advisory = "rc"
++++
+<![end-metadata]-->
+# Docker Swarm key concepts
+
+Building upon the core features of Docker Engine, Docker Swarm enables you to
+create a Swarm of Docker Engines and orchestrate services to run in the Swarm.
+This topic describes key concepts to help you begin using Docker Swarm.
+
+## Swarm
+
+**Docker Swarm** is the name for the cluster management and orchestration features
+embedded in the Docker Engine.
+
+A **Swarm** is a cluster of Docker Engines where you deploy a set of application
+services. When you deploy an application to a Swarm, you specify the desired
+state of the services, such as which services to run and how many instances of
+those services. The Swarm takes care of all orchestration duties required to
+keep the services running in the desired state.
+
+## Node
+
+A **node** is an active instance of the Docker Engine in the Swarm.
+
+When you deploy your application to a Swarm, **manager nodes** accept the
+service definition that describes the Swarm's desired state. Manager nodes also
+perform the orchestration and cluster management functions required to maintain
+the desired state of the Swarm. For example, when a manager node receives notice
+to deploy a web server, it dispatches the service tasks to worker nodes.
+
+By default the Docker Engine starts one manager node for a Swarm, but as you
+scale you can add more managers to make the cluster more fault-tolerant. If you
+require high availability Swarm management, Docker recommends three or five
+Managers in your cluster.
+
+Because Swarm manager nodes share data using Raft, there must be an odd number
+of managers. The Swarm cluster can continue functioning in the face of up to
+`N/2` failures where `N` is the number of manager nodes.  More than five
+managers is likely to degrade cluster performance and is not recommended.
+
+**Worker nodes** receive and execute tasks dispatched from manager nodes. By
+default manager nodes are also worker nodes, but you can configure managers to
+be manager-only nodes.
+
+## Services and tasks
+
+A **service** is the definition of how to run the various tasks that make up
+your application. For example, you may create a service that deploys a Redis
+image in your Swarm.
+
+A **task** is the atomic scheduling unit of Swarm. For example a task may be to
+schedule a Redis container to run on a worker node.
+
+
+## Service types
+
+For **replicated services**, Swarm deploys a specific number of replica tasks
+based upon the scale you set in the desired state.
+
+For **global services**, Swarm runs one task for the service on every available
+node in the cluster.
+
+## Load balancing
+
+Swarm uses **ingress load balancing** to expose the services you want to make
+available externally to the Swarm. Swarm can automatically assign the service a
+**PublishedPort** or you can configure a PublishedPort for the service in the
+30000-32767 range. External components, such as cloud load balancers, can access
+the service on the PublishedPort of any node in the cluster, even if the node is
+not currently running the service.
+
+Swarm has an internal DNS component that automatically assigns each service in
+the Swarm DNS entry. Swarm uses **internal load balancing** distribute requests
+among services within the cluster based upon the services' DNS name.
+
+<p style="margin-bottom:300px">&nbsp;</p>

+ 21 - 0
docs/swarm/menu.md

@@ -0,0 +1,21 @@
+<!--[metadata]>
++++
+title = "Manage a Swarm (1.12 RC)"
+description = "How to use Docker Swarm to create and manage Docker Engine clusters"
+keywords = [" docker, documentation, developer, "]
+[menu.main]
+identifier = "engine_swarm"
+parent = "engine_use"
+weight = 0
+advisory = "rc"
++++
+<![end-metadata]-->
+
+
+## Use Docker Swarm to create and manage clusters of Docker Engine called Swarms
+
+This section contains the following topics:
+
+* [Docker Swarm overview](index.md)
+* [Docker Swarm key concepts](key-concepts.md)
+* [Getting Started with Docker Swarm](swarm-tutorial/index.md)

+ 64 - 0
docs/swarm/swarm-tutorial/add-nodes.md

@@ -0,0 +1,64 @@
+<!--[metadata]>
++++
+title = "Add nodes to the Swarm"
+description = "Add nodes to the Swarm"
+keywords = ["tutorial, cluster management, swarm"]
+[menu.main]
+identifier="add-nodes"
+parent="swarm-tutorial"
+weight=13
+advisory = "rc"
++++
+<![end-metadata]-->
+
+# Add nodes to the Swarm
+
+Once you've [created a Swarm](create-swarm.md) with a manager node, you're ready
+to add worker nodes.
+
+1. Open a terminal and ssh into the machine where you want to run a worker node.
+This tutorial uses the name `worker1`.
+
+2. Run `docker swarm join MANAGER-IP:PORT` to create a worker node joined to the
+existing Swarm. Replace MANAGER-IP address of the manager node and the port
+where the manager listens.
+
+    In the tutorial, the following command joins `worker1` to the Swarm on `manager1`:
+
+    ```
+    $ docker swarm join 192.168.99.100:2377
+
+    This node joined a Swarm as a worker.
+    ```
+
+3. Open a terminal and ssh into the machine where you want to run a second
+worker node. This tutorial uses the name `worker2`.
+
+4. Run `docker swarm join MANAGER-IP:PORT` to create a worker node joined to
+the existing Swarm. Replace MANAGER-IP address of the manager node and the port
+where the manager listens.
+
+5. Open a terminal and ssh into the machine where the manager node runs and run
+the `docker node ls` command to see the worker nodes:
+
+    ```bash
+    $ docker node ls
+
+    ID              NAME      MEMBERSHIP  STATUS  AVAILABILITY  MANAGER STATUS  LEADER
+09fm6su6c24q *  manager1  Accepted    Ready   Active        Reachable       Yes
+32ljq6xijzb9    worker1   Accepted    Ready   Active
+38fsncz6fal9    worker2   Accepted    Ready   Active
+    ```
+
+    The `MANAGER` column identifies the manager nodes in the Swarm. The empty
+    status in this column for `worker1` and `worker2` identifies them as worker nodes.
+
+    Swarm management commands like `docker node ls` only work on manager nodes.
+
+
+## What's next?
+
+Now your Swarm consists of a manager and two worker nodes. In the next step of
+the tutorial, you [deploy a service](deploy-service.md) to the Swarm.
+
+<p style="margin-bottom:300px">&nbsp;</p>

+ 77 - 0
docs/swarm/swarm-tutorial/create-swarm.md

@@ -0,0 +1,77 @@
+<!--[metadata]>
++++
+title = "Create a Swarm"
+description = "Initialize the Swarm"
+keywords = ["tutorial, cluster management, swarm"]
+[menu.main]
+identifier="initialize-swarm"
+parent="swarm-tutorial"
+weight=12
+advisory = "rc"
++++
+<![end-metadata]-->
+
+# Create a Swarm
+
+After you complete the [tutorial setup](index.md) steps, you're ready
+to create a Swarm. Make sure the Docker Engine daemon is started on the host
+machines.
+
+1. Open a terminal and ssh into the machine where you want to run your manager
+node. For example, the tutorial uses a machine named `manager1`.
+
+2. Run `docker swarm init --listen-addr MANAGER-IP:PORT` to create a new Swarm.
+
+    In the tutorial, the following command creates a Swarm on the `manager1` machine:
+
+    ```
+    $ docker swarm init --listen-addr 192.168.99.100:2377
+
+    Swarm initialized: current node (09fm6su6c24qn) is now a manager.
+    ```
+
+    The `--listen-addr` flag configures the manager node to listen on port
+    `2377`. The other nodes in the Swarm must be able to access the manager at
+    the IP address.
+
+3. Run `docker info` to view the current state of the Swarm:
+
+     ```
+     $ docker info
+
+     Containers: 2
+      Running: 0
+      Paused: 0
+      Stopped: 2
+     ...snip...
+     Swarm:
+      NodeID: 09fm6su6c24qn
+      IsManager: YES
+      Managers: 1
+      Nodes: 1
+     ...snip...
+     ```
+
+4. Run the `docker node ls` command to view information about nodes:
+
+    ```
+    $ docker node ls
+
+    ID              NAME      MEMBERSHIP  STATUS  AVAILABILITY  MANAGER STATUS  LEADER
+09fm6su6c24q *  manager1  Accepted    Ready   Active        Reachable       Yes
+
+    ```
+
+     The `*` next to the node id, indicates that you're currently connected on
+     this node.
+
+     Docker Swarm automatically names the node for the machine host name. The
+     tutorial covers other columns in later steps.
+
+## What's next?
+
+In the next section of the tutorial, we'll [add two more nodes](add-nodes.md) to
+the cluster.
+
+
+<p style="margin-bottom:300px">&nbsp;</p>

+ 44 - 0
docs/swarm/swarm-tutorial/delete-service.md

@@ -0,0 +1,44 @@
+<!--[metadata]>
++++
+title = "Delete the service"
+description = "Remove the service on the Swarm"
+keywords = ["tutorial, cluster management, swarm, service"]
+[menu.main]
+identifier="swarm-tutorial-delete-service"
+parent="swarm-tutorial"
+weight=19
+advisory = "rc"
++++
+<![end-metadata]-->
+
+# Delete the service running on the Swarm
+
+The remaining steps in the tutorial don't use the `helloworld` service, so now
+you can delete the service from the Swarm.
+
+1. If you haven't already, open a terminal and ssh into the machine where you
+run your manager node. For example, the tutorial uses a machine named
+`manager1`.
+
+2. Run `docker service remove helloworld` to remove the `helloworld` service.
+
+    ```
+    $ docker service rm helloworld
+    helloworld
+    ```
+
+3. Run `docker service inspect SERVICE-ID` to veriy that Swarm removed the
+service. The CLI returns a message that the service is not found:
+
+    ```
+    $ docker service inspect helloworld
+    []
+    Error: no such service or task: helloworld
+    ```
+
+## What's next?
+
+In the next step of the tutorial, you set up a new service and and apply a
+[rolling update](rolling-update.md).
+
+<p style="margin-bottom:300px">&nbsp;</p>

+ 50 - 0
docs/swarm/swarm-tutorial/deploy-service.md

@@ -0,0 +1,50 @@
+<!--[metadata]>
++++
+title = "Deploy a service"
+description = "Deploy the application"
+keywords = ["tutorial, cluster management, swarm"]
+[menu.main]
+identifier="deploy-application"
+parent="swarm-tutorial"
+weight=16
+advisory = "rc"
++++
+<![end-metadata]-->
+
+# Deploy a service to the Swarm
+
+After you [create a Swarm](create-swarm.md), you can deploy a service to the
+Swarm. For this tutorial, you also [added worker nodes](add-nodes.md), but that
+is not a requirement to deploy a service.
+
+1. Open a terminal and ssh into the machine where you run your manager node. For
+example, the tutorial uses a machine named `manager1`.
+
+2. Run the the following command:
+
+    ```bash
+    $ docker service create --scale 1 --name helloworld alpine ping docker.com
+
+    2zs4helqu64f3k3iuwywbk49w
+    ```
+
+    * The `docker service create` command creates the service.
+    * The `--name` flag names the service `helloworld`.
+    * The `--scale` flag specifies the desired state of 1 running instance.
+    * The arguments `alpine ping docker.com` define the service as an Alpine
+    Linux container that executes the command `ping docker.com`.
+
+3. Run `docker service ls` to see the list of running services:
+
+    ```
+    $ docker service ls
+
+    ID            NAME        SCALE  IMAGE   COMMAND
+    2zs4helqu64f  helloworld  1      alpine  ping docker.com
+    ```
+
+## What's next?
+
+Now you've deployed a service to the Swarm, you're ready to [inspect the service](inspect-service.md).
+
+<p style="margin-bottom:300px">&nbsp;</p>

+ 129 - 0
docs/swarm/swarm-tutorial/drain-node.md

@@ -0,0 +1,129 @@
+<!--[metadata]>
++++
+title = "Drain a node"
+description = "Drain nodes on the Swarm"
+keywords = ["tutorial, cluster management, swarm, service, drain"]
+[menu.main]
+identifier="swarm-tutorial-drain-node"
+parent="swarm-tutorial"
+weight=21
++++
+<![end-metadata]-->
+
+# Drain a node on the Swarm
+
+In earlier steps of the tutorial, all the nodes have been running with `ACTIVE`
+availability. The Swarm manager can assign tasks to any `ACTIVE` node, so all
+nodes have been available to receive tasks.
+
+Sometimes, such as planned maintenance times, you need to set a node to `DRAIN`
+availabilty. `DRAIN` availabilty  prevents a node from receiving new tasks
+from the Swarm manager. It also means the manager stops tasks running on the
+node and launches replica tasks on a node with `ACTIVE` availability.
+
+1. If you haven't already, open a terminal and ssh into the machine where you
+run your manager node. For example, the tutorial uses a machine named
+`manager1`.
+
+2. Verify that all your nodes are actively available.
+
+    ```
+    $ docker node ls
+
+    ID               NAME      MEMBERSHIP  STATUS  AVAILABILITY  MANAGER STATUS  LEADER
+  1x2bldyhie1cj    worker1   Accepted    Ready   Active
+  1y3zuia1z224i    worker2   Accepted    Ready   Active
+  2p5bfd34mx4op *  manager1  Accepted    Ready   Active        Reachable       Yes
+    ```
+
+2. If you aren't still running the `redis` service from the [rolling
+update](rolling-update.md) tutorial, start it now:
+
+    ```bash
+    $ docker service create --scale 3 --name redis --update-delay 10s --update-parallelism 1 redis:3.0.6
+
+    69uh57k8o03jtqj9uvmteodbb
+    ```
+
+3. Run `docker service tasks redis` to see how the Swarm manager assigned the
+tasks to different nodes:
+
+    ```
+    $ docker service tasks redis
+    ID                         NAME     SERVICE  IMAGE        LAST STATE          DESIRED STATE  NODE
+    3wfqsgxecktpwoyj2zjcrcn4r  redis.1  redis    redis:3.0.6  RUNNING 13 minutes  RUNNING        worker2
+    8lcm041z3v80w0gdkczbot0gg  redis.2  redis    redis:3.0.6  RUNNING 13 minutes  RUNNING        worker1
+    d48skceeph9lkz4nbttig1z4a  redis.3  redis    redis:3.0.6  RUNNING 12 minutes  RUNNING        manager1
+    ```
+
+    In this case the Swarm manager distributed one task to each node. You may
+    see the tasks distributed differently among the nodes in your environment.
+
+4. Run `docker node update --availability drain NODE-ID` to drain a node that
+had a task assigned to it:
+
+    ```bash
+    docker node update --availability drain worker1
+    worker1
+    ```
+
+5. Inspect the node to check its availability:
+
+    ```
+    $ docker node inspect --pretty worker1
+    ID:			1x2bldyhie1cj
+    Hostname:		worker1
+    Status:
+     State:			READY
+     Availability:		DRAIN
+    ...snip...
+    ```
+
+    The drained node shows `Drain` for `AVAILABILITY`.
+
+6. Run `docker service tasks redis` to see how the Swarm manager updated the
+task assignments for the `redis` service:
+
+    ```
+    ID                         NAME     SERVICE  IMAGE        LAST STATE          DESIRED STATE  NODE
+    3wfqsgxecktpwoyj2zjcrcn4r  redis.1  redis    redis:3.0.6  RUNNING 26 minutes  RUNNING        worker2
+    ah7o4u5upostw3up1ns9vbqtc  redis.2  redis    redis:3.0.6  RUNNING 9 minutes   RUNNING        manager1
+    d48skceeph9lkz4nbttig1z4a  redis.3  redis    redis:3.0.6  RUNNING 26 minutes  RUNNING        manager1
+    ```
+
+    The Swarm manager maintains the desired state by ending the task on a node
+    with `Drain` availability and creating a new task on a node with `Active`
+    availability.
+
+7. Run  `docker node update --availability active NODE-ID` to return the drained
+node to an active state:
+
+    ```bash
+    $ docker node update --availability active worker1
+    worker1
+    ```
+
+8. Inspect the node to see the updated state:
+
+   ```
+   $ docker node inspect --pretty worker1
+   ID:			1x2bldyhie1cj
+   Hostname:		worker1
+   Status:
+    State:			READY
+    Availability:		ACTIVE
+  ...snip...
+  ```
+
+  When you set the node back to `Active` availability, it can receive new tasks:
+
+  * during a service update to scale up
+  * during a rolling update
+  * when you set another node to `Drain` availability
+  * when a task fails on another active node
+
+## What's next?
+
+The next topic in the tutorial introduces volumes.
+
+<p style="margin-bottom:300px">&nbsp;</p>

+ 87 - 0
docs/swarm/swarm-tutorial/index.md

@@ -0,0 +1,87 @@
+<!--[metadata]>
++++
+title = "Set up for the tutorial"
+description = "Getting Started tutorial for Docker Swarm"
+keywords = ["tutorial, cluster management, swarm"]
+[menu.main]
+identifier="tutorial-setup"
+parent="swarm-tutorial"
+weight=11
+advisory = "rc"
++++
+<![end-metadata]-->
+
+# Getting Started with Docker Swarm
+
+This tutorial introduces you to the key features of Docker Swarm. It guides you
+through the following activities:
+
+* initializing a cluster of Docker Engines called a Swarm
+* adding nodes to the Swarm
+* deploying application services to the Swarm
+* managing the Swarm once you have everything running
+
+This tutorial uses Docker Engine CLI commands entered on the command line of a
+terminal window. You should be able to install Docker on networked machines and
+be comfortable running commands in the shell of your choice.
+
+If you’re brand new to Docker, see [About Docker Engine](../../index.md).
+
+## Set up
+To run this tutorial, you need the following:
+
+* [three networked host machines](#three-networked-host-machines)
+* [Docker Engine 1.12 or later installed](#docker-engine-1-12-or-later)
+* [the IP address of the manager machine](#the-ip-address-of-the-manager-machine)
+* [open ports between the hosts](#open-ports-between-the-hosts)
+
+### Three networked host machines
+
+The tutorial uses three networked host machines as nodes in the Swarm. These can
+be virtual machines on your PC, in a data center, or on a cloud service
+provider. This tutorial uses the following machine names:
+
+* manager1
+* worker1
+* worker2
+
+###  Docker Engine 1.12 or later
+
+You must install Docker Engine on each one of the host machines. To use this
+version of Swarm, install the Docker Engine `v1.12.0-rc1` or later from the
+[Docker releases GitHub repository](https://github.com/docker/docker/releases).
+Alternatively, install the latest Docker for Mac or Docker for Windows Beta.
+
+Verify that the Docker Engine daemon is running on each of the machines.
+
+<!-- See the following options to install:
+
+* [Install Docker Engine](../../installation/index.md).
+
+* [Example: Manual install on cloud provider](../../installation/cloud/cloud-ex-aws.md).
+-->
+
+### The IP address of the manager machine
+
+The IP address must be assigned to an a network interface available to the host
+operating system. All nodes in the Swarm must be able to access the manager at the IP address.
+
+>**Tip**: You can run `ifconfig` on Linux or Mac OSX to see a list of the
+available network interfaces.
+
+The tutorial uses `manager1` : `192.168.99.100`.
+
+### Open ports between the hosts
+
+* **TCP port 2377** for cluster management communications
+* **TCP** and **UDP port 7946** for communication among nodes
+* **TCP** and **UDP port 4789** for overlay network traffic
+
+>**Tip**: Docker recommends that every node in the cluster be on the same layer
+3 (IP) subnet with all traffic permitted between nodes.
+
+## What's next?
+
+After you have set up your environment, you're ready to [create a Swarm](create-swarm.md).
+
+<p style="margin-bottom:300px">&nbsp;</p>

Деякі файли не було показано, через те що забагато файлів було змінено