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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
24 changes: 24 additions & 0 deletions cli/command/image/list.go
Expand Up @@ -24,6 +24,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 +60,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.46"})

return cmd
}

Expand All @@ -75,6 +79,26 @@ func runImages(ctx context.Context, dockerCLI command.Cli, options imagesOptions
filters.Add("reference", options.matchName)
}

if options.tree {
if options.quiet {
return fmt.Errorf("--quiet is not (yet) supported with --tree")
}
if options.noTrunc {
return fmt.Errorf("--no-trunc is not (yet) supported with --tree")
}
if options.showDigests {
return fmt.Errorf("--show-digest is not (yet) supported with --tree")
}
if options.format != "" {
return fmt.Errorf("--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
251 changes: 251 additions & 0 deletions cli/command/image/tree.go
@@ -0,0 +1,251 @@
package image

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

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

"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"
"github.com/fatih/color"
)

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
Contributor Author

Choose a reason for hiding this comment

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

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

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

var children []subImage
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,
Size: units.HumanSizeWithPrecision(float64(im.ContentSize+imgData.UnpackedSize), 3),
Used: imgData.Containers > 0,
},
}

children = append(children, sub)
}

for _, tag := range img.RepoTags {
view = append(view, topImage{
Name: tag,
Details: details,
Children: children,
})
}
}

return printImageTree(dockerCLI, view)
}

type imageDetails struct {
ID string
Size string
Used bool
}

type topImage struct {
Name 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: "Size", 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 {
if len(img.Name) > maxImageName {
maxImageName = len(img.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 := color.New(color.FgHiWhite).Add(color.Bold)

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

headerColor.Fprint(out, h.PrintC(headerColor, h.Title))
}

_, _ = fmt.Fprintln(out)

topNameColor := color.New(color.FgBlue).Add(color.Underline).Add(color.Bold)
normalColor := color.New(color.FgWhite)
normalFaintedColor := color.New(color.FgWhite).Add(color.Faint)
greenColor := color.New(color.FgGreen)

printDetails := func(clr *color.Color, 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.Size))
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 {
fmt.Fprint(out, headers[0].Print(topNameColor, img.Name))
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
}

func maybeUint(v int64) *uint {
u := uint(v)
return &u
}

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(color *color.Color, s string) (out string) {
if h.Left {
return h.PrintL(color, s)
}
return h.PrintC(color, s)
}

func (h header) PrintC(color *color.Color, s string) (out string) {
ln := utf8.RuneCountInString(s)
if h.Left {
return h.PrintL(color, s)
}

if ln > int(h.Width) {
return color.Sprint(truncateRunes(s, h.Width))
}

fill := int(h.Width) - ln

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

return strings.Repeat(" ", l) + color.Sprint(s) + strings.Repeat(" ", r)
}

func (h header) PrintL(color *color.Color, s string) string {
ln := utf8.RuneCountInString(s)
if ln > int(h.Width) {
return color.Sprint(truncateRunes(s, h.Width))
}

return color.Sprint(s) + strings.Repeat(" ", int(h.Width)-ln)
}
1 change: 1 addition & 0 deletions docs/reference/commandline/image_ls.md
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) | | | Don't truncate output |
| `-q`, `--quiet` | | | Only show image IDs |
| `--tree` | | | 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
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` | | | Don't truncate output |
| `-q`, `--quiet` | | | Only show image IDs |
| `--tree` | | | List multi-platform images tree [experimental, behavior may change] |


<!---MARKER_GEN_END-->
Expand Down
6 changes: 6 additions & 0 deletions vendor.mod
Expand Up @@ -6,16 +6,20 @@ module github.com/docker/cli

go 1.21.0

replace github.com/docker/docker => github.com/vvoland/moby v20.10.3-0.20240520145354-1db84181baa6+incompatible

require (
dario.cat/mergo v1.0.0
github.com/containerd/containerd v1.7.15
github.com/containerd/platforms v0.1.1
github.com/creack/pty v1.1.21
github.com/distribution/reference v0.5.0
github.com/docker/distribution v2.8.3+incompatible
github.com/docker/docker v26.1.1-0.20240516211257-06e3a49d66fa+incompatible
github.com/docker/docker-credential-helpers v0.8.1
github.com/docker/go-connections v0.5.0
github.com/docker/go-units v0.5.0
github.com/fatih/color v1.16.0
github.com/fvbommel/sortorder v1.0.2
github.com/gogo/protobuf v1.3.2
github.com/google/go-cmp v0.6.0
Expand Down Expand Up @@ -72,6 +76,8 @@ require (
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/compress v1.17.4 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/miekg/pkcs11 v1.1.1 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
Expand Down
15 changes: 13 additions & 2 deletions vendor.sum
Expand Up @@ -43,6 +43,8 @@ github.com/containerd/containerd v1.7.15 h1:afEHXdil9iAm03BmhjzKyXnnEBtjaLJefdU7
github.com/containerd/containerd v1.7.15/go.mod h1:ISzRRTMF8EXNpJlTzyr2XMhN+j9K302C21/+cr3kUnY=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v0.1.1 h1:gp0xXBoY+1CjH54gJDon0kBjIbK2C4XSX1BGwP5ptG0=
github.com/containerd/platforms v0.1.1/go.mod h1:XOM2BS6kN6gXafPLg80V6y/QUib+xoLyC3qVmHzibko=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
Expand All @@ -57,8 +59,6 @@ github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5
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 v26.1.1-0.20240516211257-06e3a49d66fa+incompatible h1:Zp6B3afdBCdGNGM6dxdiThsrmUIJSoBFkFLonLhiO1k=
github.com/docker/docker v26.1.1-0.20240516211257-06e3a49d66fa+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.8.1 h1:j/eKUktUltBtMzKqmfLB0PAgqYyMHOp5vfsD1807oKo=
github.com/docker/docker-credential-helpers v0.8.1/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=
Expand All @@ -77,6 +77,8 @@ github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
github.com/dvsekhvalnov/jose2go v0.0.0-20170216131308-f21a8cedbbae/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
Expand Down Expand Up @@ -160,6 +162,11 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/magiconair/properties v1.5.3 h1:C8fxWnhYyME3n0klPOhVM7PtYUB3eV1W3DeFmN3j53Y=
github.com/magiconair/properties v1.5.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.6.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
Expand Down Expand Up @@ -271,6 +278,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.3-0.20240520145354-1db84181baa6+incompatible h1:nQo134DBLd4cN+p22aCVrJEeVhhCsHiF4cYWqF5LDTA=
github.com/vvoland/moby v20.10.3-0.20240520145354-1db84181baa6+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 Expand Up @@ -359,6 +368,8 @@ golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
Expand Down