Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

build: add options to builder prune #1327

Merged
merged 2 commits into from
Sep 5, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 67 additions & 2 deletions cli/command/builder/prune.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,94 @@ package builder
import (
"context"
"fmt"
"strings"

"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types"
units "github.com/docker/go-units"
"github.com/spf13/cobra"
)

type pruneOptions struct {
force bool
all bool
filter opts.FilterOpt
keepStorage opts.MemBytes
}

// NewPruneCommand returns a new cobra prune command for images
func NewPruneCommand(dockerCli command.Cli) *cobra.Command {
options := pruneOptions{filter: opts.NewFilterOpt()}

cmd := &cobra.Command{
Use: "prune",
Short: "Remove build cache",
Args: cli.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
report, err := dockerCli.Client().BuildCachePrune(context.Background())
spaceReclaimed, output, err := runPrune(dockerCli, options)
if err != nil {
return err
}
fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(report.SpaceReclaimed)))
if output != "" {
fmt.Fprintln(dockerCli.Out(), output)
}
fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed)))
return nil
},
Annotations: map[string]string{"version": "1.39"},
}

flags := cmd.Flags()
flags.BoolVarP(&options.force, "force", "f", false, "Do not prompt for confirmation")
flags.BoolVarP(&options.all, "all", "a", false, "Remove all unused images, not just dangling ones")
flags.Var(&options.filter, "filter", "Provide filter values (e.g. 'max-age=24h')")
flags.Var(&options.keepStorage, "keep-storage", "Amount of disk space to keep for cache")

return cmd
}

const (
normalWarning = `WARNING! This will remove all dangling build cache. Are you sure you want to continue?`
allCacheWarning = `WARNING! This will remove all build cache. Are you sure you want to continue?`
)

func runPrune(dockerCli command.Cli, options pruneOptions) (spaceReclaimed uint64, output string, err error) {
pruneFilters := options.filter.Value()
pruneFilters = command.PruneFilters(dockerCli, pruneFilters)

warning := normalWarning
if options.all {
warning = allCacheWarning
}
if !options.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) {
return 0, "", nil
}

report, err := dockerCli.Client().BuildCachePrune(context.Background(), types.BuildCachePruneOptions{
All: options.all,
KeepStorage: options.keepStorage.Value(),
Filters: pruneFilters,
})
if err != nil {
return 0, "", err
}

if len(report.CachesDeleted) > 0 {
var sb strings.Builder
sb.WriteString("Deleted build cache objects:\n")
for _, id := range report.CachesDeleted {
sb.WriteString(id)
sb.WriteByte('\n')
}
output = sb.String()
}

return report.SpaceReclaimed, output, nil
}

