Pārlūkot izejas kodu

Refactor api/client/formatter files

- Create container.go and image.go for specific context code
- Keep common code in formatter.go and custom.go

Signed-off-by: Vincent Demeester <vincent@sbr.pm>
Vincent Demeester 9 gadi atpakaļ
vecāks
revīzija
e4fdbbc00b

+ 208 - 0
api/client/formatter/container.go

@@ -0,0 +1,208 @@
+package formatter
+
+import (
+	"bytes"
+	"fmt"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/docker/docker/api"
+	"github.com/docker/docker/pkg/stringid"
+	"github.com/docker/docker/pkg/stringutils"
+	"github.com/docker/engine-api/types"
+	"github.com/docker/go-units"
+)
+
+const (
+	defaultContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.RunningFor}} ago\t{{.Status}}\t{{.Ports}}\t{{.Names}}"
+
+	containerIDHeader = "CONTAINER ID"
+	namesHeader       = "NAMES"
+	commandHeader     = "COMMAND"
+	runningForHeader  = "CREATED"
+	statusHeader      = "STATUS"
+	portsHeader       = "PORTS"
+	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
+}
+
+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`
+			}
+		}
+	}
+
+	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{})
+}
+
+type containerContext struct {
+	baseSubContext
+	trunc bool
+	c     types.Container
+}
+
+func (c *containerContext) ID() string {
+	c.addHeader(containerIDHeader)
+	if c.trunc {
+		return stringid.TruncateID(c.c.ID)
+	}
+	return c.c.ID
+}
+
+func (c *containerContext) Names() string {
+	c.addHeader(namesHeader)
+	names := stripNamePrefix(c.c.Names)
+	if c.trunc {
+		for _, name := range names {
+			if len(strings.Split(name, "/")) == 1 {
+				names = []string{name}
+				break
+			}
+		}
+	}
+	return strings.Join(names, ",")
+}
+
+func (c *containerContext) Image() string {
+	c.addHeader(imageHeader)
+	if c.c.Image == "" {
+		return "<no image>"
+	}
+	if c.trunc {
+		if trunc := stringid.TruncateID(c.c.ImageID); trunc == stringid.TruncateID(c.c.Image) {
+			return trunc
+		}
+	}
+	return c.c.Image
+}
+
+func (c *containerContext) Command() string {
+	c.addHeader(commandHeader)
+	command := c.c.Command
+	if c.trunc {
+		command = stringutils.Truncate(command, 20)
+	}
+	return strconv.Quote(command)
+}
+
+func (c *containerContext) CreatedAt() string {
+	c.addHeader(createdAtHeader)
+	return time.Unix(int64(c.c.Created), 0).String()
+}
+
+func (c *containerContext) RunningFor() string {
+	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)
+	return api.DisplayablePorts(c.c.Ports)
+}
+
+func (c *containerContext) Status() string {
+	c.addHeader(statusHeader)
+	return c.c.Status
+}
+
+func (c *containerContext) Size() string {
+	c.addHeader(sizeHeader)
+	srw := units.HumanSize(float64(c.c.SizeRw))
+	sv := units.HumanSize(float64(c.c.SizeRootFs))
+
+	sf := srw
+	if c.c.SizeRootFs > 0 {
+		sf = fmt.Sprintf("%s (virtual %s)", srw, sv)
+	}
+	return sf
+}
+
+func (c *containerContext) Labels() string {
+	c.addHeader(labelsHeader)
+	if c.c.Labels == nil {
+		return ""
+	}
+
+	var joinLabels []string
+	for k, v := range c.c.Labels {
+		joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v))
+	}
+	return strings.Join(joinLabels, ",")
+}
+
+func (c *containerContext) Label(name string) string {
+	n := strings.Split(name, ".")
+	r := strings.NewReplacer("-", " ", "_", " ")
+	h := r.Replace(n[len(n)-1])
+
+	c.addHeader(h)
+
+	if c.c.Labels == nil {
+		return ""
+	}
+	return c.c.Labels[name]
+}
+
+func (c *containerContext) Mounts() string {
+	c.addHeader(mountsHeader)
+
+	var name string
+	var mounts []string
+	for _, m := range c.c.Mounts {
+		if m.Name == "" {
+			name = m.Source
+		} else {
+			name = m.Name
+		}
+		if c.trunc {
+			name = stringutils.Truncate(name, 15)
+		}
+		mounts = append(mounts, name)
+	}
+	return strings.Join(mounts, ",")
+}

+ 404 - 0
api/client/formatter/container_test.go

@@ -0,0 +1,404 @@
+package formatter
+
+import (
+	"bytes"
+	"fmt"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/docker/docker/pkg/stringid"
+	"github.com/docker/engine-api/types"
+)
+
+func TestContainerPsContext(t *testing.T) {
+	containerID := stringid.GenerateRandomID()
+	unix := time.Now().Add(-65 * time.Second).Unix()
+
+	var ctx containerContext
+	cases := []struct {
+		container types.Container
+		trunc     bool
+		expValue  string
+		expHeader string
+		call      func() string
+	}{
+		{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},
+		{types.Container{Image: "verylongimagename"}, false, "verylongimagename", imageHeader, ctx.Image},
+		{types.Container{
+			Image:   "a5a665ff33eced1e0803148700880edab4",
+			ImageID: "a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5",
+		},
+			true,
+			"a5a665ff33ec",
+			imageHeader,
+			ctx.Image,
+		},
+		{types.Container{
+			Image:   "a5a665ff33eced1e0803148700880edab4",
+			ImageID: "a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5",
+		},
+			false,
+			"a5a665ff33eced1e0803148700880edab4",
+			imageHeader,
+			ctx.Image,
+		},
+		{types.Container{Image: ""}, true, "<no image>", imageHeader, ctx.Image},
+		{types.Container{Command: "sh -c 'ls -la'"}, true, `"sh -c 'ls -la'"`, commandHeader, ctx.Command},
+		{types.Container{Created: unix}, true, time.Unix(unix, 0).String(), createdAtHeader, ctx.CreatedAt},
+		{types.Container{Ports: []types.Port{{PrivatePort: 8080, PublicPort: 8080, Type: "tcp"}}}, true, "8080/tcp", portsHeader, ctx.Ports},
+		{types.Container{Status: "RUNNING"}, true, "RUNNING", statusHeader, ctx.Status},
+		{types.Container{SizeRw: 10}, true, "10 B", sizeHeader, ctx.Size},
+		{types.Container{SizeRw: 10, SizeRootFs: 20}, true, "10 B (virtual 20 B)", sizeHeader, ctx.Size},
+		{types.Container{}, true, "", labelsHeader, ctx.Labels},
+		{types.Container{Labels: map[string]string{"cpu": "6", "storage": "ssd"}}, true, "cpu=6,storage=ssd", labelsHeader, ctx.Labels},
+		{types.Container{Created: unix}, true, "About a minute", runningForHeader, ctx.RunningFor},
+		{types.Container{
+			Mounts: []types.MountPoint{
+				{
+					Name:   "733908409c91817de8e92b0096373245f329f19a88e2c849f02460e9b3d1c203",
+					Driver: "local",
+					Source: "/a/path",
+				},
+			},
+		}, true, "733908409c91817", mountsHeader, ctx.Mounts},
+		{types.Container{
+			Mounts: []types.MountPoint{
+				{
+					Driver: "local",
+					Source: "/a/path",
+				},
+			},
+		}, false, "/a/path", mountsHeader, ctx.Mounts},
+		{types.Container{
+			Mounts: []types.MountPoint{
+				{
+					Name:   "733908409c91817de8e92b0096373245f329f19a88e2c849f02460e9b3d1c203",
+					Driver: "local",
+					Source: "/a/path",
+				},
+			},
+		}, false, "733908409c91817de8e92b0096373245f329f19a88e2c849f02460e9b3d1c203", mountsHeader, ctx.Mounts},
+	}
+
+	for _, c := range cases {
+		ctx = containerContext{c: c.container, trunc: c.trunc}
+		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)
+		}
+	}
+
+	c1 := types.Container{Labels: map[string]string{"com.docker.swarm.swarm-id": "33", "com.docker.swarm.node_name": "ubuntu"}}
+	ctx = containerContext{c: c1, trunc: true}
+
+	sid := ctx.Label("com.docker.swarm.swarm-id")
+	node := ctx.Label("com.docker.swarm.node_name")
+	if sid != "33" {
+		t.Fatalf("Expected 33, was %s\n", sid)
+	}
+
+	if node != "ubuntu" {
+		t.Fatalf("Expected ubuntu, was %s\n", node)
+	}
+
+	h := ctx.fullHeader()
+	if h != "SWARM ID\tNODE NAME" {
+		t.Fatalf("Expected %s, was %s\n", "SWARM ID\tNODE NAME", h)
+
+	}
+
+	c2 := types.Container{}
+	ctx = containerContext{c: c2, trunc: true}
+
+	label := ctx.Label("anything.really")
+	if label != "" {
+		t.Fatalf("Expected an empty string, was %s", label)
+	}
+
+	ctx = containerContext{c: c2, trunc: true}
+	fullHeader := ctx.fullHeader()
+	if fullHeader != "" {
+		t.Fatalf("Expected fullHeader to be empty, was %s", fullHeader)
+	}
+
+}
+
+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",
+				},
+				Size: 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",
+				},
+			},
+			`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\nubuntu\nubuntu\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\n",
+		},
+		{
+			ContainerContext{
+				Context: Context{
+					Format: "table {{.Image}}\t{{.Size}}",
+					Output: out,
+				},
+			},
+			"IMAGE               SIZE\n",
+		},
+		{
+			ContainerContext{
+				Context: Context{
+					Format: "table {{.Image}}\t{{.Size}}",
+					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()
+	}
+}

+ 2 - 196
api/client/formatter/custom.go

@@ -1,215 +1,21 @@
 package formatter
 
 import (
-	"fmt"
-	"strconv"
 	"strings"
-	"time"
-
-	"github.com/docker/docker/api"
-	"github.com/docker/docker/pkg/stringid"
-	"github.com/docker/docker/pkg/stringutils"
-	"github.com/docker/engine-api/types"
-	"github.com/docker/go-units"
 )
 
 const (
 	tableKey = "table"
 
-	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"
-	mountsHeader       = "MOUNTS"
+	nameHeader         = "NAME"
+	driverHeader       = "DRIVER"
 )
 
-type containerContext struct {
-	baseSubContext
-	trunc bool
-	c     types.Container
-}
-
-func (c *containerContext) ID() string {
-	c.addHeader(containerIDHeader)
-	if c.trunc {
-		return stringid.TruncateID(c.c.ID)
-	}
-	return c.c.ID
-}
-
-func (c *containerContext) Names() string {
-	c.addHeader(namesHeader)
-	names := stripNamePrefix(c.c.Names)
-	if c.trunc {
-		for _, name := range names {
-			if len(strings.Split(name, "/")) == 1 {
-				names = []string{name}
-				break
-			}
-		}
-	}
-	return strings.Join(names, ",")
-}
-
-func (c *containerContext) Image() string {
-	c.addHeader(imageHeader)
-	if c.c.Image == "" {
-		return "<no image>"
-	}
-	if c.trunc {
-		if trunc := stringid.TruncateID(c.c.ImageID); trunc == stringid.TruncateID(c.c.Image) {
-			return trunc
-		}
-	}
-	return c.c.Image
-}
-
-func (c *containerContext) Command() string {
-	c.addHeader(commandHeader)
-	command := c.c.Command
-	if c.trunc {
-		command = stringutils.Truncate(command, 20)
-	}
-	return strconv.Quote(command)
-}
-
-func (c *containerContext) CreatedAt() string {
-	c.addHeader(createdAtHeader)
-	return time.Unix(int64(c.c.Created), 0).String()
-}
-
-func (c *containerContext) RunningFor() string {
-	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)
-	return api.DisplayablePorts(c.c.Ports)
-}
-
-func (c *containerContext) Status() string {
-	c.addHeader(statusHeader)
-	return c.c.Status
-}
-
-func (c *containerContext) Size() string {
-	c.addHeader(sizeHeader)
-	srw := units.HumanSize(float64(c.c.SizeRw))
-	sv := units.HumanSize(float64(c.c.SizeRootFs))
-
-	sf := srw
-	if c.c.SizeRootFs > 0 {
-		sf = fmt.Sprintf("%s (virtual %s)", srw, sv)
-	}
-	return sf
-}
-
-func (c *containerContext) Labels() string {
-	c.addHeader(labelsHeader)
-	if c.c.Labels == nil {
-		return ""
-	}
-
-	var joinLabels []string
-	for k, v := range c.c.Labels {
-		joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v))
-	}
-	return strings.Join(joinLabels, ",")
-}
-
-func (c *containerContext) Label(name string) string {
-	n := strings.Split(name, ".")
-	r := strings.NewReplacer("-", " ", "_", " ")
-	h := r.Replace(n[len(n)-1])
-
-	c.addHeader(h)
-
-	if c.c.Labels == nil {
-		return ""
-	}
-	return c.c.Labels[name]
-}
-
-func (c *containerContext) Mounts() string {
-	c.addHeader(mountsHeader)
-
-	var name string
-	var mounts []string
-	for _, m := range c.c.Mounts {
-		if m.Name == "" {
-			name = m.Source
-		} else {
-			name = m.Name
-		}
-		if c.trunc {
-			name = stringutils.Truncate(name, 15)
-		}
-		mounts = append(mounts, name)
-	}
-	return strings.Join(mounts, ",")
-}
-
-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)

