diff options
author | Daniel Nephin <dnephin@docker.com> | 2016-06-13 19:56:23 -0700 |
---|---|---|
committer | Tonis Tiigi <tonistiigi@gmail.com> | 2016-06-13 22:17:15 -0700 |
commit | 12a00e60177ca42bfb1dd9ebd5dce1c7039da6dd (patch) | |
tree | 1387a35907a762bfd5471fc162733624a26dde6f | |
parent | d4abe1d84a58461fea54d2046c24ae5d869fa41e (diff) | |
download | docker-12a00e60177ca42bfb1dd9ebd5dce1c7039da6dd.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>
36 files changed, 2612 insertions, 12 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..4566f6c1a6 100644 --- a/api/client/info.go +++ b/api/client/info.go @@ -10,6 +10,7 @@ import ( "github.com/docker/docker/pkg/ioutils" flag "github.com/docker/docker/pkg/mflag" "github.com/docker/docker/utils" + "github.com/docker/engine-api/types/swarm" "github.com/docker/go-units" ) @@ -68,6 +69,21 @@ func (cli *DockerCli) CmdInfo(args ...string) error { fmt.Fprintf(cli.out, "\n") } + fmt.Fprintf(cli.out, "Swarm: %v\n", info.Swarm.LocalNodeState) + if info.Swarm.LocalNodeState != swarm.LocalNodeStateInactive { + fmt.Fprintf(cli.out, " NodeID: %s\n", info.Swarm.NodeID) + if info.Swarm.Error != "" { + fmt.Fprintf(cli.out, " Error: %v\n", info.Swarm.Error) + } + if info.Swarm.ControlAvailable { + fmt.Fprintf(cli.out, " IsManager: Yes\n") + fmt.Fprintf(cli.out, " Managers: %d\n", info.Swarm.Managers) + fmt.Fprintf(cli.out, " Nodes: %d\n", info.Swarm.Nodes) + ioutils.FprintfIfNotEmpty(cli.out, " CACertHash: %s\n", info.Swarm.CACertHash) + } else { + fmt.Fprintf(cli.out, " IsManager: No\n") + } + } ioutils.FprintfIfNotEmpty(cli.out, "Kernel Version: %s\n", info.KernelVersion) ioutils.FprintfIfNotEmpty(cli.out, "Operating System: %s\n", info.OperatingSystem) ioutils.FprintfIfNotEmpty(cli.out, "OSType: %s\n", info.OSType) 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/network/list.go b/api/client/network/list.go index 3e113b7275..292b4f62b0 100644 --- a/api/client/network/list.go +++ b/api/client/network/list.go @@ -71,7 +71,7 @@ func runList(dockerCli *client.DockerCli, opts listOptions) error { w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0) if !opts.quiet { - fmt.Fprintf(w, "NETWORK ID\tNAME\tDRIVER") + fmt.Fprintf(w, "NETWORK ID\tNAME\tDRIVER\tSCOPE") fmt.Fprintf(w, "\n") } @@ -79,6 +79,8 @@ func runList(dockerCli *client.DockerCli, opts listOptions) error { for _, networkResource := range networkResources { ID := networkResource.ID netName := networkResource.Name + driver := networkResource.Driver + scope := networkResource.Scope if !opts.noTrunc { ID = stringid.TruncateID(ID) } @@ -86,11 +88,11 @@ func runList(dockerCli *client.DockerCli, opts listOptions) error { fmt.Fprintln(w, ID) continue } - driver := networkResource.Driver - fmt.Fprintf(w, "%s\t%s\t%s\t", + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t", ID, netName, - driver) + driver, + scope) fmt.Fprint(w, "\n") } w.Flush() 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..d951043f78 --- /dev/null +++ b/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 +} diff --git a/api/client/node/demote.go b/api/client/node/demote.go new file mode 100644 index 0000000000..25f2073ee6 --- /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 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 +} diff --git a/api/client/node/inspect.go b/api/client/node/inspect.go new file mode 100644 index 0000000000..a4a7291f4b --- /dev/null +++ b/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) + } + } + +} diff --git a/api/client/node/list.go b/api/client/node/list.go new file mode 100644 index 0000000000..c21cb943d9 --- /dev/null +++ b/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) + } +} diff --git a/api/client/node/opts.go b/api/client/node/opts.go new file mode 100644 index 0000000000..cd160252d9 --- /dev/null +++ b/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 +} diff --git a/api/client/node/promote.go b/api/client/node/promote.go new file mode 100644 index 0000000000..858b36a8c7 --- /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 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 +} diff --git a/api/client/node/remove.go b/api/client/node/remove.go new file mode 100644 index 0000000000..540194062d --- /dev/null +++ b/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 +} diff --git a/api/client/node/tasks.go b/api/client/node/tasks.go new file mode 100644 index 0000000000..0c044e3dd6 --- /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", 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)) +} diff --git a/api/client/node/update.go b/api/client/node/update.go new file mode 100644 index 0000000000..c0e8d88f35 --- /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.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) + } +} diff --git a/api/client/service/cmd.go b/api/client/service/cmd.go new file mode 100644 index 0000000000..b660c19f6f --- /dev/null +++ b/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 +} diff --git a/api/client/service/create.go b/api/client/service/create.go new file mode 100644 index 0000000000..2141ca5966 --- /dev/null +++ b/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 +} diff --git a/api/client/service/inspect.go b/api/client/service/inspect.go new file mode 100644 index 0000000000..a75e4e7cc1 --- /dev/null +++ b/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) +} diff --git a/api/client/service/list.go b/api/client/service/list.go new file mode 100644 index 0000000000..c1246c86d5 --- /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 && 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) + } +} diff --git a/api/client/service/opts.go b/api/client/service/opts.go new file mode 100644 index 0000000000..f54c81b00a --- /dev/null +++ b/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") +} 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/scale.go b/api/client/service/scale.go new file mode 100644 index 0000000000..ae528b55d3 --- /dev/null +++ b/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 +} diff --git a/api/client/service/tasks.go b/api/client/service/tasks.go new file mode 100644 index 0000000000..6169d8bdb7 --- /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", 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)) +} diff --git a/api/client/service/update.go b/api/client/service/update.go new file mode 100644 index 0000000000..f5483235e8 --- /dev/null +++ b/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 +} 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..0c66246390 --- /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", "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 +} diff --git a/api/client/swarm/inspect.go b/api/client/swarm/inspect.go new file mode 100644 index 0000000000..407a0bfb83 --- /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.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) +} diff --git a/api/client/swarm/join.go b/api/client/swarm/join.go new file mode 100644 index 0000000000..346445f783 --- /dev/null +++ b/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 +} 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..fa543b6596 --- /dev/null +++ b/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)} +} diff --git a/api/client/swarm/update.go b/api/client/swarm/update.go new file mode 100644 index 0000000000..59aef4c8af --- /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", "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 +} 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..4909f159d5 --- /dev/null +++ b/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 +} diff --git a/api/client/utils.go b/api/client/utils.go index e7497ac41a..043444eae5 100644 --- a/api/client/utils.go +++ b/api/client/utils.go @@ -8,6 +8,7 @@ import ( gosignal "os/signal" "path/filepath" "runtime" + "strings" "time" "golang.org/x/net/context" @@ -163,3 +164,27 @@ func (cli *DockerCli) ForwardAllSignals(ctx context.Context, cid string) chan os }() return sigc } + +// capitalizeFirst capitalizes the first character of string +func capitalizeFirst(s string) string { + switch l := len(s); l { + case 0: + return s + case 1: + return strings.ToLower(s) + default: + return strings.ToUpper(string(s[0])) + strings.ToLower(s[1:]) + } +} + +// PrettyPrint outputs arbitrary data for human formatted output by uppercasing the first letter. +func PrettyPrint(i interface{}) string { + switch t := i.(type) { + case nil: + return "None" + case string: + return capitalizeFirst(t) + default: + return capitalizeFirst(fmt.Sprintf("%s", t)) + } +} diff --git a/cli/cobraadaptor/adaptor.go b/cli/cobraadaptor/adaptor.go index 4d2958b531..6f1a8876b3 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 73fa4f2245..3c3b321be6 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"}, {"update", "Update configuration of one or more containers"}, } diff --git a/integration-cli/docker_cli_rename_test.go b/integration-cli/docker_cli_rename_test.go index 74389a2ac7..76bbcfea9f 100644 --- a/integration-cli/docker_cli_rename_test.go +++ b/integration-cli/docker_cli_rename_test.go @@ -63,7 +63,7 @@ func (s *DockerSuite) TestRenameCheckNames(c *check.C) { name, err := inspectFieldWithError("first_name", "Name") c.Assert(err, checker.NotNil, check.Commentf(name)) - c.Assert(err.Error(), checker.Contains, "No such image or container: first_name") + c.Assert(err.Error(), checker.Contains, "No such image, container or task: first_name") } func (s *DockerSuite) TestRenameInvalidName(c *check.C) { |