diff --git a/cli/command/image/list.go b/cli/command/image/list.go
index a691efed453b..f14135ba37bd 100644
--- a/cli/command/image/list.go
+++ b/cli/command/image/list.go
@@ -24,6 +24,7 @@ type imagesOptions struct {
format string
filter opts.FilterOpt
calledAs string
+ tree bool
}
// NewImagesCommand creates a new `docker images` command
@@ -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
}
@@ -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,
diff --git a/cli/command/image/tree.go b/cli/command/image/tree.go
new file mode 100644
index 000000000000..9e90b81d6753
--- /dev/null
+++ b/cli/command/image/tree.go
@@ -0,0 +1,246 @@
+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,
+ 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: 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 usage", 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.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 *color.Color, s string) (out string) {
+ if h.Left {
+ return h.PrintL(clr, s)
+ }
+ return h.PrintC(clr, s)
+}
+
+func (h header) PrintC(clr *color.Color, s string) (out string) {
+ ln := utf8.RuneCountInString(s)
+ if h.Left {
+ return h.PrintL(clr, s)
+ }
+
+ if ln > h.Width {
+ return clr.Sprint(truncateRunes(s, h.Width))
+ }
+
+ fill := h.Width - ln
+
+ l := fill / 2
+ r := fill - l
+
+ return strings.Repeat(" ", l) + clr.Sprint(s) + strings.Repeat(" ", r)
+}
+
+func (h header) PrintL(clr *color.Color, s string) string {
+ ln := utf8.RuneCountInString(s)
+ if ln > h.Width {
+ return clr.Sprint(truncateRunes(s, h.Width))
+ }
+
+ return clr.Sprint(s) + strings.Repeat(" ", h.Width-ln)
+}
diff --git a/docs/reference/commandline/image_ls.md b/docs/reference/commandline/image_ls.md
index 1467c37c6eca..11a5ec9bdc69 100644
--- a/docs/reference/commandline/image_ls.md
+++ b/docs/reference/commandline/image_ls.md
@@ -17,6 +17,7 @@ List images
| [`--format`](#format) | `string` | | Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
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] |
diff --git a/docs/reference/commandline/images.md b/docs/reference/commandline/images.md
index 1fa9f6a319c5..f24dc2020912 100644
--- a/docs/reference/commandline/images.md
+++ b/docs/reference/commandline/images.md
@@ -17,6 +17,7 @@ List images
| `--format` | `string` | | Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
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] |