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>
This commit is contained in:
parent
d4abe1d84a
commit
12a00e6017
36 changed files with 2612 additions and 12 deletions
70
api/client/idresolver/idresolver.go
Normal file
70
api/client/idresolver/idresolver.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -71,7 +71,7 @@ func runList(dockerCli *client.DockerCli, opts listOptions) error {
|
|||
|
||||
w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0)
|
||||
if !opts.quiet {
|
||||
fmt.Fprintf(w, "NETWORK ID\tNAME\tDRIVER")
|
||||
fmt.Fprintf(w, "NETWORK ID\tNAME\tDRIVER\tSCOPE")
|
||||
fmt.Fprintf(w, "\n")
|
||||
}
|
||||
|
||||
|
@ -79,6 +79,8 @@ func runList(dockerCli *client.DockerCli, opts listOptions) error {
|
|||
for _, networkResource := range networkResources {
|
||||
ID := networkResource.ID
|
||||
netName := networkResource.Name
|
||||
driver := networkResource.Driver
|
||||
scope := networkResource.Scope
|
||||
if !opts.noTrunc {
|
||||
ID = stringid.TruncateID(ID)
|
||||
}
|
||||
|
@ -86,11 +88,11 @@ func runList(dockerCli *client.DockerCli, opts listOptions) error {
|
|||
fmt.Fprintln(w, ID)
|
||||
continue
|
||||
}
|
||||
driver := networkResource.Driver
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t",
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t",
|
||||
ID,
|
||||
netName,
|
||||
driver)
|
||||
driver,
|
||||
scope)
|
||||
fmt.Fprint(w, "\n")
|
||||
}
|
||||
w.Flush()
|
||||
|
|
40
api/client/node/accept.go
Normal file
40
api/client/node/accept.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
package node
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/docker/api/client"
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/engine-api/types/swarm"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
func newAcceptCommand(dockerCli *client.DockerCli) *cobra.Command {
|
||||
var flags *pflag.FlagSet
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "accept NODE [NODE...]",
|
||||
Short: "Accept a node in the swarm",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runAccept(dockerCli, flags, args)
|
||||
},
|
||||
}
|
||||
|
||||
flags = cmd.Flags()
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runAccept(dockerCli *client.DockerCli, flags *pflag.FlagSet, args []string) error {
|
||||
for _, id := range args {
|
||||
if err := runUpdate(dockerCli, id, func(node *swarm.Node) {
|
||||
node.Spec.Membership = swarm.NodeMembershipAccepted
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(id, "attempting to accept a node in the swarm.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
49
api/client/node/cmd.go
Normal file
49
api/client/node/cmd.go
Normal file
|
@ -0,0 +1,49 @@
|
|||
package node
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/docker/api/client"
|
||||
"github.com/docker/docker/cli"
|
||||
apiclient "github.com/docker/engine-api/client"
|
||||
)
|
||||
|
||||
// NewNodeCommand returns a cobra command for `node` subcommands
|
||||
func NewNodeCommand(dockerCli *client.DockerCli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "node",
|
||||
Short: "Manage docker swarm nodes",
|
||||
Args: cli.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
|
||||
},
|
||||
}
|
||||
cmd.AddCommand(
|
||||
newAcceptCommand(dockerCli),
|
||||
newDemoteCommand(dockerCli),
|
||||
newInspectCommand(dockerCli),
|
||||
newListCommand(dockerCli),
|
||||
newPromoteCommand(dockerCli),
|
||||
newRemoveCommand(dockerCli),
|
||||
newTasksCommand(dockerCli),
|
||||
newUpdateCommand(dockerCli),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func nodeReference(client apiclient.APIClient, ctx context.Context, ref string) (string, error) {
|
||||
// The special value "self" for a node reference is mapped to the current
|
||||
// node, hence the node ID is retrieved using the `/info` endpoint.
|
||||
if ref == "self" {
|
||||
info, err := client.Info(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return info.Swarm.NodeID, nil
|
||||
}
|
||||
return ref, nil
|
||||
}
|
40
api/client/node/demote.go
Normal file
40
api/client/node/demote.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
package node
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/docker/api/client"
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/engine-api/types/swarm"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
func newDemoteCommand(dockerCli *client.DockerCli) *cobra.Command {
|
||||
var flags *pflag.FlagSet
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "demote NODE [NODE...]",
|
||||
Short: "Demote a node from manager in the swarm",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runDemote(dockerCli, flags, args)
|
||||
},
|
||||
}
|
||||
|
||||
flags = cmd.Flags()
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runDemote(dockerCli *client.DockerCli, flags *pflag.FlagSet, args []string) error {
|
||||
for _, id := range args {
|
||||
if err := runUpdate(dockerCli, id, func(node *swarm.Node) {
|
||||
node.Spec.Role = swarm.NodeRoleWorker
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(id, "attempting to demote a manager in the swarm.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
141
api/client/node/inspect.go
Normal file
141
api/client/node/inspect.go
Normal file
|
@ -0,0 +1,141 @@
|
|||
package node
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/client"
|
||||
"github.com/docker/docker/api/client/inspect"
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
"github.com/docker/engine-api/types/swarm"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type inspectOptions struct {
|
||||
nodeIds []string
|
||||
format string
|
||||
pretty bool
|
||||
}
|
||||
|
||||
func newInspectCommand(dockerCli *client.DockerCli) *cobra.Command {
|
||||
var opts inspectOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "inspect [OPTIONS] self|NODE [NODE...]",
|
||||
Short: "Inspect a node in the swarm",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.nodeIds = args
|
||||
return runInspect(dockerCli, opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template")
|
||||
flags.BoolVarP(&opts.pretty, "pretty", "p", false, "Print the information in a human friendly format.")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runInspect(dockerCli *client.DockerCli, opts inspectOptions) error {
|
||||
client := dockerCli.Client()
|
||||
ctx := context.Background()
|
||||
getRef := func(ref string) (interface{}, []byte, error) {
|
||||
nodeRef, err := nodeReference(client, ctx, ref)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
node, err := client.NodeInspect(ctx, nodeRef)
|
||||
return node, nil, err
|
||||
}
|
||||
|
||||
if !opts.pretty {
|
||||
return inspect.Inspect(dockerCli.Out(), opts.nodeIds, opts.format, getRef)
|
||||
}
|
||||
return printHumanFriendly(dockerCli.Out(), opts.nodeIds, getRef)
|
||||
}
|
||||
|
||||
func printHumanFriendly(out io.Writer, refs []string, getRef inspect.GetRefFunc) error {
|
||||
for idx, ref := range refs {
|
||||
obj, _, err := getRef(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printNode(out, obj.(swarm.Node))
|
||||
|
||||
// TODO: better way to do this?
|
||||
// print extra space between objects, but not after the last one
|
||||
if idx+1 != len(refs) {
|
||||
fmt.Fprintf(out, "\n\n")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: use a template
|
||||
func printNode(out io.Writer, node swarm.Node) {
|
||||
fmt.Fprintf(out, "ID:\t\t\t%s\n", node.ID)
|
||||
ioutils.FprintfIfNotEmpty(out, "Name:\t\t\t%s\n", node.Spec.Name)
|
||||
if node.Spec.Labels != nil {
|
||||
fmt.Fprintln(out, "Labels:")
|
||||
for k, v := range node.Spec.Labels {
|
||||
fmt.Fprintf(out, " - %s = %s\n", k, v)
|
||||
}
|
||||
}
|
||||
|
||||
ioutils.FprintfIfNotEmpty(out, "Hostname:\t\t%s\n", node.Description.Hostname)
|
||||
fmt.Fprintln(out, "Status:")
|
||||
fmt.Fprintf(out, " State:\t\t\t%s\n", client.PrettyPrint(node.Status.State))
|
||||
ioutils.FprintfIfNotEmpty(out, " Message:\t\t%s\n", client.PrettyPrint(node.Status.Message))
|
||||
fmt.Fprintf(out, " Availability:\t\t%s\n", client.PrettyPrint(node.Spec.Availability))
|
||||
|
||||
if node.ManagerStatus != nil {
|
||||
fmt.Fprintln(out, "Manager Status:")
|
||||
fmt.Fprintf(out, " Address:\t\t%s\n", node.ManagerStatus.Addr)
|
||||
fmt.Fprintf(out, " Raft status:\t\t%s\n", client.PrettyPrint(node.ManagerStatus.Reachability))
|
||||
leader := "No"
|
||||
if node.ManagerStatus.Leader {
|
||||
leader = "Yes"
|
||||
}
|
||||
fmt.Fprintf(out, " Leader:\t\t%s\n", leader)
|
||||
}
|
||||
|
||||
fmt.Fprintln(out, "Platform:")
|
||||
fmt.Fprintf(out, " Operating System:\t%s\n", node.Description.Platform.OS)
|
||||
fmt.Fprintf(out, " Architecture:\t\t%s\n", node.Description.Platform.Architecture)
|
||||
|
||||
fmt.Fprintln(out, "Resources:")
|
||||
fmt.Fprintf(out, " CPUs:\t\t\t%d\n", node.Description.Resources.NanoCPUs/1e9)
|
||||
fmt.Fprintf(out, " Memory:\t\t%s\n", units.BytesSize(float64(node.Description.Resources.MemoryBytes)))
|
||||
|
||||
var pluginTypes []string
|
||||
pluginNamesByType := map[string][]string{}
|
||||
for _, p := range node.Description.Engine.Plugins {
|
||||
// append to pluginTypes only if not done previously
|
||||
if _, ok := pluginNamesByType[p.Type]; !ok {
|
||||
pluginTypes = append(pluginTypes, p.Type)
|
||||
}
|
||||
pluginNamesByType[p.Type] = append(pluginNamesByType[p.Type], p.Name)
|
||||
}
|
||||
|
||||
if len(pluginTypes) > 0 {
|
||||
fmt.Fprintln(out, "Plugins:")
|
||||
sort.Strings(pluginTypes) // ensure stable output
|
||||
for _, pluginType := range pluginTypes {
|
||||
fmt.Fprintf(out, " %s:\t\t%s\n", pluginType, strings.Join(pluginNamesByType[pluginType], ", "))
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(out, "Engine Version:\t\t%s\n", node.Description.Engine.EngineVersion)
|
||||
|
||||
if len(node.Description.Engine.Labels) != 0 {
|
||||
fmt.Fprintln(out, "Engine Labels:")
|
||||
for k, v := range node.Description.Engine.Labels {
|
||||
fmt.Fprintf(out, " - %s = %s", k, v)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
119
api/client/node/list.go
Normal file
119
api/client/node/list.go
Normal file
|
@ -0,0 +1,119 @@
|
|||
package node
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"text/tabwriter"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/docker/docker/api/client"
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/opts"
|
||||
"github.com/docker/engine-api/types"
|
||||
"github.com/docker/engine-api/types/swarm"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
listItemFmt = "%s\t%s\t%s\t%s\t%s\t%s\t%s\n"
|
||||
)
|
||||
|
||||
type listOptions struct {
|
||||
quiet bool
|
||||
filter opts.FilterOpt
|
||||
}
|
||||
|
||||
func newListCommand(dockerCli *client.DockerCli) *cobra.Command {
|
||||
opts := listOptions{filter: opts.NewFilterOpt()}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "ls",
|
||||
Aliases: []string{"list"},
|
||||
Short: "List nodes in the swarm",
|
||||
Args: cli.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runList(dockerCli, opts)
|
||||
},
|
||||
}
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs")
|
||||
flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runList(dockerCli *client.DockerCli, opts listOptions) error {
|
||||
client := dockerCli.Client()
|
||||
ctx := context.Background()
|
||||
|
||||
nodes, err := client.NodeList(
|
||||
ctx,
|
||||
types.NodeListOptions{Filter: opts.filter.Value()})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
info, err := client.Info(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out := dockerCli.Out()
|
||||
if opts.quiet {
|
||||
printQuiet(out, nodes)
|
||||
} else {
|
||||
printTable(out, nodes, info)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func printTable(out io.Writer, nodes []swarm.Node, info types.Info) {
|
||||
writer := tabwriter.NewWriter(out, 0, 4, 2, ' ', 0)
|
||||
|
||||
// Ignore flushing errors
|
||||
defer writer.Flush()
|
||||
|
||||
fmt.Fprintf(writer, listItemFmt, "ID", "NAME", "MEMBERSHIP", "STATUS", "AVAILABILITY", "MANAGER STATUS", "LEADER")
|
||||
for _, node := range nodes {
|
||||
name := node.Spec.Name
|
||||
availability := string(node.Spec.Availability)
|
||||
membership := string(node.Spec.Membership)
|
||||
|
||||
if name == "" {
|
||||
name = node.Description.Hostname
|
||||
}
|
||||
|
||||
leader := ""
|
||||
if node.ManagerStatus != nil && node.ManagerStatus.Leader {
|
||||
leader = "Yes"
|
||||
}
|
||||
|
||||
reachability := ""
|
||||
if node.ManagerStatus != nil {
|
||||
reachability = string(node.ManagerStatus.Reachability)
|
||||
}
|
||||
|
||||
ID := node.ID
|
||||
if node.ID == info.Swarm.NodeID {
|
||||
ID = ID + " *"
|
||||
}
|
||||
|
||||
fmt.Fprintf(
|
||||
writer,
|
||||
listItemFmt,
|
||||
ID,
|
||||
name,
|
||||
client.PrettyPrint(membership),
|
||||
client.PrettyPrint(string(node.Status.State)),
|
||||
client.PrettyPrint(availability),
|
||||
client.PrettyPrint(reachability),
|
||||
leader)
|
||||
}
|
||||
}
|
||||
|
||||
func printQuiet(out io.Writer, nodes []swarm.Node) {
|
||||
for _, node := range nodes {
|
||||
fmt.Fprintln(out, node.ID)
|
||||
}
|
||||
}
|
50
api/client/node/opts.go
Normal file
50
api/client/node/opts.go
Normal file
|
@ -0,0 +1,50 @@
|
|||
package node
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/engine-api/types/swarm"
|
||||
)
|
||||
|
||||
type nodeOptions struct {
|
||||
role string
|
||||
membership string
|
||||
availability string
|
||||
}
|
||||
|
||||
func (opts *nodeOptions) ToNodeSpec() (swarm.NodeSpec, error) {
|
||||
var spec swarm.NodeSpec
|
||||
|
||||
switch swarm.NodeRole(strings.ToLower(opts.role)) {
|
||||
case swarm.NodeRoleWorker:
|
||||
spec.Role = swarm.NodeRoleWorker
|
||||
case swarm.NodeRoleManager:
|
||||
spec.Role = swarm.NodeRoleManager
|
||||
case "":
|
||||
default:
|
||||
return swarm.NodeSpec{}, fmt.Errorf("invalid role %q, only worker and manager are supported", opts.role)
|
||||
}
|
||||
|
||||
switch swarm.NodeMembership(strings.ToLower(opts.membership)) {
|
||||
case swarm.NodeMembershipAccepted:
|
||||
spec.Membership = swarm.NodeMembershipAccepted
|
||||
case "":
|
||||
default:
|
||||
return swarm.NodeSpec{}, fmt.Errorf("invalid membership %q, only accepted is supported", opts.membership)
|
||||
}
|
||||
|
||||
switch swarm.NodeAvailability(strings.ToLower(opts.availability)) {
|
||||
case swarm.NodeAvailabilityActive:
|
||||
spec.Availability = swarm.NodeAvailabilityActive
|
||||
case swarm.NodeAvailabilityPause:
|
||||
spec.Availability = swarm.NodeAvailabilityPause
|
||||
case swarm.NodeAvailabilityDrain:
|
||||
spec.Availability = swarm.NodeAvailabilityDrain
|
||||
case "":
|
||||
default:
|
||||
return swarm.NodeSpec{}, fmt.Errorf("invalid availability %q, only active, pause and drain are supported", opts.availability)
|
||||
}
|
||||
|
||||
return spec, nil
|
||||
}
|
40
api/client/node/promote.go
Normal file
40
api/client/node/promote.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
package node
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/docker/api/client"
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/engine-api/types/swarm"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
func newPromoteCommand(dockerCli *client.DockerCli) *cobra.Command {
|
||||
var flags *pflag.FlagSet
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "promote NODE [NODE...]",
|
||||
Short: "Promote a node to a manager in the swarm",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runPromote(dockerCli, flags, args)
|
||||
},
|
||||
}
|
||||
|
||||
flags = cmd.Flags()
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runPromote(dockerCli *client.DockerCli, flags *pflag.FlagSet, args []string) error {
|
||||
for _, id := range args {
|
||||
if err := runUpdate(dockerCli, id, func(node *swarm.Node) {
|
||||
node.Spec.Role = swarm.NodeRoleManager
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(id, "attempting to promote a node to a manager in the swarm.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
36
api/client/node/remove.go
Normal file
36
api/client/node/remove.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
package node
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/docker/docker/api/client"
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newRemoveCommand(dockerCli *client.DockerCli) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "rm NODE [NODE...]",
|
||||
Aliases: []string{"remove"},
|
||||
Short: "Remove a node from the swarm",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runRemove(dockerCli, args)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func runRemove(dockerCli *client.DockerCli, args []string) error {
|
||||
client := dockerCli.Client()
|
||||
ctx := context.Background()
|
||||
for _, nodeID := range args {
|
||||
err := client.NodeRemove(ctx, nodeID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(dockerCli.Out(), "%s\n", nodeID)
|
||||
}
|
||||
return nil
|
||||
}
|
72
api/client/node/tasks.go
Normal file
72
api/client/node/tasks.go
Normal file
|
@ -0,0 +1,72 @@
|
|||
package node
|
||||
|
||||
import (
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/docker/docker/api/client"
|
||||
"github.com/docker/docker/api/client/idresolver"
|
||||
"github.com/docker/docker/api/client/task"
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/opts"
|
||||
"github.com/docker/engine-api/types"
|
||||
"github.com/docker/engine-api/types/swarm"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type tasksOptions struct {
|
||||
nodeID string
|
||||
all bool
|
||||
noResolve bool
|
||||
filter opts.FilterOpt
|
||||
}
|
||||
|
||||
func newTasksCommand(dockerCli *client.DockerCli) *cobra.Command {
|
||||
opts := tasksOptions{filter: opts.NewFilterOpt()}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "tasks [OPTIONS] self|NODE",
|
||||
Short: "List tasks running on a node",
|
||||
Args: cli.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.nodeID = args[0]
|
||||
return runTasks(dockerCli, opts)
|
||||
},
|
||||
}
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&opts.all, "all", "a", false, "Display all instances")
|
||||
flags.BoolVarP(&opts.noResolve, "no-resolve", "n", false, "Do not map IDs to Names")
|
||||
flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runTasks(dockerCli *client.DockerCli, opts tasksOptions) error {
|
||||
client := dockerCli.Client()
|
||||
ctx := context.Background()
|
||||
|
||||
nodeRef, err := nodeReference(client, ctx, opts.nodeID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
node, err := client.NodeInspect(ctx, nodeRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filter := opts.filter.Value()
|
||||
filter.Add("node", node.ID)
|
||||
if !opts.all {
|
||||
filter.Add("desired_state", string(swarm.TaskStateRunning))
|
||||
filter.Add("desired_state", string(swarm.TaskStateAccepted))
|
||||
|
||||
}
|
||||
|
||||
tasks, err := client.TaskList(
|
||||
ctx,
|
||||
types.TaskListOptions{Filter: filter})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return task.Print(dockerCli, ctx, tasks, idresolver.New(client, opts.noResolve))
|
||||
}
|
100
api/client/node/update.go
Normal file
100
api/client/node/update.go
Normal file
|
@ -0,0 +1,100 @@
|
|||
package node
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/docker/api/client"
|
||||
"github.com/docker/docker/cli"
|
||||
runconfigopts "github.com/docker/docker/runconfig/opts"
|
||||
"github.com/docker/engine-api/types/swarm"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func newUpdateCommand(dockerCli *client.DockerCli) *cobra.Command {
|
||||
var opts nodeOptions
|
||||
var flags *pflag.FlagSet
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "update [OPTIONS] NODE",
|
||||
Short: "Update a node",
|
||||
Args: cli.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runUpdate(dockerCli, args[0], mergeNodeUpdate(flags))
|
||||
},
|
||||
}
|
||||
|
||||
flags = cmd.Flags()
|
||||
flags.StringVar(&opts.role, "role", "", "Role of the node (worker/manager)")
|
||||
flags.StringVar(&opts.membership, "membership", "", "Membership of the node (accepted/rejected)")
|
||||
flags.StringVar(&opts.availability, "availability", "", "Availability of the node (active/pause/drain)")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runUpdate(dockerCli *client.DockerCli, nodeID string, mergeNode func(node *swarm.Node)) error {
|
||||
client := dockerCli.Client()
|
||||
ctx := context.Background()
|
||||
|
||||
node, err := client.NodeInspect(ctx, nodeID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mergeNode(&node)
|
||||
err = client.NodeUpdate(ctx, nodeID, node.Version, node.Spec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(dockerCli.Out(), "%s\n", nodeID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func mergeNodeUpdate(flags *pflag.FlagSet) func(*swarm.Node) {
|
||||
return func(node *swarm.Node) {
|
||||
mergeString := func(flag string, field *string) {
|
||||
if flags.Changed(flag) {
|
||||
*field, _ = flags.GetString(flag)
|
||||
}
|
||||
}
|
||||
|
||||
mergeRole := func(flag string, field *swarm.NodeRole) {
|
||||
if flags.Changed(flag) {
|
||||
str, _ := flags.GetString(flag)
|
||||
*field = swarm.NodeRole(str)
|
||||
}
|
||||
}
|
||||
|
||||
mergeMembership := func(flag string, field *swarm.NodeMembership) {
|
||||
if flags.Changed(flag) {
|
||||
str, _ := flags.GetString(flag)
|
||||
*field = swarm.NodeMembership(str)
|
||||
}
|
||||
}
|
||||
|
||||
mergeAvailability := func(flag string, field *swarm.NodeAvailability) {
|
||||
if flags.Changed(flag) {
|
||||
str, _ := flags.GetString(flag)
|
||||
*field = swarm.NodeAvailability(str)
|
||||
}
|
||||
}
|
||||
|
||||
mergeLabels := func(flag string, field *map[string]string) {
|
||||
if flags.Changed(flag) {
|
||||
values, _ := flags.GetStringSlice(flag)
|
||||
for key, value := range runconfigopts.ConvertKVStringsToMap(values) {
|
||||
(*field)[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
spec := &node.Spec
|
||||
mergeString("name", &spec.Name)
|
||||
// TODO: setting labels is not working
|
||||
mergeLabels("label", &spec.Labels)
|
||||
mergeRole("role", &spec.Role)
|
||||
mergeMembership("membership", &spec.Membership)
|
||||
mergeAvailability("availability", &spec.Availability)
|
||||
}
|
||||
}
|
32
api/client/service/cmd.go
Normal file
32
api/client/service/cmd.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/docker/api/client"
|
||||
"github.com/docker/docker/cli"
|
||||
)
|
||||
|
||||
// NewServiceCommand returns a cobra command for `service` subcommands
|
||||
func NewServiceCommand(dockerCli *client.DockerCli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "service",
|
||||
Short: "Manage docker services",
|
||||
Args: cli.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
|
||||
},
|
||||
}
|
||||
cmd.AddCommand(
|
||||
newCreateCommand(dockerCli),
|
||||
newInspectCommand(dockerCli),
|
||||
newTasksCommand(dockerCli),
|
||||
newListCommand(dockerCli),
|
||||
newRemoveCommand(dockerCli),
|
||||
newScaleCommand(dockerCli),
|
||||
newUpdateCommand(dockerCli),
|
||||
)
|
||||
return cmd
|
||||
}
|
47
api/client/service/create.go
Normal file
47
api/client/service/create.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/docker/api/client"
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func newCreateCommand(dockerCli *client.DockerCli) *cobra.Command {
|
||||
opts := newServiceOptions()
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "create [OPTIONS] IMAGE [COMMAND] [ARG...]",
|
||||
Short: "Create a new service",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.image = args[0]
|
||||
if len(args) > 1 {
|
||||
opts.args = args[1:]
|
||||
}
|
||||
return runCreate(dockerCli, opts)
|
||||
},
|
||||
}
|
||||
addServiceFlags(cmd, opts)
|
||||
cmd.Flags().SetInterspersed(false)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runCreate(dockerCli *client.DockerCli, opts *serviceOptions) error {
|
||||
client := dockerCli.Client()
|
||||
|
||||
service, err := opts.ToService()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
response, err := client.ServiceCreate(context.Background(), service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(dockerCli.Out(), "%s\n", response.ID)
|
||||
return nil
|
||||
}
|
127
api/client/service/inspect.go
Normal file
127
api/client/service/inspect.go
Normal file
|
@ -0,0 +1,127 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/docker/docker/api/client"
|
||||
"github.com/docker/docker/api/client/inspect"
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
apiclient "github.com/docker/engine-api/client"
|
||||
"github.com/docker/engine-api/types/swarm"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type inspectOptions struct {
|
||||
refs []string
|
||||
format string
|
||||
pretty bool
|
||||
}
|
||||
|
||||
func newInspectCommand(dockerCli *client.DockerCli) *cobra.Command {
|
||||
var opts inspectOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "inspect [OPTIONS] SERVICE [SERVICE...]",
|
||||
Short: "Inspect a service",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.refs = args
|
||||
|
||||
if opts.pretty && len(opts.format) > 0 {
|
||||
return fmt.Errorf("--format is incompatible with human friendly format")
|
||||
}
|
||||
return runInspect(dockerCli, opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template")
|
||||
flags.BoolVarP(&opts.pretty, "pretty", "p", false, "Print the information in a human friendly format.")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runInspect(dockerCli *client.DockerCli, opts inspectOptions) error {
|
||||
client := dockerCli.Client()
|
||||
ctx := context.Background()
|
||||
|
||||
getRef := func(ref string) (interface{}, []byte, error) {
|
||||
service, err := client.ServiceInspect(ctx, ref)
|
||||
if err == nil || !apiclient.IsErrServiceNotFound(err) {
|
||||
return service, nil, err
|
||||
}
|
||||
return nil, nil, fmt.Errorf("Error: no such service: %s", ref)
|
||||
}
|
||||
|
||||
if !opts.pretty {
|
||||
return inspect.Inspect(dockerCli.Out(), opts.refs, opts.format, getRef)
|
||||
}
|
||||
|
||||
return printHumanFriendly(dockerCli.Out(), opts.refs, getRef)
|
||||
}
|
||||
|
||||
func printHumanFriendly(out io.Writer, refs []string, getRef inspect.GetRefFunc) error {
|
||||
for idx, ref := range refs {
|
||||
obj, _, err := getRef(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printService(out, obj.(swarm.Service))
|
||||
|
||||
// TODO: better way to do this?
|
||||
// print extra space between objects, but not after the last one
|
||||
if idx+1 != len(refs) {
|
||||
fmt.Fprintf(out, "\n\n")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: use a template
|
||||
func printService(out io.Writer, service swarm.Service) {
|
||||
fmt.Fprintf(out, "ID:\t\t%s\n", service.ID)
|
||||
fmt.Fprintf(out, "Name:\t\t%s\n", service.Spec.Name)
|
||||
if service.Spec.Labels != nil {
|
||||
fmt.Fprintln(out, "Labels:")
|
||||
for k, v := range service.Spec.Labels {
|
||||
fmt.Fprintf(out, " - %s=%s\n", k, v)
|
||||
}
|
||||
}
|
||||
|
||||
if service.Spec.Mode.Global != nil {
|
||||
fmt.Fprintln(out, "Mode:\t\tGLOBAL")
|
||||
} else {
|
||||
fmt.Fprintln(out, "Mode:\t\tREPLICATED")
|
||||
if service.Spec.Mode.Replicated.Replicas != nil {
|
||||
fmt.Fprintf(out, " Replicas:\t\t%d\n", *service.Spec.Mode.Replicated.Replicas)
|
||||
}
|
||||
}
|
||||
fmt.Fprintln(out, "Placement:")
|
||||
fmt.Fprintln(out, " Strategy:\tSPREAD")
|
||||
fmt.Fprintf(out, "UpateConfig:\n")
|
||||
fmt.Fprintf(out, " Parallelism:\t%d\n", service.Spec.UpdateConfig.Parallelism)
|
||||
if service.Spec.UpdateConfig.Delay.Nanoseconds() > 0 {
|
||||
fmt.Fprintf(out, " Delay:\t\t%s\n", service.Spec.UpdateConfig.Delay)
|
||||
}
|
||||
fmt.Fprintf(out, "ContainerSpec:\n")
|
||||
printContainerSpec(out, service.Spec.TaskTemplate.ContainerSpec)
|
||||
}
|
||||
|
||||
func printContainerSpec(out io.Writer, containerSpec swarm.ContainerSpec) {
|
||||
fmt.Fprintf(out, " Image:\t\t%s\n", containerSpec.Image)
|
||||
if len(containerSpec.Command) > 0 {
|
||||
fmt.Fprintf(out, " Command:\t%s\n", strings.Join(containerSpec.Command, " "))
|
||||
}
|
||||
if len(containerSpec.Args) > 0 {
|
||||
fmt.Fprintf(out, " Args:\t%s\n", strings.Join(containerSpec.Args, " "))
|
||||
}
|
||||
if len(containerSpec.Env) > 0 {
|
||||
fmt.Fprintf(out, " Env:\t\t%s\n", strings.Join(containerSpec.Env, " "))
|
||||
}
|
||||
ioutils.FprintfIfNotEmpty(out, " Dir\t\t%s\n", containerSpec.Dir)
|
||||
ioutils.FprintfIfNotEmpty(out, " User\t\t%s\n", containerSpec.User)
|
||||
}
|
97
api/client/service/list.go
Normal file
97
api/client/service/list.go
Normal file
|
@ -0,0 +1,97 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/docker/docker/api/client"
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/opts"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/engine-api/types"
|
||||
"github.com/docker/engine-api/types/swarm"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
listItemFmt = "%s\t%s\t%s\t%s\t%s\n"
|
||||
)
|
||||
|
||||
type listOptions struct {
|
||||
quiet bool
|
||||
filter opts.FilterOpt
|
||||
}
|
||||
|
||||
func newListCommand(dockerCli *client.DockerCli) *cobra.Command {
|
||||
opts := listOptions{filter: opts.NewFilterOpt()}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "ls",
|
||||
Aliases: []string{"list"},
|
||||
Short: "List services",
|
||||
Args: cli.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runList(dockerCli, opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs")
|
||||
flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runList(dockerCli *client.DockerCli, opts listOptions) error {
|
||||
client := dockerCli.Client()
|
||||
|
||||
services, err := client.ServiceList(
|
||||
context.Background(),
|
||||
types.ServiceListOptions{Filter: opts.filter.Value()})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out := dockerCli.Out()
|
||||
if opts.quiet {
|
||||
printQuiet(out, services)
|
||||
} else {
|
||||
printTable(out, services)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func printTable(out io.Writer, services []swarm.Service) {
|
||||
writer := tabwriter.NewWriter(out, 0, 4, 2, ' ', 0)
|
||||
|
||||
// Ignore flushing errors
|
||||
defer writer.Flush()
|
||||
|
||||
fmt.Fprintf(writer, listItemFmt, "ID", "NAME", "SCALE", "IMAGE", "COMMAND")
|
||||
for _, service := range services {
|
||||
scale := ""
|
||||
if service.Spec.Mode.Replicated != nil && service.Spec.Mode.Replicated.Replicas != nil {
|
||||
scale = fmt.Sprintf("%d", *service.Spec.Mode.Replicated.Replicas)
|
||||
} else if service.Spec.Mode.Global != nil {
|
||||
scale = "global"
|
||||
}
|
||||
fmt.Fprintf(
|
||||
writer,
|
||||
listItemFmt,
|
||||
stringid.TruncateID(service.ID),
|
||||
service.Spec.Name,
|
||||
scale,
|
||||
service.Spec.TaskTemplate.ContainerSpec.Image,
|
||||
strings.Join(service.Spec.TaskTemplate.ContainerSpec.Args, " "))
|
||||
}
|
||||
}
|
||||
|
||||
func printQuiet(out io.Writer, services []swarm.Service) {
|
||||
for _, service := range services {
|
||||
fmt.Fprintln(out, service.ID)
|
||||
}
|
||||
}
|
462
api/client/service/opts.go
Normal file
462
api/client/service/opts.go
Normal file
|
@ -0,0 +1,462 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/opts"
|
||||
runconfigopts "github.com/docker/docker/runconfig/opts"
|
||||
"github.com/docker/engine-api/types/swarm"
|
||||
"github.com/docker/go-connections/nat"
|
||||
units "github.com/docker/go-units"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultReplicas is the default replicas to use for a replicated service
|
||||
DefaultReplicas uint64 = 1
|
||||
)
|
||||
|
||||
type int64Value interface {
|
||||
Value() int64
|
||||
}
|
||||
|
||||
type memBytes int64
|
||||
|
||||
func (m *memBytes) String() string {
|
||||
return strconv.FormatInt(m.Value(), 10)
|
||||
}
|
||||
|
||||
func (m *memBytes) Set(value string) error {
|
||||
val, err := units.RAMInBytes(value)
|
||||
*m = memBytes(val)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *memBytes) Type() string {
|
||||
return "MemoryBytes"
|
||||
}
|
||||
|
||||
func (m *memBytes) Value() int64 {
|
||||
return int64(*m)
|
||||
}
|
||||
|
||||
type nanoCPUs int64
|
||||
|
||||
func (c *nanoCPUs) String() string {
|
||||
return strconv.FormatInt(c.Value(), 10)
|
||||
}
|
||||
|
||||
func (c *nanoCPUs) Set(value string) error {
|
||||
cpu, ok := new(big.Rat).SetString(value)
|
||||
if !ok {
|
||||
return fmt.Errorf("Failed to parse %v as a rational number", value)
|
||||
}
|
||||
nano := cpu.Mul(cpu, big.NewRat(1e9, 1))
|
||||
if !nano.IsInt() {
|
||||
return fmt.Errorf("value is too precise")
|
||||
}
|
||||
*c = nanoCPUs(nano.Num().Int64())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *nanoCPUs) Type() string {
|
||||
return "NanoCPUs"
|
||||
}
|
||||
|
||||
func (c *nanoCPUs) Value() int64 {
|
||||
return int64(*c)
|
||||
}
|
||||
|
||||
// DurationOpt is an option type for time.Duration that uses a pointer. This
|
||||
// allows us to get nil values outside, instead of defaulting to 0
|
||||
type DurationOpt struct {
|
||||
value *time.Duration
|
||||
}
|
||||
|
||||
// Set a new value on the option
|
||||
func (d *DurationOpt) Set(s string) error {
|
||||
v, err := time.ParseDuration(s)
|
||||
d.value = &v
|
||||
return err
|
||||
}
|
||||
|
||||
// Type returns the type of this option
|
||||
func (d *DurationOpt) Type() string {
|
||||
return "duration-ptr"
|
||||
}
|
||||
|
||||
// String returns a string repr of this option
|
||||
func (d *DurationOpt) String() string {
|
||||
if d.value != nil {
|
||||
return d.value.String()
|
||||
}
|
||||
return "none"
|
||||
}
|
||||
|
||||
// Value returns the time.Duration
|
||||
func (d *DurationOpt) Value() *time.Duration {
|
||||
return d.value
|
||||
}
|
||||
|
||||
// Uint64Opt represents a uint64.
|
||||
type Uint64Opt struct {
|
||||
value *uint64
|
||||
}
|
||||
|
||||
// Set a new value on the option
|
||||
func (i *Uint64Opt) Set(s string) error {
|
||||
v, err := strconv.ParseUint(s, 0, 64)
|
||||
i.value = &v
|
||||
return err
|
||||
}
|
||||
|
||||
// Type returns the type of this option
|
||||
func (i *Uint64Opt) Type() string {
|
||||
return "uint64-ptr"
|
||||
}
|
||||
|
||||
// String returns a string repr of this option
|
||||
func (i *Uint64Opt) String() string {
|
||||
if i.value != nil {
|
||||
return fmt.Sprintf("%v", *i.value)
|
||||
}
|
||||
return "none"
|
||||
}
|
||||
|
||||
// Value returns the uint64
|
||||
func (i *Uint64Opt) Value() *uint64 {
|
||||
return i.value
|
||||
}
|
||||
|
||||
// MountOpt is a Value type for parsing mounts
|
||||
type MountOpt struct {
|
||||
values []swarm.Mount
|
||||
}
|
||||
|
||||
// Set a new mount value
|
||||
func (m *MountOpt) Set(value string) error {
|
||||
csvReader := csv.NewReader(strings.NewReader(value))
|
||||
fields, err := csvReader.Read()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mount := swarm.Mount{}
|
||||
|
||||
volumeOptions := func() *swarm.VolumeOptions {
|
||||
if mount.VolumeOptions == nil {
|
||||
mount.VolumeOptions = &swarm.VolumeOptions{
|
||||
Labels: make(map[string]string),
|
||||
}
|
||||
}
|
||||
return mount.VolumeOptions
|
||||
}
|
||||
|
||||
setValueOnMap := func(target map[string]string, value string) {
|
||||
parts := strings.SplitN(value, "=", 2)
|
||||
if len(parts) == 1 {
|
||||
target[value] = ""
|
||||
} else {
|
||||
target[parts[0]] = parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
for _, field := range fields {
|
||||
parts := strings.SplitN(field, "=", 2)
|
||||
if len(parts) == 1 && strings.ToLower(parts[0]) == "writable" {
|
||||
mount.Writable = true
|
||||
continue
|
||||
}
|
||||
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("invald field '%s' must be a key=value pair", field)
|
||||
}
|
||||
|
||||
key, value := parts[0], parts[1]
|
||||
switch strings.ToLower(key) {
|
||||
case "type":
|
||||
mount.Type = swarm.MountType(strings.ToUpper(value))
|
||||
case "source":
|
||||
mount.Source = value
|
||||
case "target":
|
||||
mount.Target = value
|
||||
case "writable":
|
||||
mount.Writable, err = strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invald value for writable: %s", err.Error())
|
||||
}
|
||||
case "bind-propagation":
|
||||
mount.BindOptions.Propagation = swarm.MountPropagation(strings.ToUpper(value))
|
||||
case "volume-populate":
|
||||
volumeOptions().Populate, err = strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invald value for populate: %s", err.Error())
|
||||
}
|
||||
case "volume-label":
|
||||
setValueOnMap(volumeOptions().Labels, value)
|
||||
case "volume-driver":
|
||||
volumeOptions().DriverConfig.Name = value
|
||||
case "volume-driver-opt":
|
||||
if volumeOptions().DriverConfig.Options == nil {
|
||||
volumeOptions().DriverConfig.Options = make(map[string]string)
|
||||
}
|
||||
setValueOnMap(volumeOptions().DriverConfig.Options, value)
|
||||
default:
|
||||
return fmt.Errorf("unexpected key '%s' in '%s'", key, value)
|
||||
}
|
||||
}
|
||||
|
||||
if mount.Type == "" {
|
||||
return fmt.Errorf("type is required")
|
||||
}
|
||||
|
||||
if mount.Target == "" {
|
||||
return fmt.Errorf("target is required")
|
||||
}
|
||||
|
||||
m.values = append(m.values, mount)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Type returns the type of this option
|
||||
func (m *MountOpt) Type() string {
|
||||
return "mount"
|
||||
}
|
||||
|
||||
// String returns a string repr of this option
|
||||
func (m *MountOpt) String() string {
|
||||
mounts := []string{}
|
||||
for _, mount := range m.values {
|
||||
mounts = append(mounts, fmt.Sprintf("%v", mount))
|
||||
}
|
||||
return strings.Join(mounts, ", ")
|
||||
}
|
||||
|
||||
// Value returns the mounts
|
||||
func (m *MountOpt) Value() []swarm.Mount {
|
||||
return m.values
|
||||
}
|
||||
|
||||
type updateOptions struct {
|
||||
parallelism uint64
|
||||
delay time.Duration
|
||||
}
|
||||
|
||||
type resourceOptions struct {
|
||||
limitCPU nanoCPUs
|
||||
limitMemBytes memBytes
|
||||
resCPU nanoCPUs
|
||||
resMemBytes memBytes
|
||||
}
|
||||
|
||||
func (r *resourceOptions) ToResourceRequirements() *swarm.ResourceRequirements {
|
||||
return &swarm.ResourceRequirements{
|
||||
Limits: &swarm.Resources{
|
||||
NanoCPUs: r.limitCPU.Value(),
|
||||
MemoryBytes: r.limitMemBytes.Value(),
|
||||
},
|
||||
Reservations: &swarm.Resources{
|
||||
NanoCPUs: r.resCPU.Value(),
|
||||
MemoryBytes: r.resMemBytes.Value(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type restartPolicyOptions struct {
|
||||
condition string
|
||||
delay DurationOpt
|
||||
maxAttempts Uint64Opt
|
||||
window DurationOpt
|
||||
}
|
||||
|
||||
func (r *restartPolicyOptions) ToRestartPolicy() *swarm.RestartPolicy {
|
||||
return &swarm.RestartPolicy{
|
||||
Condition: swarm.RestartPolicyCondition(r.condition),
|
||||
Delay: r.delay.Value(),
|
||||
MaxAttempts: r.maxAttempts.Value(),
|
||||
Window: r.window.Value(),
|
||||
}
|
||||
}
|
||||
|
||||
func convertNetworks(networks []string) []swarm.NetworkAttachmentConfig {
|
||||
nets := []swarm.NetworkAttachmentConfig{}
|
||||
for _, network := range networks {
|
||||
nets = append(nets, swarm.NetworkAttachmentConfig{Target: network})
|
||||
}
|
||||
return nets
|
||||
}
|
||||
|
||||
type endpointOptions struct {
|
||||
mode string
|
||||
ports opts.ListOpts
|
||||
}
|
||||
|
||||
func (e *endpointOptions) ToEndpointSpec() *swarm.EndpointSpec {
|
||||
portConfigs := []swarm.PortConfig{}
|
||||
// We can ignore errors because the format was already validated by ValidatePort
|
||||
ports, portBindings, _ := nat.ParsePortSpecs(e.ports.GetAll())
|
||||
|
||||
for port := range ports {
|
||||
portConfigs = append(portConfigs, convertPortToPortConfig(port, portBindings)...)
|
||||
}
|
||||
|
||||
return &swarm.EndpointSpec{
|
||||
Mode: swarm.ResolutionMode(e.mode),
|
||||
Ports: portConfigs,
|
||||
}
|
||||
}
|
||||
|
||||
func convertPortToPortConfig(
|
||||
port nat.Port,
|
||||
portBindings map[nat.Port][]nat.PortBinding,
|
||||
) []swarm.PortConfig {
|
||||
ports := []swarm.PortConfig{}
|
||||
|
||||
for _, binding := range portBindings[port] {
|
||||
hostPort, _ := strconv.ParseUint(binding.HostPort, 10, 16)
|
||||
ports = append(ports, swarm.PortConfig{
|
||||
//TODO Name: ?
|
||||
Protocol: swarm.PortConfigProtocol(strings.ToLower(port.Proto())),
|
||||
TargetPort: uint32(port.Int()),
|
||||
PublishedPort: uint32(hostPort),
|
||||
})
|
||||
}
|
||||
return ports
|
||||
}
|
||||
|
||||
// ValidatePort validates a string is in the expected format for a port definition
|
||||
func ValidatePort(value string) (string, error) {
|
||||
portMappings, err := nat.ParsePortSpec(value)
|
||||
for _, portMapping := range portMappings {
|
||||
if portMapping.Binding.HostIP != "" {
|
||||
return "", fmt.Errorf("HostIP is not supported by a service.")
|
||||
}
|
||||
}
|
||||
return value, err
|
||||
}
|
||||
|
||||
type serviceOptions struct {
|
||||
name string
|
||||
labels opts.ListOpts
|
||||
image string
|
||||
command []string
|
||||
args []string
|
||||
env opts.ListOpts
|
||||
workdir string
|
||||
user string
|
||||
mounts MountOpt
|
||||
|
||||
resources resourceOptions
|
||||
stopGrace DurationOpt
|
||||
|
||||
replicas Uint64Opt
|
||||
mode string
|
||||
|
||||
restartPolicy restartPolicyOptions
|
||||
constraints []string
|
||||
update updateOptions
|
||||
networks []string
|
||||
endpoint endpointOptions
|
||||
}
|
||||
|
||||
func newServiceOptions() *serviceOptions {
|
||||
return &serviceOptions{
|
||||
labels: opts.NewListOpts(runconfigopts.ValidateEnv),
|
||||
env: opts.NewListOpts(runconfigopts.ValidateEnv),
|
||||
endpoint: endpointOptions{
|
||||
ports: opts.NewListOpts(ValidatePort),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) {
|
||||
var service swarm.ServiceSpec
|
||||
|
||||
service = swarm.ServiceSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: opts.name,
|
||||
Labels: runconfigopts.ConvertKVStringsToMap(opts.labels.GetAll()),
|
||||
},
|
||||
TaskTemplate: swarm.TaskSpec{
|
||||
ContainerSpec: swarm.ContainerSpec{
|
||||
Image: opts.image,
|
||||
Command: opts.command,
|
||||
Args: opts.args,
|
||||
Env: opts.env.GetAll(),
|
||||
Dir: opts.workdir,
|
||||
User: opts.user,
|
||||
Mounts: opts.mounts.Value(),
|
||||
StopGracePeriod: opts.stopGrace.Value(),
|
||||
},
|
||||
Resources: opts.resources.ToResourceRequirements(),
|
||||
RestartPolicy: opts.restartPolicy.ToRestartPolicy(),
|
||||
Placement: &swarm.Placement{
|
||||
Constraints: opts.constraints,
|
||||
},
|
||||
},
|
||||
Mode: swarm.ServiceMode{},
|
||||
UpdateConfig: &swarm.UpdateConfig{
|
||||
Parallelism: opts.update.parallelism,
|
||||
Delay: opts.update.delay,
|
||||
},
|
||||
Networks: convertNetworks(opts.networks),
|
||||
EndpointSpec: opts.endpoint.ToEndpointSpec(),
|
||||
}
|
||||
|
||||
switch opts.mode {
|
||||
case "global":
|
||||
if opts.replicas.Value() != nil {
|
||||
return service, fmt.Errorf("replicas can only be used with replicated mode")
|
||||
}
|
||||
|
||||
service.Mode.Global = &swarm.GlobalService{}
|
||||
case "replicated":
|
||||
service.Mode.Replicated = &swarm.ReplicatedService{
|
||||
Replicas: opts.replicas.Value(),
|
||||
}
|
||||
default:
|
||||
return service, fmt.Errorf("Unknown mode: %s", opts.mode)
|
||||
}
|
||||
return service, nil
|
||||
}
|
||||
|
||||
// addServiceFlags adds all flags that are common to both `create` and `update.
|
||||
// Any flags that are not common are added separately in the individual command
|
||||
func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) {
|
||||
flags := cmd.Flags()
|
||||
flags.StringVar(&opts.name, "name", "", "Service name")
|
||||
flags.VarP(&opts.labels, "label", "l", "Service labels")
|
||||
|
||||
flags.VarP(&opts.env, "env", "e", "Set environment variables")
|
||||
flags.StringVarP(&opts.workdir, "workdir", "w", "", "Working directory inside the container")
|
||||
flags.StringVarP(&opts.user, "user", "u", "", "Username or UID")
|
||||
flags.VarP(&opts.mounts, "mount", "m", "Attach a mount to the service")
|
||||
|
||||
flags.Var(&opts.resources.limitCPU, "limit-cpu", "Limit CPUs")
|
||||
flags.Var(&opts.resources.limitMemBytes, "limit-memory", "Limit Memory")
|
||||
flags.Var(&opts.resources.resCPU, "reserve-cpu", "Reserve CPUs")
|
||||
flags.Var(&opts.resources.resMemBytes, "reserve-memory", "Reserve Memory")
|
||||
flags.Var(&opts.stopGrace, "stop-grace-period", "Time to wait before force killing a container")
|
||||
|
||||
flags.StringVar(&opts.mode, "mode", "replicated", "Service mode (replicated or global)")
|
||||
flags.Var(&opts.replicas, "replicas", "Number of tasks")
|
||||
|
||||
flags.StringVar(&opts.restartPolicy.condition, "restart-condition", "", "Restart when condition is met (none, on_failure, or any)")
|
||||
flags.Var(&opts.restartPolicy.delay, "restart-delay", "Delay between restart attempts")
|
||||
flags.Var(&opts.restartPolicy.maxAttempts, "restart-max-attempts", "Maximum number of restarts before giving up")
|
||||
flags.Var(&opts.restartPolicy.window, "restart-window", "Window used to evalulate the restart policy")
|
||||
|
||||
flags.StringSliceVar(&opts.constraints, "constraint", []string{}, "Placement constraints")
|
||||
|
||||
flags.Uint64Var(&opts.update.parallelism, "update-parallelism", 1, "Maximum number of tasks updated simultaneously")
|
||||
flags.DurationVar(&opts.update.delay, "update-delay", time.Duration(0), "Delay between updates")
|
||||
|
||||
flags.StringSliceVar(&opts.networks, "network", []string{}, "Network attachments")
|
||||
flags.StringVar(&opts.endpoint.mode, "endpoint-mode", "", "Endpoint mode(Valid values: VIP, DNSRR)")
|
||||
flags.VarP(&opts.endpoint.ports, "publish", "p", "Publish a port as a node port")
|
||||
}
|
47
api/client/service/remove.go
Normal file
47
api/client/service/remove.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/client"
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func newRemoveCommand(dockerCli *client.DockerCli) *cobra.Command {
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "rm [OPTIONS] SERVICE",
|
||||
Aliases: []string{"remove"},
|
||||
Short: "Remove a service",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runRemove(dockerCli, args)
|
||||
},
|
||||
}
|
||||
cmd.Flags()
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runRemove(dockerCli *client.DockerCli, sids []string) error {
|
||||
client := dockerCli.Client()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
var errs []string
|
||||
for _, sid := range sids {
|
||||
err := client.ServiceRemove(ctx, sid)
|
||||
if err != nil {
|
||||
errs = append(errs, err.Error())
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(dockerCli.Out(), "%s\n", sid)
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf(strings.Join(errs, "\n"))
|
||||
}
|
||||
return nil
|
||||
}
|
86
api/client/service/scale.go
Normal file
86
api/client/service/scale.go
Normal file
|
@ -0,0 +1,86 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/docker/docker/api/client"
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newScaleCommand(dockerCli *client.DockerCli) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "scale SERVICE=SCALE [SERVICE=SCALE...]",
|
||||
Short: "Scale one or multiple services",
|
||||
Args: scaleArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runScale(dockerCli, args)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func scaleArgs(cmd *cobra.Command, args []string) error {
|
||||
if err := cli.RequiresMinArgs(1)(cmd, args); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, arg := range args {
|
||||
if parts := strings.SplitN(arg, "=", 2); len(parts) != 2 {
|
||||
return fmt.Errorf(
|
||||
"Invalid scale specifier '%s'.\nSee '%s --help'.\n\nUsage: %s\n\n%s",
|
||||
arg,
|
||||
cmd.CommandPath(),
|
||||
cmd.UseLine(),
|
||||
cmd.Short,
|
||||
)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runScale(dockerCli *client.DockerCli, args []string) error {
|
||||
var errors []string
|
||||
for _, arg := range args {
|
||||
parts := strings.SplitN(arg, "=", 2)
|
||||
serviceID, scale := parts[0], parts[1]
|
||||
if err := runServiceScale(dockerCli, serviceID, scale); err != nil {
|
||||
errors = append(errors, fmt.Sprintf("%s: %s", serviceID, err.Error()))
|
||||
}
|
||||
}
|
||||
|
||||
if len(errors) == 0 {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf(strings.Join(errors, "\n"))
|
||||
}
|
||||
|
||||
func runServiceScale(dockerCli *client.DockerCli, serviceID string, scale string) error {
|
||||
client := dockerCli.Client()
|
||||
ctx := context.Background()
|
||||
|
||||
service, err := client.ServiceInspect(ctx, serviceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
serviceMode := &service.Spec.Mode
|
||||
if serviceMode.Replicated == nil {
|
||||
return fmt.Errorf("scale can only be used with replicated mode")
|
||||
}
|
||||
uintScale, err := strconv.ParseUint(scale, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid replicas value %s: %s", scale, err.Error())
|
||||
}
|
||||
serviceMode.Replicated.Replicas = &uintScale
|
||||
|
||||
err = client.ServiceUpdate(ctx, service.ID, service.Version, service.Spec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(dockerCli.Out(), "%s scaled to %s\n", serviceID, scale)
|
||||
return nil
|
||||
}
|
65
api/client/service/tasks.go
Normal file
65
api/client/service/tasks.go
Normal file
|
@ -0,0 +1,65 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/docker/docker/api/client"
|
||||
"github.com/docker/docker/api/client/idresolver"
|
||||
"github.com/docker/docker/api/client/task"
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/opts"
|
||||
"github.com/docker/engine-api/types"
|
||||
"github.com/docker/engine-api/types/swarm"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type tasksOptions struct {
|
||||
serviceID string
|
||||
all bool
|
||||
noResolve bool
|
||||
filter opts.FilterOpt
|
||||
}
|
||||
|
||||
func newTasksCommand(dockerCli *client.DockerCli) *cobra.Command {
|
||||
opts := tasksOptions{filter: opts.NewFilterOpt()}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "tasks [OPTIONS] SERVICE",
|
||||
Short: "List the tasks of a service",
|
||||
Args: cli.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.serviceID = args[0]
|
||||
return runTasks(dockerCli, opts)
|
||||
},
|
||||
}
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&opts.all, "all", "a", false, "Display all tasks")
|
||||
flags.BoolVarP(&opts.noResolve, "no-resolve", "n", false, "Do not map IDs to Names")
|
||||
flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runTasks(dockerCli *client.DockerCli, opts tasksOptions) error {
|
||||
client := dockerCli.Client()
|
||||
ctx := context.Background()
|
||||
|
||||
service, err := client.ServiceInspect(ctx, opts.serviceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filter := opts.filter.Value()
|
||||
filter.Add("service", service.ID)
|
||||
if !opts.all && !filter.Include("desired_state") {
|
||||
filter.Add("desired_state", string(swarm.TaskStateRunning))
|
||||
filter.Add("desired_state", string(swarm.TaskStateAccepted))
|
||||
}
|
||||
|
||||
tasks, err := client.TaskList(ctx, types.TaskListOptions{Filter: filter})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return task.Print(dockerCli, ctx, tasks, idresolver.New(client, opts.noResolve))
|
||||
}
|
244
api/client/service/update.go
Normal file
244
api/client/service/update.go
Normal file
|
@ -0,0 +1,244 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/docker/docker/api/client"
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/opts"
|
||||
runconfigopts "github.com/docker/docker/runconfig/opts"
|
||||
"github.com/docker/engine-api/types/swarm"
|
||||
"github.com/docker/go-connections/nat"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
func newUpdateCommand(dockerCli *client.DockerCli) *cobra.Command {
|
||||
opts := newServiceOptions()
|
||||
var flags *pflag.FlagSet
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "update [OPTIONS] SERVICE",
|
||||
Short: "Update a service",
|
||||
Args: cli.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runUpdate(dockerCli, flags, args[0])
|
||||
},
|
||||
}
|
||||
|
||||
flags = cmd.Flags()
|
||||
flags.String("image", "", "Service image tag")
|
||||
flags.StringSlice("command", []string{}, "Service command")
|
||||
flags.StringSlice("arg", []string{}, "Service command args")
|
||||
addServiceFlags(cmd, opts)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runUpdate(dockerCli *client.DockerCli, flags *pflag.FlagSet, serviceID string) error {
|
||||
client := dockerCli.Client()
|
||||
ctx := context.Background()
|
||||
|
||||
service, err := client.ServiceInspect(ctx, serviceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = mergeService(&service.Spec, flags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = client.ServiceUpdate(ctx, service.ID, service.Version, service.Spec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(dockerCli.Out(), "%s\n", serviceID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func mergeService(spec *swarm.ServiceSpec, flags *pflag.FlagSet) error {
|
||||
|
||||
mergeString := func(flag string, field *string) {
|
||||
if flags.Changed(flag) {
|
||||
*field, _ = flags.GetString(flag)
|
||||
}
|
||||
}
|
||||
|
||||
mergeListOpts := func(flag string, field *[]string) {
|
||||
if flags.Changed(flag) {
|
||||
value := flags.Lookup(flag).Value.(*opts.ListOpts)
|
||||
*field = value.GetAll()
|
||||
}
|
||||
}
|
||||
|
||||
mergeSlice := func(flag string, field *[]string) {
|
||||
if flags.Changed(flag) {
|
||||
*field, _ = flags.GetStringSlice(flag)
|
||||
}
|
||||
}
|
||||
|
||||
mergeInt64Value := func(flag string, field *int64) {
|
||||
if flags.Changed(flag) {
|
||||
*field = flags.Lookup(flag).Value.(int64Value).Value()
|
||||
}
|
||||
}
|
||||
|
||||
mergeDuration := func(flag string, field *time.Duration) {
|
||||
if flags.Changed(flag) {
|
||||
*field, _ = flags.GetDuration(flag)
|
||||
}
|
||||
}
|
||||
|
||||
mergeDurationOpt := func(flag string, field *time.Duration) {
|
||||
if flags.Changed(flag) {
|
||||
*field = *flags.Lookup(flag).Value.(*DurationOpt).Value()
|
||||
}
|
||||
}
|
||||
|
||||
mergeUint64 := func(flag string, field *uint64) {
|
||||
if flags.Changed(flag) {
|
||||
*field, _ = flags.GetUint64(flag)
|
||||
}
|
||||
}
|
||||
|
||||
mergeUint64Opt := func(flag string, field *uint64) {
|
||||
if flags.Changed(flag) {
|
||||
*field = *flags.Lookup(flag).Value.(*Uint64Opt).Value()
|
||||
}
|
||||
}
|
||||
|
||||
cspec := &spec.TaskTemplate.ContainerSpec
|
||||
task := &spec.TaskTemplate
|
||||
mergeString("name", &spec.Name)
|
||||
mergeLabels(flags, &spec.Labels)
|
||||
mergeString("image", &cspec.Image)
|
||||
mergeSlice("command", &cspec.Command)
|
||||
mergeSlice("arg", &cspec.Command)
|
||||
mergeListOpts("env", &cspec.Env)
|
||||
mergeString("workdir", &cspec.Dir)
|
||||
mergeString("user", &cspec.User)
|
||||
mergeMounts(flags, &cspec.Mounts)
|
||||
|
||||
mergeInt64Value("limit-cpu", &task.Resources.Limits.NanoCPUs)
|
||||
mergeInt64Value("limit-memory", &task.Resources.Limits.MemoryBytes)
|
||||
mergeInt64Value("reserve-cpu", &task.Resources.Reservations.NanoCPUs)
|
||||
mergeInt64Value("reserve-memory", &task.Resources.Reservations.MemoryBytes)
|
||||
|
||||
mergeDurationOpt("stop-grace-period", cspec.StopGracePeriod)
|
||||
|
||||
if flags.Changed("restart-policy-condition") {
|
||||
value, _ := flags.GetString("restart-policy-condition")
|
||||
task.RestartPolicy.Condition = swarm.RestartPolicyCondition(value)
|
||||
}
|
||||
mergeDurationOpt("restart-policy-delay", task.RestartPolicy.Delay)
|
||||
mergeUint64Opt("restart-policy-max-attempts", task.RestartPolicy.MaxAttempts)
|
||||
mergeDurationOpt("restart-policy-window", task.RestartPolicy.Window)
|
||||
mergeSlice("constraint", &task.Placement.Constraints)
|
||||
|
||||
if err := mergeMode(flags, &spec.Mode); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mergeUint64("updateconfig-parallelism", &spec.UpdateConfig.Parallelism)
|
||||
mergeDuration("updateconfig-delay", &spec.UpdateConfig.Delay)
|
||||
|
||||
mergeNetworks(flags, &spec.Networks)
|
||||
if flags.Changed("endpoint-mode") {
|
||||
value, _ := flags.GetString("endpoint-mode")
|
||||
spec.EndpointSpec.Mode = swarm.ResolutionMode(value)
|
||||
}
|
||||
|
||||
mergePorts(flags, &spec.EndpointSpec.Ports)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func mergeLabels(flags *pflag.FlagSet, field *map[string]string) {
|
||||
if !flags.Changed("label") {
|
||||
return
|
||||
}
|
||||
|
||||
if *field == nil {
|
||||
*field = make(map[string]string)
|
||||
}
|
||||
|
||||
values := flags.Lookup("label").Value.(*opts.ListOpts).GetAll()
|
||||
for key, value := range runconfigopts.ConvertKVStringsToMap(values) {
|
||||
(*field)[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: should this override by destination path, or does swarm handle that?
|
||||
func mergeMounts(flags *pflag.FlagSet, mounts *[]swarm.Mount) {
|
||||
if !flags.Changed("mount") {
|
||||
return
|
||||
}
|
||||
|
||||
values := flags.Lookup("mount").Value.(*MountOpt).Value()
|
||||
*mounts = append(*mounts, values...)
|
||||
}
|
||||
|
||||
// TODO: should this override by name, or does swarm handle that?
|
||||
func mergePorts(flags *pflag.FlagSet, portConfig *[]swarm.PortConfig) {
|
||||
if !flags.Changed("ports") {
|
||||
return
|
||||
}
|
||||
|
||||
values := flags.Lookup("ports").Value.(*opts.ListOpts).GetAll()
|
||||
ports, portBindings, _ := nat.ParsePortSpecs(values)
|
||||
|
||||
for port := range ports {
|
||||
*portConfig = append(*portConfig, convertPortToPortConfig(port, portBindings)...)
|
||||
}
|
||||
}
|
||||
|
||||
func mergeNetworks(flags *pflag.FlagSet, attachments *[]swarm.NetworkAttachmentConfig) {
|
||||
if !flags.Changed("network") {
|
||||
return
|
||||
}
|
||||
networks, _ := flags.GetStringSlice("network")
|
||||
for _, network := range networks {
|
||||
*attachments = append(*attachments, swarm.NetworkAttachmentConfig{Target: network})
|
||||
}
|
||||
}
|
||||
|
||||
func mergeMode(flags *pflag.FlagSet, serviceMode *swarm.ServiceMode) error {
|
||||
if !flags.Changed("mode") && !flags.Changed("scale") {
|
||||
return nil
|
||||
}
|
||||
|
||||
var mode string
|
||||
if flags.Changed("mode") {
|
||||
mode, _ = flags.GetString("mode")
|
||||
}
|
||||
|
||||
if !(mode == "replicated" || serviceMode.Replicated != nil) && flags.Changed("replicas") {
|
||||
return fmt.Errorf("replicas can only be used with replicated mode")
|
||||
}
|
||||
|
||||
if mode == "global" {
|
||||
serviceMode.Replicated = nil
|
||||
serviceMode.Global = &swarm.GlobalService{}
|
||||
return nil
|
||||
}
|
||||
|
||||
if flags.Changed("replicas") {
|
||||
replicas := flags.Lookup("replicas").Value.(*Uint64Opt).Value()
|
||||
serviceMode.Replicated = &swarm.ReplicatedService{Replicas: replicas}
|
||||
serviceMode.Global = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
if mode == "replicated" {
|
||||
if serviceMode.Replicated != nil {
|
||||
return nil
|
||||
}
|
||||
serviceMode.Replicated = &swarm.ReplicatedService{Replicas: &DefaultReplicas}
|
||||
serviceMode.Global = nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
30
api/client/swarm/cmd.go
Normal file
30
api/client/swarm/cmd.go
Normal file
|
@ -0,0 +1,30 @@
|
|||
package swarm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/docker/api/client"
|
||||
"github.com/docker/docker/cli"
|
||||
)
|
||||
|
||||
// NewSwarmCommand returns a cobra command for `swarm` subcommands
|
||||
func NewSwarmCommand(dockerCli *client.DockerCli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "swarm",
|
||||
Short: "Manage docker swarm",
|
||||
Args: cli.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
|
||||
},
|
||||
}
|
||||
cmd.AddCommand(
|
||||
newInitCommand(dockerCli),
|
||||
newJoinCommand(dockerCli),
|
||||
newUpdateCommand(dockerCli),
|
||||
newLeaveCommand(dockerCli),
|
||||
newInspectCommand(dockerCli),
|
||||
)
|
||||
return cmd
|
||||
}
|
61
api/client/swarm/init.go
Normal file
61
api/client/swarm/init.go
Normal file
|
@ -0,0 +1,61 @@
|
|||
package swarm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/docker/docker/api/client"
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/engine-api/types/swarm"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type initOptions struct {
|
||||
listenAddr NodeAddrOption
|
||||
autoAccept AutoAcceptOption
|
||||
forceNewCluster bool
|
||||
secret string
|
||||
}
|
||||
|
||||
func newInitCommand(dockerCli *client.DockerCli) *cobra.Command {
|
||||
opts := initOptions{
|
||||
listenAddr: NewNodeAddrOption(),
|
||||
autoAccept: NewAutoAcceptOption(),
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "init",
|
||||
Short: "Initialize a Swarm.",
|
||||
Args: cli.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runInit(dockerCli, opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.Var(&opts.listenAddr, "listen-addr", "Listen address")
|
||||
flags.Var(&opts.autoAccept, "auto-accept", "Auto acceptance policy (worker, manager, or none)")
|
||||
flags.StringVar(&opts.secret, "secret", "", "Set secret value needed to accept nodes into cluster")
|
||||
flags.BoolVar(&opts.forceNewCluster, "force-new-cluster", false, "Force create a new cluster from current state.")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runInit(dockerCli *client.DockerCli, opts initOptions) error {
|
||||
client := dockerCli.Client()
|
||||
ctx := context.Background()
|
||||
|
||||
req := swarm.InitRequest{
|
||||
ListenAddr: opts.listenAddr.String(),
|
||||
ForceNewCluster: opts.forceNewCluster,
|
||||
}
|
||||
|
||||
req.Spec.AcceptancePolicy.Policies = opts.autoAccept.Policies(opts.secret)
|
||||
|
||||
nodeID, err := client.SwarmInit(ctx, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("Swarm initialized: current node (%s) is now a manager.\n", nodeID)
|
||||
return nil
|
||||
}
|
56
api/client/swarm/inspect.go
Normal file
56
api/client/swarm/inspect.go
Normal file
|
@ -0,0 +1,56 @@
|
|||
package swarm
|
||||
|
||||
import (
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/docker/docker/api/client"
|
||||
"github.com/docker/docker/api/client/inspect"
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type inspectOptions struct {
|
||||
format string
|
||||
// pretty bool
|
||||
}
|
||||
|
||||
func newInspectCommand(dockerCli *client.DockerCli) *cobra.Command {
|
||||
var opts inspectOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "inspect [OPTIONS]",
|
||||
Short: "Inspect the Swarm",
|
||||
Args: cli.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// if opts.pretty && len(opts.format) > 0 {
|
||||
// return fmt.Errorf("--format is incompatible with human friendly format")
|
||||
// }
|
||||
return runInspect(dockerCli, opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template")
|
||||
//flags.BoolVarP(&opts.pretty, "pretty", "h", false, "Print the information in a human friendly format.")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runInspect(dockerCli *client.DockerCli, opts inspectOptions) error {
|
||||
client := dockerCli.Client()
|
||||
ctx := context.Background()
|
||||
|
||||
swarm, err := client.SwarmInspect(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
getRef := func(_ string) (interface{}, []byte, error) {
|
||||
return swarm, nil, nil
|
||||
}
|
||||
|
||||
// if !opts.pretty {
|
||||
return inspect.Inspect(dockerCli.Out(), []string{""}, opts.format, getRef)
|
||||
// }
|
||||
|
||||
//return printHumanFriendly(dockerCli.Out(), opts.refs, getRef)
|
||||
}
|
65
api/client/swarm/join.go
Normal file
65
api/client/swarm/join.go
Normal file
|
@ -0,0 +1,65 @@
|
|||
package swarm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/docker/api/client"
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/engine-api/types/swarm"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type joinOptions struct {
|
||||
remote string
|
||||
listenAddr NodeAddrOption
|
||||
manager bool
|
||||
secret string
|
||||
CACertHash string
|
||||
}
|
||||
|
||||
func newJoinCommand(dockerCli *client.DockerCli) *cobra.Command {
|
||||
opts := joinOptions{
|
||||
listenAddr: NodeAddrOption{addr: defaultListenAddr},
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "join [OPTIONS] HOST:PORT",
|
||||
Short: "Join a Swarm as a node and/or manager.",
|
||||
Args: cli.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.remote = args[0]
|
||||
return runJoin(dockerCli, opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.Var(&opts.listenAddr, "listen-addr", "Listen address")
|
||||
flags.BoolVar(&opts.manager, "manager", false, "Try joining as a manager.")
|
||||
flags.StringVar(&opts.secret, "secret", "", "Secret for node acceptance")
|
||||
flags.StringVar(&opts.CACertHash, "ca-hash", "", "Hash of the Root Certificate Authority certificate used for trusted join")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runJoin(dockerCli *client.DockerCli, opts joinOptions) error {
|
||||
client := dockerCli.Client()
|
||||
ctx := context.Background()
|
||||
|
||||
req := swarm.JoinRequest{
|
||||
Manager: opts.manager,
|
||||
Secret: opts.secret,
|
||||
ListenAddr: opts.listenAddr.String(),
|
||||
RemoteAddrs: []string{opts.remote},
|
||||
CACertHash: opts.CACertHash,
|
||||
}
|
||||
err := client.SwarmJoin(ctx, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if opts.manager {
|
||||
fmt.Fprintln(dockerCli.Out(), "This node joined a Swarm as a manager.")
|
||||
} else {
|
||||
fmt.Fprintln(dockerCli.Out(), "This node joined a Swarm as a worker.")
|
||||
}
|
||||
return nil
|
||||
}
|
44
api/client/swarm/leave.go
Normal file
44
api/client/swarm/leave.go
Normal file
|
@ -0,0 +1,44 @@
|
|||
package swarm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/docker/docker/api/client"
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type leaveOptions struct {
|
||||
force bool
|
||||
}
|
||||
|
||||
func newLeaveCommand(dockerCli *client.DockerCli) *cobra.Command {
|
||||
opts := leaveOptions{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "leave",
|
||||
Short: "Leave a Swarm.",
|
||||
Args: cli.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runLeave(dockerCli, opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVar(&opts.force, "force", false, "Force leave ignoring warnings.")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runLeave(dockerCli *client.DockerCli, opts leaveOptions) error {
|
||||
client := dockerCli.Client()
|
||||
ctx := context.Background()
|
||||
|
||||
if err := client.SwarmLeave(ctx, opts.force); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintln(dockerCli.Out(), "Node left the default swarm.")
|
||||
return nil
|
||||
}
|
120
api/client/swarm/opts.go
Normal file
120
api/client/swarm/opts.go
Normal file
|
@ -0,0 +1,120 @@
|
|||
package swarm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/engine-api/types/swarm"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultListenAddr = "0.0.0.0:2377"
|
||||
// WORKER constant for worker name
|
||||
WORKER = "WORKER"
|
||||
// MANAGER constant for manager name
|
||||
MANAGER = "MANAGER"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultPolicies = []swarm.Policy{
|
||||
{Role: WORKER, Autoaccept: true},
|
||||
{Role: MANAGER, Autoaccept: false},
|
||||
}
|
||||
)
|
||||
|
||||
// NodeAddrOption is a pflag.Value for listen and remote addresses
|
||||
type NodeAddrOption struct {
|
||||
addr string
|
||||
}
|
||||
|
||||
// String prints the representation of this flag
|
||||
func (a *NodeAddrOption) String() string {
|
||||
return a.addr
|
||||
}
|
||||
|
||||
// Set the value for this flag
|
||||
func (a *NodeAddrOption) Set(value string) error {
|
||||
if !strings.Contains(value, ":") {
|
||||
return fmt.Errorf("Invalud url, a host and port are required")
|
||||
}
|
||||
|
||||
parts := strings.Split(value, ":")
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("Invalud url, too many colons")
|
||||
}
|
||||
|
||||
a.addr = value
|
||||
return nil
|
||||
}
|
||||
|
||||
// Type returns the type of this flag
|
||||
func (a *NodeAddrOption) Type() string {
|
||||
return "node-addr"
|
||||
}
|
||||
|
||||
// NewNodeAddrOption returns a new node address option
|
||||
func NewNodeAddrOption() NodeAddrOption {
|
||||
return NodeAddrOption{addr: defaultListenAddr}
|
||||
}
|
||||
|
||||
// AutoAcceptOption is a value type for auto-accept policy
|
||||
type AutoAcceptOption struct {
|
||||
values map[string]bool
|
||||
}
|
||||
|
||||
// String prints a string representation of this option
|
||||
func (o *AutoAcceptOption) String() string {
|
||||
keys := []string{}
|
||||
for key := range o.values {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
return strings.Join(keys, " ")
|
||||
}
|
||||
|
||||
// Set sets a new value on this option
|
||||
func (o *AutoAcceptOption) Set(value string) error {
|
||||
value = strings.ToUpper(value)
|
||||
switch value {
|
||||
case "", "NONE":
|
||||
if accept, ok := o.values[WORKER]; ok && accept {
|
||||
return fmt.Errorf("value NONE is incompatible with %s", WORKER)
|
||||
}
|
||||
if accept, ok := o.values[MANAGER]; ok && accept {
|
||||
return fmt.Errorf("value NONE is incompatible with %s", MANAGER)
|
||||
}
|
||||
o.values[WORKER] = false
|
||||
o.values[MANAGER] = false
|
||||
case WORKER, MANAGER:
|
||||
if accept, ok := o.values[value]; ok && !accept {
|
||||
return fmt.Errorf("value NONE is incompatible with %s", value)
|
||||
}
|
||||
o.values[value] = true
|
||||
default:
|
||||
return fmt.Errorf("must be one of %s, %s, NONE", WORKER, MANAGER)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Type returns the type of this option
|
||||
func (o *AutoAcceptOption) Type() string {
|
||||
return "auto-accept"
|
||||
}
|
||||
|
||||
// Policies returns a representation of this option for the api
|
||||
func (o *AutoAcceptOption) Policies(secret string) []swarm.Policy {
|
||||
policies := []swarm.Policy{}
|
||||
for _, p := range defaultPolicies {
|
||||
if len(o.values) != 0 {
|
||||
p.Autoaccept = o.values[string(p.Role)]
|
||||
}
|
||||
p.Secret = secret
|
||||
policies = append(policies, p)
|
||||
}
|
||||
return policies
|
||||
}
|
||||
|
||||
// NewAutoAcceptOption returns a new auto-accept option
|
||||
func NewAutoAcceptOption() AutoAcceptOption {
|
||||
return AutoAcceptOption{values: make(map[string]bool)}
|
||||
}
|
93
api/client/swarm/update.go
Normal file
93
api/client/swarm/update.go
Normal file
|
@ -0,0 +1,93 @@
|
|||
package swarm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/docker/docker/api/client"
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/engine-api/types/swarm"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
type updateOptions struct {
|
||||
autoAccept AutoAcceptOption
|
||||
secret string
|
||||
taskHistoryLimit int64
|
||||
heartbeatPeriod uint64
|
||||
}
|
||||
|
||||
func newUpdateCommand(dockerCli *client.DockerCli) *cobra.Command {
|
||||
opts := updateOptions{autoAccept: NewAutoAcceptOption()}
|
||||
var flags *pflag.FlagSet
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "update",
|
||||
Short: "update the Swarm.",
|
||||
Args: cli.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runUpdate(dockerCli, flags, opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags = cmd.Flags()
|
||||
flags.Var(&opts.autoAccept, "auto-accept", "Auto acceptance policy (worker, manager or none)")
|
||||
flags.StringVar(&opts.secret, "secret", "", "Set secret value needed to accept nodes into cluster")
|
||||
flags.Int64Var(&opts.taskHistoryLimit, "task-history-limit", 10, "Task history retention limit")
|
||||
flags.Uint64Var(&opts.heartbeatPeriod, "dispatcher-heartbeat-period", 5000000000, "Dispatcher heartbeat period")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runUpdate(dockerCli *client.DockerCli, flags *pflag.FlagSet, opts updateOptions) error {
|
||||
client := dockerCli.Client()
|
||||
ctx := context.Background()
|
||||
|
||||
swarm, err := client.SwarmInspect(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = mergeSwarm(&swarm, flags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = client.SwarmUpdate(ctx, swarm.Version, swarm.Spec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("Swarm updated.")
|
||||
return nil
|
||||
}
|
||||
|
||||
func mergeSwarm(swarm *swarm.Swarm, flags *pflag.FlagSet) error {
|
||||
spec := &swarm.Spec
|
||||
|
||||
if flags.Changed("auto-accept") {
|
||||
value := flags.Lookup("auto-accept").Value.(*AutoAcceptOption)
|
||||
if len(spec.AcceptancePolicy.Policies) > 0 {
|
||||
spec.AcceptancePolicy.Policies = value.Policies(spec.AcceptancePolicy.Policies[0].Secret)
|
||||
} else {
|
||||
spec.AcceptancePolicy.Policies = value.Policies("")
|
||||
}
|
||||
}
|
||||
|
||||
if flags.Changed("secret") {
|
||||
secret, _ := flags.GetString("secret")
|
||||
for _, policy := range spec.AcceptancePolicy.Policies {
|
||||
policy.Secret = secret
|
||||
}
|
||||
}
|
||||
|
||||
if flags.Changed("task-history-limit") {
|
||||
spec.Orchestration.TaskHistoryRetentionLimit, _ = flags.GetInt64("task-history-limit")
|
||||
}
|
||||
|
||||
if flags.Changed("dispatcher-heartbeat-period") {
|
||||
spec.Dispatcher.HeartbeatPeriod, _ = flags.GetUint64("dispatcher-heartbeat-period")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
20
api/client/tag.go
Normal file
20
api/client/tag.go
Normal file
|
@ -0,0 +1,20 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"golang.org/x/net/context"
|
||||
|
||||
Cli "github.com/docker/docker/cli"
|
||||
flag "github.com/docker/docker/pkg/mflag"
|
||||
)
|
||||
|
||||
// CmdTag tags an image into a repository.
|
||||
//
|
||||
// Usage: docker tag [OPTIONS] IMAGE[:TAG] [REGISTRYHOST/][USERNAME/]NAME[:TAG]
|
||||
func (cli *DockerCli) CmdTag(args ...string) error {
|
||||
cmd := Cli.Subcmd("tag", []string{"IMAGE[:TAG] [REGISTRYHOST/][USERNAME/]NAME[:TAG]"}, Cli.DockerCommands["tag"].Description, true)
|
||||
cmd.Require(flag.Exact, 2)
|
||||
|
||||
cmd.ParseFlags(args, true)
|
||||
|
||||
return cli.client.ImageTag(context.Background(), cmd.Arg(0), cmd.Arg(1))
|
||||
}
|
79
api/client/task/print.go
Normal file
79
api/client/task/print.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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"},
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in a new issue