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 <pawel.gronowski@docker.com>
  • Loading branch information
vvoland committed Apr 4, 2024
1 parent 1d854ad commit 1b66af3
Show file tree
Hide file tree
Showing 23 changed files with 1,767 additions and 0 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
244 changes: 244 additions & 0 deletions cli/command/image/tree.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
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 _, platform := range img.PlatformImages {
sub := subImage{
Platform: platforms.Format(platform.Platform),
Available: platform.Available,
Details: imageDetails{
ID: platform.ID,
Size: units.HumanSizeWithPrecision(float64(platform.ContentSize+platform.UnpackedSize), 3),
Used: platform.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 vendor.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ go 1.21
require (
dario.cat/mergo v1.0.0
github.com/containerd/containerd v1.7.14
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
2 changes: 2 additions & 0 deletions vendor.sum
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ github.com/containerd/containerd v1.7.14 h1:H/XLzbnGuenZEGK+v0RkwTdv2u1QFAruMe5N
github.com/containerd/containerd v1.7.14/go.mod h1:YMC9Qt5yzNqXx/fO4j/5yYVIHXSRrlB3H7sxkUTvspg=
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
1 change: 1 addition & 0 deletions vendor/github.com/containerd/platforms/.gitattributes

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

30 changes: 30 additions & 0 deletions vendor/github.com/containerd/platforms/.golangci.yml

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

Loading

0 comments on commit 1b66af3

Please sign in to comment.