Procházet zdrojové kódy

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 před 9 roky
rodič
revize
b650a7bd27

+ 14 - 0
cli/command/formatter/container.go

@@ -23,6 +23,7 @@ const (
 	statusHeader      = "STATUS"
 	portsHeader       = "PORTS"
 	mountsHeader      = "MOUNTS"
+	localVolumes      = "LOCAL VOLUMES"
 )
 
 // NewContainerFormat returns a Format for rendering using a Context
@@ -199,3 +200,16 @@ func (c *containerContext) Mounts() string {
 	}
 	return strings.Join(mounts, ",")
 }
+
+func (c *containerContext) LocalVolumes() string {
+	c.AddHeader(localVolumes)
+
+	count := 0
+	for _, m := range c.c.Mounts {
+		if m.Driver == "local" {
+			count++
+		}
+	}
+
+	return fmt.Sprintf("%d", count)
+}

+ 331 - 0
cli/command/formatter/disk_usage.go

@@ -0,0 +1,331 @@
+package formatter
+
+import (
+	"bytes"
+	"fmt"
+	"strings"
+	"text/template"
+
+	"github.com/docker/distribution/reference"
+	"github.com/docker/docker/api/types"
+	units "github.com/docker/go-units"
+)
+
+const (
+	defaultDiskUsageImageTableFormat     = "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedSince}} ago\t{{.VirtualSize}}\t{{.SharedSize}}\t{{.UniqueSize}}\t{{.Containers}}"
+	defaultDiskUsageContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.LocalVolumes}}\t{{.Size}}\t{{.RunningFor}} ago\t{{.Status}}\t{{.Names}}"
+	defaultDiskUsageVolumeTableFormat    = "table {{.Name}}\t{{.Links}}\t{{.Size}}"
+	defaultDiskUsageTableFormat          = "table {{.Type}}\t{{.TotalCount}}\t{{.Active}}\t{{.Size}}\t{{.Reclaimable}}"
+
+	typeHeader        = "TYPE"
+	totalHeader       = "TOTAL"
+	activeHeader      = "ACTIVE"
+	reclaimableHeader = "RECLAIMABLE"
+	containersHeader  = "CONTAINERS"
+	sharedSizeHeader  = "SHARED SIZE"
+	uniqueSizeHeader  = "UNIQUE SiZE"
+)
+
+// DiskUsageContext contains disk usage specific information required by the formater, encapsulate a Context struct.
+type DiskUsageContext struct {
+	Context
+	Verbose    bool
+	LayersSize int64
+	Images     []*types.Image
+	Containers []*types.Container
+	Volumes    []*types.Volume
+}
+
+func (ctx *DiskUsageContext) startSubsection(format string) (*template.Template, error) {
+	ctx.buffer = bytes.NewBufferString("")
+	ctx.header = ""
+	ctx.Format = Format(format)
+	ctx.preFormat()
+
+	return ctx.parseFormat()
+}
+
+func (ctx *DiskUsageContext) Write() {
+	if ctx.Verbose == false {
+		ctx.buffer = bytes.NewBufferString("")
+		ctx.Format = defaultDiskUsageTableFormat
+		ctx.preFormat()
+
+		tmpl, err := ctx.parseFormat()
+		if err != nil {
+			return
+		}
+
+		err = ctx.contextFormat(tmpl, &diskUsageImagesContext{
+			totalSize: ctx.LayersSize,
+			images:    ctx.Images,
+		})
+		if err != nil {
+			return
+		}
+		err = ctx.contextFormat(tmpl, &diskUsageContainersContext{
+			containers: ctx.Containers,
+		})
+		if err != nil {
+			return
+		}
+
+		err = ctx.contextFormat(tmpl, &diskUsageVolumesContext{
+			volumes: ctx.Volumes,
+		})
+		if err != nil {
+			return
+		}
+
+		ctx.postFormat(tmpl, &diskUsageContainersContext{containers: []*types.Container{}})
+
+		return
+	}
+
+	// First images
+	tmpl, err := ctx.startSubsection(defaultDiskUsageImageTableFormat)
+	if err != nil {
+		return
+	}
+
+	ctx.Output.Write([]byte("Images space usage:\n\n"))
+	for _, i := range ctx.Images {
+		repo := "<none>"
+		tag := "<none>"
+		if len(i.RepoTags) > 0 && !isDangling(*i) {
+			// Only show the first tag
+			ref, err := reference.ParseNamed(i.RepoTags[0])
+			if err != nil {
+				continue
+			}
+			if nt, ok := ref.(reference.NamedTagged); ok {
+				repo = ref.Name()
+				tag = nt.Tag()
+			}
+		}
+
+		err = ctx.contextFormat(tmpl, &imageContext{
+			repo:  repo,
+			tag:   tag,
+			trunc: true,
+			i:     *i,
+		})
+		if err != nil {
+			return
+		}
+	}
+	ctx.postFormat(tmpl, &imageContext{})
+
+	// Now containers
+	ctx.Output.Write([]byte("\nContainers space usage:\n\n"))
+	tmpl, err = ctx.startSubsection(defaultDiskUsageContainerTableFormat)
+	if err != nil {
+		return
+	}
+	for _, c := range ctx.Containers {
+		// Don't display the virtual size
+		c.SizeRootFs = 0
+		err = ctx.contextFormat(tmpl, &containerContext{
+			trunc: true,
+			c:     *c,
+		})
+		if err != nil {
+			return
+		}
+	}
+	ctx.postFormat(tmpl, &containerContext{})
+
+	// And volumes
+	ctx.Output.Write([]byte("\nLocal Volumes space usage:\n\n"))
+	tmpl, err = ctx.startSubsection(defaultDiskUsageVolumeTableFormat)
+	if err != nil {
+		return
+	}
+	for _, v := range ctx.Volumes {
+		err = ctx.contextFormat(tmpl, &volumeContext{
+			v: *v,
+		})
+		if err != nil {
+			return
+		}
+	}
+	ctx.postFormat(tmpl, &volumeContext{v: types.Volume{}})
+}
+
+type diskUsageImagesContext struct {
+	HeaderContext
+	totalSize int64
+	images    []*types.Image
+}
+
+func (c *diskUsageImagesContext) Type() string {
+	c.AddHeader(typeHeader)
+	return "Images"
+}
+
+func (c *diskUsageImagesContext) TotalCount() string {
+	c.AddHeader(totalHeader)
+	return fmt.Sprintf("%d", len(c.images))
+}
+
+func (c *diskUsageImagesContext) Active() string {
+	c.AddHeader(activeHeader)
+	used := 0
+	for _, i := range c.images {
+		if i.Containers > 0 {
+			used++
+		}
+	}
+
+	return fmt.Sprintf("%d", used)
+}
+
+func (c *diskUsageImagesContext) Size() string {
+	c.AddHeader(sizeHeader)
+	return units.HumanSize(float64(c.totalSize))
+
+}
+
+func (c *diskUsageImagesContext) Reclaimable() string {
+	var used int64
+
+	c.AddHeader(reclaimableHeader)
+	for _, i := range c.images {
+		if i.Containers != 0 {
+			used += i.Size
+		}
+	}
+
+	reclaimable := c.totalSize - used
+	if c.totalSize > 0 {
+		return fmt.Sprintf("%s (%v%%)", units.HumanSize(float64(reclaimable)), (reclaimable*100)/c.totalSize)
+	}
+	return fmt.Sprintf("%s", units.HumanSize(float64(reclaimable)))
+}
+
+type diskUsageContainersContext struct {
+	HeaderContext
+	verbose    bool
+	containers []*types.Container
+}
+
+func (c *diskUsageContainersContext) Type() string {
+	c.AddHeader(typeHeader)
+	return "Containers"
+}
+
+func (c *diskUsageContainersContext) TotalCount() string {
+	c.AddHeader(totalHeader)
+	return fmt.Sprintf("%d", len(c.containers))
+}
+
+func (c *diskUsageContainersContext) isActive(container types.Container) bool {
+	return strings.Contains(container.State, "running") ||
+		strings.Contains(container.State, "paused") ||
+		strings.Contains(container.State, "restarting")
+}
+
+func (c *diskUsageContainersContext) Active() string {
+	c.AddHeader(activeHeader)
+	used := 0
+	for _, container := range c.containers {
+		if c.isActive(*container) {
+			used++
+		}
+	}
+
+	return fmt.Sprintf("%d", used)
+}
+
+func (c *diskUsageContainersContext) Size() string {
+	var size int64
+
+	c.AddHeader(sizeHeader)
+	for _, container := range c.containers {
+		size += container.SizeRw
+	}
+
+	return units.HumanSize(float64(size))
+}
+
+func (c *diskUsageContainersContext) Reclaimable() string {
+	var reclaimable int64
+	var totalSize int64
+
+	c.AddHeader(reclaimableHeader)
+	for _, container := range c.containers {
+		if !c.isActive(*container) {
+			reclaimable += container.SizeRw
+		}
+		totalSize += container.SizeRw
+	}
+
+	if totalSize > 0 {
+		return fmt.Sprintf("%s (%v%%)", units.HumanSize(float64(reclaimable)), (reclaimable*100)/totalSize)
+	}
+
+	return fmt.Sprintf("%s", units.HumanSize(float64(reclaimable)))
+}
+
+type diskUsageVolumesContext struct {
+	HeaderContext
+	verbose bool
+	volumes []*types.Volume
+}
+
+func (c *diskUsageVolumesContext) Type() string {
+	c.AddHeader(typeHeader)
+	return "Local Volumes"
+}
+
+func (c *diskUsageVolumesContext) TotalCount() string {
+	c.AddHeader(totalHeader)
+	return fmt.Sprintf("%d", len(c.volumes))
+}
+
+func (c *diskUsageVolumesContext) Active() string {
+	c.AddHeader(activeHeader)
+
+	used := 0
+	for _, v := range c.volumes {
+		if v.RefCount > 0 {
+			used++
+		}
+	}
+
+	return fmt.Sprintf("%d", used)
+}
+
+func (c *diskUsageVolumesContext) Size() string {
+	var size int64
+
+	c.AddHeader(sizeHeader)
+	for _, v := range c.volumes {
+		if v.Size != -1 {
+			size += v.Size
+		}
+	}
+
+	return units.HumanSize(float64(size))
+}
+
+func (c *diskUsageVolumesContext) Reclaimable() string {
+	var reclaimable int64
+	var totalSize int64
+
+	c.AddHeader(reclaimableHeader)
+	for _, v := range c.volumes {
+		if v.Size != -1 {
+			if v.RefCount == 0 {
+				reclaimable += v.Size
+			}
+			totalSize += v.Size
+		}
+	}
+
+	if totalSize > 0 {
+		return fmt.Sprintf("%s (%v%%)", units.HumanSize(float64(reclaimable)), (reclaimable*100)/totalSize)
+	}
+
+	return fmt.Sprintf("%s", units.HumanSize(float64(reclaimable)))
+}

