Skip to content
Open
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
74 changes: 47 additions & 27 deletions cli/command/image/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ type imagesOptions struct {
showDigests bool
format string
filter opts.FilterOpt
calledAs string
tree bool
}

Expand All @@ -42,11 +41,14 @@ func newImagesCommand(dockerCLI command.Cli) *cobra.Command {
if len(args) > 0 {
options.matchName = args[0]
}
// Pass through how the command was invoked. We use this to print
// warnings when an ambiguous argument was passed when using the
// legacy (top-level) "docker images" subcommand.
options.calledAs = cmd.CalledAs()
return runImages(cmd.Context(), dockerCLI, options)
numImages, err := runImages(cmd.Context(), dockerCLI, options)
if err != nil {
return err
}
if numImages == 0 && options.matchName != "" && cmd.CalledAs() == "images" {
printAmbiguousHint(dockerCLI.Err(), options.matchName)
}
return nil
},
Annotations: map[string]string{
"category-top": "7",
Expand Down Expand Up @@ -79,29 +81,22 @@ func newListCommand(dockerCLI command.Cli) *cobra.Command {
return &cmd
}

func runImages(ctx context.Context, dockerCLI command.Cli, options imagesOptions) error {
func runImages(ctx context.Context, dockerCLI command.Cli, options imagesOptions) (int, error) {
filters := options.filter.Value()
if options.matchName != "" {
filters.Add("reference", options.matchName)
}

if options.tree {
if options.quiet {
return errors.New("--quiet is not yet supported with --tree")
}
if options.noTrunc {
return errors.New("--no-trunc is not yet supported with --tree")
}
if options.showDigests {
return errors.New("--show-digest is not yet supported with --tree")
}
if options.format != "" {
return errors.New("--format is not yet supported with --tree")
}
useTree, err := shouldUseTree(options)
if err != nil {
return 0, err
}

if useTree {
return runTree(ctx, dockerCLI, treeOptions{
all: options.all,
filters: filters,
all: options.all,
filters: filters,
expanded: options.tree,
})
}

Expand All @@ -110,7 +105,7 @@ func runImages(ctx context.Context, dockerCLI command.Cli, options imagesOptions
Filters: filters,
})
if err != nil {
return err
return 0, err
}

format := options.format
Expand All @@ -131,12 +126,37 @@ func runImages(ctx context.Context, dockerCLI command.Cli, options imagesOptions
Digest: options.showDigests,
}
if err := formatter.ImageWrite(imageCtx, images); err != nil {
return err
return 0, err
}
if options.matchName != "" && len(images) == 0 && options.calledAs == "images" {
printAmbiguousHint(dockerCLI.Err(), options.matchName)
return len(images), nil
}

func shouldUseTree(options imagesOptions) (bool, error) {
if options.quiet {
if options.tree {
return false, errors.New("--quiet is not yet supported with --tree")
}
return false, nil
}
if options.noTrunc {
if options.tree {
return false, errors.New("--no-trunc is not yet supported with --tree")
}
return false, nil
}
if options.showDigests {
if options.tree {
return false, errors.New("--show-digest is not yet supported with --tree")
}
return false, nil
}
if options.format != "" {
if options.tree {
return false, errors.New("--format is not yet supported with --tree")
}
return false, nil
}
return nil
return true, nil
}

// isDangling is a copy of [formatter.isDangling].
Expand Down
12 changes: 10 additions & 2 deletions cli/command/image/list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func TestNewImagesCommandErrors(t *testing.T) {
cmd := newImagesCommand(test.NewFakeCli(&fakeClient{imageListFunc: tc.imageListFunc}))
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
cmd.SetArgs(tc.args)
cmd.SetArgs(nilToEmptySlice(tc.args))
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
})
}
Expand Down Expand Up @@ -88,7 +88,7 @@ func TestNewImagesCommandSuccess(t *testing.T) {
cmd := newImagesCommand(cli)
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
cmd.SetArgs(tc.args)
cmd.SetArgs(nilToEmptySlice(tc.args))
err := cmd.Execute()
assert.NilError(t, err)
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("list-command-success.%s.golden", tc.name))
Expand All @@ -98,6 +98,7 @@ func TestNewImagesCommandSuccess(t *testing.T) {

func TestNewListCommandAlias(t *testing.T) {
cmd := newListCommand(test.NewFakeCli(&fakeClient{}))
cmd.SetArgs([]string{""})
assert.Check(t, cmd.HasAlias("list"))
assert.Check(t, !cmd.HasAlias("other"))
}
Expand All @@ -115,3 +116,10 @@ func TestNewListCommandAmbiguous(t *testing.T) {
assert.NilError(t, err)
golden.Assert(t, cli.ErrBuffer().String(), "list-command-ambiguous.golden")
}

func nilToEmptySlice[T any](s []T) []T {
if s == nil {
return []T{}
}
return s
}
5 changes: 3 additions & 2 deletions cli/command/image/remove_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func (notFound) NotFound() {}

func TestNewRemoveCommandAlias(t *testing.T) {
cmd := newImageRemoveCommand(test.NewFakeCli(&fakeClient{}))
cmd.SetArgs([]string{""})
assert.Check(t, cmd.HasAlias("rmi"))
assert.Check(t, cmd.HasAlias("remove"))
assert.Check(t, !cmd.HasAlias("other"))
Expand Down Expand Up @@ -69,7 +70,7 @@ func TestNewRemoveCommandErrors(t *testing.T) {
}))
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
cmd.SetArgs(tc.args)
cmd.SetArgs(nilToEmptySlice(tc.args))
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
})
}
Expand Down Expand Up @@ -126,7 +127,7 @@ func TestNewRemoveCommandSuccess(t *testing.T) {
cmd := newRemoveCommand(cli)
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
cmd.SetArgs(tc.args)
cmd.SetArgs(nilToEmptySlice(tc.args))
assert.NilError(t, cmd.Execute())
assert.Check(t, is.Equal(tc.expectedStderr, cli.ErrBuffer().String()))
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("remove-command-success.%s.golden", tc.name))
Expand Down
1 change: 1 addition & 0 deletions cli/command/image/testdata/list-command-ambiguous.golden
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
WARNING: This output is designed for human readability. For machine-readable output, please use --format.

