Browse code

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 authored on 2016/08/04 21:59:45
Showing 8 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,208 @@
0
+package formatter
1
+
2
+import (
3
+	"bytes"
4
+	"fmt"
5
+	"strconv"
6
+	"strings"
7
+	"time"
8
+
9
+	"github.com/docker/docker/api"
10
+	"github.com/docker/docker/pkg/stringid"
11
+	"github.com/docker/docker/pkg/stringutils"
12
+	"github.com/docker/engine-api/types"
13
+	"github.com/docker/go-units"
14
+)
15
+
16
+const (
17
+	defaultContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.RunningFor}} ago\t{{.Status}}\t{{.Ports}}\t{{.Names}}"
18
+
19
+	containerIDHeader = "CONTAINER ID"
20
+	namesHeader       = "NAMES"
21
+	commandHeader     = "COMMAND"
22
+	runningForHeader  = "CREATED"
23
+	statusHeader      = "STATUS"
24
+	portsHeader       = "PORTS"
25
+	mountsHeader      = "MOUNTS"
26
+)
27
+
28
+// ContainerContext contains container specific information required by the formater, encapsulate a Context struct.
29
+type ContainerContext struct {
30
+	Context
31
+	// Size when set to true will display the size of the output.
32
+	Size bool
33
+	// Containers
34
+	Containers []types.Container
35
+}
36
+
37
+func (ctx ContainerContext) Write() {
38
+	switch ctx.Format {
39
+	case tableFormatKey:
40
+		if ctx.Quiet {
41
+			ctx.Format = defaultQuietFormat
42
+		} else {
43
+			ctx.Format = defaultContainerTableFormat
44
+			if ctx.Size {
45
+				ctx.Format += `\t{{.Size}}`
46
+			}
47
+		}
48
+	case rawFormatKey:
49
+		if ctx.Quiet {
50
+			ctx.Format = `container_id: {{.ID}}`
51
+		} else {
52
+			ctx.Format = `container_id: {{.ID}}\nimage: {{.Image}}\ncommand: {{.Command}}\ncreated_at: {{.CreatedAt}}\nstatus: {{.Status}}\nnames: {{.Names}}\nlabels: {{.Labels}}\nports: {{.Ports}}\n`
53
+			if ctx.Size {
54
+				ctx.Format += `size: {{.Size}}\n`
55
+			}
56
+		}
57
+	}
58
+
59
+	ctx.buffer = bytes.NewBufferString("")
60
+	ctx.preformat()
61
+
62
+	tmpl, err := ctx.parseFormat()
63
+	if err != nil {
64
+		return
65
+	}
66
+
67
+	for _, container := range ctx.Containers {
68
+		containerCtx := &containerContext{
69
+			trunc: ctx.Trunc,
70
+			c:     container,
71
+		}
72
+		err = ctx.contextFormat(tmpl, containerCtx)
73
+		if err != nil {
74
+			return
75
+		}
76
+	}
77
+
78
+	ctx.postformat(tmpl, &containerContext{})
79
+}
80
+
81
+type containerContext struct {
82
+	baseSubContext
83
+	trunc bool
84
+	c     types.Container
85
+}
86
+
87
+func (c *containerContext) ID() string {
88
+	c.addHeader(containerIDHeader)
89
+	if c.trunc {
90
+		return stringid.TruncateID(c.c.ID)
91
+	}
92
+	return c.c.ID
93
+}
94
+
95
+func (c *containerContext) Names() string {
96
+	c.addHeader(namesHeader)
97
+	names := stripNamePrefix(c.c.Names)
98
+	if c.trunc {
99
+		for _, name := range names {
100
+			if len(strings.Split(name, "/")) == 1 {
101
+				names = []string{name}
102
+				break
103
+			}
104
+		}
105
+	}
106
+	return strings.Join(names, ",")
107
+}
108
+
109
+func (c *containerContext) Image() string {
110
+	c.addHeader(imageHeader)
111
+	if c.c.Image == "" {
112
+		return "<no image>"
113
+	}
114
+	if c.trunc {
115
+		if trunc := stringid.TruncateID(c.c.ImageID); trunc == stringid.TruncateID(c.c.Image) {
116
+			return trunc
117
+		}
118
+	}
119
+	return c.c.Image
120
+}
121
+
122
+func (c *containerContext) Command() string {
123
+	c.addHeader(commandHeader)
124
+	command := c.c.Command
125
+	if c.trunc {
126
+		command = stringutils.Truncate(command, 20)
127
+	}
128
+	return strconv.Quote(command)
129
+}
130
+
131
+func (c *containerContext) CreatedAt() string {
132
+	c.addHeader(createdAtHeader)
133
+	return time.Unix(int64(c.c.Created), 0).String()
134
+}
135
+
136
+func (c *containerContext) RunningFor() string {
137
+	c.addHeader(runningForHeader)
138
+	createdAt := time.Unix(int64(c.c.Created), 0)
139
+	return units.HumanDuration(time.Now().UTC().Sub(createdAt))
140
+}
141
+
142
+func (c *containerContext) Ports() string {
143
+	c.addHeader(portsHeader)
144
+	return api.DisplayablePorts(c.c.Ports)
145
+}
146
+
147
+func (c *containerContext) Status() string {
148
+	c.addHeader(statusHeader)
149
+	return c.c.Status
150
+}
151
+
152
+func (c *containerContext) Size() string {
153
+	c.addHeader(sizeHeader)
154
+	srw := units.HumanSize(float64(c.c.SizeRw))
155
+	sv := units.HumanSize(float64(c.c.SizeRootFs))
156
+
157
+	sf := srw
158
+	if c.c.SizeRootFs > 0 {
159
+		sf = fmt.Sprintf("%s (virtual %s)", srw, sv)
160
+	}
161
+	return sf
162
+}
163
+
164
+func (c *containerContext) Labels() string {
165
+	c.addHeader(labelsHeader)
166
+	if c.c.Labels == nil {
167
+		return ""
168
+	}
169
+
170
+	var joinLabels []string
171
+	for k, v := range c.c.Labels {
172
+		joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v))
173
+	}
174
+	return strings.Join(joinLabels, ",")
175
+}
176
+
177
+func (c *containerContext) Label(name string) string {
178
+	n := strings.Split(name, ".")
179
+	r := strings.NewReplacer("-", " ", "_", " ")
180
+	h := r.Replace(n[len(n)-1])
181
+
182
+	c.addHeader(h)
183
+
184
+	if c.c.Labels == nil {
185
+		return ""
186
+	}
187
+	return c.c.Labels[name]
188
+}
189
+
190
+func (c *containerContext) Mounts() string {
191
+	c.addHeader(mountsHeader)
192
+
193
+	var name string
194
+	var mounts []string
195
+	for _, m := range c.c.Mounts {
196
+		if m.Name == "" {
197
+			name = m.Source
198
+		} else {
199
+			name = m.Name
200
+		}
201
+		if c.trunc {
202
+			name = stringutils.Truncate(name, 15)
203
+		}
204
+		mounts = append(mounts, name)
205
+	}
206
+	return strings.Join(mounts, ",")
207
+}
0 208
new file mode 100644
... ...
@@ -0,0 +1,404 @@
0
+package formatter
1
+
2
+import (
3
+	"bytes"
4
+	"fmt"
5
+	"strings"
6
+	"testing"
7
+	"time"
8
+
9
+	"github.com/docker/docker/pkg/stringid"
10
+	"github.com/docker/engine-api/types"
11
+)
12
+
13
+func TestContainerPsContext(t *testing.T) {
14
+	containerID := stringid.GenerateRandomID()
15
+	unix := time.Now().Add(-65 * time.Second).Unix()
16
+
17
+	var ctx containerContext
18
+	cases := []struct {
19
+		container types.Container
20
+		trunc     bool
21
+		expValue  string
22
+		expHeader string
23
+		call      func() string
24
+	}{
25
+		{types.Container{ID: containerID}, true, stringid.TruncateID(containerID), containerIDHeader, ctx.ID},
26
+		{types.Container{ID: containerID}, false, containerID, containerIDHeader, ctx.ID},
27
+		{types.Container{Names: []string{"/foobar_baz"}}, true, "foobar_baz", namesHeader, ctx.Names},
28
+		{types.Container{Image: "ubuntu"}, true, "ubuntu", imageHeader, ctx.Image},
29
+		{types.Container{Image: "verylongimagename"}, true, "verylongimagename", imageHeader, ctx.Image},
30
+		{types.Container{Image: "verylongimagename"}, false, "verylongimagename", imageHeader, ctx.Image},
31
+		{types.Container{
32
+			Image:   "a5a665ff33eced1e0803148700880edab4",
33
+			ImageID: "a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5",
34
+		},
35
+			true,
36
+			"a5a665ff33ec",
37
+			imageHeader,
38
+			ctx.Image,
39
+		},
40
+		{types.Container{
41
+			Image:   "a5a665ff33eced1e0803148700880edab4",
42
+			ImageID: "a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5",
43
+		},
44
+			false,
45
+			"a5a665ff33eced1e0803148700880edab4",
46
+			imageHeader,
47
+			ctx.Image,
48
+		},
49
+		{types.Container{Image: ""}, true, "<no image>", imageHeader, ctx.Image},
50
+		{types.Container{Command: "sh -c 'ls -la'"}, true, `"sh -c 'ls -la'"`, commandHeader, ctx.Command},
51
+		{types.Container{Created: unix}, true, time.Unix(unix, 0).String(), createdAtHeader, ctx.CreatedAt},
52
+		{types.Container{Ports: []types.Port{{PrivatePort: 8080, PublicPort: 8080, Type: "tcp"}}}, true, "8080/tcp", portsHeader, ctx.Ports},
53
+		{types.Container{Status: "RUNNING"}, true, "RUNNING", statusHeader, ctx.Status},
54
+		{types.Container{SizeRw: 10}, true, "10 B", sizeHeader, ctx.Size},
55
+		{types.Container{SizeRw: 10, SizeRootFs: 20}, true, "10 B (virtual 20 B)", sizeHeader, ctx.Size},
56
+		{types.Container{}, true, "", labelsHeader, ctx.Labels},
57
+		{types.Container{Labels: map[string]string{"cpu": "6", "storage": "ssd"}}, true, "cpu=6,storage=ssd", labelsHeader, ctx.Labels},
58
+		{types.Container{Created: unix}, true, "About a minute", runningForHeader, ctx.RunningFor},
59
+		{types.Container{
60
+			Mounts: []types.MountPoint{
61
+				{
62
+					Name:   "733908409c91817de8e92b0096373245f329f19a88e2c849f02460e9b3d1c203",
63
+					Driver: "local",
64
+					Source: "/a/path",
65
+				},
66
+			},
67
+		}, true, "733908409c91817", mountsHeader, ctx.Mounts},
68
+		{types.Container{
69
+			Mounts: []types.MountPoint{
70
+				{
71
+					Driver: "local",
72
+					Source: "/a/path",
73
+				},
74
+			},
75
+		}, false, "/a/path", mountsHeader, ctx.Mounts},
76
+		{types.Container{
77
+			Mounts: []types.MountPoint{
78
+				{
79
+					Name:   "733908409c91817de8e92b0096373245f329f19a88e2c849f02460e9b3d1c203",
80
+					Driver: "local",
81
+					Source: "/a/path",
82
+				},
83
+			},
84
+		}, false, "733908409c91817de8e92b0096373245f329f19a88e2c849f02460e9b3d1c203", mountsHeader, ctx.Mounts},
85
+	}
86
+
87
+	for _, c := range cases {
88
+		ctx = containerContext{c: c.container, trunc: c.trunc}
89
+		v := c.call()
90
+		if strings.Contains(v, ",") {
91
+			compareMultipleValues(t, v, c.expValue)
92
+		} else if v != c.expValue {
93
+			t.Fatalf("Expected %s, was %s\n", c.expValue, v)
94
+		}
95
+
96
+		h := ctx.fullHeader()
97
+		if h != c.expHeader {
98
+			t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
99
+		}
100
+	}
101
+
102
+	c1 := types.Container{Labels: map[string]string{"com.docker.swarm.swarm-id": "33", "com.docker.swarm.node_name": "ubuntu"}}
103
+	ctx = containerContext{c: c1, trunc: true}
104
+
105
+	sid := ctx.Label("com.docker.swarm.swarm-id")
106
+	node := ctx.Label("com.docker.swarm.node_name")
107
+	if sid != "33" {
108
+		t.Fatalf("Expected 33, was %s\n", sid)
109
+	}
110
+
111
+	if node != "ubuntu" {
112
+		t.Fatalf("Expected ubuntu, was %s\n", node)
113
+	}
114
+
115
+	h := ctx.fullHeader()
116
+	if h != "SWARM ID\tNODE NAME" {
117
+		t.Fatalf("Expected %s, was %s\n", "SWARM ID\tNODE NAME", h)
118
+
119
+	}
120
+
121
+	c2 := types.Container{}
122
+	ctx = containerContext{c: c2, trunc: true}
123
+
124
+	label := ctx.Label("anything.really")
125
+	if label != "" {
126
+		t.Fatalf("Expected an empty string, was %s", label)
127
+	}
128
+
129
+	ctx = containerContext{c: c2, trunc: true}
130
+	fullHeader := ctx.fullHeader()
131
+	if fullHeader != "" {
132
+		t.Fatalf("Expected fullHeader to be empty, was %s", fullHeader)
133
+	}
134
+
135
+}
136
+
137
+func TestContainerContextWrite(t *testing.T) {
138
+	unixTime := time.Now().AddDate(0, 0, -1).Unix()
139
+	expectedTime := time.Unix(unixTime, 0).String()
140
+
141
+	contexts := []struct {
142
+		context  ContainerContext
143
+		expected string
144
+	}{
145
+		// Errors
146
+		{
147
+			ContainerContext{
148
+				Context: Context{
149
+					Format: "{{InvalidFunction}}",
150
+				},
151
+			},
152
+			`Template parsing error: template: :1: function "InvalidFunction" not defined
153
+`,
154
+		},
155
+		{
156
+			ContainerContext{
157
+				Context: Context{
158
+					Format: "{{nil}}",
159
+				},
160
+			},
161
+			`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
162
+`,
163
+		},
164
+		// Table Format
165
+		{
166
+			ContainerContext{
167
+				Context: Context{
168
+					Format: "table",
169
+				},
170
+				Size: true,
171
+			},
172
+			`CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES               SIZE
173
+containerID1        ubuntu              ""                  24 hours ago                                                foobar_baz          0 B
174
+containerID2        ubuntu              ""                  24 hours ago                                                foobar_bar          0 B
175
+`,
176
+		},
177
+		{
178
+			ContainerContext{
179
+				Context: Context{
180
+					Format: "table",
181
+				},
182
+			},
183
+			`CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
184
+containerID1        ubuntu              ""                  24 hours ago                                                foobar_baz
185
+containerID2        ubuntu              ""                  24 hours ago                                                foobar_bar
186
+`,
187
+		},
188
+		{
189
+			ContainerContext{
190
+				Context: Context{
191
+					Format: "table {{.Image}}",
192
+				},
193
+			},
194
+			"IMAGE\nubuntu\nubuntu\n",
195
+		},
196
+		{
197
+			ContainerContext{
198
+				Context: Context{
199
+					Format: "table {{.Image}}",
200
+				},
201
+				Size: true,
202
+			},
203
+			"IMAGE\nubuntu\nubuntu\n",
204
+		},
205
+		{
206
+			ContainerContext{
207
+				Context: Context{
208
+					Format: "table {{.Image}}",
209
+					Quiet:  true,
210
+				},
211
+			},
212
+			"IMAGE\nubuntu\nubuntu\n",
213
+		},
214
+		{
215
+			ContainerContext{
216
+				Context: Context{
217
+					Format: "table",
218
+					Quiet:  true,
219
+				},
220
+			},
221
+			"containerID1\ncontainerID2\n",
222
+		},
223
+		// Raw Format
224
+		{
225
+			ContainerContext{
226
+				Context: Context{
227
+					Format: "raw",
228
+				},
229
+			},
230
+			fmt.Sprintf(`container_id: containerID1
231
+image: ubuntu
232
+command: ""
233
+created_at: %s
234
+status: 
235
+names: foobar_baz
236
+labels: 
237
+ports: 
238
+
239
+container_id: containerID2
240
+image: ubuntu
241
+command: ""
242
+created_at: %s
243
+status: 
244
+names: foobar_bar
245
+labels: 
246
+ports: 
247
+
248
+`, expectedTime, expectedTime),
249
+		},
250
+		{
251
+			ContainerContext{
252
+				Context: Context{
253
+					Format: "raw",
254
+				},
255
+				Size: true,
256
+			},
257
+			fmt.Sprintf(`container_id: containerID1
258
+image: ubuntu
259
+command: ""
260
+created_at: %s
261
+status: 
262
+names: foobar_baz
263
+labels: 
264
+ports: 
265
+size: 0 B
266
+
267
+container_id: containerID2
268
+image: ubuntu
269
+command: ""
270
+created_at: %s
271
+status: 
272
+names: foobar_bar
273
+labels: 
274
+ports: 
275
+size: 0 B
276
+
277
+`, expectedTime, expectedTime),
278
+		},
279
+		{
280
+			ContainerContext{
281
+				Context: Context{
282
+					Format: "raw",
283
+					Quiet:  true,
284
+				},
285
+			},
286
+			"container_id: containerID1\ncontainer_id: containerID2\n",
287
+		},
288
+		// Custom Format
289
+		{
290
+			ContainerContext{
291
+				Context: Context{
292
+					Format: "{{.Image}}",
293
+				},
294
+			},
295
+			"ubuntu\nubuntu\n",
296
+		},
297
+		{
298
+			ContainerContext{
299
+				Context: Context{
300
+					Format: "{{.Image}}",
301
+				},
302
+				Size: true,
303
+			},
304
+			"ubuntu\nubuntu\n",
305
+		},
306
+	}
307
+
308
+	for _, context := range contexts {
309
+		containers := []types.Container{
310
+			{ID: "containerID1", Names: []string{"/foobar_baz"}, Image: "ubuntu", Created: unixTime},
311
+			{ID: "containerID2", Names: []string{"/foobar_bar"}, Image: "ubuntu", Created: unixTime},
312
+		}
313
+		out := bytes.NewBufferString("")
314
+		context.context.Output = out
315
+		context.context.Containers = containers
316
+		context.context.Write()
317
+		actual := out.String()
318
+		if actual != context.expected {
319
+			t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
320
+		}
321
+		// Clean buffer
322
+		out.Reset()
323
+	}
324
+}
325
+
326
+func TestContainerContextWriteWithNoContainers(t *testing.T) {
327
+	out := bytes.NewBufferString("")
328
+	containers := []types.Container{}
329
+
330
+	contexts := []struct {
331
+		context  ContainerContext
332
+		expected string
333
+	}{
334
+		{
335
+			ContainerContext{
336
+				Context: Context{
337
+					Format: "{{.Image}}",
338
+					Output: out,
339
+				},
340
+			},
341
+			"",
342
+		},
343
+		{
344
+			ContainerContext{
345
+				Context: Context{
346
+					Format: "table {{.Image}}",
347
+					Output: out,
348
+				},
349
+			},
350
+			"IMAGE\n",
351
+		},
352
+		{
353
+			ContainerContext{
354
+				Context: Context{
355
+					Format: "{{.Image}}",
356
+					Output: out,
357
+				},
358
+				Size: true,
359
+			},
360
+			"",
361
+		},
362
+		{
363
+			ContainerContext{
364
+				Context: Context{
365
+					Format: "table {{.Image}}",
366
+					Output: out,
367
+				},
368
+				Size: true,
369
+			},
370
+			"IMAGE\n",
371
+		},
372
+		{
373
+			ContainerContext{
374
+				Context: Context{
375
+					Format: "table {{.Image}}\t{{.Size}}",
376
+					Output: out,
377
+				},
378
+			},
379
+			"IMAGE               SIZE\n",
380
+		},
381
+		{
382
+			ContainerContext{
383
+				Context: Context{
384
+					Format: "table {{.Image}}\t{{.Size}}",
385
+					Output: out,
386
+				},
387
+				Size: true,
388
+			},
389
+			"IMAGE               SIZE\n",
390
+		},
391
+	}
392
+
393
+	for _, context := range contexts {
394
+		context.context.Containers = containers
395
+		context.context.Write()
396
+		actual := out.String()
397
+		if actual != context.expected {
398
+			t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
399
+		}
400
+		// Clean buffer
401
+		out.Reset()
402
+	}
403
+}
... ...
@@ -1,215 +1,21 @@
1 1
 package formatter
2 2
 
3 3
 import (
4
-	"fmt"
5
-	"strconv"
6 4
 	"strings"
7
-	"time"
8
-
9
-	"github.com/docker/docker/api"
10
-	"github.com/docker/docker/pkg/stringid"
11
-	"github.com/docker/docker/pkg/stringutils"
12
-	"github.com/docker/engine-api/types"
13
-	"github.com/docker/go-units"
14 5
 )
15 6
 
16 7
 const (
17 8
 	tableKey = "table"
18 9
 
19
-	containerIDHeader  = "CONTAINER ID"
20 10
 	imageHeader        = "IMAGE"
21
-	namesHeader        = "NAMES"
22
-	commandHeader      = "COMMAND"
23 11
 	createdSinceHeader = "CREATED"
24 12
 	createdAtHeader    = "CREATED AT"
25
-	runningForHeader   = "CREATED"
26
-	statusHeader       = "STATUS"
27
-	portsHeader        = "PORTS"
28 13
 	sizeHeader         = "SIZE"
29 14
 	labelsHeader       = "LABELS"
30
-	imageIDHeader      = "IMAGE ID"
31
-	repositoryHeader   = "REPOSITORY"
32
-	tagHeader          = "TAG"
33
-	digestHeader       = "DIGEST"
34
-	mountsHeader       = "MOUNTS"
15
+	nameHeader         = "NAME"
16
+	driverHeader       = "DRIVER"
35 17
 )
36 18
 
37
-type containerContext struct {
38
-	baseSubContext
39
-	trunc bool
40
-	c     types.Container
41
-}
42
-
43
-func (c *containerContext) ID() string {
44
-	c.addHeader(containerIDHeader)
45
-	if c.trunc {
46
-		return stringid.TruncateID(c.c.ID)
47
-	}
48
-	return c.c.ID
49
-}
50
-
51
-func (c *containerContext) Names() string {
52
-	c.addHeader(namesHeader)
53
-	names := stripNamePrefix(c.c.Names)
54
-	if c.trunc {
55
-		for _, name := range names {
56
-			if len(strings.Split(name, "/")) == 1 {
57
-				names = []string{name}
58
-				break
59
-			}
60
-		}
61
-	}
62
-	return strings.Join(names, ",")
63
-}
64
-
65
-func (c *containerContext) Image() string {
66
-	c.addHeader(imageHeader)
67
-	if c.c.Image == "" {
68
-		return "<no image>"
69
-	}
70
-	if c.trunc {
71
-		if trunc := stringid.TruncateID(c.c.ImageID); trunc == stringid.TruncateID(c.c.Image) {
72
-			return trunc
73
-		}
74
-	}
75
-	return c.c.Image
76
-}
77
-
78
-func (c *containerContext) Command() string {
79
-	c.addHeader(commandHeader)
80
-	command := c.c.Command
81
-	if c.trunc {
82
-		command = stringutils.Truncate(command, 20)
83
-	}
84
-	return strconv.Quote(command)
85
-}
86
-
87
-func (c *containerContext) CreatedAt() string {
88
-	c.addHeader(createdAtHeader)
89
-	return time.Unix(int64(c.c.Created), 0).String()
90
-}
91
-
92
-func (c *containerContext) RunningFor() string {
93
-	c.addHeader(runningForHeader)
94
-	createdAt := time.Unix(int64(c.c.Created), 0)
95
-	return units.HumanDuration(time.Now().UTC().Sub(createdAt))
96
-}
97
-
98
-func (c *containerContext) Ports() string {
99
-	c.addHeader(portsHeader)
100
-	return api.DisplayablePorts(c.c.Ports)
101
-}
102
-
103
-func (c *containerContext) Status() string {
104
-	c.addHeader(statusHeader)
105
-	return c.c.Status
106
-}
107
-
108
-func (c *containerContext) Size() string {
109
-	c.addHeader(sizeHeader)
110
-	srw := units.HumanSize(float64(c.c.SizeRw))
111
-	sv := units.HumanSize(float64(c.c.SizeRootFs))
112
-
113
-	sf := srw
114
-	if c.c.SizeRootFs > 0 {
115
-		sf = fmt.Sprintf("%s (virtual %s)", srw, sv)
116
-	}
117
-	return sf
118
-}
119
-
120
-func (c *containerContext) Labels() string {
121
-	c.addHeader(labelsHeader)
122
-	if c.c.Labels == nil {
123
-		return ""
124
-	}
125
-
126
-	var joinLabels []string
127
-	for k, v := range c.c.Labels {
128
-		joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v))
129
-	}
130
-	return strings.Join(joinLabels, ",")
131
-}
132
-
133
-func (c *containerContext) Label(name string) string {
134
-	n := strings.Split(name, ".")
135
-	r := strings.NewReplacer("-", " ", "_", " ")
136
-	h := r.Replace(n[len(n)-1])
137
-
138
-	c.addHeader(h)
139
-
140
-	if c.c.Labels == nil {
141
-		return ""
142
-	}
143
-	return c.c.Labels[name]
144
-}
145
-
146
-func (c *containerContext) Mounts() string {
147
-	c.addHeader(mountsHeader)
148
-
149
-	var name string
150
-	var mounts []string
151
-	for _, m := range c.c.Mounts {
152
-		if m.Name == "" {
153
-			name = m.Source
154
-		} else {
155
-			name = m.Name
156
-		}
157
-		if c.trunc {
158
-			name = stringutils.Truncate(name, 15)
159
-		}
160
-		mounts = append(mounts, name)
161
-	}
162
-	return strings.Join(mounts, ",")
163
-}
164
-
165
-type imageContext struct {
166
-	baseSubContext
167
-	trunc  bool
168
-	i      types.Image
169
-	repo   string
170
-	tag    string
171
-	digest string
172
-}
173
-
174
-func (c *imageContext) ID() string {
175
-	c.addHeader(imageIDHeader)
176
-	if c.trunc {
177
-		return stringid.TruncateID(c.i.ID)
178
-	}
179
-	return c.i.ID
180
-}
181
-
182
-func (c *imageContext) Repository() string {
183
-	c.addHeader(repositoryHeader)
184
-	return c.repo
185
-}
186
-
187
-func (c *imageContext) Tag() string {
188
-	c.addHeader(tagHeader)
189
-	return c.tag
190
-}
191
-
192
-func (c *imageContext) Digest() string {
193
-	c.addHeader(digestHeader)
194
-	return c.digest
195
-}
196
-
197
-func (c *imageContext) CreatedSince() string {
198
-	c.addHeader(createdSinceHeader)
199
-	createdAt := time.Unix(int64(c.i.Created), 0)
200
-	return units.HumanDuration(time.Now().UTC().Sub(createdAt))
201
-}
202
-
203
-func (c *imageContext) CreatedAt() string {
204
-	c.addHeader(createdAtHeader)
205
-	return time.Unix(int64(c.i.Created), 0).String()
206
-}
207
-
208
-func (c *imageContext) Size() string {
209
-	c.addHeader(sizeHeader)
210
-	return units.HumanSize(float64(c.i.Size))
211
-}
212
-
213 19
 type subContext interface {
214 20
 	fullHeader() string
215 21
 	addHeader(header string)
... ...
@@ -4,172 +4,8 @@ import (
4 4
 	"reflect"
5 5
 	"strings"
6 6
 	"testing"
7
-	"time"
8
-
9
-	"github.com/docker/docker/pkg/stringid"
10
-	"github.com/docker/engine-api/types"
11 7
 )
12 8
 
13
-func TestContainerPsContext(t *testing.T) {
14
-	containerID := stringid.GenerateRandomID()
15
-	unix := time.Now().Add(-65 * time.Second).Unix()
16
-
17
-	var ctx containerContext
18
-	cases := []struct {
19
-		container types.Container
20
-		trunc     bool
21
-		expValue  string
22
-		expHeader string
23
-		call      func() string
24
-	}{
25
-		{types.Container{ID: containerID}, true, stringid.TruncateID(containerID), containerIDHeader, ctx.ID},
26
-		{types.Container{ID: containerID}, false, containerID, containerIDHeader, ctx.ID},
27
-		{types.Container{Names: []string{"/foobar_baz"}}, true, "foobar_baz", namesHeader, ctx.Names},
28
-		{types.Container{Image: "ubuntu"}, true, "ubuntu", imageHeader, ctx.Image},
29
-		{types.Container{Image: "verylongimagename"}, true, "verylongimagename", imageHeader, ctx.Image},
30
-		{types.Container{Image: "verylongimagename"}, false, "verylongimagename", imageHeader, ctx.Image},
31
-		{types.Container{
32
-			Image:   "a5a665ff33eced1e0803148700880edab4",
33
-			ImageID: "a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5",
34
-		},
35
-			true,
36
-			"a5a665ff33ec",
37
-			imageHeader,
38
-			ctx.Image,
39
-		},
40
-		{types.Container{
41
-			Image:   "a5a665ff33eced1e0803148700880edab4",
42
-			ImageID: "a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5",
43
-		},
44
-			false,
45
-			"a5a665ff33eced1e0803148700880edab4",
46
-			imageHeader,
47
-			ctx.Image,
48
-		},
49
-		{types.Container{Image: ""}, true, "<no image>", imageHeader, ctx.Image},
50
-		{types.Container{Command: "sh -c 'ls -la'"}, true, `"sh -c 'ls -la'"`, commandHeader, ctx.Command},
51
-		{types.Container{Created: unix}, true, time.Unix(unix, 0).String(), createdAtHeader, ctx.CreatedAt},
52
-		{types.Container{Ports: []types.Port{{PrivatePort: 8080, PublicPort: 8080, Type: "tcp"}}}, true, "8080/tcp", portsHeader, ctx.Ports},
53
-		{types.Container{Status: "RUNNING"}, true, "RUNNING", statusHeader, ctx.Status},
54
-		{types.Container{SizeRw: 10}, true, "10 B", sizeHeader, ctx.Size},
55
-		{types.Container{SizeRw: 10, SizeRootFs: 20}, true, "10 B (virtual 20 B)", sizeHeader, ctx.Size},
56
-		{types.Container{}, true, "", labelsHeader, ctx.Labels},
57
-		{types.Container{Labels: map[string]string{"cpu": "6", "storage": "ssd"}}, true, "cpu=6,storage=ssd", labelsHeader, ctx.Labels},
58
-		{types.Container{Created: unix}, true, "About a minute", runningForHeader, ctx.RunningFor},
59
-	}
60
-
61
-	for _, c := range cases {
62
-		ctx = containerContext{c: c.container, trunc: c.trunc}
63
-		v := c.call()
64
-		if strings.Contains(v, ",") {
65
-			compareMultipleValues(t, v, c.expValue)
66
-		} else if v != c.expValue {
67
-			t.Fatalf("Expected %s, was %s\n", c.expValue, v)
68
-		}
69
-
70
-		h := ctx.fullHeader()
71
-		if h != c.expHeader {
72
-			t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
73
-		}
74
-	}
75
-
76
-	c1 := types.Container{Labels: map[string]string{"com.docker.swarm.swarm-id": "33", "com.docker.swarm.node_name": "ubuntu"}}
77
-	ctx = containerContext{c: c1, trunc: true}
78
-
79
-	sid := ctx.Label("com.docker.swarm.swarm-id")
80
-	node := ctx.Label("com.docker.swarm.node_name")
81
-	if sid != "33" {
82
-		t.Fatalf("Expected 33, was %s\n", sid)
83
-	}
84
-
85
-	if node != "ubuntu" {
86
-		t.Fatalf("Expected ubuntu, was %s\n", node)
87
-	}
88
-
89
-	h := ctx.fullHeader()
90
-	if h != "SWARM ID\tNODE NAME" {
91
-		t.Fatalf("Expected %s, was %s\n", "SWARM ID\tNODE NAME", h)
92
-
93
-	}
94
-
95
-	c2 := types.Container{}
96
-	ctx = containerContext{c: c2, trunc: true}
97
-
98
-	label := ctx.Label("anything.really")
99
-	if label != "" {
100
-		t.Fatalf("Expected an empty string, was %s", label)
101
-	}
102
-
103
-	ctx = containerContext{c: c2, trunc: true}
104
-	fullHeader := ctx.fullHeader()
105
-	if fullHeader != "" {
106
-		t.Fatalf("Expected fullHeader to be empty, was %s", fullHeader)
107
-	}
108
-
109
-}
110
-
111
-func TestImagesContext(t *testing.T) {
112
-	imageID := stringid.GenerateRandomID()
113
-	unix := time.Now().Unix()
114
-
115
-	var ctx imageContext
116
-	cases := []struct {
117
-		imageCtx  imageContext
118
-		expValue  string
119
-		expHeader string
120
-		call      func() string
121
-	}{
122
-		{imageContext{
123
-			i:     types.Image{ID: imageID},
124
-			trunc: true,
125
-		}, stringid.TruncateID(imageID), imageIDHeader, ctx.ID},
126
-		{imageContext{
127
-			i:     types.Image{ID: imageID},
128
-			trunc: false,
129
-		}, imageID, imageIDHeader, ctx.ID},
130
-		{imageContext{
131
-			i:     types.Image{Size: 10},
132
-			trunc: true,
133
-		}, "10 B", sizeHeader, ctx.Size},
134
-		{imageContext{
135
-			i:     types.Image{Created: unix},
136
-			trunc: true,
137
-		}, time.Unix(unix, 0).String(), createdAtHeader, ctx.CreatedAt},
138
-		// FIXME
139
-		// {imageContext{
140
-		// 	i:     types.Image{Created: unix},
141
-		// 	trunc: true,
142
-		// }, units.HumanDuration(time.Unix(unix, 0)), createdSinceHeader, ctx.CreatedSince},
143
-		{imageContext{
144
-			i:    types.Image{},
145
-			repo: "busybox",
146
-		}, "busybox", repositoryHeader, ctx.Repository},
147
-		{imageContext{
148
-			i:   types.Image{},
149
-			tag: "latest",
150
-		}, "latest", tagHeader, ctx.Tag},
151
-		{imageContext{
152
-			i:      types.Image{},
153
-			digest: "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a",
154
-		}, "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a", digestHeader, ctx.Digest},
155
-	}
156
-
157
-	for _, c := range cases {
158
-		ctx = c.imageCtx
159
-		v := c.call()
160
-		if strings.Contains(v, ",") {
161
-			compareMultipleValues(t, v, c.expValue)
162
-		} else if v != c.expValue {
163
-			t.Fatalf("Expected %s, was %s\n", c.expValue, v)
164
-		}
165
-
166
-		h := ctx.fullHeader()
167
-		if h != c.expHeader {
168
-			t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
169
-		}
170
-	}
171
-}
172
-
173 9
 func compareMultipleValues(t *testing.T, value, expected string) {
174 10
 	// comma-separated values means probably a map input, which won't
175 11
 	// be guaranteed to have the same order as our expected value
... ...
@@ -8,19 +8,14 @@ import (
8 8
 	"text/tabwriter"
9 9
 	"text/template"
10 10
 
11
-	"github.com/docker/docker/reference"
12 11
 	"github.com/docker/docker/utils/templates"
13
-	"github.com/docker/engine-api/types"
14 12
 )
15 13
 
16 14
 const (
17 15
 	tableFormatKey = "table"
18 16
 	rawFormatKey   = "raw"
19 17
 
20
-	defaultContainerTableFormat       = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.RunningFor}} ago\t{{.Status}}\t{{.Ports}}\t{{.Names}}"
21
-	defaultImageTableFormat           = "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedSince}} ago\t{{.Size}}"
22
-	defaultImageTableFormatWithDigest = "table {{.Repository}}\t{{.Tag}}\t{{.Digest}}\t{{.ID}}\t{{.CreatedSince}} ago\t{{.Size}}"
23
-	defaultQuietFormat                = "{{.ID}}"
18
+	defaultQuietFormat = "{{.ID}}"
24 19
 )
25 20
 
26 21
 // 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)
93 93
 	c.buffer.WriteString("\n")
94 94
 	return nil
95 95
 }
96
-
97
-// ContainerContext contains container specific information required by the formater, encapsulate a Context struct.
98
-type ContainerContext struct {
99
-	Context
100
-	// Size when set to true will display the size of the output.
101
-	Size bool
102
-	// Containers
103
-	Containers []types.Container
104
-}
105
-
106
-// ImageContext contains image specific information required by the formater, encapsulate a Context struct.
107
-type ImageContext struct {
108
-	Context
109
-	Digest bool
110
-	// Images
111
-	Images []types.Image
112
-}
113
-
114
-func (ctx ContainerContext) Write() {
115
-	switch ctx.Format {
116
-	case tableFormatKey:
117
-		if ctx.Quiet {
118
-			ctx.Format = defaultQuietFormat
119
-		} else {
120
-			ctx.Format = defaultContainerTableFormat
121
-			if ctx.Size {
122
-				ctx.Format += `\t{{.Size}}`
123
-			}
124
-		}
125
-	case rawFormatKey:
126
-		if ctx.Quiet {
127
-			ctx.Format = `container_id: {{.ID}}`
128
-		} else {
129
-			ctx.Format = `container_id: {{.ID}}\nimage: {{.Image}}\ncommand: {{.Command}}\ncreated_at: {{.CreatedAt}}\nstatus: {{.Status}}\nnames: {{.Names}}\nlabels: {{.Labels}}\nports: {{.Ports}}\n`
130
-			if ctx.Size {
131
-				ctx.Format += `size: {{.Size}}\n`
132
-			}
133
-		}
134
-	}
135
-
136
-	ctx.buffer = bytes.NewBufferString("")
137
-	ctx.preformat()
138
-
139
-	tmpl, err := ctx.parseFormat()
140
-	if err != nil {
141
-		return
142
-	}
143
-
144
-	for _, container := range ctx.Containers {
145
-		containerCtx := &containerContext{
146
-			trunc: ctx.Trunc,
147
-			c:     container,
148
-		}
149
-		err = ctx.contextFormat(tmpl, containerCtx)
150
-		if err != nil {
151
-			return
152
-		}
153
-	}
154
-
155
-	ctx.postformat(tmpl, &containerContext{})
156
-}
157
-
158
-func isDangling(image types.Image) bool {
159
-	return len(image.RepoTags) == 1 && image.RepoTags[0] == "<none>:<none>" && len(image.RepoDigests) == 1 && image.RepoDigests[0] == "<none>@<none>"
160
-}
161
-
162
-func (ctx ImageContext) Write() {
163
-	switch ctx.Format {
164
-	case tableFormatKey:
165
-		ctx.Format = defaultImageTableFormat
166
-		if ctx.Digest {
167
-			ctx.Format = defaultImageTableFormatWithDigest
168
-		}
169
-		if ctx.Quiet {
170
-			ctx.Format = defaultQuietFormat
171
-		}
172
-	case rawFormatKey:
173
-		if ctx.Quiet {
174
-			ctx.Format = `image_id: {{.ID}}`
175
-		} else {
176
-			if ctx.Digest {
177
-				ctx.Format = `repository: {{ .Repository }}
178
-tag: {{.Tag}}
179
-digest: {{.Digest}}
180
-image_id: {{.ID}}
181
-created_at: {{.CreatedAt}}
182
-virtual_size: {{.Size}}
183
-`
184
-			} else {
185
-				ctx.Format = `repository: {{ .Repository }}
186
-tag: {{.Tag}}
187
-image_id: {{.ID}}
188
-created_at: {{.CreatedAt}}
189
-virtual_size: {{.Size}}
190
-`
191
-			}
192
-		}
193
-	}
194
-
195
-	ctx.buffer = bytes.NewBufferString("")
196
-	ctx.preformat()
197
-	if ctx.table && ctx.Digest && !strings.Contains(ctx.Format, "{{.Digest}}") {
198
-		ctx.finalFormat += "\t{{.Digest}}"
199
-	}
200
-
201
-	tmpl, err := ctx.parseFormat()
202
-	if err != nil {
203
-		return
204
-	}
205
-
206
-	for _, image := range ctx.Images {
207
-		images := []*imageContext{}
208
-		if isDangling(image) {
209
-			images = append(images, &imageContext{
210
-				trunc:  ctx.Trunc,
211
-				i:      image,
212
-				repo:   "<none>",
213
-				tag:    "<none>",
214
-				digest: "<none>",
215
-			})
216
-		} else {
217
-			repoTags := map[string][]string{}
218
-			repoDigests := map[string][]string{}
219
-
220
-			for _, refString := range append(image.RepoTags) {
221
-				ref, err := reference.ParseNamed(refString)
222
-				if err != nil {
223
-					continue
224
-				}
225
-				if nt, ok := ref.(reference.NamedTagged); ok {
226
-					repoTags[ref.Name()] = append(repoTags[ref.Name()], nt.Tag())
227
-				}
228
-			}
229
-			for _, refString := range append(image.RepoDigests) {
230
-				ref, err := reference.ParseNamed(refString)
231
-				if err != nil {
232
-					continue
233
-				}
234
-				if c, ok := ref.(reference.Canonical); ok {
235
-					repoDigests[ref.Name()] = append(repoDigests[ref.Name()], c.Digest().String())
236
-				}
237
-			}
238
-
239
-			for repo, tags := range repoTags {
240
-				digests := repoDigests[repo]
241
-
242
-				// Do not display digests as their own row
243
-				delete(repoDigests, repo)
244
-
245
-				if !ctx.Digest {
246
-					// Ignore digest references, just show tag once
247
-					digests = nil
248
-				}
249
-
250
-				for _, tag := range tags {
251
-					if len(digests) == 0 {
252
-						images = append(images, &imageContext{
253
-							trunc:  ctx.Trunc,
254
-							i:      image,
255
-							repo:   repo,
256
-							tag:    tag,
257
-							digest: "<none>",
258
-						})
259
-						continue
260
-					}
261
-					// Display the digests for each tag
262
-					for _, dgst := range digests {
263
-						images = append(images, &imageContext{
264
-							trunc:  ctx.Trunc,
265
-							i:      image,
266
-							repo:   repo,
267
-							tag:    tag,
268
-							digest: dgst,
269
-						})
270
-					}
271
-
272
-				}
273
-			}
274
-
275
-			// Show rows for remaining digest only references
276
-			for repo, digests := range repoDigests {
277
-				// If digests are displayed, show row per digest
278
-				if ctx.Digest {
279
-					for _, dgst := range digests {
280
-						images = append(images, &imageContext{
281
-							trunc:  ctx.Trunc,
282
-							i:      image,
283
-							repo:   repo,
284
-							tag:    "<none>",
285
-							digest: dgst,
286
-						})
287
-					}
288
-				} else {
289
-					images = append(images, &imageContext{
290
-						trunc: ctx.Trunc,
291
-						i:     image,
292
-						repo:  repo,
293
-						tag:   "<none>",
294
-					})
295
-				}
296
-			}
297
-		}
298
-		for _, imageCtx := range images {
299
-			err = ctx.contextFormat(tmpl, imageCtx)
300
-			if err != nil {
301
-				return
302
-			}
303
-		}
304
-	}
305
-
306
-	ctx.postformat(tmpl, &imageContext{})
307
-}
308 96
deleted file mode 100644
... ...
@@ -1,537 +0,0 @@
1
-package formatter
2
-
3
-import (
4
-	"bytes"
5
-	"fmt"
6
-	"testing"
7
-	"time"
8
-
9
-	"github.com/docker/engine-api/types"
10
-)
11
-
12
-func TestContainerContextWrite(t *testing.T) {
13
-	unixTime := time.Now().AddDate(0, 0, -1).Unix()
14
-	expectedTime := time.Unix(unixTime, 0).String()
15
-
16
-	contexts := []struct {
17
-		context  ContainerContext
18
-		expected string
19
-	}{
20
-		// Errors
21
-		{
22
-			ContainerContext{
23
-				Context: Context{
24
-					Format: "{{InvalidFunction}}",
25
-				},
26
-			},
27
-			`Template parsing error: template: :1: function "InvalidFunction" not defined
28
-`,
29
-		},
30
-		{
31
-			ContainerContext{
32
-				Context: Context{
33
-					Format: "{{nil}}",
34
-				},
35
-			},
36
-			`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
37
-`,
38
-		},
39
-		// Table Format
40
-		{
41
-			ContainerContext{
42
-				Context: Context{
43
-					Format: "table",
44
-				},
45
-			},
46
-			`CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
47
-containerID1        ubuntu              ""                  24 hours ago                                                foobar_baz
48
-containerID2        ubuntu              ""                  24 hours ago                                                foobar_bar
49
-`,
50
-		},
51
-		{
52
-			ContainerContext{
53
-				Context: Context{
54
-					Format: "table {{.Image}}",
55
-				},
56
-			},
57
-			"IMAGE\nubuntu\nubuntu\n",
58
-		},
59
-		{
60
-			ContainerContext{
61
-				Context: Context{
62
-					Format: "table {{.Image}}",
63
-				},
64
-				Size: true,
65
-			},
66
-			"IMAGE\nubuntu\nubuntu\n",
67
-		},
68
-		{
69
-			ContainerContext{
70
-				Context: Context{
71
-					Format: "table {{.Image}}",
72
-					Quiet:  true,
73
-				},
74
-			},
75
-			"IMAGE\nubuntu\nubuntu\n",
76
-		},
77
-		{
78
-			ContainerContext{
79
-				Context: Context{
80
-					Format: "table",
81
-					Quiet:  true,
82
-				},
83
-			},
84
-			"containerID1\ncontainerID2\n",
85
-		},
86
-		// Raw Format
87
-		{
88
-			ContainerContext{
89
-				Context: Context{
90
-					Format: "raw",
91
-				},
92
-			},
93
-			fmt.Sprintf(`container_id: containerID1
94
-image: ubuntu
95
-command: ""
96
-created_at: %s
97
-status: 
98
-names: foobar_baz
99
-labels: 
100
-ports: 
101
-
102
-container_id: containerID2
103
-image: ubuntu
104
-command: ""
105
-created_at: %s
106
-status: 
107
-names: foobar_bar
108
-labels: 
109
-ports: 
110
-
111
-`, expectedTime, expectedTime),
112
-		},
113
-		{
114
-			ContainerContext{
115
-				Context: Context{
116
-					Format: "raw",
117
-				},
118
-				Size: true,
119
-			},
120
-			fmt.Sprintf(`container_id: containerID1
121
-image: ubuntu
122
-command: ""
123
-created_at: %s
124
-status: 
125
-names: foobar_baz
126
-labels: 
127
-ports: 
128
-size: 0 B
129
-
130
-container_id: containerID2
131
-image: ubuntu
132
-command: ""
133
-created_at: %s
134
-status: 
135
-names: foobar_bar
136
-labels: 
137
-ports: 
138
-size: 0 B
139
-
140
-`, expectedTime, expectedTime),
141
-		},
142
-		{
143
-			ContainerContext{
144
-				Context: Context{
145
-					Format: "raw",
146
-					Quiet:  true,
147
-				},
148
-			},
149
-			"container_id: containerID1\ncontainer_id: containerID2\n",
150
-		},
151
-		// Custom Format
152
-		{
153
-			ContainerContext{
154
-				Context: Context{
155
-					Format: "{{.Image}}",
156
-				},
157
-			},
158
-			"ubuntu\nubuntu\n",
159
-		},
160
-		{
161
-			ContainerContext{
162
-				Context: Context{
163
-					Format: "{{.Image}}",
164
-				},
165
-				Size: true,
166
-			},
167
-			"ubuntu\nubuntu\n",
168
-		},
169
-	}
170
-
171
-	for _, context := range contexts {
172
-		containers := []types.Container{
173
-			{ID: "containerID1", Names: []string{"/foobar_baz"}, Image: "ubuntu", Created: unixTime},
174
-			{ID: "containerID2", Names: []string{"/foobar_bar"}, Image: "ubuntu", Created: unixTime},
175
-		}
176
-		out := bytes.NewBufferString("")
177
-		context.context.Output = out
178
-		context.context.Containers = containers
179
-		context.context.Write()
180
-		actual := out.String()
181
-		if actual != context.expected {
182
-			t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
183
-		}
184
-		// Clean buffer
185
-		out.Reset()
186
-	}
187
-}
188
-
189
-func TestContainerContextWriteWithNoContainers(t *testing.T) {
190
-	out := bytes.NewBufferString("")
191
-	containers := []types.Container{}
192
-
193
-	contexts := []struct {
194
-		context  ContainerContext
195
-		expected string
196
-	}{
197
-		{
198
-			ContainerContext{
199
-				Context: Context{
200
-					Format: "{{.Image}}",
201
-					Output: out,
202
-				},
203
-			},
204
-			"",
205
-		},
206
-		{
207
-			ContainerContext{
208
-				Context: Context{
209
-					Format: "table {{.Image}}",
210
-					Output: out,
211
-				},
212
-			},
213
-			"IMAGE\n",
214
-		},
215
-		{
216
-			ContainerContext{
217
-				Context: Context{
218
-					Format: "{{.Image}}",
219
-					Output: out,
220
-				},
221
-				Size: true,
222
-			},
223
-			"",
224
-		},
225
-		{
226
-			ContainerContext{
227
-				Context: Context{
228
-					Format: "table {{.Image}}",
229
-					Output: out,
230
-				},
231
-				Size: true,
232
-			},
233
-			"IMAGE\n",
234
-		},
235
-		{
236
-			ContainerContext{
237
-				Context: Context{
238
-					Format: "table {{.Image}}\t{{.Size}}",
239
-					Output: out,
240
-				},
241
-			},
242
-			"IMAGE               SIZE\n",
243
-		},
244
-		{
245
-			ContainerContext{
246
-				Context: Context{
247
-					Format: "table {{.Image}}\t{{.Size}}",
248
-					Output: out,
249
-				},
250
-				Size: true,
251
-			},
252
-			"IMAGE               SIZE\n",
253
-		},
254
-	}
255
-
256
-	for _, context := range contexts {
257
-		context.context.Containers = containers
258
-		context.context.Write()
259
-		actual := out.String()
260
-		if actual != context.expected {
261
-			t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
262
-		}
263
-		// Clean buffer
264
-		out.Reset()
265
-	}
266
-}
267
-
268
-func TestImageContextWrite(t *testing.T) {
269
-	unixTime := time.Now().AddDate(0, 0, -1).Unix()
270
-	expectedTime := time.Unix(unixTime, 0).String()
271
-
272
-	contexts := []struct {
273
-		context  ImageContext
274
-		expected string
275
-	}{
276
-		// Errors
277
-		{
278
-			ImageContext{
279
-				Context: Context{
280
-					Format: "{{InvalidFunction}}",
281
-				},
282
-			},
283
-			`Template parsing error: template: :1: function "InvalidFunction" not defined
284
-`,
285
-		},
286
-		{
287
-			ImageContext{
288
-				Context: Context{
289
-					Format: "{{nil}}",
290
-				},
291
-			},
292
-			`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
293
-`,
294
-		},
295
-		// Table Format
296
-		{
297
-			ImageContext{
298
-				Context: Context{
299
-					Format: "table",
300
-				},
301
-			},
302
-			`REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
303
-image               tag1                imageID1            24 hours ago        0 B
304
-image               tag2                imageID2            24 hours ago        0 B
305
-<none>              <none>              imageID3            24 hours ago        0 B
306
-`,
307
-		},
308
-		{
309
-			ImageContext{
310
-				Context: Context{
311
-					Format: "table {{.Repository}}",
312
-				},
313
-			},
314
-			"REPOSITORY\nimage\nimage\n<none>\n",
315
-		},
316
-		{
317
-			ImageContext{
318
-				Context: Context{
319
-					Format: "table {{.Repository}}",
320
-				},
321
-				Digest: true,
322
-			},
323
-			`REPOSITORY          DIGEST
324
-image               sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf
325
-image               <none>
326
-<none>              <none>
327
-`,
328
-		},
329
-		{
330
-			ImageContext{
331
-				Context: Context{
332
-					Format: "table {{.Repository}}",
333
-					Quiet:  true,
334
-				},
335
-			},
336
-			"REPOSITORY\nimage\nimage\n<none>\n",
337
-		},
338
-		{
339
-			ImageContext{
340
-				Context: Context{
341
-					Format: "table",
342
-					Quiet:  true,
343
-				},
344
-			},
345
-			"imageID1\nimageID2\nimageID3\n",
346
-		},
347
-		{
348
-			ImageContext{
349
-				Context: Context{
350
-					Format: "table",
351
-					Quiet:  false,
352
-				},
353
-				Digest: true,
354
-			},
355
-			`REPOSITORY          TAG                 DIGEST                                                                    IMAGE ID            CREATED             SIZE
356
-image               tag1                sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf   imageID1            24 hours ago        0 B
357
-image               tag2                <none>                                                                    imageID2            24 hours ago        0 B
358
-<none>              <none>              <none>                                                                    imageID3            24 hours ago        0 B
359
-`,
360
-		},
361
-		{
362
-			ImageContext{
363
-				Context: Context{
364
-					Format: "table",
365
-					Quiet:  true,
366
-				},
367
-				Digest: true,
368
-			},
369
-			"imageID1\nimageID2\nimageID3\n",
370
-		},
371
-		// Raw Format
372
-		{
373
-			ImageContext{
374
-				Context: Context{
375
-					Format: "raw",
376
-				},
377
-			},
378
-			fmt.Sprintf(`repository: image
379
-tag: tag1
380
-image_id: imageID1
381
-created_at: %s
382
-virtual_size: 0 B
383
-
384
-repository: image
385
-tag: tag2
386
-image_id: imageID2
387
-created_at: %s
388
-virtual_size: 0 B
389
-
390
-repository: <none>
391
-tag: <none>
392
-image_id: imageID3
393
-created_at: %s
394
-virtual_size: 0 B
395
-
396
-`, expectedTime, expectedTime, expectedTime),
397
-		},
398
-		{
399
-			ImageContext{
400
-				Context: Context{
401
-					Format: "raw",
402
-				},
403
-				Digest: true,
404
-			},
405
-			fmt.Sprintf(`repository: image
406
-tag: tag1
407
-digest: sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf
408
-image_id: imageID1
409
-created_at: %s
410
-virtual_size: 0 B
411
-
412
-repository: image
413
-tag: tag2
414
-digest: <none>
415
-image_id: imageID2
416
-created_at: %s
417
-virtual_size: 0 B
418
-
419
-repository: <none>
420
-tag: <none>
421
-digest: <none>
422
-image_id: imageID3
423
-created_at: %s
424
-virtual_size: 0 B
425
-
426
-`, expectedTime, expectedTime, expectedTime),
427
-		},
428
-		{
429
-			ImageContext{
430
-				Context: Context{
431
-					Format: "raw",
432
-					Quiet:  true,
433
-				},
434
-			},
435
-			`image_id: imageID1
436
-image_id: imageID2
437
-image_id: imageID3
438
-`,
439
-		},
440
-		// Custom Format
441
-		{
442
-			ImageContext{
443
-				Context: Context{
444
-					Format: "{{.Repository}}",
445
-				},
446
-			},
447
-			"image\nimage\n<none>\n",
448
-		},
449
-		{
450
-			ImageContext{
451
-				Context: Context{
452
-					Format: "{{.Repository}}",
453
-				},
454
-				Digest: true,
455
-			},
456
-			"image\nimage\n<none>\n",
457
-		},
458
-	}
459
-
460
-	for _, context := range contexts {
461
-		images := []types.Image{
462
-			{ID: "imageID1", RepoTags: []string{"image:tag1"}, RepoDigests: []string{"image@sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf"}, Created: unixTime},
463
-			{ID: "imageID2", RepoTags: []string{"image:tag2"}, Created: unixTime},
464
-			{ID: "imageID3", RepoTags: []string{"<none>:<none>"}, RepoDigests: []string{"<none>@<none>"}, Created: unixTime},
465
-		}
466
-		out := bytes.NewBufferString("")
467
-		context.context.Output = out
468
-		context.context.Images = images
469
-		context.context.Write()
470
-		actual := out.String()
471
-		if actual != context.expected {
472
-			t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
473
-		}
474
-		// Clean buffer
475
-		out.Reset()
476
-	}
477
-}
478
-
479
-func TestImageContextWriteWithNoImage(t *testing.T) {
480
-	out := bytes.NewBufferString("")
481
-	images := []types.Image{}
482
-
483
-	contexts := []struct {
484
-		context  ImageContext
485
-		expected string
486
-	}{
487
-		{
488
-			ImageContext{
489
-				Context: Context{
490
-					Format: "{{.Repository}}",
491
-					Output: out,
492
-				},
493
-			},
494
-			"",
495
-		},
496
-		{
497
-			ImageContext{
498
-				Context: Context{
499
-					Format: "table {{.Repository}}",
500
-					Output: out,
501
-				},
502
-			},
503
-			"REPOSITORY\n",
504
-		},
505
-		{
506
-			ImageContext{
507
-				Context: Context{
508
-					Format: "{{.Repository}}",
509
-					Output: out,
510
-				},
511
-				Digest: true,
512
-			},
513
-			"",
514
-		},
515
-		{
516
-			ImageContext{
517
-				Context: Context{
518
-					Format: "table {{.Repository}}",
519
-					Output: out,
520
-				},
521
-				Digest: true,
522
-			},
523
-			"REPOSITORY          DIGEST\n",
524
-		},
525
-	}
526
-
527
-	for _, context := range contexts {
528
-		context.context.Images = images
529
-		context.context.Write()
530
-		actual := out.String()
531
-		if actual != context.expected {
532
-			t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
533
-		}
534
-		// Clean buffer
535
-		out.Reset()
536
-	}
537
-}
538 1
new file mode 100644
... ...
@@ -0,0 +1,229 @@
0
+package formatter
1
+
2
+import (
3
+	"bytes"
4
+	"strings"
5
+	"time"
6
+
7
+	"github.com/docker/docker/pkg/stringid"
8
+	"github.com/docker/docker/reference"
9
+	"github.com/docker/engine-api/types"
10
+	"github.com/docker/go-units"
11
+)
12
+
13
+const (
14
+	defaultImageTableFormat           = "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedSince}} ago\t{{.Size}}"
15
+	defaultImageTableFormatWithDigest = "table {{.Repository}}\t{{.Tag}}\t{{.Digest}}\t{{.ID}}\t{{.CreatedSince}} ago\t{{.Size}}"
16
+
17
+	imageIDHeader    = "IMAGE ID"
18
+	repositoryHeader = "REPOSITORY"
19
+	tagHeader        = "TAG"
20
+	digestHeader     = "DIGEST"
21
+)
22
+
23
+// ImageContext contains image specific information required by the formater, encapsulate a Context struct.
24
+type ImageContext struct {
25
+	Context
26
+	Digest bool
27
+	// Images
28
+	Images []types.Image
29
+}
30
+
31
+func isDangling(image types.Image) bool {
32
+	return len(image.RepoTags) == 1 && image.RepoTags[0] == "<none>:<none>" && len(image.RepoDigests) == 1 && image.RepoDigests[0] == "<none>@<none>"
33
+}
34
+
35
+func (ctx ImageContext) Write() {
36
+	switch ctx.Format {
37
+	case tableFormatKey:
38
+		ctx.Format = defaultImageTableFormat
39
+		if ctx.Digest {
40
+			ctx.Format = defaultImageTableFormatWithDigest
41
+		}
42
+		if ctx.Quiet {
43
+			ctx.Format = defaultQuietFormat
44
+		}
45
+	case rawFormatKey:
46
+		if ctx.Quiet {
47
+			ctx.Format = `image_id: {{.ID}}`
48
+		} else {
49
+			if ctx.Digest {
50
+				ctx.Format = `repository: {{ .Repository }}
51
+tag: {{.Tag}}
52
+digest: {{.Digest}}
53
+image_id: {{.ID}}
54
+created_at: {{.CreatedAt}}
55
+virtual_size: {{.Size}}
56
+`
57
+			} else {
58
+				ctx.Format = `repository: {{ .Repository }}
59
+tag: {{.Tag}}
60
+image_id: {{.ID}}
61
+created_at: {{.CreatedAt}}
62
+virtual_size: {{.Size}}
63
+`
64
+			}
65
+		}
66
+	}
67
+
68
+	ctx.buffer = bytes.NewBufferString("")
69
+	ctx.preformat()
70
+	if ctx.table && ctx.Digest && !strings.Contains(ctx.Format, "{{.Digest}}") {
71
+		ctx.finalFormat += "\t{{.Digest}}"
72
+	}
73
+
74
+	tmpl, err := ctx.parseFormat()
75
+	if err != nil {
76
+		return
77
+	}
78
+
79
+	for _, image := range ctx.Images {
80
+		images := []*imageContext{}
81
+		if isDangling(image) {
82
+			images = append(images, &imageContext{
83
+				trunc:  ctx.Trunc,
84
+				i:      image,
85
+				repo:   "<none>",
86
+				tag:    "<none>",
87
+				digest: "<none>",
88
+			})
89
+		} else {
90
+			repoTags := map[string][]string{}
91
+			repoDigests := map[string][]string{}
92
+
93
+			for _, refString := range append(image.RepoTags) {
94
+				ref, err := reference.ParseNamed(refString)
95
+				if err != nil {
96
+					continue
97
+				}
98
+				if nt, ok := ref.(reference.NamedTagged); ok {
99
+					repoTags[ref.Name()] = append(repoTags[ref.Name()], nt.Tag())
100
+				}
101
+			}
102
+			for _, refString := range append(image.RepoDigests) {
103
+				ref, err := reference.ParseNamed(refString)
104
+				if err != nil {
105
+					continue
106
+				}
107
+				if c, ok := ref.(reference.Canonical); ok {
108
+					repoDigests[ref.Name()] = append(repoDigests[ref.Name()], c.Digest().String())
109
+				}
110
+			}
111
+
112
+			for repo, tags := range repoTags {
113
+				digests := repoDigests[repo]
114
+
115
+				// Do not display digests as their own row
116
+				delete(repoDigests, repo)
117
+
118
+				if !ctx.Digest {
119
+					// Ignore digest references, just show tag once
120
+					digests = nil
121
+				}
122
+
123
+				for _, tag := range tags {
124
+					if len(digests) == 0 {
125
+						images = append(images, &imageContext{
126
+							trunc:  ctx.Trunc,
127
+							i:      image,
128
+							repo:   repo,
129
+							tag:    tag,
130
+							digest: "<none>",
131
+						})
132
+						continue
133
+					}
134
+					// Display the digests for each tag
135
+					for _, dgst := range digests {
136
+						images = append(images, &imageContext{
137
+							trunc:  ctx.Trunc,
138
+							i:      image,
139
+							repo:   repo,
140
+							tag:    tag,
141
+							digest: dgst,
142
+						})
143
+					}
144
+
145
+				}
146
+			}
147
+
148
+			// Show rows for remaining digest only references
149
+			for repo, digests := range repoDigests {
150
+				// If digests are displayed, show row per digest
151
+				if ctx.Digest {
152
+					for _, dgst := range digests {
153
+						images = append(images, &imageContext{
154
+							trunc:  ctx.Trunc,
155
+							i:      image,
156
+							repo:   repo,
157
+							tag:    "<none>",
158
+							digest: dgst,
159
+						})
160
+					}
161
+				} else {
162
+					images = append(images, &imageContext{
163
+						trunc: ctx.Trunc,
164
+						i:     image,
165
+						repo:  repo,
166
+						tag:   "<none>",
167
+					})
168
+				}
169
+			}
170
+		}
171
+		for _, imageCtx := range images {
172
+			err = ctx.contextFormat(tmpl, imageCtx)
173
+			if err != nil {
174
+				return
175
+			}
176
+		}
177
+	}
178
+
179
+	ctx.postformat(tmpl, &imageContext{})
180
+}
181
+
182
+type imageContext struct {
183
+	baseSubContext
184
+	trunc  bool
185
+	i      types.Image
186
+	repo   string
187
+	tag    string
188
+	digest string
189
+}
190
+
191
+func (c *imageContext) ID() string {
192
+	c.addHeader(imageIDHeader)
193
+	if c.trunc {
194
+		return stringid.TruncateID(c.i.ID)
195
+	}
196
+	return c.i.ID
197
+}
198
+
199
+func (c *imageContext) Repository() string {
200
+	c.addHeader(repositoryHeader)
201
+	return c.repo
202
+}
203
+
204
+func (c *imageContext) Tag() string {
205
+	c.addHeader(tagHeader)
206
+	return c.tag
207
+}
208
+
209
+func (c *imageContext) Digest() string {
210
+	c.addHeader(digestHeader)
211
+	return c.digest
212
+}
213
+
214
+func (c *imageContext) CreatedSince() string {
215
+	c.addHeader(createdSinceHeader)
216
+	createdAt := time.Unix(int64(c.i.Created), 0)
217
+	return units.HumanDuration(time.Now().UTC().Sub(createdAt))
218
+}
219
+
220
+func (c *imageContext) CreatedAt() string {
221
+	c.addHeader(createdAtHeader)
222
+	return time.Unix(int64(c.i.Created), 0).String()
223
+}
224
+
225
+func (c *imageContext) Size() string {
226
+	c.addHeader(sizeHeader)
227
+	return units.HumanSize(float64(c.i.Size))
228
+}
0 229
new file mode 100644
... ...
@@ -0,0 +1,345 @@
0
+package formatter
1
+
2
+import (
3
+	"bytes"
4
+	"fmt"
5
+	"strings"
6
+	"testing"
7
+	"time"
8
+
9
+	"github.com/docker/docker/pkg/stringid"
10
+	"github.com/docker/engine-api/types"
11
+)
12
+
13
+func TestImageContext(t *testing.T) {
14
+	imageID := stringid.GenerateRandomID()
15
+	unix := time.Now().Unix()
16
+
17
+	var ctx imageContext
18
+	cases := []struct {
19
+		imageCtx  imageContext
20
+		expValue  string
21
+		expHeader string
22
+		call      func() string
23
+	}{
24
+		{imageContext{
25
+			i:     types.Image{ID: imageID},
26
+			trunc: true,
27
+		}, stringid.TruncateID(imageID), imageIDHeader, ctx.ID},
28
+		{imageContext{
29
+			i:     types.Image{ID: imageID},
30
+			trunc: false,
31
+		}, imageID, imageIDHeader, ctx.ID},
32
+		{imageContext{
33
+			i:     types.Image{Size: 10},
34
+			trunc: true,
35
+		}, "10 B", sizeHeader, ctx.Size},
36
+		{imageContext{
37
+			i:     types.Image{Created: unix},
38
+			trunc: true,
39
+		}, time.Unix(unix, 0).String(), createdAtHeader, ctx.CreatedAt},
40
+		// FIXME
41
+		// {imageContext{
42
+		// 	i:     types.Image{Created: unix},
43
+		// 	trunc: true,
44
+		// }, units.HumanDuration(time.Unix(unix, 0)), createdSinceHeader, ctx.CreatedSince},
45
+		{imageContext{
46
+			i:    types.Image{},
47
+			repo: "busybox",
48
+		}, "busybox", repositoryHeader, ctx.Repository},
49
+		{imageContext{
50
+			i:   types.Image{},
51
+			tag: "latest",
52
+		}, "latest", tagHeader, ctx.Tag},
53
+		{imageContext{
54
+			i:      types.Image{},
55
+			digest: "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a",
56
+		}, "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a", digestHeader, ctx.Digest},
57
+	}
58
+
59
+	for _, c := range cases {
60
+		ctx = c.imageCtx
61
+		v := c.call()
62
+		if strings.Contains(v, ",") {
63
+			compareMultipleValues(t, v, c.expValue)
64
+		} else if v != c.expValue {
65
+			t.Fatalf("Expected %s, was %s\n", c.expValue, v)
66
+		}
67
+
68
+		h := ctx.fullHeader()
69
+		if h != c.expHeader {
70
+			t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
71
+		}
72
+	}
73
+}
74
+
75
+func TestImageContextWrite(t *testing.T) {
76
+	unixTime := time.Now().AddDate(0, 0, -1).Unix()
77
+	expectedTime := time.Unix(unixTime, 0).String()
78
+
79
+	contexts := []struct {
80
+		context  ImageContext
81
+		expected string
82
+	}{
83
+		// Errors
84
+		{
85
+			ImageContext{
86
+				Context: Context{
87
+					Format: "{{InvalidFunction}}",
88
+				},
89
+			},
90
+			`Template parsing error: template: :1: function "InvalidFunction" not defined
91
+`,
92
+		},
93
+		{
94
+			ImageContext{
95
+				Context: Context{
96
+					Format: "{{nil}}",
97
+				},
98
+			},
99
+			`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
100
+`,
101
+		},
102
+		// Table Format
103
+		{
104
+			ImageContext{
105
+				Context: Context{
106
+					Format: "table",
107
+				},
108
+			},
109
+			`REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
110
+image               tag1                imageID1            24 hours ago        0 B
111
+image               tag2                imageID2            24 hours ago        0 B
112
+<none>              <none>              imageID3            24 hours ago        0 B
113
+`,
114
+		},
115
+		{
116
+			ImageContext{
117
+				Context: Context{
118
+					Format: "table {{.Repository}}",
119
+				},
120
+			},
121
+			"REPOSITORY\nimage\nimage\n<none>\n",
122
+		},
123
+		{
124
+			ImageContext{
125
+				Context: Context{
126
+					Format: "table {{.Repository}}",
127
+				},
128
+				Digest: true,
129
+			},
130
+			`REPOSITORY          DIGEST
131
+image               sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf
132
+image               <none>
133
+<none>              <none>
134
+`,
135
+		},
136
+		{
137
+			ImageContext{
138
+				Context: Context{
139
+					Format: "table {{.Repository}}",
140
+					Quiet:  true,
141
+				},
142
+			},
143
+			"REPOSITORY\nimage\nimage\n<none>\n",
144
+		},
145
+		{
146
+			ImageContext{
147
+				Context: Context{
148
+					Format: "table",
149
+					Quiet:  true,
150
+				},
151
+			},
152
+			"imageID1\nimageID2\nimageID3\n",
153
+		},
154
+		{
155
+			ImageContext{
156
+				Context: Context{
157
+					Format: "table",
158
+					Quiet:  false,
159
+				},
160
+				Digest: true,
161
+			},
162
+			`REPOSITORY          TAG                 DIGEST                                                                    IMAGE ID            CREATED             SIZE
163
+image               tag1                sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf   imageID1            24 hours ago        0 B
164
+image               tag2                <none>                                                                    imageID2            24 hours ago        0 B
165
+<none>              <none>              <none>                                                                    imageID3            24 hours ago        0 B
166
+`,
167
+		},
168
+		{
169
+			ImageContext{
170
+				Context: Context{
171
+					Format: "table",
172
+					Quiet:  true,
173
+				},
174
+				Digest: true,
175
+			},
176
+			"imageID1\nimageID2\nimageID3\n",
177
+		},
178
+		// Raw Format
179
+		{
180
+			ImageContext{
181
+				Context: Context{
182
+					Format: "raw",
183
+				},
184
+			},
185
+			fmt.Sprintf(`repository: image
186
+tag: tag1
187
+image_id: imageID1
188
+created_at: %s
189
+virtual_size: 0 B
190
+
191
+repository: image
192
+tag: tag2
193
+image_id: imageID2
194
+created_at: %s
195
+virtual_size: 0 B
196
+
197
+repository: <none>
198
+tag: <none>
199
+image_id: imageID3
200
+created_at: %s
201
+virtual_size: 0 B
202
+
203
+`, expectedTime, expectedTime, expectedTime),
204
+		},
205
+		{
206
+			ImageContext{
207
+				Context: Context{
208
+					Format: "raw",
209
+				},
210
+				Digest: true,
211
+			},
212
+			fmt.Sprintf(`repository: image
213
+tag: tag1
214
+digest: sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf
215
+image_id: imageID1
216
+created_at: %s
217
+virtual_size: 0 B
218
+
219
+repository: image
220
+tag: tag2
221
+digest: <none>
222
+image_id: imageID2
223
+created_at: %s
224
+virtual_size: 0 B
225
+
226
+repository: <none>
227
+tag: <none>
228
+digest: <none>
229
+image_id: imageID3
230
+created_at: %s
231
+virtual_size: 0 B
232
+
233
+`, expectedTime, expectedTime, expectedTime),
234
+		},
235
+		{
236
+			ImageContext{
237
+				Context: Context{
238
+					Format: "raw",
239
+					Quiet:  true,
240
+				},
241
+			},
242
+			`image_id: imageID1
243
+image_id: imageID2
244
+image_id: imageID3
245
+`,
246
+		},
247
+		// Custom Format
248
+		{
249
+			ImageContext{
250
+				Context: Context{
251
+					Format: "{{.Repository}}",
252
+				},
253
+			},
254
+			"image\nimage\n<none>\n",
255
+		},
256
+		{
257
+			ImageContext{
258
+				Context: Context{
259
+					Format: "{{.Repository}}",
260
+				},
261
+				Digest: true,
262
+			},
263
+			"image\nimage\n<none>\n",
264
+		},
265
+	}
266
+
267
+	for _, context := range contexts {
268
+		images := []types.Image{
269
+			{ID: "imageID1", RepoTags: []string{"image:tag1"}, RepoDigests: []string{"image@sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf"}, Created: unixTime},
270
+			{ID: "imageID2", RepoTags: []string{"image:tag2"}, Created: unixTime},
271
+			{ID: "imageID3", RepoTags: []string{"<none>:<none>"}, RepoDigests: []string{"<none>@<none>"}, Created: unixTime},
272
+		}
273
+		out := bytes.NewBufferString("")
274
+		context.context.Output = out
275
+		context.context.Images = images
276
+		context.context.Write()
277
+		actual := out.String()
278
+		if actual != context.expected {
279
+			t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
280
+		}
281
+		// Clean buffer
282
+		out.Reset()
283
+	}
284
+}
285
+
286
+func TestImageContextWriteWithNoImage(t *testing.T) {
287
+	out := bytes.NewBufferString("")
288
+	images := []types.Image{}
289
+
290
+	contexts := []struct {
291
+		context  ImageContext
292
+		expected string
293
+	}{
294
+		{
295
+			ImageContext{
296
+				Context: Context{
297
+					Format: "{{.Repository}}",
298
+					Output: out,
299
+				},
300
+			},
301
+			"",
302
+		},
303
+		{
304
+			ImageContext{
305
+				Context: Context{
306
+					Format: "table {{.Repository}}",
307
+					Output: out,
308
+				},
309
+			},
310
+			"REPOSITORY\n",
311
+		},
312
+		{
313
+			ImageContext{
314
+				Context: Context{
315
+					Format: "{{.Repository}}",
316
+					Output: out,
317
+				},
318
+				Digest: true,
319
+			},
320
+			"",
321
+		},
322
+		{
323
+			ImageContext{
324
+				Context: Context{
325
+					Format: "table {{.Repository}}",
326
+					Output: out,
327
+				},
328
+				Digest: true,
329
+			},
330
+			"REPOSITORY          DIGEST\n",
331
+		},
332
+	}
333
+
334
+	for _, context := range contexts {
335
+		context.context.Images = images
336
+		context.context.Write()
337
+		actual := out.String()
338
+		if actual != context.expected {
339
+			t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
340
+		}
341
+		// Clean buffer
342
+		out.Reset()
343
+	}
344
+}