mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 11:03:34 +00:00
feat(images): list image platforms and do not group images by machine
This commit is contained in:
+80
-39
@@ -4,11 +4,14 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"text/tabwriter"
|
"text/tabwriter"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/containerd/platforms"
|
||||||
|
"github.com/docker/docker/api/types/image"
|
||||||
"github.com/docker/go-units"
|
"github.com/docker/go-units"
|
||||||
"github.com/psviderski/uncloud/internal/cli"
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
@@ -53,6 +56,18 @@ func NewListCommand() *cobra.Command {
|
|||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// imageRow represents a single image with its metadata for display.
|
||||||
|
type imageRow struct {
|
||||||
|
id string
|
||||||
|
name string
|
||||||
|
platforms string
|
||||||
|
createdHuman string
|
||||||
|
createdUnix int64
|
||||||
|
size string
|
||||||
|
store string
|
||||||
|
machine string
|
||||||
|
}
|
||||||
|
|
||||||
func list(ctx context.Context, uncli *cli.CLI, opts listOptions) error {
|
func list(ctx context.Context, uncli *cli.CLI, opts listOptions) error {
|
||||||
clusterClient, err := uncli.ConnectCluster(ctx, opts.context)
|
clusterClient, err := uncli.ConnectCluster(ctx, opts.context)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -80,53 +95,33 @@ func list(ctx context.Context, uncli *cli.CLI, opts listOptions) error {
|
|||||||
return fmt.Errorf("list images: %w", err)
|
return fmt.Errorf("list images: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if there are any images across all machines.
|
// Collect all images from all machines.
|
||||||
hasImages := false
|
var rows []imageRow
|
||||||
|
|
||||||
for _, machineImages := range clusterImages {
|
for _, machineImages := range clusterImages {
|
||||||
if len(machineImages.Images) > 0 {
|
// Get machine name for better readability.
|
||||||
hasImages = true
|
machineName := machineImages.Metadata.Machine
|
||||||
break
|
if m := allMachines.FindByNameOrID(machineName); m != nil {
|
||||||
|
machineName = m.Machine.Name
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if !hasImages {
|
|
||||||
fmt.Println("No images found.")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace machine IDs with names in metadata for better readability.
|
|
||||||
for _, machineImages := range clusterImages {
|
|
||||||
if m := allMachines.FindByNameOrID(machineImages.Metadata.Machine); m != nil {
|
|
||||||
machineImages.Metadata.Machine = m.Machine.Name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Sort machines alphabetically by name.
|
|
||||||
sort.Slice(clusterImages, func(i, j int) bool {
|
|
||||||
return clusterImages[i].Metadata.Machine < clusterImages[j].Metadata.Machine
|
|
||||||
})
|
|
||||||
|
|
||||||
// Print the images in a table format.
|
|
||||||
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
|
|
||||||
if _, err = fmt.Fprintln(tw, "MACHINE\tIMAGE\tIMAGE ID\tCREATED\tSIZE\tSTORE"); err != nil {
|
|
||||||
return fmt.Errorf("write header: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print rows for each machine's images.
|
|
||||||
for _, machineImages := range clusterImages {
|
|
||||||
store := "docker"
|
store := "docker"
|
||||||
if machineImages.ContainerdStore {
|
if machineImages.ContainerdStore {
|
||||||
store = "containerd"
|
store = "containerd"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print each image for this machine.
|
// Process each image for this machine.
|
||||||
for _, img := range machineImages.Images {
|
for _, img := range machineImages.Images {
|
||||||
imageName := "<none>"
|
// Show the first 12 chars without 'sha256:' as the image ID like Docker does.
|
||||||
|
id := strings.TrimPrefix(img.ID, "sha256:")[:12]
|
||||||
|
|
||||||
|
name := "<none>"
|
||||||
if len(img.RepoTags) > 0 && img.RepoTags[0] != "<none>:<none>" {
|
if len(img.RepoTags) > 0 && img.RepoTags[0] != "<none>:<none>" {
|
||||||
imageName = img.RepoTags[0]
|
name = img.RepoTags[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show the first 12 chars without 'sha256:' as the image ID like Docker does.
|
imgPlatforms := imagePlatforms(img)
|
||||||
imageID := strings.TrimPrefix(img.ID, "sha256:")[:12]
|
formattedPlatforms := strings.Join(imgPlatforms, ",")
|
||||||
|
|
||||||
created := ""
|
created := ""
|
||||||
createdAt := time.Unix(img.Created, 0)
|
createdAt := time.Unix(img.Created, 0)
|
||||||
@@ -136,12 +131,58 @@ func list(ctx context.Context, uncli *cli.CLI, opts listOptions) error {
|
|||||||
|
|
||||||
size := units.HumanSizeWithPrecision(float64(img.Size), 3)
|
size := units.HumanSizeWithPrecision(float64(img.Size), 3)
|
||||||
|
|
||||||
if _, err = fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\t%s\n",
|
rows = append(rows, imageRow{
|
||||||
machineImages.Metadata.Machine, imageName, imageID, created, size, store); err != nil {
|
id: id,
|
||||||
return fmt.Errorf("write row: %w", err)
|
name: name,
|
||||||
}
|
platforms: formattedPlatforms,
|
||||||
|
createdHuman: created,
|
||||||
|
createdUnix: img.Created,
|
||||||
|
size: size,
|
||||||
|
store: store,
|
||||||
|
machine: machineName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rows) == 0 {
|
||||||
|
fmt.Println("No images found.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort images by created time (newest first), then by machine name.
|
||||||
|
sort.Slice(rows, func(i, j int) bool {
|
||||||
|
if rows[i].createdUnix != rows[j].createdUnix {
|
||||||
|
return rows[i].createdUnix > rows[j].createdUnix
|
||||||
|
}
|
||||||
|
return rows[i].machine < rows[j].machine
|
||||||
|
})
|
||||||
|
|
||||||
|
// Print the images in a table format.
|
||||||
|
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
|
||||||
|
if _, err = fmt.Fprintln(tw, "IMAGE ID\tNAME\tPLATFORMS\tCREATED\tSIZE\tSTORE\tMACHINE"); err != nil {
|
||||||
|
return fmt.Errorf("write header: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, img := range rows {
|
||||||
|
if _, err = fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
|
||||||
|
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()
|
return tw.Flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func imagePlatforms(img image.Summary) []string {
|
||||||
|
var formattedPlatforms []string
|
||||||
|
|
||||||
|
for _, m := range img.Manifests {
|
||||||
|
if m.Kind != image.ManifestKindImage || !m.Available {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
formattedPlatforms = append(formattedPlatforms, platforms.Format(m.ImageData.Platform))
|
||||||
|
}
|
||||||
|
|
||||||
|
slices.Sort(formattedPlatforms)
|
||||||
|
return formattedPlatforms
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user