Browse code

Merge pull request #30733 from yongtang/02022017-formatter-header

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

Kenfe-Mickaƫl Laventure authored on 2017/03/04 04:25:19
Showing 18 changed files
... ...
@@ -15,7 +15,7 @@ import (
15 15
 )
16 16
 
17 17
 const (
18
-	defaultContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.RunningFor}} ago\t{{.Status}}\t{{.Ports}}\t{{.Names}}"
18
+	defaultContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.RunningFor}}\t{{.Status}}\t{{.Ports}}\t{{.Names}}"
19 19
 
20 20
 	containerIDHeader = "CONTAINER ID"
21 21
 	namesHeader       = "NAMES"
... ...
@@ -72,7 +72,17 @@ func ContainerWrite(ctx Context, containers []types.Container) error {
72 72
 		}
73 73
 		return nil
74 74
 	}
75
-	return ctx.Write(&containerContext{}, render)
75
+	return ctx.Write(newContainerContext(), render)
76
+}
77
+
78
+type containerHeaderContext map[string]string
79
+
80
+func (c containerHeaderContext) Label(name string) string {
81
+	n := strings.Split(name, ".")
82
+	r := strings.NewReplacer("-", " ", "_", " ")
83
+	h := r.Replace(n[len(n)-1])
84
+
85
+	return h
76 86
 }
77 87
 
