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>
| 1 | 1 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,70 @@ |
| 0 |
+package idresolver |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ |
|
| 5 |
+ "golang.org/x/net/context" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/docker/engine-api/client" |
|
| 8 |
+ "github.com/docker/engine-api/types/swarm" |
|
| 9 |
+) |
|
| 10 |
+ |
|
| 11 |
+// IDResolver provides ID to Name resolution. |
|
| 12 |
+type IDResolver struct {
|
|
| 13 |
+ client client.APIClient |
|
| 14 |
+ noResolve bool |
|
| 15 |
+ cache map[string]string |
|
| 16 |
+} |
|
| 17 |
+ |
|
| 18 |
+// New creates a new IDResolver. |
|
| 19 |
+func New(client client.APIClient, noResolve bool) *IDResolver {
|
|
| 20 |
+ return &IDResolver{
|
|
| 21 |
+ client: client, |
|
| 22 |
+ noResolve: noResolve, |
|
| 23 |
+ cache: make(map[string]string), |
|
| 24 |
+ } |
|
| 25 |
+} |
|
| 26 |
+ |
|
| 27 |
+func (r *IDResolver) get(ctx context.Context, t interface{}, id string) (string, error) {
|
|
| 28 |
+ switch t.(type) {
|
|
| 29 |
+ case swarm.Node: |
|
| 30 |
+ node, err := r.client.NodeInspect(ctx, id) |
|
| 31 |
+ if err != nil {
|
|
| 32 |
+ return id, nil |
|
| 33 |
+ } |
|
| 34 |
+ if node.Spec.Annotations.Name != "" {
|
|
| 35 |
+ return node.Spec.Annotations.Name, nil |
|
| 36 |
+ } |
|
| 37 |
+ if node.Description.Hostname != "" {
|
|
| 38 |
+ return node.Description.Hostname, nil |
|
| 39 |
+ } |
|
| 40 |
+ return id, nil |
|
| 41 |
+ case swarm.Service: |
|
| 42 |
+ service, err := r.client.ServiceInspect(ctx, id) |
|
| 43 |
+ if err != nil {
|
|
| 44 |
+ return id, nil |
|
| 45 |
+ } |
|
| 46 |
+ return service.Spec.Annotations.Name, nil |
|
| 47 |
+ default: |
|
| 48 |
+ return "", fmt.Errorf("unsupported type")
|
|
| 49 |
+ } |
|
| 50 |
+ |
|
| 51 |
+} |
|
| 52 |
+ |
|
| 53 |
+// Resolve will attempt to resolve an ID to a Name by querying the manager. |
|
| 54 |
+// Results are stored into a cache. |
|
| 55 |
+// If the `-n` flag is used in the command-line, resolution is disabled. |
|
| 56 |
+func (r *IDResolver) Resolve(ctx context.Context, t interface{}, id string) (string, error) {
|
|
| 57 |
+ if r.noResolve {
|
|
| 58 |
+ return id, nil |
|
| 59 |
+ } |
|
| 60 |
+ if name, ok := r.cache[id]; ok {
|
|
| 61 |
+ return name, nil |
|
| 62 |
+ } |
|
| 63 |
+ name, err := r.get(ctx, t, id) |
|
| 64 |
+ if err != nil {
|
|
| 65 |
+ return "", err |
|
| 66 |
+ } |
|
| 67 |
+ r.cache[id] = name |
|
| 68 |
+ return name, nil |
|
| 69 |
+} |
| ... | ... |
@@ -10,6 +10,7 @@ import ( |
| 10 | 10 |
"github.com/docker/docker/pkg/ioutils" |
| 11 | 11 |
flag "github.com/docker/docker/pkg/mflag" |
| 12 | 12 |
"github.com/docker/docker/utils" |
| 13 |
+ "github.com/docker/engine-api/types/swarm" |
|
| 13 | 14 |
"github.com/docker/go-units" |
| 14 | 15 |
) |
| 15 | 16 |
|
| ... | ... |
@@ -68,6 +69,21 @@ func (cli *DockerCli) CmdInfo(args ...string) error {
|
| 68 | 68 |
fmt.Fprintf(cli.out, "\n") |
| 69 | 69 |
} |
| 70 | 70 |
|
| 71 |
+ fmt.Fprintf(cli.out, "Swarm: %v\n", info.Swarm.LocalNodeState) |
|
| 72 |
+ if info.Swarm.LocalNodeState != swarm.LocalNodeStateInactive {
|
|
| 73 |
+ fmt.Fprintf(cli.out, " NodeID: %s\n", info.Swarm.NodeID) |
|
| 74 |
+ if info.Swarm.Error != "" {
|
|
| 75 |
+ fmt.Fprintf(cli.out, " Error: %v\n", info.Swarm.Error) |
|
| 76 |
+ } |
|
| 77 |
+ if info.Swarm.ControlAvailable {
|
|
| 78 |
+ fmt.Fprintf(cli.out, " IsManager: Yes\n") |
|
| 79 |
+ fmt.Fprintf(cli.out, " Managers: %d\n", info.Swarm.Managers) |
|
| 80 |
+ fmt.Fprintf(cli.out, " Nodes: %d\n", info.Swarm.Nodes) |
|
| 81 |
+ ioutils.FprintfIfNotEmpty(cli.out, " CACertHash: %s\n", info.Swarm.CACertHash) |
|
| 82 |
+ } else {
|
|
| 83 |
+ fmt.Fprintf(cli.out, " IsManager: No\n") |
|
| 84 |
+ } |
|
| 85 |
+ } |
|
| 71 | 86 |
ioutils.FprintfIfNotEmpty(cli.out, "Kernel Version: %s\n", info.KernelVersion) |
| 72 | 87 |
ioutils.FprintfIfNotEmpty(cli.out, "Operating System: %s\n", info.OperatingSystem) |
| 73 | 88 |
ioutils.FprintfIfNotEmpty(cli.out, "OSType: %s\n", info.OSType) |
| ... | ... |
@@ -11,19 +11,19 @@ import ( |
| 11 | 11 |
"github.com/docker/engine-api/client" |
| 12 | 12 |
) |
| 13 | 13 |
|
| 14 |
-// CmdInspect displays low-level information on one or more containers or images. |
|
| 14 |
+// CmdInspect displays low-level information on one or more containers, images or tasks. |
|
| 15 | 15 |
// |
| 16 |
-// Usage: docker inspect [OPTIONS] CONTAINER|IMAGE [CONTAINER|IMAGE...] |
|
| 16 |
+// Usage: docker inspect [OPTIONS] CONTAINER|IMAGE|TASK [CONTAINER|IMAGE|TASK...] |
|
| 17 | 17 |
func (cli *DockerCli) CmdInspect(args ...string) error {
|
| 18 |
- cmd := Cli.Subcmd("inspect", []string{"CONTAINER|IMAGE [CONTAINER|IMAGE...]"}, Cli.DockerCommands["inspect"].Description, true)
|
|
| 18 |
+ cmd := Cli.Subcmd("inspect", []string{"CONTAINER|IMAGE|TASK [CONTAINER|IMAGE|TASK...]"}, Cli.DockerCommands["inspect"].Description, true)
|
|
| 19 | 19 |
tmplStr := cmd.String([]string{"f", "-format"}, "", "Format the output using the given go template")
|
| 20 |
- inspectType := cmd.String([]string{"-type"}, "", "Return JSON for specified type, (e.g image or container)")
|
|
| 20 |
+ inspectType := cmd.String([]string{"-type"}, "", "Return JSON for specified type, (e.g image, container or task)")
|
|
| 21 | 21 |
size := cmd.Bool([]string{"s", "-size"}, false, "Display total file sizes if the type is container")
|
| 22 | 22 |
cmd.Require(flag.Min, 1) |
| 23 | 23 |
|
| 24 | 24 |
cmd.ParseFlags(args, true) |
| 25 | 25 |
|
| 26 |
- if *inspectType != "" && *inspectType != "container" && *inspectType != "image" {
|
|
| 26 |
+ if *inspectType != "" && *inspectType != "container" && *inspectType != "image" && *inspectType != "task" {
|
|
| 27 | 27 |
return fmt.Errorf("%q is not a valid value for --type", *inspectType)
|
| 28 | 28 |
} |
| 29 | 29 |
|
| ... | ... |
@@ -35,6 +35,11 @@ func (cli *DockerCli) CmdInspect(args ...string) error {
|
| 35 | 35 |
elementSearcher = cli.inspectContainers(ctx, *size) |
| 36 | 36 |
case "image": |
| 37 | 37 |
elementSearcher = cli.inspectImages(ctx, *size) |
| 38 |
+ case "task": |
|
| 39 |
+ if *size {
|
|
| 40 |
+ fmt.Fprintln(cli.err, "WARNING: --size ignored for tasks") |
|
| 41 |
+ } |
|
| 42 |
+ elementSearcher = cli.inspectTasks(ctx) |
|
| 38 | 43 |
default: |
| 39 | 44 |
elementSearcher = cli.inspectAll(ctx, *size) |
| 40 | 45 |
} |
| ... | ... |
@@ -54,6 +59,12 @@ func (cli *DockerCli) inspectImages(ctx context.Context, getSize bool) inspect.G |
| 54 | 54 |
} |
| 55 | 55 |
} |
| 56 | 56 |
|
| 57 |
+func (cli *DockerCli) inspectTasks(ctx context.Context) inspect.GetRefFunc {
|
|
| 58 |
+ return func(ref string) (interface{}, []byte, error) {
|
|
| 59 |
+ return cli.client.TaskInspectWithRaw(ctx, ref) |
|
| 60 |
+ } |
|
| 61 |
+} |
|
| 62 |
+ |
|
| 57 | 63 |
func (cli *DockerCli) inspectAll(ctx context.Context, getSize bool) inspect.GetRefFunc {
|
| 58 | 64 |
return func(ref string) (interface{}, []byte, error) {
|
| 59 | 65 |
c, rawContainer, err := cli.client.ContainerInspectWithRaw(ctx, ref, getSize) |
| ... | ... |
@@ -63,7 +74,15 @@ func (cli *DockerCli) inspectAll(ctx context.Context, getSize bool) inspect.GetR |
| 63 | 63 |
i, rawImage, err := cli.client.ImageInspectWithRaw(ctx, ref, getSize) |
| 64 | 64 |
if err != nil {
|
| 65 | 65 |
if client.IsErrImageNotFound(err) {
|
| 66 |
- return nil, nil, fmt.Errorf("Error: No such image or container: %s", ref)
|
|
| 66 |
+ // Search for task with that id if an image doesn't exists. |
|
| 67 |
+ t, rawTask, err := cli.client.TaskInspectWithRaw(ctx, ref) |
|
| 68 |
+ if err != nil {
|
|
| 69 |
+ return nil, nil, fmt.Errorf("Error: No such image, container or task: %s", ref)
|
|
| 70 |
+ } |
|
| 71 |
+ if getSize {
|
|
| 72 |
+ fmt.Fprintln(cli.err, "WARNING: --size ignored for tasks") |
|
| 73 |
+ } |
|
| 74 |
+ return t, rawTask, nil |
|
| 67 | 75 |
} |
| 68 | 76 |
return nil, nil, err |
| 69 | 77 |
} |
| ... | ... |
@@ -71,7 +71,7 @@ func runList(dockerCli *client.DockerCli, opts listOptions) error {
|
| 71 | 71 |
|
| 72 | 72 |
w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0) |
| 73 | 73 |
if !opts.quiet {
|
| 74 |
- fmt.Fprintf(w, "NETWORK ID\tNAME\tDRIVER") |
|
| 74 |
+ fmt.Fprintf(w, "NETWORK ID\tNAME\tDRIVER\tSCOPE") |
|
| 75 | 75 |
fmt.Fprintf(w, "\n") |
| 76 | 76 |
} |
| 77 | 77 |
|
| ... | ... |
@@ -79,6 +79,8 @@ func runList(dockerCli *client.DockerCli, opts listOptions) error {
|
| 79 | 79 |
for _, networkResource := range networkResources {
|
| 80 | 80 |
ID := networkResource.ID |
| 81 | 81 |
netName := networkResource.Name |
| 82 |
+ driver := networkResource.Driver |
|
| 83 |
+ scope := networkResource.Scope |
|
| 82 | 84 |
if !opts.noTrunc {
|
| 83 | 85 |
ID = stringid.TruncateID(ID) |
| 84 | 86 |
} |
| ... | ... |
@@ -86,11 +88,11 @@ func runList(dockerCli *client.DockerCli, opts listOptions) error {
|
| 86 | 86 |
fmt.Fprintln(w, ID) |
| 87 | 87 |
continue |
| 88 | 88 |
} |
| 89 |
- driver := networkResource.Driver |
|
| 90 |
- fmt.Fprintf(w, "%s\t%s\t%s\t", |
|
| 89 |
+ fmt.Fprintf(w, "%s\t%s\t%s\t%s\t", |
|
| 91 | 90 |
ID, |
| 92 | 91 |
netName, |
| 93 |
- driver) |
|
| 92 |
+ driver, |
|
| 93 |
+ scope) |
|
| 94 | 94 |
fmt.Fprint(w, "\n") |
| 95 | 95 |
} |
| 96 | 96 |
w.Flush() |
| 97 | 97 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,40 @@ |
| 0 |
+package node |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ |
|
| 5 |
+ "github.com/docker/docker/api/client" |
|
| 6 |
+ "github.com/docker/docker/cli" |
|
| 7 |
+ "github.com/docker/engine-api/types/swarm" |
|
| 8 |
+ "github.com/spf13/cobra" |
|
| 9 |
+ "github.com/spf13/pflag" |
|
| 10 |
+) |
|
| 11 |
+ |
|
| 12 |
+func newAcceptCommand(dockerCli *client.DockerCli) *cobra.Command {
|
|
| 13 |
+ var flags *pflag.FlagSet |
|
| 14 |
+ |
|
| 15 |
+ cmd := &cobra.Command{
|
|
| 16 |
+ Use: "accept NODE [NODE...]", |
|
| 17 |
+ Short: "Accept a node in the swarm", |
|
| 18 |
+ Args: cli.RequiresMinArgs(1), |
|
| 19 |
+ RunE: func(cmd *cobra.Command, args []string) error {
|
|
| 20 |
+ return runAccept(dockerCli, flags, args) |
|
| 21 |
+ }, |
|
| 22 |
+ } |
|
| 23 |
+ |
|
| 24 |
+ flags = cmd.Flags() |
|
| 25 |
+ return cmd |
|
| 26 |
+} |
|
| 27 |
+ |
|
| 28 |
+func runAccept(dockerCli *client.DockerCli, flags *pflag.FlagSet, args []string) error {
|
|
| 29 |
+ for _, id := range args {
|
|
| 30 |
+ if err := runUpdate(dockerCli, id, func(node *swarm.Node) {
|
|
| 31 |
+ node.Spec.Membership = swarm.NodeMembershipAccepted |
|
| 32 |
+ }); err != nil {
|
|
| 33 |
+ return err |
|
| 34 |
+ } |
|
| 35 |
+ fmt.Println(id, "attempting to accept a node in the swarm.") |
|
| 36 |
+ } |
|
| 37 |
+ |
|
| 38 |
+ return nil |
|
| 39 |
+} |
| 0 | 40 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,49 @@ |
| 0 |
+package node |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ |
|
| 5 |
+ "golang.org/x/net/context" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/spf13/cobra" |
|
| 8 |
+ |
|
| 9 |
+ "github.com/docker/docker/api/client" |
|
| 10 |
+ "github.com/docker/docker/cli" |
|
| 11 |
+ apiclient "github.com/docker/engine-api/client" |
|
| 12 |
+) |
|
| 13 |
+ |
|
| 14 |
+// NewNodeCommand returns a cobra command for `node` subcommands |
|
| 15 |
+func NewNodeCommand(dockerCli *client.DockerCli) *cobra.Command {
|
|
| 16 |
+ cmd := &cobra.Command{
|
|
| 17 |
+ Use: "node", |
|
| 18 |
+ Short: "Manage docker swarm nodes", |
|
| 19 |
+ Args: cli.NoArgs, |
|
| 20 |
+ Run: func(cmd *cobra.Command, args []string) {
|
|
| 21 |
+ fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) |
|
| 22 |
+ }, |
|
| 23 |
+ } |
|
| 24 |
+ cmd.AddCommand( |
|
| 25 |
+ newAcceptCommand(dockerCli), |
|
| 26 |
+ newDemoteCommand(dockerCli), |
|
| 27 |
+ newInspectCommand(dockerCli), |
|
| 28 |
+ newListCommand(dockerCli), |
|
| 29 |
+ newPromoteCommand(dockerCli), |
|
| 30 |
+ newRemoveCommand(dockerCli), |
|
| 31 |
+ newTasksCommand(dockerCli), |
|
| 32 |
+ newUpdateCommand(dockerCli), |
|
| 33 |
+ ) |
|
| 34 |
+ return cmd |
|
| 35 |
+} |
|
| 36 |
+ |
|
| 37 |
+func nodeReference(client apiclient.APIClient, ctx context.Context, ref string) (string, error) {
|
|
| 38 |
+ // The special value "self" for a node reference is mapped to the current |
|
| 39 |
+ // node, hence the node ID is retrieved using the `/info` endpoint. |
|
| 40 |
+ if ref == "self" {
|
|
| 41 |
+ info, err := client.Info(ctx) |
|
| 42 |
+ if err != nil {
|
|
| 43 |
+ return "", err |
|
| 44 |
+ } |
|
| 45 |
+ return info.Swarm.NodeID, nil |
|
| 46 |
+ } |
|
| 47 |
+ return ref, nil |
|
| 48 |
+} |
| 0 | 49 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,40 @@ |
| 0 |
+package node |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ |
|
| 5 |
+ "github.com/docker/docker/api/client" |
|
| 6 |
+ "github.com/docker/docker/cli" |
|
| 7 |
+ "github.com/docker/engine-api/types/swarm" |
|
| 8 |
+ "github.com/spf13/cobra" |
|
| 9 |
+ "github.com/spf13/pflag" |
|
| 10 |
+) |
|
| 11 |
+ |
|
| 12 |
+func newDemoteCommand(dockerCli *client.DockerCli) *cobra.Command {
|
|
| 13 |
+ var flags *pflag.FlagSet |
|
| 14 |
+ |
|
| 15 |
+ cmd := &cobra.Command{
|
|
| 16 |
+ Use: "demote NODE [NODE...]", |
|
| 17 |
+ Short: "Demote a node from manager in the swarm", |
|
| 18 |
+ Args: cli.RequiresMinArgs(1), |
|
| 19 |
+ RunE: func(cmd *cobra.Command, args []string) error {
|
|
| 20 |
+ return runDemote(dockerCli, flags, args) |
|
| 21 |
+ }, |
|
| 22 |
+ } |
|
| 23 |
+ |
|
| 24 |
+ flags = cmd.Flags() |
|
| 25 |
+ return cmd |
|
| 26 |
+} |
|
| 27 |
+ |
|
| 28 |
+func runDemote(dockerCli *client.DockerCli, flags *pflag.FlagSet, args []string) error {
|
|
| 29 |
+ for _, id := range args {
|
|
| 30 |
+ if err := runUpdate(dockerCli, id, func(node *swarm.Node) {
|
|
| 31 |
+ node.Spec.Role = swarm.NodeRoleWorker |
|
| 32 |
+ }); err != nil {
|
|
| 33 |
+ return err |
|
| 34 |
+ } |
|
| 35 |
+ fmt.Println(id, "attempting to demote a manager in the swarm.") |
|
| 36 |
+ } |
|
| 37 |
+ |
|
| 38 |
+ return nil |
|
| 39 |
+} |
| 0 | 40 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,141 @@ |
| 0 |
+package node |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ "io" |
|
| 5 |
+ "sort" |
|
| 6 |
+ "strings" |
|
| 7 |
+ |
|
| 8 |
+ "github.com/docker/docker/api/client" |
|
| 9 |
+ "github.com/docker/docker/api/client/inspect" |
|
| 10 |
+ "github.com/docker/docker/cli" |
|
| 11 |
+ "github.com/docker/docker/pkg/ioutils" |
|
| 12 |
+ "github.com/docker/engine-api/types/swarm" |
|
| 13 |
+ "github.com/docker/go-units" |
|
| 14 |
+ "github.com/spf13/cobra" |
|
| 15 |
+ "golang.org/x/net/context" |
|
| 16 |
+) |
|
| 17 |
+ |
|
| 18 |
+type inspectOptions struct {
|
|
| 19 |
+ nodeIds []string |
|
| 20 |
+ format string |
|
| 21 |
+ pretty bool |
|
| 22 |
+} |
|
| 23 |
+ |
|
| 24 |
+func newInspectCommand(dockerCli *client.DockerCli) *cobra.Command {
|
|
| 25 |
+ var opts inspectOptions |
|
| 26 |
+ |
|
| 27 |
+ cmd := &cobra.Command{
|
|
| 28 |
+ Use: "inspect [OPTIONS] self|NODE [NODE...]", |
|
| 29 |
+ Short: "Inspect a node in the swarm", |
|
| 30 |
+ Args: cli.RequiresMinArgs(1), |
|
| 31 |
+ RunE: func(cmd *cobra.Command, args []string) error {
|
|
| 32 |
+ opts.nodeIds = args |
|
| 33 |
+ return runInspect(dockerCli, opts) |
|
| 34 |
+ }, |
|
| 35 |
+ } |
|
| 36 |
+ |
|
| 37 |
+ flags := cmd.Flags() |
|
| 38 |
+ flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template") |
|
| 39 |
+ flags.BoolVarP(&opts.pretty, "pretty", "p", false, "Print the information in a human friendly format.") |
|
| 40 |
+ return cmd |
|
| 41 |
+} |
|
| 42 |
+ |
|
| 43 |
+func runInspect(dockerCli *client.DockerCli, opts inspectOptions) error {
|
|
| 44 |
+ client := dockerCli.Client() |
|
| 45 |
+ ctx := context.Background() |
|
| 46 |
+ getRef := func(ref string) (interface{}, []byte, error) {
|
|
| 47 |
+ nodeRef, err := nodeReference(client, ctx, ref) |
|
| 48 |
+ if err != nil {
|
|
| 49 |
+ return nil, nil, err |
|
| 50 |
+ } |
|
| 51 |
+ node, err := client.NodeInspect(ctx, nodeRef) |
|
| 52 |
+ return node, nil, err |
|
| 53 |
+ } |
|
| 54 |
+ |
|
| 55 |
+ if !opts.pretty {
|
|
| 56 |
+ return inspect.Inspect(dockerCli.Out(), opts.nodeIds, opts.format, getRef) |
|
| 57 |
+ } |
|
| 58 |
+ return printHumanFriendly(dockerCli.Out(), opts.nodeIds, getRef) |
|
| 59 |
+} |
|
| 60 |
+ |
|
| 61 |
+func printHumanFriendly(out io.Writer, refs []string, getRef inspect.GetRefFunc) error {
|
|
| 62 |
+ for idx, ref := range refs {
|
|
| 63 |
+ obj, _, err := getRef(ref) |
|
| 64 |
+ if err != nil {
|
|
| 65 |
+ return err |
|
| 66 |
+ } |
|
| 67 |
+ printNode(out, obj.(swarm.Node)) |
|
| 68 |
+ |
|
| 69 |
+ // TODO: better way to do this? |
|
| 70 |
+ // print extra space between objects, but not after the last one |
|
| 71 |
+ if idx+1 != len(refs) {
|
|
| 72 |
+ fmt.Fprintf(out, "\n\n") |
|
| 73 |
+ } |
|
| 74 |
+ } |
|
| 75 |
+ return nil |
|
| 76 |
+} |
|
| 77 |
+ |
|
| 78 |
+// TODO: use a template |
|
| 79 |
+func printNode(out io.Writer, node swarm.Node) {
|
|
| 80 |
+ fmt.Fprintf(out, "ID:\t\t\t%s\n", node.ID) |
|
| 81 |
+ ioutils.FprintfIfNotEmpty(out, "Name:\t\t\t%s\n", node.Spec.Name) |
|
| 82 |
+ if node.Spec.Labels != nil {
|
|
| 83 |
+ fmt.Fprintln(out, "Labels:") |
|
| 84 |
+ for k, v := range node.Spec.Labels {
|
|
| 85 |
+ fmt.Fprintf(out, " - %s = %s\n", k, v) |
|
| 86 |
+ } |
|
| 87 |
+ } |
|
| 88 |
+ |
|
| 89 |
+ ioutils.FprintfIfNotEmpty(out, "Hostname:\t\t%s\n", node.Description.Hostname) |
|
| 90 |
+ fmt.Fprintln(out, "Status:") |
|
| 91 |
+ fmt.Fprintf(out, " State:\t\t\t%s\n", client.PrettyPrint(node.Status.State)) |
|
| 92 |
+ ioutils.FprintfIfNotEmpty(out, " Message:\t\t%s\n", client.PrettyPrint(node.Status.Message)) |
|
| 93 |
+ fmt.Fprintf(out, " Availability:\t\t%s\n", client.PrettyPrint(node.Spec.Availability)) |
|
| 94 |
+ |
|
| 95 |
+ if node.ManagerStatus != nil {
|
|
| 96 |
+ fmt.Fprintln(out, "Manager Status:") |
|
| 97 |
+ fmt.Fprintf(out, " Address:\t\t%s\n", node.ManagerStatus.Addr) |
|
| 98 |
+ fmt.Fprintf(out, " Raft status:\t\t%s\n", client.PrettyPrint(node.ManagerStatus.Reachability)) |
|
| 99 |
+ leader := "No" |
|
| 100 |
+ if node.ManagerStatus.Leader {
|
|
| 101 |
+ leader = "Yes" |
|
| 102 |
+ } |
|
| 103 |
+ fmt.Fprintf(out, " Leader:\t\t%s\n", leader) |
|
| 104 |
+ } |
|
| 105 |
+ |
|
| 106 |
+ fmt.Fprintln(out, "Platform:") |
|
| 107 |
+ fmt.Fprintf(out, " Operating System:\t%s\n", node.Description.Platform.OS) |
|
| 108 |
+ fmt.Fprintf(out, " Architecture:\t\t%s\n", node.Description.Platform.Architecture) |
|
| 109 |
+ |
|
| 110 |
+ fmt.Fprintln(out, "Resources:") |
|
| 111 |
+ fmt.Fprintf(out, " CPUs:\t\t\t%d\n", node.Description.Resources.NanoCPUs/1e9) |
|
| 112 |
+ fmt.Fprintf(out, " Memory:\t\t%s\n", units.BytesSize(float64(node.Description.Resources.MemoryBytes))) |
|
| 113 |
+ |
|
| 114 |
+ var pluginTypes []string |
|
| 115 |
+ pluginNamesByType := map[string][]string{}
|
|
| 116 |
+ for _, p := range node.Description.Engine.Plugins {
|
|
| 117 |
+ // append to pluginTypes only if not done previously |
|
| 118 |
+ if _, ok := pluginNamesByType[p.Type]; !ok {
|
|
| 119 |
+ pluginTypes = append(pluginTypes, p.Type) |
|
| 120 |
+ } |
|
| 121 |
+ pluginNamesByType[p.Type] = append(pluginNamesByType[p.Type], p.Name) |
|
| 122 |
+ } |
|
| 123 |
+ |
|
| 124 |
+ if len(pluginTypes) > 0 {
|
|
| 125 |
+ fmt.Fprintln(out, "Plugins:") |
|
| 126 |
+ sort.Strings(pluginTypes) // ensure stable output |
|
| 127 |
+ for _, pluginType := range pluginTypes {
|
|
| 128 |
+ fmt.Fprintf(out, " %s:\t\t%s\n", pluginType, strings.Join(pluginNamesByType[pluginType], ", ")) |
|
| 129 |
+ } |
|
| 130 |
+ } |
|
| 131 |
+ fmt.Fprintf(out, "Engine Version:\t\t%s\n", node.Description.Engine.EngineVersion) |
|
| 132 |
+ |
|
| 133 |
+ if len(node.Description.Engine.Labels) != 0 {
|
|
| 134 |
+ fmt.Fprintln(out, "Engine Labels:") |
|
| 135 |
+ for k, v := range node.Description.Engine.Labels {
|
|
| 136 |
+ fmt.Fprintf(out, " - %s = %s", k, v) |
|
| 137 |
+ } |
|
| 138 |
+ } |
|
| 139 |
+ |
|
| 140 |
+} |
| 0 | 141 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,119 @@ |
| 0 |
+package node |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ "io" |
|
| 5 |
+ "text/tabwriter" |
|
| 6 |
+ |
|
| 7 |
+ "golang.org/x/net/context" |
|
| 8 |
+ |
|
| 9 |
+ "github.com/docker/docker/api/client" |
|
| 10 |
+ "github.com/docker/docker/cli" |
|
| 11 |
+ "github.com/docker/docker/opts" |
|
| 12 |
+ "github.com/docker/engine-api/types" |
|
| 13 |
+ "github.com/docker/engine-api/types/swarm" |
|
| 14 |
+ "github.com/spf13/cobra" |
|
| 15 |
+) |
|
| 16 |
+ |
|
| 17 |
+const ( |
|
| 18 |
+ listItemFmt = "%s\t%s\t%s\t%s\t%s\t%s\t%s\n" |
|
| 19 |
+) |
|
| 20 |
+ |
|
| 21 |
+type listOptions struct {
|
|
| 22 |
+ quiet bool |
|
| 23 |
+ filter opts.FilterOpt |
|
| 24 |
+} |
|
| 25 |
+ |
|
| 26 |
+func newListCommand(dockerCli *client.DockerCli) *cobra.Command {
|
|
| 27 |
+ opts := listOptions{filter: opts.NewFilterOpt()}
|
|
| 28 |
+ |
|
| 29 |
+ cmd := &cobra.Command{
|
|
| 30 |
+ Use: "ls", |
|
| 31 |
+ Aliases: []string{"list"},
|
|
| 32 |
+ Short: "List nodes in the swarm", |
|
| 33 |
+ Args: cli.NoArgs, |
|
| 34 |
+ RunE: func(cmd *cobra.Command, args []string) error {
|
|
| 35 |
+ return runList(dockerCli, opts) |
|
| 36 |
+ }, |
|
| 37 |
+ } |
|
| 38 |
+ flags := cmd.Flags() |
|
| 39 |
+ flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs") |
|
| 40 |
+ flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") |
|
| 41 |
+ |
|
| 42 |
+ return cmd |
|
| 43 |
+} |
|
| 44 |
+ |
|
| 45 |
+func runList(dockerCli *client.DockerCli, opts listOptions) error {
|
|
| 46 |
+ client := dockerCli.Client() |
|
| 47 |
+ ctx := context.Background() |
|
| 48 |
+ |
|
| 49 |
+ nodes, err := client.NodeList( |
|
| 50 |
+ ctx, |
|
| 51 |
+ types.NodeListOptions{Filter: opts.filter.Value()})
|
|
| 52 |
+ if err != nil {
|
|
| 53 |
+ return err |
|
| 54 |
+ } |
|
| 55 |
+ |
|
| 56 |
+ info, err := client.Info(ctx) |
|
| 57 |
+ if err != nil {
|
|
| 58 |
+ return err |
|
| 59 |
+ } |
|
| 60 |
+ |
|
| 61 |
+ out := dockerCli.Out() |
|
| 62 |
+ if opts.quiet {
|
|
| 63 |
+ printQuiet(out, nodes) |
|
| 64 |
+ } else {
|
|
| 65 |
+ printTable(out, nodes, info) |
|
| 66 |
+ } |
|
| 67 |
+ return nil |
|
| 68 |
+} |
|
| 69 |
+ |
|
| 70 |
+func printTable(out io.Writer, nodes []swarm.Node, info types.Info) {
|
|
| 71 |
+ writer := tabwriter.NewWriter(out, 0, 4, 2, ' ', 0) |
|
| 72 |
+ |
|
| 73 |
+ // Ignore flushing errors |
|
| 74 |
+ defer writer.Flush() |
|
| 75 |
+ |
|
| 76 |
+ fmt.Fprintf(writer, listItemFmt, "ID", "NAME", "MEMBERSHIP", "STATUS", "AVAILABILITY", "MANAGER STATUS", "LEADER") |
|
| 77 |
+ for _, node := range nodes {
|
|
| 78 |
+ name := node.Spec.Name |
|
| 79 |
+ availability := string(node.Spec.Availability) |
|
| 80 |
+ membership := string(node.Spec.Membership) |
|
| 81 |
+ |
|
| 82 |
+ if name == "" {
|
|
| 83 |
+ name = node.Description.Hostname |
|
| 84 |
+ } |
|
| 85 |
+ |
|
| 86 |
+ leader := "" |
|
| 87 |
+ if node.ManagerStatus != nil && node.ManagerStatus.Leader {
|
|
| 88 |
+ leader = "Yes" |
|
| 89 |
+ } |
|
| 90 |
+ |
|
| 91 |
+ reachability := "" |
|
| 92 |
+ if node.ManagerStatus != nil {
|
|
| 93 |
+ reachability = string(node.ManagerStatus.Reachability) |
|
| 94 |
+ } |
|
| 95 |
+ |
|
| 96 |
+ ID := node.ID |
|
| 97 |
+ if node.ID == info.Swarm.NodeID {
|
|
| 98 |
+ ID = ID + " *" |
|
| 99 |
+ } |
|
| 100 |
+ |
|
| 101 |
+ fmt.Fprintf( |
|
| 102 |
+ writer, |
|
| 103 |
+ listItemFmt, |
|
| 104 |
+ ID, |
|
| 105 |
+ name, |
|
| 106 |
+ client.PrettyPrint(membership), |
|
| 107 |
+ client.PrettyPrint(string(node.Status.State)), |
|
| 108 |
+ client.PrettyPrint(availability), |
|
| 109 |
+ client.PrettyPrint(reachability), |
|
| 110 |
+ leader) |
|
| 111 |
+ } |
|
| 112 |
+} |
|
| 113 |
+ |
|
| 114 |
+func printQuiet(out io.Writer, nodes []swarm.Node) {
|
|
| 115 |
+ for _, node := range nodes {
|
|
| 116 |
+ fmt.Fprintln(out, node.ID) |
|
| 117 |
+ } |
|
| 118 |
+} |
| 0 | 119 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,50 @@ |
| 0 |
+package node |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ "strings" |
|
| 5 |
+ |
|
| 6 |
+ "github.com/docker/engine-api/types/swarm" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+type nodeOptions struct {
|
|
| 10 |
+ role string |
|
| 11 |
+ membership string |
|
| 12 |
+ availability string |
|
| 13 |
+} |
|
| 14 |
+ |
|
| 15 |
+func (opts *nodeOptions) ToNodeSpec() (swarm.NodeSpec, error) {
|
|
| 16 |
+ var spec swarm.NodeSpec |
|
| 17 |
+ |
|
| 18 |
+ switch swarm.NodeRole(strings.ToLower(opts.role)) {
|
|
| 19 |
+ case swarm.NodeRoleWorker: |
|
| 20 |
+ spec.Role = swarm.NodeRoleWorker |
|
| 21 |
+ case swarm.NodeRoleManager: |
|
| 22 |
+ spec.Role = swarm.NodeRoleManager |
|
| 23 |
+ case "": |
|
| 24 |
+ default: |
|
| 25 |
+ return swarm.NodeSpec{}, fmt.Errorf("invalid role %q, only worker and manager are supported", opts.role)
|
|
| 26 |
+ } |
|
| 27 |
+ |
|
| 28 |
+ switch swarm.NodeMembership(strings.ToLower(opts.membership)) {
|
|
| 29 |
+ case swarm.NodeMembershipAccepted: |
|
| 30 |
+ spec.Membership = swarm.NodeMembershipAccepted |
|
| 31 |
+ case "": |
|
| 32 |
+ default: |
|
| 33 |
+ return swarm.NodeSpec{}, fmt.Errorf("invalid membership %q, only accepted is supported", opts.membership)
|
|
| 34 |
+ } |
|
| 35 |
+ |
|
| 36 |
+ switch swarm.NodeAvailability(strings.ToLower(opts.availability)) {
|
|
| 37 |
+ case swarm.NodeAvailabilityActive: |
|
| 38 |
+ spec.Availability = swarm.NodeAvailabilityActive |
|
| 39 |
+ case swarm.NodeAvailabilityPause: |
|
| 40 |
+ spec.Availability = swarm.NodeAvailabilityPause |
|
| 41 |
+ case swarm.NodeAvailabilityDrain: |
|
| 42 |
+ spec.Availability = swarm.NodeAvailabilityDrain |
|
| 43 |
+ case "": |
|
| 44 |
+ default: |
|
| 45 |
+ return swarm.NodeSpec{}, fmt.Errorf("invalid availability %q, only active, pause and drain are supported", opts.availability)
|
|
| 46 |
+ } |
|
| 47 |
+ |
|
| 48 |
+ return spec, nil |
|
| 49 |
+} |
| 0 | 50 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,40 @@ |
| 0 |
+package node |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ |
|
| 5 |
+ "github.com/docker/docker/api/client" |
|
| 6 |
+ "github.com/docker/docker/cli" |
|
| 7 |
+ "github.com/docker/engine-api/types/swarm" |
|
| 8 |
+ "github.com/spf13/cobra" |
|
| 9 |
+ "github.com/spf13/pflag" |
|
| 10 |
+) |
|
| 11 |
+ |
|
| 12 |
+func newPromoteCommand(dockerCli *client.DockerCli) *cobra.Command {
|
|
| 13 |
+ var flags *pflag.FlagSet |
|
| 14 |
+ |
|
| 15 |
+ cmd := &cobra.Command{
|
|
| 16 |
+ Use: "promote NODE [NODE...]", |
|
| 17 |
+ Short: "Promote a node to a manager in the swarm", |
|
| 18 |
+ Args: cli.RequiresMinArgs(1), |
|
| 19 |
+ RunE: func(cmd *cobra.Command, args []string) error {
|
|
| 20 |
+ return runPromote(dockerCli, flags, args) |
|
| 21 |
+ }, |
|
| 22 |
+ } |
|
| 23 |
+ |
|
| 24 |
+ flags = cmd.Flags() |
|
| 25 |
+ return cmd |
|
| 26 |
+} |
|
| 27 |
+ |
|
| 28 |
+func runPromote(dockerCli *client.DockerCli, flags *pflag.FlagSet, args []string) error {
|
|
| 29 |
+ for _, id := range args {
|
|
| 30 |
+ if err := runUpdate(dockerCli, id, func(node *swarm.Node) {
|
|
| 31 |
+ node.Spec.Role = swarm.NodeRoleManager |
|
| 32 |
+ }); err != nil {
|
|
| 33 |
+ return err |
|
| 34 |
+ } |
|
| 35 |
+ fmt.Println(id, "attempting to promote a node to a manager in the swarm.") |
|
| 36 |
+ } |
|
| 37 |
+ |
|
| 38 |
+ return nil |
|
| 39 |
+} |
| 0 | 40 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,36 @@ |
| 0 |
+package node |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ |
|
| 5 |
+ "golang.org/x/net/context" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/docker/docker/api/client" |
|
| 8 |
+ "github.com/docker/docker/cli" |
|
| 9 |
+ "github.com/spf13/cobra" |
|
| 10 |
+) |
|
| 11 |
+ |
|
| 12 |
+func newRemoveCommand(dockerCli *client.DockerCli) *cobra.Command {
|
|
| 13 |
+ return &cobra.Command{
|
|
| 14 |
+ Use: "rm NODE [NODE...]", |
|
| 15 |
+ Aliases: []string{"remove"},
|
|
| 16 |
+ Short: "Remove a node from the swarm", |
|
| 17 |
+ Args: cli.RequiresMinArgs(1), |
|
| 18 |
+ RunE: func(cmd *cobra.Command, args []string) error {
|
|
| 19 |
+ return runRemove(dockerCli, args) |
|
| 20 |
+ }, |
|
| 21 |
+ } |
|
| 22 |
+} |
|
| 23 |
+ |
|
| 24 |
+func runRemove(dockerCli *client.DockerCli, args []string) error {
|
|
| 25 |
+ client := dockerCli.Client() |
|
| 26 |
+ ctx := context.Background() |
|
| 27 |
+ for _, nodeID := range args {
|
|
| 28 |
+ err := client.NodeRemove(ctx, nodeID) |
|
| 29 |
+ if err != nil {
|
|
| 30 |
+ return err |
|
| 31 |
+ } |
|
| 32 |
+ fmt.Fprintf(dockerCli.Out(), "%s\n", nodeID) |
|
| 33 |
+ } |
|
| 34 |
+ return nil |
|
| 35 |
+} |
| 0 | 36 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,72 @@ |
| 0 |
+package node |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "golang.org/x/net/context" |
|
| 4 |
+ |
|
| 5 |
+ "github.com/docker/docker/api/client" |
|
| 6 |
+ "github.com/docker/docker/api/client/idresolver" |
|
| 7 |
+ "github.com/docker/docker/api/client/task" |
|
| 8 |
+ "github.com/docker/docker/cli" |
|
| 9 |
+ "github.com/docker/docker/opts" |
|
| 10 |
+ "github.com/docker/engine-api/types" |
|
| 11 |
+ "github.com/docker/engine-api/types/swarm" |
|
| 12 |
+ "github.com/spf13/cobra" |
|
| 13 |
+) |
|
| 14 |
+ |
|
| 15 |
+type tasksOptions struct {
|
|
| 16 |
+ nodeID string |
|
| 17 |
+ all bool |
|
| 18 |
+ noResolve bool |
|
| 19 |
+ filter opts.FilterOpt |
|
| 20 |
+} |
|
| 21 |
+ |
|
| 22 |
+func newTasksCommand(dockerCli *client.DockerCli) *cobra.Command {
|
|
| 23 |
+ opts := tasksOptions{filter: opts.NewFilterOpt()}
|
|
| 24 |
+ |
|
| 25 |
+ cmd := &cobra.Command{
|
|
| 26 |
+ Use: "tasks [OPTIONS] self|NODE", |
|
| 27 |
+ Short: "List tasks running on a node", |
|
| 28 |
+ Args: cli.ExactArgs(1), |
|
| 29 |
+ RunE: func(cmd *cobra.Command, args []string) error {
|
|
| 30 |
+ opts.nodeID = args[0] |
|
| 31 |
+ return runTasks(dockerCli, opts) |
|
| 32 |
+ }, |
|
| 33 |
+ } |
|
| 34 |
+ flags := cmd.Flags() |
|
| 35 |
+ flags.BoolVarP(&opts.all, "all", "a", false, "Display all instances") |
|
| 36 |
+ flags.BoolVarP(&opts.noResolve, "no-resolve", "n", false, "Do not map IDs to Names") |
|
| 37 |
+ flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") |
|
| 38 |
+ |
|
| 39 |
+ return cmd |
|
| 40 |
+} |
|
| 41 |
+ |
|
| 42 |
+func runTasks(dockerCli *client.DockerCli, opts tasksOptions) error {
|
|
| 43 |
+ client := dockerCli.Client() |
|
| 44 |
+ ctx := context.Background() |
|
| 45 |
+ |
|
| 46 |
+ nodeRef, err := nodeReference(client, ctx, opts.nodeID) |
|
| 47 |
+ if err != nil {
|
|
| 48 |
+ return nil |
|
| 49 |
+ } |
|
| 50 |
+ node, err := client.NodeInspect(ctx, nodeRef) |
|
| 51 |
+ if err != nil {
|
|
| 52 |
+ return err |
|
| 53 |
+ } |
|
| 54 |
+ |
|
| 55 |
+ filter := opts.filter.Value() |
|
| 56 |
+ filter.Add("node", node.ID)
|
|
| 57 |
+ if !opts.all {
|
|
| 58 |
+ filter.Add("desired_state", string(swarm.TaskStateRunning))
|
|
| 59 |
+ filter.Add("desired_state", string(swarm.TaskStateAccepted))
|
|
| 60 |
+ |
|
| 61 |
+ } |
|
| 62 |
+ |
|
| 63 |
+ tasks, err := client.TaskList( |
|
| 64 |
+ ctx, |
|
| 65 |
+ types.TaskListOptions{Filter: filter})
|
|
| 66 |
+ if err != nil {
|
|
| 67 |
+ return err |
|
| 68 |
+ } |
|
| 69 |
+ |
|
| 70 |
+ return task.Print(dockerCli, ctx, tasks, idresolver.New(client, opts.noResolve)) |
|
| 71 |
+} |
| 0 | 72 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,100 @@ |
| 0 |
+package node |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ |
|
| 5 |
+ "github.com/docker/docker/api/client" |
|
| 6 |
+ "github.com/docker/docker/cli" |
|
| 7 |
+ runconfigopts "github.com/docker/docker/runconfig/opts" |
|
| 8 |
+ "github.com/docker/engine-api/types/swarm" |
|
| 9 |
+ "github.com/spf13/cobra" |
|
| 10 |
+ "github.com/spf13/pflag" |
|
| 11 |
+ "golang.org/x/net/context" |
|
| 12 |
+) |
|
| 13 |
+ |
|
| 14 |
+func newUpdateCommand(dockerCli *client.DockerCli) *cobra.Command {
|
|
| 15 |
+ var opts nodeOptions |
|
| 16 |
+ var flags *pflag.FlagSet |
|
| 17 |
+ |
|
| 18 |
+ cmd := &cobra.Command{
|
|
| 19 |
+ Use: "update [OPTIONS] NODE", |
|
| 20 |
+ Short: "Update a node", |
|
| 21 |
+ Args: cli.ExactArgs(1), |
|
| 22 |
+ RunE: func(cmd *cobra.Command, args []string) error {
|
|
| 23 |
+ return runUpdate(dockerCli, args[0], mergeNodeUpdate(flags)) |
|
| 24 |
+ }, |
|
| 25 |
+ } |
|
| 26 |
+ |
|
| 27 |
+ flags = cmd.Flags() |
|
| 28 |
+ flags.StringVar(&opts.role, "role", "", "Role of the node (worker/manager)") |
|
| 29 |
+ flags.StringVar(&opts.membership, "membership", "", "Membership of the node (accepted/rejected)") |
|
| 30 |
+ flags.StringVar(&opts.availability, "availability", "", "Availability of the node (active/pause/drain)") |
|
| 31 |
+ return cmd |
|
| 32 |
+} |
|
| 33 |
+ |
|
| 34 |
+func runUpdate(dockerCli *client.DockerCli, nodeID string, mergeNode func(node *swarm.Node)) error {
|
|
| 35 |
+ client := dockerCli.Client() |
|
| 36 |
+ ctx := context.Background() |
|
| 37 |
+ |
|
| 38 |
+ node, err := client.NodeInspect(ctx, nodeID) |
|
| 39 |
+ if err != nil {
|
|
| 40 |
+ return err |
|
| 41 |
+ } |
|
| 42 |
+ |
|
| 43 |
+ mergeNode(&node) |
|
| 44 |
+ err = client.NodeUpdate(ctx, nodeID, node.Version, node.Spec) |
|
| 45 |
+ if err != nil {
|
|
| 46 |
+ return err |
|
| 47 |
+ } |
|
| 48 |
+ |
|
| 49 |
+ fmt.Fprintf(dockerCli.Out(), "%s\n", nodeID) |
|
| 50 |
+ return nil |
|
| 51 |
+} |
|
| 52 |
+ |
|
| 53 |
+func mergeNodeUpdate(flags *pflag.FlagSet) func(*swarm.Node) {
|
|
| 54 |
+ return func(node *swarm.Node) {
|
|
| 55 |
+ mergeString := func(flag string, field *string) {
|
|
| 56 |
+ if flags.Changed(flag) {
|
|
| 57 |
+ *field, _ = flags.GetString(flag) |
|
| 58 |
+ } |
|
| 59 |
+ } |
|
| 60 |
+ |
|
| 61 |
+ mergeRole := func(flag string, field *swarm.NodeRole) {
|
|
| 62 |
+ if flags.Changed(flag) {
|
|
| 63 |
+ str, _ := flags.GetString(flag) |
|
| 64 |
+ *field = swarm.NodeRole(str) |
|
| 65 |
+ } |
|
| 66 |
+ } |
|
| 67 |
+ |
|
| 68 |
+ mergeMembership := func(flag string, field *swarm.NodeMembership) {
|
|
| 69 |
+ if flags.Changed(flag) {
|
|
| 70 |
+ str, _ := flags.GetString(flag) |
|
| 71 |
+ *field = swarm.NodeMembership(str) |
|
| 72 |
+ } |
|
| 73 |
+ } |
|
| 74 |
+ |
|
| 75 |
+ mergeAvailability := func(flag string, field *swarm.NodeAvailability) {
|
|
| 76 |
+ if flags.Changed(flag) {
|
|
| 77 |
+ str, _ := flags.GetString(flag) |
|
| 78 |
+ *field = swarm.NodeAvailability(str) |
|
| 79 |
+ } |
|
| 80 |
+ } |
|
| 81 |
+ |
|
| 82 |
+ mergeLabels := func(flag string, field *map[string]string) {
|
|
| 83 |
+ if flags.Changed(flag) {
|
|
| 84 |
+ values, _ := flags.GetStringSlice(flag) |
|
| 85 |
+ for key, value := range runconfigopts.ConvertKVStringsToMap(values) {
|
|
| 86 |
+ (*field)[key] = value |
|
| 87 |
+ } |
|
| 88 |
+ } |
|
| 89 |
+ } |
|
| 90 |
+ |
|
| 91 |
+ spec := &node.Spec |
|
| 92 |
+ mergeString("name", &spec.Name)
|
|
| 93 |
+ // TODO: setting labels is not working |
|
| 94 |
+ mergeLabels("label", &spec.Labels)
|
|
| 95 |
+ mergeRole("role", &spec.Role)
|
|
| 96 |
+ mergeMembership("membership", &spec.Membership)
|
|
| 97 |
+ mergeAvailability("availability", &spec.Availability)
|
|
| 98 |
+ } |
|
| 99 |
+} |
| 0 | 100 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,32 @@ |
| 0 |
+package service |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ |
|
| 5 |
+ "github.com/spf13/cobra" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/docker/docker/api/client" |
|
| 8 |
+ "github.com/docker/docker/cli" |
|
| 9 |
+) |
|
| 10 |
+ |
|
| 11 |
+// NewServiceCommand returns a cobra command for `service` subcommands |
|
| 12 |
+func NewServiceCommand(dockerCli *client.DockerCli) *cobra.Command {
|
|
| 13 |
+ cmd := &cobra.Command{
|
|
| 14 |
+ Use: "service", |
|
| 15 |
+ Short: "Manage docker services", |
|
| 16 |
+ Args: cli.NoArgs, |
|
| 17 |
+ Run: func(cmd *cobra.Command, args []string) {
|
|
| 18 |
+ fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) |
|
| 19 |
+ }, |
|
| 20 |
+ } |
|
| 21 |
+ cmd.AddCommand( |
|
| 22 |
+ newCreateCommand(dockerCli), |
|
| 23 |
+ newInspectCommand(dockerCli), |
|
| 24 |
+ newTasksCommand(dockerCli), |
|
| 25 |
+ newListCommand(dockerCli), |
|
| 26 |
+ newRemoveCommand(dockerCli), |
|
| 27 |
+ newScaleCommand(dockerCli), |
|
| 28 |
+ newUpdateCommand(dockerCli), |
|
| 29 |
+ ) |
|
| 30 |
+ return cmd |
|
| 31 |
+} |
| 0 | 32 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,47 @@ |
| 0 |
+package service |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ |
|
| 5 |
+ "github.com/docker/docker/api/client" |
|
| 6 |
+ "github.com/docker/docker/cli" |
|
| 7 |
+ "github.com/spf13/cobra" |
|
| 8 |
+ "golang.org/x/net/context" |
|
| 9 |
+) |
|
| 10 |
+ |
|
| 11 |
+func newCreateCommand(dockerCli *client.DockerCli) *cobra.Command {
|
|
| 12 |
+ opts := newServiceOptions() |
|
| 13 |
+ |
|
| 14 |
+ cmd := &cobra.Command{
|
|
| 15 |
+ Use: "create [OPTIONS] IMAGE [COMMAND] [ARG...]", |
|
| 16 |
+ Short: "Create a new service", |
|
| 17 |
+ Args: cli.RequiresMinArgs(1), |
|
| 18 |
+ RunE: func(cmd *cobra.Command, args []string) error {
|
|
| 19 |
+ opts.image = args[0] |
|
| 20 |
+ if len(args) > 1 {
|
|
| 21 |
+ opts.args = args[1:] |
|
| 22 |
+ } |
|
| 23 |
+ return runCreate(dockerCli, opts) |
|
| 24 |
+ }, |
|
| 25 |
+ } |
|
| 26 |
+ addServiceFlags(cmd, opts) |
|
| 27 |
+ cmd.Flags().SetInterspersed(false) |
|
| 28 |
+ return cmd |
|
| 29 |
+} |
|
| 30 |
+ |
|
| 31 |
+func runCreate(dockerCli *client.DockerCli, opts *serviceOptions) error {
|
|
| 32 |
+ client := dockerCli.Client() |
|
| 33 |
+ |
|
| 34 |
+ service, err := opts.ToService() |
|
| 35 |
+ if err != nil {
|
|
| 36 |
+ return err |
|
| 37 |
+ } |
|
| 38 |
+ |
|
| 39 |
+ response, err := client.ServiceCreate(context.Background(), service) |
|
| 40 |
+ if err != nil {
|
|
| 41 |
+ return err |
|
| 42 |
+ } |
|
| 43 |
+ |
|
| 44 |
+ fmt.Fprintf(dockerCli.Out(), "%s\n", response.ID) |
|
| 45 |
+ return nil |
|
| 46 |
+} |
| 0 | 47 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,127 @@ |
| 0 |
+package service |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ "io" |
|
| 5 |
+ "strings" |
|
| 6 |
+ |
|
| 7 |
+ "golang.org/x/net/context" |
|
| 8 |
+ |
|
| 9 |
+ "github.com/docker/docker/api/client" |
|
| 10 |
+ "github.com/docker/docker/api/client/inspect" |
|
| 11 |
+ "github.com/docker/docker/cli" |
|
| 12 |
+ "github.com/docker/docker/pkg/ioutils" |
|
| 13 |
+ apiclient "github.com/docker/engine-api/client" |
|
| 14 |
+ "github.com/docker/engine-api/types/swarm" |
|
| 15 |
+ "github.com/spf13/cobra" |
|
| 16 |
+) |
|
| 17 |
+ |
|
| 18 |
+type inspectOptions struct {
|
|
| 19 |
+ refs []string |
|
| 20 |
+ format string |
|
| 21 |
+ pretty bool |
|
| 22 |
+} |
|
| 23 |
+ |
|
| 24 |
+func newInspectCommand(dockerCli *client.DockerCli) *cobra.Command {
|
|
| 25 |
+ var opts inspectOptions |
|
| 26 |
+ |
|
| 27 |
+ cmd := &cobra.Command{
|
|
| 28 |
+ Use: "inspect [OPTIONS] SERVICE [SERVICE...]", |
|
| 29 |
+ Short: "Inspect a service", |
|
| 30 |
+ Args: cli.RequiresMinArgs(1), |
|
| 31 |
+ RunE: func(cmd *cobra.Command, args []string) error {
|
|
| 32 |
+ opts.refs = args |
|
| 33 |
+ |
|
| 34 |
+ if opts.pretty && len(opts.format) > 0 {
|
|
| 35 |
+ return fmt.Errorf("--format is incompatible with human friendly format")
|
|
| 36 |
+ } |
|
| 37 |
+ return runInspect(dockerCli, opts) |
|
| 38 |
+ }, |
|
| 39 |
+ } |
|
| 40 |
+ |
|
| 41 |
+ flags := cmd.Flags() |
|
| 42 |
+ flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template") |
|
| 43 |
+ flags.BoolVarP(&opts.pretty, "pretty", "p", false, "Print the information in a human friendly format.") |
|
| 44 |
+ return cmd |
|
| 45 |
+} |
|
| 46 |
+ |
|
| 47 |
+func runInspect(dockerCli *client.DockerCli, opts inspectOptions) error {
|
|
| 48 |
+ client := dockerCli.Client() |
|
| 49 |
+ ctx := context.Background() |
|
| 50 |
+ |
|
| 51 |
+ getRef := func(ref string) (interface{}, []byte, error) {
|
|
| 52 |
+ service, err := client.ServiceInspect(ctx, ref) |
|
| 53 |
+ if err == nil || !apiclient.IsErrServiceNotFound(err) {
|
|
| 54 |
+ return service, nil, err |
|
| 55 |
+ } |
|
| 56 |
+ return nil, nil, fmt.Errorf("Error: no such service: %s", ref)
|
|
| 57 |
+ } |
|
| 58 |
+ |
|
| 59 |
+ if !opts.pretty {
|
|
| 60 |
+ return inspect.Inspect(dockerCli.Out(), opts.refs, opts.format, getRef) |
|
| 61 |
+ } |
|
| 62 |
+ |
|
| 63 |
+ return printHumanFriendly(dockerCli.Out(), opts.refs, getRef) |
|
| 64 |
+} |
|
| 65 |
+ |
|
| 66 |
+func printHumanFriendly(out io.Writer, refs []string, getRef inspect.GetRefFunc) error {
|
|
| 67 |
+ for idx, ref := range refs {
|
|
| 68 |
+ obj, _, err := getRef(ref) |
|
| 69 |
+ if err != nil {
|
|
| 70 |
+ return err |
|
| 71 |
+ } |
|
| 72 |
+ printService(out, obj.(swarm.Service)) |
|
| 73 |
+ |
|
| 74 |
+ // TODO: better way to do this? |
|
| 75 |
+ // print extra space between objects, but not after the last one |
|
| 76 |
+ if idx+1 != len(refs) {
|
|
| 77 |
+ fmt.Fprintf(out, "\n\n") |
|
| 78 |
+ } |
|
| 79 |
+ } |
|
| 80 |
+ return nil |
|
| 81 |
+} |
|
| 82 |
+ |
|
| 83 |
+// TODO: use a template |
|
| 84 |
+func printService(out io.Writer, service swarm.Service) {
|
|
| 85 |
+ fmt.Fprintf(out, "ID:\t\t%s\n", service.ID) |
|
| 86 |
+ fmt.Fprintf(out, "Name:\t\t%s\n", service.Spec.Name) |
|
| 87 |
+ if service.Spec.Labels != nil {
|
|
| 88 |
+ fmt.Fprintln(out, "Labels:") |
|
| 89 |
+ for k, v := range service.Spec.Labels {
|
|
| 90 |
+ fmt.Fprintf(out, " - %s=%s\n", k, v) |
|
| 91 |
+ } |
|
| 92 |
+ } |
|
| 93 |
+ |
|
| 94 |
+ if service.Spec.Mode.Global != nil {
|
|
| 95 |
+ fmt.Fprintln(out, "Mode:\t\tGLOBAL") |
|
| 96 |
+ } else {
|
|
| 97 |
+ fmt.Fprintln(out, "Mode:\t\tREPLICATED") |
|
| 98 |
+ if service.Spec.Mode.Replicated.Replicas != nil {
|
|
| 99 |
+ fmt.Fprintf(out, " Replicas:\t\t%d\n", *service.Spec.Mode.Replicated.Replicas) |
|
| 100 |
+ } |
|
| 101 |
+ } |
|
| 102 |
+ fmt.Fprintln(out, "Placement:") |
|
| 103 |
+ fmt.Fprintln(out, " Strategy:\tSPREAD") |
|
| 104 |
+ fmt.Fprintf(out, "UpateConfig:\n") |
|
| 105 |
+ fmt.Fprintf(out, " Parallelism:\t%d\n", service.Spec.UpdateConfig.Parallelism) |
|
| 106 |
+ if service.Spec.UpdateConfig.Delay.Nanoseconds() > 0 {
|
|
| 107 |
+ fmt.Fprintf(out, " Delay:\t\t%s\n", service.Spec.UpdateConfig.Delay) |
|
| 108 |
+ } |
|
| 109 |
+ fmt.Fprintf(out, "ContainerSpec:\n") |
|
| 110 |
+ printContainerSpec(out, service.Spec.TaskTemplate.ContainerSpec) |
|
| 111 |
+} |
|
| 112 |
+ |
|
| 113 |
+func printContainerSpec(out io.Writer, containerSpec swarm.ContainerSpec) {
|
|
| 114 |
+ fmt.Fprintf(out, " Image:\t\t%s\n", containerSpec.Image) |
|
| 115 |
+ if len(containerSpec.Command) > 0 {
|
|
| 116 |
+ fmt.Fprintf(out, " Command:\t%s\n", strings.Join(containerSpec.Command, " ")) |
|
| 117 |
+ } |
|
| 118 |
+ if len(containerSpec.Args) > 0 {
|
|
| 119 |
+ fmt.Fprintf(out, " Args:\t%s\n", strings.Join(containerSpec.Args, " ")) |
|
| 120 |
+ } |
|
| 121 |
+ if len(containerSpec.Env) > 0 {
|
|
| 122 |
+ fmt.Fprintf(out, " Env:\t\t%s\n", strings.Join(containerSpec.Env, " ")) |
|
| 123 |
+ } |
|
| 124 |
+ ioutils.FprintfIfNotEmpty(out, " Dir\t\t%s\n", containerSpec.Dir) |
|
| 125 |
+ ioutils.FprintfIfNotEmpty(out, " User\t\t%s\n", containerSpec.User) |
|
| 126 |
+} |
| 0 | 127 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,97 @@ |
| 0 |
+package service |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ "io" |
|
| 5 |
+ "strings" |
|
| 6 |
+ "text/tabwriter" |
|
| 7 |
+ |
|
| 8 |
+ "golang.org/x/net/context" |
|
| 9 |
+ |
|
| 10 |
+ "github.com/docker/docker/api/client" |
|
| 11 |
+ "github.com/docker/docker/cli" |
|
| 12 |
+ "github.com/docker/docker/opts" |
|
| 13 |
+ "github.com/docker/docker/pkg/stringid" |
|
| 14 |
+ "github.com/docker/engine-api/types" |
|
| 15 |
+ "github.com/docker/engine-api/types/swarm" |
|
| 16 |
+ "github.com/spf13/cobra" |
|
| 17 |
+) |
|
| 18 |
+ |
|
| 19 |
+const ( |
|
| 20 |
+ listItemFmt = "%s\t%s\t%s\t%s\t%s\n" |
|
| 21 |
+) |
|
| 22 |
+ |
|
| 23 |
+type listOptions struct {
|
|
| 24 |
+ quiet bool |
|
| 25 |
+ filter opts.FilterOpt |
|
| 26 |
+} |
|
| 27 |
+ |
|
| 28 |
+func newListCommand(dockerCli *client.DockerCli) *cobra.Command {
|
|
| 29 |
+ opts := listOptions{filter: opts.NewFilterOpt()}
|
|
| 30 |
+ |
|
| 31 |
+ cmd := &cobra.Command{
|
|
| 32 |
+ Use: "ls", |
|
| 33 |
+ Aliases: []string{"list"},
|
|
| 34 |
+ Short: "List services", |
|
| 35 |
+ Args: cli.NoArgs, |
|
| 36 |
+ RunE: func(cmd *cobra.Command, args []string) error {
|
|
| 37 |
+ return runList(dockerCli, opts) |
|
| 38 |
+ }, |
|
| 39 |
+ } |
|
| 40 |
+ |
|
| 41 |
+ flags := cmd.Flags() |
|
| 42 |
+ flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs") |
|
| 43 |
+ flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") |
|
| 44 |
+ |
|
| 45 |
+ return cmd |
|
| 46 |
+} |
|
| 47 |
+ |
|
| 48 |
+func runList(dockerCli *client.DockerCli, opts listOptions) error {
|
|
| 49 |
+ client := dockerCli.Client() |
|
| 50 |
+ |
|
| 51 |
+ services, err := client.ServiceList( |
|
| 52 |
+ context.Background(), |
|
| 53 |
+ types.ServiceListOptions{Filter: opts.filter.Value()})
|
|
| 54 |
+ if err != nil {
|
|
| 55 |
+ return err |
|
| 56 |
+ } |
|
| 57 |
+ |
|
| 58 |
+ out := dockerCli.Out() |
|
| 59 |
+ if opts.quiet {
|
|
| 60 |
+ printQuiet(out, services) |
|
| 61 |
+ } else {
|
|
| 62 |
+ printTable(out, services) |
|
| 63 |
+ } |
|
| 64 |
+ return nil |
|
| 65 |
+} |
|
| 66 |
+ |
|
| 67 |
+func printTable(out io.Writer, services []swarm.Service) {
|
|
| 68 |
+ writer := tabwriter.NewWriter(out, 0, 4, 2, ' ', 0) |
|
| 69 |
+ |
|
| 70 |
+ // Ignore flushing errors |
|
| 71 |
+ defer writer.Flush() |
|
| 72 |
+ |
|
| 73 |
+ fmt.Fprintf(writer, listItemFmt, "ID", "NAME", "SCALE", "IMAGE", "COMMAND") |
|
| 74 |
+ for _, service := range services {
|
|
| 75 |
+ scale := "" |
|
| 76 |
+ if service.Spec.Mode.Replicated != nil && service.Spec.Mode.Replicated.Replicas != nil {
|
|
| 77 |
+ scale = fmt.Sprintf("%d", *service.Spec.Mode.Replicated.Replicas)
|
|
| 78 |
+ } else if service.Spec.Mode.Global != nil {
|
|
| 79 |
+ scale = "global" |
|
| 80 |
+ } |
|
| 81 |
+ fmt.Fprintf( |
|
| 82 |
+ writer, |
|
| 83 |
+ listItemFmt, |
|
| 84 |
+ stringid.TruncateID(service.ID), |
|
| 85 |
+ service.Spec.Name, |
|
| 86 |
+ scale, |
|
| 87 |
+ service.Spec.TaskTemplate.ContainerSpec.Image, |
|
| 88 |
+ strings.Join(service.Spec.TaskTemplate.ContainerSpec.Args, " ")) |
|
| 89 |
+ } |
|
| 90 |
+} |
|
| 91 |
+ |
|
| 92 |
+func printQuiet(out io.Writer, services []swarm.Service) {
|
|
| 93 |
+ for _, service := range services {
|
|
| 94 |
+ fmt.Fprintln(out, service.ID) |
|
| 95 |
+ } |
|
| 96 |
+} |
| 0 | 97 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,462 @@ |
| 0 |
+package service |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "encoding/csv" |
|
| 4 |
+ "fmt" |
|
| 5 |
+ "math/big" |
|
| 6 |
+ "strconv" |
|
| 7 |
+ "strings" |
|
| 8 |
+ "time" |
|
| 9 |
+ |
|
| 10 |
+ "github.com/docker/docker/opts" |
|
| 11 |
+ runconfigopts "github.com/docker/docker/runconfig/opts" |
|
| 12 |
+ "github.com/docker/engine-api/types/swarm" |
|
| 13 |
+ "github.com/docker/go-connections/nat" |
|
| 14 |
+ units "github.com/docker/go-units" |
|
| 15 |
+ "github.com/spf13/cobra" |
|
| 16 |
+) |
|
| 17 |
+ |
|
| 18 |
+var ( |
|
| 19 |
+ // DefaultReplicas is the default replicas to use for a replicated service |
|
| 20 |
+ DefaultReplicas uint64 = 1 |
|
| 21 |
+) |
|
| 22 |
+ |
|
| 23 |
+type int64Value interface {
|
|
| 24 |
+ Value() int64 |
|
| 25 |
+} |
|
| 26 |
+ |
|
| 27 |
+type memBytes int64 |
|
| 28 |
+ |
|
| 29 |
+func (m *memBytes) String() string {
|
|
| 30 |
+ return strconv.FormatInt(m.Value(), 10) |
|
| 31 |
+} |
|
| 32 |
+ |
|
| 33 |
+func (m *memBytes) Set(value string) error {
|
|
| 34 |
+ val, err := units.RAMInBytes(value) |
|
| 35 |
+ *m = memBytes(val) |
|
| 36 |
+ return err |
|
| 37 |
+} |
|
| 38 |
+ |
|
| 39 |
+func (m *memBytes) Type() string {
|
|
| 40 |
+ return "MemoryBytes" |
|
| 41 |
+} |
|
| 42 |
+ |
|
| 43 |
+func (m *memBytes) Value() int64 {
|
|
| 44 |
+ return int64(*m) |
|
| 45 |
+} |
|
| 46 |
+ |
|
| 47 |
+type nanoCPUs int64 |
|
| 48 |
+ |
|
| 49 |
+func (c *nanoCPUs) String() string {
|
|
| 50 |
+ return strconv.FormatInt(c.Value(), 10) |
|
| 51 |
+} |
|
| 52 |
+ |
|
| 53 |
+func (c *nanoCPUs) Set(value string) error {
|
|
| 54 |
+ cpu, ok := new(big.Rat).SetString(value) |
|
| 55 |
+ if !ok {
|
|
| 56 |
+ return fmt.Errorf("Failed to parse %v as a rational number", value)
|
|
| 57 |
+ } |
|
| 58 |
+ nano := cpu.Mul(cpu, big.NewRat(1e9, 1)) |
|
| 59 |
+ if !nano.IsInt() {
|
|
| 60 |
+ return fmt.Errorf("value is too precise")
|
|
| 61 |
+ } |
|
| 62 |
+ *c = nanoCPUs(nano.Num().Int64()) |
|
| 63 |
+ return nil |
|
| 64 |
+} |
|
| 65 |
+ |
|
| 66 |
+func (c *nanoCPUs) Type() string {
|
|
| 67 |
+ return "NanoCPUs" |
|
| 68 |
+} |
|
| 69 |
+ |
|
| 70 |
+func (c *nanoCPUs) Value() int64 {
|
|
| 71 |
+ return int64(*c) |
|
| 72 |
+} |
|
| 73 |
+ |
|
| 74 |
+// DurationOpt is an option type for time.Duration that uses a pointer. This |
|
| 75 |
+// allows us to get nil values outside, instead of defaulting to 0 |
|
| 76 |
+type DurationOpt struct {
|
|
| 77 |
+ value *time.Duration |
|
| 78 |
+} |
|
| 79 |
+ |
|
| 80 |
+// Set a new value on the option |
|
| 81 |
+func (d *DurationOpt) Set(s string) error {
|
|
| 82 |
+ v, err := time.ParseDuration(s) |
|
| 83 |
+ d.value = &v |
|
| 84 |
+ return err |
|
| 85 |
+} |
|
| 86 |
+ |
|
| 87 |
+// Type returns the type of this option |
|
| 88 |
+func (d *DurationOpt) Type() string {
|
|
| 89 |
+ return "duration-ptr" |
|
| 90 |
+} |
|
| 91 |
+ |
|
| 92 |
+// String returns a string repr of this option |
|
| 93 |
+func (d *DurationOpt) String() string {
|
|
| 94 |
+ if d.value != nil {
|
|
| 95 |
+ return d.value.String() |
|
| 96 |
+ } |
|
| 97 |
+ return "none" |
|
| 98 |
+} |
|
| 99 |
+ |
|
| 100 |
+// Value returns the time.Duration |
|
| 101 |
+func (d *DurationOpt) Value() *time.Duration {
|
|
| 102 |
+ return d.value |
|
| 103 |
+} |
|
| 104 |
+ |
|
| 105 |
+// Uint64Opt represents a uint64. |
|
| 106 |
+type Uint64Opt struct {
|
|
| 107 |
+ value *uint64 |
|
| 108 |
+} |
|
| 109 |
+ |
|
| 110 |
+// Set a new value on the option |
|
| 111 |
+func (i *Uint64Opt) Set(s string) error {
|
|
| 112 |
+ v, err := strconv.ParseUint(s, 0, 64) |
|
| 113 |
+ i.value = &v |
|
| 114 |
+ return err |
|
| 115 |
+} |
|
| 116 |
+ |
|
| 117 |
+// Type returns the type of this option |
|
| 118 |
+func (i *Uint64Opt) Type() string {
|
|
| 119 |
+ return "uint64-ptr" |
|
| 120 |
+} |
|
| 121 |
+ |
|
| 122 |
+// String returns a string repr of this option |
|
| 123 |
+func (i *Uint64Opt) String() string {
|
|
| 124 |
+ if i.value != nil {
|
|
| 125 |
+ return fmt.Sprintf("%v", *i.value)
|
|
| 126 |
+ } |
|
| 127 |
+ return "none" |
|
| 128 |
+} |
|
| 129 |
+ |
|
| 130 |
+// Value returns the uint64 |
|
| 131 |
+func (i *Uint64Opt) Value() *uint64 {
|
|
| 132 |
+ return i.value |
|
| 133 |
+} |
|
| 134 |
+ |
|
| 135 |
+// MountOpt is a Value type for parsing mounts |
|
| 136 |
+type MountOpt struct {
|
|
| 137 |
+ values []swarm.Mount |
|
| 138 |
+} |
|
| 139 |
+ |
|
| 140 |
+// Set a new mount value |
|
| 141 |
+func (m *MountOpt) Set(value string) error {
|
|
| 142 |
+ csvReader := csv.NewReader(strings.NewReader(value)) |
|
| 143 |
+ fields, err := csvReader.Read() |
|
| 144 |
+ if err != nil {
|
|
| 145 |
+ return err |
|
| 146 |
+ } |
|
| 147 |
+ |
|
| 148 |
+ mount := swarm.Mount{}
|
|
| 149 |
+ |
|
| 150 |
+ volumeOptions := func() *swarm.VolumeOptions {
|
|
| 151 |
+ if mount.VolumeOptions == nil {
|
|
| 152 |
+ mount.VolumeOptions = &swarm.VolumeOptions{
|
|
| 153 |
+ Labels: make(map[string]string), |
|
| 154 |
+ } |
|
| 155 |
+ } |
|
| 156 |
+ return mount.VolumeOptions |
|
| 157 |
+ } |
|
| 158 |
+ |
|
| 159 |
+ setValueOnMap := func(target map[string]string, value string) {
|
|
| 160 |
+ parts := strings.SplitN(value, "=", 2) |
|
| 161 |
+ if len(parts) == 1 {
|
|
| 162 |
+ target[value] = "" |
|
| 163 |
+ } else {
|
|
| 164 |
+ target[parts[0]] = parts[1] |
|
| 165 |
+ } |
|
| 166 |
+ } |
|
| 167 |
+ |
|
| 168 |
+ for _, field := range fields {
|
|
| 169 |
+ parts := strings.SplitN(field, "=", 2) |
|
| 170 |
+ if len(parts) == 1 && strings.ToLower(parts[0]) == "writable" {
|
|
| 171 |
+ mount.Writable = true |
|
| 172 |
+ continue |
|
| 173 |
+ } |
|
| 174 |
+ |
|
| 175 |
+ if len(parts) != 2 {
|
|
| 176 |
+ return fmt.Errorf("invald field '%s' must be a key=value pair", field)
|
|
| 177 |
+ } |
|
| 178 |
+ |
|
| 179 |
+ key, value := parts[0], parts[1] |
|
| 180 |
+ switch strings.ToLower(key) {
|
|
| 181 |
+ case "type": |
|
| 182 |
+ mount.Type = swarm.MountType(strings.ToUpper(value)) |
|
| 183 |
+ case "source": |
|
| 184 |
+ mount.Source = value |
|
| 185 |
+ case "target": |
|
| 186 |
+ mount.Target = value |
|
| 187 |
+ case "writable": |
|
| 188 |
+ mount.Writable, err = strconv.ParseBool(value) |
|
| 189 |
+ if err != nil {
|
|
| 190 |
+ return fmt.Errorf("invald value for writable: %s", err.Error())
|
|
| 191 |
+ } |
|
| 192 |
+ case "bind-propagation": |
|
| 193 |
+ mount.BindOptions.Propagation = swarm.MountPropagation(strings.ToUpper(value)) |
|
| 194 |
+ case "volume-populate": |
|
| 195 |
+ volumeOptions().Populate, err = strconv.ParseBool(value) |
|
| 196 |
+ if err != nil {
|
|
| 197 |
+ return fmt.Errorf("invald value for populate: %s", err.Error())
|
|
| 198 |
+ } |
|
| 199 |
+ case "volume-label": |
|
| 200 |
+ setValueOnMap(volumeOptions().Labels, value) |
|
| 201 |
+ case "volume-driver": |
|
| 202 |
+ volumeOptions().DriverConfig.Name = value |
|
| 203 |
+ case "volume-driver-opt": |
|
| 204 |
+ if volumeOptions().DriverConfig.Options == nil {
|
|
| 205 |
+ volumeOptions().DriverConfig.Options = make(map[string]string) |
|
| 206 |
+ } |
|
| 207 |
+ setValueOnMap(volumeOptions().DriverConfig.Options, value) |
|
| 208 |
+ default: |
|
| 209 |
+ return fmt.Errorf("unexpected key '%s' in '%s'", key, value)
|
|
| 210 |
+ } |
|
| 211 |
+ } |
|
| 212 |
+ |
|
| 213 |
+ if mount.Type == "" {
|
|
| 214 |
+ return fmt.Errorf("type is required")
|
|
| 215 |
+ } |
|
| 216 |
+ |
|
| 217 |
+ if mount.Target == "" {
|
|
| 218 |
+ return fmt.Errorf("target is required")
|
|
| 219 |
+ } |
|
| 220 |
+ |
|
| 221 |
+ m.values = append(m.values, mount) |
|
| 222 |
+ return nil |
|
| 223 |
+} |
|
| 224 |
+ |
|
| 225 |
+// Type returns the type of this option |
|
| 226 |
+func (m *MountOpt) Type() string {
|
|
| 227 |
+ return "mount" |
|
| 228 |
+} |
|
| 229 |
+ |
|
| 230 |
+// String returns a string repr of this option |
|
| 231 |
+func (m *MountOpt) String() string {
|
|
| 232 |
+ mounts := []string{}
|
|
| 233 |
+ for _, mount := range m.values {
|
|
| 234 |
+ mounts = append(mounts, fmt.Sprintf("%v", mount))
|
|
| 235 |
+ } |
|
| 236 |
+ return strings.Join(mounts, ", ") |
|
| 237 |
+} |
|
| 238 |
+ |
|
| 239 |
+// Value returns the mounts |
|
| 240 |
+func (m *MountOpt) Value() []swarm.Mount {
|
|
| 241 |
+ return m.values |
|
| 242 |
+} |
|
| 243 |
+ |
|
| 244 |
+type updateOptions struct {
|
|
| 245 |
+ parallelism uint64 |
|
| 246 |
+ delay time.Duration |
|
| 247 |
+} |
|
| 248 |
+ |
|
| 249 |
+type resourceOptions struct {
|
|
| 250 |
+ limitCPU nanoCPUs |
|
| 251 |
+ limitMemBytes memBytes |
|
| 252 |
+ resCPU nanoCPUs |
|
| 253 |
+ resMemBytes memBytes |
|
| 254 |
+} |
|
| 255 |
+ |
|
| 256 |
+func (r *resourceOptions) ToResourceRequirements() *swarm.ResourceRequirements {
|
|
| 257 |
+ return &swarm.ResourceRequirements{
|
|
| 258 |
+ Limits: &swarm.Resources{
|
|
| 259 |
+ NanoCPUs: r.limitCPU.Value(), |
|
| 260 |
+ MemoryBytes: r.limitMemBytes.Value(), |
|
| 261 |
+ }, |
|
| 262 |
+ Reservations: &swarm.Resources{
|
|
| 263 |
+ NanoCPUs: r.resCPU.Value(), |
|
| 264 |
+ MemoryBytes: r.resMemBytes.Value(), |
|
| 265 |
+ }, |
|
| 266 |
+ } |
|
| 267 |
+} |
|
| 268 |
+ |
|
| 269 |
+type restartPolicyOptions struct {
|
|
| 270 |
+ condition string |
|
| 271 |
+ delay DurationOpt |
|
| 272 |
+ maxAttempts Uint64Opt |
|
| 273 |
+ window DurationOpt |
|
| 274 |
+} |
|
| 275 |
+ |
|
| 276 |
+func (r *restartPolicyOptions) ToRestartPolicy() *swarm.RestartPolicy {
|
|
| 277 |
+ return &swarm.RestartPolicy{
|
|
| 278 |
+ Condition: swarm.RestartPolicyCondition(r.condition), |
|
| 279 |
+ Delay: r.delay.Value(), |
|
| 280 |
+ MaxAttempts: r.maxAttempts.Value(), |
|
| 281 |
+ Window: r.window.Value(), |
|
| 282 |
+ } |
|
| 283 |
+} |
|
| 284 |
+ |
|
| 285 |
+func convertNetworks(networks []string) []swarm.NetworkAttachmentConfig {
|
|
| 286 |
+ nets := []swarm.NetworkAttachmentConfig{}
|
|
| 287 |
+ for _, network := range networks {
|
|
| 288 |
+ nets = append(nets, swarm.NetworkAttachmentConfig{Target: network})
|
|
| 289 |
+ } |
|
| 290 |
+ return nets |
|
| 291 |
+} |
|
| 292 |
+ |
|
| 293 |
+type endpointOptions struct {
|
|
| 294 |
+ mode string |
|
| 295 |
+ ports opts.ListOpts |
|
| 296 |
+} |
|
| 297 |
+ |
|
| 298 |
+func (e *endpointOptions) ToEndpointSpec() *swarm.EndpointSpec {
|
|
| 299 |
+ portConfigs := []swarm.PortConfig{}
|
|
| 300 |
+ // We can ignore errors because the format was already validated by ValidatePort |
|
| 301 |
+ ports, portBindings, _ := nat.ParsePortSpecs(e.ports.GetAll()) |
|
| 302 |
+ |
|
| 303 |
+ for port := range ports {
|
|
| 304 |
+ portConfigs = append(portConfigs, convertPortToPortConfig(port, portBindings)...) |
|
| 305 |
+ } |
|
| 306 |
+ |
|
| 307 |
+ return &swarm.EndpointSpec{
|
|
| 308 |
+ Mode: swarm.ResolutionMode(e.mode), |
|
| 309 |
+ Ports: portConfigs, |
|
| 310 |
+ } |
|
| 311 |
+} |
|
| 312 |
+ |
|
| 313 |
+func convertPortToPortConfig( |
|
| 314 |
+ port nat.Port, |
|
| 315 |
+ portBindings map[nat.Port][]nat.PortBinding, |
|
| 316 |
+) []swarm.PortConfig {
|
|
| 317 |
+ ports := []swarm.PortConfig{}
|
|
| 318 |
+ |
|
| 319 |
+ for _, binding := range portBindings[port] {
|
|
| 320 |
+ hostPort, _ := strconv.ParseUint(binding.HostPort, 10, 16) |
|
| 321 |
+ ports = append(ports, swarm.PortConfig{
|
|
| 322 |
+ //TODO Name: ? |
|
| 323 |
+ Protocol: swarm.PortConfigProtocol(strings.ToLower(port.Proto())), |
|
| 324 |
+ TargetPort: uint32(port.Int()), |
|
| 325 |
+ PublishedPort: uint32(hostPort), |
|
| 326 |
+ }) |
|
| 327 |
+ } |
|
| 328 |
+ return ports |
|
| 329 |
+} |
|
| 330 |
+ |
|
| 331 |
+// ValidatePort validates a string is in the expected format for a port definition |
|
| 332 |
+func ValidatePort(value string) (string, error) {
|
|
| 333 |
+ portMappings, err := nat.ParsePortSpec(value) |
|
| 334 |
+ for _, portMapping := range portMappings {
|
|
| 335 |
+ if portMapping.Binding.HostIP != "" {
|
|
| 336 |
+ return "", fmt.Errorf("HostIP is not supported by a service.")
|
|
| 337 |
+ } |
|
| 338 |
+ } |
|
| 339 |
+ return value, err |
|
| 340 |
+} |
|
| 341 |
+ |
|
| 342 |
+type serviceOptions struct {
|
|
| 343 |
+ name string |
|
| 344 |
+ labels opts.ListOpts |
|
| 345 |
+ image string |
|
| 346 |
+ command []string |
|
| 347 |
+ args []string |
|
| 348 |
+ env opts.ListOpts |
|
| 349 |
+ workdir string |
|
| 350 |
+ user string |
|
| 351 |
+ mounts MountOpt |
|
| 352 |
+ |
|
| 353 |
+ resources resourceOptions |
|
| 354 |
+ stopGrace DurationOpt |
|
| 355 |
+ |
|
| 356 |
+ replicas Uint64Opt |
|
| 357 |
+ mode string |
|
| 358 |
+ |
|
| 359 |
+ restartPolicy restartPolicyOptions |
|
| 360 |
+ constraints []string |
|
| 361 |
+ update updateOptions |
|
| 362 |
+ networks []string |
|
| 363 |
+ endpoint endpointOptions |
|
| 364 |
+} |
|
| 365 |
+ |
|
| 366 |
+func newServiceOptions() *serviceOptions {
|
|
| 367 |
+ return &serviceOptions{
|
|
| 368 |
+ labels: opts.NewListOpts(runconfigopts.ValidateEnv), |
|
| 369 |
+ env: opts.NewListOpts(runconfigopts.ValidateEnv), |
|
| 370 |
+ endpoint: endpointOptions{
|
|
| 371 |
+ ports: opts.NewListOpts(ValidatePort), |
|
| 372 |
+ }, |
|
| 373 |
+ } |
|
| 374 |
+} |
|
| 375 |
+ |
|
| 376 |
+func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) {
|
|
| 377 |
+ var service swarm.ServiceSpec |
|
| 378 |
+ |
|
| 379 |
+ service = swarm.ServiceSpec{
|
|
| 380 |
+ Annotations: swarm.Annotations{
|
|
| 381 |
+ Name: opts.name, |
|
| 382 |
+ Labels: runconfigopts.ConvertKVStringsToMap(opts.labels.GetAll()), |
|
| 383 |
+ }, |
|
| 384 |
+ TaskTemplate: swarm.TaskSpec{
|
|
| 385 |
+ ContainerSpec: swarm.ContainerSpec{
|
|
| 386 |
+ Image: opts.image, |
|
| 387 |
+ Command: opts.command, |
|
| 388 |
+ Args: opts.args, |
|
| 389 |
+ Env: opts.env.GetAll(), |
|
| 390 |
+ Dir: opts.workdir, |
|
| 391 |
+ User: opts.user, |
|
| 392 |
+ Mounts: opts.mounts.Value(), |
|
| 393 |
+ StopGracePeriod: opts.stopGrace.Value(), |
|
| 394 |
+ }, |
|
| 395 |
+ Resources: opts.resources.ToResourceRequirements(), |
|
| 396 |
+ RestartPolicy: opts.restartPolicy.ToRestartPolicy(), |
|
| 397 |
+ Placement: &swarm.Placement{
|
|
| 398 |
+ Constraints: opts.constraints, |
|
| 399 |
+ }, |
|
| 400 |
+ }, |
|
| 401 |
+ Mode: swarm.ServiceMode{},
|
|
| 402 |
+ UpdateConfig: &swarm.UpdateConfig{
|
|
| 403 |
+ Parallelism: opts.update.parallelism, |
|
| 404 |
+ Delay: opts.update.delay, |
|
| 405 |
+ }, |
|
| 406 |
+ Networks: convertNetworks(opts.networks), |
|
| 407 |
+ EndpointSpec: opts.endpoint.ToEndpointSpec(), |
|
| 408 |
+ } |
|
| 409 |
+ |
|
| 410 |
+ switch opts.mode {
|
|
| 411 |
+ case "global": |
|
| 412 |
+ if opts.replicas.Value() != nil {
|
|
| 413 |
+ return service, fmt.Errorf("replicas can only be used with replicated mode")
|
|
| 414 |
+ } |
|
| 415 |
+ |
|
| 416 |
+ service.Mode.Global = &swarm.GlobalService{}
|
|
| 417 |
+ case "replicated": |
|
| 418 |
+ service.Mode.Replicated = &swarm.ReplicatedService{
|
|
| 419 |
+ Replicas: opts.replicas.Value(), |
|
| 420 |
+ } |
|
| 421 |
+ default: |
|
| 422 |
+ return service, fmt.Errorf("Unknown mode: %s", opts.mode)
|
|
| 423 |
+ } |
|
| 424 |
+ return service, nil |
|
| 425 |
+} |
|
| 426 |
+ |
|
| 427 |
+// addServiceFlags adds all flags that are common to both `create` and `update. |
|
| 428 |
+// Any flags that are not common are added separately in the individual command |
|
| 429 |
+func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) {
|
|
| 430 |
+ flags := cmd.Flags() |
|
| 431 |
+ flags.StringVar(&opts.name, "name", "", "Service name") |
|
| 432 |
+ flags.VarP(&opts.labels, "label", "l", "Service labels") |
|
| 433 |
+ |
|
| 434 |
+ flags.VarP(&opts.env, "env", "e", "Set environment variables") |
|
| 435 |
+ flags.StringVarP(&opts.workdir, "workdir", "w", "", "Working directory inside the container") |
|
| 436 |
+ flags.StringVarP(&opts.user, "user", "u", "", "Username or UID") |
|
| 437 |
+ flags.VarP(&opts.mounts, "mount", "m", "Attach a mount to the service") |
|
| 438 |
+ |
|
| 439 |
+ flags.Var(&opts.resources.limitCPU, "limit-cpu", "Limit CPUs") |
|
| 440 |
+ flags.Var(&opts.resources.limitMemBytes, "limit-memory", "Limit Memory") |
|
| 441 |
+ flags.Var(&opts.resources.resCPU, "reserve-cpu", "Reserve CPUs") |
|
| 442 |
+ flags.Var(&opts.resources.resMemBytes, "reserve-memory", "Reserve Memory") |
|
| 443 |
+ flags.Var(&opts.stopGrace, "stop-grace-period", "Time to wait before force killing a container") |
|
| 444 |
+ |
|
| 445 |
+ flags.StringVar(&opts.mode, "mode", "replicated", "Service mode (replicated or global)") |
|
| 446 |
+ flags.Var(&opts.replicas, "replicas", "Number of tasks") |
|
| 447 |
+ |
|
| 448 |
+ flags.StringVar(&opts.restartPolicy.condition, "restart-condition", "", "Restart when condition is met (none, on_failure, or any)") |
|
| 449 |
+ flags.Var(&opts.restartPolicy.delay, "restart-delay", "Delay between restart attempts") |
|
| 450 |
+ flags.Var(&opts.restartPolicy.maxAttempts, "restart-max-attempts", "Maximum number of restarts before giving up") |
|
| 451 |
+ flags.Var(&opts.restartPolicy.window, "restart-window", "Window used to evalulate the restart policy") |
|
| 452 |
+ |
|
| 453 |
+ flags.StringSliceVar(&opts.constraints, "constraint", []string{}, "Placement constraints")
|
|
| 454 |
+ |
|
| 455 |
+ flags.Uint64Var(&opts.update.parallelism, "update-parallelism", 1, "Maximum number of tasks updated simultaneously") |
|
| 456 |
+ flags.DurationVar(&opts.update.delay, "update-delay", time.Duration(0), "Delay between updates") |
|
| 457 |
+ |
|
| 458 |
+ flags.StringSliceVar(&opts.networks, "network", []string{}, "Network attachments")
|
|
| 459 |
+ flags.StringVar(&opts.endpoint.mode, "endpoint-mode", "", "Endpoint mode(Valid values: VIP, DNSRR)") |
|
| 460 |
+ flags.VarP(&opts.endpoint.ports, "publish", "p", "Publish a port as a node port") |
|
| 461 |
+} |
| 0 | 462 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,47 @@ |
| 0 |
+package service |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ "strings" |
|
| 5 |
+ |
|
| 6 |
+ "github.com/docker/docker/api/client" |
|
| 7 |
+ "github.com/docker/docker/cli" |
|
| 8 |
+ "github.com/spf13/cobra" |
|
| 9 |
+ "golang.org/x/net/context" |
|
| 10 |
+) |
|
| 11 |
+ |
|
| 12 |
+func newRemoveCommand(dockerCli *client.DockerCli) *cobra.Command {
|
|
| 13 |
+ |
|
| 14 |
+ cmd := &cobra.Command{
|
|
| 15 |
+ Use: "rm [OPTIONS] SERVICE", |
|
| 16 |
+ Aliases: []string{"remove"},
|
|
| 17 |
+ Short: "Remove a service", |
|
| 18 |
+ Args: cli.RequiresMinArgs(1), |
|
| 19 |
+ RunE: func(cmd *cobra.Command, args []string) error {
|
|
| 20 |
+ return runRemove(dockerCli, args) |
|
| 21 |
+ }, |
|
| 22 |
+ } |
|
| 23 |
+ cmd.Flags() |
|
| 24 |
+ |
|
| 25 |
+ return cmd |
|
| 26 |
+} |
|
| 27 |
+ |
|
| 28 |
+func runRemove(dockerCli *client.DockerCli, sids []string) error {
|
|
| 29 |
+ client := dockerCli.Client() |
|
| 30 |
+ |
|
| 31 |
+ ctx := context.Background() |
|
| 32 |
+ |
|
| 33 |
+ var errs []string |
|
| 34 |
+ for _, sid := range sids {
|
|
| 35 |
+ err := client.ServiceRemove(ctx, sid) |
|
| 36 |
+ if err != nil {
|
|
| 37 |
+ errs = append(errs, err.Error()) |
|
| 38 |
+ continue |
|
| 39 |
+ } |
|
| 40 |
+ fmt.Fprintf(dockerCli.Out(), "%s\n", sid) |
|
| 41 |
+ } |
|
| 42 |
+ if len(errs) > 0 {
|
|
| 43 |
+ return fmt.Errorf(strings.Join(errs, "\n")) |
|
| 44 |
+ } |
|
| 45 |
+ return nil |
|
| 46 |
+} |
| 0 | 47 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,86 @@ |
| 0 |
+package service |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ "strconv" |
|
| 5 |
+ "strings" |
|
| 6 |
+ |
|
| 7 |
+ "golang.org/x/net/context" |
|
| 8 |
+ |
|
| 9 |
+ "github.com/docker/docker/api/client" |
|
| 10 |
+ "github.com/docker/docker/cli" |
|
| 11 |
+ "github.com/spf13/cobra" |
|
| 12 |
+) |
|
| 13 |
+ |
|
| 14 |
+func newScaleCommand(dockerCli *client.DockerCli) *cobra.Command {
|
|
| 15 |
+ return &cobra.Command{
|
|
| 16 |
+ Use: "scale SERVICE=SCALE [SERVICE=SCALE...]", |
|
| 17 |
+ Short: "Scale one or multiple services", |
|
| 18 |
+ Args: scaleArgs, |
|
| 19 |
+ RunE: func(cmd *cobra.Command, args []string) error {
|
|
| 20 |
+ return runScale(dockerCli, args) |
|
| 21 |
+ }, |
|
| 22 |
+ } |
|
| 23 |
+} |
|
| 24 |
+ |
|
| 25 |
+func scaleArgs(cmd *cobra.Command, args []string) error {
|
|
| 26 |
+ if err := cli.RequiresMinArgs(1)(cmd, args); err != nil {
|
|
| 27 |
+ return err |
|
| 28 |
+ } |
|
| 29 |
+ for _, arg := range args {
|
|
| 30 |
+ if parts := strings.SplitN(arg, "=", 2); len(parts) != 2 {
|
|
| 31 |
+ return fmt.Errorf( |
|
| 32 |
+ "Invalid scale specifier '%s'.\nSee '%s --help'.\n\nUsage: %s\n\n%s", |
|
| 33 |
+ arg, |
|
| 34 |
+ cmd.CommandPath(), |
|
| 35 |
+ cmd.UseLine(), |
|
| 36 |
+ cmd.Short, |
|
| 37 |
+ ) |
|
| 38 |
+ } |
|
| 39 |
+ } |
|
| 40 |
+ return nil |
|
| 41 |
+} |
|
| 42 |
+ |
|
| 43 |
+func runScale(dockerCli *client.DockerCli, args []string) error {
|
|
| 44 |
+ var errors []string |
|
| 45 |
+ for _, arg := range args {
|
|
| 46 |
+ parts := strings.SplitN(arg, "=", 2) |
|
| 47 |
+ serviceID, scale := parts[0], parts[1] |
|
| 48 |
+ if err := runServiceScale(dockerCli, serviceID, scale); err != nil {
|
|
| 49 |
+ errors = append(errors, fmt.Sprintf("%s: %s", serviceID, err.Error()))
|
|
| 50 |
+ } |
|
| 51 |
+ } |
|
| 52 |
+ |
|
| 53 |
+ if len(errors) == 0 {
|
|
| 54 |
+ return nil |
|
| 55 |
+ } |
|
| 56 |
+ return fmt.Errorf(strings.Join(errors, "\n")) |
|
| 57 |
+} |
|
| 58 |
+ |
|
| 59 |
+func runServiceScale(dockerCli *client.DockerCli, serviceID string, scale string) error {
|
|
| 60 |
+ client := dockerCli.Client() |
|
| 61 |
+ ctx := context.Background() |
|
| 62 |
+ |
|
| 63 |
+ service, err := client.ServiceInspect(ctx, serviceID) |
|
| 64 |
+ if err != nil {
|
|
| 65 |
+ return err |
|
| 66 |
+ } |
|
| 67 |
+ |
|
| 68 |
+ serviceMode := &service.Spec.Mode |
|
| 69 |
+ if serviceMode.Replicated == nil {
|
|
| 70 |
+ return fmt.Errorf("scale can only be used with replicated mode")
|
|
| 71 |
+ } |
|
| 72 |
+ uintScale, err := strconv.ParseUint(scale, 10, 64) |
|
| 73 |
+ if err != nil {
|
|
| 74 |
+ return fmt.Errorf("invalid replicas value %s: %s", scale, err.Error())
|
|
| 75 |
+ } |
|
| 76 |
+ serviceMode.Replicated.Replicas = &uintScale |
|
| 77 |
+ |
|
| 78 |
+ err = client.ServiceUpdate(ctx, service.ID, service.Version, service.Spec) |
|
| 79 |
+ if err != nil {
|
|
| 80 |
+ return err |
|
| 81 |
+ } |
|
| 82 |
+ |
|
| 83 |
+ fmt.Fprintf(dockerCli.Out(), "%s scaled to %s\n", serviceID, scale) |
|
| 84 |
+ return nil |
|
| 85 |
+} |
| 0 | 86 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,65 @@ |
| 0 |
+package service |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "golang.org/x/net/context" |
|
| 4 |
+ |
|
| 5 |
+ "github.com/docker/docker/api/client" |
|
| 6 |
+ "github.com/docker/docker/api/client/idresolver" |
|
| 7 |
+ "github.com/docker/docker/api/client/task" |
|
| 8 |
+ "github.com/docker/docker/cli" |
|
| 9 |
+ "github.com/docker/docker/opts" |
|
| 10 |
+ "github.com/docker/engine-api/types" |
|
| 11 |
+ "github.com/docker/engine-api/types/swarm" |
|
| 12 |
+ "github.com/spf13/cobra" |
|
| 13 |
+) |
|
| 14 |
+ |
|
| 15 |
+type tasksOptions struct {
|
|
| 16 |
+ serviceID string |
|
| 17 |
+ all bool |
|
| 18 |
+ noResolve bool |
|
| 19 |
+ filter opts.FilterOpt |
|
| 20 |
+} |
|
| 21 |
+ |
|
| 22 |
+func newTasksCommand(dockerCli *client.DockerCli) *cobra.Command {
|
|
| 23 |
+ opts := tasksOptions{filter: opts.NewFilterOpt()}
|
|
| 24 |
+ |
|
| 25 |
+ cmd := &cobra.Command{
|
|
| 26 |
+ Use: "tasks [OPTIONS] SERVICE", |
|
| 27 |
+ Short: "List the tasks of a service", |
|
| 28 |
+ Args: cli.ExactArgs(1), |
|
| 29 |
+ RunE: func(cmd *cobra.Command, args []string) error {
|
|
| 30 |
+ opts.serviceID = args[0] |
|
| 31 |
+ return runTasks(dockerCli, opts) |
|
| 32 |
+ }, |
|
| 33 |
+ } |
|
| 34 |
+ flags := cmd.Flags() |
|
| 35 |
+ flags.BoolVarP(&opts.all, "all", "a", false, "Display all tasks") |
|
| 36 |
+ flags.BoolVarP(&opts.noResolve, "no-resolve", "n", false, "Do not map IDs to Names") |
|
| 37 |
+ flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") |
|
| 38 |
+ |
|
| 39 |
+ return cmd |
|
| 40 |
+} |
|
| 41 |
+ |
|
| 42 |
+func runTasks(dockerCli *client.DockerCli, opts tasksOptions) error {
|
|
| 43 |
+ client := dockerCli.Client() |
|
| 44 |
+ ctx := context.Background() |
|
| 45 |
+ |
|
| 46 |
+ service, err := client.ServiceInspect(ctx, opts.serviceID) |
|
| 47 |
+ if err != nil {
|
|
| 48 |
+ return err |
|
| 49 |
+ } |
|
| 50 |
+ |
|
| 51 |
+ filter := opts.filter.Value() |
|
| 52 |
+ filter.Add("service", service.ID)
|
|
| 53 |
+ if !opts.all && !filter.Include("desired_state") {
|
|
| 54 |
+ filter.Add("desired_state", string(swarm.TaskStateRunning))
|
|
| 55 |
+ filter.Add("desired_state", string(swarm.TaskStateAccepted))
|
|
| 56 |
+ } |
|
| 57 |
+ |
|
| 58 |
+ tasks, err := client.TaskList(ctx, types.TaskListOptions{Filter: filter})
|
|
| 59 |
+ if err != nil {
|
|
| 60 |
+ return err |
|
| 61 |
+ } |
|
| 62 |
+ |
|
| 63 |
+ return task.Print(dockerCli, ctx, tasks, idresolver.New(client, opts.noResolve)) |
|
| 64 |
+} |
| 0 | 65 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,244 @@ |
| 0 |
+package service |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ "time" |
|
| 5 |
+ |
|
| 6 |
+ "golang.org/x/net/context" |
|
| 7 |
+ |
|
| 8 |
+ "github.com/docker/docker/api/client" |
|
| 9 |
+ "github.com/docker/docker/cli" |
|
| 10 |
+ "github.com/docker/docker/opts" |
|
| 11 |
+ runconfigopts "github.com/docker/docker/runconfig/opts" |
|
| 12 |
+ "github.com/docker/engine-api/types/swarm" |
|
| 13 |
+ "github.com/docker/go-connections/nat" |
|
| 14 |
+ "github.com/spf13/cobra" |
|
| 15 |
+ "github.com/spf13/pflag" |
|
| 16 |
+) |
|
| 17 |
+ |
|
| 18 |
+func newUpdateCommand(dockerCli *client.DockerCli) *cobra.Command {
|
|
| 19 |
+ opts := newServiceOptions() |
|
| 20 |
+ var flags *pflag.FlagSet |
|
| 21 |
+ |
|
| 22 |
+ cmd := &cobra.Command{
|
|
| 23 |
+ Use: "update [OPTIONS] SERVICE", |
|
| 24 |
+ Short: "Update a service", |
|
| 25 |
+ Args: cli.ExactArgs(1), |
|
| 26 |
+ RunE: func(cmd *cobra.Command, args []string) error {
|
|
| 27 |
+ return runUpdate(dockerCli, flags, args[0]) |
|
| 28 |
+ }, |
|
| 29 |
+ } |
|
| 30 |
+ |
|
| 31 |
+ flags = cmd.Flags() |
|
| 32 |
+ flags.String("image", "", "Service image tag")
|
|
| 33 |
+ flags.StringSlice("command", []string{}, "Service command")
|
|
| 34 |
+ flags.StringSlice("arg", []string{}, "Service command args")
|
|
| 35 |
+ addServiceFlags(cmd, opts) |
|
| 36 |
+ return cmd |
|
| 37 |
+} |
|
| 38 |
+ |
|
| 39 |
+func runUpdate(dockerCli *client.DockerCli, flags *pflag.FlagSet, serviceID string) error {
|
|
| 40 |
+ client := dockerCli.Client() |
|
| 41 |
+ ctx := context.Background() |
|
| 42 |
+ |
|
| 43 |
+ service, err := client.ServiceInspect(ctx, serviceID) |
|
| 44 |
+ if err != nil {
|
|
| 45 |
+ return err |
|
| 46 |
+ } |
|
| 47 |
+ |
|
| 48 |
+ err = mergeService(&service.Spec, flags) |
|
| 49 |
+ if err != nil {
|
|
| 50 |
+ return err |
|
| 51 |
+ } |
|
| 52 |
+ err = client.ServiceUpdate(ctx, service.ID, service.Version, service.Spec) |
|
| 53 |
+ if err != nil {
|
|
| 54 |
+ return err |
|
| 55 |
+ } |
|
| 56 |
+ |
|
| 57 |
+ fmt.Fprintf(dockerCli.Out(), "%s\n", serviceID) |
|
| 58 |
+ return nil |
|
| 59 |
+} |
|
| 60 |
+ |
|
| 61 |
+func mergeService(spec *swarm.ServiceSpec, flags *pflag.FlagSet) error {
|
|
| 62 |
+ |
|
| 63 |
+ mergeString := func(flag string, field *string) {
|
|
| 64 |
+ if flags.Changed(flag) {
|
|
| 65 |
+ *field, _ = flags.GetString(flag) |
|
| 66 |
+ } |
|
| 67 |
+ } |
|
| 68 |
+ |
|
| 69 |
+ mergeListOpts := func(flag string, field *[]string) {
|
|
| 70 |
+ if flags.Changed(flag) {
|
|
| 71 |
+ value := flags.Lookup(flag).Value.(*opts.ListOpts) |
|
| 72 |
+ *field = value.GetAll() |
|
| 73 |
+ } |
|
| 74 |
+ } |
|
| 75 |
+ |
|
| 76 |
+ mergeSlice := func(flag string, field *[]string) {
|
|
| 77 |
+ if flags.Changed(flag) {
|
|
| 78 |
+ *field, _ = flags.GetStringSlice(flag) |
|
| 79 |
+ } |
|
| 80 |
+ } |
|
| 81 |
+ |
|
| 82 |
+ mergeInt64Value := func(flag string, field *int64) {
|
|
| 83 |
+ if flags.Changed(flag) {
|
|
| 84 |
+ *field = flags.Lookup(flag).Value.(int64Value).Value() |
|
| 85 |
+ } |
|
| 86 |
+ } |
|
| 87 |
+ |
|
| 88 |
+ mergeDuration := func(flag string, field *time.Duration) {
|
|
| 89 |
+ if flags.Changed(flag) {
|
|
| 90 |
+ *field, _ = flags.GetDuration(flag) |
|
| 91 |
+ } |
|
| 92 |
+ } |
|
| 93 |
+ |
|
| 94 |
+ mergeDurationOpt := func(flag string, field *time.Duration) {
|
|
| 95 |
+ if flags.Changed(flag) {
|
|
| 96 |
+ *field = *flags.Lookup(flag).Value.(*DurationOpt).Value() |
|
| 97 |
+ } |
|
| 98 |
+ } |
|
| 99 |
+ |
|
| 100 |
+ mergeUint64 := func(flag string, field *uint64) {
|
|
| 101 |
+ if flags.Changed(flag) {
|
|
| 102 |
+ *field, _ = flags.GetUint64(flag) |
|
| 103 |
+ } |
|
| 104 |
+ } |
|
| 105 |
+ |
|
| 106 |
+ mergeUint64Opt := func(flag string, field *uint64) {
|
|
| 107 |
+ if flags.Changed(flag) {
|
|
| 108 |
+ *field = *flags.Lookup(flag).Value.(*Uint64Opt).Value() |
|
| 109 |
+ } |
|
| 110 |
+ } |
|
| 111 |
+ |
|
| 112 |
+ cspec := &spec.TaskTemplate.ContainerSpec |
|
| 113 |
+ task := &spec.TaskTemplate |
|
| 114 |
+ mergeString("name", &spec.Name)
|
|
| 115 |
+ mergeLabels(flags, &spec.Labels) |
|
| 116 |
+ mergeString("image", &cspec.Image)
|
|
| 117 |
+ mergeSlice("command", &cspec.Command)
|
|
| 118 |
+ mergeSlice("arg", &cspec.Command)
|
|
| 119 |
+ mergeListOpts("env", &cspec.Env)
|
|
| 120 |
+ mergeString("workdir", &cspec.Dir)
|
|
| 121 |
+ mergeString("user", &cspec.User)
|
|
| 122 |
+ mergeMounts(flags, &cspec.Mounts) |
|
| 123 |
+ |
|
| 124 |
+ mergeInt64Value("limit-cpu", &task.Resources.Limits.NanoCPUs)
|
|
| 125 |
+ mergeInt64Value("limit-memory", &task.Resources.Limits.MemoryBytes)
|
|
| 126 |
+ mergeInt64Value("reserve-cpu", &task.Resources.Reservations.NanoCPUs)
|
|
| 127 |
+ mergeInt64Value("reserve-memory", &task.Resources.Reservations.MemoryBytes)
|
|
| 128 |
+ |
|
| 129 |
+ mergeDurationOpt("stop-grace-period", cspec.StopGracePeriod)
|
|
| 130 |
+ |
|
| 131 |
+ if flags.Changed("restart-policy-condition") {
|
|
| 132 |
+ value, _ := flags.GetString("restart-policy-condition")
|
|
| 133 |
+ task.RestartPolicy.Condition = swarm.RestartPolicyCondition(value) |
|
| 134 |
+ } |
|
| 135 |
+ mergeDurationOpt("restart-policy-delay", task.RestartPolicy.Delay)
|
|
| 136 |
+ mergeUint64Opt("restart-policy-max-attempts", task.RestartPolicy.MaxAttempts)
|
|
| 137 |
+ mergeDurationOpt("restart-policy-window", task.RestartPolicy.Window)
|
|
| 138 |
+ mergeSlice("constraint", &task.Placement.Constraints)
|
|
| 139 |
+ |
|
| 140 |
+ if err := mergeMode(flags, &spec.Mode); err != nil {
|
|
| 141 |
+ return err |
|
| 142 |
+ } |
|
| 143 |
+ |
|
| 144 |
+ mergeUint64("updateconfig-parallelism", &spec.UpdateConfig.Parallelism)
|
|
| 145 |
+ mergeDuration("updateconfig-delay", &spec.UpdateConfig.Delay)
|
|
| 146 |
+ |
|
| 147 |
+ mergeNetworks(flags, &spec.Networks) |
|
| 148 |
+ if flags.Changed("endpoint-mode") {
|
|
| 149 |
+ value, _ := flags.GetString("endpoint-mode")
|
|
| 150 |
+ spec.EndpointSpec.Mode = swarm.ResolutionMode(value) |
|
| 151 |
+ } |
|
| 152 |
+ |
|
| 153 |
+ mergePorts(flags, &spec.EndpointSpec.Ports) |
|
| 154 |
+ |
|
| 155 |
+ return nil |
|
| 156 |
+} |
|
| 157 |
+ |
|
| 158 |
+func mergeLabels(flags *pflag.FlagSet, field *map[string]string) {
|
|
| 159 |
+ if !flags.Changed("label") {
|
|
| 160 |
+ return |
|
| 161 |
+ } |
|
| 162 |
+ |
|
| 163 |
+ if *field == nil {
|
|
| 164 |
+ *field = make(map[string]string) |
|
| 165 |
+ } |
|
| 166 |
+ |
|
| 167 |
+ values := flags.Lookup("label").Value.(*opts.ListOpts).GetAll()
|
|
| 168 |
+ for key, value := range runconfigopts.ConvertKVStringsToMap(values) {
|
|
| 169 |
+ (*field)[key] = value |
|
| 170 |
+ } |
|
| 171 |
+} |
|
| 172 |
+ |
|
| 173 |
+// TODO: should this override by destination path, or does swarm handle that? |
|
| 174 |
+func mergeMounts(flags *pflag.FlagSet, mounts *[]swarm.Mount) {
|
|
| 175 |
+ if !flags.Changed("mount") {
|
|
| 176 |
+ return |
|
| 177 |
+ } |
|
| 178 |
+ |
|
| 179 |
+ values := flags.Lookup("mount").Value.(*MountOpt).Value()
|
|
| 180 |
+ *mounts = append(*mounts, values...) |
|
| 181 |
+} |
|
| 182 |
+ |
|
| 183 |
+// TODO: should this override by name, or does swarm handle that? |
|
| 184 |
+func mergePorts(flags *pflag.FlagSet, portConfig *[]swarm.PortConfig) {
|
|
| 185 |
+ if !flags.Changed("ports") {
|
|
| 186 |
+ return |
|
| 187 |
+ } |
|
| 188 |
+ |
|
| 189 |
+ values := flags.Lookup("ports").Value.(*opts.ListOpts).GetAll()
|
|
| 190 |
+ ports, portBindings, _ := nat.ParsePortSpecs(values) |
|
| 191 |
+ |
|
| 192 |
+ for port := range ports {
|
|
| 193 |
+ *portConfig = append(*portConfig, convertPortToPortConfig(port, portBindings)...) |
|
| 194 |
+ } |
|
| 195 |
+} |
|
| 196 |
+ |
|
| 197 |
+func mergeNetworks(flags *pflag.FlagSet, attachments *[]swarm.NetworkAttachmentConfig) {
|
|
| 198 |
+ if !flags.Changed("network") {
|
|
| 199 |
+ return |
|
| 200 |
+ } |
|
| 201 |
+ networks, _ := flags.GetStringSlice("network")
|
|
| 202 |
+ for _, network := range networks {
|
|
| 203 |
+ *attachments = append(*attachments, swarm.NetworkAttachmentConfig{Target: network})
|
|
| 204 |
+ } |
|
| 205 |
+} |
|
| 206 |
+ |
|
| 207 |
+func mergeMode(flags *pflag.FlagSet, serviceMode *swarm.ServiceMode) error {
|
|
| 208 |
+ if !flags.Changed("mode") && !flags.Changed("scale") {
|
|
| 209 |
+ return nil |
|
| 210 |
+ } |
|
| 211 |
+ |
|
| 212 |
+ var mode string |
|
| 213 |
+ if flags.Changed("mode") {
|
|
| 214 |
+ mode, _ = flags.GetString("mode")
|
|
| 215 |
+ } |
|
| 216 |
+ |
|
| 217 |
+ if !(mode == "replicated" || serviceMode.Replicated != nil) && flags.Changed("replicas") {
|
|
| 218 |
+ return fmt.Errorf("replicas can only be used with replicated mode")
|
|
| 219 |
+ } |
|
| 220 |
+ |
|
| 221 |
+ if mode == "global" {
|
|
| 222 |
+ serviceMode.Replicated = nil |
|
| 223 |
+ serviceMode.Global = &swarm.GlobalService{}
|
|
| 224 |
+ return nil |
|
| 225 |
+ } |
|
| 226 |
+ |
|
| 227 |
+ if flags.Changed("replicas") {
|
|
| 228 |
+ replicas := flags.Lookup("replicas").Value.(*Uint64Opt).Value()
|
|
| 229 |
+ serviceMode.Replicated = &swarm.ReplicatedService{Replicas: replicas}
|
|
| 230 |
+ serviceMode.Global = nil |
|
| 231 |
+ return nil |
|
| 232 |
+ } |
|
| 233 |
+ |
|
| 234 |
+ if mode == "replicated" {
|
|
| 235 |
+ if serviceMode.Replicated != nil {
|
|
| 236 |
+ return nil |
|
| 237 |
+ } |
|
| 238 |
+ serviceMode.Replicated = &swarm.ReplicatedService{Replicas: &DefaultReplicas}
|
|
| 239 |
+ serviceMode.Global = nil |
|
| 240 |
+ } |
|
| 241 |
+ |
|
| 242 |
+ return nil |
|
| 243 |
+} |
| 0 | 244 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,30 @@ |
| 0 |
+package swarm |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ |
|
| 5 |
+ "github.com/spf13/cobra" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/docker/docker/api/client" |
|
| 8 |
+ "github.com/docker/docker/cli" |
|
| 9 |
+) |
|
| 10 |
+ |
|
| 11 |
+// NewSwarmCommand returns a cobra command for `swarm` subcommands |
|
| 12 |
+func NewSwarmCommand(dockerCli *client.DockerCli) *cobra.Command {
|
|
| 13 |
+ cmd := &cobra.Command{
|
|
| 14 |
+ Use: "swarm", |
|
| 15 |
+ Short: "Manage docker swarm", |
|
| 16 |
+ Args: cli.NoArgs, |
|
| 17 |
+ Run: func(cmd *cobra.Command, args []string) {
|
|
| 18 |
+ fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) |
|
| 19 |
+ }, |
|
| 20 |
+ } |
|
| 21 |
+ cmd.AddCommand( |
|
| 22 |
+ newInitCommand(dockerCli), |
|
| 23 |
+ newJoinCommand(dockerCli), |
|
| 24 |
+ newUpdateCommand(dockerCli), |
|
| 25 |
+ newLeaveCommand(dockerCli), |
|
| 26 |
+ newInspectCommand(dockerCli), |
|
| 27 |
+ ) |
|
| 28 |
+ return cmd |
|
| 29 |
+} |
| 0 | 30 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,61 @@ |
| 0 |
+package swarm |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ |
|
| 5 |
+ "golang.org/x/net/context" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/docker/docker/api/client" |
|
| 8 |
+ "github.com/docker/docker/cli" |
|
| 9 |
+ "github.com/docker/engine-api/types/swarm" |
|
| 10 |
+ "github.com/spf13/cobra" |
|
| 11 |
+) |
|
| 12 |
+ |
|
| 13 |
+type initOptions struct {
|
|
| 14 |
+ listenAddr NodeAddrOption |
|
| 15 |
+ autoAccept AutoAcceptOption |
|
| 16 |
+ forceNewCluster bool |
|
| 17 |
+ secret string |
|
| 18 |
+} |
|
| 19 |
+ |
|
| 20 |
+func newInitCommand(dockerCli *client.DockerCli) *cobra.Command {
|
|
| 21 |
+ opts := initOptions{
|
|
| 22 |
+ listenAddr: NewNodeAddrOption(), |
|
| 23 |
+ autoAccept: NewAutoAcceptOption(), |
|
| 24 |
+ } |
|
| 25 |
+ |
|
| 26 |
+ cmd := &cobra.Command{
|
|
| 27 |
+ Use: "init", |
|
| 28 |
+ Short: "Initialize a Swarm.", |
|
| 29 |
+ Args: cli.NoArgs, |
|
| 30 |
+ RunE: func(cmd *cobra.Command, args []string) error {
|
|
| 31 |
+ return runInit(dockerCli, opts) |
|
| 32 |
+ }, |
|
| 33 |
+ } |
|
| 34 |
+ |
|
| 35 |
+ flags := cmd.Flags() |
|
| 36 |
+ flags.Var(&opts.listenAddr, "listen-addr", "Listen address") |
|
| 37 |
+ flags.Var(&opts.autoAccept, "auto-accept", "Auto acceptance policy (worker, manager, or none)") |
|
| 38 |
+ flags.StringVar(&opts.secret, "secret", "", "Set secret value needed to accept nodes into cluster") |
|
| 39 |
+ flags.BoolVar(&opts.forceNewCluster, "force-new-cluster", false, "Force create a new cluster from current state.") |
|
| 40 |
+ return cmd |
|
| 41 |
+} |
|
| 42 |
+ |
|
| 43 |
+func runInit(dockerCli *client.DockerCli, opts initOptions) error {
|
|
| 44 |
+ client := dockerCli.Client() |
|
| 45 |
+ ctx := context.Background() |
|
| 46 |
+ |
|
| 47 |
+ req := swarm.InitRequest{
|
|
| 48 |
+ ListenAddr: opts.listenAddr.String(), |
|
| 49 |
+ ForceNewCluster: opts.forceNewCluster, |
|
| 50 |
+ } |
|
| 51 |
+ |
|
| 52 |
+ req.Spec.AcceptancePolicy.Policies = opts.autoAccept.Policies(opts.secret) |
|
| 53 |
+ |
|
| 54 |
+ nodeID, err := client.SwarmInit(ctx, req) |
|
| 55 |
+ if err != nil {
|
|
| 56 |
+ return err |
|
| 57 |
+ } |
|
| 58 |
+ fmt.Printf("Swarm initialized: current node (%s) is now a manager.\n", nodeID)
|
|
| 59 |
+ return nil |
|
| 60 |
+} |
| 0 | 61 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,56 @@ |
| 0 |
+package swarm |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "golang.org/x/net/context" |
|
| 4 |
+ |
|
| 5 |
+ "github.com/docker/docker/api/client" |
|
| 6 |
+ "github.com/docker/docker/api/client/inspect" |
|
| 7 |
+ "github.com/docker/docker/cli" |
|
| 8 |
+ "github.com/spf13/cobra" |
|
| 9 |
+) |
|
| 10 |
+ |
|
| 11 |
+type inspectOptions struct {
|
|
| 12 |
+ format string |
|
| 13 |
+ // pretty bool |
|
| 14 |
+} |
|
| 15 |
+ |
|
| 16 |
+func newInspectCommand(dockerCli *client.DockerCli) *cobra.Command {
|
|
| 17 |
+ var opts inspectOptions |
|
| 18 |
+ |
|
| 19 |
+ cmd := &cobra.Command{
|
|
| 20 |
+ Use: "inspect [OPTIONS]", |
|
| 21 |
+ Short: "Inspect the Swarm", |
|
| 22 |
+ Args: cli.NoArgs, |
|
| 23 |
+ RunE: func(cmd *cobra.Command, args []string) error {
|
|
| 24 |
+ // if opts.pretty && len(opts.format) > 0 {
|
|
| 25 |
+ // return fmt.Errorf("--format is incompatible with human friendly format")
|
|
| 26 |
+ // } |
|
| 27 |
+ return runInspect(dockerCli, opts) |
|
| 28 |
+ }, |
|
| 29 |
+ } |
|
| 30 |
+ |
|
| 31 |
+ flags := cmd.Flags() |
|
| 32 |
+ flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template") |
|
| 33 |
+ //flags.BoolVarP(&opts.pretty, "pretty", "h", false, "Print the information in a human friendly format.") |
|
| 34 |
+ return cmd |
|
| 35 |
+} |
|
| 36 |
+ |
|
| 37 |
+func runInspect(dockerCli *client.DockerCli, opts inspectOptions) error {
|
|
| 38 |
+ client := dockerCli.Client() |
|
| 39 |
+ ctx := context.Background() |
|
| 40 |
+ |
|
| 41 |
+ swarm, err := client.SwarmInspect(ctx) |
|
| 42 |
+ if err != nil {
|
|
| 43 |
+ return err |
|
| 44 |
+ } |
|
| 45 |
+ |
|
| 46 |
+ getRef := func(_ string) (interface{}, []byte, error) {
|
|
| 47 |
+ return swarm, nil, nil |
|
| 48 |
+ } |
|
| 49 |
+ |
|
| 50 |
+ // if !opts.pretty {
|
|
| 51 |
+ return inspect.Inspect(dockerCli.Out(), []string{""}, opts.format, getRef)
|
|
| 52 |
+ // } |
|
| 53 |
+ |
|
| 54 |
+ //return printHumanFriendly(dockerCli.Out(), opts.refs, getRef) |
|
| 55 |
+} |
| 0 | 56 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,65 @@ |
| 0 |
+package swarm |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ |
|
| 5 |
+ "github.com/docker/docker/api/client" |
|
| 6 |
+ "github.com/docker/docker/cli" |
|
| 7 |
+ "github.com/docker/engine-api/types/swarm" |
|
| 8 |
+ "github.com/spf13/cobra" |
|
| 9 |
+ "golang.org/x/net/context" |
|
| 10 |
+) |
|
| 11 |
+ |
|
| 12 |
+type joinOptions struct {
|
|
| 13 |
+ remote string |
|
| 14 |
+ listenAddr NodeAddrOption |
|
| 15 |
+ manager bool |
|
| 16 |
+ secret string |
|
| 17 |
+ CACertHash string |
|
| 18 |
+} |
|
| 19 |
+ |
|
| 20 |
+func newJoinCommand(dockerCli *client.DockerCli) *cobra.Command {
|
|
| 21 |
+ opts := joinOptions{
|
|
| 22 |
+ listenAddr: NodeAddrOption{addr: defaultListenAddr},
|
|
| 23 |
+ } |
|
| 24 |
+ |
|
| 25 |
+ cmd := &cobra.Command{
|
|
| 26 |
+ Use: "join [OPTIONS] HOST:PORT", |
|
| 27 |
+ Short: "Join a Swarm as a node and/or manager.", |
|
| 28 |
+ Args: cli.ExactArgs(1), |
|
| 29 |
+ RunE: func(cmd *cobra.Command, args []string) error {
|
|
| 30 |
+ opts.remote = args[0] |
|
| 31 |
+ return runJoin(dockerCli, opts) |
|
| 32 |
+ }, |
|
| 33 |
+ } |
|
| 34 |
+ |
|
| 35 |
+ flags := cmd.Flags() |
|
| 36 |
+ flags.Var(&opts.listenAddr, "listen-addr", "Listen address") |
|
| 37 |
+ flags.BoolVar(&opts.manager, "manager", false, "Try joining as a manager.") |
|
| 38 |
+ flags.StringVar(&opts.secret, "secret", "", "Secret for node acceptance") |
|
| 39 |
+ flags.StringVar(&opts.CACertHash, "ca-hash", "", "Hash of the Root Certificate Authority certificate used for trusted join") |
|
| 40 |
+ return cmd |
|
| 41 |
+} |
|
| 42 |
+ |
|
| 43 |
+func runJoin(dockerCli *client.DockerCli, opts joinOptions) error {
|
|
| 44 |
+ client := dockerCli.Client() |
|
| 45 |
+ ctx := context.Background() |
|
| 46 |
+ |
|
| 47 |
+ req := swarm.JoinRequest{
|
|
| 48 |
+ Manager: opts.manager, |
|
| 49 |
+ Secret: opts.secret, |
|
| 50 |
+ ListenAddr: opts.listenAddr.String(), |
|
| 51 |
+ RemoteAddrs: []string{opts.remote},
|
|
| 52 |
+ CACertHash: opts.CACertHash, |
|
| 53 |
+ } |
|
| 54 |
+ err := client.SwarmJoin(ctx, req) |
|
| 55 |
+ if err != nil {
|
|
| 56 |
+ return err |
|
| 57 |
+ } |
|
| 58 |
+ if opts.manager {
|
|
| 59 |
+ fmt.Fprintln(dockerCli.Out(), "This node joined a Swarm as a manager.") |
|
| 60 |
+ } else {
|
|
| 61 |
+ fmt.Fprintln(dockerCli.Out(), "This node joined a Swarm as a worker.") |
|
| 62 |
+ } |
|
| 63 |
+ return nil |
|
| 64 |
+} |
| 0 | 65 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,44 @@ |
| 0 |
+package swarm |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ |
|
| 5 |
+ "golang.org/x/net/context" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/docker/docker/api/client" |
|
| 8 |
+ "github.com/docker/docker/cli" |
|
| 9 |
+ "github.com/spf13/cobra" |
|
| 10 |
+) |
|
| 11 |
+ |
|
| 12 |
+type leaveOptions struct {
|
|
| 13 |
+ force bool |
|
| 14 |
+} |
|
| 15 |
+ |
|
| 16 |
+func newLeaveCommand(dockerCli *client.DockerCli) *cobra.Command {
|
|
| 17 |
+ opts := leaveOptions{}
|
|
| 18 |
+ |
|
| 19 |
+ cmd := &cobra.Command{
|
|
| 20 |
+ Use: "leave", |
|
| 21 |
+ Short: "Leave a Swarm.", |
|
| 22 |
+ Args: cli.NoArgs, |
|
| 23 |
+ RunE: func(cmd *cobra.Command, args []string) error {
|
|
| 24 |
+ return runLeave(dockerCli, opts) |
|
| 25 |
+ }, |
|
| 26 |
+ } |
|
| 27 |
+ |
|
| 28 |
+ flags := cmd.Flags() |
|
| 29 |
+ flags.BoolVar(&opts.force, "force", false, "Force leave ignoring warnings.") |
|
| 30 |
+ return cmd |
|
| 31 |
+} |
|
| 32 |
+ |
|
| 33 |
+func runLeave(dockerCli *client.DockerCli, opts leaveOptions) error {
|
|
| 34 |
+ client := dockerCli.Client() |
|
| 35 |
+ ctx := context.Background() |
|
| 36 |
+ |
|
| 37 |
+ if err := client.SwarmLeave(ctx, opts.force); err != nil {
|
|
| 38 |
+ return err |
|
| 39 |
+ } |
|
| 40 |
+ |
|
| 41 |
+ fmt.Fprintln(dockerCli.Out(), "Node left the default swarm.") |
|
| 42 |
+ return nil |
|
| 43 |
+} |
| 0 | 44 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,120 @@ |
| 0 |
+package swarm |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ "strings" |
|
| 5 |
+ |
|
| 6 |
+ "github.com/docker/engine-api/types/swarm" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+const ( |
|
| 10 |
+ defaultListenAddr = "0.0.0.0:2377" |
|
| 11 |
+ // WORKER constant for worker name |
|
| 12 |
+ WORKER = "WORKER" |
|
| 13 |
+ // MANAGER constant for manager name |
|
| 14 |
+ MANAGER = "MANAGER" |
|
| 15 |
+) |
|
| 16 |
+ |
|
| 17 |
+var ( |
|
| 18 |
+ defaultPolicies = []swarm.Policy{
|
|
| 19 |
+ {Role: WORKER, Autoaccept: true},
|
|
| 20 |
+ {Role: MANAGER, Autoaccept: false},
|
|
| 21 |
+ } |
|
| 22 |
+) |
|
| 23 |
+ |
|
| 24 |
+// NodeAddrOption is a pflag.Value for listen and remote addresses |
|
| 25 |
+type NodeAddrOption struct {
|
|
| 26 |
+ addr string |
|
| 27 |
+} |
|
| 28 |
+ |
|
| 29 |
+// String prints the representation of this flag |
|
| 30 |
+func (a *NodeAddrOption) String() string {
|
|
| 31 |
+ return a.addr |
|
| 32 |
+} |
|
| 33 |
+ |
|
| 34 |
+// Set the value for this flag |
|
| 35 |
+func (a *NodeAddrOption) Set(value string) error {
|
|
| 36 |
+ if !strings.Contains(value, ":") {
|
|
| 37 |
+ return fmt.Errorf("Invalud url, a host and port are required")
|
|
| 38 |
+ } |
|
| 39 |
+ |
|
| 40 |
+ parts := strings.Split(value, ":") |
|
| 41 |
+ if len(parts) != 2 {
|
|
| 42 |
+ return fmt.Errorf("Invalud url, too many colons")
|
|
| 43 |
+ } |
|
| 44 |
+ |
|
| 45 |
+ a.addr = value |
|
| 46 |
+ return nil |
|
| 47 |
+} |
|
| 48 |
+ |
|
| 49 |
+// Type returns the type of this flag |
|
| 50 |
+func (a *NodeAddrOption) Type() string {
|
|
| 51 |
+ return "node-addr" |
|
| 52 |
+} |
|
| 53 |
+ |
|
| 54 |
+// NewNodeAddrOption returns a new node address option |
|
| 55 |
+func NewNodeAddrOption() NodeAddrOption {
|
|
| 56 |
+ return NodeAddrOption{addr: defaultListenAddr}
|
|
| 57 |
+} |
|
| 58 |
+ |
|
| 59 |
+// AutoAcceptOption is a value type for auto-accept policy |
|
| 60 |
+type AutoAcceptOption struct {
|
|
| 61 |
+ values map[string]bool |
|
| 62 |
+} |
|
| 63 |
+ |
|
| 64 |
+// String prints a string representation of this option |
|
| 65 |
+func (o *AutoAcceptOption) String() string {
|
|
| 66 |
+ keys := []string{}
|
|
| 67 |
+ for key := range o.values {
|
|
| 68 |
+ keys = append(keys, key) |
|
| 69 |
+ } |
|
| 70 |
+ return strings.Join(keys, " ") |
|
| 71 |
+} |
|
| 72 |
+ |
|
| 73 |
+// Set sets a new value on this option |
|
| 74 |
+func (o *AutoAcceptOption) Set(value string) error {
|
|
| 75 |
+ value = strings.ToUpper(value) |
|
| 76 |
+ switch value {
|
|
| 77 |
+ case "", "NONE": |
|
| 78 |
+ if accept, ok := o.values[WORKER]; ok && accept {
|
|
| 79 |
+ return fmt.Errorf("value NONE is incompatible with %s", WORKER)
|
|
| 80 |
+ } |
|
| 81 |
+ if accept, ok := o.values[MANAGER]; ok && accept {
|
|
| 82 |
+ return fmt.Errorf("value NONE is incompatible with %s", MANAGER)
|
|
| 83 |
+ } |
|
| 84 |
+ o.values[WORKER] = false |
|
| 85 |
+ o.values[MANAGER] = false |
|
| 86 |
+ case WORKER, MANAGER: |
|
| 87 |
+ if accept, ok := o.values[value]; ok && !accept {
|
|
| 88 |
+ return fmt.Errorf("value NONE is incompatible with %s", value)
|
|
| 89 |
+ } |
|
| 90 |
+ o.values[value] = true |
|
| 91 |
+ default: |
|
| 92 |
+ return fmt.Errorf("must be one of %s, %s, NONE", WORKER, MANAGER)
|
|
| 93 |
+ } |
|
| 94 |
+ |
|
| 95 |
+ return nil |
|
| 96 |
+} |
|
| 97 |
+ |
|
| 98 |
+// Type returns the type of this option |
|
| 99 |
+func (o *AutoAcceptOption) Type() string {
|
|
| 100 |
+ return "auto-accept" |
|
| 101 |
+} |
|
| 102 |
+ |
|
| 103 |
+// Policies returns a representation of this option for the api |
|
| 104 |
+func (o *AutoAcceptOption) Policies(secret string) []swarm.Policy {
|
|
| 105 |
+ policies := []swarm.Policy{}
|
|
| 106 |
+ for _, p := range defaultPolicies {
|
|
| 107 |
+ if len(o.values) != 0 {
|
|
| 108 |
+ p.Autoaccept = o.values[string(p.Role)] |
|
| 109 |
+ } |
|
| 110 |
+ p.Secret = secret |
|
| 111 |
+ policies = append(policies, p) |
|
| 112 |
+ } |
|
| 113 |
+ return policies |
|
| 114 |
+} |
|
| 115 |
+ |
|
| 116 |
+// NewAutoAcceptOption returns a new auto-accept option |
|
| 117 |
+func NewAutoAcceptOption() AutoAcceptOption {
|
|
| 118 |
+ return AutoAcceptOption{values: make(map[string]bool)}
|
|
| 119 |
+} |
| 0 | 120 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,93 @@ |
| 0 |
+package swarm |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ |
|
| 5 |
+ "golang.org/x/net/context" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/docker/docker/api/client" |
|
| 8 |
+ "github.com/docker/docker/cli" |
|
| 9 |
+ "github.com/docker/engine-api/types/swarm" |
|
| 10 |
+ "github.com/spf13/cobra" |
|
| 11 |
+ "github.com/spf13/pflag" |
|
| 12 |
+) |
|
| 13 |
+ |
|
| 14 |
+type updateOptions struct {
|
|
| 15 |
+ autoAccept AutoAcceptOption |
|
| 16 |
+ secret string |
|
| 17 |
+ taskHistoryLimit int64 |
|
| 18 |
+ heartbeatPeriod uint64 |
|
| 19 |
+} |
|
| 20 |
+ |
|
| 21 |
+func newUpdateCommand(dockerCli *client.DockerCli) *cobra.Command {
|
|
| 22 |
+ opts := updateOptions{autoAccept: NewAutoAcceptOption()}
|
|
| 23 |
+ var flags *pflag.FlagSet |
|
| 24 |
+ |
|
| 25 |
+ cmd := &cobra.Command{
|
|
| 26 |
+ Use: "update", |
|
| 27 |
+ Short: "update the Swarm.", |
|
| 28 |
+ Args: cli.NoArgs, |
|
| 29 |
+ RunE: func(cmd *cobra.Command, args []string) error {
|
|
| 30 |
+ return runUpdate(dockerCli, flags, opts) |
|
| 31 |
+ }, |
|
| 32 |
+ } |
|
| 33 |
+ |
|
| 34 |
+ flags = cmd.Flags() |
|
| 35 |
+ flags.Var(&opts.autoAccept, "auto-accept", "Auto acceptance policy (worker, manager or none)") |
|
| 36 |
+ flags.StringVar(&opts.secret, "secret", "", "Set secret value needed to accept nodes into cluster") |
|
| 37 |
+ flags.Int64Var(&opts.taskHistoryLimit, "task-history-limit", 10, "Task history retention limit") |
|
| 38 |
+ flags.Uint64Var(&opts.heartbeatPeriod, "dispatcher-heartbeat-period", 5000000000, "Dispatcher heartbeat period") |
|
| 39 |
+ return cmd |
|
| 40 |
+} |
|
| 41 |
+ |
|
| 42 |
+func runUpdate(dockerCli *client.DockerCli, flags *pflag.FlagSet, opts updateOptions) error {
|
|
| 43 |
+ client := dockerCli.Client() |
|
| 44 |
+ ctx := context.Background() |
|
| 45 |
+ |
|
| 46 |
+ swarm, err := client.SwarmInspect(ctx) |
|
| 47 |
+ if err != nil {
|
|
| 48 |
+ return err |
|
| 49 |
+ } |
|
| 50 |
+ |
|
| 51 |
+ err = mergeSwarm(&swarm, flags) |
|
| 52 |
+ if err != nil {
|
|
| 53 |
+ return err |
|
| 54 |
+ } |
|
| 55 |
+ err = client.SwarmUpdate(ctx, swarm.Version, swarm.Spec) |
|
| 56 |
+ if err != nil {
|
|
| 57 |
+ return err |
|
| 58 |
+ } |
|
| 59 |
+ |
|
| 60 |
+ fmt.Println("Swarm updated.")
|
|
| 61 |
+ return nil |
|
| 62 |
+} |
|
| 63 |
+ |
|
| 64 |
+func mergeSwarm(swarm *swarm.Swarm, flags *pflag.FlagSet) error {
|
|
| 65 |
+ spec := &swarm.Spec |
|
| 66 |
+ |
|
| 67 |
+ if flags.Changed("auto-accept") {
|
|
| 68 |
+ value := flags.Lookup("auto-accept").Value.(*AutoAcceptOption)
|
|
| 69 |
+ if len(spec.AcceptancePolicy.Policies) > 0 {
|
|
| 70 |
+ spec.AcceptancePolicy.Policies = value.Policies(spec.AcceptancePolicy.Policies[0].Secret) |
|
| 71 |
+ } else {
|
|
| 72 |
+ spec.AcceptancePolicy.Policies = value.Policies("")
|
|
| 73 |
+ } |
|
| 74 |
+ } |
|
| 75 |
+ |
|
| 76 |
+ if flags.Changed("secret") {
|
|
| 77 |
+ secret, _ := flags.GetString("secret")
|
|
| 78 |
+ for _, policy := range spec.AcceptancePolicy.Policies {
|
|
| 79 |
+ policy.Secret = secret |
|
| 80 |
+ } |
|
| 81 |
+ } |
|
| 82 |
+ |
|
| 83 |
+ if flags.Changed("task-history-limit") {
|
|
| 84 |
+ spec.Orchestration.TaskHistoryRetentionLimit, _ = flags.GetInt64("task-history-limit")
|
|
| 85 |
+ } |
|
| 86 |
+ |
|
| 87 |
+ if flags.Changed("dispatcher-heartbeat-period") {
|
|
| 88 |
+ spec.Dispatcher.HeartbeatPeriod, _ = flags.GetUint64("dispatcher-heartbeat-period")
|
|
| 89 |
+ } |
|
| 90 |
+ |
|
| 91 |
+ return nil |
|
| 92 |
+} |
| 0 | 93 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,20 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "golang.org/x/net/context" |
|
| 4 |
+ |
|
| 5 |
+ Cli "github.com/docker/docker/cli" |
|
| 6 |
+ flag "github.com/docker/docker/pkg/mflag" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+// CmdTag tags an image into a repository. |
|
| 10 |
+// |
|
| 11 |
+// Usage: docker tag [OPTIONS] IMAGE[:TAG] [REGISTRYHOST/][USERNAME/]NAME[:TAG] |
|
| 12 |
+func (cli *DockerCli) CmdTag(args ...string) error {
|
|
| 13 |
+ cmd := Cli.Subcmd("tag", []string{"IMAGE[:TAG] [REGISTRYHOST/][USERNAME/]NAME[:TAG]"}, Cli.DockerCommands["tag"].Description, true)
|
|
| 14 |
+ cmd.Require(flag.Exact, 2) |
|
| 15 |
+ |
|
| 16 |
+ cmd.ParseFlags(args, true) |
|
| 17 |
+ |
|
| 18 |
+ return cli.client.ImageTag(context.Background(), cmd.Arg(0), cmd.Arg(1)) |
|
| 19 |
+} |
| 0 | 20 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,79 @@ |
| 0 |
+package task |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ "sort" |
|
| 5 |
+ "strings" |
|
| 6 |
+ "text/tabwriter" |
|
| 7 |
+ "time" |
|
| 8 |
+ |
|
| 9 |
+ "golang.org/x/net/context" |
|
| 10 |
+ |
|
| 11 |
+ "github.com/docker/docker/api/client" |
|
| 12 |
+ "github.com/docker/docker/api/client/idresolver" |
|
| 13 |
+ "github.com/docker/engine-api/types/swarm" |
|
| 14 |
+ "github.com/docker/go-units" |
|
| 15 |
+) |
|
| 16 |
+ |
|
| 17 |
+const ( |
|
| 18 |
+ psTaskItemFmt = "%s\t%s\t%s\t%s\t%s %s\t%s\t%s\n" |
|
| 19 |
+) |
|
| 20 |
+ |
|
| 21 |
+type tasksBySlot []swarm.Task |
|
| 22 |
+ |
|
| 23 |
+func (t tasksBySlot) Len() int {
|
|
| 24 |
+ return len(t) |
|
| 25 |
+} |
|
| 26 |
+ |
|
| 27 |
+func (t tasksBySlot) Swap(i, j int) {
|
|
| 28 |
+ t[i], t[j] = t[j], t[i] |
|
| 29 |
+} |
|
| 30 |
+ |
|
| 31 |
+func (t tasksBySlot) Less(i, j int) bool {
|
|
| 32 |
+ // Sort by slot. |
|
| 33 |
+ if t[i].Slot != t[j].Slot {
|
|
| 34 |
+ return t[i].Slot < t[j].Slot |
|
| 35 |
+ } |
|
| 36 |
+ |
|
| 37 |
+ // If same slot, sort by most recent. |
|
| 38 |
+ return t[j].Meta.CreatedAt.Before(t[i].CreatedAt) |
|
| 39 |
+} |
|
| 40 |
+ |
|
| 41 |
+// Print task information in a table format |
|
| 42 |
+func Print(dockerCli *client.DockerCli, ctx context.Context, tasks []swarm.Task, resolver *idresolver.IDResolver) error {
|
|
| 43 |
+ sort.Stable(tasksBySlot(tasks)) |
|
| 44 |
+ |
|
| 45 |
+ writer := tabwriter.NewWriter(dockerCli.Out(), 0, 4, 2, ' ', 0) |
|
| 46 |
+ |
|
| 47 |
+ // Ignore flushing errors |
|
| 48 |
+ defer writer.Flush() |
|
| 49 |
+ fmt.Fprintln(writer, strings.Join([]string{"ID", "NAME", "SERVICE", "IMAGE", "LAST STATE", "DESIRED STATE", "NODE"}, "\t"))
|
|
| 50 |
+ for _, task := range tasks {
|
|
| 51 |
+ serviceValue, err := resolver.Resolve(ctx, swarm.Service{}, task.ServiceID)
|
|
| 52 |
+ if err != nil {
|
|
| 53 |
+ return err |
|
| 54 |
+ } |
|
| 55 |
+ nodeValue, err := resolver.Resolve(ctx, swarm.Node{}, task.NodeID)
|
|
| 56 |
+ if err != nil {
|
|
| 57 |
+ return err |
|
| 58 |
+ } |
|
| 59 |
+ name := serviceValue |
|
| 60 |
+ if task.Slot > 0 {
|
|
| 61 |
+ name = fmt.Sprintf("%s.%d", name, task.Slot)
|
|
| 62 |
+ } |
|
| 63 |
+ fmt.Fprintf( |
|
| 64 |
+ writer, |
|
| 65 |
+ psTaskItemFmt, |
|
| 66 |
+ task.ID, |
|
| 67 |
+ name, |
|
| 68 |
+ serviceValue, |
|
| 69 |
+ task.Spec.ContainerSpec.Image, |
|
| 70 |
+ client.PrettyPrint(task.Status.State), |
|
| 71 |
+ units.HumanDuration(time.Since(task.Status.Timestamp)), |
|
| 72 |
+ client.PrettyPrint(task.DesiredState), |
|
| 73 |
+ nodeValue, |
|
| 74 |
+ ) |
|
| 75 |
+ } |
|
| 76 |
+ |
|
| 77 |
+ return nil |
|
| 78 |
+} |
| ... | ... |
@@ -8,6 +8,7 @@ import ( |
| 8 | 8 |
gosignal "os/signal" |
| 9 | 9 |
"path/filepath" |
| 10 | 10 |
"runtime" |
| 11 |
+ "strings" |
|
| 11 | 12 |
"time" |
| 12 | 13 |
|
| 13 | 14 |
"golang.org/x/net/context" |
| ... | ... |
@@ -163,3 +164,27 @@ func (cli *DockerCli) ForwardAllSignals(ctx context.Context, cid string) chan os |
| 163 | 163 |
}() |
| 164 | 164 |
return sigc |
| 165 | 165 |
} |
| 166 |
+ |
|
| 167 |
+// capitalizeFirst capitalizes the first character of string |
|
| 168 |
+func capitalizeFirst(s string) string {
|
|
| 169 |
+ switch l := len(s); l {
|
|
| 170 |
+ case 0: |
|
| 171 |
+ return s |
|
| 172 |
+ case 1: |
|
| 173 |
+ return strings.ToLower(s) |
|
| 174 |
+ default: |
|
| 175 |
+ return strings.ToUpper(string(s[0])) + strings.ToLower(s[1:]) |
|
| 176 |
+ } |
|
| 177 |
+} |
|
| 178 |
+ |
|
| 179 |
+// PrettyPrint outputs arbitrary data for human formatted output by uppercasing the first letter. |
|
| 180 |
+func PrettyPrint(i interface{}) string {
|
|
| 181 |
+ switch t := i.(type) {
|
|
| 182 |
+ case nil: |
|
| 183 |
+ return "None" |
|
| 184 |
+ case string: |
|
| 185 |
+ return capitalizeFirst(t) |
|
| 186 |
+ default: |
|
| 187 |
+ return capitalizeFirst(fmt.Sprintf("%s", t))
|
|
| 188 |
+ } |
|
| 189 |
+} |
| ... | ... |
@@ -5,7 +5,10 @@ import ( |
| 5 | 5 |
"github.com/docker/docker/api/client/container" |
| 6 | 6 |
"github.com/docker/docker/api/client/image" |
| 7 | 7 |
"github.com/docker/docker/api/client/network" |
| 8 |
+ "github.com/docker/docker/api/client/node" |
|
| 8 | 9 |
"github.com/docker/docker/api/client/registry" |
| 10 |
+ "github.com/docker/docker/api/client/service" |
|
| 11 |
+ "github.com/docker/docker/api/client/swarm" |
|
| 9 | 12 |
"github.com/docker/docker/api/client/system" |
| 10 | 13 |
"github.com/docker/docker/api/client/volume" |
| 11 | 14 |
"github.com/docker/docker/cli" |
| ... | ... |
@@ -36,6 +39,9 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor {
|
| 36 | 36 |
rootCmd.SetFlagErrorFunc(cli.FlagErrorFunc) |
| 37 | 37 |
rootCmd.SetOutput(stdout) |
| 38 | 38 |
rootCmd.AddCommand( |
| 39 |
+ node.NewNodeCommand(dockerCli), |
|
| 40 |
+ service.NewServiceCommand(dockerCli), |
|
| 41 |
+ swarm.NewSwarmCommand(dockerCli), |
|
| 39 | 42 |
container.NewAttachCommand(dockerCli), |
| 40 | 43 |
container.NewCommitCommand(dockerCli), |
| 41 | 44 |
container.NewCreateCommand(dockerCli), |
| ... | ... |
@@ -11,7 +11,7 @@ var DockerCommandUsage = []Command{
|
| 11 | 11 |
{"cp", "Copy files/folders between a container and the local filesystem"},
|
| 12 | 12 |
{"exec", "Run a command in a running container"},
|
| 13 | 13 |
{"info", "Display system-wide information"},
|
| 14 |
- {"inspect", "Return low-level information on a container or image"},
|
|
| 14 |
+ {"inspect", "Return low-level information on a container, image or task"},
|
|
| 15 | 15 |
{"update", "Update configuration of one or more containers"},
|
| 16 | 16 |
} |
| 17 | 17 |
|
| ... | ... |
@@ -63,7 +63,7 @@ func (s *DockerSuite) TestRenameCheckNames(c *check.C) {
|
| 63 | 63 |
|
| 64 | 64 |
name, err := inspectFieldWithError("first_name", "Name")
|
| 65 | 65 |
c.Assert(err, checker.NotNil, check.Commentf(name)) |
| 66 |
- c.Assert(err.Error(), checker.Contains, "No such image or container: first_name") |
|
| 66 |
+ c.Assert(err.Error(), checker.Contains, "No such image, container or task: first_name") |
|
| 67 | 67 |
} |
| 68 | 68 |
|
| 69 | 69 |
func (s *DockerSuite) TestRenameInvalidName(c *check.C) {
|