From 280c8723667af385e0807a090ddc5cc57c46807e Mon Sep 17 00:00:00 2001 From: Kenfe-Mickael Laventure Date: Thu, 22 Sep 2016 14:04:34 -0700 Subject: [PATCH] Add subcommand prune to the container, volume, image and system commands Signed-off-by: Kenfe-Mickael Laventure --- cli/command/container/cmd.go | 1 + cli/command/container/prune.go | 74 +++++++++++++++++++ cli/command/container/stats.go | 3 +- cli/command/{system => }/events_utils.go | 2 +- cli/command/image/cmd.go | 2 + cli/command/image/prune.go | 90 ++++++++++++++++++++++++ cli/command/prune/prune.go | 39 ++++++++++ cli/command/system/cmd.go | 1 + cli/command/system/prune.go | 90 ++++++++++++++++++++++++ cli/command/utils.go | 22 ++++++ cli/command/volume/cmd.go | 1 + cli/command/volume/prune.go | 74 +++++++++++++++++++ client/container_prune.go | 26 +++++++ client/image_prune.go | 26 +++++++ client/interface.go | 4 ++ client/volume_prune.go | 26 +++++++ 16 files changed, 478 insertions(+), 3 deletions(-) create mode 100644 cli/command/container/prune.go rename cli/command/{system => }/events_utils.go (98%) create mode 100644 cli/command/image/prune.go create mode 100644 cli/command/prune/prune.go create mode 100644 cli/command/system/prune.go create mode 100644 cli/command/volume/prune.go create mode 100644 client/container_prune.go create mode 100644 client/image_prune.go create mode 100644 client/volume_prune.go diff --git a/cli/command/container/cmd.go b/cli/command/container/cmd.go index da9ea6d41d..f06b863b58 100644 --- a/cli/command/container/cmd.go +++ b/cli/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/cli/command/container/prune.go b/cli/command/container/prune.go new file mode 100644 index 0000000000..13e283a8b2 --- /dev/null +++ b/cli/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/cli/command/container/stats.go b/cli/command/container/stats.go index 394302d087..2e3714486b 100644 --- a/cli/command/container/stats.go +++ b/cli/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/cli/command/system/events_utils.go b/cli/command/events_utils.go similarity index 98% rename from cli/command/system/events_utils.go rename to cli/command/events_utils.go index b0dd909d15..e710c97576 100644 --- a/cli/command/system/events_utils.go +++ b/cli/command/events_utils.go @@ -1,4 +1,4 @@ -package system +package command import ( "sync" diff --git a/cli/command/image/cmd.go b/cli/command/image/cmd.go index f60ffeeb8f..6f8e7b7d4b 100644 --- a/cli/command/image/cmd.go +++ b/cli/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/cli/command/image/prune.go b/cli/command/image/prune.go new file mode 100644 index 0000000000..6944664a54 --- /dev/null +++ b/cli/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/cli/command/prune/prune.go b/cli/command/prune/prune.go new file mode 100644 index 0000000000..0b1374eda9 --- /dev/null +++ b/cli/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/cli/command/system/cmd.go b/cli/command/system/cmd.go index 8ce9d93ae7..f967c1b72e 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), + NewPruneCommand(dockerCli), ) return cmd } diff --git a/cli/command/system/prune.go b/cli/command/system/prune.go new file mode 100644 index 0000000000..4a9e952ada --- /dev/null +++ b/cli/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/cli/command/utils.go b/cli/command/utils.go index bceb7b335c..e768cf770d 100644 --- a/cli/command/utils.go +++ b/cli/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/cli/command/volume/cmd.go b/cli/command/volume/cmd.go index caf6afcaa3..5f39d3cf33 100644 --- a/cli/command/volume/cmd.go +++ b/cli/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/cli/command/volume/prune.go b/cli/command/volume/prune.go new file mode 100644 index 0000000000..59f3c94635 --- /dev/null +++ b/cli/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}) +} diff --git a/client/container_prune.go b/client/container_prune.go new file mode 100644 index 0000000000..0d8bd3292c --- /dev/null +++ b/client/container_prune.go @@ -0,0 +1,26 @@ +package client + +import ( + "encoding/json" + "fmt" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +// ContainersPrune requests the daemon to delete unused data +func (cli *Client) ContainersPrune(ctx context.Context, cfg types.ContainersPruneConfig) (types.ContainersPruneReport, error) { + var report types.ContainersPruneReport + + serverResp, err := cli.post(ctx, "/containers/prune", nil, cfg, nil) + if err != nil { + return report, err + } + defer ensureReaderClosed(serverResp) + + if err := json.NewDecoder(serverResp.body).Decode(&report); err != nil { + return report, fmt.Errorf("Error retrieving disk usage: %v", err) + } + + return report, nil +} diff --git a/client/image_prune.go b/client/image_prune.go new file mode 100644 index 0000000000..f6752e5043 --- /dev/null +++ b/client/image_prune.go @@ -0,0 +1,26 @@ +package client + +import ( + "encoding/json" + "fmt" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +// ImagesPrune requests the daemon to delete unused data +func (cli *Client) ImagesPrune(ctx context.Context, cfg types.ImagesPruneConfig) (types.ImagesPruneReport, error) { + var report types.ImagesPruneReport + + serverResp, err := cli.post(ctx, "/images/prune", nil, cfg, nil) + if err != nil { + return report, err + } + defer ensureReaderClosed(serverResp) + + if err := json.NewDecoder(serverResp.body).Decode(&report); err != nil { + return report, fmt.Errorf("Error retrieving disk usage: %v", err) + } + + return report, nil +} diff --git a/client/interface.go b/client/interface.go index 81320918b3..de06b848ae 100644 --- a/client/interface.go +++ b/client/interface.go @@ -61,6 +61,7 @@ type ContainerAPIClient interface { ContainerWait(ctx context.Context, container string) (int, error) CopyFromContainer(ctx context.Context, container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) CopyToContainer(ctx context.Context, container, path string, content io.Reader, options types.CopyToContainerOptions) error + ContainersPrune(ctx context.Context, cfg types.ContainersPruneConfig) (types.ContainersPruneReport, error) } // ImageAPIClient defines API client methods for the images @@ -78,6 +79,7 @@ type ImageAPIClient interface { ImageSearch(ctx context.Context, term string, options types.ImageSearchOptions) ([]registry.SearchResult, error) ImageSave(ctx context.Context, images []string) (io.ReadCloser, error) ImageTag(ctx context.Context, image, ref string) error + ImagesPrune(ctx context.Context, cfg types.ImagesPruneConfig) (types.ImagesPruneReport, error) } // NetworkAPIClient defines API client methods for the networks @@ -124,6 +126,7 @@ type SystemAPIClient interface { Events(ctx context.Context, options types.EventsOptions) (<-chan events.Message, <-chan error) Info(ctx context.Context) (types.Info, error) RegistryLogin(ctx context.Context, auth types.AuthConfig) (types.AuthResponse, error) + DiskUsage(ctx context.Context) (types.DiskUsage, error) } // VolumeAPIClient defines API client methods for the volumes @@ -133,4 +136,5 @@ type VolumeAPIClient interface { VolumeInspectWithRaw(ctx context.Context, volumeID string) (types.Volume, []byte, error) VolumeList(ctx context.Context, filter filters.Args) (types.VolumesListResponse, error) VolumeRemove(ctx context.Context, volumeID string, force bool) error + VolumesPrune(ctx context.Context, cfg types.VolumesPruneConfig) (types.VolumesPruneReport, error) } diff --git a/client/volume_prune.go b/client/volume_prune.go new file mode 100644 index 0000000000..e7ea7b591d --- /dev/null +++ b/client/volume_prune.go @@ -0,0 +1,26 @@ +package client + +import ( + "encoding/json" + "fmt" + + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +// VolumesPrune requests the daemon to delete unused data +func (cli *Client) VolumesPrune(ctx context.Context, cfg types.VolumesPruneConfig) (types.VolumesPruneReport, error) { + var report types.VolumesPruneReport + + serverResp, err := cli.post(ctx, "/volumes/prune", nil, cfg, nil) + if err != nil { + return report, err + } + defer ensureReaderClosed(serverResp) + + if err := json.NewDecoder(serverResp.body).Decode(&report); err != nil { + return report, fmt.Errorf("Error retrieving disk usage: %v", err) + } + + return report, nil +}