78 88
 type containerContext struct {
... ...
@@ -81,12 +91,31 @@ type containerContext struct {
81 81
 	c     types.Container
82 82
 }
83 83
 
84
+func newContainerContext() *containerContext {
85
+	containerCtx := containerContext{}
86
+	containerCtx.header = containerHeaderContext{
87
+		"ID":           containerIDHeader,
88
+		"Names":        namesHeader,
89
+		"Image":        imageHeader,
90
+		"Command":      commandHeader,
91
+		"CreatedAt":    createdAtHeader,
92
+		"RunningFor":   runningForHeader,
93
+		"Ports":        portsHeader,
94
+		"Status":       statusHeader,
95
+		"Size":         sizeHeader,
96
+		"Labels":       labelsHeader,
97
+		"Mounts":       mountsHeader,
98
+		"LocalVolumes": localVolumes,
99
+		"Networks":     networksHeader,
100
+	}
101
+	return &containerCtx
102
+}
103
+
84 104
 func (c *containerContext) MarshalJSON() ([]byte, error) {
85 105
 	return marshalJSON(c)
86 106
 }
87 107
 
88 108
 func (c *containerContext) ID() string {
89
-	c.AddHeader(containerIDHeader)
90 109
 	if c.trunc {
91 110
 		return stringid.TruncateID(c.c.ID)
92 111
 	}
... ...
@@ -94,7 +123,6 @@ func (c *containerContext) ID() string {
94 94
 }
95 95
 
96 96
 func (c *containerContext) Names() string {
97
-	c.AddHeader(namesHeader)
98 97
 	names := stripNamePrefix(c.c.Names)
99 98
 	if c.trunc {
100 99
 		for _, name := range names {
... ...
@@ -108,7 +136,6 @@ func (c *containerContext) Names() string {
108 108
 }
109 109
 
110 110
 func (c *containerContext) Image() string {
111
-	c.AddHeader(imageHeader)
112 111
 	if c.c.Image == "" {
113 112
 		return "<no image>"
114 113
 	}
... ...
@@ -136,7 +163,6 @@ func (c *containerContext) Image() string {
136 136
 }
137 137
 
138 138
 func (c *containerContext) Command() string {
139
-	c.AddHeader(commandHeader)
140 139
 	command := c.c.Command
141 140
 	if c.trunc {
142 141
 		command = stringutils.Ellipsis(command, 20)
... ...
@@ -145,28 +171,23 @@ func (c *containerContext) Command() string {
145 145
 }
146 146
 
147 147
 func (c *containerContext) CreatedAt() string {
148
-	c.AddHeader(createdAtHeader)
149 148
 	return time.Unix(int64(c.c.Created), 0).String()
150 149
 }
151 150
 
152 151
 func (c *containerContext) RunningFor() string {
153
-	c.AddHeader(runningForHeader)
154 152
 	createdAt := time.Unix(int64(c.c.Created), 0)
155
-	return units.HumanDuration(time.Now().UTC().Sub(createdAt))
153
+	return units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago"
156 154
 }
157 155
 
158 156
 func (c *containerContext) Ports() string {
159
-	c.AddHeader(portsHeader)
160 157
 	return api.DisplayablePorts(c.c.Ports)
161 158
 }
162 159
 
163 160
 func (c *containerContext) Status() string {
164
-	c.AddHeader(statusHeader)
165 161
 	return c.c.Status
166 162
 }
167 163
 
168 164
 func (c *containerContext) Size() string {
169
-	c.AddHeader(sizeHeader)
170 165
 	srw := units.HumanSizeWithPrecision(float64(c.c.SizeRw), 3)
171 166
 	sv := units.HumanSizeWithPrecision(float64(c.c.SizeRootFs), 3)
172 167
 
... ...
@@ -178,7 +199,6 @@ func (c *containerContext) Size() string {
178 178
 }
179 179
 
180 180
 func (c *containerContext) Labels() string {
181
-	c.AddHeader(labelsHeader)
182 181
 	if c.c.Labels == nil {
183 182
 		return ""
184 183
 	}
... ...
@@ -191,12 +211,6 @@ func (c *containerContext) Labels() string {
191 191
 }
192 192
 
193 193
 func (c *containerContext) Label(name string) string {
194
-	n := strings.Split(name, ".")
195
-	r := strings.NewReplacer("-", " ", "_", " ")
196
-	h := r.Replace(n[len(n)-1])
197
-
198
-	c.AddHeader(h)
199
-
200 194
 	if c.c.Labels == nil {
201 195
 		return ""
202 196
 	}
... ...
@@ -204,8 +218,6 @@ func (c *containerContext) Label(name string) string {
204 204
 }
205 205
 
206 206
 func (c *containerContext) Mounts() string {
207
-	c.AddHeader(mountsHeader)
208
-
209 207
 	var name string
210 208
 	var mounts []string
211 209
 	for _, m := range c.c.Mounts {
... ...
@@ -223,8 +235,6 @@ func (c *containerContext) Mounts() string {
223 223
 }
224 224
 
225 225
 func (c *containerContext) LocalVolumes() string {
226
-	c.AddHeader(localVolumes)
227
-
228 226
 	count := 0
229 227
 	for _, m := range c.c.Mounts {
230 228
 		if m.Driver == "local" {
... ...
@@ -236,8 +246,6 @@ func (c *containerContext) LocalVolumes() string {
236 236
 }
237 237
 
238 238
 func (c *containerContext) Networks() string {
239
-	c.AddHeader(networksHeader)
240
-
241 239
 	if c.c.NetworkSettings == nil {
242 240
 		return ""
243 241
 	}
... ...
@@ -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) {
... ...
@@ -247,6 +226,14 @@ size: 0B
247 247
 			Context{Format: NewContainerFormat("{{.Image}}", false, true)},
248 248
 			"ubuntu\nubuntu\n",
249 249
 		},
250
+		// Special headers for customerized table format
251
+		{
252
+			Context{Format: NewContainerFormat(`table {{truncate .ID 5}}\t{{json .Image}} {{.RunningFor}}/{{title .Status}}/{{pad .Ports 2 2}}.{{upper .Names}} {{lower .Status}}`, false, true)},
253
+			`CONTAINER ID        IMAGE CREATED/STATUS/  PORTS  .NAMES STATUS
254
+conta               "ubuntu" 24 hours ago//.FOOBAR_BAZ 
255
+conta               "ubuntu" 24 hours ago//.FOOBAR_BAR 
256
+`,
257
+		},
250 258
 	}
251 259
 
252 260
 	for _, testcase := range cases {
... ...
@@ -333,8 +320,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,47 @@
0
+package formatter
1
+
2
+import (
3
+	"bytes"
4
+	"testing"
5
+
6
+	"github.com/docker/docker/pkg/testutil/assert"
7
+)
8
+
9
+func TestDiskUsageContextFormatWrite(t *testing.T) {
10
+	// Check default output format (verbose and non-verbose mode) for table headers
11
+	cases := []struct {
12
+		context  DiskUsageContext
13
+		expected string
14
+	}{
15
+		{
16
+			DiskUsageContext{Verbose: false},
17
+			`TYPE                TOTAL               ACTIVE              SIZE                RECLAIMABLE
18
+Images              0                   0                   0B                  0B
19
+Containers          0                   0                   0B                  0B
20
+Local Volumes       0                   0                   0B                  0B
21
+`,
22
+		},
23
+		{
24
+			DiskUsageContext{Verbose: true},
25
+			`Images space usage:
26
+
27
+REPOSITORY          TAG                 IMAGE ID            CREATED ago         SIZE                SHARED SIZE         UNIQUE SiZE         CONTAINERS
28
+
29
+Containers space usage:
30
+
31
+CONTAINER ID        IMAGE               COMMAND             LOCAL VOLUMES       SIZE                CREATED ago         STATUS              NAMES
32
+
33
+Local Volumes space usage:
34
+
35
+VOLUME NAME         LINKS               SIZE
36
+`,
37
+		},
38
+	}
39
+
40
+	for _, testcase := range cases {
41
+		out := bytes.NewBufferString("")
42
+		testcase.context.Output = out
43
+		testcase.context.Write()
44
+		assert.Equal(t, out.String(), testcase.expected)
45
+	}
46
+}
... ...
@@ -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.Funcs(templates.HeaderFunctions).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,7 @@ 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
+	return ctx.Write(newImageContext(), render)
80 80
 }
81 81
 
82 82
 func imageFormat(ctx ImageContext, images []types.ImageSummary, format func(subContext subContext) error) error {
... ...
@@ -192,12 +192,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 +222,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 +254,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 +265,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
 
... ...
@@ -388,7 +388,15 @@ func ServiceListWrite(ctx Context, services []swarm.Service, info map[string]Ser
388 388
 		}
389 389
 		return nil
390 390
 	}
391
-	return ctx.Write(&serviceContext{}, render)
391
+	serviceCtx := serviceContext{}
392
+	serviceCtx.header = map[string]string{
393
+		"ID":       serviceIDHeader,
394
+		"Name":     nameHeader,
395
+		"Mode":     modeHeader,
396
+		"Replicas": replicasHeader,
397
+		"Image":    imageHeader,
398
+	}
399
+	return ctx.Write(&serviceCtx, render)
392 400
 }
393 401
 
394 402
 type serviceContext struct {
... ...
@@ -403,27 +411,22 @@ func (c *serviceContext) MarshalJSON() ([]byte, error) {
403 403
 }
404 404
 
405 405
 func (c *serviceContext) ID() string {
406
-	c.AddHeader(serviceIDHeader)
407 406
 	return stringid.TruncateID(c.service.ID)
408 407
 }
409 408
 
410 409
 func (c *serviceContext) Name() string {
411
-	c.AddHeader(nameHeader)
412 410
 	return c.service.Spec.Name
413 411
 }
414 412
 
415 413
 func (c *serviceContext) Mode() string {
416
-	c.AddHeader(modeHeader)
417 414
 	return c.mode
418 415
 }
419 416
 
420 417
 func (c *serviceContext) Replicas() string {
421
-	c.AddHeader(replicasHeader)
422 418
 	return c.replicas
423 419
 }
424 420
 
425 421
 func (c *serviceContext) Image() string {
426
-	c.AddHeader(imageHeader)
427 422
 	image := c.service.Spec.TaskTemplate.ContainerSpec.Image
428 423
 	if ref, err := reference.ParseNormalizedNamed(image); err == nil {
429 424
 		// 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
 
... ...
@@ -22,6 +22,28 @@ var basicFunctions = template.FuncMap{
22 22
 	"truncate": truncateWithLength,
23 23
 }
24 24
 
25
+// HeaderFunctions are used to created headers of a table.
26
+// This is a replacement of basicFunctions for header generation
27
+// because we want the header to remain intact.
28
+// Some functions like `split` are irrevelant so not added.
29
+var HeaderFunctions = template.FuncMap{
30
+	"json": func(v string) string {
31
+		return v
32
+	},
33
+	"title": func(v string) string {
34
+		return v
35
+	},
36
+	"lower": func(v string) string {
37
+		return v
38
+	},
39
+	"upper": func(v string) string {
40
+		return v
41
+	},
42
+	"truncate": func(v string, l int) string {
43
+		return v
44
+	},
45
+}
46
+
25 47
 // Parse creates a new anonymous template with the basic functions
26 48
 // and parses the given format.
27 49
 func Parse(format string) (*template.Template, error) {