Browse code

Add new df subcomand to the system command

This command display the state of the data usage of the docker daemon.

Signed-off-by: Kenfe-Mickael Laventure <mickael.laventure@gmail.com>

Kenfe-Mickael Laventure authored on 2016/08/24 08:37:37
Showing 8 changed files
... ...
@@ -23,6 +23,7 @@ const (
23 23
 	statusHeader      = "STATUS"
24 24
 	portsHeader       = "PORTS"
25 25
 	mountsHeader      = "MOUNTS"
26
+	localVolumes      = "LOCAL VOLUMES"
26 27
 )
27 28
 
28 29
 // NewContainerFormat returns a Format for rendering using a Context
... ...
@@ -199,3 +200,16 @@ func (c *containerContext) Mounts() string {
199 199
 	}
200 200
 	return strings.Join(mounts, ",")
201 201
 }
202
+
203
+func (c *containerContext) LocalVolumes() string {
204
+	c.AddHeader(localVolumes)
205
+
206
+	count := 0
207
+	for _, m := range c.c.Mounts {
208
+		if m.Driver == "local" {
209
+			count++
210
+		}
211
+	}
212
+
213
+	return fmt.Sprintf("%d", count)
214
+}
202 215
new file mode 100644
... ...
@@ -0,0 +1,331 @@
0
+package formatter
1
+
2
+import (
3
+	"bytes"
4
+	"fmt"
5
+	"strings"
6
+	"text/template"
7
+
8
+	"github.com/docker/distribution/reference"
9
+	"github.com/docker/docker/api/types"
10
+	units "github.com/docker/go-units"
11
+)
12
+
13
+const (
14
+	defaultDiskUsageImageTableFormat     = "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedSince}} ago\t{{.VirtualSize}}\t{{.SharedSize}}\t{{.UniqueSize}}\t{{.Containers}}"
15
+	defaultDiskUsageContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.LocalVolumes}}\t{{.Size}}\t{{.RunningFor}} ago\t{{.Status}}\t{{.Names}}"
16
+	defaultDiskUsageVolumeTableFormat    = "table {{.Name}}\t{{.Links}}\t{{.Size}}"
17
+	defaultDiskUsageTableFormat          = "table {{.Type}}\t{{.TotalCount}}\t{{.Active}}\t{{.Size}}\t{{.Reclaimable}}"
18
+
19
+	typeHeader        = "TYPE"
20
+	totalHeader       = "TOTAL"
21
+	activeHeader      = "ACTIVE"
22
+	reclaimableHeader = "RECLAIMABLE"
23
+	containersHeader  = "CONTAINERS"
24
+	sharedSizeHeader  = "SHARED SIZE"
25
+	uniqueSizeHeader  = "UNIQUE SiZE"
26
+)
27
+
28
+// DiskUsageContext contains disk usage specific information required by the formater, encapsulate a Context struct.
29
+type DiskUsageContext struct {
30
+	Context
31
+	Verbose    bool
32
+	LayersSize int64
33
+	Images     []*types.Image
34
+	Containers []*types.Container
35
+	Volumes    []*types.Volume
36
+}
37
+
38
+func (ctx *DiskUsageContext) startSubsection(format string) (*template.Template, error) {
39
+	ctx.buffer = bytes.NewBufferString("")
40
+	ctx.header = ""
41
+	ctx.Format = Format(format)
42
+	ctx.preFormat()
43
+
44
+	return ctx.parseFormat()
45
+}
46
+
47
+func (ctx *DiskUsageContext) Write() {
48
+	if ctx.Verbose == false {
49
+		ctx.buffer = bytes.NewBufferString("")
50
+		ctx.Format = defaultDiskUsageTableFormat
51
+		ctx.preFormat()
52
+
53
+		tmpl, err := ctx.parseFormat()
54
+		if err != nil {
55
+			return
56
+		}
57
+
58
+		err = ctx.contextFormat(tmpl, &diskUsageImagesContext{
59
+			totalSize: ctx.LayersSize,
60
+			images:    ctx.Images,
61
+		})
62
+		if err != nil {
63
+			return
64
+		}
65
+		err = ctx.contextFormat(tmpl, &diskUsageContainersContext{
66
+			containers: ctx.Containers,
67
+		})
68
+		if err != nil {
69
+			return
70
+		}
71
+
72
+		err = ctx.contextFormat(tmpl, &diskUsageVolumesContext{
73
+			volumes: ctx.Volumes,
74
+		})
75
+		if err != nil {
76
+			return
77
+		}
78
+
79
+		ctx.postFormat(tmpl, &diskUsageContainersContext{containers: []*types.Container{}})
80
+
81
+		return
82
+	}
83
+
84
+	// First images
85
+	tmpl, err := ctx.startSubsection(defaultDiskUsageImageTableFormat)
86
+	if err != nil {
87
+		return
88
+	}
89
+
90
+	ctx.Output.Write([]byte("Images space usage:\n\n"))
91
+	for _, i := range ctx.Images {
92
+		repo := "<none>"
93
+		tag := "<none>"
94
+		if len(i.RepoTags) > 0 && !isDangling(*i) {
95
+			// Only show the first tag
96
+			ref, err := reference.ParseNamed(i.RepoTags[0])
97
+			if err != nil {
98
+				continue
99
+			}
100
+			if nt, ok := ref.(reference.NamedTagged); ok {
101
+				repo = ref.Name()
102
+				tag = nt.Tag()
103
+			}
104
+		}
105
+
106
+		err = ctx.contextFormat(tmpl, &imageContext{
107
+			repo:  repo,
108
+			tag:   tag,
109
+			trunc: true,
110
+			i:     *i,
111
+		})
112
+		if err != nil {
113
+			return
114
+		}
115
+	}
116
+	ctx.postFormat(tmpl, &imageContext{})
117
+
118
+	// Now containers
119
+	ctx.Output.Write([]byte("\nContainers space usage:\n\n"))
120
+	tmpl, err = ctx.startSubsection(defaultDiskUsageContainerTableFormat)
121
+	if err != nil {
122
+		return
123
+	}
124
+	for _, c := range ctx.Containers {
125
+		// Don't display the virtual size
126
+		c.SizeRootFs = 0
127
+		err = ctx.contextFormat(tmpl, &containerContext{
128
+			trunc: true,
129
+			c:     *c,
130
+		})
131
+		if err != nil {
132
+			return
133
+		}
134
+	}
135
+	ctx.postFormat(tmpl, &containerContext{})
136
+
137
+	// And volumes
138
+	ctx.Output.Write([]byte("\nLocal Volumes space usage:\n\n"))
139
+	tmpl, err = ctx.startSubsection(defaultDiskUsageVolumeTableFormat)
140
+	if err != nil {
141
+		return
142
+	}
143
+	for _, v := range ctx.Volumes {
144
+		err = ctx.contextFormat(tmpl, &volumeContext{
145
+			v: *v,
146
+		})
147
+		if err != nil {
148
+			return
149
+		}
150
+	}
151
+	ctx.postFormat(tmpl, &volumeContext{v: types.Volume{}})
152
+}
153
+
154
+type diskUsageImagesContext struct {
155
+	HeaderContext
156
+	totalSize int64
157
+	images    []*types.Image
158
+}
159
+
160
+func (c *diskUsageImagesContext) Type() string {
161
+	c.AddHeader(typeHeader)
162
+	return "Images"
163
+}
164
+
165
+func (c *diskUsageImagesContext) TotalCount() string {
166
+	c.AddHeader(totalHeader)
167
+	return fmt.Sprintf("%d", len(c.images))
168
+}
169
+
170
+func (c *diskUsageImagesContext) Active() string {
171
+	c.AddHeader(activeHeader)
172
+	used := 0
173
+	for _, i := range c.images {
174
+		if i.Containers > 0 {
175
+			used++
176
+		}
177
+	}
178
+
179
+	return fmt.Sprintf("%d", used)
180
+}
181
+
182
+func (c *diskUsageImagesContext) Size() string {
183
+	c.AddHeader(sizeHeader)
184
+	return units.HumanSize(float64(c.totalSize))
185
+
186
+}
187
+
188
+func (c *diskUsageImagesContext) Reclaimable() string {
189
+	var used int64
190
+
191
+	c.AddHeader(reclaimableHeader)
192
+	for _, i := range c.images {
193
+		if i.Containers != 0 {
194
+			used += i.Size
195
+		}
196
+	}
197
+
198
+	reclaimable := c.totalSize - used
199
+	if c.totalSize > 0 {
200
+		return fmt.Sprintf("%s (%v%%)", units.HumanSize(float64(reclaimable)), (reclaimable*100)/c.totalSize)
201
+	}
202
+	return fmt.Sprintf("%s", units.HumanSize(float64(reclaimable)))
203
+}
204
+
205
+type diskUsageContainersContext struct {
206
+	HeaderContext
207
+	verbose    bool
208
+	containers []*types.Container
209
+}
210
+
211
+func (c *diskUsageContainersContext) Type() string {
212
+	c.AddHeader(typeHeader)
213
+	return "Containers"
214
+}
215
+
216
+func (c *diskUsageContainersContext) TotalCount() string {
217
+	c.AddHeader(totalHeader)
218
+	return fmt.Sprintf("%d", len(c.containers))
219
+}
220
+
221
+func (c *diskUsageContainersContext) isActive(container types.Container) bool {
222
+	return strings.Contains(container.State, "running") ||
223
+		strings.Contains(container.State, "paused") ||
224
+		strings.Contains(container.State, "restarting")
225
+}
226
+
227
+func (c *diskUsageContainersContext) Active() string {
228
+	c.AddHeader(activeHeader)
229
+	used := 0
230
+	for _, container := range c.containers {
231
+		if c.isActive(*container) {
232
+			used++
233
+		}
234
+	}
235
+
236
+	return fmt.Sprintf("%d", used)
237
+}
238
+
239
+func (c *diskUsageContainersContext) Size() string {
240
+	var size int64
241
+
242
+	c.AddHeader(sizeHeader)
243
+	for _, container := range c.containers {
244
+		size += container.SizeRw
245
+	}
246
+
247
+	return units.HumanSize(float64(size))
248
+}
249
+
250
+func (c *diskUsageContainersContext) Reclaimable() string {
251
+	var reclaimable int64
252
+	var totalSize int64
253
+
254
+	c.AddHeader(reclaimableHeader)
255
+	for _, container := range c.containers {
256
+		if !c.isActive(*container) {
257
+			reclaimable += container.SizeRw
258
+		}
259
+		totalSize += container.SizeRw
260
+	}
261
+
262
+	if totalSize > 0 {
263
+		return fmt.Sprintf("%s (%v%%)", units.HumanSize(float64(reclaimable)), (reclaimable*100)/totalSize)
264
+	}
265
+
266
+	return fmt.Sprintf("%s", units.HumanSize(float64(reclaimable)))
267
+}
268
+
269
+type diskUsageVolumesContext struct {
270
+	HeaderContext
271
+	verbose bool
272
+	volumes []*types.Volume
273
+}
274
+
275
+func (c *diskUsageVolumesContext) Type() string {
276
+	c.AddHeader(typeHeader)
277
+	return "Local Volumes"
278
+}
279
+
280
+func (c *diskUsageVolumesContext) TotalCount() string {
281
+	c.AddHeader(totalHeader)
282
+	return fmt.Sprintf("%d", len(c.volumes))
283
+}
284
+
285
+func (c *diskUsageVolumesContext) Active() string {
286
+	c.AddHeader(activeHeader)
287
+
288
+	used := 0
289
+	for _, v := range c.volumes {
290
+		if v.RefCount > 0 {
291
+			used++
292
+		}
293
+	}
294
+
295
+	return fmt.Sprintf("%d", used)
296
+}
297
+
298
+func (c *diskUsageVolumesContext) Size() string {
299
+	var size int64
300
+
301
+	c.AddHeader(sizeHeader)
302
+	for _, v := range c.volumes {
303
+		if v.Size != -1 {
304
+			size += v.Size
305
+		}
306
+	}
307
+
308
+	return units.HumanSize(float64(size))
309
+}
310
+
311
+func (c *diskUsageVolumesContext) Reclaimable() string {
312
+	var reclaimable int64
313
+	var totalSize int64
314
+
315
+	c.AddHeader(reclaimableHeader)
316
+	for _, v := range c.volumes {
317
+		if v.Size != -1 {
318
+			if v.RefCount == 0 {
319
+				reclaimable += v.Size
320
+			}
321
+			totalSize += v.Size
322
+		}
323
+	}
324
+
325
+	if totalSize > 0 {
326
+		return fmt.Sprintf("%s (%v%%)", units.HumanSize(float64(reclaimable)), (reclaimable*100)/totalSize)
327
+	}
328
+
329
+	return fmt.Sprintf("%s", units.HumanSize(float64(reclaimable)))
330
+}
... ...
@@ -1,6 +1,7 @@
1 1
 package formatter
