Merge pull request #17692 from vdemeester/images-format

Add --format support to images command
This commit is contained in:
Alexander Morozov
2015-12-21 09:57:29 -08:00
17 changed files with 1126 additions and 479 deletions

View File

@@ -68,11 +68,17 @@ func (cli *DockerCli) CheckTtyInput(attachStdin, ttyMode bool) error {
}
// PsFormat returns the format string specified in the configuration.
// String contains columns and format specification, for example {{ID}\t{{Name}}.
// String contains columns and format specification, for example {{ID}}\t{{Name}}.
func (cli *DockerCli) PsFormat() string {
return cli.configFile.PsFormat
}
// ImagesFormat returns the format string specified in the configuration.
// String contains columns and format specification, for example {{ID}}\t{{Name}}.
func (cli *DockerCli) ImagesFormat() string {
return cli.configFile.ImagesFormat
}
// NewDockerCli returns a DockerCli instance with IO output and error streams set by in, out and err.
// The key file, protocol (i.e. unix) and address are passed in as strings, along with the tls.Config. If the tls.Config
// is set the client scheme will be set to https.

View File

@@ -1,4 +1,4 @@
package ps
package formatter
import (
"fmt"
@@ -16,26 +16,31 @@ import (
const (
tableKey = "table"
idHeader = "CONTAINER ID"
imageHeader = "IMAGE"
namesHeader = "NAMES"
commandHeader = "COMMAND"
createdAtHeader = "CREATED AT"
runningForHeader = "CREATED"
statusHeader = "STATUS"
portsHeader = "PORTS"
sizeHeader = "SIZE"
labelsHeader = "LABELS"
containerIDHeader = "CONTAINER ID"
imageHeader = "IMAGE"
namesHeader = "NAMES"
commandHeader = "COMMAND"
createdSinceHeader = "CREATED"
createdAtHeader = "CREATED AT"
runningForHeader = "CREATED"
statusHeader = "STATUS"
portsHeader = "PORTS"
sizeHeader = "SIZE"
labelsHeader = "LABELS"
imageIDHeader = "IMAGE ID"
repositoryHeader = "REPOSITORY"
tagHeader = "TAG"
digestHeader = "DIGEST"
)
type containerContext struct {
trunc bool
header []string
c types.Container
baseSubContext
trunc bool
c types.Container
}
func (c *containerContext) ID() string {
c.addHeader(idHeader)
c.addHeader(containerIDHeader)
if c.trunc {
return stringid.TruncateID(c.c.ID)
}
@@ -137,14 +142,71 @@ func (c *containerContext) Label(name string) string {
return c.c.Labels[name]
}
func (c *containerContext) fullHeader() string {
type imageContext struct {
baseSubContext
trunc bool
i types.Image
repo string
tag string
digest string
}
func (c *imageContext) ID() string {
c.addHeader(imageIDHeader)
if c.trunc {
return stringid.TruncateID(c.i.ID)
}
return c.i.ID
}
func (c *imageContext) Repository() string {
c.addHeader(repositoryHeader)
return c.repo
}
func (c *imageContext) Tag() string {
c.addHeader(tagHeader)
return c.tag
}
func (c *imageContext) Digest() string {
c.addHeader(digestHeader)
return c.digest
}
func (c *imageContext) CreatedSince() string {
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)
return time.Unix(int64(c.i.Created), 0).String()
}
func (c *imageContext) Size() string {
c.addHeader(sizeHeader)
return units.HumanSize(float64(c.i.Size))
}
type subContext interface {
fullHeader() string
addHeader(header string)
}
type baseSubContext struct {
header []string
}
func (c *baseSubContext) fullHeader() string {
if c.header == nil {
return ""
}
return strings.Join(c.header, "\t")
}
func (c *containerContext) addHeader(header string) {
func (c *baseSubContext) addHeader(header string) {
if c.header == nil {
c.header = []string{}
}

View File

@@ -1,4 +1,4 @@
package ps
package formatter
import (
"reflect"
@@ -22,8 +22,8 @@ func TestContainerPsContext(t *testing.T) {
expHeader string
call func() string
}{
{types.Container{ID: containerID}, true, stringid.TruncateID(containerID), idHeader, ctx.ID},
{types.Container{ID: containerID}, false, containerID, idHeader, ctx.ID},
{types.Container{ID: containerID}, true, stringid.TruncateID(containerID), containerIDHeader, ctx.ID},
{types.Container{ID: containerID}, false, containerID, containerIDHeader, ctx.ID},
{types.Container{Names: []string{"/foobar_baz"}}, true, "foobar_baz", namesHeader, ctx.Names},
{types.Container{Image: "ubuntu"}, true, "ubuntu", imageHeader, ctx.Image},
{types.Container{Image: "verylongimagename"}, true, "verylongimagename", imageHeader, ctx.Image},
@@ -62,24 +62,7 @@ func TestContainerPsContext(t *testing.T) {
ctx = containerContext{c: c.container, trunc: c.trunc}
v := c.call()
if strings.Contains(v, ",") {
// comma-separated values means probably a map input, which won't
// be guaranteed to have the same order as our expected value
// We'll create maps and use reflect.DeepEquals to check instead:
entriesMap := make(map[string]string)
expMap := make(map[string]string)
entries := strings.Split(v, ",")
expectedEntries := strings.Split(c.expValue, ",")
for _, entry := range entries {
keyval := strings.Split(entry, "=")
entriesMap[keyval[0]] = keyval[1]
}
for _, expected := range expectedEntries {
keyval := strings.Split(expected, "=")
expMap[keyval[0]] = keyval[1]
}
if !reflect.DeepEqual(expMap, entriesMap) {
t.Fatalf("Expected entries: %v, got: %v", c.expValue, v)
}
compareMultipleValues(t, v, c.expValue)
} else if v != c.expValue {
t.Fatalf("Expected %s, was %s\n", c.expValue, v)
}
@@ -124,3 +107,86 @@ func TestContainerPsContext(t *testing.T) {
}
}
func TestImagesContext(t *testing.T) {
imageID := stringid.GenerateRandomID()
unix := time.Now().Unix()
var ctx imageContext
cases := []struct {
imageCtx imageContext
expValue string
expHeader string
call func() string
}{
{imageContext{
i: types.Image{ID: imageID},
trunc: true,
}, stringid.TruncateID(imageID), imageIDHeader, ctx.ID},
{imageContext{
i: types.Image{ID: imageID},
trunc: false,
}, imageID, imageIDHeader, ctx.ID},
{imageContext{
i: types.Image{Size: 10},
trunc: true,
}, "10 B", sizeHeader, ctx.Size},
{imageContext{
i: types.Image{Created: unix},
trunc: true,
}, time.Unix(unix, 0).String(), createdAtHeader, ctx.CreatedAt},
// FIXME
// {imageContext{
// i: types.Image{Created: unix},
// trunc: true,
// }, units.HumanDuration(time.Unix(unix, 0)), createdSinceHeader, ctx.CreatedSince},
{imageContext{
i: types.Image{},
repo: "busybox",
}, "busybox", repositoryHeader, ctx.Repository},
{imageContext{
i: types.Image{},
tag: "latest",
}, "latest", tagHeader, ctx.Tag},
{imageContext{
i: types.Image{},
digest: "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a",
}, "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a", digestHeader, ctx.Digest},
}
for _, c := range cases {
ctx = c.imageCtx
v := c.call()
if strings.Contains(v, ",") {
compareMultipleValues(t, v, c.expValue)
} else if v != c.expValue {
t.Fatalf("Expected %s, was %s\n", c.expValue, v)
}
h := ctx.fullHeader()
if h != c.expHeader {
t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
}
}
}
func compareMultipleValues(t *testing.T, value, expected string) {
// comma-separated values means probably a map input, which won't
// be guaranteed to have the same order as our expected value
// We'll create maps and use reflect.DeepEquals to check instead:
entriesMap := make(map[string]string)
expMap := make(map[string]string)
entries := strings.Split(value, ",")
expectedEntries := strings.Split(expected, ",")
for _, entry := range entries {
keyval := strings.Split(entry, "=")
entriesMap[keyval[0]] = keyval[1]
}
for _, expected := range expectedEntries {
keyval := strings.Split(expected, "=")
expMap[keyval[0]] = keyval[1]
}
if !reflect.DeepEqual(expMap, entriesMap) {
t.Fatalf("Expected entries: %v, got: %v", expected, value)
}
}

View File

@@ -0,0 +1,254 @@
package formatter
import (
"bytes"
"fmt"
"io"
"strings"
"text/tabwriter"
"text/template"
"github.com/docker/docker/api/types"
"github.com/docker/docker/reference"
)
const (
tableFormatKey = "table"
rawFormatKey = "raw"
defaultContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.RunningFor}} ago\t{{.Status}}\t{{.Ports}}\t{{.Names}}"
defaultImageTableFormat = "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedSince}} ago\t{{.Size}}"
defaultImageTableFormatWithDigest = "table {{.Repository}}\t{{.Tag}}\t{{.Digest}}\t{{.ID}}\t{{.CreatedSince}} ago\t{{.Size}}"
defaultQuietFormat = "{{.ID}}"
)
// 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
// 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
if strings.HasPrefix(c.Format, tableKey) {
c.table = true
c.finalFormat = c.finalFormat[len(tableKey):]
}
c.finalFormat = strings.Trim(c.finalFormat, " ")
r := strings.NewReplacer(`\t`, "\t", `\n`, "\n")
c.finalFormat = r.Replace(c.finalFormat)
}
func (c *Context) parseFormat() (*template.Template, error) {
tmpl, err := template.New("").Parse(c.finalFormat)
if err != nil {
c.buffer.WriteString(fmt.Sprintf("Template parsing error: %v\n", err))
c.buffer.WriteTo(c.Output)
}
return tmpl, err
}
func (c *Context) postformat(tmpl *template.Template, subContext subContext) {
if c.table {
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()
}
t := tabwriter.NewWriter(c.Output, 20, 1, 3, ' ', 0)
t.Write([]byte(c.header))
t.Write([]byte("\n"))
c.buffer.WriteTo(t)
t.Flush()
} else {
c.buffer.WriteTo(c.Output)
}
}
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
}
if c.table && len(c.header) == 0 {
c.header = subContext.fullHeader()
}
c.buffer.WriteString("\n")
return nil
}
// 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
}
// ImageContext contains image specific information required by the formater, encapsulate a Context struct.
type ImageContext struct {
Context
Digest bool
// Images
Images []types.Image
}
func (ctx ContainerContext) Write() {
switch ctx.Format {
case tableFormatKey:
ctx.Format = defaultContainerTableFormat
if ctx.Quiet {
ctx.Format = defaultQuietFormat
}
case rawFormatKey:
if ctx.Quiet {
ctx.Format = `container_id: {{.ID}}`
} else {
ctx.Format = `container_id: {{.ID}}
image: {{.Image}}
command: {{.Command}}
created_at: {{.CreatedAt}}
status: {{.Status}}
names: {{.Names}}
labels: {{.Labels}}
ports: {{.Ports}}
`
if ctx.Size {
ctx.Format += `size: {{.Size}}
`
}
}
}
ctx.buffer = bytes.NewBufferString("")
ctx.preformat()
if ctx.table && ctx.Size {
ctx.finalFormat += "\t{{.Size}}"
}
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{})
}
func (ctx ImageContext) Write() {
switch ctx.Format {
case tableFormatKey:
ctx.Format = defaultImageTableFormat
if ctx.Digest {
ctx.Format = defaultImageTableFormatWithDigest
}
if ctx.Quiet {
ctx.Format = defaultQuietFormat
}
case rawFormatKey:
if ctx.Quiet {
ctx.Format = `image_id: {{.ID}}`
} else {
if ctx.Digest {
ctx.Format = `repository: {{ .Repository }}
tag: {{.Tag}}
digest: {{.Digest}}
image_id: {{.ID}}
created_at: {{.CreatedAt}}
virtual_size: {{.Size}}
`
} else {
ctx.Format = `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}}"
}
tmpl, err := ctx.parseFormat()
if err != nil {
return
}
for _, image := range ctx.Images {
repoTags := image.RepoTags
repoDigests := image.RepoDigests
if len(repoTags) == 1 && repoTags[0] == "<none>:<none>" && len(repoDigests) == 1 && repoDigests[0] == "<none>@<none>" {
// dangling image - clear out either repoTags or repoDigests so we only show it once below
repoDigests = []string{}
}
// combine the tags and digests lists
tagsAndDigests := append(repoTags, repoDigests...)
for _, repoAndRef := range tagsAndDigests {
repo := "<none>"
tag := "<none>"
digest := "<none>"
if !strings.HasPrefix(repoAndRef, "<none>") {
ref, err := reference.ParseNamed(repoAndRef)
if err != nil {
continue
}
repo = ref.Name()
switch x := ref.(type) {
case reference.Canonical:
digest = x.Digest().String()
case reference.NamedTagged:
tag = x.Tag()
}
}
imageCtx := &imageContext{
trunc: ctx.Trunc,
i: image,
repo: repo,
tag: tag,
digest: digest,
}
err = ctx.contextFormat(tmpl, imageCtx)
if err != nil {
return
}
}
}
ctx.postformat(tmpl, &imageContext{})
}

View File

@@ -0,0 +1,535 @@
package formatter
import (
"bytes"
"fmt"
"testing"
"time"
"github.com/docker/docker/api/types"
)
func TestContainerContextWrite(t *testing.T) {
unixTime := time.Now().AddDate(0, 0, -1).Unix()
expectedTime := time.Unix(unixTime, 0).String()
contexts := []struct {
context ContainerContext
expected string
}{
// Errors
{
ContainerContext{
Context: Context{
Format: "{{InvalidFunction}}",
},
},
`Template parsing error: template: :1: function "InvalidFunction" not defined
`,
},
{
ContainerContext{
Context: Context{
Format: "{{nil}}",
},
},
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
`,
},
// Table Format
{
ContainerContext{
Context: Context{
Format: "table",
},
},
`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}}",
},
},
"IMAGE\nubuntu\nubuntu\n",
},
{
ContainerContext{
Context: Context{
Format: "table {{.Image}}",
},
Size: true,
},
"IMAGE SIZE\nubuntu 0 B\nubuntu 0 B\n",
},
{
ContainerContext{
Context: Context{
Format: "table {{.Image}}",
Quiet: true,
},
},
"IMAGE\nubuntu\nubuntu\n",
},
{
ContainerContext{
Context: Context{
Format: "table",
Quiet: true,
},
},
"containerID1\ncontainerID2\n",
},
// Raw Format
{
ContainerContext{
Context: Context{
Format: "raw",
},
},
fmt.Sprintf(`container_id: containerID1
image: ubuntu
command: ""
created_at: %s
status:
names: foobar_baz
labels:
ports:
container_id: containerID2
image: ubuntu
command: ""
created_at: %s
status:
names: foobar_bar
labels:
ports:
`, expectedTime, expectedTime),
},
{
ContainerContext{
Context: Context{
Format: "raw",
},
Size: true,
},
fmt.Sprintf(`container_id: containerID1
image: ubuntu
command: ""
created_at: %s
status:
names: foobar_baz
labels:
ports:
size: 0 B
container_id: containerID2
image: ubuntu
command: ""
created_at: %s
status:
names: foobar_bar
labels:
ports:
size: 0 B
`, expectedTime, expectedTime),
},
{
ContainerContext{
Context: Context{
Format: "raw",
Quiet: true,
},
},
"container_id: containerID1\ncontainer_id: containerID2\n",
},
// Custom Format
{
ContainerContext{
Context: Context{
Format: "{{.Image}}",
},
},
"ubuntu\nubuntu\n",
},
{
ContainerContext{
Context: Context{
Format: "{{.Image}}",
},
Size: true,
},
"ubuntu\nubuntu\n",
},
}
for _, context := range contexts {
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()
if actual != context.expected {
t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
}
// Clean buffer
out.Reset()
}
}
func TestContainerContextWriteWithNoContainers(t *testing.T) {
out := bytes.NewBufferString("")
containers := []types.Container{}
contexts := []struct {
context ContainerContext
expected string
}{
{
ContainerContext{
Context: Context{
Format: "{{.Image}}",
Output: out,
},
},
"",
},
{
ContainerContext{
Context: Context{
Format: "table {{.Image}}",
Output: out,
},
},
"IMAGE\n",
},
{
ContainerContext{
Context: Context{
Format: "{{.Image}}",
Output: out,
},
Size: true,
},
"",
},
{
ContainerContext{
Context: Context{
Format: "table {{.Image}}",
Output: out,
},
Size: true,
},
"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)
}
// Clean buffer
out.Reset()
}
}
func TestImageContextWrite(t *testing.T) {
unixTime := time.Now().AddDate(0, 0, -1).Unix()
expectedTime := time.Unix(unixTime, 0).String()
contexts := []struct {
context ImageContext
expected string
}{
// Errors
{
ImageContext{
Context: Context{
Format: "{{InvalidFunction}}",
},
},
`Template parsing error: template: :1: function "InvalidFunction" not defined
`,
},
{
ImageContext{
Context: Context{
Format: "{{nil}}",
},
},
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
`,
},
// Table Format
{
ImageContext{
Context: Context{
Format: "table",
},
},
`REPOSITORY TAG IMAGE ID CREATED SIZE
image tag1 imageID1 24 hours ago 0 B
image <none> imageID1 24 hours ago 0 B
image tag2 imageID2 24 hours ago 0 B
<none> <none> imageID3 24 hours ago 0 B
`,
},
{
ImageContext{
Context: Context{
Format: "table {{.Repository}}",
},
},
"REPOSITORY\nimage\nimage\nimage\n<none>\n",
},
{
ImageContext{
Context: Context{
Format: "table {{.Repository}}",
},
Digest: true,
},
`REPOSITORY DIGEST
image <none>
image sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf
image <none>
<none> <none>
`,
},
{
ImageContext{
Context: Context{
Format: "table {{.Repository}}",
Quiet: true,
},
},
"REPOSITORY\nimage\nimage\nimage\n<none>\n",
},
{
ImageContext{
Context: Context{
Format: "table",
Quiet: true,
},
},
"imageID1\nimageID1\nimageID2\nimageID3\n",
},
{
ImageContext{
Context: Context{
Format: "table",
Quiet: false,
},
Digest: true,
},
`REPOSITORY TAG DIGEST IMAGE ID CREATED SIZE
image tag1 <none> imageID1 24 hours ago 0 B
image <none> sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf imageID1 24 hours ago 0 B
image tag2 <none> imageID2 24 hours ago 0 B
<none> <none> <none> imageID3 24 hours ago 0 B
`,
},
{
ImageContext{
Context: Context{
Format: "table",
Quiet: true,
},
Digest: true,
},
"imageID1\nimageID1\nimageID2\nimageID3\n",
},
// Raw Format
{
ImageContext{
Context: Context{
Format: "raw",
},
},
fmt.Sprintf(`repository: image
tag: tag1
image_id: imageID1
created_at: %s
virtual_size: 0 B
repository: image
tag: <none>
image_id: imageID1
created_at: %s
virtual_size: 0 B
repository: image
tag: tag2
image_id: imageID2
created_at: %s
virtual_size: 0 B
repository: <none>
tag: <none>
image_id: imageID3
created_at: %s
virtual_size: 0 B
`, expectedTime, expectedTime, expectedTime, expectedTime),
},
{
ImageContext{
Context: Context{
Format: "raw",
},
Digest: true,
},
fmt.Sprintf(`repository: image
tag: tag1
digest: <none>
image_id: imageID1
created_at: %s
virtual_size: 0 B
repository: image
tag: <none>
digest: sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf
image_id: imageID1
created_at: %s
virtual_size: 0 B
repository: image
tag: tag2
digest: <none>
image_id: imageID2
created_at: %s
virtual_size: 0 B
repository: <none>
tag: <none>
digest: <none>
image_id: imageID3
created_at: %s
virtual_size: 0 B
`, expectedTime, expectedTime, expectedTime, expectedTime),
},
{
ImageContext{
Context: Context{
Format: "raw",
Quiet: true,
},
},
`image_id: imageID1
image_id: imageID1
image_id: imageID2
image_id: imageID3
`,
},
// Custom Format
{
ImageContext{
Context: Context{
Format: "{{.Repository}}",
},
},
"image\nimage\nimage\n<none>\n",
},
{
ImageContext{
Context: Context{
Format: "{{.Repository}}",
},
Digest: true,
},
"image\nimage\nimage\n<none>\n",
},
}
for _, context := range contexts {
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)
}
// Clean buffer
out.Reset()
}
}
func TestImageContextWriteWithNoImage(t *testing.T) {
out := bytes.NewBufferString("")
images := []types.Image{}
contexts := []struct {
context ImageContext
expected string
}{
{
ImageContext{
Context: Context{
Format: "{{.Repository}}",
Output: out,
},
},
"",
},
{
ImageContext{
Context: Context{
Format: "table {{.Repository}}",
Output: out,
},
},
"REPOSITORY\n",
},
{
ImageContext{
Context: Context{
Format: "{{.Repository}}",
Output: out,
},
Digest: true,
},
"",
},
{
ImageContext{
Context: Context{
Format: "table {{.Repository}}",
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)
}
// Clean buffer
out.Reset()
}
}

View File

@@ -1,19 +1,12 @@
package client
import (
"fmt"
"strings"
"text/tabwriter"
"time"
"github.com/docker/docker/api/client/formatter"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
Cli "github.com/docker/docker/cli"
"github.com/docker/docker/opts"
flag "github.com/docker/docker/pkg/mflag"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/docker/reference"
"github.com/docker/go-units"
)
// CmdImages lists the images in a specified repository, or all top-level images if no repository is specified.
@@ -25,6 +18,7 @@ func (cli *DockerCli) CmdImages(args ...string) error {
all := cmd.Bool([]string{"a", "-all"}, false, "Show all images (default hides intermediate images)")
noTrunc := cmd.Bool([]string{"-no-trunc"}, false, "Don't truncate output")
showDigests := cmd.Bool([]string{"-digests"}, false, "Show digests")
format := cmd.String([]string{"-format"}, "", "Pretty-print images using a Go template")
flFilter := opts.NewListOpts(nil)
cmd.Var(&flFilter, []string{"f", "-filter"}, "Filter output based on conditions provided")
@@ -59,66 +53,27 @@ func (cli *DockerCli) CmdImages(args ...string) error {
return err
}
w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0)
if !*quiet {
if *showDigests {
fmt.Fprintln(w, "REPOSITORY\tTAG\tDIGEST\tIMAGE ID\tCREATED\tSIZE")
f := *format
if len(f) == 0 {
if len(cli.ImagesFormat()) > 0 && !*quiet {
f = cli.ImagesFormat()
} else {
fmt.Fprintln(w, "REPOSITORY\tTAG\tIMAGE ID\tCREATED\tSIZE")
f = "table"
}
}
for _, image := range images {
ID := image.ID
if !*noTrunc {
ID = stringid.TruncateID(ID)
}
repoTags := image.RepoTags
repoDigests := image.RepoDigests
if len(repoTags) == 1 && repoTags[0] == "<none>:<none>" && len(repoDigests) == 1 && repoDigests[0] == "<none>@<none>" {
// dangling image - clear out either repoTags or repoDigsts so we only show it once below
repoDigests = []string{}
}
// combine the tags and digests lists
tagsAndDigests := append(repoTags, repoDigests...)
for _, repoAndRef := range tagsAndDigests {
// default repo, tag, and digest to none - if there's a value, it'll be set below
repo := "<none>"
tag := "<none>"
digest := "<none>"
if !strings.HasPrefix(repoAndRef, "<none>") {
ref, err := reference.ParseNamed(repoAndRef)
if err != nil {
return err
}
repo = ref.Name()
switch x := ref.(type) {
case reference.Canonical:
digest = x.Digest().String()
case reference.NamedTagged:
tag = x.Tag()
}
}
if !*quiet {
if *showDigests {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s ago\t%s\n", repo, tag, digest, ID, units.HumanDuration(time.Now().UTC().Sub(time.Unix(int64(image.Created), 0))), units.HumanSize(float64(image.Size)))
} else {
fmt.Fprintf(w, "%s\t%s\t%s\t%s ago\t%s\n", repo, tag, ID, units.HumanDuration(time.Now().UTC().Sub(time.Unix(int64(image.Created), 0))), units.HumanSize(float64(image.Size)))
}
} else {
fmt.Fprintln(w, ID)
}
}
imagesCtx := formatter.ImageContext{
Context: formatter.Context{
Output: cli.out,
Format: f,
Quiet: *quiet,
Trunc: !*noTrunc,
},
Digest: *showDigests,
Images: images,
}
if !*quiet {
w.Flush()
}
imagesCtx.Write()
return nil
}

View File

@@ -1,7 +1,7 @@
package client
import (
"github.com/docker/docker/api/client/ps"
"github.com/docker/docker/api/client/formatter"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
Cli "github.com/docker/docker/cli"
@@ -70,15 +70,18 @@ func (cli *DockerCli) CmdPs(args ...string) error {
}
}
psCtx := ps.Context{
Output: cli.out,
Format: f,
Quiet: *quiet,
Size: *size,
Trunc: !*noTrunc,
psCtx := formatter.ContainerContext{
Context: formatter.Context{
Output: cli.out,
Format: f,
Quiet: *quiet,
Trunc: !*noTrunc,
},
Size: *size,
Containers: containers,
}
ps.Format(psCtx, containers)
psCtx.Write()
return nil
}

View File

@@ -1,140 +0,0 @@
package ps
import (
"bytes"
"fmt"
"io"
"strings"
"text/tabwriter"
"text/template"
"github.com/docker/docker/api/types"
)
const (
tableFormatKey = "table"
rawFormatKey = "raw"
defaultTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.RunningFor}} ago\t{{.Status}}\t{{.Ports}}\t{{.Names}}"
defaultQuietFormat = "{{.ID}}"
)
// 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
// Size when set to true will display the size of the output.
Size bool
// Quiet when set to true will simply print minimal information.
Quiet bool
// Trunc when set to true will truncate the output of certain fields such as Container ID.
Trunc bool
}
// Format helps to format the output using the parameters set in the Context.
// Currently Format allow to display in raw, table or custom format the output.
func Format(ctx Context, containers []types.Container) {
switch ctx.Format {
case tableFormatKey:
tableFormat(ctx, containers)
case rawFormatKey:
rawFormat(ctx, containers)
default:
customFormat(ctx, containers)
}
}
func rawFormat(ctx Context, containers []types.Container) {
if ctx.Quiet {
ctx.Format = `container_id: {{.ID}}`
} else {
ctx.Format = `container_id: {{.ID}}
image: {{.Image}}
command: {{.Command}}
created_at: {{.CreatedAt}}
status: {{.Status}}
names: {{.Names}}
labels: {{.Labels}}
ports: {{.Ports}}
`
if ctx.Size {
ctx.Format += `size: {{.Size}}
`
}
}
customFormat(ctx, containers)
}
func tableFormat(ctx Context, containers []types.Container) {
ctx.Format = defaultTableFormat
if ctx.Quiet {
ctx.Format = defaultQuietFormat
}
customFormat(ctx, containers)
}
func customFormat(ctx Context, containers []types.Container) {
var (
table bool
header string
format = ctx.Format
buffer = bytes.NewBufferString("")
)
if strings.HasPrefix(ctx.Format, tableKey) {
table = true
format = format[len(tableKey):]
}
format = strings.Trim(format, " ")
r := strings.NewReplacer(`\t`, "\t", `\n`, "\n")
format = r.Replace(format)
if table && ctx.Size {
format += "\t{{.Size}}"
}
tmpl, err := template.New("").Parse(format)
if err != nil {
buffer.WriteString(fmt.Sprintf("Template parsing error: %v\n", err))
buffer.WriteTo(ctx.Output)
return
}
for _, container := range containers {
containerCtx := &containerContext{
trunc: ctx.Trunc,
c: container,
}
if err := tmpl.Execute(buffer, containerCtx); err != nil {
buffer = bytes.NewBufferString(fmt.Sprintf("Template parsing error: %v\n", err))
buffer.WriteTo(ctx.Output)
return
}
if table && len(header) == 0 {
header = containerCtx.fullHeader()
}
buffer.WriteString("\n")
}
if table {
if len(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
containerCtx := &containerContext{}
tmpl.Execute(bytes.NewBufferString(""), containerCtx)
header = containerCtx.fullHeader()
}
t := tabwriter.NewWriter(ctx.Output, 20, 1, 3, ' ', 0)
t.Write([]byte(header))
t.Write([]byte("\n"))
buffer.WriteTo(t)
t.Flush()
} else {
buffer.WriteTo(ctx.Output)
}
}

