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

image/list: Add --tree flag #4982

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
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
25 changes: 25 additions & 0 deletions cli/command/image/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package image

import (
"context"
"errors"
"fmt"
"io"

Expand All @@ -24,6 +25,7 @@ type imagesOptions struct {
format string
filter opts.FilterOpt
calledAs string
tree bool
}

// NewImagesCommand creates a new `docker images` command
Expand Down Expand Up @@ -59,6 +61,9 @@ func NewImagesCommand(dockerCLI command.Cli) *cobra.Command {
flags.StringVar(&options.format, "format", "", flagsHelper.FormatHelp)
flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided")

flags.BoolVar(&options.tree, "tree", false, "List multi-platform images tree [experimental, behavior may change]")
flags.SetAnnotation("tree", "api", []string{"1.47"})

return cmd
}

Expand All @@ -75,6 +80,26 @@ func runImages(ctx context.Context, dockerCLI command.Cli, options imagesOptions
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")
}

return runTree(ctx, dockerCLI, treeOptions{
all: options.all,
filters: filters,
})
}

images, err := dockerCLI.Client().ImageList(ctx, image.ListOptions{
All: options.all,
Filters: filters,
Expand Down
252 changes: 252 additions & 0 deletions cli/command/image/tree.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
package image

import (
"context"
"fmt"
"strings"
"unicode/utf8"

"github.com/docker/cli/cli/command"
"github.com/morikuni/aec"

"github.com/containerd/platforms"
"github.com/docker/docker/api/types/filters"
imagetypes "github.com/docker/docker/api/types/image"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/go-units"
)

type treeOptions struct {
all bool
filters filters.Args
}

func runTree(ctx context.Context, dockerCLI command.Cli, opts treeOptions) error {
images, err := dockerCLI.Client().ImageList(ctx, imagetypes.ListOptions{
All: opts.all,
ContainerCount: true,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filters: opts.filters,
Manifests: true,
})
if err != nil {
return err
}

view := make([]topImage, 0, len(images))
for _, img := range images {
details := imageDetails{
ID: img.ID,
DiskUsage: units.HumanSizeWithPrecision(float64(img.Size), 3),
Used: img.Containers > 0,
}

children := make([]subImage, 0, len(img.Manifests))
for _, im := range img.Manifests {
if im.Kind != imagetypes.ImageManifestKindImage {
continue
}

imgData := im.ImageData
platform := imgData.Platform

sub := subImage{
Platform: platforms.Format(platform),
Available: im.Available,
Details: imageDetails{
ID: im.ID,
DiskUsage: units.HumanSizeWithPrecision(float64(im.ContentSize+imgData.UnpackedSize), 3),
Used: len(imgData.Containers) > 0,
},
}

children = append(children, sub)
}

view = append(view, topImage{
Names: img.RepoTags,
Details: details,
Children: children,
})
}

return printImageTree(dockerCLI, view)
}

type imageDetails struct {
ID string
DiskUsage string
Used bool
}

type topImage struct {
Names []string
Details imageDetails
Children []subImage
}

type subImage struct {
Platform string
Available bool
Details imageDetails
}

func printImageTree(dockerCLI command.Cli, images []topImage) error {
out := dockerCLI.Out()
_, width := out.GetTtySize()

headers := []header{
{Title: "Image", Width: 0, Left: true},
{Title: "ID", Width: 12},
{Title: "Disk use", Width: 8},
{Title: "Used", Width: 4},
}

const spacing = 3
nameWidth := int(width)
for _, h := range headers {
if h.Width == 0 {
continue
}
nameWidth -= h.Width
nameWidth -= spacing
}

maxImageName := len(headers[0].Title)
for _, img := range images {
for _, name := range img.Names {
if len(name) > maxImageName {
maxImageName = len(name)
}
}
for _, sub := range img.Children {
if len(sub.Platform) > maxImageName {
maxImageName = len(sub.Platform)
}
}
}

if nameWidth > maxImageName+spacing {
nameWidth = maxImageName + spacing
}

if nameWidth < 0 {
headers = headers[:1]
nameWidth = int(width)
}
headers[0].Width = nameWidth

headerColor := aec.NewBuilder(aec.DefaultF, aec.Bold).ANSI

// Print headers
for i, h := range headers {
if i > 0 {
_, _ = fmt.Fprint(out, strings.Repeat(" ", spacing))
}

_, _ = fmt.Fprint(out, h.PrintC(headerColor, h.Title))
}

_, _ = fmt.Fprintln(out)

topNameColor := aec.NewBuilder(aec.BlueF, aec.Underline, aec.Bold).ANSI
normalColor := aec.NewBuilder(aec.DefaultF).ANSI
normalFaintedColor := aec.NewBuilder(aec.DefaultF).Faint().ANSI
greenColor := aec.NewBuilder(aec.GreenF).ANSI

printDetails := func(clr aec.ANSI, details imageDetails) {
truncID := stringid.TruncateID(details.ID)
fmt.Fprint(out, headers[1].Print(clr, truncID))
fmt.Fprint(out, strings.Repeat(" ", spacing))

fmt.Fprint(out, headers[2].Print(clr, details.DiskUsage))
fmt.Fprint(out, strings.Repeat(" ", spacing))

if details.Used {
fmt.Fprint(out, headers[3].Print(greenColor, " ✔ ️"))
} else {
fmt.Fprint(out, headers[3].Print(clr, " "))
}
}

// Print images
for _, img := range images {
for idx, name := range img.Names {
fmt.Fprint(out, headers[0].Print(topNameColor, name))
if idx != len(img.Names)-1 {
fmt.Fprintln(out, "")
}
}
fmt.Fprint(out, strings.Repeat(" ", spacing))

printDetails(normalColor, img.Details)

_, _ = fmt.Fprintln(out, "")
for idx, sub := range img.Children {
clr := normalColor
if !sub.Available {
clr = normalFaintedColor
}

if idx != len(img.Children)-1 {
fmt.Fprint(out, headers[0].Print(clr, "├─ "+sub.Platform))
} else {
fmt.Fprint(out, headers[0].Print(clr, "└─ "+sub.Platform))
}

fmt.Fprint(out, strings.Repeat(" ", spacing))
printDetails(clr, sub.Details)

fmt.Fprintln(out, "")
}
}

return nil
}

type header struct {
Title string
Width int
Left bool
}

func truncateRunes(s string, length int) string {
runes := []rune(s)
if len(runes) > length {
return string(runes[:length])
}
return s
}

func (h header) Print(clr aec.ANSI, s string) (out string) {
if h.Left {
return h.PrintL(clr, s)
}
return h.PrintC(clr, s)
}

func (h header) PrintC(clr aec.ANSI, s string) (out string) {
ln := utf8.RuneCountInString(s)
if h.Left {
return h.PrintL(clr, s)
}

if ln > h.Width {
return clr.Apply(truncateRunes(s, h.Width))
}

fill := h.Width - ln

l := fill / 2
r := fill - l

return strings.Repeat(" ", l) + clr.Apply(s) + strings.Repeat(" ", r)
}

func (h header) PrintL(clr aec.ANSI, s string) string {
ln := utf8.RuneCountInString(s)
if ln > h.Width {
return clr.Apply(truncateRunes(s, h.Width))
}

return clr.Apply(s) + strings.Repeat(" ", h.Width-ln)
}
1 change: 1 addition & 0 deletions docs/reference/commandline/image_ls.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ List images
| [`--format`](#format) | `string` | | Format output using a custom template:<br>'table': Print output in table format with column headers (default)<br>'table TEMPLATE': Print output in table format using the given Go template<br>'json': Print in JSON format<br>'TEMPLATE': Print output using the given Go template.<br>Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates |
| [`--no-trunc`](#no-trunc) | `bool` | | Don't truncate output |
| `-q`, `--quiet` | `bool` | | Only show image IDs |
| `--tree` | `bool` | | List multi-platform images tree [experimental, behavior may change] |


<!---MARKER_GEN_END-->
Expand Down
1 change: 1 addition & 0 deletions docs/reference/commandline/images.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ List images
| `--format` | `string` | | Format output using a custom template:<br>'table': Print output in table format with column headers (default)<br>'table TEMPLATE': Print output in table format using the given Go template<br>'json': Print in JSON format<br>'TEMPLATE': Print output using the given Go template.<br>Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates |
| `--no-trunc` | `bool` | | Don't truncate output |
| `-q`, `--quiet` | `bool` | | Only show image IDs |
| `--tree` | `bool` | | List multi-platform images tree [experimental, behavior may change] |


<!---MARKER_GEN_END-->
Expand Down
4 changes: 4 additions & 0 deletions vendor.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ module github.com/docker/cli

go 1.21.0

replace github.com/docker/docker => github.com/vvoland/moby v20.10.16-0.20240718121800-6c429bba16a6+incompatible

//replace github.com/docker/docker => github.com/vvoland/moby c8d-list-multiplatform

require (
dario.cat/mergo v1.0.0
github.com/containerd/platforms v0.2.1
Expand Down
4 changes: 2 additions & 2 deletions vendor.sum
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,6 @@ github.com/docker/cli-docs-tool v0.8.0/go.mod h1:8TQQ3E7mOXoYUs811LiPdUnAhXrcVsB
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v27.0.2-0.20240715215214-aae044039ca4+incompatible h1:OJJ+48ACIraFECLY+qp2Zi5kkr6J8vXm2bzIJqgsS9A=
github.com/docker/docker v27.0.2-0.20240715215214-aae044039ca4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=
Expand Down Expand Up @@ -274,6 +272,8 @@ github.com/theupdateframework/notary v0.7.1-0.20210315103452-bf96a202a09a h1:tlJ
github.com/theupdateframework/notary v0.7.1-0.20210315103452-bf96a202a09a/go.mod h1:Y94A6rPp2OwNfP/7vmf8O2xx2IykP8pPXQ1DLouGnEw=
github.com/tonistiigi/go-rosetta v0.0.0-20200727161949-f79598599c5d h1:wvQZpqy8p0D/FUia6ipKDhXrzPzBVJE4PZyPc5+5Ay0=
github.com/tonistiigi/go-rosetta v0.0.0-20200727161949-f79598599c5d/go.mod h1:xKQhd7snlzKFuUi1taTGWjpRE8iFTA06DeacYi3CVFQ=
github.com/vvoland/moby v20.10.16-0.20240718121800-6c429bba16a6+incompatible h1:CT1+xdO/OMA2acK0e6+r7UTPvSZgqOmfNWKgruRXutU=
github.com/vvoland/moby v20.10.16-0.20240718121800-6c429bba16a6+incompatible/go.mod h1:nLN96xVmxZq8CPEl0UgxMpO/G2e8MQtjhAdlRDUdBi0=
github.com/weppos/publicsuffix-go v0.15.1-0.20210511084619-b1f36a2d6c0b h1:FsyNrX12e5BkplJq7wKOLk0+C6LZ+KGXvuEcKUYm5ss=
github.com/weppos/publicsuffix-go v0.15.1-0.20210511084619-b1f36a2d6c0b/go.mod h1:HYux0V0Zi04bHNwOHy4cXJVz/TQjYonnF6aoYhj+3QE=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
Expand Down
2 changes: 1 addition & 1 deletion vendor/github.com/docker/docker/api/common.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading