summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Nephin <dnephin@docker.com>2016-06-07 14:56:18 -0700
committerTonis Tiigi <tonistiigi@gmail.com>2016-06-13 18:24:17 -0700
commitbdcd80212d268be8120fb137f2ca771e323482db (patch)
treeaee047ff4cf568127211d6f171fea157e48be16f
parenta7d0b2c466348ae0fc08f8442155756c6765c8f6 (diff)
downloaddocker-bdcd80212d268be8120fb137f2ca771e323482db.tar.gz
Add Swarm management CLI commands
As described in our ROADMAP.md, introduce new Swarm management commands to call to the corresponding API endpoints. This PR is fully backward compatible (joining a Swarm is an optional feature of the Engine, and existing commands are not impacted). Signed-off-by: Daniel Nephin <dnephin@docker.com> Signed-off-by: Victor Vieux <vieux@docker.com> Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
-rw-r--r--api/client/idresolver/idresolver.go70
-rw-r--r--api/client/info.go13
-rw-r--r--api/client/inspect.go31
-rw-r--r--api/client/node/accept.go40
-rw-r--r--api/client/node/cmd.go50
-rw-r--r--api/client/node/demote.go40
-rw-r--r--api/client/node/inspect.go141
-rw-r--r--api/client/node/list.go123
-rw-r--r--api/client/node/opts.go52
-rw-r--r--api/client/node/promote.go40
-rw-r--r--api/client/node/reject.go40
-rw-r--r--api/client/node/remove.go35
-rw-r--r--api/client/node/tasks.go72
-rw-r--r--api/client/node/update.go100
-rw-r--r--api/client/service/cmd.go31
-rw-r--r--api/client/service/create.go41
-rw-r--r--api/client/service/inspect.go126
-rw-r--r--api/client/service/list.go97
-rw-r--r--api/client/service/opts.go372
-rw-r--r--api/client/service/remove.go47
-rw-r--r--api/client/service/tasks.go65
-rw-r--r--api/client/service/update.go248
-rw-r--r--api/client/swarm/cmd.go30
-rw-r--r--api/client/swarm/init.go61
-rw-r--r--api/client/swarm/inspect.go56
-rw-r--r--api/client/swarm/join.go62
-rw-r--r--api/client/swarm/leave.go44
-rw-r--r--api/client/swarm/opts.go122
-rw-r--r--api/client/swarm/update.go93
-rw-r--r--api/client/tag.go20
-rw-r--r--api/client/task/print.go78
-rw-r--r--cli/cobraadaptor/adaptor.go6
-rw-r--r--cli/usage.go2
33 files changed, 2441 insertions, 7 deletions
diff --git a/api/client/idresolver/idresolver.go b/api/client/idresolver/idresolver.go
new file mode 100644
index 0000000000..05c4c9c366
--- /dev/null
+++ b/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
+}
diff --git a/api/client/info.go b/api/client/info.go
index 283b77b3df..28a71dbe24 100644
--- a/api/client/info.go
+++ b/api/client/info.go
@@ -68,6 +68,19 @@ func (cli *DockerCli) CmdInfo(args ...string) error {
fmt.Fprintf(cli.out, "\n")
}
+ if info.Swarm.IsAgent {
+ fmt.Fprintf(cli.out, "Swarm: \n")
+ fmt.Fprintf(cli.out, " NodeID: %s\n", info.Swarm.NodeID)
+ if info.Swarm.IsManager {
+ fmt.Fprintf(cli.out, " IsManager: YES\n")
+ } else {
+ fmt.Fprintf(cli.out, " IsManager: NO\n")
+ }
+ fmt.Fprintf(cli.out, " Managers: %d\n", info.Swarm.Managers)
+ fmt.Fprintf(cli.out, " Nodes: %d\n", info.Swarm.Nodes)
+ } else {
+ fmt.Fprintf(cli.out, "Swarm: not part of a swarm\n")
+ }
ioutils.FprintfIfNotEmpty(cli.out, "Kernel Version: %s\n", info.KernelVersion)
ioutils.FprintfIfNotEmpty(cli.out, "Operating System: %s\n", info.OperatingSystem)
ioutils.FprintfIfNotEmpty(cli.out, "OSType: %s\n", info.OSType)
diff --git a/api/client/inspect.go b/api/client/inspect.go
index cb16b1bb52..3b107a7e43 100644
--- a/api/client/inspect.go
+++ b/api/client/inspect.go
@@ -11,19 +11,19 @@ import (
"github.com/docker/engine-api/client"
)
-// CmdInspect displays low-level information on one or more containers or images.
+// CmdInspect displays low-level information on one or more containers, images or tasks.
//
-// Usage: docker inspect [OPTIONS] CONTAINER|IMAGE [CONTAINER|IMAGE...]
+// Usage: docker inspect [OPTIONS] CONTAINER|IMAGE|TASK [CONTAINER|IMAGE|TASK...]
func (cli *DockerCli) CmdInspect(args ...string) error {
- cmd := Cli.Subcmd("inspect", []string{"CONTAINER|IMAGE [CONTAINER|IMAGE...]"}, Cli.DockerCommands["inspect"].Description, true)
+ cmd := Cli.Subcmd("inspect", []string{"CONTAINER|IMAGE|TASK [CONTAINER|IMAGE|TASK...]"}, Cli.DockerCommands["inspect"].Description, true)
tmplStr := cmd.String([]string{"f", "-format"}, "", "Format the output using the given go template")
- inspectType := cmd.String([]string{"-type"}, "", "Return JSON for specified type, (e.g image or container)")
+ inspectType := cmd.String([]string{"-type"}, "", "Return JSON for specified type, (e.g image, container or task)")
size := cmd.Bool([]string{"s", "-size"}, false, "Display total file sizes if the type is container")
cmd.Require(flag.Min, 1)
cmd.ParseFlags(args, true)
- if *inspectType != "" && *inspectType != "container" && *inspectType != "image" {
+ if *inspectType != "" && *inspectType != "container" && *inspectType != "image" && *inspectType != "task" {
return fmt.Errorf("%q is not a valid value for --type", *inspectType)
}
@@ -35,6 +35,11 @@ func (cli *DockerCli) CmdInspect(args ...string) error {
elementSearcher = cli.inspectContainers(ctx, *size)
case "image":
elementSearcher = cli.inspectImages(ctx, *size)
+ case "task":
+ if *size {
+ fmt.Fprintln(cli.err, "WARNING: --size ignored for tasks")
+ }
+ elementSearcher = cli.inspectTasks(ctx)
default:
elementSearcher = cli.inspectAll(ctx, *size)
}
@@ -54,6 +59,12 @@ func (cli *DockerCli) inspectImages(ctx context.Context, getSize bool) inspect.G
}
}
+func (cli *DockerCli) inspectTasks(ctx context.Context) inspect.GetRefFunc {
+ return func(ref string) (interface{}, []byte, error) {
+ return cli.client.TaskInspectWithRaw(ctx, ref)
+ }
+}
+
func (cli *DockerCli) inspectAll(ctx context.Context, getSize bool) inspect.GetRefFunc {
return func(ref string) (interface{}, []byte, error) {
c, rawContainer, err := cli.client.ContainerInspectWithRaw(ctx, ref, getSize)
@@ -63,7 +74,15 @@ func (cli *DockerCli) inspectAll(ctx context.Context, getSize bool) inspect.GetR
i, rawImage, err := cli.client.ImageInspectWithRaw(ctx, ref, getSize)
if err != nil {
if client.IsErrImageNotFound(err) {
- return nil, nil, fmt.Errorf("Error: No such image or container: %s", ref)
+ // Search for task with that id if an image doesn't exists.
+ t, rawTask, err := cli.client.TaskInspectWithRaw(ctx, ref)
+ if err != nil {
+ return nil, nil, fmt.Errorf("Error: No such image, container or task: %s", ref)
+ }
+ if getSize {
+ fmt.Fprintln(cli.err, "WARNING: --size ignored for tasks")
+ }
+ return t, rawTask, nil
}
return nil, nil, err
}
diff --git a/api/client/node/accept.go b/api/client/node/accept.go
new file mode 100644
index 0000000000..ae672ffe90
--- /dev/null
+++ b/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
+}
diff --git a/api/client/node/cmd.go b/api/client/node/cmd.go
new file mode 100644
index 0000000000..b403f5d2c4
--- /dev/null
+++ b/api/client/node/cmd.go
@@ -0,0 +1,50 @@
+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),
+ newRejectCommand(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
+}
diff --git a/api/client/node/demote.go b/api/client/node/demote.go
new file mode 100644
index 0000000000..22db76a0e6
--- /dev/null
+++ b/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 as 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
+}
diff --git a/api/client/node/inspect.go b/api/client/node/inspect.go
new file mode 100644
index 0000000000..dffbf8efba
--- /dev/null
+++ b/api/client/node/inspect.go
@@ -0,0 +1,141 @@
+package node
+
+import (
+ "fmt"
+ "io"
+ "sort"
+
+ "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.Bool("help", false, "Print usage")
+ 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()
+ getRef := func(ref string) (interface{}, []byte, error) {
+ ctx := context.Background()
+ 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", node.Status.State)
+ ioutils.FprintfIfNotEmpty(out, " Message:\t\t%s\n", node.Status.Message)
+ fmt.Fprintf(out, " Availability:\t\t%s\n", node.Spec.Availability)
+
+ if node.Manager != nil {
+ fmt.Fprintln(out, "Manager:")
+ fmt.Fprintf(out, " Address:\t\t%s\n", node.Manager.Raft.Addr)
+ fmt.Fprintf(out, " Raft status:\t\t%s\n", node.Manager.Raft.Status.Reachability)
+ leader := "no"
+ if node.Manager.Raft.Status.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%v\n", pluginType, 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)
+ }
+ }
+
+}
diff --git a/api/client/node/list.go b/api/client/node/list.go
new file mode 100644
index 0000000000..3885c1af2c
--- /dev/null
+++ b/api/client/node/list.go
@@ -0,0 +1,123 @@
+package node
+
+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\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()
+
+ nodes, err := client.NodeList(
+ context.Background(),
+ types.NodeListOptions{Filter: opts.filter.Value()})
+ if err != nil {
+ return err
+ }
+
+ info, err := client.Info(context.Background())
+ 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)
+ prettyPrint := func(s string) string {
+ return strings.Title(strings.ToLower(s))
+ }
+
+ // 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.Manager != nil && node.Manager.Raft.Status.Leader {
+ leader = "Yes"
+ }
+
+ reachability := ""
+ if node.Manager != nil {
+ reachability = string(node.Manager.Raft.Status.Reachability)
+ }
+
+ ID := stringid.TruncateID(node.ID)
+ if node.Description.Hostname == info.Name {
+ ID = ID + " *"
+ }
+
+ fmt.Fprintf(
+ writer,
+ listItemFmt,
+ ID,
+ name,
+ prettyPrint(membership),
+ prettyPrint(string(node.Status.State)),
+ prettyPrint(availability),
+ prettyPrint(reachability),
+ leader)
+ }
+}
+
+func printQuiet(out io.Writer, nodes []swarm.Node) {
+ for _, node := range nodes {
+ fmt.Fprintln(out, node.ID)
+ }
+}
diff --git a/api/client/node/opts.go b/api/client/node/opts.go
new file mode 100644
index 0000000000..0b746d4c3e
--- /dev/null
+++ b/api/client/node/opts.go
@@ -0,0 +1,52 @@
+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 strings.ToUpper(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 strings.ToUpper(opts.membership) {
+ case swarm.NodeMembershipAccepted:
+ spec.Membership = swarm.NodeMembershipAccepted
+ case swarm.NodeMembershipRejected:
+ spec.Membership = swarm.NodeMembershipRejected
+ case "":
+ default:
+ return swarm.NodeSpec{}, fmt.Errorf("invalid membership %q, only accepted and rejected are supported", opts.membership)
+ }
+
+ switch strings.ToUpper(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
+}
diff --git a/api/client/node/promote.go b/api/client/node/promote.go
new file mode 100644
index 0000000000..1a0fa4d685
--- /dev/null
+++ b/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 as 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
+}
diff --git a/api/client/node/reject.go b/api/client/node/reject.go
new file mode 100644
index 0000000000..0557b273a6
--- /dev/null
+++ b/api/client/node/reject.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 newRejectCommand(dockerCli *client.DockerCli) *cobra.Command {
+ var flags *pflag.FlagSet
+
+ cmd := &cobra.Command{
+ Use: "reject NODE [NODE...]",
+ Short: "Reject a node from the swarm",
+ Args: cli.RequiresMinArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return runReject(dockerCli, flags, args)
+ },
+ }
+
+ flags = cmd.Flags()
+ return cmd
+}
+
+func runReject(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.NodeMembershipRejected
+ }); err != nil {
+ return err
+ }
+ fmt.Println(id, "attempting to reject a node from the swarm.")
+ }
+
+ return nil
+}
diff --git a/api/client/node/remove.go b/api/client/node/remove.go
new file mode 100644
index 0000000000..6c73ba766c
--- /dev/null
+++ b/api/client/node/remove.go
@@ -0,0 +1,35 @@
+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()
+ for _, nodeID := range args {
+ err := client.NodeRemove(context.Background(), nodeID)
+ if err != nil {
+ return err
+ }
+ fmt.Fprintf(dockerCli.Out(), "%s\n", nodeID)
+ }
+ return nil
+}
diff --git a/api/client/node/tasks.go b/api/client/node/tasks.go
new file mode 100644
index 0000000000..0b8833f8aa
--- /dev/null
+++ b/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", swarm.TaskStateRunning)
+ filter.Add("desired_state", 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))
+}
diff --git a/api/client/node/update.go b/api/client/node/update.go
new file mode 100644
index 0000000000..01f1770af9
--- /dev/null
+++ b/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)
+ 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)
+ }
+}
diff --git a/api/client/service/cmd.go b/api/client/service/cmd.go
new file mode 100644
index 0000000000..f72b067aec
--- /dev/null
+++ b/api/client/service/cmd.go
@@ -0,0 +1,31 @@
+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),
+ newUpdateCommand(dockerCli),
+ )
+ return cmd
+}
diff --git a/api/client/service/create.go b/api/client/service/create.go
new file mode 100644
index 0000000000..86e5bb7cb6
--- /dev/null
+++ b/api/client/service/create.go
@@ -0,0 +1,41 @@
+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.command = 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()
+ response, err := client.ServiceCreate(context.Background(), opts.ToService())
+ if err != nil {
+ return err
+ }
+
+ fmt.Fprintf(dockerCli.Out(), "%s\n", response.ID)
+ return nil
+}
diff --git a/api/client/service/inspect.go b/api/client/service/inspect.go
new file mode 100644
index 0000000000..00809b09b8
--- /dev/null
+++ b/api/client/service/inspect.go
@@ -0,0 +1,126 @@
+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|TASK [SERVICE|TASK...]",
+ Short: "Inspect a service or service tasks",
+ 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.Bool("help", false, "Print usage")
+ 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")
+ fmt.Fprintf(out, " Scale:\t\t%d\n", service.Spec.Mode.Replicated.Instances)
+ }
+ 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.TaskSpec.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)
+}
diff --git a/api/client/service/list.go b/api/client/service/list.go
new file mode 100644
index 0000000000..9d140dd0dc
--- /dev/null
+++ b/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 {
+ scale = fmt.Sprintf("%d", *service.Spec.Mode.Replicated.Instances)
+ } else if service.Spec.Mode.Global != nil {
+ scale = "global"
+ }
+ fmt.Fprintf(
+ writer,
+ listItemFmt,
+ stringid.TruncateID(service.ID),
+ service.Spec.Name,
+ scale,
+ service.Spec.TaskSpec.ContainerSpec.Image,
+ strings.Join(service.Spec.TaskSpec.ContainerSpec.Command, " "))
+ }
+}
+
+func printQuiet(out io.Writer, services []swarm.Service) {
+ for _, service := range services {
+ fmt.Fprintln(out, service.ID)
+ }
+}
diff --git a/api/client/service/opts.go b/api/client/service/opts.go
new file mode 100644
index 0000000000..47f3ab8c1f
--- /dev/null
+++ b/api/client/service/opts.go
@@ -0,0 +1,372 @@
+package service
+
+import (
+ "fmt"
+ "math/big"
+ "strconv"
+ "time"
+
+ "github.com/docker/docker/opts"
+ runconfigopts "github.com/docker/docker/runconfig/opts"
+ "github.com/docker/docker/volume"
+ "github.com/docker/engine-api/types/swarm"
+ "github.com/docker/go-connections/nat"
+ units "github.com/docker/go-units"
+ "github.com/spf13/cobra"
+)
+
+var (
+ // DefaultScale is the default scale to use for a replicated service
+ DefaultScale 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)
+ }
+ *c = nanoCPUs(cpu.Mul(cpu, big.NewRat(1e9, 1)).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 time.Duration
+func (i *Uint64Opt) Value() *uint64 {
+ return i.value
+}
+
+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
+ ingress 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),
+ Ingress: swarm.IngressRouting(e.ingress),
+ ExposedPorts: 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(port.Proto()),
+ Port: uint32(port.Int()),
+ SwarmPort: 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
+}
+
+// ValidateMount validates that a mount flag has the correct format
+func ValidateMount(value string) (string, error) {
+ // TODO: this is wrong when the client and daemon OS don't match
+ _, err := volume.ParseMountSpec(value, "")
+ return value, err
+}
+
+// ConvertMounts converts mount strings into a swarm.Mount object
+func ConvertMounts(rawMounts []string) []swarm.Mount {
+ mounts := []swarm.Mount{}
+
+ for _, rawMount := range rawMounts {
+ // TODO: this is wrong when the client and daemon OS don't match
+ mountPoint, _ := volume.ParseMountSpec(rawMount, "")
+
+ mounts = append(mounts, swarm.Mount{
+ Target: mountPoint.Destination,
+ Source: mountPoint.Source,
+ // TODO: fix with new mounts
+ // Mask: swarm.MountMask(mountPoint.Mode),
+ // no more VolumeName
+ Type: swarm.MountType(mountPoint.Type()),
+ })
+ }
+ return mounts
+}
+
+type serviceOptions struct {
+ name string
+ labels opts.ListOpts
+ image string
+ command []string
+ args []string
+ env opts.ListOpts
+ workdir string
+ user string
+ mounts opts.ListOpts
+
+ resources resourceOptions
+ stopGrace DurationOpt
+
+ scale 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),
+ mounts: opts.NewListOpts(ValidateMount),
+ endpoint: endpointOptions{
+ ports: opts.NewListOpts(ValidatePort),
+ },
+ }
+}
+
+func (opts *serviceOptions) ToService() swarm.ServiceSpec {
+ service := swarm.ServiceSpec{
+ Annotations: swarm.Annotations{
+ Name: opts.name,
+ Labels: runconfigopts.ConvertKVStringsToMap(opts.labels.GetAll()),
+ },
+ TaskSpec: 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: ConvertMounts(opts.mounts.GetAll()),
+ 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(),
+ }
+
+ // TODO: add error if both global and instances are specified or if invalid value
+ if opts.mode == "global" {
+ service.Mode.Global = &swarm.GlobalService{}
+ } else {
+ service.Mode.Replicated = &swarm.ReplicatedService{
+ Instances: opts.scale.Value(),
+ }
+ }
+ return service
+}
+
+// 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, "volume", "v", "Attach a volume or create a bind mount")
+
+ 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.scale, "scale", "Number of tasks")
+
+ // TODO: help strings
+ flags.StringVar(&opts.restartPolicy.condition, "restart-policy-condition", "", "")
+ flags.Var(&opts.restartPolicy.delay, "restart-policy-delay", "")
+ flags.Var(&opts.restartPolicy.maxAttempts, "restart-policy-max-attempts", "")
+ flags.Var(&opts.restartPolicy.window, "restart-policy-window", "")
+
+ flags.StringSliceVar(&opts.constraints, "constraint", []string{}, "Placement constraints")
+
+ flags.Uint64Var(&opts.update.parallelism, "updateconfig-parallelism", 1, "UpdateConfig Parallelism")
+ flags.DurationVar(&opts.update.delay, "updateconfig-delay", time.Duration(0), "UpdateConfig Delay")
+
+ flags.StringSliceVar(&opts.networks, "network", []string{}, "Network attachments")
+ flags.StringVar(&opts.endpoint.mode, "endpoint-mode", "", "Endpoint mode")
+ flags.StringVar(&opts.endpoint.ingress, "endpoint-ingress", "", "Endpoint ingress")
+ flags.VarP(&opts.endpoint.ports, "port", "p", "Publish a port as a node port")
+}
diff --git a/api/client/service/remove.go b/api/client/service/remove.go
new file mode 100644
index 0000000000..acbdae0f2f
--- /dev/null
+++ b/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
+}
diff --git a/api/client/service/tasks.go b/api/client/service/tasks.go
new file mode 100644
index 0000000000..d533801eff
--- /dev/null
+++ b/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", swarm.TaskStateRunning)
+ filter.Add("desired_state", 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))
+}
diff --git a/api/client/service/update.go b/api/client/service/update.go
new file mode 100644
index 0000000000..1b0de707d7
--- /dev/null
+++ b/api/client/service/update.go
@@ -0,0 +1,248 @@
+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.TaskSpec.ContainerSpec
+ task := &spec.TaskSpec
+ 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)
+ }
+ if flags.Changed("endpoint-ingress") {
+ value, _ := flags.GetString("endpoint-ingress")
+ spec.EndpointSpec.Ingress = swarm.IngressRouting(value)
+ }
+
+ mergePorts(flags, &spec.EndpointSpec.ExposedPorts)
+
+ 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("volume") {
+ return
+ }
+
+ values := flags.Lookup("volume").Value.(*opts.ListOpts).GetAll()
+ *mounts = append(*mounts, ConvertMounts(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("scale") {
+ return fmt.Errorf("scale can only be used with replicated mode")
+ }
+
+ if mode == "global" {
+ serviceMode.Replicated = nil
+ serviceMode.Global = &swarm.GlobalService{}
+ return nil
+ }
+
+ if flags.Changed("scale") {
+ scale := flags.Lookup("scale").Value.(*Uint64Opt).Value()
+ serviceMode.Replicated = &swarm.ReplicatedService{Instances: scale}
+ serviceMode.Global = nil
+ return nil
+ }
+
+ if mode == "replicated" {
+ if serviceMode.Replicated != nil {
+ return nil
+ }
+ serviceMode.Replicated = &swarm.ReplicatedService{Instances: &DefaultScale}
+ serviceMode.Global = nil
+ }
+
+ return nil
+}
diff --git a/api/client/swarm/cmd.go b/api/client/swarm/cmd.go
new file mode 100644
index 0000000000..0c40d20d9c
--- /dev/null
+++ b/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
+}
diff --git a/api/client/swarm/init.go b/api/client/swarm/init.go
new file mode 100644
index 0000000000..a4377d9d89
--- /dev/null
+++ b/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", "Acceptance policy")
+ 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
+}
diff --git a/api/client/swarm/inspect.go b/api/client/swarm/inspect.go
new file mode 100644
index 0000000000..c2b862a936
--- /dev/null
+++ b/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.Bool("help", false, "Print usage")
+ 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()
+
+ getRef := func(_ string) (interface{}, []byte, error) {
+ swarm, err := client.SwarmInspect(ctx)
+ if err != nil {
+ return nil, nil, err
+ }
+ return swarm, nil, nil
+ }
+
+ // if !opts.pretty {
+ return inspect.Inspect(dockerCli.Out(), []string{""}, opts.format, getRef)
+ // }
+
+ //return printHumanFriendly(dockerCli.Out(), opts.refs, getRef)
+}
diff --git a/api/client/swarm/join.go b/api/client/swarm/join.go
new file mode 100644
index 0000000000..4f3860127d
--- /dev/null
+++ b/api/client/swarm/join.go
@@ -0,0 +1,62 @@
+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
+}
+
+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")
+ 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(),
+ RemoteAddr: opts.remote,
+ }
+ err := client.SwarmJoin(ctx, req)
+ if err != nil {
+ return err
+ }
+ if opts.manager {
+ fmt.Fprintln(dockerCli.Out(), "This node is attempting to join a Swarm as a manager.")
+ } else {
+ fmt.Fprintln(dockerCli.Out(), "This node is attempting to join a Swarm.")
+ }
+ return nil
+}
diff --git a/api/client/swarm/leave.go b/api/client/swarm/leave.go
new file mode 100644
index 0000000000..e3f8fbfa57
--- /dev/null
+++ b/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
+}
diff --git a/api/client/swarm/opts.go b/api/client/swarm/opts.go
new file mode 100644
index 0000000000..ff465c553f
--- /dev/null
+++ b/api/client/swarm/opts.go
@@ -0,0 +1,122 @@
+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 (
+ defaultPolicy = swarm.Policy{Role: WORKER, Autoaccept: true}
+)
+
+// 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{}
+
+ if len(o.values) == 0 {
+ return append(policies, defaultPolicy)
+ }
+
+ for role, enabled := range o.values {
+ policies = append(policies, swarm.Policy{
+ Role: role,
+ Autoaccept: enabled,
+ Secret: secret,
+ })
+ }
+ return policies
+}
+
+// NewAutoAcceptOption returns a new auto-accept option
+func NewAutoAcceptOption() AutoAcceptOption {
+ return AutoAcceptOption{values: make(map[string]bool)}
+}
diff --git a/api/client/swarm/update.go b/api/client/swarm/update.go
new file mode 100644
index 0000000000..354811bff8
--- /dev/null
+++ b/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", "Acceptance policy")
+ 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)
+ 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
+}
diff --git a/api/client/tag.go b/api/client/tag.go
new file mode 100644
index 0000000000..0b6a073ccb
--- /dev/null
+++ b/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))
+}
diff --git a/api/client/task/print.go b/api/client/task/print.go
new file mode 100644
index 0000000000..5605949fca
--- /dev/null
+++ b/api/client/task/print.go
@@ -0,0 +1,78 @@
+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 tasksByInstance []swarm.Task
+
+func (t tasksByInstance) Len() int {
+ return len(t)
+}
+
+func (t tasksByInstance) Swap(i, j int) {
+ t[i], t[j] = t[j], t[i]
+}
+
+func (t tasksByInstance) Less(i, j int) bool {
+ // Sort by instance.
+ if t[i].Instance != t[j].Instance {
+ return t[i].Instance < t[j].Instance
+ }
+
+ // If same instance, 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(tasksByInstance(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.Instance > 0 {
+ name = fmt.Sprintf("%s.%d", name, task.Instance)
+ }
+ fmt.Fprintf(
+ writer,
+ psTaskItemFmt,
+ task.ID,
+ name,
+ serviceValue,
+ task.Spec.ContainerSpec.Image,
+ task.Status.State, units.HumanDuration(time.Since(task.Status.Timestamp)),
+ task.DesiredState,
+ nodeValue,
+ )
+ }
+
+ return nil
+}
diff --git a/cli/cobraadaptor/adaptor.go b/cli/cobraadaptor/adaptor.go
index b1980491a8..c83962d162 100644
--- a/cli/cobraadaptor/adaptor.go
+++ b/cli/cobraadaptor/adaptor.go
@@ -5,7 +5,10 @@ import (
"github.com/docker/docker/api/client/container"
"github.com/docker/docker/api/client/image"
"github.com/docker/docker/api/client/network"
+ "github.com/docker/docker/api/client/node"
"github.com/docker/docker/api/client/registry"
+ "github.com/docker/docker/api/client/service"
+ "github.com/docker/docker/api/client/swarm"
"github.com/docker/docker/api/client/system"
"github.com/docker/docker/api/client/volume"
"github.com/docker/docker/cli"
@@ -36,6 +39,9 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor {
rootCmd.SetFlagErrorFunc(cli.FlagErrorFunc)
rootCmd.SetOutput(stdout)
rootCmd.AddCommand(
+ node.NewNodeCommand(dockerCli),
+ service.NewServiceCommand(dockerCli),
+ swarm.NewSwarmCommand(dockerCli),
container.NewAttachCommand(dockerCli),
container.NewCommitCommand(dockerCli),
container.NewCreateCommand(dockerCli),
diff --git a/cli/usage.go b/cli/usage.go
index 9cd7acd244..122abdbd4a 100644
--- a/cli/usage.go
+++ b/cli/usage.go
@@ -11,7 +11,7 @@ var DockerCommandUsage = []Command{
{"cp", "Copy files/folders between a container and the local filesystem"},
{"exec", "Run a command in a running container"},
{"info", "Display system-wide information"},
- {"inspect", "Return low-level information on a container or image"},
+ {"inspect", "Return low-level information on a container, image or task"},
{"ps", "List containers"},
{"update", "Update configuration of one or more containers"},
}