Merge pull request #26537 from dnephin/refactor_cli_formatter
Refactor cli/command/formatter
This commit is contained in:
commit
a7c25f9540
15 changed files with 405 additions and 597 deletions
|
@ -106,27 +106,19 @@ func runPs(dockerCli *command.DockerCli, opts *psOptions) error {
|
|||
return err
|
||||
}
|
||||
|
||||
f := opts.format
|
||||
if len(f) == 0 {
|
||||
format := opts.format
|
||||
if len(format) == 0 {
|
||||
if len(dockerCli.ConfigFile().PsFormat) > 0 && !opts.quiet {
|
||||
f = dockerCli.ConfigFile().PsFormat
|
||||
format = dockerCli.ConfigFile().PsFormat
|
||||
} else {
|
||||
f = "table"
|
||||
format = formatter.TableFormatKey
|
||||
}
|
||||
}
|
||||
|
||||
psCtx := formatter.ContainerContext{
|
||||
Context: formatter.Context{
|
||||
Output: dockerCli.Out(),
|
||||
Format: f,
|
||||
Quiet: opts.quiet,
|
||||
Trunc: !opts.noTrunc,
|
||||
},
|
||||
Size: listOptions.Size,
|
||||
Containers: containers,
|
||||
containerCtx := formatter.Context{
|
||||
Output: dockerCli.Out(),
|
||||
Format: formatter.NewContainerFormat(format, opts.quiet, listOptions.Size),
|
||||
Trunc: !opts.noTrunc,
|
||||
}
|
||||
|
||||
psCtx.Write()
|
||||
|
||||
return nil
|
||||
return formatter.ContainerWrite(containerCtx, containers)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package formatter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -11,7 +10,7 @@ import (
|
|||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/docker/pkg/stringutils"
|
||||
"github.com/docker/go-units"
|
||||
units "github.com/docker/go-units"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -26,67 +25,61 @@ const (
|
|||
mountsHeader = "MOUNTS"
|
||||
)
|
||||
|
||||
// ContainerContext contains container specific information required by the formater, encapsulate a Context struct.
|
||||
type ContainerContext struct {
|
||||
Context
|
||||
// Size when set to true will display the size of the output.
|
||||
Size bool
|
||||
// Containers
|
||||
Containers []types.Container
|
||||
// NewContainerFormat returns a Format for rendering using a Context
|
||||
func NewContainerFormat(source string, quiet bool, size bool) Format {
|
||||
switch source {
|
||||
case TableFormatKey:
|
||||
if quiet {
|
||||
return defaultQuietFormat
|
||||
}
|
||||
format := defaultContainerTableFormat
|
||||
if size {
|
||||
format += `\t{{.Size}}`
|
||||
}
|
||||
return Format(format)
|
||||
case RawFormatKey:
|
||||
if quiet {
|
||||
return `container_id: {{.ID}}`
|
||||
}
|
||||
format := `container_id: {{.ID}}
|
||||
image: {{.Image}}
|
||||
command: {{.Command}}
|
||||
created_at: {{.CreatedAt}}
|
||||
status: {{- pad .Status 1 0}}
|
||||
names: {{.Names}}
|
||||
labels: {{- pad .Labels 1 0}}
|
||||
ports: {{- pad .Ports 1 0}}
|
||||
`
|
||||
if size {
|
||||
format += `size: {{.Size}}\n`
|
||||
}
|
||||
return Format(format)
|
||||
}
|
||||
return Format(source)
|
||||
}
|
||||
|
||||
func (ctx ContainerContext) Write() {
|
||||
switch ctx.Format {
|
||||
case tableFormatKey:
|
||||
if ctx.Quiet {
|
||||
ctx.Format = defaultQuietFormat
|
||||
} else {
|
||||
ctx.Format = defaultContainerTableFormat
|
||||
if ctx.Size {
|
||||
ctx.Format += `\t{{.Size}}`
|
||||
}
|
||||
}
|
||||
case rawFormatKey:
|
||||
if ctx.Quiet {
|
||||
ctx.Format = `container_id: {{.ID}}`
|
||||
} else {
|
||||
ctx.Format = `container_id: {{.ID}}\nimage: {{.Image}}\ncommand: {{.Command}}\ncreated_at: {{.CreatedAt}}\nstatus: {{.Status}}\nnames: {{.Names}}\nlabels: {{.Labels}}\nports: {{.Ports}}\n`
|
||||
if ctx.Size {
|
||||
ctx.Format += `size: {{.Size}}\n`
|
||||
// ContainerWrite renders the context for a list of containers
|
||||
func ContainerWrite(ctx Context, containers []types.Container) error {
|
||||
render := func(format func(subContext subContext) error) error {
|
||||
for _, container := range containers {
|
||||
err := format(&containerContext{trunc: ctx.Trunc, c: container})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx.buffer = bytes.NewBufferString("")
|
||||
ctx.preformat()
|
||||
|
||||
tmpl, err := ctx.parseFormat()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, container := range ctx.Containers {
|
||||
containerCtx := &containerContext{
|
||||
trunc: ctx.Trunc,
|
||||
c: container,
|
||||
}
|
||||
err = ctx.contextFormat(tmpl, containerCtx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.postformat(tmpl, &containerContext{})
|
||||
return ctx.Write(&containerContext{}, render)
|
||||
}
|
||||
|
||||
type containerContext struct {
|
||||
baseSubContext
|
||||
HeaderContext
|
||||
trunc bool
|
||||
c types.Container
|
||||
}
|
||||
|
||||
func (c *containerContext) ID() string {
|
||||
c.addHeader(containerIDHeader)
|
||||
c.AddHeader(containerIDHeader)
|
||||
if c.trunc {
|
||||
return stringid.TruncateID(c.c.ID)
|
||||
}
|
||||
|
@ -94,7 +87,7 @@ func (c *containerContext) ID() string {
|
|||
}
|
||||
|
||||
func (c *containerContext) Names() string {
|
||||
c.addHeader(namesHeader)
|
||||
c.AddHeader(namesHeader)
|
||||
names := stripNamePrefix(c.c.Names)
|
||||
if c.trunc {
|
||||
for _, name := range names {
|
||||
|
@ -108,7 +101,7 @@ func (c *containerContext) Names() string {
|
|||
}
|
||||
|
||||
func (c *containerContext) Image() string {
|
||||
c.addHeader(imageHeader)
|
||||
c.AddHeader(imageHeader)
|
||||
if c.c.Image == "" {
|
||||
return "<no image>"
|
||||
}
|
||||
|
@ -121,7 +114,7 @@ func (c *containerContext) Image() string {
|
|||
}
|
||||
|
||||
func (c *containerContext) Command() string {
|
||||
c.addHeader(commandHeader)
|
||||
c.AddHeader(commandHeader)
|
||||
command := c.c.Command
|
||||
if c.trunc {
|
||||
command = stringutils.Ellipsis(command, 20)
|
||||
|
@ -130,28 +123,28 @@ func (c *containerContext) Command() string {
|
|||
}
|
||||
|
||||
func (c *containerContext) CreatedAt() string {
|
||||
c.addHeader(createdAtHeader)
|
||||
c.AddHeader(createdAtHeader)
|
||||
return time.Unix(int64(c.c.Created), 0).String()
|
||||
}
|
||||
|
||||
func (c *containerContext) RunningFor() string {
|
||||
c.addHeader(runningForHeader)
|
||||
c.AddHeader(runningForHeader)
|
||||
createdAt := time.Unix(int64(c.c.Created), 0)
|
||||
return units.HumanDuration(time.Now().UTC().Sub(createdAt))
|
||||
}
|
||||
|
||||
func (c *containerContext) Ports() string {
|
||||
c.addHeader(portsHeader)
|
||||
c.AddHeader(portsHeader)
|
||||
return api.DisplayablePorts(c.c.Ports)
|
||||
}
|
||||
|
||||
func (c *containerContext) Status() string {
|
||||
c.addHeader(statusHeader)
|
||||
c.AddHeader(statusHeader)
|
||||
return c.c.Status
|
||||
}
|
||||
|
||||
func (c *containerContext) Size() string {
|
||||
c.addHeader(sizeHeader)
|
||||
c.AddHeader(sizeHeader)
|
||||
srw := units.HumanSizeWithPrecision(float64(c.c.SizeRw), 3)
|
||||
sv := units.HumanSizeWithPrecision(float64(c.c.SizeRootFs), 3)
|
||||
|
||||
|
@ -163,7 +156,7 @@ func (c *containerContext) Size() string {
|
|||
}
|
||||
|
||||
func (c *containerContext) Labels() string {
|
||||
c.addHeader(labelsHeader)
|
||||
c.AddHeader(labelsHeader)
|
||||
if c.c.Labels == nil {
|
||||
return ""
|
||||
}
|
||||
|
@ -180,7 +173,7 @@ func (c *containerContext) Label(name string) string {
|
|||
r := strings.NewReplacer("-", " ", "_", " ")
|
||||
h := r.Replace(n[len(n)-1])
|
||||
|
||||
c.addHeader(h)
|
||||
c.AddHeader(h)
|
||||
|
||||
if c.c.Labels == nil {
|
||||
return ""
|
||||
|
@ -189,7 +182,7 @@ func (c *containerContext) Label(name string) string {
|
|||
}
|
||||
|
||||
func (c *containerContext) Mounts() string {
|
||||
c.addHeader(mountsHeader)
|
||||
c.AddHeader(mountsHeader)
|
||||
|
||||
var name string
|
||||
var mounts []string
|
||||
|
|
|
@ -95,7 +95,7 @@ func TestContainerPsContext(t *testing.T) {
|
|||
t.Fatalf("Expected %s, was %s\n", c.expValue, v)
|
||||
}
|
||||
|
||||
h := ctx.fullHeader()
|
||||
h := ctx.FullHeader()
|
||||
if h != c.expHeader {
|
||||
t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
|
||||
}
|
||||
|
@ -114,7 +114,7 @@ func TestContainerPsContext(t *testing.T) {
|
|||
t.Fatalf("Expected ubuntu, was %s\n", node)
|
||||
}
|
||||
|
||||
h := ctx.fullHeader()
|
||||
h := ctx.FullHeader()
|
||||
if h != "SWARM ID\tNODE NAME" {
|
||||
t.Fatalf("Expected %s, was %s\n", "SWARM ID\tNODE NAME", h)
|
||||
|
||||
|
@ -129,9 +129,9 @@ func TestContainerPsContext(t *testing.T) {
|
|||
}
|
||||
|
||||
ctx = containerContext{c: c2, trunc: true}
|
||||
fullHeader := ctx.fullHeader()
|
||||
if fullHeader != "" {
|
||||
t.Fatalf("Expected fullHeader to be empty, was %s", fullHeader)
|
||||
FullHeader := ctx.FullHeader()
|
||||
if FullHeader != "" {
|
||||
t.Fatalf("Expected FullHeader to be empty, was %s", FullHeader)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -140,186 +140,127 @@ func TestContainerContextWrite(t *testing.T) {
|
|||
unixTime := time.Now().AddDate(0, 0, -1).Unix()
|
||||
expectedTime := time.Unix(unixTime, 0).String()
|
||||
|
||||
contexts := []struct {
|
||||
context ContainerContext
|
||||
cases := []struct {
|
||||
context Context
|
||||
expected string
|
||||
}{
|
||||
// Errors
|
||||
{
|
||||
ContainerContext{
|
||||
Context: Context{
|
||||
Format: "{{InvalidFunction}}",
|
||||
},
|
||||
},
|
||||
Context{Format: "{{InvalidFunction}}"},
|
||||
`Template parsing error: template: :1: function "InvalidFunction" not defined
|
||||
`,
|
||||
},
|
||||
{
|
||||
ContainerContext{
|
||||
Context: Context{
|
||||
Format: "{{nil}}",
|
||||
},
|
||||
},
|
||||
Context{Format: "{{nil}}"},
|
||||
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
|
||||
`,
|
||||
},
|
||||
// Table Format
|
||||
{
|
||||
ContainerContext{
|
||||
Context: Context{
|
||||
Format: "table",
|
||||
},
|
||||
Size: true,
|
||||
},
|
||||
Context{Format: NewContainerFormat("table", false, true)},
|
||||
`CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES SIZE
|
||||
containerID1 ubuntu "" 24 hours ago foobar_baz 0 B
|
||||
containerID2 ubuntu "" 24 hours ago foobar_bar 0 B
|
||||
`,
|
||||
},
|
||||
{
|
||||
ContainerContext{
|
||||
Context: Context{
|
||||
Format: "table",
|
||||
},
|
||||
},
|
||||
Context{Format: NewContainerFormat("table", false, false)},
|
||||
`CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
||||
containerID1 ubuntu "" 24 hours ago foobar_baz
|
||||
containerID2 ubuntu "" 24 hours ago foobar_bar
|
||||
`,
|
||||
},
|
||||
{
|
||||
ContainerContext{
|
||||
Context: Context{
|
||||
Format: "table {{.Image}}",
|
||||
},
|
||||
},
|
||||
Context{Format: NewContainerFormat("table {{.Image}}", false, false)},
|
||||
"IMAGE\nubuntu\nubuntu\n",
|
||||
},
|
||||
{
|
||||
ContainerContext{
|
||||
Context: Context{
|
||||
Format: "table {{.Image}}",
|
||||
},
|
||||
Size: true,
|
||||
},
|
||||
Context{Format: NewContainerFormat("table {{.Image}}", false, true)},
|
||||
"IMAGE\nubuntu\nubuntu\n",
|
||||
},
|
||||
{
|
||||
ContainerContext{
|
||||
Context: Context{
|
||||
Format: "table {{.Image}}",
|
||||
Quiet: true,
|
||||
},
|
||||
},
|
||||
Context{Format: NewContainerFormat("table {{.Image}}", true, false)},
|
||||
"IMAGE\nubuntu\nubuntu\n",
|
||||
},
|
||||
{
|
||||
ContainerContext{
|
||||
Context: Context{
|
||||
Format: "table",
|
||||
Quiet: true,
|
||||
},
|
||||
},
|
||||
Context{Format: NewContainerFormat("table", true, false)},
|
||||
"containerID1\ncontainerID2\n",
|
||||
},
|
||||
// Raw Format
|
||||
{
|
||||
ContainerContext{
|
||||
Context: Context{
|
||||
Format: "raw",
|
||||
},
|
||||
},
|
||||
Context{Format: NewContainerFormat("raw", false, false)},
|
||||
fmt.Sprintf(`container_id: containerID1
|
||||
image: ubuntu
|
||||
command: ""
|
||||
created_at: %s
|
||||
status:
|
||||
status:
|
||||
names: foobar_baz
|
||||
labels:
|
||||
ports:
|
||||
labels:
|
||||
ports:
|
||||
|
||||
container_id: containerID2
|
||||
image: ubuntu
|
||||
command: ""
|
||||
created_at: %s
|
||||
status:
|
||||
status:
|
||||
names: foobar_bar
|
||||
labels:
|
||||
ports:
|
||||
labels:
|
||||
ports:
|
||||
|
||||
`, expectedTime, expectedTime),
|
||||
},
|
||||
{
|
||||
ContainerContext{
|
||||
Context: Context{
|
||||
Format: "raw",
|
||||
},
|
||||
Size: true,
|
||||
},
|
||||
Context{Format: NewContainerFormat("raw", false, true)},
|
||||
fmt.Sprintf(`container_id: containerID1
|
||||
image: ubuntu
|
||||
command: ""
|
||||
created_at: %s
|
||||
status:
|
||||
status:
|
||||
names: foobar_baz
|
||||
labels:
|
||||
ports:
|
||||
labels:
|
||||
ports:
|
||||
size: 0 B
|
||||
|
||||
container_id: containerID2
|
||||
image: ubuntu
|
||||
command: ""
|
||||
created_at: %s
|
||||
status:
|
||||
status:
|
||||
names: foobar_bar
|
||||
labels:
|
||||
ports:
|
||||
labels:
|
||||
ports:
|
||||
size: 0 B
|
||||
|
||||
`, expectedTime, expectedTime),
|
||||
},
|
||||
{
|
||||
ContainerContext{
|
||||
Context: Context{
|
||||
Format: "raw",
|
||||
Quiet: true,
|
||||
},
|
||||
},
|
||||
Context{Format: NewContainerFormat("raw", true, false)},
|
||||
"container_id: containerID1\ncontainer_id: containerID2\n",
|
||||
},
|
||||
// Custom Format
|
||||
{
|
||||
ContainerContext{
|
||||
Context: Context{
|
||||
Format: "{{.Image}}",
|
||||
},
|
||||
},
|
||||
Context{Format: "{{.Image}}"},
|
||||
"ubuntu\nubuntu\n",
|
||||
},
|
||||
{
|
||||
ContainerContext{
|
||||
Context: Context{
|
||||
Format: "{{.Image}}",
|
||||
},
|
||||
Size: true,
|
||||
},
|
||||
Context{Format: NewContainerFormat("{{.Image}}", false, true)},
|
||||
"ubuntu\nubuntu\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, context := range contexts {
|
||||
for _, testcase := range cases {
|
||||
containers := []types.Container{
|
||||
{ID: "containerID1", Names: []string{"/foobar_baz"}, Image: "ubuntu", Created: unixTime},
|
||||
{ID: "containerID2", Names: []string{"/foobar_bar"}, Image: "ubuntu", Created: unixTime},
|
||||
}
|
||||
out := bytes.NewBufferString("")
|
||||
context.context.Output = out
|
||||
context.context.Containers = containers
|
||||
context.context.Write()
|
||||
actual := out.String()
|
||||
assert.Equal(t, actual, context.expected)
|
||||
// Clean buffer
|
||||
out.Reset()
|
||||
testcase.context.Output = out
|
||||
err := ContainerWrite(testcase.context, containers)
|
||||
if err != nil {
|
||||
assert.Error(t, err, testcase.expected)
|
||||
} else {
|
||||
assert.Equal(t, out.String(), testcase.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -328,75 +269,56 @@ func TestContainerContextWriteWithNoContainers(t *testing.T) {
|
|||
containers := []types.Container{}
|
||||
|
||||
contexts := []struct {
|
||||
context ContainerContext
|
||||
context Context
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
ContainerContext{
|
||||
Context: Context{
|
||||
Format: "{{.Image}}",
|
||||
Output: out,
|
||||
},
|
||||
Context{
|
||||
Format: "{{.Image}}",
|
||||
Output: out,
|
||||
},
|
||||
"",
|
||||
},
|
||||
{
|
||||
ContainerContext{
|
||||
Context: Context{
|
||||
Format: "table {{.Image}}",
|
||||
Output: out,
|
||||
},
|
||||
Context{
|
||||
Format: "table {{.Image}}",
|
||||
Output: out,
|
||||
},
|
||||
"IMAGE\n",
|
||||
},
|
||||
{
|
||||
ContainerContext{
|
||||
Context: Context{
|
||||
Format: "{{.Image}}",
|
||||
Output: out,
|
||||
},
|
||||
Size: true,
|
||||
Context{
|
||||
Format: NewContainerFormat("{{.Image}}", false, true),
|
||||
Output: out,
|
||||
},
|
||||
"",
|
||||
},
|
||||
{
|
||||
ContainerContext{
|
||||
Context: Context{
|
||||
Format: "table {{.Image}}",
|
||||
Output: out,
|
||||
},
|
||||
Size: true,
|
||||
Context{
|
||||
Format: NewContainerFormat("table {{.Image}}", false, true),
|
||||
Output: out,
|
||||
},
|
||||
"IMAGE\n",
|
||||
},
|
||||
{
|
||||
ContainerContext{
|
||||
Context: Context{
|
||||
Format: "table {{.Image}}\t{{.Size}}",
|
||||
Output: out,
|
||||
},
|
||||
Context{
|
||||
Format: "table {{.Image}}\t{{.Size}}",
|
||||
Output: out,
|
||||
},
|
||||
"IMAGE SIZE\n",
|
||||
},
|
||||
{
|
||||
ContainerContext{
|
||||
Context: Context{
|
||||
Format: "table {{.Image}}\t{{.Size}}",
|
||||
Output: out,
|
||||
},
|
||||
Size: true,
|
||||
Context{
|
||||
Format: NewContainerFormat("table {{.Image}}\t{{.Size}}", false, true),
|
||||
Output: out,
|
||||
},
|
||||
"IMAGE SIZE\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, context := range contexts {
|
||||
context.context.Containers = containers
|
||||
context.context.Write()
|
||||
actual := out.String()
|
||||
if actual != context.expected {
|
||||
t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
|
||||
}
|
||||
ContainerWrite(context.context, containers)
|
||||
assert.Equal(t, context.expected, out.String())
|
||||
// Clean buffer
|
||||
out.Reset()
|
||||
}
|
||||
|
|
|
@ -5,8 +5,6 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
tableKey = "table"
|
||||
|
||||
imageHeader = "IMAGE"
|
||||
createdSinceHeader = "CREATED"
|
||||
createdAtHeader = "CREATED AT"
|
||||
|
@ -18,22 +16,25 @@ const (
|
|||
)
|
||||
|
||||
type subContext interface {
|
||||
fullHeader() string
|
||||
addHeader(header string)
|
||||
FullHeader() string
|
||||
AddHeader(header string)
|
||||
}
|
||||
|
||||
type baseSubContext struct {
|
||||
// HeaderContext provides the subContext interface for managing headers
|
||||
type HeaderContext struct {
|
||||
header []string
|
||||
}
|
||||
|
||||
func (c *baseSubContext) fullHeader() string {
|
||||
// FullHeader returns the header as a string
|
||||
func (c *HeaderContext) FullHeader() string {
|
||||
if c.header == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.Join(c.header, "\t")
|
||||
}
|
||||
|
||||
func (c *baseSubContext) addHeader(header string) {
|
||||
// AddHeader adds another column to the header
|
||||
func (c *HeaderContext) AddHeader(header string) {
|
||||
if c.header == nil {
|
||||
c.header = []string{}
|
||||
}
|
||||
|
|
|
@ -12,36 +12,48 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
tableFormatKey = "table"
|
||||
rawFormatKey = "raw"
|
||||
// TableFormatKey is the key used to format as a table
|
||||
TableFormatKey = "table"
|
||||
// RawFormatKey is the key used to format as raw JSON
|
||||
RawFormatKey = "raw"
|
||||
|
||||
defaultQuietFormat = "{{.ID}}"
|
||||
)
|
||||
|
||||
// Format is the format string rendered using the Context
|
||||
type Format string
|
||||
|
||||
// IsTable returns true if the format is a table-type format
|
||||
func (f Format) IsTable() bool {
|
||||
return strings.HasPrefix(string(f), TableFormatKey)
|
||||
}
|
||||
|
||||
// Contains returns true if the format contains the substring
|
||||
func (f Format) Contains(sub string) bool {
|
||||
return strings.Contains(string(f), sub)
|
||||
}
|
||||
|
||||
// Context contains information required by the formatter to print the output as desired.
|
||||
type Context struct {
|
||||
// Output is the output stream to which the formatted string is written.
|
||||
Output io.Writer
|
||||
// Format is used to choose raw, table or custom format for the output.
|
||||
Format string
|
||||
// Quiet when set to true will simply print minimal information.
|
||||
Quiet bool
|
||||
Format Format
|
||||
// Trunc when set to true will truncate the output of certain fields such as Container ID.
|
||||
Trunc bool
|
||||
|
||||
// internal element
|
||||
table bool
|
||||
finalFormat string
|
||||
header string
|
||||
buffer *bytes.Buffer
|
||||
}
|
||||
|
||||
func (c *Context) preformat() {
|
||||
c.finalFormat = c.Format
|
||||
func (c *Context) preFormat() {
|
||||
c.finalFormat = string(c.Format)
|
||||
|
||||
if strings.HasPrefix(c.Format, tableKey) {
|
||||
c.table = true
|
||||
c.finalFormat = c.finalFormat[len(tableKey):]
|
||||
// TODO: handle this in the Format type
|
||||
if c.Format.IsTable() {
|
||||
c.finalFormat = c.finalFormat[len(TableFormatKey):]
|
||||
}
|
||||
|
||||
c.finalFormat = strings.Trim(c.finalFormat, " ")
|
||||
|
@ -52,18 +64,17 @@ func (c *Context) preformat() {
|
|||
func (c *Context) parseFormat() (*template.Template, error) {
|
||||
tmpl, err := templates.Parse(c.finalFormat)
|
||||
if err != nil {
|
||||
c.buffer.WriteString(fmt.Sprintf("Template parsing error: %v\n", err))
|
||||
c.buffer.WriteTo(c.Output)
|
||||
return tmpl, fmt.Errorf("Template parsing error: %v\n", err)
|
||||
}
|
||||
return tmpl, err
|
||||
}
|
||||
|
||||
func (c *Context) postformat(tmpl *template.Template, subContext subContext) {
|
||||
if c.table {
|
||||
func (c *Context) postFormat(tmpl *template.Template, subContext subContext) {
|
||||
if c.Format.IsTable() {
|
||||
if len(c.header) == 0 {
|
||||
// if we still don't have a header, we didn't have any containers so we need to fake it to get the right headers from the template
|
||||
tmpl.Execute(bytes.NewBufferString(""), subContext)
|
||||
c.header = subContext.fullHeader()
|
||||
c.header = subContext.FullHeader()
|
||||
}
|
||||
|
||||
t := tabwriter.NewWriter(c.Output, 20, 1, 3, ' ', 0)
|
||||
|
@ -78,13 +89,35 @@ func (c *Context) postformat(tmpl *template.Template, subContext subContext) {
|
|||
|
||||
func (c *Context) contextFormat(tmpl *template.Template, subContext subContext) error {
|
||||
if err := tmpl.Execute(c.buffer, subContext); err != nil {
|
||||
c.buffer = bytes.NewBufferString(fmt.Sprintf("Template parsing error: %v\n", err))
|
||||
c.buffer.WriteTo(c.Output)
|
||||
return err
|
||||
return fmt.Errorf("Template parsing error: %v\n", err)
|
||||
}
|
||||
if c.table && len(c.header) == 0 {
|
||||
c.header = subContext.fullHeader()
|
||||
if c.Format.IsTable() && len(c.header) == 0 {
|
||||
c.header = subContext.FullHeader()
|
||||
}
|
||||
c.buffer.WriteString("\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
// SubFormat is a function type accepted by Write()
|
||||
type SubFormat func(func(subContext) error) error
|
||||
|
||||
// Write the template to the buffer using this Context
|
||||
func (c *Context) Write(sub subContext, f SubFormat) error {
|
||||
c.buffer = bytes.NewBufferString("")
|
||||
c.preFormat()
|
||||
|
||||
tmpl, err := c.parseFormat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
subFormat := func(subContext subContext) error {
|
||||
return c.contextFormat(tmpl, subContext)
|
||||
}
|
||||
if err := f(subFormat); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.postFormat(tmpl, sub)
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
package formatter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/docker/reference"
|
||||
"github.com/docker/go-units"
|
||||
units "github.com/docker/go-units"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -25,59 +23,63 @@ const (
|
|||
type ImageContext struct {
|
||||
Context
|
||||
Digest bool
|
||||
// Images
|
||||
Images []types.Image
|
||||
}
|
||||
|
||||
func isDangling(image types.Image) bool {
|
||||
return len(image.RepoTags) == 1 && image.RepoTags[0] == "<none>:<none>" && len(image.RepoDigests) == 1 && image.RepoDigests[0] == "<none>@<none>"
|
||||
}
|
||||
|
||||
func (ctx ImageContext) Write() {
|
||||
switch ctx.Format {
|
||||
case tableFormatKey:
|
||||
ctx.Format = defaultImageTableFormat
|
||||
if ctx.Digest {
|
||||
ctx.Format = defaultImageTableFormatWithDigest
|
||||
// NewImageFormat returns a format for rendering an ImageContext
|
||||
func NewImageFormat(source string, quiet bool, digest bool) Format {
|
||||
switch source {
|
||||
case TableFormatKey:
|
||||
switch {
|
||||
case quiet:
|
||||
return defaultQuietFormat
|
||||
case digest:
|
||||
return defaultImageTableFormatWithDigest
|
||||
default:
|
||||
return defaultImageTableFormat
|
||||
}
|
||||
if ctx.Quiet {
|
||||
ctx.Format = defaultQuietFormat
|
||||
}
|
||||
case rawFormatKey:
|
||||
if ctx.Quiet {
|
||||
ctx.Format = `image_id: {{.ID}}`
|
||||
} else {
|
||||
if ctx.Digest {
|
||||
ctx.Format = `repository: {{ .Repository }}
|
||||
case RawFormatKey:
|
||||
switch {
|
||||
case quiet:
|
||||
return `image_id: {{.ID}}`
|
||||
case digest:
|
||||
return `repository: {{ .Repository }}
|
||||
tag: {{.Tag}}
|
||||
digest: {{.Digest}}
|
||||
image_id: {{.ID}}
|
||||
created_at: {{.CreatedAt}}
|
||||
virtual_size: {{.Size}}
|
||||
`
|
||||
} else {
|
||||
ctx.Format = `repository: {{ .Repository }}
|
||||
default:
|
||||
return `repository: {{ .Repository }}
|
||||
tag: {{.Tag}}
|
||||
image_id: {{.ID}}
|
||||
created_at: {{.CreatedAt}}
|
||||
virtual_size: {{.Size}}
|
||||
`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.buffer = bytes.NewBufferString("")
|
||||
ctx.preformat()
|
||||
if ctx.table && ctx.Digest && !strings.Contains(ctx.Format, "{{.Digest}}") {
|
||||
ctx.finalFormat += "\t{{.Digest}}"
|
||||
format := Format(source)
|
||||
if format.IsTable() && digest && !format.Contains("{{.Digest}}") {
|
||||
format += "\t{{.Digest}}"
|
||||
}
|
||||
return format
|
||||
}
|
||||
|
||||
tmpl, err := ctx.parseFormat()
|
||||
if err != nil {
|
||||
return
|
||||
// ImageWrite writes the formatter images using the ImageContext
|
||||
func ImageWrite(ctx ImageContext, images []types.Image) error {
|
||||
render := func(format func(subContext subContext) error) error {
|
||||
return imageFormat(ctx, images, format)
|
||||
}
|
||||
return ctx.Write(&imageContext{}, render)
|
||||
}
|
||||
|
||||
for _, image := range ctx.Images {
|
||||
func imageFormat(ctx ImageContext, images []types.Image, format func(subContext subContext) error) error {
|
||||
for _, image := range images {
|
||||
images := []*imageContext{}
|
||||
if isDangling(image) {
|
||||
images = append(images, &imageContext{
|
||||
|
@ -170,18 +172,16 @@ virtual_size: {{.Size}}
|
|||
}
|
||||
}
|
||||
for _, imageCtx := range images {
|
||||
err = ctx.contextFormat(tmpl, imageCtx)
|
||||
if err != nil {
|
||||
return
|
||||
if err := format(imageCtx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.postformat(tmpl, &imageContext{})
|
||||
return nil
|
||||
}
|
||||
|
||||
type imageContext struct {
|
||||
baseSubContext
|
||||
HeaderContext
|
||||
trunc bool
|
||||
i types.Image
|
||||
repo string
|
||||
|
@ -190,7 +190,7 @@ type imageContext struct {
|
|||
}
|
||||
|
||||
func (c *imageContext) ID() string {
|
||||
c.addHeader(imageIDHeader)
|
||||
c.AddHeader(imageIDHeader)
|
||||
if c.trunc {
|
||||
return stringid.TruncateID(c.i.ID)
|
||||
}
|
||||
|
@ -198,32 +198,32 @@ func (c *imageContext) ID() string {
|
|||
}
|
||||
|
||||
func (c *imageContext) Repository() string {
|
||||
c.addHeader(repositoryHeader)
|
||||
c.AddHeader(repositoryHeader)
|
||||
return c.repo
|
||||
}
|
||||
|
||||
func (c *imageContext) Tag() string {
|
||||
c.addHeader(tagHeader)
|
||||
c.AddHeader(tagHeader)
|
||||
return c.tag
|
||||
}
|
||||
|
||||
func (c *imageContext) Digest() string {
|
||||
c.addHeader(digestHeader)
|
||||
c.AddHeader(digestHeader)
|
||||
return c.digest
|
||||
}
|
||||
|
||||
func (c *imageContext) CreatedSince() string {
|
||||
c.addHeader(createdSinceHeader)
|
||||
c.AddHeader(createdSinceHeader)
|
||||
createdAt := time.Unix(int64(c.i.Created), 0)
|
||||
return units.HumanDuration(time.Now().UTC().Sub(createdAt))
|
||||
}
|
||||
|
||||
func (c *imageContext) CreatedAt() string {
|
||||
c.addHeader(createdAtHeader)
|
||||
c.AddHeader(createdAtHeader)
|
||||
return time.Unix(int64(c.i.Created), 0).String()
|
||||
}
|
||||
|
||||
func (c *imageContext) Size() string {
|
||||
c.addHeader(sizeHeader)
|
||||
c.AddHeader(sizeHeader)
|
||||
return units.HumanSizeWithPrecision(float64(c.i.Size), 3)
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/docker/pkg/testutil/assert"
|
||||
)
|
||||
|
||||
func TestImageContext(t *testing.T) {
|
||||
|
@ -66,7 +67,7 @@ func TestImageContext(t *testing.T) {
|
|||
t.Fatalf("Expected %s, was %s\n", c.expValue, v)
|
||||
}
|
||||
|
||||
h := ctx.fullHeader()
|
||||
h := ctx.FullHeader()
|
||||
if h != c.expHeader {
|
||||
t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
|
||||
}
|
||||
|
@ -77,7 +78,7 @@ func TestImageContextWrite(t *testing.T) {
|
|||
unixTime := time.Now().AddDate(0, 0, -1).Unix()
|
||||
expectedTime := time.Unix(unixTime, 0).String()
|
||||
|
||||
contexts := []struct {
|
||||
cases := []struct {
|
||||
context ImageContext
|
||||
expected string
|
||||
}{
|
||||
|
@ -104,7 +105,7 @@ func TestImageContextWrite(t *testing.T) {
|
|||
{
|
||||
ImageContext{
|
||||
Context: Context{
|
||||
Format: "table",
|
||||
Format: NewImageFormat("table", false, false),
|
||||
},
|
||||
},
|
||||
`REPOSITORY TAG IMAGE ID CREATED SIZE
|
||||
|
@ -116,7 +117,7 @@ image tag2 imageID2 24 hours ago
|
|||
{
|
||||
ImageContext{
|
||||
Context: Context{
|
||||
Format: "table {{.Repository}}",
|
||||
Format: NewImageFormat("table {{.Repository}}", false, false),
|
||||
},
|
||||
},
|
||||
"REPOSITORY\nimage\nimage\n<none>\n",
|
||||
|
@ -124,7 +125,7 @@ image tag2 imageID2 24 hours ago
|
|||
{
|
||||
ImageContext{
|
||||
Context: Context{
|
||||
Format: "table {{.Repository}}",
|
||||
Format: NewImageFormat("table {{.Repository}}", false, true),
|
||||
},
|
||||
Digest: true,
|
||||
},
|
||||
|
@ -137,8 +138,7 @@ image <none>
|
|||
{
|
||||
ImageContext{
|
||||
Context: Context{
|
||||
Format: "table {{.Repository}}",
|
||||
Quiet: true,
|
||||
Format: NewImageFormat("table {{.Repository}}", true, false),
|
||||
},
|
||||
},
|
||||
"REPOSITORY\nimage\nimage\n<none>\n",
|
||||
|
@ -146,8 +146,7 @@ image <none>
|
|||
{
|
||||
ImageContext{
|
||||
Context: Context{
|
||||
Format: "table",
|
||||
Quiet: true,
|
||||
Format: NewImageFormat("table", true, false),
|
||||
},
|
||||
},
|
||||
"imageID1\nimageID2\nimageID3\n",
|
||||
|
@ -155,8 +154,7 @@ image <none>
|
|||
{
|
||||
ImageContext{
|
||||
Context: Context{
|
||||
Format: "table",
|
||||
Quiet: false,
|
||||
Format: NewImageFormat("table", false, true),
|
||||
},
|
||||
Digest: true,
|
||||
},
|
||||
|
@ -169,8 +167,7 @@ image tag2 <none>
|
|||
{
|
||||
ImageContext{
|
||||
Context: Context{
|
||||
Format: "table",
|
||||
Quiet: true,
|
||||
Format: NewImageFormat("table", true, true),
|
||||
},
|
||||
Digest: true,
|
||||
},
|
||||
|
@ -180,7 +177,7 @@ image tag2 <none>
|
|||
{
|
||||
ImageContext{
|
||||
Context: Context{
|
||||
Format: "raw",
|
||||
Format: NewImageFormat("raw", false, false),
|
||||
},
|
||||
},
|
||||
fmt.Sprintf(`repository: image
|
||||
|
@ -206,7 +203,7 @@ virtual_size: 0 B
|
|||
{
|
||||
ImageContext{
|
||||
Context: Context{
|
||||
Format: "raw",
|
||||
Format: NewImageFormat("raw", false, true),
|
||||
},
|
||||
Digest: true,
|
||||
},
|
||||
|
@ -236,8 +233,7 @@ virtual_size: 0 B
|
|||
{
|
||||
ImageContext{
|
||||
Context: Context{
|
||||
Format: "raw",
|
||||
Quiet: true,
|
||||
Format: NewImageFormat("raw", true, false),
|
||||
},
|
||||
},
|
||||
`image_id: imageID1
|
||||
|
@ -249,7 +245,7 @@ image_id: imageID3
|
|||
{
|
||||
ImageContext{
|
||||
Context: Context{
|
||||
Format: "{{.Repository}}",
|
||||
Format: NewImageFormat("{{.Repository}}", false, false),
|
||||
},
|
||||
},
|
||||
"image\nimage\n<none>\n",
|
||||
|
@ -257,7 +253,7 @@ image_id: imageID3
|
|||
{
|
||||
ImageContext{
|
||||
Context: Context{
|
||||
Format: "{{.Repository}}",
|
||||
Format: NewImageFormat("{{.Repository}}", false, true),
|
||||
},
|
||||
Digest: true,
|
||||
},
|
||||
|
@ -265,22 +261,20 @@ image_id: imageID3
|
|||
},
|
||||
}
|
||||
|
||||
for _, context := range contexts {
|
||||
for _, testcase := range cases {
|
||||
images := []types.Image{
|
||||
{ID: "imageID1", RepoTags: []string{"image:tag1"}, RepoDigests: []string{"image@sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf"}, Created: unixTime},
|
||||
{ID: "imageID2", RepoTags: []string{"image:tag2"}, Created: unixTime},
|
||||
{ID: "imageID3", RepoTags: []string{"<none>:<none>"}, RepoDigests: []string{"<none>@<none>"}, Created: unixTime},
|
||||
}
|
||||
out := bytes.NewBufferString("")
|
||||
context.context.Output = out
|
||||
context.context.Images = images
|
||||
context.context.Write()
|
||||
actual := out.String()
|
||||
if actual != context.expected {
|
||||
t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
|
||||
testcase.context.Output = out
|
||||
err := ImageWrite(testcase.context, images)
|
||||
if err != nil {
|
||||
assert.Error(t, err, testcase.expected)
|
||||
} else {
|
||||
assert.Equal(t, out.String(), testcase.expected)
|
||||
}
|
||||
// Clean buffer
|
||||
out.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -295,7 +289,7 @@ func TestImageContextWriteWithNoImage(t *testing.T) {
|
|||
{
|
||||
ImageContext{
|
||||
Context: Context{
|
||||
Format: "{{.Repository}}",
|
||||
Format: NewImageFormat("{{.Repository}}", false, false),
|
||||
Output: out,
|
||||
},
|
||||
},
|
||||
|
@ -304,7 +298,7 @@ func TestImageContextWriteWithNoImage(t *testing.T) {
|
|||
{
|
||||
ImageContext{
|
||||
Context: Context{
|
||||
Format: "table {{.Repository}}",
|
||||
Format: NewImageFormat("table {{.Repository}}", false, false),
|
||||
Output: out,
|
||||
},
|
||||
},
|
||||
|
@ -313,32 +307,26 @@ func TestImageContextWriteWithNoImage(t *testing.T) {
|
|||
{
|
||||
ImageContext{
|
||||
Context: Context{
|
||||
Format: "{{.Repository}}",
|
||||
Format: NewImageFormat("{{.Repository}}", false, true),
|
||||
Output: out,
|
||||
},
|
||||
Digest: true,
|
||||
},
|
||||
"",
|
||||
},
|
||||
{
|
||||
ImageContext{
|
||||
Context: Context{
|
||||
Format: "table {{.Repository}}",
|
||||
Format: NewImageFormat("table {{.Repository}}", false, true),
|
||||
Output: out,
|
||||
},
|
||||
Digest: true,
|
||||
},
|
||||
"REPOSITORY DIGEST\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, context := range contexts {
|
||||
context.context.Images = images
|
||||
context.context.Write()
|
||||
actual := out.String()
|
||||
if actual != context.expected {
|
||||
t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
|
||||
}
|
||||
ImageWrite(context.context, images)
|
||||
assert.Equal(t, out.String(), context.expected)
|
||||
// Clean buffer
|
||||
out.Reset()
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package formatter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
|
@ -17,60 +16,45 @@ const (
|
|||
internalHeader = "INTERNAL"
|
||||
)
|
||||
|
||||
// NetworkContext contains network specific information required by the formatter,
|
||||
// encapsulate a Context struct.
|
||||
type NetworkContext struct {
|
||||
Context
|
||||
// Networks
|
||||
Networks []types.NetworkResource
|
||||
// NewNetworkFormat returns a Format for rendering using a network Context
|
||||
func NewNetworkFormat(source string, quiet bool) Format {
|
||||
switch source {
|
||||
case TableFormatKey:
|
||||
if quiet {
|
||||
return defaultQuietFormat
|
||||
}
|
||||
return defaultNetworkTableFormat
|
||||
case RawFormatKey:
|
||||
if quiet {
|
||||
return `network_id: {{.ID}}`
|
||||
}
|
||||
return `network_id: {{.ID}}\nname: {{.Name}}\ndriver: {{.Driver}}\nscope: {{.Scope}}\n`
|
||||
}
|
||||
return Format(source)
|
||||
}
|
||||
|
||||
func (ctx NetworkContext) Write() {
|
||||
switch ctx.Format {
|
||||
case tableFormatKey:
|
||||
if ctx.Quiet {
|
||||
ctx.Format = defaultQuietFormat
|
||||
} else {
|
||||
ctx.Format = defaultNetworkTableFormat
|
||||
}
|
||||
case rawFormatKey:
|
||||
if ctx.Quiet {
|
||||
ctx.Format = `network_id: {{.ID}}`
|
||||
} else {
|
||||
ctx.Format = `network_id: {{.ID}}\nname: {{.Name}}\ndriver: {{.Driver}}\nscope: {{.Scope}}\n`
|
||||
// NetworkWrite writes the context
|
||||
func NetworkWrite(ctx Context, networks []types.NetworkResource) error {
|
||||
render := func(format func(subContext subContext) error) error {
|
||||
for _, network := range networks {
|
||||
networkCtx := &networkContext{trunc: ctx.Trunc, n: network}
|
||||
if err := format(networkCtx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx.buffer = bytes.NewBufferString("")
|
||||
ctx.preformat()
|
||||
|
||||
tmpl, err := ctx.parseFormat()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, network := range ctx.Networks {
|
||||
networkCtx := &networkContext{
|
||||
trunc: ctx.Trunc,
|
||||
n: network,
|
||||
}
|
||||
err = ctx.contextFormat(tmpl, networkCtx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.postformat(tmpl, &networkContext{})
|
||||
return ctx.Write(&networkContext{}, render)
|
||||
}
|
||||
|
||||
type networkContext struct {
|
||||
baseSubContext
|
||||
HeaderContext
|
||||
trunc bool
|
||||
n types.NetworkResource
|
||||
}
|
||||
|
||||
func (c *networkContext) ID() string {
|
||||
c.addHeader(networkIDHeader)
|
||||
c.AddHeader(networkIDHeader)
|
||||
if c.trunc {
|
||||
return stringid.TruncateID(c.n.ID)
|
||||
}
|
||||
|
@ -78,32 +62,32 @@ func (c *networkContext) ID() string {
|
|||
}
|
||||
|
||||
func (c *networkContext) Name() string {
|
||||
c.addHeader(nameHeader)
|
||||
c.AddHeader(nameHeader)
|
||||
return c.n.Name
|
||||
}
|
||||
|
||||
func (c *networkContext) Driver() string {
|
||||
c.addHeader(driverHeader)
|
||||
c.AddHeader(driverHeader)
|
||||
return c.n.Driver
|
||||
}
|
||||
|
||||
func (c *networkContext) Scope() string {
|
||||
c.addHeader(scopeHeader)
|
||||
c.AddHeader(scopeHeader)
|
||||
return c.n.Scope
|
||||
}
|
||||
|
||||
func (c *networkContext) IPv6() string {
|
||||
c.addHeader(ipv6Header)
|
||||
c.AddHeader(ipv6Header)
|
||||
return fmt.Sprintf("%v", c.n.EnableIPv6)
|
||||
}
|
||||
|
||||
func (c *networkContext) Internal() string {
|
||||
c.addHeader(internalHeader)
|
||||
c.AddHeader(internalHeader)
|
||||
return fmt.Sprintf("%v", c.n.Internal)
|
||||
}
|
||||
|
||||
func (c *networkContext) Labels() string {
|
||||
c.addHeader(labelsHeader)
|
||||
c.AddHeader(labelsHeader)
|
||||
if c.n.Labels == nil {
|
||||
return ""
|
||||
}
|
||||
|
@ -120,7 +104,7 @@ func (c *networkContext) Label(name string) string {
|
|||
r := strings.NewReplacer("-", " ", "_", " ")
|
||||
h := r.Replace(n[len(n)-1])
|
||||
|
||||
c.addHeader(h)
|
||||
c.AddHeader(h)
|
||||
|
||||
if c.n.Labels == nil {
|
||||
return ""
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/docker/pkg/testutil/assert"
|
||||
)
|
||||
|
||||
func TestNetworkContext(t *testing.T) {
|
||||
|
@ -62,7 +63,7 @@ func TestNetworkContext(t *testing.T) {
|
|||
t.Fatalf("Expected %s, was %s\n", c.expValue, v)
|
||||
}
|
||||
|
||||
h := ctx.fullHeader()
|
||||
h := ctx.FullHeader()
|
||||
if h != c.expHeader {
|
||||
t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
|
||||
}
|
||||
|
@ -70,71 +71,45 @@ func TestNetworkContext(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestNetworkContextWrite(t *testing.T) {
|
||||
contexts := []struct {
|
||||
context NetworkContext
|
||||
cases := []struct {
|
||||
context Context
|
||||
expected string
|
||||
}{
|
||||
|
||||
// Errors
|
||||
{
|
||||
NetworkContext{
|
||||
Context: Context{
|
||||
Format: "{{InvalidFunction}}",
|
||||
},
|
||||
},
|
||||
Context{Format: "{{InvalidFunction}}"},
|
||||
`Template parsing error: template: :1: function "InvalidFunction" not defined
|
||||
`,
|
||||
},
|
||||
{
|
||||
NetworkContext{
|
||||
Context: Context{
|
||||
Format: "{{nil}}",
|
||||
},
|
||||
},
|
||||
Context{Format: "{{nil}}"},
|
||||
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
|
||||
`,
|
||||
},
|
||||
// Table format
|
||||
{
|
||||
NetworkContext{
|
||||
Context: Context{
|
||||
Format: "table",
|
||||
},
|
||||
},
|
||||
Context{Format: NewNetworkFormat("table", false)},
|
||||
`NETWORK ID NAME DRIVER SCOPE
|
||||
networkID1 foobar_baz foo local
|
||||
networkID2 foobar_bar bar local
|
||||
`,
|
||||
},
|
||||
{
|
||||
NetworkContext{
|
||||
Context: Context{
|
||||
Format: "table",
|
||||
Quiet: true,
|
||||
},
|
||||
},
|
||||
Context{Format: NewNetworkFormat("table", true)},
|
||||
`networkID1
|
||||
networkID2
|
||||
`,
|
||||
},
|
||||
{
|
||||
NetworkContext{
|
||||
Context: Context{
|
||||
Format: "table {{.Name}}",
|
||||
},
|
||||
},
|
||||
Context{Format: NewNetworkFormat("table {{.Name}}", false)},
|
||||
`NAME
|
||||
foobar_baz
|
||||
foobar_bar
|
||||
`,
|
||||
},
|
||||
{
|
||||
NetworkContext{
|
||||
Context: Context{
|
||||
Format: "table {{.Name}}",
|
||||
Quiet: true,
|
||||
},
|
||||
},
|
||||
Context{Format: NewNetworkFormat("table {{.Name}}", true)},
|
||||
`NAME
|
||||
foobar_baz
|
||||
foobar_bar
|
||||
|
@ -142,11 +117,8 @@ foobar_bar
|
|||
},
|
||||
// Raw Format
|
||||
{
|
||||
NetworkContext{
|
||||
Context: Context{
|
||||
Format: "raw",
|
||||
},
|
||||
}, `network_id: networkID1
|
||||
Context{Format: NewNetworkFormat("raw", false)},
|
||||
`network_id: networkID1
|
||||
name: foobar_baz
|
||||
driver: foo
|
||||
scope: local
|
||||
|
@ -159,43 +131,32 @@ scope: local
|
|||
`,
|
||||
},
|
||||
{
|
||||
NetworkContext{
|
||||
Context: Context{
|
||||
Format: "raw",
|
||||
Quiet: true,
|
||||
},
|
||||
},
|
||||
Context{Format: NewNetworkFormat("raw", true)},
|
||||
`network_id: networkID1
|
||||
network_id: networkID2
|
||||
`,
|
||||
},
|
||||
// Custom Format
|
||||
{
|
||||
NetworkContext{
|
||||
Context: Context{
|
||||
Format: "{{.Name}}",
|
||||
},
|
||||
},
|
||||
Context{Format: NewNetworkFormat("{{.Name}}", false)},
|
||||
`foobar_baz
|
||||
foobar_bar
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, context := range contexts {
|
||||
for _, testcase := range cases {
|
||||
networks := []types.NetworkResource{
|
||||
{ID: "networkID1", Name: "foobar_baz", Driver: "foo", Scope: "local"},
|
||||
{ID: "networkID2", Name: "foobar_bar", Driver: "bar", Scope: "local"},
|
||||
}
|
||||
out := bytes.NewBufferString("")
|
||||
context.context.Output = out
|
||||
context.context.Networks = networks
|
||||
context.context.Write()
|
||||
actual := out.String()
|
||||
if actual != context.expected {
|
||||
t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
|
||||
testcase.context.Output = out
|
||||
err := NetworkWrite(testcase.context, networks)
|
||||
if err != nil {
|
||||
assert.Error(t, err, testcase.expected)
|
||||
} else {
|
||||
assert.Equal(t, out.String(), testcase.expected)
|
||||
}
|
||||
// Clean buffer
|
||||
out.Reset()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package formatter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
|
@ -16,78 +15,63 @@ const (
|
|||
// Status header ?
|
||||
)
|
||||
|
||||
// VolumeContext contains volume specific information required by the formatter,
|
||||
// encapsulate a Context struct.
|
||||
type VolumeContext struct {
|
||||
Context
|
||||
// Volumes
|
||||
Volumes []*types.Volume
|
||||
// NewVolumeFormat returns a format for use with a volume Context
|
||||
func NewVolumeFormat(source string, quiet bool) Format {
|
||||
switch source {
|
||||
case TableFormatKey:
|
||||
if quiet {
|
||||
return defaultVolumeQuietFormat
|
||||
}
|
||||
return defaultVolumeTableFormat
|
||||
case RawFormatKey:
|
||||
if quiet {
|
||||
return `name: {{.Name}}`
|
||||
}
|
||||
return `name: {{.Name}}\ndriver: {{.Driver}}\n`
|
||||
}
|
||||
return Format(source)
|
||||
}
|
||||
|
||||
func (ctx VolumeContext) Write() {
|
||||
switch ctx.Format {
|
||||
case tableFormatKey:
|
||||
if ctx.Quiet {
|
||||
ctx.Format = defaultVolumeQuietFormat
|
||||
} else {
|
||||
ctx.Format = defaultVolumeTableFormat
|
||||
}
|
||||
case rawFormatKey:
|
||||
if ctx.Quiet {
|
||||
ctx.Format = `name: {{.Name}}`
|
||||
} else {
|
||||
ctx.Format = `name: {{.Name}}\ndriver: {{.Driver}}\n`
|
||||
// VolumeWrite writes formatted volumes using the Context
|
||||
func VolumeWrite(ctx Context, volumes []*types.Volume) error {
|
||||
render := func(format func(subContext subContext) error) error {
|
||||
for _, volume := range volumes {
|
||||
if err := format(&volumeContext{v: *volume}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx.buffer = bytes.NewBufferString("")
|
||||
ctx.preformat()
|
||||
|
||||
tmpl, err := ctx.parseFormat()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, volume := range ctx.Volumes {
|
||||
volumeCtx := &volumeContext{
|
||||
v: volume,
|
||||
}
|
||||
err = ctx.contextFormat(tmpl, volumeCtx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.postformat(tmpl, &networkContext{})
|
||||
return ctx.Write(&volumeContext{}, render)
|
||||
}
|
||||
|
||||
type volumeContext struct {
|
||||
baseSubContext
|
||||
v *types.Volume
|
||||
HeaderContext
|
||||
v types.Volume
|
||||
}
|
||||
|
||||
func (c *volumeContext) Name() string {
|
||||
c.addHeader(nameHeader)
|
||||
c.AddHeader(nameHeader)
|
||||
return c.v.Name
|
||||
}
|
||||
|
||||
func (c *volumeContext) Driver() string {
|
||||
c.addHeader(driverHeader)
|
||||
c.AddHeader(driverHeader)
|
||||
return c.v.Driver
|
||||
}
|
||||
|
||||
func (c *volumeContext) Scope() string {
|
||||
c.addHeader(scopeHeader)
|
||||
c.AddHeader(scopeHeader)
|
||||
return c.v.Scope
|
||||
}
|
||||
|
||||
func (c *volumeContext) Mountpoint() string {
|
||||
c.addHeader(mountpointHeader)
|
||||
c.AddHeader(mountpointHeader)
|
||||
return c.v.Mountpoint
|
||||
}
|
||||
|
||||
func (c *volumeContext) Labels() string {
|
||||
c.addHeader(labelsHeader)
|
||||
c.AddHeader(labelsHeader)
|
||||
if c.v.Labels == nil {
|
||||
return ""
|
||||
}
|
||||
|
@ -105,7 +89,7 @@ func (c *volumeContext) Label(name string) string {
|
|||
r := strings.NewReplacer("-", " ", "_", " ")
|
||||
h := r.Replace(n[len(n)-1])
|
||||
|
||||
c.addHeader(h)
|
||||
c.AddHeader(h)
|
||||
|
||||
if c.v.Labels == nil {
|
||||
return ""
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/docker/pkg/testutil/assert"
|
||||
)
|
||||
|
||||
func TestVolumeContext(t *testing.T) {
|
||||
|
@ -20,22 +21,22 @@ func TestVolumeContext(t *testing.T) {
|
|||
call func() string
|
||||
}{
|
||||
{volumeContext{
|
||||
v: &types.Volume{Name: volumeName},
|
||||
v: types.Volume{Name: volumeName},
|
||||
}, volumeName, nameHeader, ctx.Name},
|
||||
{volumeContext{
|
||||
v: &types.Volume{Driver: "driver_name"},
|
||||
v: types.Volume{Driver: "driver_name"},
|
||||
}, "driver_name", driverHeader, ctx.Driver},
|
||||
{volumeContext{
|
||||
v: &types.Volume{Scope: "local"},
|
||||
v: types.Volume{Scope: "local"},
|
||||
}, "local", scopeHeader, ctx.Scope},
|
||||
{volumeContext{
|
||||
v: &types.Volume{Mountpoint: "mountpoint"},
|
||||
v: types.Volume{Mountpoint: "mountpoint"},
|
||||
}, "mountpoint", mountpointHeader, ctx.Mountpoint},
|
||||
{volumeContext{
|
||||
v: &types.Volume{},
|
||||
v: types.Volume{},
|
||||
}, "", labelsHeader, ctx.Labels},
|
||||
{volumeContext{
|
||||
v: &types.Volume{Labels: map[string]string{"label1": "value1", "label2": "value2"}},
|
||||
v: types.Volume{Labels: map[string]string{"label1": "value1", "label2": "value2"}},
|
||||
}, "label1=value1,label2=value2", labelsHeader, ctx.Labels},
|
||||
}
|
||||
|
||||
|
@ -48,7 +49,7 @@ func TestVolumeContext(t *testing.T) {
|
|||
t.Fatalf("Expected %s, was %s\n", c.expValue, v)
|
||||
}
|
||||
|
||||
h := ctx.fullHeader()
|
||||
h := ctx.FullHeader()
|
||||
if h != c.expHeader {
|
||||
t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
|
||||
}
|
||||
|
@ -56,71 +57,45 @@ func TestVolumeContext(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestVolumeContextWrite(t *testing.T) {
|
||||
contexts := []struct {
|
||||
context VolumeContext
|
||||
cases := []struct {
|
||||
context Context
|
||||
expected string
|
||||
}{
|
||||
|
||||
// Errors
|
||||
{
|
||||
VolumeContext{
|
||||
Context: Context{
|
||||
Format: "{{InvalidFunction}}",
|
||||
},
|
||||
},
|
||||
Context{Format: "{{InvalidFunction}}"},
|
||||
`Template parsing error: template: :1: function "InvalidFunction" not defined
|
||||
`,
|
||||
},
|
||||
{
|
||||
VolumeContext{
|
||||
Context: Context{
|
||||
Format: "{{nil}}",
|
||||
},
|
||||
},
|
||||
Context{Format: "{{nil}}"},
|
||||
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
|
||||
`,
|
||||
},
|
||||
// Table format
|
||||
{
|
||||
VolumeContext{
|
||||
Context: Context{
|
||||
Format: "table",
|
||||
},
|
||||
},
|
||||
Context{Format: NewVolumeFormat("table", false)},
|
||||
`DRIVER NAME
|
||||
foo foobar_baz
|
||||
bar foobar_bar
|
||||
`,
|
||||
},
|
||||
{
|
||||
VolumeContext{
|
||||
Context: Context{
|
||||
Format: "table",
|
||||
Quiet: true,
|
||||
},
|
||||
},
|
||||
Context{Format: NewVolumeFormat("table", true)},
|
||||
`foobar_baz
|
||||
foobar_bar
|
||||
`,
|
||||
},
|
||||
{
|
||||
VolumeContext{
|
||||
Context: Context{
|
||||
Format: "table {{.Name}}",
|
||||
},
|
||||
},
|
||||
Context{Format: NewVolumeFormat("table {{.Name}}", false)},
|
||||
`NAME
|
||||
foobar_baz
|
||||
foobar_bar
|
||||
`,
|
||||
},
|
||||
{
|
||||
VolumeContext{
|
||||
Context: Context{
|
||||
Format: "table {{.Name}}",
|
||||
Quiet: true,
|
||||
},
|
||||
},
|
||||
Context{Format: NewVolumeFormat("table {{.Name}}", true)},
|
||||
`NAME
|
||||
foobar_baz
|
||||
foobar_bar
|
||||
|
@ -128,11 +103,8 @@ foobar_bar
|
|||
},
|
||||
// Raw Format
|
||||
{
|
||||
VolumeContext{
|
||||
Context: Context{
|
||||
Format: "raw",
|
||||
},
|
||||
}, `name: foobar_baz
|
||||
Context{Format: NewVolumeFormat("raw", false)},
|
||||
`name: foobar_baz
|
||||
driver: foo
|
||||
|
||||
name: foobar_bar
|
||||
|
@ -141,43 +113,32 @@ driver: bar
|
|||
`,
|
||||
},
|
||||
{
|
||||
VolumeContext{
|
||||
Context: Context{
|
||||
Format: "raw",
|
||||
Quiet: true,
|
||||
},
|
||||
},
|
||||
Context{Format: NewVolumeFormat("raw", true)},
|
||||
`name: foobar_baz
|
||||
name: foobar_bar
|
||||
`,
|
||||
},
|
||||
// Custom Format
|
||||
{
|
||||
VolumeContext{
|
||||
Context: Context{
|
||||
Format: "{{.Name}}",
|
||||
},
|
||||
},
|
||||
Context{Format: NewVolumeFormat("{{.Name}}", false)},
|
||||
`foobar_baz
|
||||
foobar_bar
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, context := range contexts {
|
||||
for _, testcase := range cases {
|
||||
volumes := []*types.Volume{
|
||||
{Name: "foobar_baz", Driver: "foo"},
|
||||
{Name: "foobar_bar", Driver: "bar"},
|
||||
}
|
||||
out := bytes.NewBufferString("")
|
||||
context.context.Output = out
|
||||
context.context.Volumes = volumes
|
||||
context.context.Write()
|
||||
actual := out.String()
|
||||
if actual != context.expected {
|
||||
t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
|
||||
testcase.context.Output = out
|
||||
err := VolumeWrite(testcase.context, volumes)
|
||||
if err != nil {
|
||||
assert.Error(t, err, testcase.expected)
|
||||
} else {
|
||||
assert.Equal(t, out.String(), testcase.expected)
|
||||
}
|
||||
// Clean buffer
|
||||
out.Reset()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -64,27 +64,22 @@ func runImages(dockerCli *command.DockerCli, opts imagesOptions) error {
|
|||
return err
|
||||
}
|
||||
|
||||
f := opts.format
|
||||
if len(f) == 0 {
|
||||
format := opts.format
|
||||
if len(format) == 0 {
|
||||
if len(dockerCli.ConfigFile().ImagesFormat) > 0 && !opts.quiet {
|
||||
f = dockerCli.ConfigFile().ImagesFormat
|
||||
format = dockerCli.ConfigFile().ImagesFormat
|
||||
} else {
|
||||
f = "table"
|
||||
format = formatter.TableFormatKey
|
||||
}
|
||||
}
|
||||
|
||||
imagesCtx := formatter.ImageContext{
|
||||
imageCtx := formatter.ImageContext{
|
||||
Context: formatter.Context{
|
||||
Output: dockerCli.Out(),
|
||||
Format: f,
|
||||
Quiet: opts.quiet,
|
||||
Format: formatter.NewImageFormat(format, opts.quiet, opts.showDigests),
|
||||
Trunc: !opts.noTrunc,
|
||||
},
|
||||
Digest: opts.showDigests,
|
||||
Images: images,
|
||||
}
|
||||
|
||||
imagesCtx.Write()
|
||||
|
||||
return nil
|
||||
return formatter.ImageWrite(imageCtx, images)
|
||||
}
|
||||
|
|
|
@ -50,35 +50,27 @@ func newListCommand(dockerCli *command.DockerCli) *cobra.Command {
|
|||
|
||||
func runList(dockerCli *command.DockerCli, opts listOptions) error {
|
||||
client := dockerCli.Client()
|
||||
|
||||
options := types.NetworkListOptions{Filters: opts.filter.Value()}
|
||||
networkResources, err := client.NetworkList(context.Background(), options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f := opts.format
|
||||
if len(f) == 0 {
|
||||
format := opts.format
|
||||
if len(format) == 0 {
|
||||
if len(dockerCli.ConfigFile().NetworksFormat) > 0 && !opts.quiet {
|
||||
f = dockerCli.ConfigFile().NetworksFormat
|
||||
format = dockerCli.ConfigFile().NetworksFormat
|
||||
} else {
|
||||
f = "table"
|
||||
format = formatter.TableFormatKey
|
||||
}
|
||||
}
|
||||
|
||||
sort.Sort(byNetworkName(networkResources))
|
||||
|
||||
networksCtx := formatter.NetworkContext{
|
||||
Context: formatter.Context{
|
||||
Output: dockerCli.Out(),
|
||||
Format: f,
|
||||
Quiet: opts.quiet,
|
||||
Trunc: !opts.noTrunc,
|
||||
},
|
||||
Networks: networkResources,
|
||||
networksCtx := formatter.Context{
|
||||
Output: dockerCli.Out(),
|
||||
Format: formatter.NewNetworkFormat(format, opts.quiet),
|
||||
Trunc: !opts.noTrunc,
|
||||
}
|
||||
|
||||
networksCtx.Write()
|
||||
|
||||
return nil
|
||||
return formatter.NetworkWrite(networksCtx, networkResources)
|
||||
}
|
||||
|
|
|
@ -56,29 +56,22 @@ func runList(dockerCli *command.DockerCli, opts listOptions) error {
|
|||
return err
|
||||
}
|
||||
|
||||
f := opts.format
|
||||
if len(f) == 0 {
|
||||
format := opts.format
|
||||
if len(format) == 0 {
|
||||
if len(dockerCli.ConfigFile().VolumesFormat) > 0 && !opts.quiet {
|
||||
f = dockerCli.ConfigFile().VolumesFormat
|
||||
format = dockerCli.ConfigFile().VolumesFormat
|
||||
} else {
|
||||
f = "table"
|
||||
format = formatter.TableFormatKey
|
||||
}
|
||||
}
|
||||
|
||||
sort.Sort(byVolumeName(volumes.Volumes))
|
||||
|
||||
volumeCtx := formatter.VolumeContext{
|
||||
Context: formatter.Context{
|
||||
Output: dockerCli.Out(),
|
||||
Format: f,
|
||||
Quiet: opts.quiet,
|
||||
},
|
||||
Volumes: volumes.Volumes,
|
||||
volumeCtx := formatter.Context{
|
||||
Output: dockerCli.Out(),
|
||||
Format: formatter.NewVolumeFormat(format, opts.quiet),
|
||||
}
|
||||
|
||||
volumeCtx.Write()
|
||||
|
||||
return nil
|
||||
return formatter.VolumeWrite(volumeCtx, volumes.Volumes)
|
||||
}
|
||||
|
||||
var listDescription = `
|
||||
|
|
|
@ -18,6 +18,7 @@ var basicFunctions = template.FuncMap{
|
|||
"title": strings.Title,
|
||||
"lower": strings.ToLower,
|
||||
"upper": strings.ToUpper,
|
||||
"pad": padWithSpace,
|
||||
}
|
||||
|
||||
// Parse creates a new annonymous template with the basic functions
|
||||
|
@ -31,3 +32,11 @@ func Parse(format string) (*template.Template, error) {
|
|||
func NewParse(tag, format string) (*template.Template, error) {
|
||||
return template.New(tag).Funcs(basicFunctions).Parse(format)
|
||||
}
|
||||
|
||||
// padWithSpace adds whitespace to the input if the input is non-empty
|
||||
func padWithSpace(source string, prefix, suffix int) string {
|
||||
if source == "" {
|
||||
return source
|
||||
}
|
||||
return strings.Repeat(" ", prefix) + source + strings.Repeat(" ", suffix)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue