Browse code

Allow `--format` to use different delim in `table` format

This fix is an attempt to address
https://github.com/docker/docker/pull/28213#issuecomment-273840405

Currently when specify table format with table `--format "table {{.ID}}..."`,
the delimiter in the header section of the table is always `"\t"`.
That is actually different from the content of the table as the delimiter
could be anything (or even contatenated with `.`, for example):
```
$ docker service ps web --format 'table {{.Name}}.{{.ID}}' --no-trunc

NAME ID
web.1.inyhxhvjcijl0hdbu8lgrwwh7
\_ web.1.p9m4kx2srjqmfms4igam0uqlb
```

This fix is an attampt to address the skewness of the table when delimiter
is not `"\t"`.

The basic idea is that, when header consists of `table` key, the header section
will be redendered the same way as content section. A map mapping each
placeholder name to the HEADER entry name is used for the context of the header.

Unit tests have been updated and added to cover the changes.

This fix is related to #28313.

Signed-off-by: Yong Tang <yong.tang.github@outlook.com>

Yong Tang authored on 2017/02/04 09:48:46
Showing 17 changed files
... ...
@@ -14,7 +14,7 @@ import (
14 14
 )
15 15
 
16 16
 const (
17
-	defaultContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.RunningFor}} ago\t{{.Status}}\t{{.Ports}}\t{{.Names}}"
17
+	defaultContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.RunningFor}}\t{{.Status}}\t{{.Ports}}\t{{.Names}}"
18 18
 
19 19
 	containerIDHeader = "CONTAINER ID"
20 20
 	namesHeader       = "NAMES"
... ...
@@ -71,7 +71,17 @@ func ContainerWrite(ctx Context, containers []types.Container) error {
71 71
 		}
72 72
 		return nil
73 73
 	}
74
-	return ctx.Write(&containerContext{}, render)
74
+	return ctx.Write(newContainerContext(), render)
75
+}
76
+
77
+type containerHeaderContext map[string]string
78
+
79
+func (c containerHeaderContext) Label(name string) string {
80
+	n := strings.Split(name, ".")
81
+	r := strings.NewReplacer("-", " ", "_", " ")
82
+	h := r.Replace(n[len(n)-1])
83
+
84
+	return h
75 85
 }
76 86
 
