Skip to content
Draft
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
5 changes: 5 additions & 0 deletions cli/command/builder/prune.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,15 +85,20 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
}
}

progress := pruner.NewProgressPrinter(dockerCli, pruner.TypeBuildCache)
resp, err := dockerCli.Client().BuildCachePrune(ctx, client.BuildCachePruneOptions{
All: options.all,
ReservedSpace: options.reservedSpace.Value(),
Filters: pruneFilters,
OnProgress: progress.OnProgress,
})
if err != nil {
return 0, "", err
}
report := resp.Report
if progress.Started {
return report.SpaceReclaimed, "", progress.Finish()
}
if len(report.CachesDeleted) > 0 {
var sb strings.Builder
sb.WriteString("Deleted build cache objects:\n")
Expand Down
7 changes: 6 additions & 1 deletion cli/command/container/prune.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,19 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
}
}

progress := pruner.NewProgressPrinter(dockerCli, pruner.TypeContainer)
res, err := dockerCli.Client().ContainerPrune(ctx, client.ContainerPruneOptions{
Filters: pruneFilters,
Filters: pruneFilters,
OnProgress: progress.OnProgress,
})
if err != nil {
return 0, "", err
}

var out strings.Builder
if progress.Started {
return res.Report.SpaceReclaimed, "", progress.Finish()
}
if len(res.Report.ContainersDeleted) > 0 {
out.WriteString("Deleted Containers:\n")
for _, id := range res.Report.ContainersDeleted {
Expand Down
21 changes: 21 additions & 0 deletions cli/command/container/prune_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import (
"testing"

"github.com/docker/cli/internal/test"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
"gotest.tools/v3/assert"
)

func TestContainerPrunePromptTermination(t *testing.T) {
Expand All @@ -25,3 +27,22 @@ func TestContainerPrunePromptTermination(t *testing.T) {
cmd.SetErr(io.Discard)
test.TerminatePrompt(ctx, t, cmd, cli)
}

func TestContainerPruneProgressBeforeReport(t *testing.T) {
var cli *test.FakeCli
cli = test.NewFakeCli(&fakeClient{
containerPruneFunc: func(_ context.Context, opts client.ContainerPruneOptions) (client.ContainerPruneResult, error) {
assert.Assert(t, opts.OnProgress != nil)
assert.NilError(t, opts.OnProgress(client.PruneProgress{ID: "removed", Action: "deleted"}))
assert.Equal(t, cli.OutBuffer().String(), "Deleted Containers:\nremoved\n")
return client.ContainerPruneResult{Report: container.PruneReport{
ContainersDeleted: []string{"removed"},
SpaceReclaimed: 42,
}}, nil
},
})
cmd := newPruneCommand(cli)
cmd.SetArgs([]string{"--force"})
assert.NilError(t, cmd.Execute())
assert.Equal(t, cli.OutBuffer().String(), "Deleted Containers:\nremoved\n\nTotal reclaimed space: 42B\n")
}
7 changes: 6 additions & 1 deletion cli/command/image/prune.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,19 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
}
}

progress := pruner.NewProgressPrinter(dockerCli, pruner.TypeImage)
res, err := dockerCli.Client().ImagePrune(ctx, client.ImagePruneOptions{
Filters: pruneFilters,
Filters: pruneFilters,
OnProgress: progress.OnProgress,
})
if err != nil {
return 0, "", err
}

var sb strings.Builder
if progress.Started {
return res.Report.SpaceReclaimed, "", progress.Finish()
}
if len(res.Report.ImagesDeleted) > 0 {
sb.WriteString("Deleted Images:\n")
for _, st := range res.Report.ImagesDeleted {
Expand Down
7 changes: 6 additions & 1 deletion cli/command/network/prune.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,19 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
}
}

progress := pruner.NewProgressPrinter(dockerCli, pruner.TypeNetwork)
res, err := dockerCli.Client().NetworkPrune(ctx, client.NetworkPruneOptions{
Filters: pruneFilters,
Filters: pruneFilters,
OnProgress: progress.OnProgress,
})
if err != nil {
return "", err
}

var out strings.Builder
if progress.Started {
return "", progress.Finish()
}
if len(res.Report.NetworksDeleted) > 0 {
out.WriteString("Deleted Networks:\n")
for _, id := range res.Report.NetworksDeleted {
Expand Down
57 changes: 57 additions & 0 deletions cli/command/system/pruner/progress.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package pruner

import (
"fmt"
"io"

"github.com/docker/cli/cli/command"
"github.com/moby/moby/client"
"github.com/moby/moby/client/pkg/versions"
)

// ProgressPrinter prints confirmed deletions as they arrive from the daemon.
type ProgressPrinter struct {
OnProgress func(client.PruneProgress) error
Started bool
out io.Writer
}

// NewProgressPrinter uses streaming when supported by the negotiated API version.
// Older daemons continue to use the final prune report.
func NewProgressPrinter(cli command.Cli, contentType ContentType) *ProgressPrinter {
p := &ProgressPrinter{out: cli.Out()}
if versions.LessThan(cli.CurrentVersion(), "1.56") {
return p
}
headings := map[ContentType]string{
TypeContainer: "Deleted Containers:",
TypeNetwork: "Deleted Networks:",
TypeVolume: "Deleted Volumes:",
TypeImage: "Deleted Images:",
TypeBuildCache: "Deleted build cache objects:",
}
p.OnProgress = func(progress client.PruneProgress) error {
if !p.Started {
if _, err := fmt.Fprintln(p.out, headings[contentType]); err != nil {
return err
}
p.Started = true
}
if contentType == TypeImage {
_, err := fmt.Fprintf(p.out, "%s: %s\n", progress.Action, progress.ID)
return err
}
_, err := fmt.Fprintln(p.out, progress.ID)
return err
}
return p
}

// Finish preserves the blank line after a nonempty prune report.
func (p *ProgressPrinter) Finish() error {
if !p.Started {
return nil
}
_, err := fmt.Fprintln(p.out)
return err
}
71 changes: 71 additions & 0 deletions cli/command/system/pruner/progress_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package pruner

import (
"errors"
"io"
"testing"

"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/streams"
"github.com/docker/cli/internal/test"
"github.com/moby/moby/client"
"gotest.tools/v3/assert"
)

type versionedCLI struct {
command.Cli
version string
}

func (c versionedCLI) CurrentVersion() string { return c.version }

func TestProgressPrinter(t *testing.T) {
for _, tc := range []struct {
contentType ContentType
expected string
}{
{TypeContainer, "Deleted Containers:\nfirst\nsecond\n\n"},
{TypeNetwork, "Deleted Networks:\nfirst\nsecond\n\n"},
{TypeVolume, "Deleted Volumes:\nfirst\nsecond\n\n"},
{TypeImage, "Deleted Images:\nuntagged: first\ndeleted: second\n\n"},
{TypeBuildCache, "Deleted build cache objects:\nfirst\nsecond\n\n"},
} {
t.Run(string(tc.contentType), func(t *testing.T) {
cli := test.NewFakeCli(nil)
p := NewProgressPrinter(versionedCLI{Cli: cli, version: "1.56"}, tc.contentType)
assert.Assert(t, p.OnProgress != nil)
assert.NilError(t, p.OnProgress(client.PruneProgress{ID: "first", Action: "untagged"}))
assert.Assert(t, p.Started)
assert.NilError(t, p.OnProgress(client.PruneProgress{ID: "second", Action: "deleted"}))
assert.NilError(t, p.Finish())
assert.Equal(t, cli.OutBuffer().String(), tc.expected)
})
}
}

func TestProgressPrinterOldAPI(t *testing.T) {
cli := test.NewFakeCli(nil)
p := NewProgressPrinter(versionedCLI{Cli: cli, version: "1.55"}, TypeContainer)
assert.Assert(t, p.OnProgress == nil)
assert.NilError(t, p.Finish())
assert.Equal(t, cli.OutBuffer().String(), "")
}

func TestProgressPrinterEmpty(t *testing.T) {
cli := test.NewFakeCli(nil)
p := NewProgressPrinter(versionedCLI{Cli: cli, version: "1.56"}, TypeContainer)
assert.NilError(t, p.Finish())
assert.Equal(t, cli.OutBuffer().String(), "")
}

type failingWriter struct{}

func (failingWriter) Write([]byte) (int, error) { return 0, io.ErrClosedPipe }

func TestProgressPrinterOutputError(t *testing.T) {
cli := test.NewFakeCli(nil)
cli.SetOut(streams.NewOut(failingWriter{}))
p := NewProgressPrinter(versionedCLI{Cli: cli, version: "1.56"}, TypeContainer)
err := p.OnProgress(client.PruneProgress{ID: "first", Action: "deleted"})
assert.Assert(t, errors.Is(err, io.ErrClosedPipe))
}
7 changes: 6 additions & 1 deletion cli/command/volume/prune.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,19 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
}
}

progress := pruner.NewProgressPrinter(dockerCli, pruner.TypeVolume)
res, err := dockerCli.Client().VolumePrune(ctx, client.VolumePruneOptions{
Filters: pruneFilters,
Filters: pruneFilters,
OnProgress: progress.OnProgress,
})
if err != nil {
return 0, "", err
}

var out strings.Builder
if progress.Started {
return res.Report.SpaceReclaimed, "", progress.Finish()
}
if len(res.Report.VolumesDeleted) > 0 {
out.WriteString("Deleted Volumes:\n")
for _, id := range res.Report.VolumesDeleted {
Expand Down