From db0952ad22e9fc8e3241f0a2ed15e4f32cc70e15 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 12 Sep 2016 16:59:18 -0400 Subject: [PATCH] Refactor formatter. Signed-off-by: Daniel Nephin --- command/container/ps.go | 26 ++-- command/formatter/container.go | 107 +++++++-------- command/formatter/container_test.go | 200 +++++++++------------------- command/formatter/custom.go | 15 ++- command/formatter/formatter.go | 75 ++++++++--- command/formatter/image.go | 88 ++++++------ command/formatter/image_test.go | 68 ++++------ command/formatter/network.go | 84 +++++------- command/formatter/network_test.go | 81 +++-------- command/formatter/volume.go | 78 +++++------ command/formatter/volume_test.go | 81 +++-------- command/image/images.go | 19 +-- command/network/list.go | 26 ++-- command/volume/list.go | 23 ++-- 14 files changed, 381 insertions(+), 590 deletions(-) diff --git a/command/container/ps.go b/command/container/ps.go index 3583ee1092..9d015fd707 100644 --- a/command/container/ps.go +++ b/command/container/ps.go @@ -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 = "table" } } - 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, opts.size), + Trunc: !opts.noTrunc, } - - psCtx.Write() - - return nil + return formatter.ContainerWrite(containerCtx, containers) } diff --git a/command/formatter/container.go b/command/formatter/container.go index 6f519e4493..6f3a162fe3 100644 --- a/command/formatter/container.go +++ b/command/formatter/container.go @@ -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,53 @@ 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}}\nimage: {{.Image}}\ncommand: {{.Command}}\ncreated_at: {{.CreatedAt}}\nstatus: {{.Status}}\nnames: {{.Names}}\nlabels: {{.Labels}}\nports: {{.Ports}}\n` + 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 +79,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 +93,7 @@ func (c *containerContext) Names() string { } func (c *containerContext) Image() string { - c.addHeader(imageHeader) + c.AddHeader(imageHeader) if c.c.Image == "" { return "" } @@ -121,7 +106,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 +115,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 +148,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 +165,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 +174,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 diff --git a/command/formatter/container_test.go b/command/formatter/container_test.go index 29b8450db9..1ef48ae2d2 100644 --- a/command/formatter/container_test.go +++ b/command/formatter/container_test.go @@ -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 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() } diff --git a/command/formatter/custom.go b/command/formatter/custom.go index 2aa2e7b554..df32684429 100644 --- a/command/formatter/custom.go +++ b/command/formatter/custom.go @@ -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{} } diff --git a/command/formatter/formatter.go b/command/formatter/formatter.go index de71c3cdd4..32f9a4d359 100644 --- a/command/formatter/formatter.go +++ b/command/formatter/formatter.go @@ -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 +} diff --git a/command/formatter/image.go b/command/formatter/image.go index 012860e04e..54cb7b62fa 100644 --- a/command/formatter/image.go +++ b/command/formatter/image.go @@ -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] == ":" && len(image.RepoDigests) == 1 && image.RepoDigests[0] == "@" } -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) } diff --git a/command/formatter/image_test.go b/command/formatter/image_test.go index 7c87f393fc..6dc7f73db3 100644 --- a/command/formatter/image_test.go +++ b/command/formatter/image_test.go @@ -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\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 { ImageContext{ Context: Context{ - Format: "table {{.Repository}}", - Quiet: true, + Format: NewImageFormat("table {{.Repository}}", true, false), }, }, "REPOSITORY\nimage\nimage\n\n", @@ -146,8 +146,7 @@ image { ImageContext{ Context: Context{ - Format: "table", - Quiet: true, + Format: NewImageFormat("table", true, false), }, }, "imageID1\nimageID2\nimageID3\n", @@ -155,8 +154,7 @@ image { ImageContext{ Context: Context{ - Format: "table", - Quiet: false, + Format: NewImageFormat("table", false, true), }, Digest: true, }, @@ -169,8 +167,7 @@ image tag2 { ImageContext{ Context: Context{ - Format: "table", - Quiet: true, + Format: NewImageFormat("table", true, true), }, Digest: true, }, @@ -180,7 +177,7 @@ image tag2 { 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\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{":"}, RepoDigests: []string{"@"}, 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() } diff --git a/command/formatter/network.go b/command/formatter/network.go index 6eb820879e..d808fdc22d 100644 --- a/command/formatter/network.go +++ b/command/formatter/network.go @@ -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 "" diff --git a/command/formatter/network_test.go b/command/formatter/network_test.go index b5f826af6d..28f078548f 100644 --- a/command/formatter/network_test.go +++ b/command/formatter/network_test.go @@ -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 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() } } diff --git a/command/formatter/volume.go b/command/formatter/volume.go index ba24b06a4f..2fec59d8fb 100644 --- a/command/formatter/volume.go +++ b/command/formatter/volume.go @@ -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 + 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 "" diff --git a/command/formatter/volume_test.go b/command/formatter/volume_test.go index 2295eff3ef..1d5f74e42c 100644 --- a/command/formatter/volume_test.go +++ b/command/formatter/volume_test.go @@ -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) { @@ -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 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() } } diff --git a/command/image/images.go b/command/image/images.go index 648236dfe5..b7dbd05671 100644 --- a/command/image/images.go +++ b/command/image/images.go @@ -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 = "table" } } - 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) } diff --git a/command/network/list.go b/command/network/list.go index a0f2e7f4f0..dd7b72fea7 100644 --- a/command/network/list.go +++ b/command/network/list.go @@ -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 = "table" } } 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) } diff --git a/command/volume/list.go b/command/volume/list.go index 6d32d2cbfb..cdbbaafc61 100644 --- a/command/volume/list.go +++ b/command/volume/list.go @@ -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 = "table" } } 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 = `