mirror of https://github.com/docker/cli.git
Merge pull request #26108 from mlaventure/data-mngt
New Data Management commands
This commit is contained in:
commit
71ffe11776
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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})
|
||||||
|
}
|
|
@ -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,4 +1,4 @@
|
||||||
package system
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
|
@ -23,6 +23,7 @@ const (
|
||||||
statusHeader = "STATUS"
|
statusHeader = "STATUS"
|
||||||
portsHeader = "PORTS"
|
portsHeader = "PORTS"
|
||||||
mountsHeader = "MOUNTS"
|
mountsHeader = "MOUNTS"
|
||||||
|
localVolumes = "LOCAL VOLUMES"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewContainerFormat returns a Format for rendering using a Context
|
// NewContainerFormat returns a Format for rendering using a Context
|
||||||
|
@ -199,3 +200,16 @@ func (c *containerContext) Mounts() string {
|
||||||
}
|
}
|
||||||
return strings.Join(mounts, ",")
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -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 := "<none>"
|
||||||
|
tag := "<none>"
|
||||||
|
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)))
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
package formatter
|
package formatter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
|
@ -225,5 +226,35 @@ func (c *imageContext) CreatedAt() string {
|
||||||
|
|
||||||
func (c *imageContext) Size() string {
|
func (c *imageContext) Size() string {
|
||||||
c.AddHeader(sizeHeader)
|
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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ func TestImageContext(t *testing.T) {
|
||||||
trunc: false,
|
trunc: false,
|
||||||
}, imageID, imageIDHeader, ctx.ID},
|
}, imageID, imageIDHeader, ctx.ID},
|
||||||
{imageContext{
|
{imageContext{
|
||||||
i: types.Image{Size: 10},
|
i: types.Image{Size: 10, VirtualSize: 10},
|
||||||
trunc: true,
|
trunc: true,
|
||||||
}, "10 B", sizeHeader, ctx.Size},
|
}, "10 B", sizeHeader, ctx.Size},
|
||||||
{imageContext{
|
{imageContext{
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
|
units "github.com/docker/go-units"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -12,6 +13,7 @@ const (
|
||||||
defaultVolumeTableFormat = "table {{.Driver}}\t{{.Name}}"
|
defaultVolumeTableFormat = "table {{.Driver}}\t{{.Name}}"
|
||||||
|
|
||||||
mountpointHeader = "MOUNTPOINT"
|
mountpointHeader = "MOUNTPOINT"
|
||||||
|
linksHeader = "LINKS"
|
||||||
// Status header ?
|
// Status header ?
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -96,3 +98,19 @@ func (c *volumeContext) Label(name string) string {
|
||||||
}
|
}
|
||||||
return c.v.Labels[name]
|
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))
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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})
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -22,6 +22,8 @@ func NewSystemCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
NewEventsCommand(dockerCli),
|
NewEventsCommand(dockerCli),
|
||||||
NewInfoCommand(dockerCli),
|
NewInfoCommand(dockerCli),
|
||||||
|
NewDiskUsageCommand(dockerCli),
|
||||||
|
NewPruneCommand(dockerCli),
|
||||||
)
|
)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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})
|
||||||
|
}
|
Loading…
Reference in New Issue