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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"text/tabwriter"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/cli/command"
|
||||
"github.com/docker/docker/cli/command/formatter"
|
||||
"github.com/docker/docker/opts"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
listItemFmt = "%s\t%s\t%s\t%s\t%s\n"
|
||||
)
|
||||
|
||||
type listOptions struct {
|
||||
quiet bool
|
||||
format string
|
||||
filter opts.FilterOpt
|
||||
}
|
||||
|
||||
|
@ -38,6 +31,7 @@ func newListCommand(dockerCli command.Cli) *cobra.Command {
|
|||
}
|
||||
flags := cmd.Flags()
|
||||
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")
|
||||
|
||||
return cmd
|
||||
|
@ -45,7 +39,6 @@ func newListCommand(dockerCli command.Cli) *cobra.Command {
|
|||
|
||||
func runList(dockerCli command.Cli, opts listOptions) error {
|
||||
client := dockerCli.Client()
|
||||
out := dockerCli.Out()
|
||||
ctx := context.Background()
|
||||
|
||||
nodes, err := client.NodeList(
|
||||
|
@ -55,61 +48,26 @@ func runList(dockerCli command.Cli, opts listOptions) error {
|
|||
return err
|
||||
}
|
||||
|
||||
info := types.Info{}
|
||||
if len(nodes) > 0 && !opts.quiet {
|
||||
// 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 {
|
||||
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
|
||||
}
|
||||
|
||||
func printTable(out io.Writer, nodes []swarm.Node, info types.Info) {
|
||||
writer := tabwriter.NewWriter(out, 0, 4, 2, ' ', 0)
|
||||
|
||||
// Ignore flushing errors
|
||||
defer writer.Flush()
|
||||
|
||||
fmt.Fprintf(writer, listItemFmt, "ID", "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)
|
||||
}
|
||||
format := opts.format
|
||||
if len(format) == 0 {
|
||||
format = formatter.TableFormatKey
|
||||
if len(dockerCli.ConfigFile().NodesFormat) > 0 && !opts.quiet {
|
||||
format = dockerCli.ConfigFile().NodesFormat
|
||||
}
|
||||
|
||||
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) {
|
||||
for _, node := range nodes {
|
||||
fmt.Fprintln(out, node.ID)
|
||||
nodesCtx := formatter.Context{
|
||||
Output: dockerCli.Out(),
|
||||
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/swarm"
|
||||
"github.com/docker/docker/cli/config/configfile"
|
||||
"github.com/docker/docker/cli/internal/test"
|
||||
"github.com/pkg/errors"
|
||||
// Import builders to get the builder function as package function
|
||||
|
@ -42,11 +43,12 @@ func TestNodeListErrorOnAPIFailure(t *testing.T) {
|
|||
}
|
||||
for _, tc := range testCases {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := newListCommand(
|
||||
test.NewFakeCli(&fakeClient{
|
||||
nodeListFunc: tc.nodeListFunc,
|
||||
infoFunc: tc.infoFunc,
|
||||
}, buf))
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
nodeListFunc: tc.nodeListFunc,
|
||||
infoFunc: tc.infoFunc,
|
||||
}, buf)
|
||||
cli.SetConfigfile(&configfile.ConfigFile{})
|
||||
cmd := newListCommand(cli)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
assert.Error(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
|
@ -54,39 +56,41 @@ func TestNodeListErrorOnAPIFailure(t *testing.T) {
|
|||
|
||||
func TestNodeList(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := newListCommand(
|
||||
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 := 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{})
|
||||
cmd := newListCommand(cli)
|
||||
assert.NilError(t, cmd.Execute())
|
||||
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(), `nodeID3 nodeHostname3 Ready Active`)
|
||||
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(), `nodeID3 nodeHostname3 Ready Active`)
|
||||
}
|
||||
|
||||
func TestNodeListQuietShouldOnlyPrintIDs(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := newListCommand(
|
||||
test.NewFakeCli(&fakeClient{
|
||||
nodeListFunc: func() ([]swarm.Node, error) {
|
||||
return []swarm.Node{
|
||||
*Node(),
|
||||
}, nil
|
||||
},
|
||||
}, buf))
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
nodeListFunc: func() ([]swarm.Node, error) {
|
||||
return []swarm.Node{
|
||||
*Node(),
|
||||
}, nil
|
||||
},
|
||||
}, buf)
|
||||
cli.SetConfigfile(&configfile.ConfigFile{})
|
||||
cmd := newListCommand(cli)
|
||||
cmd.Flags().Set("quiet", "true")
|
||||
assert.NilError(t, cmd.Execute())
|
||||
assert.Contains(t, buf.String(), "nodeID")
|
||||
|
@ -95,7 +99,64 @@ func TestNodeListQuietShouldOnlyPrintIDs(t *testing.T) {
|
|||
// Test case for #24090
|
||||
func TestNodeListContainsHostname(t *testing.T) {
|
||||
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.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"`
|
||||
TasksFormat string `json:"tasksFormat,omitempty"`
|
||||
SecretFormat string `json:"secretFormat,omitempty"`
|
||||
NodesFormat string `json:"nodesFormat,omitempty"`
|
||||
}
|
||||
|
||||
// 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
|
||||
[**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
|
||||
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}}",
|
||||
"secretFormat": "table {{.ID}}\t{{.Name}}\t{{.CreatedAt}}\t{{.UpdatedAt}}",
|
||||
"serviceInspectFormat": "pretty",
|
||||
"nodesFormat": "table {{.ID}}\t{{.Hostname}}\t{{.Availability}}",
|
||||
"detachKeys": "ctrl-e,e",
|
||||
"credsStore": "secretservice",
|
||||
"credHelpers": {
|
||||
|
|
|
@ -24,9 +24,10 @@ Aliases:
|
|||
ls, list
|
||||
|
||||
Options:
|
||||
-f, --filter value Filter output based on conditions provided
|
||||
--help Print usage
|
||||
-q, --quiet Only display IDs
|
||||
-f, --filter filter Filter output based on conditions provided
|
||||
--format string Pretty-print nodes using a Go template
|
||||
--help Print usage
|
||||
-q, --quiet Only display IDs
|
||||
```
|
||||
|
||||
## Description
|
||||
|
@ -45,6 +46,10 @@ ID HOSTNAME STATUS AVAILABILITY MANAGER STATU
|
|||
38ciaotwjuritcdtn9npbnkuz swarm-worker1 Ready Active
|
||||
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
|
||||
|
||||
|
@ -124,6 +129,34 @@ ID HOSTNAME STATUS AVAILABILITY MANAGER STATU
|
|||
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
|
||||
|
||||
* [node demote](node_demote.md)
|
||||
|
|
Loading…
Reference in a new issue