diff --git a/cli/command/formatter/container.go b/cli/command/formatter/container.go index 30a6492476..ceef75890f 100644 --- a/cli/command/formatter/container.go +++ b/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) +} diff --git a/cli/command/formatter/disk_usage.go b/cli/command/formatter/disk_usage.go new file mode 100644 index 0000000000..866e9bd04a --- /dev/null +++ b/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 := "" + tag := "" + 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))) +} diff --git a/cli/command/formatter/image.go b/cli/command/formatter/image.go index 39e05378c7..1e71bda3aa 100644 --- a/cli/command/formatter/image.go +++ b/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)) +} diff --git a/cli/command/formatter/image_test.go b/cli/command/formatter/image_test.go index 6dc7f73db3..73b3c3f2e9 100644 --- a/cli/command/formatter/image_test.go +++ b/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{ diff --git a/cli/command/formatter/volume.go b/cli/command/formatter/volume.go index e41ee266bf..8fb11732e3 100644 --- a/cli/command/formatter/volume.go +++ b/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)) +} diff --git a/cli/command/system/cmd.go b/cli/command/system/cmd.go index f967c1b72e..46caa2491c 100644 --- a/cli/command/system/cmd.go +++ b/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 diff --git a/cli/command/system/df.go b/cli/command/system/df.go new file mode 100644 index 0000000000..085d680fe8 --- /dev/null +++ b/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 +} diff --git a/daemon/images.go b/daemon/images.go index b7dc6444b1..a127b00264 100644 --- a/daemon/images.go +++ b/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)