// CachePrune executes a prune command for build cache
func CachePrune(dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error) {
return runPrune(dockerCli, pruneOptions{force: true, all: all, filter: filter})
}
2 changes: 1 addition & 1 deletion cli/command/container/prune.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,6 @@ func runPrune(dockerCli command.Cli, options pruneOptions) (spaceReclaimed uint6

// RunPrune calls the Container Prune API
// This returns the amount of space reclaimed and a detailed output string
func RunPrune(dockerCli command.Cli, filter opts.FilterOpt) (uint64, string, error) {
func RunPrune(dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error) {
return runPrune(dockerCli, pruneOptions{force: true, filter: filter})
}
165 changes: 165 additions & 0 deletions cli/command/formatter/buildcache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package formatter

import (
"fmt"
"sort"
"strings"
"time"

"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/go-units"
)

const (
defaultBuildCacheTableFormat = "table {{.ID}}\t{{.Type}}\t{{.Size}}\t{{.CreatedSince}}\t{{.LastUsedSince}}\t{{.UsageCount}}\t{{.Shared}}\t{{.Description}}"

cacheIDHeader = "CACHE ID"
parentHeader = "PARENT"
lastUsedSinceHeader = "LAST USED"
usageCountHeader = "USAGE"
inUseHeader = "IN USE"
sharedHeader = "SHARED"
)

// NewBuildCacheFormat returns a Format for rendering using a Context
func NewBuildCacheFormat(source string, quiet bool) Format {
switch source {
case TableFormatKey:
if quiet {
return defaultQuietFormat
}
return Format(defaultBuildCacheTableFormat)
case RawFormatKey:
if quiet {
return `build_cache_id: {{.ID}}`
}
format := `build_cache_id: {{.ID}}
parent_id: {{.Parent}}
type: {{.Type}}
description: {{.Description}}
created_at: {{.CreatedSince}}
last_used_at: {{.LastUsedSince}}
usage_count: {{.UsageCount}}
in_use: {{.InUse}}
shared: {{.Shared}}
`
return Format(format)
}
return Format(source)
}

func buildCacheSort(buildCache []*types.BuildCache) {
sort.Slice(buildCache, func(i, j int) bool {
lui, luj := buildCache[i].LastUsedAt, buildCache[j].LastUsedAt
switch {
case lui == nil && luj == nil:
return strings.Compare(buildCache[i].ID, buildCache[j].ID) < 0
case lui == nil:
return true
case luj == nil:
return false
case lui.Equal(*luj):
return strings.Compare(buildCache[i].ID, buildCache[j].ID) < 0
default:
return lui.Before(*luj)
}
})
}

// BuildCacheWrite renders the context for a list of containers
func BuildCacheWrite(ctx Context, buildCaches []*types.BuildCache) error {
render := func(format func(subContext subContext) error) error {
buildCacheSort(buildCaches)
for _, bc := range buildCaches {
err := format(&buildCacheContext{trunc: ctx.Trunc, v: bc})
if err != nil {
return err
}
}
return nil
}
return ctx.Write(newBuildCacheContext(), render)
}

type buildCacheHeaderContext map[string]string

type buildCacheContext struct {
HeaderContext
trunc bool
v *types.BuildCache
}

func newBuildCacheContext() *buildCacheContext {
buildCacheCtx := buildCacheContext{}
buildCacheCtx.header = buildCacheHeaderContext{
"ID": cacheIDHeader,
"Parent": parentHeader,
"Type": typeHeader,
"Size": sizeHeader,
"CreatedSince": createdSinceHeader,
"LastUsedSince": lastUsedSinceHeader,
"UsageCount": usageCountHeader,
"InUse": inUseHeader,
"Shared": sharedHeader,
"Description": descriptionHeader,
}
return &buildCacheCtx
}

func (c *buildCacheContext) MarshalJSON() ([]byte, error) {
return marshalJSON(c)
}

func (c *buildCacheContext) ID() string {
id := c.v.ID
if c.trunc {
id = stringid.TruncateID(c.v.ID)
}
if c.v.InUse {
return id + "*"
}
return id
}

func (c *buildCacheContext) Parent() string {
if c.trunc {
return stringid.TruncateID(c.v.Parent)
}
return c.v.Parent
}

func (c *buildCacheContext) Type() string {
return c.v.Type
}

func (c *buildCacheContext) Description() string {
return c.v.Description
}

func (c *buildCacheContext) Size() string {
return units.HumanSizeWithPrecision(float64(c.v.Size), 3)
}

func (c *buildCacheContext) CreatedSince() string {
return units.HumanDuration(time.Now().UTC().Sub(c.v.CreatedAt)) + " ago"
}

func (c *buildCacheContext) LastUsedSince() string {
if c.v.LastUsedAt == nil {
return ""
}
return units.HumanDuration(time.Now().UTC().Sub(*c.v.LastUsedAt)) + " ago"
}

func (c *buildCacheContext) UsageCount() string {
return fmt.Sprintf("%d", c.v.UsageCount)
}

func (c *buildCacheContext) InUse() string {
return fmt.Sprintf("%t", c.v.InUse)
}

func (c *buildCacheContext) Shared() string {
return fmt.Sprintf("%t", c.v.Shared)
}
36 changes: 17 additions & 19 deletions cli/command/formatter/disk_usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,19 @@ import (
)

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}}"
defaultBuildCacheVerboseFormat = `
ID: {{.ID}}
Description: {{.Description}}
Mutable: {{.Mutable}}
Size: {{.Size}}
CreatedAt: {{.CreatedAt}}
LastUsedAt: {{.LastUsedAt}}
UsageCount: {{.UsageCount}}
`
defaultDiskUsageImageTableFormat = "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedSince}}\t{{.VirtualSize}}\t{{.SharedSize}}\t{{.UniqueSize}}\t{{.Containers}}"
defaultDiskUsageContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.LocalVolumes}}\t{{.Size}}\t{{.RunningFor}}\t{{.Status}}\t{{.Names}}"
defaultDiskUsageVolumeTableFormat = "table {{.Name}}\t{{.Links}}\t{{.Size}}"
defaultDiskUsageTableFormat = "table {{.Type}}\t{{.TotalCount}}\t{{.Active}}\t{{.Size}}\t{{.Reclaimable}}"
defaultDiskUsageBuildCacheTableFormat = "table {{.ID}}\t{{.Type}}\t{{.Size}}\t{{.CreatedSince}}\t{{.LastUsedSince}}\t{{.UsageCount}}\t{{.Shared}}"

