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
|
Out() *OutStream
|
||||||
Err() io.Writer
|
Err() io.Writer
|
||||||
In() *InStream
|
In() *InStream
|
||||||
|
ConfigFile() *configfile.ConfigFile
|
||||||
}
|
}
|
||||||
|
|
||||||
// DockerCli is an instance the docker command line client.
|
// 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/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/cli/command/idresolver"
|
"github.com/docker/docker/cli/command/idresolver"
|
||||||
"github.com/docker/docker/cli/command/task"
|
"github.com/docker/docker/cli/command/task"
|
||||||
"github.com/docker/docker/opts"
|
"github.com/docker/docker/opts"
|
||||||
|
@ -19,6 +20,8 @@ type psOptions struct {
|
||||||
nodeIDs []string
|
nodeIDs []string
|
||||||
noResolve bool
|
noResolve bool
|
||||||
noTrunc bool
|
noTrunc bool
|
||||||
|
quiet bool
|
||||||
|
format string
|
||||||
filter opts.FilterOpt
|
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.noTrunc, "no-trunc", false, "Do not truncate output")
|
||||||
flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names")
|
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.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
|
return cmd
|
||||||
}
|
}
|
||||||
|
@ -81,7 +86,16 @@ func runPs(dockerCli command.Cli, opts psOptions) error {
|
||||||
tasks = append(tasks, nodeTasks...)
|
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())
|
errs = append(errs, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"github.com/docker/docker/api/types/filters"
|
"github.com/docker/docker/api/types/filters"
|
||||||
"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/cli/command/idresolver"
|
"github.com/docker/docker/cli/command/idresolver"
|
||||||
"github.com/docker/docker/cli/command/node"
|
"github.com/docker/docker/cli/command/node"
|
||||||
"github.com/docker/docker/cli/command/task"
|
"github.com/docker/docker/cli/command/task"
|
||||||
|
@ -22,6 +23,7 @@ type psOptions struct {
|
||||||
quiet bool
|
quiet bool
|
||||||
noResolve bool
|
noResolve bool
|
||||||
noTrunc bool
|
noTrunc bool
|
||||||
|
format string
|
||||||
filter opts.FilterOpt
|
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.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display task IDs")
|
||||||
flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output")
|
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.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")
|
flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided")
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
|
@ -107,8 +110,14 @@ func runPS(dockerCli *command.DockerCli, opts psOptions) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.quiet {
|
format := opts.format
|
||||||
return task.PrintQuiet(dockerCli, tasks)
|
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/api/types"
|
||||||
"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/cli/command/idresolver"
|
"github.com/docker/docker/cli/command/idresolver"
|
||||||
"github.com/docker/docker/cli/command/task"
|
"github.com/docker/docker/cli/command/task"
|
||||||
"github.com/docker/docker/opts"
|
"github.com/docker/docker/opts"
|
||||||
|
@ -19,6 +20,8 @@ type psOptions struct {
|
||||||
noTrunc bool
|
noTrunc bool
|
||||||
namespace string
|
namespace string
|
||||||
noResolve bool
|
noResolve bool
|
||||||
|
quiet bool
|
||||||
|
format string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPsCommand(dockerCli *command.DockerCli) *cobra.Command {
|
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.noTrunc, "no-trunc", false, "Do not truncate output")
|
||||||
flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names")
|
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.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
|
return cmd
|
||||||
}
|
}
|
||||||
|
@ -58,5 +63,14 @@ func runPS(dockerCli *command.DockerCli, opts psOptions) error {
|
||||||
return nil
|
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
|
||||||
"text/tabwriter"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/net/context"
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
"github.com/docker/distribution/reference"
|
|
||||||
"github.com/docker/docker/api/types/swarm"
|
"github.com/docker/docker/api/types/swarm"
|
||||||
"github.com/docker/docker/cli/command"
|
"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/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
|
type tasksBySlot []swarm.Task
|
||||||
|
|
||||||
func (t tasksBySlot) Len() int {
|
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)
|
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>`
|
// Besides this, command `docker node ps <node>`
|
||||||
// and `docker stack ps` will call this, too.
|
// 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))
|
sort.Stable(tasksBySlot(tasks))
|
||||||
|
|
||||||
writer := tabwriter.NewWriter(dockerCli.Out(), 0, 4, 2, ' ', 0)
|
names := map[string]string{}
|
||||||
|
nodes := map[string]string{}
|
||||||
|
|
||||||
// Ignore flushing errors
|
tasksCtx := formatter.Context{
|
||||||
defer writer.Flush()
|
Output: dockerCli.Out(),
|
||||||
fmt.Fprintln(writer, strings.Join([]string{"ID", "NAME", "IMAGE", "NODE", "DESIRED STATE", "CURRENT STATE", "ERROR", "PORTS"}, "\t"))
|
Format: formatter.NewTaskFormat(format, quiet),
|
||||||
|
Trunc: trunc,
|
||||||
return print(writer, ctx, tasks, resolver, noTrunc)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 := ""
|
prevName := ""
|
||||||
for _, task := range tasks {
|
for _, task := range tasks {
|
||||||
id := task.ID
|
|
||||||
if !noTrunc {
|
|
||||||
id = stringid.TruncateID(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
serviceName, err := resolver.Resolve(ctx, swarm.Service{}, task.ServiceID)
|
serviceName, err := resolver.Resolve(ctx, swarm.Service{}, task.ServiceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -118,42 +73,12 @@ func print(out io.Writer, ctx context.Context, tasks []swarm.Task, resolver *idr
|
||||||
}
|
}
|
||||||
prevName = name
|
prevName = name
|
||||||
|
|
||||||
// Trim and quote the error message.
|
names[task.ID] = name
|
||||||
taskErr := task.Status.Err
|
if tasksCtx.Format.IsTable() {
|
||||||
if !noTrunc && len(taskErr) > maxErrLength {
|
names[task.ID] = indentedName
|
||||||
taskErr = fmt.Sprintf("%s…", taskErr[:maxErrLength-1])
|
|
||||||
}
|
}
|
||||||
if len(taskErr) > 0 {
|
nodes[task.ID] = nodeValue
|
||||||
taskErr = fmt.Sprintf("\"%s\"", taskErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
Filename string `json:"-"` // Note: for internal use only
|
||||||
ServiceInspectFormat string `json:"serviceInspectFormat,omitempty"`
|
ServiceInspectFormat string `json:"serviceInspectFormat,omitempty"`
|
||||||
ServicesFormat string `json:"servicesFormat,omitempty"`
|
ServicesFormat string `json:"servicesFormat,omitempty"`
|
||||||
|
TasksFormat string `json:"tasksFormat,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
|
||||||
|
|
Loading…
Reference in New Issue