diff --git a/command/container/cmd.go b/command/container/cmd.go index da9ea6d41d..f06b863b58 100644 --- a/command/container/cmd.go +++ b/command/container/cmd.go @@ -44,6 +44,7 @@ func NewContainerCommand(dockerCli *command.DockerCli) *cobra.Command { NewWaitCommand(dockerCli), newListCommand(dockerCli), newInspectCommand(dockerCli), + NewPruneCommand(dockerCli), ) return cmd } diff --git a/command/container/prune.go b/command/container/prune.go new file mode 100644 index 0000000000..13e283a8b2 --- /dev/null +++ b/command/container/prune.go @@ -0,0 +1,74 @@ +package container + +import ( + "fmt" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + units "github.com/docker/go-units" + "github.com/spf13/cobra" +) + +type pruneOptions struct { + force bool +} + +// NewPruneCommand returns a new cobra prune command for containers +func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts pruneOptions + + cmd := &cobra.Command{ + Use: "prune", + Short: "Remove all stopped containers", + Args: cli.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + spaceReclaimed, output, err := runPrune(dockerCli, opts) + if err != nil { + return err + } + if output != "" { + fmt.Fprintln(dockerCli.Out(), output) + } + fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed))) + return nil + }, + } + + flags := cmd.Flags() + flags.BoolVarP(&opts.force, "force", "f", false, "Do not prompt for confirmation") + + return cmd +} + +const warning = `WARNING! This will remove all stopped containers. +Are you sure you want to continue? [y/N] ` + +func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (spaceReclaimed uint64, output string, err error) { + if !opts.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) { + return + } + + report, err := dockerCli.Client().ContainersPrune(context.Background(), types.ContainersPruneConfig{}) + if err != nil { + return + } + + if len(report.ContainersDeleted) > 0 { + output = "Deleted Containers:" + for _, id := range report.ContainersDeleted { + output += id + "\n" + } + spaceReclaimed = report.SpaceReclaimed + } + + return +} + +// RunPrune call the Container Prune API +// This returns the amount of space reclaimed and a detailed output string +func RunPrune(dockerCli *command.DockerCli) (uint64, string, error) { + return runPrune(dockerCli, pruneOptions{force: true}) +} diff --git a/command/container/stats.go b/command/container/stats.go index 394302d087..2e3714486b 100644 --- a/command/container/stats.go +++ b/command/container/stats.go @@ -15,7 +15,6 @@ import ( "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command/formatter" - "github.com/docker/docker/cli/command/system" "github.com/spf13/cobra" ) @@ -110,7 +109,7 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error { // retrieving the list of running containers to avoid a race where we // would "miss" a creation. started := make(chan struct{}) - eh := system.InitEventHandler() + eh := command.InitEventHandler() eh.Handle("create", func(e events.Message) { if opts.all { s := formatter.NewContainerStats(e.ID[:12], daemonOSType) diff --git a/command/system/events_utils.go b/command/events_utils.go similarity index 98% rename from command/system/events_utils.go rename to command/events_utils.go index b0dd909d15..e710c97576 100644 --- a/command/system/events_utils.go +++ b/command/events_utils.go @@ -1,4 +1,4 @@ -package system +package command import ( "sync" diff --git a/command/formatter/container.go b/command/formatter/container.go index 30a6492476..ceef75890f 100644 --- a/command/formatter/container.go +++ b/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/command/formatter/disk_usage.go b/command/formatter/disk_usage.go new file mode 100644 index 0000000000..866e9bd04a --- /dev/null +++ b/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/command/formatter/image.go b/command/formatter/image.go index 54cb7b62fa..1e71bda3aa 100644 --- a/command/formatter/image.go +++ b/command/formatter/image.go @@ -1,6 +1,7 @@ package formatter import ( + "fmt" "time" "github.com/docker/docker/api/types" @@ -225,5 +226,35 @@ func (c *imageContext) CreatedAt() string { func (c *imageContext) Size() string { c.AddHeader(sizeHeader) - return units.HumanSizeWithPrecision(float64(c.i.Size), 3) + //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/command/formatter/image_test.go b/command/formatter/image_test.go index 6dc7f73db3..73b3c3f2e9 100644 --- a/command/formatter/image_test.go +++ b/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/command/formatter/volume.go b/command/formatter/volume.go index e41ee266bf..8fb11732e3 100644 --- a/command/formatter/volume.go +++ b/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/command/image/cmd.go b/command/image/cmd.go index f60ffeeb8f..6f8e7b7d4b 100644 --- a/command/image/cmd.go +++ b/command/image/cmd.go @@ -31,6 +31,8 @@ func NewImageCommand(dockerCli *command.DockerCli) *cobra.Command { newListCommand(dockerCli), newRemoveCommand(dockerCli), newInspectCommand(dockerCli), + NewPruneCommand(dockerCli), ) + return cmd } diff --git a/command/image/prune.go b/command/image/prune.go new file mode 100644 index 0000000000..6944664a54 --- /dev/null +++ b/command/image/prune.go @@ -0,0 +1,90 @@ +package image + +import ( + "fmt" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + units "github.com/docker/go-units" + "github.com/spf13/cobra" +) + +type pruneOptions struct { + force bool + all bool +} + +// NewPruneCommand returns a new cobra prune command for images +func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts pruneOptions + + cmd := &cobra.Command{ + Use: "prune", + Short: "Remove unused images", + Args: cli.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + spaceReclaimed, output, err := runPrune(dockerCli, opts) + if err != nil { + return err + } + if output != "" { + fmt.Fprintln(dockerCli.Out(), output) + } + fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed))) + return nil + }, + } + + flags := cmd.Flags() + flags.BoolVarP(&opts.force, "force", "f", false, "Do not prompt for confirmation") + flags.BoolVarP(&opts.all, "all", "a", false, "Remove all unused images, not just dangling ones") + + return cmd +} + +const ( + allImageWarning = `WARNING! This will remove all images without at least one container associated to them. +Are you sure you want to continue?` + danglingWarning = `WARNING! This will remove all dangling images. +Are you sure you want to continue?` +) + +func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (spaceReclaimed uint64, output string, err error) { + warning := danglingWarning + if opts.all { + warning = allImageWarning + } + if !opts.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) { + return + } + + report, err := dockerCli.Client().ImagesPrune(context.Background(), types.ImagesPruneConfig{ + DanglingOnly: !opts.all, + }) + if err != nil { + return + } + + if len(report.ImagesDeleted) > 0 { + output = "Deleted Images:\n" + for _, st := range report.ImagesDeleted { + if st.Untagged != "" { + output += fmt.Sprintln("untagged:", st.Untagged) + } else { + output += fmt.Sprintln("deleted:", st.Deleted) + } + } + spaceReclaimed = report.SpaceReclaimed + } + + return +} + +// RunPrune call the Image Prune API +// This returns the amount of space reclaimed and a detailed output string +func RunPrune(dockerCli *command.DockerCli, all bool) (uint64, string, error) { + return runPrune(dockerCli, pruneOptions{force: true, all: all}) +} diff --git a/command/prune/prune.go b/command/prune/prune.go new file mode 100644 index 0000000000..0b1374eda9 --- /dev/null +++ b/command/prune/prune.go @@ -0,0 +1,39 @@ +package prune + +import ( + "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/container" + "github.com/docker/docker/cli/command/image" + "github.com/docker/docker/cli/command/volume" + "github.com/spf13/cobra" +) + +// NewContainerPruneCommand return a cobra prune command for containers +func NewContainerPruneCommand(dockerCli *command.DockerCli) *cobra.Command { + return container.NewPruneCommand(dockerCli) +} + +// NewVolumePruneCommand return a cobra prune command for volumes +func NewVolumePruneCommand(dockerCli *command.DockerCli) *cobra.Command { + return volume.NewPruneCommand(dockerCli) +} + +// NewImagePruneCommand return a cobra prune command for images +func NewImagePruneCommand(dockerCli *command.DockerCli) *cobra.Command { + return image.NewPruneCommand(dockerCli) +} + +// RunContainerPrune execute a prune command for containers +func RunContainerPrune(dockerCli *command.DockerCli) (uint64, string, error) { + return container.RunPrune(dockerCli) +} + +// RunVolumePrune execute a prune command for volumes +func RunVolumePrune(dockerCli *command.DockerCli) (uint64, string, error) { + return volume.RunPrune(dockerCli) +} + +// RunImagePrune execute a prune command for images +func RunImagePrune(dockerCli *command.DockerCli, all bool) (uint64, string, error) { + return image.RunPrune(dockerCli, all) +} diff --git a/command/system/cmd.go b/command/system/cmd.go index 8ce9d93ae7..46caa2491c 100644 --- a/command/system/cmd.go +++ b/command/system/cmd.go @@ -22,6 +22,8 @@ func NewSystemCommand(dockerCli *command.DockerCli) *cobra.Command { cmd.AddCommand( NewEventsCommand(dockerCli), NewInfoCommand(dockerCli), + NewDiskUsageCommand(dockerCli), + NewPruneCommand(dockerCli), ) return cmd } diff --git a/command/system/df.go b/command/system/df.go new file mode 100644 index 0000000000..085d680fe8 --- /dev/null +++ b/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/command/system/prune.go b/command/system/prune.go new file mode 100644 index 0000000000..4a9e952ada --- /dev/null +++ b/command/system/prune.go @@ -0,0 +1,90 @@ +package system + +import ( + "fmt" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/prune" + units "github.com/docker/go-units" + "github.com/spf13/cobra" +) + +type pruneOptions struct { + force bool + all bool +} + +// NewPruneCommand creates a new cobra.Command for `docker du` +func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts pruneOptions + + cmd := &cobra.Command{ + Use: "prune [COMMAND]", + Short: "Remove unused data.", + Args: cli.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return runPrune(dockerCli, opts) + }, + } + + flags := cmd.Flags() + flags.BoolVarP(&opts.force, "force", "f", false, "Do not prompt for confirmation") + flags.BoolVarP(&opts.all, "all", "a", false, "Remove all unused images not just dangling ones") + + return cmd +} + +const ( + warning = `WARNING! This will remove: + - all stopped containers + - all volumes not used by at least one container + %s +Are you sure you want to continue?` + + danglingImageDesc = "- all dangling images" + allImageDesc = `- all images without at least one container associated to them` +) + +func runPrune(dockerCli *command.DockerCli, opts pruneOptions) error { + var message string + + if opts.all { + message = fmt.Sprintf(warning, allImageDesc) + } else { + message = fmt.Sprintf(warning, danglingImageDesc) + } + + if !opts.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), message) { + return nil + } + + var spaceReclaimed uint64 + + for _, pruneFn := range []func(dockerCli *command.DockerCli) (uint64, string, error){ + prune.RunContainerPrune, + prune.RunVolumePrune, + } { + spc, output, err := pruneFn(dockerCli) + if err != nil { + return err + } + if spc > 0 { + spaceReclaimed += spc + fmt.Fprintln(dockerCli.Out(), output) + } + } + + spc, output, err := prune.RunImagePrune(dockerCli, opts.all) + if err != nil { + return err + } + if spc > 0 { + spaceReclaimed += spc + fmt.Fprintln(dockerCli.Out(), output) + } + + fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed))) + + return nil +} diff --git a/command/utils.go b/command/utils.go index bceb7b335c..e768cf770d 100644 --- a/command/utils.go +++ b/command/utils.go @@ -57,3 +57,25 @@ func PrettyPrint(i interface{}) string { return capitalizeFirst(fmt.Sprintf("%s", t)) } } + +// PromptForConfirmation request and check confirmation from user. +// This will display the provided message followed by ' [y/N] '. If +// the user input 'y' or 'Y' it returns true other false. If no +// message is provided "Are you sure you want to proceeed? [y/N] " +// will be used instead. +func PromptForConfirmation(ins *InStream, outs *OutStream, message string) bool { + if message == "" { + message = "Are you sure you want to proceeed?" + } + message += " [y/N] " + + fmt.Fprintf(outs, message) + + answer := "" + n, _ := fmt.Fscan(ins, &answer) + if n != 1 || (answer != "y" && answer != "Y") { + return false + } + + return true +} diff --git a/command/volume/cmd.go b/command/volume/cmd.go index caf6afcaa3..5f39d3cf33 100644 --- a/command/volume/cmd.go +++ b/command/volume/cmd.go @@ -25,6 +25,7 @@ func NewVolumeCommand(dockerCli *command.DockerCli) *cobra.Command { newInspectCommand(dockerCli), newListCommand(dockerCli), newRemoveCommand(dockerCli), + NewPruneCommand(dockerCli), ) return cmd } diff --git a/command/volume/prune.go b/command/volume/prune.go new file mode 100644 index 0000000000..59f3c94635 --- /dev/null +++ b/command/volume/prune.go @@ -0,0 +1,74 @@ +package volume + +import ( + "fmt" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + units "github.com/docker/go-units" + "github.com/spf13/cobra" +) + +type pruneOptions struct { + force bool +} + +// NewPruneCommand returns a new cobra prune command for volumes +func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts pruneOptions + + cmd := &cobra.Command{ + Use: "prune", + Short: "Remove all unused volumes", + Args: cli.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + spaceReclaimed, output, err := runPrune(dockerCli, opts) + if err != nil { + return err + } + if output != "" { + fmt.Fprintln(dockerCli.Out(), output) + } + fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed))) + return nil + }, + } + + flags := cmd.Flags() + flags.BoolVarP(&opts.force, "force", "f", false, "Do not prompt for confirmation") + + return cmd +} + +const warning = `WARNING! This will remove all volumes not used by at least one container. +Are you sure you want to continue?` + +func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (spaceReclaimed uint64, output string, err error) { + if !opts.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) { + return + } + + report, err := dockerCli.Client().VolumesPrune(context.Background(), types.VolumesPruneConfig{}) + if err != nil { + return + } + + if len(report.VolumesDeleted) > 0 { + output = "Deleted Volumes:\n" + for _, id := range report.VolumesDeleted { + output += id + "\n" + } + spaceReclaimed = report.SpaceReclaimed + } + + return +} + +// RunPrune call the Volume Prune API +// This returns the amount of space reclaimed and a detailed output string +func RunPrune(dockerCli *command.DockerCli) (uint64, string, error) { + return runPrune(dockerCli, pruneOptions{force: true}) +}