mirror of
https://github.com/psviderski/uncloud.git
synced 2026-11-10 20:54:17 +00:00
feat(images): include all tags and digest references when listing images
This commit is contained in:
+53
-44
@@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/charmbracelet/colorprofile"
|
"github.com/charmbracelet/colorprofile"
|
||||||
"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/docker/pkg/stringid"
|
||||||
"github.com/docker/go-units"
|
"github.com/docker/go-units"
|
||||||
|
|
||||||
"github.com/psviderski/uncloud/internal/cli"
|
"github.com/psviderski/uncloud/internal/cli"
|
||||||
@@ -116,49 +117,7 @@ func list(ctx context.Context, uncli *cli.CLI, opts listOptions) error {
|
|||||||
store = "containerd"
|
store = "containerd"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process each image for this machine.
|
rows = append(rows, imageRows(machineImages.Images, machineName, store)...)
|
||||||
for _, img := range machineImages.Images {
|
|
||||||
// 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>" {
|
|
||||||
name = img.RepoTags[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
imgPlatforms, _ := imagePlatforms(img)
|
|
||||||
formattedPlatforms := formatPlatforms(imgPlatforms)
|
|
||||||
|
|
||||||
created := ""
|
|
||||||
createdAt := time.Unix(img.Created, 0)
|
|
||||||
if !createdAt.IsZero() {
|
|
||||||
created = units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago"
|
|
||||||
}
|
|
||||||
|
|
||||||
size := units.HumanSizeWithPrecision(float64(img.Size), 3)
|
|
||||||
|
|
||||||
// Check if the image is in use by any containers. Only supported by Docker API >=1.51
|
|
||||||
inUse := "-"
|
|
||||||
if img.Containers != -1 { // -1 means the info is not available.
|
|
||||||
if img.Containers > 0 {
|
|
||||||
inUse = lipgloss.NewStyle().Foreground(lipgloss.Color("10")).Render("●")
|
|
||||||
} else {
|
|
||||||
inUse = lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Render("○")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rows = append(rows, imageRow{
|
|
||||||
id: id,
|
|
||||||
name: name,
|
|
||||||
platforms: formattedPlatforms,
|
|
||||||
createdHuman: created,
|
|
||||||
createdUnix: img.Created,
|
|
||||||
size: size,
|
|
||||||
inUse: inUse,
|
|
||||||
store: store,
|
|
||||||
machine: machineName,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(rows) == 0 {
|
if len(rows) == 0 {
|
||||||
@@ -184,13 +143,63 @@ func list(ctx context.Context, uncli *cli.CLI, opts listOptions) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// imageRows keeps every tag visible and retains digest-only and unnamed images.
|
||||||
|
func imageRows(images []image.Summary, machineName, store string) []imageRow {
|
||||||
|
var rows []imageRow
|
||||||
|
for _, img := range images {
|
||||||
|
imageRefs := api.ImageReferences(img)
|
||||||
|
var imageNames []string
|
||||||
|
for _, ref := range imageRefs {
|
||||||
|
if !strings.Contains(ref, "@") {
|
||||||
|
imageNames = append(imageNames, ref)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(imageNames) == 0 {
|
||||||
|
imageNames = imageRefs
|
||||||
|
}
|
||||||
|
if len(imageNames) == 0 {
|
||||||
|
imageNames = []string{"<none>"}
|
||||||
|
}
|
||||||
|
|
||||||
|
imgPlatforms, _ := imagePlatforms(img)
|
||||||
|
created := ""
|
||||||
|
if img.Created > 0 {
|
||||||
|
created = units.HumanDuration(time.Since(time.Unix(img.Created, 0))) + " ago"
|
||||||
|
}
|
||||||
|
// Check if the image is in use by any containers. Only supported by Docker API >=1.51
|
||||||
|
inUse := "-"
|
||||||
|
if img.Containers >= 0 { // -1 means the info is not available.
|
||||||
|
inUse = lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Render("○")
|
||||||
|
if img.Containers > 0 {
|
||||||
|
inUse = lipgloss.NewStyle().Foreground(lipgloss.Color("10")).Render("●")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range imageNames {
|
||||||
|
rows = append(rows, imageRow{
|
||||||
|
id: stringid.TruncateID(img.ID),
|
||||||
|
name: name,
|
||||||
|
platforms: formatPlatforms(imgPlatforms),
|
||||||
|
createdHuman: created,
|
||||||
|
createdUnix: img.Created,
|
||||||
|
size: units.HumanSizeWithPrecision(float64(img.Size), 3),
|
||||||
|
inUse: inUse,
|
||||||
|
store: store,
|
||||||
|
machine: machineName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
// imagePlatforms returns a list of platforms supported by the image and a boolean indicating if it's multi-platform.
|
// imagePlatforms returns a list of platforms supported by the image and a boolean indicating if it's multi-platform.
|
||||||
func imagePlatforms(img image.Summary) ([]string, bool) {
|
func imagePlatforms(img image.Summary) ([]string, bool) {
|
||||||
var formattedPlatforms []string
|
var formattedPlatforms []string
|
||||||
multiPlatform := false
|
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 || m.ImageData == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"slices"
|
||||||
|
|
||||||
"github.com/distribution/reference"
|
"github.com/distribution/reference"
|
||||||
"github.com/docker/docker/api/types/image"
|
"github.com/docker/docker/api/types/image"
|
||||||
@@ -42,6 +43,28 @@ type ImageFilter struct {
|
|||||||
Name string
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ImageReferences returns the image tag and named digest references, sorted and without duplicates.
|
||||||
|
// References use familiar names such as "alpine:latest" and "alpine@sha256:...".
|
||||||
|
// Bare digests and invalid references are excluded. It returns nil if there are no named references.
|
||||||
|
func ImageReferences(img image.Summary) []string {
|
||||||
|
var refs []string
|
||||||
|
for _, names := range [][]string{img.RepoTags, img.RepoDigests} {
|
||||||
|
for _, name := range names {
|
||||||
|
ref, err := reference.ParseAnyReference(name)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// RepoDigests can contain bare digests. Keep only references with a repository name.
|
||||||
|
if named, ok := ref.(reference.Named); ok {
|
||||||
|
refs = append(refs, reference.FamiliarString(named))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
slices.Sort(refs)
|
||||||
|
return slices.Compact(refs)
|
||||||
|
}
|
||||||
|
|
||||||
// MachineRemoteImage represents an image in a remote registry fetched by a particular machine.
|
// MachineRemoteImage represents an image in a remote registry fetched by a particular machine.
|
||||||
type MachineRemoteImage struct {
|
type MachineRemoteImage struct {
|
||||||
Metadata *pb.Metadata
|
Metadata *pb.Metadata
|
||||||
|
|||||||
+1
-1
@@ -61,7 +61,7 @@ func (cli *Client) ListImages(ctx context.Context, filter api.ImageFilter) ([]ap
|
|||||||
// Broadcast the image list request to the specified machines or all machines if none specified.
|
// Broadcast the image list request to the specified machines or all machines if none specified.
|
||||||
listCtx := ProxyMachinesContext(ctx, filter.Machines)
|
listCtx := ProxyMachinesContext(ctx, filter.Machines)
|
||||||
|
|
||||||
opts := image.ListOptions{Manifests: true}
|
opts := image.ListOptions{All: true, Manifests: true}
|
||||||
if filter.Name != "" {
|
if filter.Name != "" {
|
||||||
opts.Filters = filters.NewArgs(
|
opts.Filters = filters.NewArgs(
|
||||||
filters.Arg("reference", filter.Name),
|
filters.Arg("reference", filter.Name),
|
||||||
|
|||||||
Reference in New Issue
Block a user