No images found matching "ls": did you mean "docker image ls"?
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
REPOSITORY TAG IMAGE ID CREATED SIZE
Info -> U In Use
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
2 changes: 2 additions & 0 deletions cli/command/image/testdata/list-command-success.format.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Info -> U In Use
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
REPOSITORY TAG IMAGE ID CREATED SIZE
Info -> U In Use
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
REPOSITORY TAG IMAGE ID CREATED SIZE
Info -> U In Use
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
40 changes: 22 additions & 18 deletions cli/command/image/tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ import (
)

type treeOptions struct {
all bool
filters client.Filters
all bool
filters client.Filters
expanded bool
}

type treeView struct {
Expand All @@ -35,14 +36,14 @@ type treeView struct {
imageSpacing bool
}

func runTree(ctx context.Context, dockerCLI command.Cli, opts treeOptions) error {
func runTree(ctx context.Context, dockerCLI command.Cli, opts treeOptions) (int, error) {
images, err := dockerCLI.Client().ImageList(ctx, client.ImageListOptions{
All: opts.all,
Filters: opts.filters,
Manifests: true,
})
if err != nil {
return err
return 0, err
}
if !opts.all {
images = slices.DeleteFunc(images, isDangling)
Expand All @@ -54,7 +55,7 @@ func runTree(ctx context.Context, dockerCLI command.Cli, opts treeOptions) error
attested := make(map[digest.Digest]bool)

for _, img := range images {
details := imageDetails{
topDetails := imageDetails{
ID: img.ID,
DiskUsage: units.HumanSizeWithPrecision(float64(img.Size), 3),
InUse: img.Containers > 0,
Expand All @@ -73,33 +74,38 @@ func runTree(ctx context.Context, dockerCLI command.Cli, opts treeOptions) error
continue
}

inUse := len(im.ImageData.Containers) > 0
if inUse {
// Mark top-level parent image as used if any of its subimages are used.
topDetails.InUse = true
}

if !opts.expanded {
continue
}

sub := subImage{
Platform: platforms.Format(im.ImageData.Platform),
Available: im.Available,
Details: imageDetails{
ID: im.ID,
DiskUsage: units.HumanSizeWithPrecision(float64(im.Size.Total), 3),
InUse: len(im.ImageData.Containers) > 0,
InUse: inUse,
ContentSize: units.HumanSizeWithPrecision(float64(im.Size.Content), 3),
},
}

if sub.Details.InUse {
// Mark top-level parent image as used if any of its subimages are used.
details.InUse = true
}

children = append(children, sub)

// Add extra spacing between images if there's at least one entry with children.
view.imageSpacing = true
}

details.ContentSize = units.HumanSizeWithPrecision(float64(totalContent), 3)
topDetails.ContentSize = units.HumanSizeWithPrecision(float64(totalContent), 3)

view.images = append(view.images, topImage{
Names: img.RepoTags,
Details: details,
Details: topDetails,
Children: children,
created: img.Created,
})
Expand All @@ -109,7 +115,8 @@ func runTree(ctx context.Context, dockerCLI command.Cli, opts treeOptions) error
return view.images[i].created > view.images[j].created
})

return printImageTree(dockerCLI, view)
printImageTree(dockerCLI, view)
return len(view.images), nil
}

type imageDetails struct {
Expand Down Expand Up @@ -192,7 +199,7 @@ func getPossibleChips(view treeView) (chips []imageChip) {
return possible
}

func printImageTree(dockerCLI command.Cli, view treeView) error {
func printImageTree(dockerCLI command.Cli, view treeView) {
if streamRedirected(dockerCLI.Out()) {
_, _ = fmt.Fprintln(dockerCLI.Err(), "WARNING: This output is designed for human readability. For machine-readable output, please use --format.")
}
Expand Down Expand Up @@ -299,8 +306,6 @@ func printImageTree(dockerCLI command.Cli, view treeView) error {
printChildren(out, columns, img, normalColor)
_, _ = fmt.Fprintln(out)
}

return nil
}

// adjustColumns adjusts the width of the first column to maximize the space
Expand Down Expand Up @@ -342,7 +347,6 @@ func generateLegend(out tui.Output, width uint) string {
legend += " |"
}
}
legend += " "

r := int(width) - tui.Width(legend)
if r < 0 {
Expand Down