mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-28 03:53:33 +00:00
chore(images): format platforms as pills, sort images by name
This commit is contained in:
+70
-22
@@ -3,16 +3,17 @@ package image
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"slices"
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"text/tabwriter"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/charmbracelet/lipgloss/table"
|
||||||
"github.com/containerd/platforms"
|
"github.com/containerd/platforms"
|
||||||
"github.com/docker/docker/api/types/image"
|
"github.com/docker/docker/api/types/image"
|
||||||
"github.com/docker/go-units"
|
"github.com/docker/go-units"
|
||||||
|
"github.com/muesli/termenv"
|
||||||
"github.com/psviderski/uncloud/internal/cli"
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -120,8 +121,8 @@ func list(ctx context.Context, uncli *cli.CLI, opts listOptions) error {
|
|||||||
name = img.RepoTags[0]
|
name = img.RepoTags[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
imgPlatforms := imagePlatforms(img)
|
imgPlatforms, _ := imagePlatforms(img)
|
||||||
formattedPlatforms := strings.Join(imgPlatforms, ",")
|
formattedPlatforms := formatPlatforms(imgPlatforms)
|
||||||
|
|
||||||
created := ""
|
created := ""
|
||||||
createdAt := time.Unix(img.Created, 0)
|
createdAt := time.Unix(img.Created, 0)
|
||||||
@@ -149,40 +150,87 @@ func list(ctx context.Context, uncli *cli.CLI, opts listOptions) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort images by created time (newest first), then by machine name.
|
// Sort images by name, then by machine name.
|
||||||
sort.Slice(rows, func(i, j int) bool {
|
sort.Slice(rows, func(i, j int) bool {
|
||||||
if rows[i].createdUnix != rows[j].createdUnix {
|
if rows[i].name != rows[j].name {
|
||||||
return rows[i].createdUnix > rows[j].createdUnix
|
return rows[i].name < rows[j].name
|
||||||
}
|
}
|
||||||
return rows[i].machine < rows[j].machine
|
return rows[i].machine < rows[j].machine
|
||||||
})
|
})
|
||||||
|
|
||||||
// Print the images in a table format.
|
// Print the images in a table format.
|
||||||
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
|
fmt.Println(formatImageTable(rows))
|
||||||
if _, err = fmt.Fprintln(tw, "IMAGE ID\tNAME\tPLATFORMS\tCREATED\tSIZE\tSTORE\tMACHINE"); err != nil {
|
|
||||||
return fmt.Errorf("write header: %w", err)
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, img := range rows {
|
// imagePlatforms returns a list of platforms supported by the image and a boolean indicating if it's multi-platform.
|
||||||
if _, err = fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
|
func imagePlatforms(img image.Summary) ([]string, bool) {
|
||||||
img.id, img.name, img.platforms, img.createdHuman, img.size, img.store, img.machine); err != nil {
|
|
||||||
return fmt.Errorf("write row: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tw.Flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
func imagePlatforms(img image.Summary) []string {
|
|
||||||
var formattedPlatforms []string
|
var formattedPlatforms []string
|
||||||
|
multiPlatform := false
|
||||||
|
|
||||||
for _, m := range img.Manifests {
|
for _, m := range img.Manifests {
|
||||||
if m.Kind != image.ManifestKindImage || !m.Available {
|
if m.Kind != image.ManifestKindImage || !m.Available {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if m.ID != img.ID {
|
||||||
|
// There is an image manifest that has digest different from the main image digest.
|
||||||
|
// This means the image manifest is an index or a manifest list (multi-platform image).
|
||||||
|
multiPlatform = true
|
||||||
|
}
|
||||||
formattedPlatforms = append(formattedPlatforms, platforms.Format(m.ImageData.Platform))
|
formattedPlatforms = append(formattedPlatforms, platforms.Format(m.ImageData.Platform))
|
||||||
}
|
}
|
||||||
|
|
||||||
slices.Sort(formattedPlatforms)
|
slices.Sort(formattedPlatforms)
|
||||||
return formattedPlatforms
|
|
||||||
|
return formattedPlatforms, multiPlatform
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatPlatforms(platforms []string) string {
|
||||||
|
if len(platforms) == 0 {
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
|
|
||||||
|
platformStyle := lipgloss.NewStyle().
|
||||||
|
BorderForeground(lipgloss.Color("152")).
|
||||||
|
Foreground(lipgloss.Color("0")).
|
||||||
|
Background(lipgloss.Color("152"))
|
||||||
|
// Use fancy pill borders only if the output is a terminal with color support.
|
||||||
|
if lipgloss.ColorProfile() != termenv.Ascii {
|
||||||
|
platformStyle = platformStyle.Border(lipgloss.Border{Left: "", Right: ""}, false, true, false, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
styledPlatforms := make([]string, len(platforms))
|
||||||
|
for i, p := range platforms {
|
||||||
|
styledPlatforms[i] = platformStyle.Render(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(styledPlatforms, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatImageTable(rows []imageRow) string {
|
||||||
|
t := table.New().
|
||||||
|
// Remove the default border.
|
||||||
|
Border(lipgloss.Border{}).
|
||||||
|
BorderTop(false).
|
||||||
|
BorderBottom(false).
|
||||||
|
BorderLeft(false).
|
||||||
|
BorderRight(false).
|
||||||
|
BorderHeader(false).
|
||||||
|
BorderColumn(false).
|
||||||
|
StyleFunc(func(row, col int) lipgloss.Style {
|
||||||
|
if row == table.HeaderRow {
|
||||||
|
return lipgloss.NewStyle().Bold(true).PaddingRight(3)
|
||||||
|
}
|
||||||
|
// Regular style for data rows with padding.
|
||||||
|
return lipgloss.NewStyle().PaddingRight(3)
|
||||||
|
}).
|
||||||
|
Headers("IMAGE ID", "NAME", "PLATFORMS", "CREATED", "SIZE", "STORE", "MACHINE")
|
||||||
|
|
||||||
|
for _, row := range rows {
|
||||||
|
t.Row(row.id, row.name, row.platforms, row.createdHuman, row.size, row.store, row.machine)
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.String()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ require (
|
|||||||
github.com/miekg/dns v1.1.65
|
github.com/miekg/dns v1.1.65
|
||||||
github.com/mitchellh/mapstructure v1.5.0
|
github.com/mitchellh/mapstructure v1.5.0
|
||||||
github.com/moby/term v0.5.0
|
github.com/moby/term v0.5.0
|
||||||
|
github.com/muesli/termenv v0.16.0
|
||||||
github.com/opencontainers/go-digest v1.0.0
|
github.com/opencontainers/go-digest v1.0.0
|
||||||
github.com/opencontainers/image-spec v1.1.1
|
github.com/opencontainers/image-spec v1.1.1
|
||||||
github.com/psviderski/unregistry v0.3.1
|
github.com/psviderski/unregistry v0.3.1
|
||||||
@@ -234,7 +235,6 @@ require (
|
|||||||
github.com/mr-tron/base58 v1.2.0 // indirect
|
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
github.com/muesli/termenv v0.16.0 // indirect
|
|
||||||
github.com/multiformats/go-base32 v0.1.0 // indirect
|
github.com/multiformats/go-base32 v0.1.0 // indirect
|
||||||
github.com/multiformats/go-base36 v0.2.0 // indirect
|
github.com/multiformats/go-base36 v0.2.0 // indirect
|
||||||
github.com/multiformats/go-multiaddr v0.13.0 // indirect
|
github.com/multiformats/go-multiaddr v0.13.0 // indirect
|
||||||
|
|||||||
Reference in New Issue
Block a user