Merge pull request #26537 from dnephin/refactor_cli_formatter

Refactor cli/command/formatter
This commit is contained in:
Vincent Demeester 2016-09-15 21:52:52 +02:00 committed by GitHub
commit a7c25f9540
15 changed files with 405 additions and 597 deletions

View file

@ -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)
}

View file

@ -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

View file

@ -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()
}

View file

@ -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{}
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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()
}

View file

@ -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 ""

View file

@ -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()
}
}

View file

@ -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 ""

View file

@ -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()
}
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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 = `

View file

@ -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)
}