2 2
 
3 3
 import (
4
+	"fmt"
4 5
 	"time"
5 6
 
6 7
 	"github.com/docker/docker/api/types"
... ...
@@ -228,3 +229,32 @@ func (c *imageContext) Size() string {
228 228
 	//NOTE: For backward compatibility we need to return VirtualSize
229 229
 	return units.HumanSizeWithPrecision(float64(c.i.VirtualSize), 3)
230 230
 }
231
+
232
+func (c *imageContext) Containers() string {
233
+	c.AddHeader(containersHeader)
234
+	if c.i.Containers == -1 {
235
+		return "N/A"
236
+	}
237
+	return fmt.Sprintf("%d", c.i.Containers)
238
+}
239
+
240
+func (c *imageContext) VirtualSize() string {
241
+	c.AddHeader(sizeHeader)
242
+	return units.HumanSize(float64(c.i.VirtualSize))
243
+}
244
+
245
+func (c *imageContext) SharedSize() string {
246
+	c.AddHeader(sharedSizeHeader)
247
+	if c.i.SharedSize == -1 {
248
+		return "N/A"
249
+	}
250
+	return units.HumanSize(float64(c.i.SharedSize))
251
+}
252
+
253
+func (c *imageContext) UniqueSize() string {
254
+	c.AddHeader(uniqueSizeHeader)
255
+	if c.i.Size == -1 {
256
+		return "N/A"
257
+	}
258
+	return units.HumanSize(float64(c.i.Size))
259
+}
... ...
@@ -32,7 +32,7 @@ func TestImageContext(t *testing.T) {
32 32
 			trunc: false,
33 33
 		}, imageID, imageIDHeader, ctx.ID},