+ 0 - 164
api/client/formatter/custom_test.go

@@ -4,172 +4,8 @@ import (
 	"reflect"
 	"strings"
 	"testing"
-	"time"
-
-	"github.com/docker/docker/pkg/stringid"
-	"github.com/docker/engine-api/types"
 )
 
-func TestContainerPsContext(t *testing.T) {
-	containerID := stringid.GenerateRandomID()
-	unix := time.Now().Add(-65 * time.Second).Unix()
-
-	var ctx containerContext
-	cases := []struct {
-		container types.Container
-		trunc     bool
-		expValue  string
-		expHeader string
-		call      func() string
-	}{
-		{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},
-		{types.Container{Image: "verylongimagename"}, false, "verylongimagename", imageHeader, ctx.Image},
-		{types.Container{
-			Image:   "a5a665ff33eced1e0803148700880edab4",
-			ImageID: "a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5",
-		},
-			true,
-			"a5a665ff33ec",
-			imageHeader,
-			ctx.Image,
-		},
-		{types.Container{
-			Image:   "a5a665ff33eced1e0803148700880edab4",
-			ImageID: "a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5",
-		},
-			false,
-			"a5a665ff33eced1e0803148700880edab4",
-			imageHeader,
-			ctx.Image,
-		},
-		{types.Container{Image: ""}, true, "<no image>", imageHeader, ctx.Image},
-		{types.Container{Command: "sh -c 'ls -la'"}, true, `"sh -c 'ls -la'"`, commandHeader, ctx.Command},
-		{types.Container{Created: unix}, true, time.Unix(unix, 0).String(), createdAtHeader, ctx.CreatedAt},
-		{types.Container{Ports: []types.Port{{PrivatePort: 8080, PublicPort: 8080, Type: "tcp"}}}, true, "8080/tcp", portsHeader, ctx.Ports},
-		{types.Container{Status: "RUNNING"}, true, "RUNNING", statusHeader, ctx.Status},
-		{types.Container{SizeRw: 10}, true, "10 B", sizeHeader, ctx.Size},
-		{types.Container{SizeRw: 10, SizeRootFs: 20}, true, "10 B (virtual 20 B)", sizeHeader, ctx.Size},
-		{types.Container{}, true, "", labelsHeader, ctx.Labels},
-		{types.Container{Labels: map[string]string{"cpu": "6", "storage": "ssd"}}, true, "cpu=6,storage=ssd", labelsHeader, ctx.Labels},
-		{types.Container{Created: unix}, true, "About a minute", runningForHeader, ctx.RunningFor},
-	}
-
-	for _, c := range cases {
-		ctx = containerContext{c: c.container, trunc: c.trunc}
-		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)
-		}
-	}
-
-	c1 := types.Container{Labels: map[string]string{"com.docker.swarm.swarm-id": "33", "com.docker.swarm.node_name": "ubuntu"}}
-	ctx = containerContext{c: c1, trunc: true}
-
-	sid := ctx.Label("com.docker.swarm.swarm-id")
-	node := ctx.Label("com.docker.swarm.node_name")
-	if sid != "33" {
-		t.Fatalf("Expected 33, was %s\n", sid)
-	}
-
-	if node != "ubuntu" {
-		t.Fatalf("Expected ubuntu, was %s\n", node)
-	}
-
-	h := ctx.fullHeader()
-	if h != "SWARM ID\tNODE NAME" {
-		t.Fatalf("Expected %s, was %s\n", "SWARM ID\tNODE NAME", h)
-
-	}
-
-	c2 := types.Container{}
-	ctx = containerContext{c: c2, trunc: true}
-
-	label := ctx.Label("anything.really")
-	if label != "" {
-		t.Fatalf("Expected an empty string, was %s", label)
-	}
-
-	ctx = containerContext{c: c2, trunc: true}
-	fullHeader := ctx.fullHeader()
-	if fullHeader != "" {
-		t.Fatalf("Expected fullHeader to be empty, was %s", fullHeader)
-	}
-
-}
-
-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