+ 30 - 0
cli/command/formatter/image.go

@@ -1,6 +1,7 @@
 package formatter
 
 import (
+	"fmt"
 	"time"
 
 	"github.com/docker/docker/api/types"
@@ -228,3 +229,32 @@ func (c *imageContext) Size() string {
 	//NOTE: For backward compatibility we need to return VirtualSize
 	return units.HumanSizeWithPrecision(float64(c.i.VirtualSize), 3)
 }
+
+func (c *imageContext) Containers() string {
+	c.AddHeader(containersHeader)
+	if c.i.Containers == -1 {
+		return "N/A"
+	}
+	return fmt.Sprintf("%d", c.i.Containers)
+}
+
+func (c *imageContext) VirtualSize() string {
+	c.AddHeader(sizeHeader)
+	return units.HumanSize(float64(c.i.VirtualSize))
+}
+
+func (c *imageContext) SharedSize() string {
+	c.AddHeader(sharedSizeHeader)
+	if c.i.SharedSize == -1 {
+		return "N/A"
+	}
+	return units.HumanSize(float64(c.i.SharedSize))
+}
+
+func (c *imageContext) UniqueSize() string {
+	c.AddHeader(uniqueSizeHeader)
+	if c.i.Size == -1 {
+		return "N/A"
+	}
+	return units.HumanSize(float64(c.i.Size))
+}

+ 1 - 1
cli/command/formatter/image_test.go

@@ -32,7 +32,7 @@ func TestImageContext(t *testing.T) {
 			trunc: false,
 		}, imageID, imageIDHeader, ctx.ID},
 		{imageContext{
-			i:     types.Image{Size: 10},
+			i:     types.Image{Size: 10, VirtualSize: 10},
 			trunc: true,
 		}, "10 B", sizeHeader, ctx.Size},
 		{imageContext{

+ 18 - 0
cli/command/formatter/volume.go

@@ -5,6 +5,7 @@ import (
 	"strings"
 
 	"github.com/docker/docker/api/types"
+	units "github.com/docker/go-units"
 )
 
 const (
@@ -12,6 +13,7 @@ const (
 	defaultVolumeTableFormat = "table {{.Driver}}\t{{.Name}}"
 
 	mountpointHeader = "MOUNTPOINT"
+	linksHeader      = "LINKS"
 	// Status header ?
 )
 
@@ -96,3 +98,19 @@ func (c *volumeContext) Label(name string) string {
 	}
 	return c.v.Labels[name]
 }
+
+func (c *volumeContext) Links() string {
+	c.AddHeader(linksHeader)
+	if c.v.Size == -1 {
+		return "N/A"
+	}
+	return fmt.Sprintf("%d", c.v.RefCount)
+}
+
+func (c *volumeContext) Size() string {
+	c.AddHeader(sizeHeader)
+	if c.v.Size == -1 {
+		return "N/A"
+	}
+	return units.HumanSize(float64(c.v.Size))
+}

+ 1 - 0
cli/command/system/cmd.go

@@ -22,6 +22,7 @@ func NewSystemCommand(dockerCli *command.DockerCli) *cobra.Command {
 	cmd.AddCommand(
 		NewEventsCommand(dockerCli),
 		NewInfoCommand(dockerCli),
+		NewDiskUsageCommand(dockerCli),
 		NewPruneCommand(dockerCli),
 	)
 	return cmd

+ 55 - 0
cli/command/system/df.go

@@ -0,0 +1,55 @@
+package system
+
+import (
+	"github.com/docker/docker/cli"
+	"github.com/docker/docker/cli/command"
+	"github.com/docker/docker/cli/command/formatter"
+	"github.com/spf13/cobra"
+	"golang.org/x/net/context"
+)
+
+type diskUsageOptions struct {
+	verbose bool
+}
+
+// NewDiskUsageCommand creates a new cobra.Command for `docker df`
+func NewDiskUsageCommand(dockerCli *command.DockerCli) *cobra.Command {
+	var opts diskUsageOptions
+
+	cmd := &cobra.Command{
+		Use:   "df [OPTIONS]",
+		Short: "Show docker disk usage",
+		Args:  cli.RequiresMaxArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runDiskUsage(dockerCli, opts)
+		},
+	}
+
+	flags := cmd.Flags()
+
+	flags.BoolVarP(&opts.verbose, "verbose", "v", false, "Show detailed information on space usage")
+
+	return cmd
+}
+
+func runDiskUsage(dockerCli *command.DockerCli, opts diskUsageOptions) error {
+	du, err := dockerCli.Client().DiskUsage(context.Background())
+	if err != nil {
+		return err
+	}
+
+	duCtx := formatter.DiskUsageContext{
+		Context: formatter.Context{
+			Output: dockerCli.Out(),
+		},
+		LayersSize: du.LayersSize,
+		Images:     du.Images,
+		Containers: du.Containers,
+		Volumes:    du.Volumes,
+		Verbose:    opts.verbose,
+	}
+
+	duCtx.Write()
+
+	return nil
+}

+ 1 - 1
daemon/images.go

@@ -178,7 +178,7 @@ func (daemon *Daemon) Images(filterArgs, filter string, all bool, withExtraAttrs
 
 		if withExtraAttrs {
 			// lazyly init variables
-			if len(allContainers) == 0 {
+			if imagesMap == nil {
 				allContainers = daemon.List()
 				allLayers = daemon.layerStore.Map()
 				imagesMap = make(map[*image.Image]*types.Image)