34 34
 		{imageContext{
35
-			i:     types.Image{Size: 10},
35
+			i:     types.Image{Size: 10, VirtualSize: 10},
36 36
 			trunc: true,
37 37
 		}, "10 B", sizeHeader, ctx.Size},
38 38
 		{imageContext{
... ...
@@ -5,6 +5,7 @@ import (
5 5
 	"strings"
6 6
 
7 7
 	"github.com/docker/docker/api/types"
8
+	units "github.com/docker/go-units"
8 9
 )
9 10
 
10 11
 const (
... ...
@@ -12,6 +13,7 @@ const (
12 12
 	defaultVolumeTableFormat = "table {{.Driver}}\t{{.Name}}"
13 13
 
14 14
 	mountpointHeader = "MOUNTPOINT"
15
+	linksHeader      = "LINKS"
15 16
 	// Status header ?
16 17
 )
17 18
 
... ...
@@ -96,3 +98,19 @@ func (c *volumeContext) Label(name string) string {
96 96
 	}
97 97
 	return c.v.Labels[name]
98 98
 }
99
+
100
+func (c *volumeContext) Links() string {
101
+	c.AddHeader(linksHeader)
102
+	if c.v.Size == -1 {
103
+		return "N/A"
104
+	}
105
+	return fmt.Sprintf("%d", c.v.RefCount)
106
+}
107
+
108
+func (c *volumeContext) Size() string {
109
+	c.AddHeader(sizeHeader)
110
+	if c.v.Size == -1 {
111
+		return "N/A"
112
+	}
113
+	return units.HumanSize(float64(c.v.Size))
114
+}
... ...
@@ -22,6 +22,7 @@ func NewSystemCommand(dockerCli *command.DockerCli) *cobra.Command {
22 22
 	cmd.AddCommand(
23 23
 		NewEventsCommand(dockerCli),
24 24
 		NewInfoCommand(dockerCli),
25
+		NewDiskUsageCommand(dockerCli),
25 26
 		NewPruneCommand(dockerCli),
26 27
 	)
27 28
 	return cmd
28 29
new file mode 100644
... ...
@@ -0,0 +1,55 @@
0
+package system
1
+
2
+import (
3
+	"github.com/docker/docker/cli"
4
+	"github.com/docker/docker/cli/command"
5
+	"github.com/docker/docker/cli/command/formatter"
6
+	"github.com/spf13/cobra"
7
+	"golang.org/x/net/context"
8
+)
9
+
10
+type diskUsageOptions struct {
11
+	verbose bool
12
+}
13
+
14
+// NewDiskUsageCommand creates a new cobra.Command for `docker df`
15
+func NewDiskUsageCommand(dockerCli *command.DockerCli) *cobra.Command {
16
+	var opts diskUsageOptions
17
+
18
+	cmd := &cobra.Command{
19
+		Use:   "df [OPTIONS]",
20
+		Short: "Show docker disk usage",
21
+		Args:  cli.RequiresMaxArgs(1),
22
+		RunE: func(cmd *cobra.Command, args []string) error {
23
+			return runDiskUsage(dockerCli, opts)
24
+		},
25
+	}
26
+
27
+	flags := cmd.Flags()
28
+
29
+	flags.BoolVarP(&opts.verbose, "verbose", "v", false, "Show detailed information on space usage")
30
+
31
+	return cmd
32
+}
33
+
34
+func runDiskUsage(dockerCli *command.DockerCli, opts diskUsageOptions) error {
35
+	du, err := dockerCli.Client().DiskUsage(context.Background())
36
+	if err != nil {
37
+		return err
38
+	}
39
+
40
+	duCtx := formatter.DiskUsageContext{
41
+		Context: formatter.Context{
42
+			Output: dockerCli.Out(),
43
+		},
44
+		LayersSize: du.LayersSize,
45
+		Images:     du.Images,
46
+		Containers: du.Containers,
47
+		Volumes:    du.Volumes,
48
+		Verbose:    opts.verbose,
49
+	}
50
+
51
+	duCtx.Write()
52
+
53
+	return nil
54
+}
... ...
@@ -178,7 +178,7 @@ func (daemon *Daemon) Images(filterArgs, filter string, all bool, withExtraAttrs
178 178
 
179 179
 		if withExtraAttrs {
180 180
 			// lazyly init variables
181
-			if len(allContainers) == 0 {
181
+			if imagesMap == nil {
182 182
 				allContainers = daemon.List()
183 183
 				allLayers = daemon.layerStore.Map()
184 184
 				imagesMap = make(map[*image.Image]*types.Image)