typeHeader = "TYPE"
totalHeader = "TOTAL"
activeHeader = "ACTIVE"
reclaimableHeader = "RECLAIMABLE"
containersHeader = "CONTAINERS"
sharedSizeHeader = "SHARED SIZE"
uniqueSizeHeader = "UNIQUE SiZE"
uniqueSizeHeader = "UNIQUE SIZE"
)

// DiskUsageContext contains disk usage specific information required by the formatter, encapsulate a Context struct.
Expand All @@ -56,7 +48,6 @@ func (ctx *DiskUsageContext) startSubsection(format string) (*template.Template,
return ctx.parseFormat()
}

//
// NewDiskUsageFormat returns a format for rendering an DiskUsageContext
func NewDiskUsageFormat(source string) Format {
switch source {
Expand Down Expand Up @@ -129,6 +120,7 @@ func (ctx *DiskUsageContext) Write() (err error) {
return err
}

// nolint: gocyclo
func (ctx *DiskUsageContext) verboseWrite() error {
// First images
tmpl, err := ctx.startSubsection(defaultDiskUsageImageTableFormat)
Expand Down Expand Up @@ -196,11 +188,17 @@ func (ctx *DiskUsageContext) verboseWrite() error {
// And build cache
fmt.Fprintf(ctx.Output, "\nBuild cache usage: %s\n\n", units.HumanSize(float64(ctx.BuilderSize)))

t := template.Must(template.New("buildcache").Parse(defaultBuildCacheVerboseFormat))

tmpl, err = ctx.startSubsection(defaultDiskUsageBuildCacheTableFormat)
if err != nil {
return err
}
buildCacheSort(ctx.BuildCache)
for _, v := range ctx.BuildCache {
t.Execute(ctx.Output, *v)
if err := ctx.contextFormat(tmpl, &buildCacheContext{v: v, trunc: true}); err != nil {
return err
}
}
ctx.postFormat(tmpl, newBuildCacheContext())

return nil
}
Expand Down Expand Up @@ -416,7 +414,7 @@ func (c *diskUsageBuilderContext) Size() string {
func (c *diskUsageBuilderContext) Reclaimable() string {
var inUseBytes int64
for _, bc := range c.buildCache {
if bc.InUse {
if bc.InUse && !bc.Shared {
inUseBytes += bc.Size
}
}
Expand Down
5 changes: 3 additions & 2 deletions cli/command/formatter/disk_usage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,19 @@ Build Cache 0 0 0B
DiskUsageContext{Verbose: true},
`Images space usage:

REPOSITORY TAG IMAGE ID CREATED ago SIZE SHARED SIZE UNIQUE SiZE CONTAINERS
REPOSITORY TAG IMAGE ID CREATED SIZE SHARED SIZE UNIQUE SIZE CONTAINERS

Containers space usage:

CONTAINER ID IMAGE COMMAND LOCAL VOLUMES SIZE CREATED ago STATUS NAMES
CONTAINER ID IMAGE COMMAND LOCAL VOLUMES SIZE CREATED STATUS NAMES

Local Volumes space usage:

VOLUME NAME LINKS SIZE

Build cache usage: 0B

CACHE ID TYPE SIZE CREATED LAST USED USAGE SHARED
`,
},
// Errors
Expand Down
2 changes: 1 addition & 1 deletion cli/command/network/prune.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func runPrune(dockerCli command.Cli, options pruneOptions) (output string, err e

// RunPrune calls the Network Prune API
// This returns the amount of space reclaimed and a detailed output string
func RunPrune(dockerCli command.Cli, filter opts.FilterOpt) (uint64, string, error) {
func RunPrune(dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error) {
output, err := runPrune(dockerCli, pruneOptions{force: true, filter: filter})
return 0, output, err
}
Loading