View File

@@ -1,213 +0,0 @@
package ps
import (
"bytes"
"fmt"
"testing"
"time"
"github.com/docker/docker/api/types"
)
func TestFormat(t *testing.T) {
unixTime := time.Now().Add(-50 * time.Hour).Unix()
expectedTime := time.Unix(unixTime, 0).String()
contexts := []struct {
context Context
expected string
}{
// Errors
{
Context{
Format: "{{InvalidFunction}}",
},
`Template parsing error: template: :1: function "InvalidFunction" not defined
`,
},
{
Context{
Format: "{{nil}}",
},
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
`,
},
// Table Format
{
Context{
Format: "table",
},
`CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
containerID1 ubuntu "" 2 days ago foobar_baz
containerID2 ubuntu "" 2 days ago foobar_bar
`,
},
{
Context{
Format: "table {{.Image}}",
},
"IMAGE\nubuntu\nubuntu\n",
},
{
Context{
Format: "table {{.Image}}",
Size: true,
},
"IMAGE SIZE\nubuntu 0 B\nubuntu 0 B\n",
},
{
Context{
Format: "table {{.Image}}",
Quiet: true,
},
"IMAGE\nubuntu\nubuntu\n",
},
{
Context{
Format: "table",
Quiet: true,
},
"containerID1\ncontainerID2\n",
},
// Raw Format
{
Context{
Format: "raw",
},
fmt.Sprintf(`container_id: containerID1
image: ubuntu
command: ""
created_at: %s
status:
names: foobar_baz
labels:
ports:
container_id: containerID2
image: ubuntu
command: ""
created_at: %s
status:
names: foobar_bar
labels:
ports:
`, expectedTime, expectedTime),
},
{
Context{
Format: "raw",
Size: true,
},
fmt.Sprintf(`container_id: containerID1
image: ubuntu
command: ""
created_at: %s
status:
names: foobar_baz
labels:
ports:
size: 0 B
container_id: containerID2
image: ubuntu
command: ""
created_at: %s
status:
names: foobar_bar
labels:
ports:
size: 0 B
`, expectedTime, expectedTime),
},
{
Context{
Format: "raw",
Quiet: true,
},
"container_id: containerID1\ncontainer_id: containerID2\n",
},
// Custom Format
{
Context{
Format: "{{.Image}}",
},
"ubuntu\nubuntu\n",
},
{
Context{
Format: "{{.Image}}",
Size: true,
},
"ubuntu\nubuntu\n",
},
}
for _, context := range contexts {
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
Format(context.context, containers)
actual := out.String()
if actual != context.expected {
t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
}
// Clean buffer
out.Reset()
}
}
func TestCustomFormatNoContainers(t *testing.T) {
out := bytes.NewBufferString("")
containers := []types.Container{}
contexts := []struct {
context Context
expected string
}{
{
Context{
Format: "{{.Image}}",
Output: out,
},
"",
},
{
Context{
Format: "table {{.Image}}",
Output: out,
},
"IMAGE\n",
},
{
Context{
Format: "{{.Image}}",
Output: out,
Size: true,
},
"",
},
{
Context{
Format: "table {{.Image}}",
Output: out,
Size: true,
},
"IMAGE SIZE\n",
},
}
for _, context := range contexts {
customFormat(context.context, containers)
actual := out.String()
if actual != context.expected {
t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
}
// Clean buffer
out.Reset()
}
}