mirror of https://github.com/docker/cli.git
Merge pull request #5353 from vvoland/4982-27.x
[27.x backport] image/list: Add `--tree` flag
This commit is contained in:
commit
60ce3fbc96
|
@ -2,6 +2,7 @@ package image
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
|
@ -24,6 +25,7 @@ type imagesOptions struct {
|
||||||
format string
|
format string
|
||||||
filter opts.FilterOpt
|
filter opts.FilterOpt
|
||||||
calledAs string
|
calledAs string
|
||||||
|
tree bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewImagesCommand creates a new `docker images` command
|
// NewImagesCommand creates a new `docker images` command
|
||||||
|
@ -59,6 +61,10 @@ func NewImagesCommand(dockerCLI command.Cli) *cobra.Command {
|
||||||
flags.StringVar(&options.format, "format", "", flagsHelper.FormatHelp)
|
flags.StringVar(&options.format, "format", "", flagsHelper.FormatHelp)
|
||||||
flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided")
|
flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided")
|
||||||
|
|
||||||
|
flags.BoolVar(&options.tree, "tree", false, "List multi-platform images as a tree (EXPERIMENTAL)")
|
||||||
|
flags.SetAnnotation("tree", "version", []string{"1.47"})
|
||||||
|
flags.SetAnnotation("tree", "experimentalCLI", nil)
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,6 +81,26 @@ func runImages(ctx context.Context, dockerCLI command.Cli, options imagesOptions
|
||||||
filters.Add("reference", options.matchName)
|
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{
|
images, err := dockerCLI.Client().ImageList(ctx, image.ListOptions{
|
||||||
All: options.all,
|
All: options.all,
|
||||||
Filters: filters,
|
Filters: filters,
|
||||||
|
|
|
@ -0,0 +1,376 @@
|
||||||
|
package image
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/containerd/platforms"
|
||||||
|
"github.com/docker/cli/cli/command"
|
||||||
|
"github.com/docker/cli/cli/streams"
|
||||||
|
"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/morikuni/aec"
|
||||||
|
)
|
||||||
|
|
||||||
|
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,
|
||||||
|
Filters: opts.filters,
|
||||||
|
Manifests: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
view := make([]topImage, 0, len(images))
|
||||||
|
for _, img := range images {
|
||||||
|
details := imageDetails{
|
||||||
|
ID: img.ID,
|
||||||
|
DiskUsage: units.HumanSizeWithPrecision(float64(img.Size), 3),
|
||||||
|
Used: img.Containers > 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalContent int64
|
||||||
|
children := make([]subImage, 0, len(img.Manifests))
|
||||||
|
for _, im := range img.Manifests {
|
||||||
|
if im.Kind != imagetypes.ManifestKindImage {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
im := im
|
||||||
|
sub := subImage{
|
||||||
|
Platform: platforms.Format(im.ImageData.Platform),
|
||||||
|
Available: im.Available,
|
||||||
|
Details: imageDetails{
|
||||||
|
ID: im.ID,
|
||||||
|
DiskUsage: units.HumanSizeWithPrecision(float64(im.Size.Total), 3),
|
||||||
|
Used: len(im.ImageData.Containers) > 0,
|
||||||
|
ContentSize: units.HumanSizeWithPrecision(float64(im.Size.Content), 3),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if sub.Details.Used {
|
||||||
|
// Mark top-level parent image as used if any of its subimages are used.
|
||||||
|
details.Used = true
|
||||||
|
}
|
||||||
|
|
||||||
|
totalContent += im.Size.Content
|
||||||
|
children = append(children, sub)
|
||||||
|
}
|
||||||
|
|
||||||
|
details.ContentSize = units.HumanSizeWithPrecision(float64(totalContent), 3)
|
||||||
|
|
||||||
|
view = append(view, topImage{
|
||||||
|
Names: img.RepoTags,
|
||||||
|
Details: details,
|
||||||
|
Children: children,
|
||||||
|
created: img.Created,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(view, func(i, j int) bool {
|
||||||
|
return view[i].created > view[j].created
|
||||||
|
})
|
||||||
|
|
||||||
|
return printImageTree(dockerCLI, view)
|
||||||
|
}
|
||||||
|
|
||||||
|
type imageDetails struct {
|
||||||
|
ID string
|
||||||
|
DiskUsage string
|
||||||
|
Used bool
|
||||||
|
ContentSize string
|
||||||
|
}
|
||||||
|
|
||||||
|
type topImage struct {
|
||||||
|
Names []string
|
||||||
|
Details imageDetails
|
||||||
|
Children []subImage
|
||||||
|
|
||||||
|
created int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type subImage struct {
|
||||||
|
Platform string
|
||||||
|
Available bool
|
||||||
|
Details imageDetails
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnSpacing = 3
|
||||||
|
|
||||||
|
func printImageTree(dockerCLI command.Cli, images []topImage) error {
|
||||||
|
out := dockerCLI.Out()
|
||||||
|
_, width := out.GetTtySize()
|
||||||
|
if width == 0 {
|
||||||
|
width = 80
|
||||||
|
}
|
||||||
|
if width < 20 {
|
||||||
|
width = 20
|
||||||
|
}
|
||||||
|
|
||||||
|
warningColor := aec.LightYellowF
|
||||||
|
headerColor := aec.NewBuilder(aec.DefaultF, aec.Bold).ANSI
|
||||||
|
topNameColor := aec.NewBuilder(aec.BlueF, aec.Underline, aec.Bold).ANSI
|
||||||
|
normalColor := aec.NewBuilder(aec.DefaultF).ANSI
|
||||||
|
greenColor := aec.NewBuilder(aec.GreenF).ANSI
|
||||||
|
untaggedColor := aec.NewBuilder(aec.Faint).ANSI
|
||||||
|
if !out.IsTerminal() {
|
||||||
|
headerColor = noColor{}
|
||||||
|
topNameColor = noColor{}
|
||||||
|
normalColor = noColor{}
|
||||||
|
greenColor = noColor{}
|
||||||
|
warningColor = noColor{}
|
||||||
|
untaggedColor = noColor{}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = fmt.Fprintln(out, warningColor.Apply("WARNING: This is an experimental feature. The output may change and shouldn't be depended on."))
|
||||||
|
_, _ = fmt.Fprintln(out, "")
|
||||||
|
|
||||||
|
columns := []imgColumn{
|
||||||
|
{
|
||||||
|
Title: "Image",
|
||||||
|
Align: alignLeft,
|
||||||
|
Width: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Title: "ID",
|
||||||
|
Align: alignLeft,
|
||||||
|
Width: 12,
|
||||||
|
DetailsValue: func(d *imageDetails) string {
|
||||||
|
return stringid.TruncateID(d.ID)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Title: "Disk usage",
|
||||||
|
Align: alignRight,
|
||||||
|
Width: 10,
|
||||||
|
DetailsValue: func(d *imageDetails) string {
|
||||||
|
return d.DiskUsage
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Title: "Content size",
|
||||||
|
Align: alignRight,
|
||||||
|
Width: 12,
|
||||||
|
DetailsValue: func(d *imageDetails) string {
|
||||||
|
return d.ContentSize
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Title: "Used",
|
||||||
|
Align: alignCenter,
|
||||||
|
Width: 4,
|
||||||
|
Color: &greenColor,
|
||||||
|
DetailsValue: func(d *imageDetails) string {
|
||||||
|
if d.Used {
|
||||||
|
return "✔"
|
||||||
|
}
|
||||||
|
return " "
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
nameWidth := int(width)
|
||||||
|
for idx, h := range columns {
|
||||||
|
if h.Width == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
d := h.Width
|
||||||
|
if idx > 0 {
|
||||||
|
d += columnSpacing
|
||||||
|
}
|
||||||
|
// If the first column gets too short, remove remaining columns
|
||||||
|
if nameWidth-d < 12 {
|
||||||
|
columns = columns[:idx]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
nameWidth -= d
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to make the first column as narrow as possible
|
||||||
|
widest := widestFirstColumnValue(columns, images)
|
||||||
|
if nameWidth > widest {
|
||||||
|
nameWidth = widest
|
||||||
|
}
|
||||||
|
columns[0].Width = nameWidth
|
||||||
|
|
||||||
|
// Print columns
|
||||||
|
for i, h := range columns {
|
||||||
|
if i > 0 {
|
||||||
|
_, _ = fmt.Fprint(out, strings.Repeat(" ", columnSpacing))
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = fmt.Fprint(out, h.Print(headerColor, strings.ToUpper(h.Title)))
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = fmt.Fprintln(out)
|
||||||
|
|
||||||
|
// Print images
|
||||||
|
for _, img := range images {
|
||||||
|
_, _ = fmt.Fprintln(out, "")
|
||||||
|
printNames(out, columns, img, topNameColor, untaggedColor)
|
||||||
|
printDetails(out, columns, normalColor, img.Details)
|
||||||
|
printChildren(out, columns, img, normalColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func printDetails(out *streams.Out, headers []imgColumn, defaultColor aec.ANSI, details imageDetails) {
|
||||||
|
for _, h := range headers {
|
||||||
|
if h.DetailsValue == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = fmt.Fprint(out, strings.Repeat(" ", columnSpacing))
|
||||||
|
clr := defaultColor
|
||||||
|
if h.Color != nil {
|
||||||
|
clr = *h.Color
|
||||||
|
}
|
||||||
|
val := h.DetailsValue(&details)
|
||||||
|
_, _ = fmt.Fprint(out, h.Print(clr, val))
|
||||||
|
}
|
||||||
|
fmt.Printf("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func printChildren(out *streams.Out, headers []imgColumn, img topImage, normalColor aec.ANSI) {
|
||||||
|
for idx, sub := range img.Children {
|
||||||
|
clr := normalColor
|
||||||
|
if !sub.Available {
|
||||||
|
clr = normalColor.With(aec.Faint)
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
printDetails(out, headers, clr, sub.Details)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printNames(out *streams.Out, headers []imgColumn, img topImage, color, untaggedColor aec.ANSI) {
|
||||||
|
if len(img.Names) == 0 {
|
||||||
|
_, _ = fmt.Fprint(out, headers[0].Print(untaggedColor, "<untagged>"))
|
||||||
|
}
|
||||||
|
|
||||||
|
for nameIdx, name := range img.Names {
|
||||||
|
if nameIdx != 0 {
|
||||||
|
_, _ = fmt.Fprintln(out, "")
|
||||||
|
}
|
||||||
|
_, _ = fmt.Fprint(out, headers[0].Print(color, name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type alignment int
|
||||||
|
|
||||||
|
const (
|
||||||
|
alignLeft alignment = iota
|
||||||
|
alignCenter
|
||||||
|
alignRight
|
||||||
|
)
|
||||||
|
|
||||||
|
type imgColumn struct {
|
||||||
|
Title string
|
||||||
|
Width int
|
||||||
|
Align alignment
|
||||||
|
|
||||||
|
DetailsValue func(*imageDetails) string
|
||||||
|
Color *aec.ANSI
|
||||||
|
}
|
||||||
|
|
||||||
|
func truncateRunes(s string, length int) string {
|
||||||
|
runes := []rune(s)
|
||||||
|
if len(runes) > length {
|
||||||
|
return string(runes[:length-3]) + "..."
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h imgColumn) Print(clr aec.ANSI, s string) string {
|
||||||
|
switch h.Align {
|
||||||
|
case alignCenter:
|
||||||
|
return h.PrintC(clr, s)
|
||||||
|
case alignRight:
|
||||||
|
return h.PrintR(clr, s)
|
||||||
|
case alignLeft:
|
||||||
|
}
|
||||||
|
return h.PrintL(clr, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h imgColumn) PrintC(clr aec.ANSI, s string) string {
|
||||||
|
ln := utf8.RuneCountInString(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 imgColumn) 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h imgColumn) PrintR(clr aec.ANSI, s string) string {
|
||||||
|
ln := utf8.RuneCountInString(s)
|
||||||
|
if ln > h.Width {
|
||||||
|
return clr.Apply(truncateRunes(s, h.Width))
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Repeat(" ", h.Width-ln) + clr.Apply(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
type noColor struct{}
|
||||||
|
|
||||||
|
func (a noColor) With(_ ...aec.ANSI) aec.ANSI {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a noColor) Apply(s string) string {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a noColor) String() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// widestFirstColumnValue calculates the width needed to fully display the image names and platforms.
|
||||||
|
func widestFirstColumnValue(headers []imgColumn, images []topImage) int {
|
||||||
|
width := len(headers[0].Title)
|
||||||
|
for _, img := range images {
|
||||||
|
for _, name := range img.Names {
|
||||||
|
if len(name) > width {
|
||||||
|
width = len(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, sub := range img.Children {
|
||||||
|
pl := len(sub.Platform) + len("└─ ")
|
||||||
|
if pl > width {
|
||||||
|
width = pl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return width
|
||||||
|
}
|
|
@ -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 |
|
| [`--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 |
|
| [`--no-trunc`](#no-trunc) | `bool` | | Don't truncate output |
|
||||||
| `-q`, `--quiet` | `bool` | | Only show image IDs |
|
| `-q`, `--quiet` | `bool` | | Only show image IDs |
|
||||||
|
| `--tree` | `bool` | | List multi-platform images as a tree (EXPERIMENTAL) |
|
||||||
|
|
||||||
|
|
||||||
<!---MARKER_GEN_END-->
|
<!---MARKER_GEN_END-->
|
||||||
|
|
|
@ -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 |
|
| `--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 |
|
| `--no-trunc` | `bool` | | Don't truncate output |
|
||||||
| `-q`, `--quiet` | `bool` | | Only show image IDs |
|
| `-q`, `--quiet` | `bool` | | Only show image IDs |
|
||||||
|
| `--tree` | `bool` | | List multi-platform images as a tree (EXPERIMENTAL) |
|
||||||
|
|
||||||
|
|
||||||
<!---MARKER_GEN_END-->
|
<!---MARKER_GEN_END-->
|
||||||
|
|
Loading…
Reference in New Issue