Skip to content

Commit

Permalink
image/list: Add --tree flag
Browse files Browse the repository at this point in the history
Signed-off-by: Paweł Gronowski <[email protected]>
  • Loading branch information
vvoland committed May 20, 2024
1 parent 1ce4704 commit 7a5dc95
Show file tree
Hide file tree
Showing 25 changed files with 1,779 additions and 3 deletions.
24 changes: 24 additions & 0 deletions cli/command/image/list.go
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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,
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
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) | | | 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
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` | | | 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 vendor.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ replace github.com/docker/docker => github.com/vvoland/moby v20.10.3-0.202405201
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
Expand Down
6 changes: 4 additions & 2 deletions vendor.sum
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,12 @@ github.com/cloudflare/cfssl v1.6.4/go.mod h1:8b3CQMxfWPAeom3zBnGJ6sd+G1NkL5TXqmD
github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo=
github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA=
github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI=
github.com/containerd/containerd v1.7.14 h1:H/XLzbnGuenZEGK+v0RkwTdv2u1QFAruMe5N0GNPJwA=
github.com/containerd/containerd v1.7.14/go.mod h1:YMC9Qt5yzNqXx/fO4j/5yYVIHXSRrlB3H7sxkUTvspg=
github.com/containerd/containerd v1.7.15 h1:afEHXdil9iAm03BmhjzKyXnnEBtjaLJefdU7DV0IFes=
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 Down

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

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

Loading

0 comments on commit 7a5dc95

Please sign in to comment.