mirror of https://github.com/docker/cli.git
image/list: Add `--tree` flag
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit be11b74ee9
)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
This commit is contained in:
parent
f90dc28f1e
commit
99b647cfca
|
@ -2,6 +2,7 @@ package image
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
|
@ -24,6 +25,7 @@ type imagesOptions struct {
|
|||
format string
|
||||
filter opts.FilterOpt
|
||||
calledAs string
|
||||
tree bool
|
||||
}
|
||||
|
||||
// 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.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
|
||||
}
|
||||
|
||||
|
@ -75,6 +81,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,
|
||||
|
|
|
@ -0,0 +1,312 @@
|
|||
package image
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"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,
|
||||
}
|
||||
|
||||
children := make([]subImage, 0, len(img.Manifests))
|
||||
for _, im := range img.Manifests {
|
||||
if im.Kind != imagetypes.ManifestKindImage {
|
||||
continue
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
children = append(children, sub)
|
||||
}
|
||||
|
||||
view = append(view, topImage{
|
||||
Names: img.RepoTags,
|
||||
Details: details,
|
||||
Children: children,
|
||||
})
|
||||
}
|
||||
|
||||
return printImageTree(dockerCLI, view)
|
||||
}
|
||||
|
||||
type imageDetails struct {
|
||||
ID string
|
||||
DiskUsage string
|
||||
Used bool
|
||||
}
|
||||
|
||||
type topImage struct {
|
||||
Names []string
|
||||
Details imageDetails
|
||||
Children []subImage
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
if !out.IsTerminal() {
|
||||
headerColor = noColor{}
|
||||
topNameColor = noColor{}
|
||||
normalColor = noColor{}
|
||||
greenColor = noColor{}
|
||||
}
|
||||
|
||||
columns := []imgColumn{
|
||||
{Title: "Image", Width: 0, Left: true},
|
||||
{
|
||||
Title: "ID",
|
||||
Width: 12,
|
||||
DetailsValue: func(d *imageDetails) string {
|
||||
return stringid.TruncateID(d.ID)
|
||||
},
|
||||
},
|
||||
{
|
||||
Title: "Disk usage",
|
||||
Width: 10,
|
||||
DetailsValue: func(d *imageDetails) string {
|
||||
return d.DiskUsage
|
||||
},
|
||||
},
|
||||
{
|
||||
Title: "Used",
|
||||
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, h.Title))
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintln(out)
|
||||
|
||||
// Print images
|
||||
for idx, img := range images {
|
||||
if idx != 0 {
|
||||
_, _ = fmt.Fprintln(out, "")
|
||||
}
|
||||
|
||||
printNames(out, columns, img, topNameColor)
|
||||
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 aec.ANSI) {
|
||||
for nameIdx, name := range img.Names {
|
||||
if nameIdx != 0 {
|
||||
_, _ = fmt.Fprintln(out, "")
|
||||
}
|
||||
_, _ = fmt.Fprint(out, headers[0].Print(color, name))
|
||||
}
|
||||
}
|
||||
|
||||
type imgColumn struct {
|
||||
Title string
|
||||
Width int
|
||||
Left bool
|
||||
|
||||
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) (out string) {
|
||||
if h.Left {
|
||||
return h.PrintL(clr, s)
|
||||
}
|
||||
return h.PrintC(clr, s)
|
||||
}
|
||||
|
||||
func (h imgColumn) PrintC(clr aec.ANSI, s string) (out 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)
|
||||
}
|
||||
|
||||
type noColor struct{}
|
||||
|
||||
func (a noColor) With(ansi ...aec.ANSI) aec.ANSI {
|
||||
return aec.NewBuilder(ansi...).ANSI
|
||||
}
|
||||
|
||||
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 |
|
||||
| [`--no-trunc`](#no-trunc) | `bool` | | Don't truncate output |
|
||||
| `-q`, `--quiet` | `bool` | | Only show image IDs |
|
||||
| `--tree` | `bool` | | List multi-platform images as a tree (EXPERIMENTAL) |
|
||||
|
||||
|
||||
<!---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 |
|
||||
| `--no-trunc` | `bool` | | Don't truncate output |
|
||||
| `-q`, `--quiet` | `bool` | | Only show image IDs |
|
||||
| `--tree` | `bool` | | List multi-platform images as a tree (EXPERIMENTAL) |
|
||||
|
||||
|
||||
<!---MARKER_GEN_END-->
|
||||
|
|
Loading…
Reference in New Issue