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 Jul 18, 2024
1 parent 07baebe commit a51e3bc
Show file tree
Hide file tree
Showing 4 changed files with 274 additions and 0 deletions.
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.46"})

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
247 changes: 247 additions & 0 deletions cli/command/image/tree.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
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,
Filters: opts.filters,
Manifests: true,
})
if err != nil {
return err
}

var view []topImage
for _, img := range images {
details := imageDetails{
ID: img.ID,
DiskUsage: 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,
DiskUsage: units.HumanSizeWithPrecision(float64(im.ContentSize+imgData.UnpackedSize), 3),
Used: len(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
DiskUsage 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: "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 {
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 := 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 {
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
}

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

0 comments on commit a51e3bc

Please sign in to comment.