mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
Add --format support to images command
- rename `api/client/ps` to `api/client/formatter` - add a a image formatter Signed-off-by: Vincent Demeester <vincent@sbr.pm>
This commit is contained in:
parent
67620bc028
commit
34a3c3cacf
17 changed files with 1126 additions and 479 deletions
|
@ -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.
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package ps
|
||||
package formatter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
@ -16,26 +16,31 @@ import (
|
|||
const (
|
||||
tableKey = "table"
|
||||
|
||||
idHeader = "CONTAINER ID"
|
||||
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 {
|
||||
baseSubContext
|
||||
trunc bool
|
||||
header []string
|
||||
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{}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
254
api/client/formatter/formatter.go
Normal file
254
api/client/formatter/formatter.go
Normal 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{})
|
||||
}
|
535
api/client/formatter/formatter_test.go
Normal file
535
api/client/formatter/formatter_test.go
Normal 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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
imagesCtx := formatter.ImageContext{
|
||||
Context: formatter.Context{
|
||||
Output: cli.out,
|
||||
Format: f,
|
||||
Quiet: *quiet,
|
||||
Trunc: !*noTrunc,
|
||||
},
|
||||
Digest: *showDigests,
|
||||
Images: images,
|
||||
}
|
||||
|
||||
repoTags := image.RepoTags
|
||||
repoDigests := image.RepoDigests
|
||||
imagesCtx.Write()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !*quiet {
|
||||
w.Flush()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -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{
|
||||
psCtx := formatter.ContainerContext{
|
||||
Context: formatter.Context{
|
||||
Output: cli.out,
|
||||
Format: f,
|
||||
Quiet: *quiet,
|
||||
Size: *size,
|
||||
Trunc: !*noTrunc,
|
||||
},
|
||||
Size: *size,
|
||||
Containers: containers,
|
||||
}
|
||||
|
||||
ps.Format(psCtx, containers)
|
||||
psCtx.Write()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -50,6 +50,7 @@ type ConfigFile struct {
|
|||
AuthConfigs map[string]types.AuthConfig `json:"auths"`
|
||||
HTTPHeaders map[string]string `json:"HttpHeaders,omitempty"`
|
||||
PsFormat string `json:"psFormat,omitempty"`
|
||||
ImagesFormat string `json:"imagesFormat,omitempty"`
|
||||
filename string // Note: not serialized - for internal use only
|
||||
}
|
||||
|
||||
|
|
|
@ -927,6 +927,9 @@ _docker_images() {
|
|||
fi
|
||||
return
|
||||
;;
|
||||
--format)
|
||||
return
|
||||
;;
|
||||
esac
|
||||
|
||||
case "${words[$cword-2]}$prev=" in
|
||||
|
@ -941,7 +944,7 @@ _docker_images() {
|
|||
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=( $( compgen -W "--all -a --digests --filter -f --help --no-trunc --quiet -q" -- "$cur" ) )
|
||||
COMPREPLY=( $( compgen -W "--all -a --digests --filter -f --format --help --no-trunc --quiet -q" -- "$cur" ) )
|
||||
;;
|
||||
=)
|
||||
return
|
||||
|
|
|
@ -692,8 +692,9 @@ __docker_subcommand() {
|
|||
_arguments $(__docker_arguments) \
|
||||
$opts_help \
|
||||
"($help -a --all)"{-a,--all}"[Show all images]" \
|
||||
"($help)--digest[Show digests]" \
|
||||
"($help)--digests[Show digests]" \
|
||||
"($help)*"{-f=,--filter=}"[Filter values]:filter: " \
|
||||
"($help)--format[Pretty-print containers using a Go template]:format: " \
|
||||
"($help)--no-trunc[Do not truncate output]" \
|
||||
"($help -q --quiet)"{-q,--quiet}"[Only show numeric IDs]" \
|
||||
"($help -): :__docker_repositories" && ret=0
|
||||
|
|
|
@ -103,6 +103,12 @@ Docker's client uses this property. If this property is not set, the client
|
|||
falls back to the default table format. For a list of supported formatting
|
||||
directives, see the [**Formatting** section in the `docker ps` documentation](ps.md)
|
||||
|
||||
The property `imagesFormat` specifies the default format for `docker images` output.
|
||||
When the `--format` flag is not provided with the `docker images` command,
|
||||
Docker's client uses this property. If this property is not set, the client
|
||||
falls back to the default table format. For a list of supported formatting
|
||||
directives, see the [**Formatting** section in the `docker images` documentation](images.md)
|
||||
|
||||
Following is a sample `config.json` file:
|
||||
|
||||
{
|
||||
|
@ -110,6 +116,7 @@ Following is a sample `config.json` file:
|
|||
"MyHeader": "MyValue"
|
||||
},
|
||||
"psFormat": "table {{.ID}}\\t{{.Image}}\\t{{.Command}}\\t{{.Labels}}"
|
||||
"imagesFormat": "table {{.ID}}\\t{{.Repository}}\\t{{.Tag}}\\t{{.CreatedAt}}"
|
||||
}
|
||||
|
||||
### Notary
|
||||
|
|
|
@ -177,3 +177,53 @@ In this example, with the `0.1` value, it returns an empty set because no matche
|
|||
|
||||
$ docker images --filter "label=com.example.version=0.1"
|
||||
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
|
||||
|
||||
## Formatting
|
||||
|
||||
The formatting option (`--format`) will pretty print container output
|
||||
using a Go template.
|
||||
|
||||
Valid placeholders for the Go template are listed below:
|
||||
|
||||
Placeholder | Description
|
||||
---- | ----
|
||||
`.ID` | Image ID
|
||||
`.Repository` | Image repository
|
||||
`.Tag` | Image tag
|
||||
`.Digest` | Image digest
|
||||
`.CreatedSince` | Elapsed time since the image was created.
|
||||
`.CreatedAt` | Time when the image was created.
|
||||
`.Size` | Image disk size.
|
||||
|
||||
When using the `--format` option, the `image` command will either
|
||||
output the data exactly as the template declares or, when using the
|
||||
`table` directive, will include column headers as well.
|
||||
|
||||
The following example uses a template without headers and outputs the
|
||||
`ID` and `Repository` entries separated by a colon for all images:
|
||||
|
||||
$ docker images --format "{{.ID}}: {{.Repository}}"
|
||||
77af4d6b9913: <none>
|
||||
b6fa739cedf5: committ
|
||||
78a85c484f71: <none>
|
||||
30557a29d5ab: docker
|
||||
5ed6274db6ce: <none>
|
||||
746b819f315e: postgres
|
||||
746b819f315e: postgres
|
||||
746b819f315e: postgres
|
||||
746b819f315e: postgres
|
||||
|
||||
To list all images with their repository and tag in a table format you
|
||||
can use:
|
||||
|
||||
$ docker images --format "table {{.ID}}\t{{.Repository}}\t{{.Tag}}"
|
||||
IMAGE ID REPOSITORY TAG
|
||||
77af4d6b9913 <none> <none>
|
||||
b6fa739cedf5 committ latest
|
||||
78a85c484f71 <none> <none>
|
||||
30557a29d5ab docker latest
|
||||
5ed6274db6ce <none> <none>
|
||||
746b819f315e postgres 9
|
||||
746b819f315e postgres 9.3
|
||||
746b819f315e postgres 9.3.5
|
||||
746b819f315e postgres latest
|
||||
|
|
|
@ -2,6 +2,9 @@ package main
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
|
@ -233,3 +236,46 @@ func (s *DockerSuite) TestImagesFilterNameWithPort(c *check.C) {
|
|||
out, _ = dockerCmd(c, "images", tag+":no-such-tag")
|
||||
c.Assert(out, checker.Not(checker.Contains), tag)
|
||||
}
|
||||
|
||||
func (s *DockerSuite) TestImagesFormat(c *check.C) {
|
||||
// testRequires(c, DaemonIsLinux)
|
||||
tag := "myimage"
|
||||
dockerCmd(c, "tag", "busybox", tag+":v1")
|
||||
dockerCmd(c, "tag", "busybox", tag+":v2")
|
||||
|
||||
out, _ := dockerCmd(c, "images", "--format", "{{.Repository}}", tag)
|
||||
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
|
||||
|
||||
expected := []string{"myimage", "myimage"}
|
||||
var names []string
|
||||
for _, l := range lines {
|
||||
names = append(names, l)
|
||||
}
|
||||
c.Assert(expected, checker.DeepEquals, names, check.Commentf("Expected array with truncated names: %v, got: %v", expected, names))
|
||||
}
|
||||
|
||||
// ImagesDefaultFormatAndQuiet
|
||||
func (s *DockerSuite) TestImagesFormatDefaultFormat(c *check.C) {
|
||||
testRequires(c, DaemonIsLinux)
|
||||
|
||||
// create container 1
|
||||
out, _ := dockerCmd(c, "run", "-d", "busybox", "true")
|
||||
containerID1 := strings.TrimSpace(out)
|
||||
|
||||
// tag as foobox
|
||||
out, _ = dockerCmd(c, "commit", containerID1, "myimage")
|
||||
imageID := stringid.TruncateID(strings.TrimSpace(out))
|
||||
|
||||
config := `{
|
||||
"imagesFormat": "{{ .ID }} default"
|
||||
}`
|
||||
d, err := ioutil.TempDir("", "integration-cli-")
|
||||
c.Assert(err, checker.IsNil)
|
||||
defer os.RemoveAll(d)
|
||||
|
||||
err = ioutil.WriteFile(filepath.Join(d, "config.json"), []byte(config), 0644)
|
||||
c.Assert(err, checker.IsNil)
|
||||
|
||||
out, _ = dockerCmd(c, "--config", d, "images", "-q", "myimage")
|
||||
c.Assert(out, checker.Equals, imageID+"\n", check.Commentf("Expected to print only the image id, got %v\n", out))
|
||||
}
|
||||
|
|
|
@ -568,7 +568,7 @@ func (s *DockerSuite) TestPsFormatHeaders(c *check.C) {
|
|||
func (s *DockerSuite) TestPsDefaultFormatAndQuiet(c *check.C) {
|
||||
testRequires(c, DaemonIsLinux)
|
||||
config := `{
|
||||
"psFormat": "{{ .ID }} default"
|
||||
"psFormat": "default {{ .ID }}"
|
||||
}`
|
||||
d, err := ioutil.TempDir("", "integration-cli-")
|
||||
c.Assert(err, checker.IsNil)
|
||||
|
|
|
@ -40,6 +40,17 @@ versions.
|
|||
**-f**, **--filter**=[]
|
||||
Filters the output. The dangling=true filter finds unused images. While label=com.foo=amd64 filters for images with a com.foo value of amd64. The label=com.foo filter finds images with the label com.foo of any value.
|
||||
|
||||
**--format**="*TEMPLATE*"
|
||||
Pretty-print containers using a Go template.
|
||||
Valid placeholders:
|
||||
.ID - Image ID
|
||||
.Repository - Image repository
|
||||
.Tag - Image tag
|
||||
.Digest - Image digest
|
||||
.CreatedSince - Elapsed time since the image was created.
|
||||
.CreatedAt - Time when the image was created..
|
||||
.Size - Image disk size.
|
||||
|
||||
**--help**
|
||||
Print usage statement
|
||||
|
||||
|
|
Loading…
Reference in a new issue