+ 1 - 218
api/client/formatter/formatter.go

@@ -8,19 +8,14 @@ import (
 	"text/tabwriter"
 	"text/template"
 
-	"github.com/docker/docker/reference"
 	"github.com/docker/docker/utils/templates"
-	"github.com/docker/engine-api/types"
 )
 
 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}}"
+	defaultQuietFormat = "{{.ID}}"
 )
 
 // Context contains information required by the formatter to print the output as desired.
@@ -93,215 +88,3 @@ func (c *Context) contextFormat(tmpl *template.Template, subContext subContext)
 	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:
-		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`
-			}
-		}
-	}
-
-	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{})
-}
-
-func isDangling(image types.Image) bool {
-	return len(image.RepoTags) == 1 && image.RepoTags[0] == "<none>:<none>" && len(image.RepoDigests) == 1 && image.RepoDigests[0] == "<none>@<none>"
-}
-
-func (ctx ImageContext) Write() {
-	switch ctx.Format {
-	case tableFormatKey:
-		ctx.Format = defaultImageTableFormat
-		if ctx.Digest {
-			ctx.Format = defaultImageTableFormatWithDigest
-		}
-		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 {
-		images := []*imageContext{}
-		if isDangling(image) {
-			images = append(images, &imageContext{
-				trunc:  ctx.Trunc,
-				i:      image,
-				repo:   "<none>",
-				tag:    "<none>",
-				digest: "<none>",
-			})
-		} else {
-			repoTags := map[string][]string{}
-			repoDigests := map[string][]string{}
-
-			for _, refString := range append(image.RepoTags) {
-				ref, err := reference.ParseNamed(refString)
-				if err != nil {
-					continue
-				}
-				if nt, ok := ref.(reference.NamedTagged); ok {
-					repoTags[ref.Name()] = append(repoTags[ref.Name()], nt.Tag())
-				}
-			}
-			for _, refString := range append(image.RepoDigests) {
-				ref, err := reference.ParseNamed(refString)
-				if err != nil {
-					continue
-				}
-				if c, ok := ref.(reference.Canonical); ok {
-					repoDigests[ref.Name()] = append(repoDigests[ref.Name()], c.Digest().String())
-				}
-			}
-
-			for repo, tags := range repoTags {
-				digests := repoDigests[repo]
-
-				// Do not display digests as their own row
-				delete(repoDigests, repo)
-
-				if !ctx.Digest {
-					// Ignore digest references, just show tag once
-					digests = nil
-				}
-
-				for _, tag := range tags {
-					if len(digests) == 0 {
-						images = append(images, &imageContext{
-							trunc:  ctx.Trunc,
-							i:      image,
-							repo:   repo,
-							tag:    tag,
-							digest: "<none>",
-						})
-						continue
-					}
-					// Display the digests for each tag
-					for _, dgst := range digests {
-						images = append(images, &imageContext{
-							trunc:  ctx.Trunc,
-							i:      image,
-							repo:   repo,
-							tag:    tag,
-							digest: dgst,
-						})
-					}
-
-				}
-			}
-
-			// Show rows for remaining digest only references
-			for repo, digests := range repoDigests {
-				// If digests are displayed, show row per digest
-				if ctx.Digest {
-					for _, dgst := range digests {
-						images = append(images, &imageContext{
-							trunc:  ctx.Trunc,
-							i:      image,
-							repo:   repo,
-							tag:    "<none>",
-							digest: dgst,
-						})
-					}
-				} else {
-					images = append(images, &imageContext{
-						trunc: ctx.Trunc,
-						i:     image,
-						repo:  repo,
-						tag:   "<none>",
-					})
-				}
-			}
-		}
-		for _, imageCtx := range images {
-			err = ctx.contextFormat(tmpl, imageCtx)
-			if err != nil {
-				return
-			}
-		}
-	}
-
-	ctx.postformat(tmpl, &imageContext{})
-}

+ 229 - 0
api/client/formatter/image.go

@@ -0,0 +1,229 @@
+package formatter
+
+import (
+	"bytes"
+	"strings"
+	"time"
+
+	"github.com/docker/docker/pkg/stringid"
+	"github.com/docker/docker/reference"
+	"github.com/docker/engine-api/types"
+	"github.com/docker/go-units"
+)
+
+const (
+	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}}"
+
+	imageIDHeader    = "IMAGE ID"
+	repositoryHeader = "REPOSITORY"
+	tagHeader        = "TAG"
+	digestHeader     = "DIGEST"
+)
+
+// ImageContext contains image specific information required by the formater, encapsulate a Context struct.
+type ImageContext struct {
+	Context
+	Digest bool
+	// Images
+	Images []types.Image
+}
+
+func isDangling(image types.Image) bool {
+	return len(image.RepoTags) == 1 && image.RepoTags[0] == "<none>:<none>" && len(image.RepoDigests) == 1 && image.RepoDigests[0] == "<none>@<none>"
+}
+
+func (ctx ImageContext) Write() {
+	switch ctx.Format {
+	case tableFormatKey:
+		ctx.Format = defaultImageTableFormat
+		if ctx.Digest {
+			ctx.Format = defaultImageTableFormatWithDigest
+		}
+		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 {
+		images := []*imageContext{}
+		if isDangling(image) {
+			images = append(images, &imageContext{
+				trunc:  ctx.Trunc,
+				i:      image,
+				repo:   "<none>",
+				tag:    "<none>",
+				digest: "<none>",
+			})
+		} else {
+			repoTags := map[string][]string{}
+			repoDigests := map[string][]string{}
+
+			for _, refString := range append(image.RepoTags) {
+				ref, err := reference.ParseNamed(refString)
+				if err != nil {
+					continue
+				}
+				if nt, ok := ref.(reference.NamedTagged); ok {
+					repoTags[ref.Name()] = append(repoTags[ref.Name()], nt.Tag())
+				}
+			}
+			for _, refString := range append(image.RepoDigests) {
+				ref, err := reference.ParseNamed(refString)
+				if err != nil {
+					continue
+				}
+				if c, ok := ref.(reference.Canonical); ok {
+					repoDigests[ref.Name()] = append(repoDigests[ref.Name()], c.Digest().String())
+				}
+			}
+
+			for repo, tags := range repoTags {
+				digests := repoDigests[repo]
+
+				// Do not display digests as their own row
+				delete(repoDigests, repo)
+
+				if !ctx.Digest {
+					// Ignore digest references, just show tag once
+					digests = nil
+				}
+
+				for _, tag := range tags {
+					if len(digests) == 0 {
+						images = append(images, &imageContext{
+							trunc:  ctx.Trunc,
+							i:      image,
+							repo:   repo,
+							tag:    tag,
+							digest: "<none>",
+						})
+						continue
+					}
+					// Display the digests for each tag
+					for _, dgst := range digests {
+						images = append(images, &imageContext{
+							trunc:  ctx.Trunc,
+							i:      image,
+							repo:   repo,
+							tag:    tag,
+							digest: dgst,
+						})
+					}
+
+				}
+			}
+
+			// Show rows for remaining digest only references
+			for repo, digests := range repoDigests {
+				// If digests are displayed, show row per digest
+				if ctx.Digest {
+					for _, dgst := range digests {
+						images = append(images, &imageContext{
+							trunc:  ctx.Trunc,
+							i:      image,
+							repo:   repo,
+							tag:    "<none>",
+							digest: dgst,
+						})
+					}
+				} else {
+					images = append(images, &imageContext{
+						trunc: ctx.Trunc,
+						i:     image,
+						repo:  repo,
+						tag:   "<none>",
+					})
+				}
+			}
+		}
+		for _, imageCtx := range images {
+			err = ctx.contextFormat(tmpl, imageCtx)
+			if err != nil {
+				return
+			}
+		}
+	}
+
+	ctx.postformat(tmpl, &imageContext{})
+}
+
+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))
+}

+ 54 - 246
api/client/formatter/formatter_test.go → api/client/formatter/image_test.go

@@ -3,265 +3,73 @@ package formatter
 import (
 	"bytes"
 	"fmt"
+	"strings"
 	"testing"
 	"time"
 
+	"github.com/docker/docker/pkg/stringid"
 	"github.com/docker/engine-api/types"
 )
 
-func TestContainerContextWrite(t *testing.T) {
-	unixTime := time.Now().AddDate(0, 0, -1).Unix()
-	expectedTime := time.Unix(unixTime, 0).String()
+func TestImageContext(t *testing.T) {
+	imageID := stringid.GenerateRandomID()
+	unix := time.Now().Unix()
 
-	contexts := []struct {
-		context  ContainerContext
-		expected string
+	var ctx imageContext
+	cases := []struct {
+		imageCtx  imageContext
+		expValue  string
+		expHeader string
+		call      func() 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\nubuntu\nubuntu\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",
-		},
+		{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 _, 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},
+	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)
 		}
-		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\n",
-		},
-		{
-			ContainerContext{
-				Context: Context{
-					Format: "table {{.Image}}\t{{.Size}}",
-					Output: out,
-				},
-			},
-			"IMAGE               SIZE\n",
-		},
-		{
-			ContainerContext{
-				Context: Context{
-					Format: "table {{.Image}}\t{{.Size}}",
-					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)
+		h := ctx.fullHeader()
+		if h != c.expHeader {
+			t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
 		}
-		// Clean buffer
-		out.Reset()
 	}
 }