mirror of https://github.com/docker/cli.git
Add `--format` to `docker service ps`
This fix tries to address the issue raised in 27189 where it is not possible to support configured formatting stored in config.json. Since `--format` was not supported in `docker service ps`, the flag `--format` has also been added in this fix. This fix 1. Add `--format` to `docker service ps` 2. Add `tasksFormat` to config.json 3. Add `--format` to `docker stack ps` 4. Add `--format` to `docker node ps` The related docs has been updated. An integration test has been added. This fix fixes 27189. Signed-off-by: Yong Tang <yong.tang.github@outlook.com>
This commit is contained in:
parent
9e940b9020
commit
53bdc98713
|
@ -38,6 +38,7 @@ type Cli interface {
|
|||
Out() *OutStream
|
||||
Err() io.Writer
|
||||
In() *InStream
|
||||
ConfigFile() *configfile.ConfigFile
|
||||
}
|
||||
|
||||
// DockerCli is an instance the docker command line client.
|
||||
|
|
|
@ -0,0 +1,145 @@
|
|||
package formatter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/cli/command"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/go-units"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultTaskTableFormat = "table {{.ID}}\t{{.Name}}\t{{.Image}}\t{{.Node}}\t{{.DesiredState}}\t{{.CurrentState}}\t{{.Error}}\t{{.Ports}}"
|
||||
|
||||
nodeHeader = "NODE"
|
||||
taskIDHeader = "ID"
|
||||
desiredStateHeader = "DESIRED STATE"
|
||||
currentStateHeader = "CURRENT STATE"
|
||||
errorHeader = "ERROR"
|
||||
|
||||
maxErrLength = 30
|
||||
)
|
||||
|
||||
// NewTaskFormat returns a Format for rendering using a task Context
|
||||
func NewTaskFormat(source string, quiet bool) Format {
|
||||
switch source {
|
||||
case TableFormatKey:
|
||||
if quiet {
|
||||
return defaultQuietFormat
|
||||
}
|
||||
return defaultTaskTableFormat
|
||||
case RawFormatKey:
|
||||
if quiet {
|
||||
return `id: {{.ID}}`
|
||||
}
|
||||
return `id: {{.ID}}\nname: {{.Name}}\nimage: {{.Image}}\nnode: {{.Node}}\ndesired_state: {{.DesiredState}}\ncurrent_state: {{.CurrentState}}\nerror: {{.Error}}\nports: {{.Ports}}\n`
|
||||
}
|
||||
return Format(source)
|
||||
}
|
||||
|
||||
// TaskWrite writes the context
|
||||
func TaskWrite(ctx Context, tasks []swarm.Task, names map[string]string, nodes map[string]string) error {
|
||||
render := func(format func(subContext subContext) error) error {
|
||||
for _, task := range tasks {
|
||||
taskCtx := &taskContext{trunc: ctx.Trunc, task: task, name: names[task.ID], node: nodes[task.ID]}
|
||||
if err := format(taskCtx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return ctx.Write(&taskContext{}, render)
|
||||
}
|
||||
|
||||
type taskContext struct {
|
||||
HeaderContext
|
||||
trunc bool
|
||||
task swarm.Task
|
||||
name string
|
||||
node string
|
||||
}
|
||||
|
||||
func (c *taskContext) MarshalJSON() ([]byte, error) {
|
||||
return marshalJSON(c)
|
||||
}
|
||||
|
||||
func (c *taskContext) ID() string {
|
||||
c.AddHeader(taskIDHeader)
|
||||
if c.trunc {
|
||||
return stringid.TruncateID(c.task.ID)
|
||||
}
|
||||
return c.task.ID
|
||||
}
|
||||
|
||||
func (c *taskContext) Name() string {
|
||||
c.AddHeader(nameHeader)
|
||||
return c.name
|
||||
}
|
||||
|
||||
func (c *taskContext) Image() string {
|
||||
c.AddHeader(imageHeader)
|
||||
image := c.task.Spec.ContainerSpec.Image
|
||||
if c.trunc {
|
||||
ref, err := reference.ParseNormalizedNamed(image)
|
||||
if err == nil {
|
||||
// update image string for display, (strips any digest)
|
||||
if nt, ok := ref.(reference.NamedTagged); ok {
|
||||
if namedTagged, err := reference.WithTag(reference.TrimNamed(nt), nt.Tag()); err == nil {
|
||||
image = reference.FamiliarString(namedTagged)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return image
|
||||
}
|
||||
|
||||
func (c *taskContext) Node() string {
|
||||
c.AddHeader(nodeHeader)
|
||||
return c.node
|
||||
}
|
||||
|
||||
func (c *taskContext) DesiredState() string {
|
||||
c.AddHeader(desiredStateHeader)
|
||||
return command.PrettyPrint(c.task.DesiredState)
|
||||
}
|
||||
|
||||
func (c *taskContext) CurrentState() string {
|
||||
c.AddHeader(currentStateHeader)
|
||||
return fmt.Sprintf("%s %s ago",
|
||||
command.PrettyPrint(c.task.Status.State),
|
||||
strings.ToLower(units.HumanDuration(time.Since(c.task.Status.Timestamp))),
|
||||
)
|
||||
}
|
||||
|
||||
func (c *taskContext) Error() string {
|
||||
c.AddHeader(errorHeader)
|
||||
// Trim and quote the error message.
|
||||
taskErr := c.task.Status.Err
|
||||
if c.trunc && len(taskErr) > maxErrLength {
|
||||
taskErr = fmt.Sprintf("%s…", taskErr[:maxErrLength-1])
|
||||
}
|
||||
if len(taskErr) > 0 {
|
||||
taskErr = fmt.Sprintf("\"%s\"", taskErr)
|
||||
}
|
||||
return taskErr
|
||||
}
|
||||
|
||||
func (c *taskContext) Ports() string {
|
||||
c.AddHeader(portsHeader)
|
||||
if len(c.task.Status.PortStatus.Ports) == 0 {
|
||||
return ""
|
||||
}
|
||||
ports := []string{}
|
||||
for _, pConfig := range c.task.Status.PortStatus.Ports {
|
||||
ports = append(ports, fmt.Sprintf("*:%d->%d/%s",
|
||||
pConfig.PublishedPort,
|
||||
pConfig.TargetPort,
|
||||
pConfig.Protocol,
|
||||
))
|
||||
}
|
||||
return strings.Join(ports, ",")
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
package formatter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/pkg/testutil/assert"
|
||||
)
|
||||
|
||||
func TestTaskContextWrite(t *testing.T) {
|
||||
cases := []struct {
|
||||
context Context
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
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
|
||||
`,
|
||||
},
|
||||
{
|
||||
Context{Format: NewTaskFormat("table", true)},
|
||||
`taskID1
|
||||
taskID2
|
||||
`,
|
||||
},
|
||||
{
|
||||
Context{Format: NewTaskFormat("table {{.Name}} {{.Node}} {{.Ports}}", false)},
|
||||
`NAME NODE PORTS
|
||||
foobar_baz foo1
|
||||
foobar_bar foo2
|
||||
`,
|
||||
},
|
||||
{
|
||||
Context{Format: NewTaskFormat("table {{.Name}}", true)},
|
||||
`NAME
|
||||
foobar_baz
|
||||
foobar_bar
|
||||
`,
|
||||
},
|
||||
{
|
||||
Context{Format: NewTaskFormat("raw", true)},
|
||||
`id: taskID1
|
||||
id: taskID2
|
||||
`,
|
||||
},
|
||||
{
|
||||
Context{Format: NewTaskFormat("{{.Name}} {{.Node}}", false)},
|
||||
`foobar_baz foo1
|
||||
foobar_bar foo2
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range cases {
|
||||
tasks := []swarm.Task{
|
||||
{ID: "taskID1"},
|
||||
{ID: "taskID2"},
|
||||
}
|
||||
names := map[string]string{
|
||||
"taskID1": "foobar_baz",
|
||||
"taskID2": "foobar_bar",
|
||||
}
|
||||
nodes := map[string]string{
|
||||
"taskID1": "foo1",
|
||||
"taskID2": "foo2",
|
||||
}
|
||||
out := bytes.NewBufferString("")
|
||||
testcase.context.Output = out
|
||||
err := TaskWrite(testcase.context, tasks, names, nodes)
|
||||
if err != nil {
|
||||
assert.Error(t, err, testcase.expected)
|
||||
} else {
|
||||
assert.Equal(t, out.String(), testcase.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskContextWriteJSONField(t *testing.T) {
|
||||
tasks := []swarm.Task{
|
||||
{ID: "taskID1"},
|
||||
{ID: "taskID2"},
|
||||
}
|
||||
names := map[string]string{
|
||||
"taskID1": "foobar_baz",
|
||||
"taskID2": "foobar_bar",
|
||||
}
|
||||
out := bytes.NewBufferString("")
|
||||
err := TaskWrite(Context{Format: "{{json .ID}}", Output: out}, tasks, names, map[string]string{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
|
||||
var s string
|
||||
if err := json.Unmarshal([]byte(line), &s); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, s, tasks[i].ID)
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ import (
|
|||
"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/cli/command/idresolver"
|
||||
"github.com/docker/docker/cli/command/task"
|
||||
"github.com/docker/docker/opts"
|
||||
|
@ -19,6 +20,8 @@ type psOptions struct {
|
|||
nodeIDs []string
|
||||
noResolve bool
|
||||
noTrunc bool
|
||||
quiet bool
|
||||
format string
|
||||
filter opts.FilterOpt
|
||||
}
|
||||
|
||||
|
@ -43,6 +46,8 @@ func newPsCommand(dockerCli command.Cli) *cobra.Command {
|
|||
flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output")
|
||||
flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names")
|
||||
flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided")
|
||||
flags.StringVar(&opts.format, "format", "", "Pretty-print tasks using a Go template")
|
||||
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display task IDs")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
@ -81,7 +86,16 @@ func runPs(dockerCli command.Cli, opts psOptions) error {
|
|||
tasks = append(tasks, nodeTasks...)
|
||||
}
|
||||
|
||||
if err := task.Print(dockerCli, ctx, tasks, idresolver.New(client, opts.noResolve), opts.noTrunc); err != nil {
|
||||
format := opts.format
|
||||
if len(format) == 0 {
|
||||
if dockerCli.ConfigFile() != nil && len(dockerCli.ConfigFile().TasksFormat) > 0 && !opts.quiet {
|
||||
format = dockerCli.ConfigFile().TasksFormat
|
||||
} else {
|
||||
format = formatter.TableFormatKey
|
||||
}
|
||||
}
|
||||
|
||||
if err := task.Print(dockerCli, ctx, tasks, idresolver.New(client, opts.noResolve), !opts.noTrunc, opts.quiet, format); err != nil {
|
||||
errs = append(errs, err.Error())
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/cli/command"
|
||||
"github.com/docker/docker/cli/command/formatter"
|
||||
"github.com/docker/docker/cli/command/idresolver"
|
||||
"github.com/docker/docker/cli/command/node"
|
||||
"github.com/docker/docker/cli/command/task"
|
||||
|
@ -22,6 +23,7 @@ type psOptions struct {
|
|||
quiet bool
|
||||
noResolve bool
|
||||
noTrunc bool
|
||||
format string
|
||||
filter opts.FilterOpt
|
||||
}
|
||||
|
||||
|
@ -41,6 +43,7 @@ func newPsCommand(dockerCli *command.DockerCli) *cobra.Command {
|
|||
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display task IDs")
|
||||
flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output")
|
||||
flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names")
|
||||
flags.StringVar(&opts.format, "format", "", "Pretty-print tasks using a Go template")
|
||||
flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided")
|
||||
|
||||
return cmd
|
||||
|
@ -107,8 +110,14 @@ func runPS(dockerCli *command.DockerCli, opts psOptions) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if opts.quiet {
|
||||
return task.PrintQuiet(dockerCli, tasks)
|
||||
format := opts.format
|
||||
if len(format) == 0 {
|
||||
if len(dockerCli.ConfigFile().TasksFormat) > 0 && !opts.quiet {
|
||||
format = dockerCli.ConfigFile().TasksFormat
|
||||
} else {
|
||||
format = formatter.TableFormatKey
|
||||
}
|
||||
return task.Print(dockerCli, ctx, tasks, idresolver.New(client, opts.noResolve), opts.noTrunc)
|
||||
}
|
||||
|
||||
return task.Print(dockerCli, ctx, tasks, idresolver.New(client, opts.noResolve), !opts.noTrunc, opts.quiet, format)
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/cli/command"
|
||||
"github.com/docker/docker/cli/command/formatter"
|
||||
"github.com/docker/docker/cli/command/idresolver"
|
||||
"github.com/docker/docker/cli/command/task"
|
||||
"github.com/docker/docker/opts"
|
||||
|
@ -19,6 +20,8 @@ type psOptions struct {
|
|||
noTrunc bool
|
||||
namespace string
|
||||
noResolve bool
|
||||
quiet bool
|
||||
format string
|
||||
}
|
||||
|
||||
func newPsCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
|
@ -37,6 +40,8 @@ func newPsCommand(dockerCli *command.DockerCli) *cobra.Command {
|
|||
flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output")
|
||||
flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names")
|
||||
flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided")
|
||||
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display task IDs")
|
||||
flags.StringVar(&opts.format, "format", "", "Pretty-print tasks using a Go template")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
@ -58,5 +63,14 @@ func runPS(dockerCli *command.DockerCli, opts psOptions) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
return task.Print(dockerCli, ctx, tasks, idresolver.New(client, opts.noResolve), opts.noTrunc)
|
||||
format := opts.format
|
||||
if len(format) == 0 {
|
||||
if len(dockerCli.ConfigFile().TasksFormat) > 0 && !opts.quiet {
|
||||
format = dockerCli.ConfigFile().TasksFormat
|
||||
} else {
|
||||
format = formatter.TableFormatKey
|
||||
}
|
||||
}
|
||||
|
||||
return task.Print(dockerCli, ctx, tasks, idresolver.New(client, opts.noResolve), !opts.noTrunc, opts.quiet, format)
|
||||
}
|
||||
|
|
|
@ -2,42 +2,16 @@ package task
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/cli/command"
|
||||
"github.com/docker/docker/cli/command/formatter"
|
||||
"github.com/docker/docker/cli/command/idresolver"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/go-units"
|
||||
)
|
||||
|
||||
const (
|
||||
psTaskItemFmt = "%s\t%s\t%s\t%s\t%s\t%s %s ago\t%s\t%s\n"
|
||||
maxErrLength = 30
|
||||
)
|
||||
|
||||
type portStatus swarm.PortStatus
|
||||
|
||||
func (ps portStatus) String() string {
|
||||
if len(ps.Ports) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
str := fmt.Sprintf("*:%d->%d/%s", ps.Ports[0].PublishedPort, ps.Ports[0].TargetPort, ps.Ports[0].Protocol)
|
||||
for _, pConfig := range ps.Ports[1:] {
|
||||
str += fmt.Sprintf(",*:%d->%d/%s", pConfig.PublishedPort, pConfig.TargetPort, pConfig.Protocol)
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
type tasksBySlot []swarm.Task
|
||||
|
||||
func (t tasksBySlot) Len() int {
|
||||
|
@ -58,42 +32,23 @@ func (t tasksBySlot) Less(i, j int) bool {
|
|||
return t[j].Meta.CreatedAt.Before(t[i].CreatedAt)
|
||||
}
|
||||
|
||||
// Print task information in a table format.
|
||||
// Print task information in a format.
|
||||
// Besides this, command `docker node ps <node>`
|
||||
// and `docker stack ps` will call this, too.
|
||||
func Print(dockerCli command.Cli, ctx context.Context, tasks []swarm.Task, resolver *idresolver.IDResolver, noTrunc bool) error {
|
||||
func Print(dockerCli command.Cli, ctx context.Context, tasks []swarm.Task, resolver *idresolver.IDResolver, trunc, quiet bool, format string) error {
|
||||
sort.Stable(tasksBySlot(tasks))
|
||||
|
||||
writer := tabwriter.NewWriter(dockerCli.Out(), 0, 4, 2, ' ', 0)
|
||||
names := map[string]string{}
|
||||
nodes := map[string]string{}
|
||||
|
||||
// Ignore flushing errors
|
||||
defer writer.Flush()
|
||||
fmt.Fprintln(writer, strings.Join([]string{"ID", "NAME", "IMAGE", "NODE", "DESIRED STATE", "CURRENT STATE", "ERROR", "PORTS"}, "\t"))
|
||||
|
||||
return print(writer, ctx, tasks, resolver, noTrunc)
|
||||
tasksCtx := formatter.Context{
|
||||
Output: dockerCli.Out(),
|
||||
Format: formatter.NewTaskFormat(format, quiet),
|
||||
Trunc: trunc,
|
||||
}
|
||||
|
||||
// PrintQuiet shows task list in a quiet way.
|
||||
func PrintQuiet(dockerCli command.Cli, tasks []swarm.Task) error {
|
||||
sort.Stable(tasksBySlot(tasks))
|
||||
|
||||
out := dockerCli.Out()
|
||||
|
||||
for _, task := range tasks {
|
||||
fmt.Fprintln(out, task.ID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func print(out io.Writer, ctx context.Context, tasks []swarm.Task, resolver *idresolver.IDResolver, noTrunc bool) error {
|
||||
prevName := ""
|
||||
for _, task := range tasks {
|
||||
id := task.ID
|
||||
if !noTrunc {
|
||||
id = stringid.TruncateID(id)
|
||||
}
|
||||
|
||||
serviceName, err := resolver.Resolve(ctx, swarm.Service{}, task.ServiceID)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -118,42 +73,12 @@ func print(out io.Writer, ctx context.Context, tasks []swarm.Task, resolver *idr
|
|||
}
|
||||
prevName = name
|
||||
|
||||
// Trim and quote the error message.
|
||||
taskErr := task.Status.Err
|
||||
if !noTrunc && len(taskErr) > maxErrLength {
|
||||
taskErr = fmt.Sprintf("%s…", taskErr[:maxErrLength-1])
|
||||
names[task.ID] = name
|
||||
if tasksCtx.Format.IsTable() {
|
||||
names[task.ID] = indentedName
|
||||
}
|
||||
if len(taskErr) > 0 {
|
||||
taskErr = fmt.Sprintf("\"%s\"", taskErr)
|
||||
nodes[task.ID] = nodeValue
|
||||
}
|
||||
|
||||
image := task.Spec.ContainerSpec.Image
|
||||
if !noTrunc {
|
||||
ref, err := reference.ParseNormalizedNamed(image)
|
||||
if err == nil {
|
||||
// update image string for display, (strips any digest)
|
||||
if nt, ok := ref.(reference.NamedTagged); ok {
|
||||
if namedTagged, err := reference.WithTag(reference.TrimNamed(nt), nt.Tag()); err == nil {
|
||||
image = reference.FamiliarString(namedTagged)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(
|
||||
out,
|
||||
psTaskItemFmt,
|
||||
id,
|
||||
indentedName,
|
||||
image,
|
||||
nodeValue,
|
||||
command.PrettyPrint(task.DesiredState),
|
||||
command.PrettyPrint(task.Status.State),
|
||||
strings.ToLower(units.HumanDuration(time.Since(task.Status.Timestamp))),
|
||||
taskErr,
|
||||
portStatus(task.Status.PortStatus),
|
||||
)
|
||||
}
|
||||
return nil
|
||||
return formatter.TaskWrite(tasksCtx, tasks, names, nodes)
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@ type ConfigFile struct {
|
|||
Filename string `json:"-"` // Note: for internal use only
|
||||
ServiceInspectFormat string `json:"serviceInspectFormat,omitempty"`
|
||||
ServicesFormat string `json:"servicesFormat,omitempty"`
|
||||
TasksFormat string `json:"tasksFormat,omitempty"`
|
||||
}
|
||||
|
||||
// LegacyLoadFromReader reads the non-nested configuration data given and sets up the
|
||||
|
|
Loading…
Reference in New Issue