77 87
 type containerContext struct {
... ...
@@ -80,12 +90,31 @@ type containerContext struct {
80 80
 	c     types.Container
81 81
 }
82 82
 
83
+func newContainerContext() *containerContext {
84
+	containerCtx := containerContext{}
85
+	containerCtx.header = containerHeaderContext{
86
+		"ID":           containerIDHeader,
87
+		"Names":        namesHeader,
88
+		"Image":        imageHeader,
89
+		"Command":      commandHeader,
90
+		"CreatedAt":    createdAtHeader,
91
+		"RunningFor":   runningForHeader,
92
+		"Ports":        portsHeader,
93
+		"Status":       statusHeader,
94
+		"Size":         sizeHeader,
95
+		"Labels":       labelsHeader,
96
+		"Mounts":       mountsHeader,
97
+		"LocalVolumes": localVolumes,
98
+		"Networks":     networksHeader,
99
+	}
100
+	return &containerCtx
101
+}
102
+
83 103
 func (c *containerContext) MarshalJSON() ([]byte, error) {
84 104
 	return marshalJSON(c)
85 105
 }
86 106
 
87 107
 func (c *containerContext) ID() string {
88
-	c.AddHeader(containerIDHeader)
89 108
 	if c.trunc {
90 109
 		return stringid.TruncateID(c.c.ID)
91 110
 	}
... ...
@@ -93,7 +122,6 @@ func (c *containerContext) ID() string {
93 93
 }
94 94
 
95 95
 func (c *containerContext) Names() string {
96
-	c.AddHeader(namesHeader)
97 96
 	names := stripNamePrefix(c.c.Names)
98 97
 	if c.trunc {
99 98
 		for _, name := range names {
... ...
@@ -107,7 +135,6 @@ func (c *containerContext) Names() string {
107 107
 }
108 108
 
109 109
 func (c *containerContext) Image() string {
110
-	c.AddHeader(imageHeader)
111 110
 	if c.c.Image == "" {
112 111
 		return "<no image>"
113 112
 	}
... ...
@@ -120,7 +147,6 @@ func (c *containerContext) Image() string {
120 120
 }
121 121
 
122 122
 func (c *containerContext) Command() string {
123
-	c.AddHeader(commandHeader)
124 123
 	command := c.c.Command
125 124
 	if c.trunc {
126 125
 		command = stringutils.Ellipsis(command, 20)
... ...
@@ -129,28 +155,23 @@ func (c *containerContext) Command() string {
129 129
 }
130 130
 
131 131
 func (c *containerContext) CreatedAt() string {
132
-	c.AddHeader(createdAtHeader)
133 132
 	return time.Unix(int64(c.c.Created), 0).String()
134 133
 }
135 134
 
136 135
 func (c *containerContext) RunningFor() string {
137
-	c.AddHeader(runningForHeader)
138 136
 	createdAt := time.Unix(int64(c.c.Created), 0)
139
-	return units.HumanDuration(time.Now().UTC().Sub(createdAt))
137
+	return units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago"
140 138
 }
141 139
 
142 140
 func (c *containerContext) Ports() string {
143
-	c.AddHeader(portsHeader)
144 141
 	return api.DisplayablePorts(c.c.Ports)
145 142
 }
146 143
 
147 144
 func (c *containerContext) Status() string {
148
-	c.AddHeader(statusHeader)
149 145
 	return c.c.Status
150 146
 }
151 147
 
152 148
 func (c *containerContext) Size() string {
153
-	c.AddHeader(sizeHeader)
154 149
 	srw := units.HumanSizeWithPrecision(float64(c.c.SizeRw), 3)
155 150
 	sv := units.HumanSizeWithPrecision(float64(c.c.SizeRootFs), 3)
156 151
 
... ...
@@ -162,7 +183,6 @@ func (c *containerContext) Size() string {
162 162
 }
163 163
 
164 164
 func (c *containerContext) Labels() string {
165
-	c.AddHeader(labelsHeader)
166 165
 	if c.c.Labels == nil {
167 166
 		return ""
168 167
 	}
... ...
@@ -175,12 +195,6 @@ func (c *containerContext) Labels() string {
175 175
 }
176 176
 
177 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 178
 	if c.c.Labels == nil {
185 179
 		return ""
186 180
 	}
... ...
@@ -188,8 +202,6 @@ func (c *containerContext) Label(name string) string {
188 188
 }
189 189
 
190 190
 func (c *containerContext) Mounts() string {
191
-	c.AddHeader(mountsHeader)
192
-
193 191
 	var name string
194 192
 	var mounts []string
195 193
 	for _, m := range c.c.Mounts {
... ...
@@ -207,8 +219,6 @@ func (c *containerContext) Mounts() string {
207 207
 }
208 208
 
209 209
 func (c *containerContext) LocalVolumes() string {
210
-	c.AddHeader(localVolumes)
211
-
212 210
 	count := 0
213 211
 	for _, m := range c.c.Mounts {
214 212
 		if m.Driver == "local" {
... ...
@@ -220,8 +230,6 @@ func (c *containerContext) LocalVolumes() string {
220 220
 }
221 221
 
222 222
 func (c *containerContext) Networks() string {
223
-	c.AddHeader(networksHeader)
224
-
225 223
 	if c.c.NetworkSettings == nil {
226 224
 		return ""
227 225
 	}
... ...
@@ -22,22 +22,20 @@ func TestContainerPsContext(t *testing.T) {
22 22
 		container types.Container
23 23
 		trunc     bool
24 24
 		expValue  string
25
-		expHeader string
26 25
 		call      func() string
27 26
 	}{
28
-		{types.Container{ID: containerID}, true, stringid.TruncateID(containerID), containerIDHeader, ctx.ID},
29
-		{types.Container{ID: containerID}, false, containerID, containerIDHeader, ctx.ID},
30
-		{types.Container{Names: []string{"/foobar_baz"}}, true, "foobar_baz", namesHeader, ctx.Names},
31
-		{types.Container{Image: "ubuntu"}, true, "ubuntu", imageHeader, ctx.Image},
32
-		{types.Container{Image: "verylongimagename"}, true, "verylongimagename", imageHeader, ctx.Image},
33
-		{types.Container{Image: "verylongimagename"}, false, "verylongimagename", imageHeader, ctx.Image},
27
+		{types.Container{ID: containerID}, true, stringid.TruncateID(containerID), ctx.ID},
28
+		{types.Container{ID: containerID}, false, containerID, ctx.ID},
29
+		{types.Container{Names: []string{"/foobar_baz"}}, true, "foobar_baz", ctx.Names},
30
+		{types.Container{Image: "ubuntu"}, true, "ubuntu", ctx.Image},
31
+		{types.Container{Image: "verylongimagename"}, true, "verylongimagename", ctx.Image},
32
+		{types.Container{Image: "verylongimagename"}, false, "verylongimagename", ctx.Image},
34 33
 		{types.Container{
35 34
 			Image:   "a5a665ff33eced1e0803148700880edab4",
36 35
 			ImageID: "a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5",
37 36
 		},
38 37
 			true,
39 38
 			"a5a665ff33ec",
40
-			imageHeader,
41 39
 			ctx.Image,
42 40
 		},
43 41
 		{types.Container{
... ...
@@ -46,19 +44,18 @@ func TestContainerPsContext(t *testing.T) {
46 46
 		},
47 47
 			false,
48 48
 			"a5a665ff33eced1e0803148700880edab4",
49
-			imageHeader,
50 49
 			ctx.Image,
51 50
 		},
52
-		{types.Container{Image: ""}, true, "<no image>", imageHeader, ctx.Image},
53
-		{types.Container{Command: "sh -c 'ls -la'"}, true, `"sh -c 'ls -la'"`, commandHeader, ctx.Command},
54
-		{types.Container{Created: unix}, true, time.Unix(unix, 0).String(), createdAtHeader, ctx.CreatedAt},
55
-		{types.Container{Ports: []types.Port{{PrivatePort: 8080, PublicPort: 8080, Type: "tcp"}}}, true, "8080/tcp", portsHeader, ctx.Ports},
56
-		{types.Container{Status: "RUNNING"}, true, "RUNNING", statusHeader, ctx.Status},
57
-		{types.Container{SizeRw: 10}, true, "10B", sizeHeader, ctx.Size},
58
-		{types.Container{SizeRw: 10, SizeRootFs: 20}, true, "10B (virtual 20B)", sizeHeader, ctx.Size},
59
-		{types.Container{}, true, "", labelsHeader, ctx.Labels},
60
-		{types.Container{Labels: map[string]string{"cpu": "6", "storage": "ssd"}}, true, "cpu=6,storage=ssd", labelsHeader, ctx.Labels},
61
-		{types.Container{Created: unix}, true, "About a minute", runningForHeader, ctx.RunningFor},
51
+		{types.Container{Image: ""}, true, "<no image>", ctx.Image},
52
+		{types.Container{Command: "sh -c 'ls -la'"}, true, `"sh -c 'ls -la'"`, ctx.Command},
53
+		{types.Container{Created: unix}, true, time.Unix(unix, 0).String(), ctx.CreatedAt},
54
+		{types.Container{Ports: []types.Port{{PrivatePort: 8080, PublicPort: 8080, Type: "tcp"}}}, true, "8080/tcp", ctx.Ports},
55
+		{types.Container{Status: "RUNNING"}, true, "RUNNING", ctx.Status},
56
+		{types.Container{SizeRw: 10}, true, "10B", ctx.Size},
57
+		{types.Container{SizeRw: 10, SizeRootFs: 20}, true, "10B (virtual 20B)", ctx.Size},
58
+		{types.Container{}, true, "", ctx.Labels},
59
+		{types.Container{Labels: map[string]string{"cpu": "6", "storage": "ssd"}}, true, "cpu=6,storage=ssd", ctx.Labels},
60
+		{types.Container{Created: unix}, true, "About a minute ago", ctx.RunningFor},
62 61
 		{types.Container{
63 62
 			Mounts: []types.MountPoint{
64 63
 				{
... ...
@@ -67,7 +64,7 @@ func TestContainerPsContext(t *testing.T) {
67 67
 					Source: "/a/path",
68 68
 				},
69 69
 			},
70
-		}, true, "this-is-a-lo...", mountsHeader, ctx.Mounts},
70
+		}, true, "this-is-a-lo...", ctx.Mounts},
71 71
 		{types.Container{
72 72
 			Mounts: []types.MountPoint{
73 73
 				{
... ...
@@ -75,7 +72,7 @@ func TestContainerPsContext(t *testing.T) {
75 75
 					Source: "/a/path",
76 76
 				},
77 77
 			},
78
-		}, false, "/a/path", mountsHeader, ctx.Mounts},
78
+		}, false, "/a/path", ctx.Mounts},
79 79
 		{types.Container{
80 80
 			Mounts: []types.MountPoint{
81 81
 				{
... ...
@@ -84,7 +81,7 @@ func TestContainerPsContext(t *testing.T) {
84 84
 					Source: "/a/path",
85 85
 				},
86 86
 			},
87
-		}, false, "733908409c91817de8e92b0096373245f329f19a88e2c849f02460e9b3d1c203", mountsHeader, ctx.Mounts},
87
+		}, false, "733908409c91817de8e92b0096373245f329f19a88e2c849f02460e9b3d1c203", ctx.Mounts},
88 88
 	}
89 89
 
90 90
 	for _, c := range cases {
... ...
@@ -95,11 +92,6 @@ func TestContainerPsContext(t *testing.T) {
95 95
 		} else if v != c.expValue {
96 96
 			t.Fatalf("Expected %s, was %s\n", c.expValue, v)
97 97
 		}
98
-
99
-		h := ctx.FullHeader()
100
-		if h != c.expHeader {
101
-			t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
102
-		}
103 98
 	}
104 99
 
105 100
 	c1 := types.Container{Labels: map[string]string{"com.docker.swarm.swarm-id": "33", "com.docker.swarm.node_name": "ubuntu"}}
... ...
@@ -115,12 +107,6 @@ func TestContainerPsContext(t *testing.T) {
115 115
 		t.Fatalf("Expected ubuntu, was %s\n", node)
116 116
 	}
117 117
 
118
-	h := ctx.FullHeader()
119
-	if h != "SWARM ID\tNODE NAME" {
120
-		t.Fatalf("Expected %s, was %s\n", "SWARM ID\tNODE NAME", h)
121
-
122
-	}
123
-
124 118
 	c2 := types.Container{}
125 119
 	ctx = containerContext{c: c2, trunc: true}
126 120
 
... ...
@@ -128,13 +114,6 @@ func TestContainerPsContext(t *testing.T) {
128 128
 	if label != "" {
129 129
 		t.Fatalf("Expected an empty string, was %s", label)
130 130
 	}
131
-
132
-	ctx = containerContext{c: c2, trunc: true}
133
-	FullHeader := ctx.FullHeader()
134
-	if FullHeader != "" {
135
-		t.Fatalf("Expected FullHeader to be empty, was %s", FullHeader)
136
-	}
137
-
138 131
 }
139 132
 
140 133
 func TestContainerContextWrite(t *testing.T) {
... ...
@@ -333,8 +312,8 @@ func TestContainerContextWriteJSON(t *testing.T) {
333 333
 	}
334 334
 	expectedCreated := time.Unix(unix, 0).String()
335 335
 	expectedJSONs := []map[string]interface{}{
336
-		{"Command": "\"\"", "CreatedAt": expectedCreated, "ID": "containerID1", "Image": "ubuntu", "Labels": "", "LocalVolumes": "0", "Mounts": "", "Names": "foobar_baz", "Networks": "", "Ports": "", "RunningFor": "About a minute", "Size": "0B", "Status": ""},
337
-		{"Command": "\"\"", "CreatedAt": expectedCreated, "ID": "containerID2", "Image": "ubuntu", "Labels": "", "LocalVolumes": "0", "Mounts": "", "Names": "foobar_bar", "Networks": "", "Ports": "", "RunningFor": "About a minute", "Size": "0B", "Status": ""},
336
+		{"Command": "\"\"", "CreatedAt": expectedCreated, "ID": "containerID1", "Image": "ubuntu", "Labels": "", "LocalVolumes": "0", "Mounts": "", "Names": "foobar_baz", "Networks": "", "Ports": "", "RunningFor": "About a minute ago", "Size": "0B", "Status": ""},
337
+		{"Command": "\"\"", "CreatedAt": expectedCreated, "ID": "containerID2", "Image": "ubuntu", "Labels": "", "LocalVolumes": "0", "Mounts": "", "Names": "foobar_bar", "Networks": "", "Ports": "", "RunningFor": "About a minute ago", "Size": "0B", "Status": ""},
338 338
 	}
339 339
 	out := bytes.NewBufferString("")
340 340
 	err := ContainerWrite(Context{Format: "{{json .}}", Output: out}, containers)
... ...
@@ -1,9 +1,5 @@
1 1
 package formatter
2 2
 
3
-import (
4
-	"strings"
5
-)
6
-
7 3
 const (
8 4
 	imageHeader        = "IMAGE"
9 5
 	createdSinceHeader = "CREATED"
... ...
@@ -16,29 +12,17 @@ const (
16 16
 )
17 17
 
18 18
 type subContext interface {
19
-	FullHeader() string
20
-	AddHeader(header string)
19
+	FullHeader() interface{}
21 20
 }
22 21
 
23 22
 // HeaderContext provides the subContext interface for managing headers
24 23
 type HeaderContext struct {
25
-	header []string
26
-}
27
-
28
-// FullHeader returns the header as a string
29
-func (c *HeaderContext) FullHeader() string {
30
-	if c.header == nil {
31
-		return ""
32
-	}
33
-	return strings.Join(c.header, "\t")
24
+	header interface{}
34 25
 }
35 26
 
36
-// AddHeader adds another column to the header
37
-func (c *HeaderContext) AddHeader(header string) {
38
-	if c.header == nil {
39
-		c.header = []string{}
40
-	}
41
-	c.header = append(c.header, strings.ToUpper(header))
27
+// FullHeader returns the header as an interface
28
+func (c *HeaderContext) FullHeader() interface{} {
29
+	return c.header
42 30
 }
43 31
 
44 32
 func stripNamePrefix(ss []string) []string {
... ...
@@ -77,7 +77,15 @@ func (ctx *DiskUsageContext) Write() {
77 77
 			return
78 78
 		}
79 79
 
80
-		ctx.postFormat(tmpl, &diskUsageContainersContext{containers: []*types.Container{}})
80
+		diskUsageContainersCtx := diskUsageContainersContext{containers: []*types.Container{}}
81
+		diskUsageContainersCtx.header = map[string]string{
82
+			"Type":        typeHeader,
83
+			"TotalCount":  totalHeader,
84
+			"Active":      activeHeader,
85
+			"Size":        sizeHeader,
86
+			"Reclaimable": reclaimableHeader,
87
+		}
88
+		ctx.postFormat(tmpl, &diskUsageContainersCtx)
81 89
 
82 90
 		return
83 91
 	}
... ...
@@ -114,7 +122,7 @@ func (ctx *DiskUsageContext) Write() {
114 114
 			return
115 115
 		}
116 116
 	}
117
-	ctx.postFormat(tmpl, &imageContext{})
117
+	ctx.postFormat(tmpl, newImageContext())
118 118
 
119 119
 	// Now containers
120 120
 	ctx.Output.Write([]byte("\nContainers space usage:\n\n"))
... ...
@@ -133,7 +141,7 @@ func (ctx *DiskUsageContext) Write() {
133 133
 			return
134 134
 		}
135 135
 	}
136
-	ctx.postFormat(tmpl, &containerContext{})
136
+	ctx.postFormat(tmpl, newContainerContext())
137 137
 
138 138
 	// And volumes
139 139
 	ctx.Output.Write([]byte("\nLocal Volumes space usage:\n\n"))
... ...
@@ -149,7 +157,7 @@ func (ctx *DiskUsageContext) Write() {
149 149
 			return
150 150
 		}
151 151
 	}
152
-	ctx.postFormat(tmpl, &volumeContext{v: types.Volume{}})
152
+	ctx.postFormat(tmpl, newVolumeContext())
153 153
 }
154 154
 
155 155
 type diskUsageImagesContext struct {
... ...
@@ -163,17 +171,14 @@ func (c *diskUsageImagesContext) MarshalJSON() ([]byte, error) {
163 163
 }
164 164
 
165 165
 func (c *diskUsageImagesContext) Type() string {
166
-	c.AddHeader(typeHeader)
167 166
 	return "Images"
168 167
 }
169 168
 
170 169
 func (c *diskUsageImagesContext) TotalCount() string {
171
-	c.AddHeader(totalHeader)
172 170
 	return fmt.Sprintf("%d", len(c.images))
173 171
 }
174 172
 
175 173
 func (c *diskUsageImagesContext) Active() string {
176
-	c.AddHeader(activeHeader)
177 174
 	used := 0
178 175
 	for _, i := range c.images {
179 176
 		if i.Containers > 0 {
... ...
@@ -185,7 +190,6 @@ func (c *diskUsageImagesContext) Active() string {
185 185
 }
186 186
 
187 187
 func (c *diskUsageImagesContext) Size() string {
188
-	c.AddHeader(sizeHeader)
189 188
 	return units.HumanSize(float64(c.totalSize))
190 189
 
191 190
 }
... ...
@@ -193,7 +197,6 @@ func (c *diskUsageImagesContext) Size() string {
193 193
 func (c *diskUsageImagesContext) Reclaimable() string {
194 194
 	var used int64
195 195
 
196
-	c.AddHeader(reclaimableHeader)
197 196
 	for _, i := range c.images {
198 197
 		if i.Containers != 0 {
199 198
 			if i.VirtualSize == -1 || i.SharedSize == -1 {
... ...
@@ -221,12 +224,10 @@ func (c *diskUsageContainersContext) MarshalJSON() ([]byte, error) {
221 221
 }
222 222
 
223 223
 func (c *diskUsageContainersContext) Type() string {
224
-	c.AddHeader(typeHeader)
225 224
 	return "Containers"
226 225
 }
227 226
 
228 227
 func (c *diskUsageContainersContext) TotalCount() string {
229
-	c.AddHeader(totalHeader)
230 228
 	return fmt.Sprintf("%d", len(c.containers))
231 229
 }
232 230
 
... ...
@@ -237,7 +238,6 @@ func (c *diskUsageContainersContext) isActive(container types.Container) bool {
237 237
 }
238 238
 
239 239
 func (c *diskUsageContainersContext) Active() string {
240
-	c.AddHeader(activeHeader)
241 240
 	used := 0
242 241
 	for _, container := range c.containers {
243 242
 		if c.isActive(*container) {
... ...
@@ -251,7 +251,6 @@ func (c *diskUsageContainersContext) Active() string {
251 251
 func (c *diskUsageContainersContext) Size() string {
252 252
 	var size int64
253 253
 
254
-	c.AddHeader(sizeHeader)
255 254
 	for _, container := range c.containers {
256 255
 		size += container.SizeRw
257 256
 	}
... ...
@@ -263,7 +262,6 @@ func (c *diskUsageContainersContext) Reclaimable() string {
263 263
 	var reclaimable int64
264 264
 	var totalSize int64
265 265
 
266
-	c.AddHeader(reclaimableHeader)
267 266
 	for _, container := range c.containers {
268 267
 		if !c.isActive(*container) {
269 268
 			reclaimable += container.SizeRw
... ...
@@ -289,17 +287,14 @@ func (c *diskUsageVolumesContext) MarshalJSON() ([]byte, error) {
289 289
 }
290 290
 
291 291
 func (c *diskUsageVolumesContext) Type() string {
292
-	c.AddHeader(typeHeader)
293 292
 	return "Local Volumes"
294 293
 }
295 294
 
296 295
 func (c *diskUsageVolumesContext) TotalCount() string {
297
-	c.AddHeader(totalHeader)
298 296
 	return fmt.Sprintf("%d", len(c.volumes))
299 297
 }
300 298
 
301 299
 func (c *diskUsageVolumesContext) Active() string {
302
-	c.AddHeader(activeHeader)
303 300
 
304 301
 	used := 0
305 302
 	for _, v := range c.volumes {
... ...
@@ -314,7 +309,6 @@ func (c *diskUsageVolumesContext) Active() string {
314 314
 func (c *diskUsageVolumesContext) Size() string {
315 315
 	var size int64
316 316
 
317
-	c.AddHeader(sizeHeader)
318 317
 	for _, v := range c.volumes {
319 318
 		if v.UsageData.Size != -1 {
320 319
 			size += v.UsageData.Size
... ...
@@ -328,7 +322,6 @@ func (c *diskUsageVolumesContext) Reclaimable() string {
328 328
 	var reclaimable int64
329 329
 	var totalSize int64
330 330
 
331
-	c.AddHeader(reclaimableHeader)
332 331
 	for _, v := range c.volumes {
333 332
 		if v.UsageData.Size != -1 {
334 333
 			if v.UsageData.RefCount == 0 {
335 334
new file mode 100644
... ...
@@ -0,0 +1,56 @@
0
+package formatter
1
+
2
+import (
3
+	"bytes"
4
+	//"encoding/json"
5
+	//"strings"
6
+	"testing"
7
+	//"time"
8
+
9
+	//"github.com/docker/docker/api/types"
10
+	//"github.com/docker/docker/pkg/stringid"
11
+	"github.com/docker/docker/pkg/testutil/assert"
12
+)
13
+
14
+func TestDiskUsageContextFormatWrite(t *testing.T) {
15
+	// Check default output format (verbose and non-verbose mode) for table headers
16
+	cases := []struct {
17
+		context  DiskUsageContext
18
+		expected string
19
+	}{
20
+		{
21
+			DiskUsageContext{Verbose: false},
22
+			`TYPE                TOTAL               ACTIVE              SIZE                RECLAIMABLE
23
+Images              0                   0                   0B                  0B
24
+Containers          0                   0                   0B                  0B
25
+Local Volumes       0                   0                   0B                  0B
26
+`,
27
+		},
28
+		{
29
+			DiskUsageContext{Verbose: true},
30
+			`Images space usage:
31
+
32
+REPOSITORY          TAG                 IMAGE ID            CREATED ago         SIZE                SHARED SIZE         UNIQUE SiZE         CONTAINERS
33
+
34
+Containers space usage:
35
+
36
+CONTAINER ID        IMAGE               COMMAND             LOCAL VOLUMES       SIZE                CREATED ago         STATUS              NAMES
37
+
38
+Local Volumes space usage:
39
+
40
+VOLUME NAME         LINKS               SIZE
41
+`,
42
+		},
43
+	}
44
+
45
+	for _, testcase := range cases {
46
+		//networks := []types.NetworkResource{
47
+		//	{ID: "networkID1", Name: "foobar_baz", Driver: "foo", Scope: "local", Created: timestamp1},
48
+		//	{ID: "networkID2", Name: "foobar_bar", Driver: "bar", Scope: "local", Created: timestamp2},
49
+		//}
50
+		out := bytes.NewBufferString("")
51
+		testcase.context.Output = out
52
+		testcase.context.Write()
53
+		assert.Equal(t, out.String(), testcase.expected)
54
+	}
55
+}
... ...
@@ -44,7 +44,7 @@ type Context struct {
44 44
 
45 45
 	// internal element
46 46
 	finalFormat string
47
-	header      string
47
+	header      interface{}
48 48
 	buffer      *bytes.Buffer
49 49
 }
50 50
 
... ...
@@ -71,14 +71,10 @@ func (c *Context) parseFormat() (*template.Template, error) {
71 71
 
72 72
 func (c *Context) postFormat(tmpl *template.Template, subContext subContext) {
73 73
 	if c.Format.IsTable() {
74
-		if len(c.header) == 0 {
75
-			// if we still don't have a header, we didn't have any containers so we need to fake it to get the right headers from the template
76
-			tmpl.Execute(bytes.NewBufferString(""), subContext)
77
-			c.header = subContext.FullHeader()
78
-		}
79
-
80 74
 		t := tabwriter.NewWriter(c.Output, 20, 1, 3, ' ', 0)
81
-		t.Write([]byte(c.header))
75
+		buffer := bytes.NewBufferString("")
76
+		tmpl.Execute(buffer, subContext.FullHeader())
77
+		buffer.WriteTo(t)
82 78
 		t.Write([]byte("\n"))
83 79
 		c.buffer.WriteTo(t)
84 80
 		t.Flush()
... ...
@@ -91,7 +87,7 @@ func (c *Context) contextFormat(tmpl *template.Template, subContext subContext)
91 91
 	if err := tmpl.Execute(c.buffer, subContext); err != nil {
92 92
 		return fmt.Errorf("Template parsing error: %v\n", err)
93 93
 	}
94
-	if c.Format.IsTable() && len(c.header) == 0 {
94
+	if c.Format.IsTable() && c.header != nil {
95 95
 		c.header = subContext.FullHeader()
96 96
 	}
97 97
 	c.buffer.WriteString("\n")
... ...
@@ -11,8 +11,8 @@ import (
11 11
 )
12 12
 
13 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}}"
14
+	defaultImageTableFormat           = "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedSince}}\t{{.Size}}"
15
+	defaultImageTableFormatWithDigest = "table {{.Repository}}\t{{.Tag}}\t{{.Digest}}\t{{.ID}}\t{{.CreatedSince}}\t{{.Size}}"
16 16
 
17 17
 	imageIDHeader    = "IMAGE ID"
18 18
 	repositoryHeader = "REPOSITORY"
... ...
@@ -76,7 +76,21 @@ func ImageWrite(ctx ImageContext, images []types.ImageSummary) error {
76 76
 	render := func(format func(subContext subContext) error) error {
77 77
 		return imageFormat(ctx, images, format)
78 78
 	}
79
-	return ctx.Write(&imageContext{}, render)
79
+	imageCtx := imageContext{}
80
+	imageCtx.header = map[string]string{
81
+		"ID":           imageIDHeader,
82
+		"Repository":   repositoryHeader,
83
+		"Tag":          tagHeader,
84
+		"Digest":       digestHeader,
85
+		"CreatedSince": createdSinceHeader,
86
+		"CreatedAt":    createdAtHeader,
87
+		"Size":         sizeHeader,
88
+		"Containers":   containersHeader,
89
+		"VirtualSize":  sizeHeader,
90
+		"SharedSize":   sharedSizeHeader,
91
+		"UniqueSize":   uniqueSizeHeader,
92
+	}
93
+	return ctx.Write(newImageContext(), render)
80 94
 }
81 95
 
82 96
 func imageFormat(ctx ImageContext, images []types.ImageSummary, format func(subContext subContext) error) error {
... ...
@@ -192,12 +206,29 @@ type imageContext struct {
192 192
 	digest string
193 193
 }
194 194
 
195
+func newImageContext() *imageContext {
196
+	imageCtx := imageContext{}
197
+	imageCtx.header = map[string]string{
198
+		"ID":           imageIDHeader,
199
+		"Repository":   repositoryHeader,
200
+		"Tag":          tagHeader,
201
+		"Digest":       digestHeader,
202
+		"CreatedSince": createdSinceHeader,
203
+		"CreatedAt":    createdAtHeader,
204
+		"Size":         sizeHeader,
205
+		"Containers":   containersHeader,
206
+		"VirtualSize":  sizeHeader,
207
+		"SharedSize":   sharedSizeHeader,
208
+		"UniqueSize":   uniqueSizeHeader,
209
+	}
210
+	return &imageCtx
211
+}
212
+
195 213
 func (c *imageContext) MarshalJSON() ([]byte, error) {
196 214
 	return marshalJSON(c)
197 215
 }
198 216
 
199 217
 func (c *imageContext) ID() string {
200
-	c.AddHeader(imageIDHeader)
201 218
 	if c.trunc {
202 219
 		return stringid.TruncateID(c.i.ID)
203 220
 	}
... ...
@@ -205,38 +236,31 @@ func (c *imageContext) ID() string {
205 205
 }
206 206
 
207 207
 func (c *imageContext) Repository() string {
208
-	c.AddHeader(repositoryHeader)
209 208
 	return c.repo
210 209
 }
211 210
 
212 211
 func (c *imageContext) Tag() string {
213
-	c.AddHeader(tagHeader)
214 212
 	return c.tag
215 213
 }
216 214
 
217 215
 func (c *imageContext) Digest() string {
218
-	c.AddHeader(digestHeader)
219 216
 	return c.digest
220 217
 }
221 218
 
222 219
 func (c *imageContext) CreatedSince() string {
223
-	c.AddHeader(createdSinceHeader)
224 220
 	createdAt := time.Unix(int64(c.i.Created), 0)
225
-	return units.HumanDuration(time.Now().UTC().Sub(createdAt))
221
+	return units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago"
226 222
 }
227 223
 
228 224
 func (c *imageContext) CreatedAt() string {
229
-	c.AddHeader(createdAtHeader)
230 225
 	return time.Unix(int64(c.i.Created), 0).String()
231 226
 }
232 227
 
233 228
 func (c *imageContext) Size() string {
234
-	c.AddHeader(sizeHeader)
235 229
 	return units.HumanSizeWithPrecision(float64(c.i.Size), 3)
236 230
 }
237 231
 
238 232
 func (c *imageContext) Containers() string {
239
-	c.AddHeader(containersHeader)
240 233
 	if c.i.Containers == -1 {
241 234
 		return "N/A"
242 235
 	}
... ...
@@ -244,12 +268,10 @@ func (c *imageContext) Containers() string {
244 244
 }
245 245
 
246 246
 func (c *imageContext) VirtualSize() string {
247
-	c.AddHeader(sizeHeader)
248 247
 	return units.HumanSize(float64(c.i.VirtualSize))
249 248
 }
250 249
 
251 250
 func (c *imageContext) SharedSize() string {
252
-	c.AddHeader(sharedSizeHeader)
253 251
 	if c.i.SharedSize == -1 {
254 252
 		return "N/A"
255 253
 	}
... ...
@@ -257,7 +279,6 @@ func (c *imageContext) SharedSize() string {
257 257
 }
258 258
 
259 259
 func (c *imageContext) UniqueSize() string {
260
-	c.AddHeader(uniqueSizeHeader)
261 260
 	if c.i.VirtualSize == -1 || c.i.SharedSize == -1 {
262 261
 		return "N/A"
263 262
 	}
... ...
@@ -18,27 +18,26 @@ func TestImageContext(t *testing.T) {
18 18
 
19 19
 	var ctx imageContext
20 20
 	cases := []struct {
21
-		imageCtx  imageContext
22
-		expValue  string
23
-		expHeader string
24
-		call      func() string
21
+		imageCtx imageContext
22
+		expValue string
23
+		call     func() string
25 24
 	}{
26 25
 		{imageContext{
27 26
 			i:     types.ImageSummary{ID: imageID},
28 27
 			trunc: true,
29
-		}, stringid.TruncateID(imageID), imageIDHeader, ctx.ID},
28
+		}, stringid.TruncateID(imageID), ctx.ID},
30 29
 		{imageContext{
31 30
 			i:     types.ImageSummary{ID: imageID},
32 31
 			trunc: false,
33
-		}, imageID, imageIDHeader, ctx.ID},
32
+		}, imageID, ctx.ID},
34 33
 		{imageContext{
35 34
 			i:     types.ImageSummary{Size: 10, VirtualSize: 10},
36 35
 			trunc: true,
37
-		}, "10B", sizeHeader, ctx.Size},
36
+		}, "10B", ctx.Size},
38 37
 		{imageContext{
39 38
 			i:     types.ImageSummary{Created: unix},
40 39
 			trunc: true,
41
-		}, time.Unix(unix, 0).String(), createdAtHeader, ctx.CreatedAt},
40
+		}, time.Unix(unix, 0).String(), ctx.CreatedAt},
42 41
 		// FIXME
43 42
 		// {imageContext{
44 43
 		// 	i:     types.ImageSummary{Created: unix},
... ...
@@ -47,15 +46,15 @@ func TestImageContext(t *testing.T) {
47 47
 		{imageContext{
48 48
 			i:    types.ImageSummary{},
49 49
 			repo: "busybox",
50
-		}, "busybox", repositoryHeader, ctx.Repository},
50
+		}, "busybox", ctx.Repository},
51 51
 		{imageContext{
52 52
 			i:   types.ImageSummary{},
53 53
 			tag: "latest",
54
-		}, "latest", tagHeader, ctx.Tag},
54
+		}, "latest", ctx.Tag},
55 55
 		{imageContext{
56 56
 			i:      types.ImageSummary{},
57 57
 			digest: "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a",
58
-		}, "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a", digestHeader, ctx.Digest},
58
+		}, "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a", ctx.Digest},
59 59
 	}
60 60
 
61 61
 	for _, c := range cases {
... ...
@@ -66,11 +65,6 @@ func TestImageContext(t *testing.T) {
66 66
 		} else if v != c.expValue {
67 67
 			t.Fatalf("Expected %s, was %s\n", c.expValue, v)
68 68
 		}
69
-
70
-		h := ctx.FullHeader()
71
-		if h != c.expHeader {
72
-			t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
73
-		}
74 69
 	}
75 70
 }
76 71
 
... ...
@@ -44,7 +44,28 @@ func NetworkWrite(ctx Context, networks []types.NetworkResource) error {
44 44
 		}
45 45
 		return nil
46 46
 	}
47
-	return ctx.Write(&networkContext{}, render)
47
+	networkCtx := networkContext{}
48
+	networkCtx.header = networkHeaderContext{
49
+		"ID":        networkIDHeader,
50
+		"Name":      nameHeader,
51
+		"Driver":    driverHeader,
52
+		"Scope":     scopeHeader,
53
+		"IPv6":      ipv6Header,
54
+		"Internal":  internalHeader,
55
+		"Labels":    labelsHeader,
56
+		"CreatedAt": createdAtHeader,
57
+	}
58
+	return ctx.Write(&networkCtx, render)
59
+}
60
+
61
+type networkHeaderContext map[string]string
62
+
63
+func (c networkHeaderContext) Label(name string) string {
64
+	n := strings.Split(name, ".")
65
+	r := strings.NewReplacer("-", " ", "_", " ")
66
+	h := r.Replace(n[len(n)-1])
67
+
68
+	return h
48 69
 }
49 70
 
50 71
 type networkContext struct {
... ...
@@ -58,7 +79,6 @@ func (c *networkContext) MarshalJSON() ([]byte, error) {
58 58
 }
59 59
 
60 60
 func (c *networkContext) ID() string {
61
-	c.AddHeader(networkIDHeader)
62 61
 	if c.trunc {
63 62
 		return stringid.TruncateID(c.n.ID)
64 63
 	}
... ...
@@ -66,32 +86,26 @@ func (c *networkContext) ID() string {
66 66
 }
67 67
 
68 68
 func (c *networkContext) Name() string {
69
-	c.AddHeader(nameHeader)
70 69
 	return c.n.Name
71 70
 }
72 71
 
73 72
 func (c *networkContext) Driver() string {
74
-	c.AddHeader(driverHeader)
75 73
 	return c.n.Driver
76 74
 }
77 75
 
78 76
 func (c *networkContext) Scope() string {
79
-	c.AddHeader(scopeHeader)
80 77
 	return c.n.Scope
81 78
 }
82 79
 
83 80
 func (c *networkContext) IPv6() string {
84
-	c.AddHeader(ipv6Header)
85 81
 	return fmt.Sprintf("%v", c.n.EnableIPv6)
86 82
 }
87 83
 
88 84
 func (c *networkContext) Internal() string {
89
-	c.AddHeader(internalHeader)
90 85
 	return fmt.Sprintf("%v", c.n.Internal)
91 86
 }
92 87
 
93 88
 func (c *networkContext) Labels() string {
94
-	c.AddHeader(labelsHeader)
95 89
 	if c.n.Labels == nil {
96 90
 		return ""
97 91
 	}
... ...
@@ -104,12 +118,6 @@ func (c *networkContext) Labels() string {
104 104
 }
105 105
 
106 106
 func (c *networkContext) Label(name string) string {
107
-	n := strings.Split(name, ".")
108
-	r := strings.NewReplacer("-", " ", "_", " ")
109
-	h := r.Replace(n[len(n)-1])
110
-
111
-	c.AddHeader(h)
112
-
113 107
 	if c.n.Labels == nil {
114 108
 		return ""
115 109
 	}
... ...
@@ -117,6 +125,5 @@ func (c *networkContext) Label(name string) string {
117 117
 }
118 118
 
119 119
 func (c *networkContext) CreatedAt() string {
120
-	c.AddHeader(createdAtHeader)
121 120
 	return c.n.Created.String()
122 121
 }
... ...
@@ -19,41 +19,40 @@ func TestNetworkContext(t *testing.T) {
19 19
 	cases := []struct {
20 20
 		networkCtx networkContext
21 21
 		expValue   string
22
-		expHeader  string
23 22
 		call       func() string
24 23
 	}{
25 24
 		{networkContext{
26 25
 			n:     types.NetworkResource{ID: networkID},
27 26
 			trunc: false,
28
-		}, networkID, networkIDHeader, ctx.ID},
27
+		}, networkID, ctx.ID},
29 28
 		{networkContext{
30 29
 			n:     types.NetworkResource{ID: networkID},
31 30
 			trunc: true,
32
-		}, stringid.TruncateID(networkID), networkIDHeader, ctx.ID},
31
+		}, stringid.TruncateID(networkID), ctx.ID},
33 32
 		{networkContext{
34 33
 			n: types.NetworkResource{Name: "network_name"},
35
-		}, "network_name", nameHeader, ctx.Name},
34
+		}, "network_name", ctx.Name},
36 35
 		{networkContext{
37 36
 			n: types.NetworkResource{Driver: "driver_name"},
38
-		}, "driver_name", driverHeader, ctx.Driver},
37
+		}, "driver_name", ctx.Driver},
39 38
 		{networkContext{
40 39
 			n: types.NetworkResource{EnableIPv6: true},
41
-		}, "true", ipv6Header, ctx.IPv6},
40
+		}, "true", ctx.IPv6},
42 41
 		{networkContext{
43 42
 			n: types.NetworkResource{EnableIPv6: false},
44
-		}, "false", ipv6Header, ctx.IPv6},
43
+		}, "false", ctx.IPv6},
45 44
 		{networkContext{
46 45
 			n: types.NetworkResource{Internal: true},
47
-		}, "true", internalHeader, ctx.Internal},
46
+		}, "true", ctx.Internal},
48 47
 		{networkContext{
49 48
 			n: types.NetworkResource{Internal: false},
50
-		}, "false", internalHeader, ctx.Internal},
49
+		}, "false", ctx.Internal},
51 50
 		{networkContext{
52 51
 			n: types.NetworkResource{},
53
-		}, "", labelsHeader, ctx.Labels},
52
+		}, "", ctx.Labels},
54 53
 		{networkContext{
55 54
 			n: types.NetworkResource{Labels: map[string]string{"label1": "value1", "label2": "value2"}},
56
-		}, "label1=value1,label2=value2", labelsHeader, ctx.Labels},
55
+		}, "label1=value1,label2=value2", ctx.Labels},
57 56
 	}
58 57
 
59 58
 	for _, c := range cases {
... ...
@@ -64,11 +63,6 @@ func TestNetworkContext(t *testing.T) {
64 64
 		} else if v != c.expValue {
65 65
 			t.Fatalf("Expected %s, was %s\n", c.expValue, v)
66 66
 		}
67
-
68
-		h := ctx.FullHeader()
69
-		if h != c.expHeader {
70
-			t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
71
-		}
72 67
 	}
73 68
 }
74 69
 
... ...
@@ -44,7 +44,15 @@ func PluginWrite(ctx Context, plugins []*types.Plugin) error {
44 44
 		}
45 45
 		return nil
46 46
 	}
47
-	return ctx.Write(&pluginContext{}, render)
47
+	pluginCtx := pluginContext{}
48
+	pluginCtx.header = map[string]string{
49
+		"ID":              pluginIDHeader,
50
+		"Name":            nameHeader,
51
+		"Description":     descriptionHeader,
52
+		"Enabled":         enabledHeader,
53
+		"PluginReference": imageHeader,
54
+	}
55
+	return ctx.Write(&pluginCtx, render)
48 56
 }
49 57
 
50 58
 type pluginContext struct {
... ...
@@ -58,7 +66,6 @@ func (c *pluginContext) MarshalJSON() ([]byte, error) {
58 58
 }
59 59
 
60 60
 func (c *pluginContext) ID() string {
61
-	c.AddHeader(pluginIDHeader)
62 61
 	if c.trunc {
63 62
 		return stringid.TruncateID(c.p.ID)
64 63
 	}
... ...
@@ -66,12 +73,10 @@ func (c *pluginContext) ID() string {
66 66
 }
67 67
 
68 68
 func (c *pluginContext) Name() string {
69
-	c.AddHeader(nameHeader)
70 69
 	return c.p.Name
71 70
 }
72 71
 
73 72
 func (c *pluginContext) Description() string {
74
-	c.AddHeader(descriptionHeader)
75 73
 	desc := strings.Replace(c.p.Config.Description, "\n", "", -1)
76 74
 	desc = strings.Replace(desc, "\r", "", -1)
77 75
 	if c.trunc {
... ...
@@ -82,11 +87,9 @@ func (c *pluginContext) Description() string {
82 82
 }
83 83
 
84 84
 func (c *pluginContext) Enabled() bool {
85
-	c.AddHeader(enabledHeader)
86 85
 	return c.p.Enabled
87 86
 }
88 87
 
89 88
 func (c *pluginContext) PluginReference() string {
90
-	c.AddHeader(imageHeader)
91 89
 	return c.p.PluginReference
92 90
 }
... ...
@@ -18,23 +18,22 @@ func TestPluginContext(t *testing.T) {
18 18
 	cases := []struct {
19 19
 		pluginCtx pluginContext
20 20
 		expValue  string
21
-		expHeader string
22 21
 		call      func() string
23 22
 	}{
24 23
 		{pluginContext{
25 24
 			p:     types.Plugin{ID: pluginID},
26 25
 			trunc: false,
27
-		}, pluginID, pluginIDHeader, ctx.ID},
26
+		}, pluginID, ctx.ID},
28 27
 		{pluginContext{
29 28
 			p:     types.Plugin{ID: pluginID},
30 29
 			trunc: true,
31
-		}, stringid.TruncateID(pluginID), pluginIDHeader, ctx.ID},
30
+		}, stringid.TruncateID(pluginID), ctx.ID},
32 31
 		{pluginContext{
33 32
 			p: types.Plugin{Name: "plugin_name"},
34
-		}, "plugin_name", nameHeader, ctx.Name},
33
+		}, "plugin_name", ctx.Name},
35 34
 		{pluginContext{
36 35
 			p: types.Plugin{Config: types.PluginConfig{Description: "plugin_description"}},
37
-		}, "plugin_description", descriptionHeader, ctx.Description},
36
+		}, "plugin_description", ctx.Description},
38 37
 	}
39 38
 
40 39
 	for _, c := range cases {
... ...
@@ -45,11 +44,6 @@ func TestPluginContext(t *testing.T) {
45 45
 		} else if v != c.expValue {
46 46
 			t.Fatalf("Expected %s, was %s\n", c.expValue, v)
47 47
 		}
48
-
49
-		h := ctx.FullHeader()
50
-		if h != c.expHeader {
51
-			t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
52
-		}
53 48
 	}
54 49
 }
55 50
 
... ...
@@ -372,7 +372,15 @@ func ServiceListWrite(ctx Context, services []swarm.Service, info map[string]Ser
372 372
 		}
373 373
 		return nil
374 374
 	}
375
-	return ctx.Write(&serviceContext{}, render)
375
+	serviceCtx := serviceContext{}
376
+	serviceCtx.header = map[string]string{
377
+		"ID":       serviceIDHeader,
378
+		"Name":     nameHeader,
379
+		"Mode":     modeHeader,
380
+		"Replicas": replicasHeader,
381
+		"Image":    imageHeader,
382
+	}
383
+	return ctx.Write(&serviceCtx, render)
376 384
 }
377 385
 
378 386
 type serviceContext struct {
... ...
@@ -387,27 +395,22 @@ func (c *serviceContext) MarshalJSON() ([]byte, error) {
387 387
 }
388 388
 
389 389
 func (c *serviceContext) ID() string {
390
-	c.AddHeader(serviceIDHeader)
391 390
 	return stringid.TruncateID(c.service.ID)
392 391
 }
393 392
 
394 393
 func (c *serviceContext) Name() string {
395
-	c.AddHeader(nameHeader)
396 394
 	return c.service.Spec.Name
397 395
 }
398 396
 
399 397
 func (c *serviceContext) Mode() string {
400
-	c.AddHeader(modeHeader)
401 398
 	return c.mode
402 399
 }
403 400
 
404 401
 func (c *serviceContext) Replicas() string {
405
-	c.AddHeader(replicasHeader)
406 402
 	return c.replicas
407 403
 }
408 404
 
409 405
 func (c *serviceContext) Image() string {
410
-	c.AddHeader(imageHeader)
411 406
 	image := c.service.Spec.TaskTemplate.ContainerSpec.Image
412 407
 	if ref, err := reference.ParseNormalizedNamed(image); err == nil {
413 408
 		// update image string for display, (strips any digest)
... ...
@@ -129,7 +129,24 @@ func ContainerStatsWrite(ctx Context, containerStats []StatsEntry, osType string
129 129
 		}
130 130
 		return nil
131 131
 	}
132
-	return ctx.Write(&containerStatsContext{os: osType}, render)
132
+	memUsage := memUseHeader
133
+	if osType == winOSType {
134
+		memUsage = winMemUseHeader
135
+	}
136
+	containerStatsCtx := containerStatsContext{}
137
+	containerStatsCtx.header = map[string]string{
138
+		"Container": containerHeader,
139
+		"Name":      nameHeader,
140
+		"ID":        containerIDHeader,
141
+		"CPUPerc":   cpuPercHeader,
142
+		"MemUsage":  memUsage,
143
+		"MemPerc":   memPercHeader,
144
+		"NetIO":     netIOHeader,
145
+		"BlockIO":   blockIOHeader,
146
+		"PIDs":      pidsHeader,
147
+	}
148
+	containerStatsCtx.os = osType
149
+	return ctx.Write(&containerStatsCtx, render)
133 150
 }
134 151
 
135 152
 type containerStatsContext struct {
... ...
@@ -143,12 +160,10 @@ func (c *containerStatsContext) MarshalJSON() ([]byte, error) {
143 143
 }
144 144
 
145 145
 func (c *containerStatsContext) Container() string {
146
-	c.AddHeader(containerHeader)
147 146
 	return c.s.Container
148 147
 }
149 148
 
150 149
 func (c *containerStatsContext) Name() string {
151
-	c.AddHeader(nameHeader)
152 150
 	if len(c.s.Name) > 1 {
153 151
 		return c.s.Name[1:]
154 152
 	}
... ...
@@ -156,12 +171,10 @@ func (c *containerStatsContext) Name() string {
156 156
 }
157 157
 
158 158
 func (c *containerStatsContext) ID() string {
159
-	c.AddHeader(containerIDHeader)
160 159
 	return c.s.ID
161 160
 }
162 161
 
163 162
 func (c *containerStatsContext) CPUPerc() string {
164
-	c.AddHeader(cpuPercHeader)
165 163
 	if c.s.IsInvalid {
166 164
 		return fmt.Sprintf("--")
167 165
 	}
... ...
@@ -169,11 +182,6 @@ func (c *containerStatsContext) CPUPerc() string {
169 169
 }
170 170
 
171 171
 func (c *containerStatsContext) MemUsage() string {
172
-	header := memUseHeader
173
-	if c.os == winOSType {
174
-		header = winMemUseHeader
175
-	}
176
-	c.AddHeader(header)
177 172
 	if c.s.IsInvalid {
178 173
 		return fmt.Sprintf("-- / --")
179 174
 	}
... ...
@@ -184,8 +192,6 @@ func (c *containerStatsContext) MemUsage() string {
184 184
 }
185 185
 
186 186
 func (c *containerStatsContext) MemPerc() string {
187
-	header := memPercHeader
188
-	c.AddHeader(header)
189 187
 	if c.s.IsInvalid || c.os == winOSType {
190 188
 		return fmt.Sprintf("--")
191 189
 	}
... ...
@@ -193,7 +199,6 @@ func (c *containerStatsContext) MemPerc() string {
193 193
 }
194 194
 
195 195
 func (c *containerStatsContext) NetIO() string {
196
-	c.AddHeader(netIOHeader)
197 196
 	if c.s.IsInvalid {
198 197
 		return fmt.Sprintf("--")
199 198
 	}
... ...
@@ -201,7 +206,6 @@ func (c *containerStatsContext) NetIO() string {
201 201
 }
202 202
 
203 203
 func (c *containerStatsContext) BlockIO() string {
204
-	c.AddHeader(blockIOHeader)
205 204
 	if c.s.IsInvalid {
206 205
 		return fmt.Sprintf("--")
207 206
 	}
... ...
@@ -209,7 +213,6 @@ func (c *containerStatsContext) BlockIO() string {
209 209
 }
210 210
 
211 211
 func (c *containerStatsContext) PIDs() string {
212
-	c.AddHeader(pidsHeader)
213 212
 	if c.s.IsInvalid || c.os == winOSType {
214 213
 		return fmt.Sprintf("--")
215 214
 	}
... ...
@@ -42,11 +42,6 @@ func TestContainerStatsContext(t *testing.T) {
42 42
 		if v := te.call(); v != te.expValue {
43 43
 			t.Fatalf("Expected %q, got %q", te.expValue, v)
44 44
 		}
45
-
46
-		h := ctx.FullHeader()
47
-		if h != te.expHeader {
48
-			t.Fatalf("Expected %q, got %q", te.expHeader, h)
49
-		}
50 45
 	}
51 46
 }
52 47
 
... ...
@@ -45,7 +45,17 @@ func VolumeWrite(ctx Context, volumes []*types.Volume) error {
45 45
 		}
46 46
 		return nil
47 47
 	}
48
-	return ctx.Write(&volumeContext{}, render)
48
+	return ctx.Write(newVolumeContext(), render)
49
+}
50
+
51
+type volumeHeaderContext map[string]string
52
+
53
+func (c volumeHeaderContext) Label(name string) string {
54
+	n := strings.Split(name, ".")
55
+	r := strings.NewReplacer("-", " ", "_", " ")
56
+	h := r.Replace(n[len(n)-1])
57
+
58
+	return h
49 59
 }
50 60
 
51 61
 type volumeContext struct {
... ...
@@ -53,32 +63,41 @@ type volumeContext struct {
53 53
 	v types.Volume
54 54
 }
55 55
 
56
+func newVolumeContext() *volumeContext {
57
+	volumeCtx := volumeContext{}
58
+	volumeCtx.header = volumeHeaderContext{
59
+		"Name":       volumeNameHeader,
60
+		"Driver":     driverHeader,
61
+		"Scope":      scopeHeader,
62
+		"Mountpoint": mountpointHeader,
63
+		"Labels":     labelsHeader,
64
+		"Links":      linksHeader,
65
+		"Size":       sizeHeader,
66
+	}
67
+	return &volumeCtx
68
+}
69
+
56 70
 func (c *volumeContext) MarshalJSON() ([]byte, error) {
57 71
 	return marshalJSON(c)
58 72
 }
59 73
 
60 74
 func (c *volumeContext) Name() string {
61
-	c.AddHeader(volumeNameHeader)
62 75
 	return c.v.Name
63 76
 }
64 77
 
65 78
 func (c *volumeContext) Driver() string {
66
-	c.AddHeader(driverHeader)
67 79
 	return c.v.Driver
68 80
 }
69 81
 
70 82
 func (c *volumeContext) Scope() string {
71
-	c.AddHeader(scopeHeader)
72 83
 	return c.v.Scope
73 84
 }
74 85
 
75 86
 func (c *volumeContext) Mountpoint() string {
76
-	c.AddHeader(mountpointHeader)
77 87
 	return c.v.Mountpoint
78 88
 }
79 89
 
80 90
 func (c *volumeContext) Labels() string {
81
-	c.AddHeader(labelsHeader)
82 91
 	if c.v.Labels == nil {
83 92
 		return ""
84 93
 	}
... ...
@@ -91,13 +110,6 @@ func (c *volumeContext) Labels() string {
91 91
 }
92 92
 
93 93
 func (c *volumeContext) Label(name string) string {
94
-
95
-	n := strings.Split(name, ".")
96
-	r := strings.NewReplacer("-", " ", "_", " ")
97
-	h := r.Replace(n[len(n)-1])
98
-
99
-	c.AddHeader(h)
100
-
101 94
 	if c.v.Labels == nil {
102 95
 		return ""
103 96
 	}
... ...
@@ -105,7 +117,6 @@ func (c *volumeContext) Label(name string) string {
105 105
 }
106 106
 
107 107
 func (c *volumeContext) Links() string {
108
-	c.AddHeader(linksHeader)
109 108
 	if c.v.UsageData == nil {
110 109
 		return "N/A"
111 110
 	}
... ...
@@ -113,7 +124,6 @@ func (c *volumeContext) Links() string {
113 113
 }
114 114
 
115 115
 func (c *volumeContext) Size() string {
116
-	c.AddHeader(sizeHeader)
117 116
 	if c.v.UsageData == nil {
118 117
 		return "N/A"
119 118
 	}
... ...
@@ -18,27 +18,26 @@ func TestVolumeContext(t *testing.T) {
18 18
 	cases := []struct {
19 19
 		volumeCtx volumeContext
20 20
 		expValue  string
21
-		expHeader string
22 21
 		call      func() string
23 22
 	}{
24 23
 		{volumeContext{
25 24
 			v: types.Volume{Name: volumeName},
26
-		}, volumeName, volumeNameHeader, ctx.Name},
25
+		}, volumeName, ctx.Name},
27 26
 		{volumeContext{
28 27
 			v: types.Volume{Driver: "driver_name"},
29
-		}, "driver_name", driverHeader, ctx.Driver},
28
+		}, "driver_name", ctx.Driver},
30 29
 		{volumeContext{
31 30
 			v: types.Volume{Scope: "local"},
32
-		}, "local", scopeHeader, ctx.Scope},
31
+		}, "local", ctx.Scope},
33 32
 		{volumeContext{
34 33
 			v: types.Volume{Mountpoint: "mountpoint"},
35
-		}, "mountpoint", mountpointHeader, ctx.Mountpoint},
34
+		}, "mountpoint", ctx.Mountpoint},
36 35
 		{volumeContext{
37 36
 			v: types.Volume{},
38
-		}, "", labelsHeader, ctx.Labels},
37
+		}, "", ctx.Labels},
39 38
 		{volumeContext{
40 39
 			v: types.Volume{Labels: map[string]string{"label1": "value1", "label2": "value2"}},
41
-		}, "label1=value1,label2=value2", labelsHeader, ctx.Labels},
40
+		}, "label1=value1,label2=value2", ctx.Labels},
42 41
 	}
43 42
 
44 43
 	for _, c := range cases {
... ...
@@ -49,11 +48,6 @@ func TestVolumeContext(t *testing.T) {
49 49
 		} else if v != c.expValue {
50 50
 			t.Fatalf("Expected %s, was %s\n", c.expValue, v)
51 51
 		}
52
-
53
-		h := ctx.FullHeader()
54
-		if h != c.expHeader {
55
-			t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
56
-		}
57 52
 	}
58 53
 }
59 54