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:
145
command/formatter/task.go
Normal file
145
command/formatter/task.go
Normal file
@ -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, ",")
|
||||
}
|
||||
107
command/formatter/task_test.go
Normal file
107
command/formatter/task_test.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user