Browse Source

Add subcommand prune to the container, volume, image and system commands

Signed-off-by: Kenfe-Mickael Laventure <mickael.laventure@gmail.com>
Kenfe-Mickael Laventure 8 years ago
parent
commit
280c872366

+ 1 - 0
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
 }

+ 74 - 0
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})
+}

+ 1 - 2
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)

+ 1 - 1
cli/command/system/events_utils.go → cli/command/events_utils.go

@@ -1,4 +1,4 @@
-package system
+package command
 
 import (
 	"sync"

+ 2 - 0
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
 }

+ 90 - 0
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})
+}

+ 39 - 0
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)
+}

+ 1 - 0
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
 }

+ 90 - 0
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
+}

+ 22 - 0
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
+}

+ 1 - 0
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
 }

+ 74 - 0
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})
+}

+ 26 - 0
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
+}

+ 26 - 0
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
+}

+ 4 - 0
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)
 }

+ 26 - 0
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
+}