mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
Add --format
for docker node ls
This fix tries to address the comment https://github.com/docker/docker/pull/30376#discussion_r97465334 where it was not possible to specify `--format` for `docker node ls`. The `--format` flag is a quite useful flag that could be used in many places such as completion. This fix implements `--format` for `docker node ls` and add `nodesFormat` in config.json so that it is possible to specify the output when `docker node ls` is invoked. Related documentations have been updated. A set of unit tests have been added. This fix is related to #30376. Signed-off-by: Yong Tang <yong.tang.github@outlook.com>
This commit is contained in:
parent
f6b7dc9837
commit
32e21ecbfe
7 changed files with 439 additions and 93 deletions
99
cli/command/formatter/node.go
Normal file
99
cli/command/formatter/node.go
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
package formatter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/swarm"
|
||||||
|
"github.com/docker/docker/cli/command"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultNodeTableFormat = "table {{.ID}}\t{{.Hostname}}\t{{.Status}}\t{{.Availability}}\t{{.ManagerStatus}}"
|
||||||
|
|
||||||
|
nodeIDHeader = "ID"
|
||||||
|
hostnameHeader = "HOSTNAME"
|
||||||
|
availabilityHeader = "AVAILABILITY"
|
||||||
|
managerStatusHeader = "MANAGER STATUS"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewNodeFormat returns a Format for rendering using a node Context
|
||||||
|
func NewNodeFormat(source string, quiet bool) Format {
|
||||||
|
switch source {
|
||||||
|
case TableFormatKey:
|
||||||
|
if quiet {
|
||||||
|
return defaultQuietFormat
|
||||||
|
}
|
||||||
|
return defaultNodeTableFormat
|
||||||
|
case RawFormatKey:
|
||||||
|
if quiet {
|
||||||
|
return `node_id: {{.ID}}`
|
||||||
|
}
|
||||||
|
return `node_id: {{.ID}}\nhostname: {{.Hostname}}\nstatus: {{.Status}}\navailability: {{.Availability}}\nmanager_status: {{.ManagerStatus}}\n`
|
||||||
|
}
|
||||||
|
return Format(source)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NodeWrite writes the context
|
||||||
|
func NodeWrite(ctx Context, nodes []swarm.Node, info types.Info) error {
|
||||||
|
render := func(format func(subContext subContext) error) error {
|
||||||
|
for _, node := range nodes {
|
||||||
|
nodeCtx := &nodeContext{n: node, info: info}
|
||||||
|
if err := format(nodeCtx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
nodeCtx := nodeContext{}
|
||||||
|
nodeCtx.header = nodeHeaderContext{
|
||||||
|
"ID": nodeIDHeader,
|
||||||
|
"Hostname": hostnameHeader,
|
||||||
|
"Status": statusHeader,
|
||||||
|
"Availability": availabilityHeader,
|
||||||
|
"ManagerStatus": managerStatusHeader,
|
||||||
|
}
|
||||||
|
return ctx.Write(&nodeCtx, render)
|
||||||
|
}
|
||||||
|
|
||||||
|
type nodeHeaderContext map[string]string
|
||||||
|
|
||||||
|
type nodeContext struct {
|
||||||
|
HeaderContext
|
||||||
|
n swarm.Node
|
||||||
|
info types.Info
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *nodeContext) MarshalJSON() ([]byte, error) {
|
||||||
|
return marshalJSON(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *nodeContext) ID() string {
|
||||||
|
nodeID := c.n.ID
|
||||||
|
if nodeID == c.info.Swarm.NodeID {
|
||||||
|
nodeID = nodeID + " *"
|
||||||
|
}
|
||||||
|
return nodeID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *nodeContext) Hostname() string {
|
||||||
|
return c.n.Description.Hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *nodeContext) Status() string {
|
||||||
|
return command.PrettyPrint(string(c.n.Status.State))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *nodeContext) Availability() string {
|
||||||
|
return command.PrettyPrint(string(c.n.Spec.Availability))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *nodeContext) ManagerStatus() string {
|
||||||
|
reachability := ""
|
||||||
|
if c.n.ManagerStatus != nil {
|
||||||
|
if c.n.ManagerStatus.Leader {
|
||||||
|
reachability = "Leader"
|
||||||
|
} else {
|
||||||
|
reachability = string(c.n.ManagerStatus.Reachability)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return command.PrettyPrint(reachability)
|
||||||
|
}
|
188
cli/command/formatter/node_test.go
Normal file
188
cli/command/formatter/node_test.go
Normal file
|
@ -0,0 +1,188 @@
|
||||||
|
package formatter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/swarm"
|
||||||
|
"github.com/docker/docker/pkg/stringid"
|
||||||
|
"github.com/docker/docker/pkg/testutil/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNodeContext(t *testing.T) {
|
||||||
|
nodeID := stringid.GenerateRandomID()
|
||||||
|
|
||||||
|
var ctx nodeContext
|
||||||
|
cases := []struct {
|
||||||
|
nodeCtx nodeContext
|
||||||
|
expValue string
|
||||||
|
call func() string
|
||||||
|
}{
|
||||||
|
{nodeContext{
|
||||||
|
n: swarm.Node{ID: nodeID},
|
||||||
|
}, nodeID, ctx.ID},
|
||||||
|
{nodeContext{
|
||||||
|
n: swarm.Node{Description: swarm.NodeDescription{Hostname: "node_hostname"}},
|
||||||
|
}, "node_hostname", ctx.Hostname},
|
||||||
|
{nodeContext{
|
||||||
|
n: swarm.Node{Status: swarm.NodeStatus{State: swarm.NodeState("foo")}},
|
||||||
|
}, "Foo", ctx.Status},
|
||||||
|
{nodeContext{
|
||||||
|
n: swarm.Node{Spec: swarm.NodeSpec{Availability: swarm.NodeAvailability("drain")}},
|
||||||
|
}, "Drain", ctx.Availability},
|
||||||
|
{nodeContext{
|
||||||
|
n: swarm.Node{ManagerStatus: &swarm.ManagerStatus{Leader: true}},
|
||||||
|
}, "Leader", ctx.ManagerStatus},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
ctx = c.nodeCtx
|
||||||
|
v := c.call()
|
||||||
|
if strings.Contains(v, ",") {
|
||||||
|
compareMultipleValues(t, v, c.expValue)
|
||||||
|
} else if v != c.expValue {
|
||||||
|
t.Fatalf("Expected %s, was %s\n", c.expValue, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNodeContextWrite(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
context Context
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
|
||||||
|
// Errors
|
||||||
|
{
|
||||||
|
Context{Format: "{{InvalidFunction}}"},
|
||||||
|
`Template parsing error: template: :1: function "InvalidFunction" not defined
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Context{Format: "{{nil}}"},
|
||||||
|
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
// Table format
|
||||||
|
{
|
||||||
|
Context{Format: NewNodeFormat("table", false)},
|
||||||
|
`ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS
|
||||||
|
nodeID1 foobar_baz Foo Drain Leader
|
||||||
|
nodeID2 foobar_bar Bar Active Reachable
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Context{Format: NewNodeFormat("table", true)},
|
||||||
|
`nodeID1
|
||||||
|
nodeID2
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Context{Format: NewNodeFormat("table {{.Hostname}}", false)},
|
||||||
|
`HOSTNAME
|
||||||
|
foobar_baz
|
||||||
|
foobar_bar
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Context{Format: NewNodeFormat("table {{.Hostname}}", true)},
|
||||||
|
`HOSTNAME
|
||||||
|
foobar_baz
|
||||||
|
foobar_bar
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
// Raw Format
|
||||||
|
{
|
||||||
|
Context{Format: NewNodeFormat("raw", false)},
|
||||||
|
`node_id: nodeID1
|
||||||
|
hostname: foobar_baz
|
||||||
|
status: Foo
|
||||||
|
availability: Drain
|
||||||
|
manager_status: Leader
|
||||||
|
|
||||||
|
node_id: nodeID2
|
||||||
|
hostname: foobar_bar
|
||||||
|
status: Bar
|
||||||
|
availability: Active
|
||||||
|
manager_status: Reachable
|
||||||
|
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Context{Format: NewNodeFormat("raw", true)},
|
||||||
|
`node_id: nodeID1
|
||||||
|
node_id: nodeID2
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
// Custom Format
|
||||||
|
{
|
||||||
|
Context{Format: NewNodeFormat("{{.Hostname}}", false)},
|
||||||
|
`foobar_baz
|
||||||
|
foobar_bar
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testcase := range cases {
|
||||||
|
nodes := []swarm.Node{
|
||||||
|
{ID: "nodeID1", Description: swarm.NodeDescription{Hostname: "foobar_baz"}, Status: swarm.NodeStatus{State: swarm.NodeState("foo")}, Spec: swarm.NodeSpec{Availability: swarm.NodeAvailability("drain")}, ManagerStatus: &swarm.ManagerStatus{Leader: true}},
|
||||||
|
{ID: "nodeID2", Description: swarm.NodeDescription{Hostname: "foobar_bar"}, Status: swarm.NodeStatus{State: swarm.NodeState("bar")}, Spec: swarm.NodeSpec{Availability: swarm.NodeAvailability("active")}, ManagerStatus: &swarm.ManagerStatus{Leader: false, Reachability: swarm.Reachability("Reachable")}},
|
||||||
|
}
|
||||||
|
out := bytes.NewBufferString("")
|
||||||
|
testcase.context.Output = out
|
||||||
|
err := NodeWrite(testcase.context, nodes, types.Info{})
|
||||||
|
if err != nil {
|
||||||
|
assert.Error(t, err, testcase.expected)
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, out.String(), testcase.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNodeContextWriteJSON(t *testing.T) {
|
||||||
|
nodes := []swarm.Node{
|
||||||
|
{ID: "nodeID1", Description: swarm.NodeDescription{Hostname: "foobar_baz"}},
|
||||||
|
{ID: "nodeID2", Description: swarm.NodeDescription{Hostname: "foobar_bar"}},
|
||||||
|
}
|
||||||
|
expectedJSONs := []map[string]interface{}{
|
||||||
|
{"Availability": "", "Hostname": "foobar_baz", "ID": "nodeID1", "ManagerStatus": "", "Status": ""},
|
||||||
|
{"Availability": "", "Hostname": "foobar_bar", "ID": "nodeID2", "ManagerStatus": "", "Status": ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
out := bytes.NewBufferString("")
|
||||||
|
err := NodeWrite(Context{Format: "{{json .}}", Output: out}, nodes, types.Info{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
|
||||||
|
t.Logf("Output: line %d: %s", i, line)
|
||||||
|
var m map[string]interface{}
|
||||||
|
if err := json.Unmarshal([]byte(line), &m); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
assert.DeepEqual(t, m, expectedJSONs[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNodeContextWriteJSONField(t *testing.T) {
|
||||||
|
nodes := []swarm.Node{
|
||||||
|
{ID: "nodeID1", Description: swarm.NodeDescription{Hostname: "foobar_baz"}},
|
||||||
|
{ID: "nodeID2", Description: swarm.NodeDescription{Hostname: "foobar_bar"}},
|
||||||
|
}
|
||||||
|
out := bytes.NewBufferString("")
|
||||||
|
err := NodeWrite(Context{Format: "{{json .ID}}", Output: out}, nodes, types.Info{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
|
||||||
|
t.Logf("Output: line %d: %s", i, line)
|
||||||
|
var s string
|
||||||
|
if err := json.Unmarshal([]byte(line), &s); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
assert.Equal(t, s, nodes[i].ID)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,26 +1,19 @@
|
||||||
package node
|
package node
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"text/tabwriter"
|
|
||||||
|
|
||||||
"golang.org/x/net/context"
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/swarm"
|
|
||||||
"github.com/docker/docker/cli"
|
"github.com/docker/docker/cli"
|
||||||
"github.com/docker/docker/cli/command"
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/docker/docker/cli/command/formatter"
|
||||||
"github.com/docker/docker/opts"
|
"github.com/docker/docker/opts"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
listItemFmt = "%s\t%s\t%s\t%s\t%s\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
type listOptions struct {
|
type listOptions struct {
|
||||||
quiet bool
|
quiet bool
|
||||||
|
format string
|
||||||
filter opts.FilterOpt
|
filter opts.FilterOpt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,6 +31,7 @@ func newListCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
}
|
}
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs")
|
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs")
|
||||||
|
flags.StringVar(&opts.format, "format", "", "Pretty-print nodes using a Go template")
|
||||||
flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided")
|
flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided")
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
|
@ -45,7 +39,6 @@ func newListCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
|
|
||||||
func runList(dockerCli command.Cli, opts listOptions) error {
|
func runList(dockerCli command.Cli, opts listOptions) error {
|
||||||
client := dockerCli.Client()
|
client := dockerCli.Client()
|
||||||
out := dockerCli.Out()
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
nodes, err := client.NodeList(
|
nodes, err := client.NodeList(
|
||||||
|
@ -55,61 +48,26 @@ func runList(dockerCli command.Cli, opts listOptions) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
info := types.Info{}
|
||||||
if len(nodes) > 0 && !opts.quiet {
|
if len(nodes) > 0 && !opts.quiet {
|
||||||
// only non-empty nodes and not quiet, should we call /info api
|
// only non-empty nodes and not quiet, should we call /info api
|
||||||
info, err := client.Info(ctx)
|
info, err = client.Info(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
printTable(out, nodes, info)
|
|
||||||
} else if !opts.quiet {
|
|
||||||
// no nodes and not quiet, print only one line with columns ID, HOSTNAME, ...
|
|
||||||
printTable(out, nodes, types.Info{})
|
|
||||||
} else {
|
|
||||||
printQuiet(out, nodes)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
format := opts.format
|
||||||
}
|
if len(format) == 0 {
|
||||||
|
format = formatter.TableFormatKey
|
||||||
func printTable(out io.Writer, nodes []swarm.Node, info types.Info) {
|
if len(dockerCli.ConfigFile().NodesFormat) > 0 && !opts.quiet {
|
||||||
writer := tabwriter.NewWriter(out, 0, 4, 2, ' ', 0)
|
format = dockerCli.ConfigFile().NodesFormat
|
||||||
|
|
||||||
// Ignore flushing errors
|
|
||||||
defer writer.Flush()
|
|
||||||
|
|
||||||
fmt.Fprintf(writer, listItemFmt, "ID", "HOSTNAME", "STATUS", "AVAILABILITY", "MANAGER STATUS")
|
|
||||||
for _, node := range nodes {
|
|
||||||
name := node.Description.Hostname
|
|
||||||
availability := string(node.Spec.Availability)
|
|
||||||
|
|
||||||
reachability := ""
|
|
||||||
if node.ManagerStatus != nil {
|
|
||||||
if node.ManagerStatus.Leader {
|
|
||||||
reachability = "Leader"
|
|
||||||
} else {
|
|
||||||
reachability = string(node.ManagerStatus.Reachability)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ID := node.ID
|
|
||||||
if node.ID == info.Swarm.NodeID {
|
|
||||||
ID = ID + " *"
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintf(
|
|
||||||
writer,
|
|
||||||
listItemFmt,
|
|
||||||
ID,
|
|
||||||
name,
|
|
||||||
command.PrettyPrint(string(node.Status.State)),
|
|
||||||
command.PrettyPrint(availability),
|
|
||||||
command.PrettyPrint(reachability))
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func printQuiet(out io.Writer, nodes []swarm.Node) {
|
nodesCtx := formatter.Context{
|
||||||
for _, node := range nodes {
|
Output: dockerCli.Out(),
|
||||||
fmt.Fprintln(out, node.ID)
|
Format: formatter.NewNodeFormat(format, opts.quiet),
|
||||||
}
|
}
|
||||||
|
return formatter.NodeWrite(nodesCtx, nodes, info)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/swarm"
|
"github.com/docker/docker/api/types/swarm"
|
||||||
|
"github.com/docker/docker/cli/config/configfile"
|
||||||
"github.com/docker/docker/cli/internal/test"
|
"github.com/docker/docker/cli/internal/test"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
// Import builders to get the builder function as package function
|
// Import builders to get the builder function as package function
|
||||||
|
@ -42,11 +43,12 @@ func TestNodeListErrorOnAPIFailure(t *testing.T) {
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
cmd := newListCommand(
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
test.NewFakeCli(&fakeClient{
|
nodeListFunc: tc.nodeListFunc,
|
||||||
nodeListFunc: tc.nodeListFunc,
|
infoFunc: tc.infoFunc,
|
||||||
infoFunc: tc.infoFunc,
|
}, buf)
|
||||||
}, buf))
|
cli.SetConfigfile(&configfile.ConfigFile{})
|
||||||
|
cmd := newListCommand(cli)
|
||||||
cmd.SetOutput(ioutil.Discard)
|
cmd.SetOutput(ioutil.Discard)
|
||||||
assert.Error(t, cmd.Execute(), tc.expectedError)
|
assert.Error(t, cmd.Execute(), tc.expectedError)
|
||||||
}
|
}
|
||||||
|
@ -54,39 +56,41 @@ func TestNodeListErrorOnAPIFailure(t *testing.T) {
|
||||||
|
|
||||||
func TestNodeList(t *testing.T) {
|
func TestNodeList(t *testing.T) {
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
cmd := newListCommand(
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
test.NewFakeCli(&fakeClient{
|
nodeListFunc: func() ([]swarm.Node, error) {
|
||||||
nodeListFunc: func() ([]swarm.Node, error) {
|
return []swarm.Node{
|
||||||
return []swarm.Node{
|
*Node(NodeID("nodeID1"), Hostname("nodeHostname1"), Manager(Leader())),
|
||||||
*Node(NodeID("nodeID1"), Hostname("nodeHostname1"), Manager(Leader())),
|
*Node(NodeID("nodeID2"), Hostname("nodeHostname2"), Manager()),
|
||||||
*Node(NodeID("nodeID2"), Hostname("nodeHostname2"), Manager()),
|
*Node(NodeID("nodeID3"), Hostname("nodeHostname3")),
|
||||||
*Node(NodeID("nodeID3"), Hostname("nodeHostname3")),
|
}, nil
|
||||||
}, nil
|
},
|
||||||
},
|
infoFunc: func() (types.Info, error) {
|
||||||
infoFunc: func() (types.Info, error) {
|
return types.Info{
|
||||||
return types.Info{
|
Swarm: swarm.Info{
|
||||||
Swarm: swarm.Info{
|
NodeID: "nodeID1",
|
||||||
NodeID: "nodeID1",
|
},
|
||||||
},
|
}, nil
|
||||||
}, nil
|
},
|
||||||
},
|
}, buf)
|
||||||
}, buf))
|
cli.SetConfigfile(&configfile.ConfigFile{})
|
||||||
|
cmd := newListCommand(cli)
|
||||||
assert.NilError(t, cmd.Execute())
|
assert.NilError(t, cmd.Execute())
|
||||||
assert.Contains(t, buf.String(), `nodeID1 * nodeHostname1 Ready Active Leader`)
|
assert.Contains(t, buf.String(), `nodeID1 * nodeHostname1 Ready Active Leader`)
|
||||||
assert.Contains(t, buf.String(), `nodeID2 nodeHostname2 Ready Active Reachable`)
|
assert.Contains(t, buf.String(), `nodeID2 nodeHostname2 Ready Active Reachable`)
|
||||||
assert.Contains(t, buf.String(), `nodeID3 nodeHostname3 Ready Active`)
|
assert.Contains(t, buf.String(), `nodeID3 nodeHostname3 Ready Active`)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNodeListQuietShouldOnlyPrintIDs(t *testing.T) {
|
func TestNodeListQuietShouldOnlyPrintIDs(t *testing.T) {
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
cmd := newListCommand(
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
test.NewFakeCli(&fakeClient{
|
nodeListFunc: func() ([]swarm.Node, error) {
|
||||||
nodeListFunc: func() ([]swarm.Node, error) {
|
return []swarm.Node{
|
||||||
return []swarm.Node{
|
*Node(),
|
||||||
*Node(),
|
}, nil
|
||||||
}, nil
|
},
|
||||||
},
|
}, buf)
|
||||||
}, buf))
|
cli.SetConfigfile(&configfile.ConfigFile{})
|
||||||
|
cmd := newListCommand(cli)
|
||||||
cmd.Flags().Set("quiet", "true")
|
cmd.Flags().Set("quiet", "true")
|
||||||
assert.NilError(t, cmd.Execute())
|
assert.NilError(t, cmd.Execute())
|
||||||
assert.Contains(t, buf.String(), "nodeID")
|
assert.Contains(t, buf.String(), "nodeID")
|
||||||
|
@ -95,7 +99,64 @@ func TestNodeListQuietShouldOnlyPrintIDs(t *testing.T) {
|
||||||
// Test case for #24090
|
// Test case for #24090
|
||||||
func TestNodeListContainsHostname(t *testing.T) {
|
func TestNodeListContainsHostname(t *testing.T) {
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
cmd := newListCommand(test.NewFakeCli(&fakeClient{}, buf))
|
cli := test.NewFakeCli(&fakeClient{}, buf)
|
||||||
|
cli.SetConfigfile(&configfile.ConfigFile{})
|
||||||
|
cmd := newListCommand(cli)
|
||||||
assert.NilError(t, cmd.Execute())
|
assert.NilError(t, cmd.Execute())
|
||||||
assert.Contains(t, buf.String(), "HOSTNAME")
|
assert.Contains(t, buf.String(), "HOSTNAME")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNodeListDefaultFormat(t *testing.T) {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
|
nodeListFunc: func() ([]swarm.Node, error) {
|
||||||
|
return []swarm.Node{
|
||||||
|
*Node(NodeID("nodeID1"), Hostname("nodeHostname1"), Manager(Leader())),
|
||||||
|
*Node(NodeID("nodeID2"), Hostname("nodeHostname2"), Manager()),
|
||||||
|
*Node(NodeID("nodeID3"), Hostname("nodeHostname3")),
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
infoFunc: func() (types.Info, error) {
|
||||||
|
return types.Info{
|
||||||
|
Swarm: swarm.Info{
|
||||||
|
NodeID: "nodeID1",
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
}, buf)
|
||||||
|
cli.SetConfigfile(&configfile.ConfigFile{
|
||||||
|
NodesFormat: "{{.ID}}: {{.Hostname}} {{.Status}}/{{.ManagerStatus}}",
|
||||||
|
})
|
||||||
|
cmd := newListCommand(cli)
|
||||||
|
assert.NilError(t, cmd.Execute())
|
||||||
|
assert.Contains(t, buf.String(), `nodeID1 *: nodeHostname1 Ready/Leader`)
|
||||||
|
assert.Contains(t, buf.String(), `nodeID2: nodeHostname2 Ready/Reachable`)
|
||||||
|
assert.Contains(t, buf.String(), `nodeID3: nodeHostname3 Ready`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNodeListFormat(t *testing.T) {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
|
nodeListFunc: func() ([]swarm.Node, error) {
|
||||||
|
return []swarm.Node{
|
||||||
|
*Node(NodeID("nodeID1"), Hostname("nodeHostname1"), Manager(Leader())),
|
||||||
|
*Node(NodeID("nodeID2"), Hostname("nodeHostname2"), Manager()),
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
infoFunc: func() (types.Info, error) {
|
||||||
|
return types.Info{
|
||||||
|
Swarm: swarm.Info{
|
||||||
|
NodeID: "nodeID1",
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
}, buf)
|
||||||
|
cli.SetConfigfile(&configfile.ConfigFile{
|
||||||
|
NodesFormat: "{{.ID}}: {{.Hostname}} {{.Status}}/{{.ManagerStatus}}",
|
||||||
|
})
|
||||||
|
cmd := newListCommand(cli)
|
||||||
|
cmd.Flags().Set("format", "{{.Hostname}}: {{.ManagerStatus}}")
|
||||||
|
assert.NilError(t, cmd.Execute())
|
||||||
|
assert.Contains(t, buf.String(), `nodeHostname1: Leader`)
|
||||||
|
assert.Contains(t, buf.String(), `nodeHostname2: Reachable`)
|
||||||
|
}
|
||||||
|
|
|
@ -38,6 +38,7 @@ type ConfigFile struct {
|
||||||
ServicesFormat string `json:"servicesFormat,omitempty"`
|
ServicesFormat string `json:"servicesFormat,omitempty"`
|
||||||
TasksFormat string `json:"tasksFormat,omitempty"`
|
TasksFormat string `json:"tasksFormat,omitempty"`
|
||||||
SecretFormat string `json:"secretFormat,omitempty"`
|
SecretFormat string `json:"secretFormat,omitempty"`
|
||||||
|
NodesFormat string `json:"nodesFormat,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// LegacyLoadFromReader reads the non-nested configuration data given and sets up the
|
// LegacyLoadFromReader reads the non-nested configuration data given and sets up the
|
||||||
|
|
|
@ -167,6 +167,11 @@ property is not set, the client falls back to the default table
|
||||||
format. For a list of supported formatting directives, see
|
format. For a list of supported formatting directives, see
|
||||||
[**Formatting** section in the `docker secret ls` documentation](secret_ls.md)
|
[**Formatting** section in the `docker secret ls` documentation](secret_ls.md)
|
||||||
|
|
||||||
|
The property `nodesFormat` specifies the default format for `docker node ls` output.
|
||||||
|
When the `--format` flag is not provided with the `docker node ls` command,
|
||||||
|
Docker's client uses this property. If this property is not set, the client
|
||||||
|
falls back to the default table format. For a list of supported formatting
|
||||||
|
directives, see the [**Formatting** section in the `docker node ls` documentation](node_ls.md)
|
||||||
|
|
||||||
The property `credsStore` specifies an external binary to serve as the default
|
The property `credsStore` specifies an external binary to serve as the default
|
||||||
credential store. When this property is set, `docker login` will attempt to
|
credential store. When this property is set, `docker login` will attempt to
|
||||||
|
@ -214,6 +219,7 @@ Following is a sample `config.json` file:
|
||||||
"servicesFormat": "table {{.ID}}\t{{.Name}}\t{{.Mode}}",
|
"servicesFormat": "table {{.ID}}\t{{.Name}}\t{{.Mode}}",
|
||||||
"secretFormat": "table {{.ID}}\t{{.Name}}\t{{.CreatedAt}}\t{{.UpdatedAt}}",
|
"secretFormat": "table {{.ID}}\t{{.Name}}\t{{.CreatedAt}}\t{{.UpdatedAt}}",
|
||||||
"serviceInspectFormat": "pretty",
|
"serviceInspectFormat": "pretty",
|
||||||
|
"nodesFormat": "table {{.ID}}\t{{.Hostname}}\t{{.Availability}}",
|
||||||
"detachKeys": "ctrl-e,e",
|
"detachKeys": "ctrl-e,e",
|
||||||
"credsStore": "secretservice",
|
"credsStore": "secretservice",
|
||||||
"credHelpers": {
|
"credHelpers": {
|
||||||
|
|
|
@ -24,9 +24,10 @@ Aliases:
|
||||||
ls, list
|
ls, list
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-f, --filter value Filter output based on conditions provided
|
-f, --filter filter Filter output based on conditions provided
|
||||||
--help Print usage
|
--format string Pretty-print nodes using a Go template
|
||||||
-q, --quiet Only display IDs
|
--help Print usage
|
||||||
|
-q, --quiet Only display IDs
|
||||||
```
|
```
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
@ -45,6 +46,10 @@ ID HOSTNAME STATUS AVAILABILITY MANAGER STATU
|
||||||
38ciaotwjuritcdtn9npbnkuz swarm-worker1 Ready Active
|
38ciaotwjuritcdtn9npbnkuz swarm-worker1 Ready Active
|
||||||
e216jshn25ckzbvmwlnh5jr3g * swarm-manager1 Ready Active Leader
|
e216jshn25ckzbvmwlnh5jr3g * swarm-manager1 Ready Active Leader
|
||||||
```
|
```
|
||||||
|
> **Note:**
|
||||||
|
> If the `ID` field of the node is followed by a `*` (e.g., `e216jshn25ckzbvmwlnh5jr3g *`)
|
||||||
|
> in the above example output, then this node is also the node of the current docker daemon.
|
||||||
|
|
||||||
|
|
||||||
### Filtering
|
### Filtering
|
||||||
|
|
||||||
|
@ -124,6 +129,34 @@ ID HOSTNAME STATUS AVAILABILITY MANAGER STATU
|
||||||
e216jshn25ckzbvmwlnh5jr3g * swarm-manager1 Ready Active Leader
|
e216jshn25ckzbvmwlnh5jr3g * swarm-manager1 Ready Active Leader
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Formatting
|
||||||
|
|
||||||
|
The formatting options (`--format`) pretty-prints nodes output
|
||||||
|
using a Go template.
|
||||||
|
|
||||||
|
Valid placeholders for the Go template are listed below:
|
||||||
|
|
||||||
|
Placeholder | Description
|
||||||
|
-----------------|------------------------------------------------------------------------------------------
|
||||||
|
`.ID` | Node ID
|
||||||
|
`.Hostname` | Node hostname
|
||||||
|
`.Status` | Node status
|
||||||
|
`.Availability` | Node availability ("active", "pause", or "drain")
|
||||||
|
`.ManagerStatus` | Manager status of the node
|
||||||
|
|
||||||
|
When using the `--format` option, the `node ls` command will either
|
||||||
|
output the data exactly as the template declares or, when using the
|
||||||
|
`table` directive, includes column headers as well.
|
||||||
|
|
||||||
|
The following example uses a template without headers and outputs the
|
||||||
|
`ID` and `Hostname` entries separated by a colon for all nodes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ docker node ls --format "{{.ID}}: {{.Hostname}}"
|
||||||
|
e216jshn25ckzbvmwlnh5jr3g *: swarm-manager1
|
||||||
|
``
|
||||||
|
|
||||||
|
|
||||||
## Related commands
|
## Related commands
|
||||||
|
|
||||||
* [node demote](node_demote.md)
|
* [node demote](node_demote.md)
|
||||||
|
|
Loading…
Reference in a new issue