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),
 		NewWaitCommand(dockerCli),
 		newListCommand(dockerCli),
 		newListCommand(dockerCli),
 		newInspectCommand(dockerCli),
 		newInspectCommand(dockerCli),
+		NewPruneCommand(dockerCli),
 	)
 	)
 	return cmd
 	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"
 	"github.com/docker/docker/cli/command"
 	"github.com/docker/docker/cli/command"
 	"github.com/docker/docker/cli/command/formatter"
 	"github.com/docker/docker/cli/command/formatter"
-	"github.com/docker/docker/cli/command/system"
 	"github.com/spf13/cobra"
 	"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
 		// retrieving the list of running containers to avoid a race where we
 		// would "miss" a creation.
 		// would "miss" a creation.
 		started := make(chan struct{})
 		started := make(chan struct{})
-		eh := system.InitEventHandler()
+		eh := command.InitEventHandler()
 		eh.Handle("create", func(e events.Message) {
 		eh.Handle("create", func(e events.Message) {
 			if opts.all {
 			if opts.all {
 				s := formatter.NewContainerStats(e.ID[:12], daemonOSType)
 				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 (
 import (
 	"sync"
 	"sync"

+ 2 - 0
cli/command/image/cmd.go

@@ -31,6 +31,8 @@ func NewImageCommand(dockerCli *command.DockerCli) *cobra.Command {
 		newListCommand(dockerCli),
 		newListCommand(dockerCli),
 		newRemoveCommand(dockerCli),
 		newRemoveCommand(dockerCli),
 		newInspectCommand(dockerCli),
 		newInspectCommand(dockerCli),
+		NewPruneCommand(dockerCli),
 	)
 	)
+
 	return cmd
 	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(
 	cmd.AddCommand(
 		NewEventsCommand(dockerCli),
 		NewEventsCommand(dockerCli),
 		NewInfoCommand(dockerCli),
 		NewInfoCommand(dockerCli),
+		NewPruneCommand(dockerCli),
 	)
 	)
 	return cmd
 	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))
 		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),
 		newInspectCommand(dockerCli),
 		newListCommand(dockerCli),
 		newListCommand(dockerCli),
 		newRemoveCommand(dockerCli),
 		newRemoveCommand(dockerCli),
+		NewPruneCommand(dockerCli),
 	)
 	)
 	return cmd
 	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)
 	ContainerWait(ctx context.Context, container string) (int, error)
 	CopyFromContainer(ctx context.Context, container, srcPath string) (io.ReadCloser, types.ContainerPathStat, 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
 	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
 // 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)
 	ImageSearch(ctx context.Context, term string, options types.ImageSearchOptions) ([]registry.SearchResult, error)
 	ImageSave(ctx context.Context, images []string) (io.ReadCloser, error)
 	ImageSave(ctx context.Context, images []string) (io.ReadCloser, error)
 	ImageTag(ctx context.Context, image, ref string) 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
 // 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)
 	Events(ctx context.Context, options types.EventsOptions) (<-chan events.Message, <-chan error)
 	Info(ctx context.Context) (types.Info, error)
 	Info(ctx context.Context) (types.Info, error)
 	RegistryLogin(ctx context.Context, auth types.AuthConfig) (types.AuthResponse, 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
 // 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)
 	VolumeInspectWithRaw(ctx context.Context, volumeID string) (types.Volume, []byte, error)
 	VolumeList(ctx context.Context, filter filters.Args) (types.VolumesListResponse, error)
 	VolumeList(ctx context.Context, filter filters.Args) (types.VolumesListResponse, error)
 	VolumeRemove(ctx context.Context, volumeID string, force bool) 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
+}