From a2c6cbe6333653eb8085f3c518c5fc3b78559edd Mon Sep 17 00:00:00 2001 From: Pasha Sviderski Date: Wed, 8 Oct 2025 12:33:13 +1000 Subject: [PATCH] chore(images): filter images by name with wildcard pattern using Docker filter --- cmd/uncloud/image/ls.go | 44 +++++++---------------------------------- pkg/api/image.go | 3 +++ pkg/client/image.go | 7 +++++++ 3 files changed, 17 insertions(+), 37 deletions(-) diff --git a/cmd/uncloud/image/ls.go b/cmd/uncloud/image/ls.go index 6e2baeb7..a677c183 100644 --- a/cmd/uncloud/image/ls.go +++ b/cmd/uncloud/image/ls.go @@ -29,7 +29,7 @@ func NewListCommand() *cobra.Command { opts := listOptions{} cmd := &cobra.Command{ - Use: "ls [IMAGE]", + Use: "ls [REPO:[TAG]]", Aliases: []string{"list"}, Short: "List images on machines in the cluster.", Long: "List images on machines in the cluster. By default, on all machines. Optionally filter by image name.", @@ -45,8 +45,8 @@ func NewListCommand() *cobra.Command { # List images filtered by name (with any tag) on all machines. uc image ls myapp - # List images filtered by name and tag on specific machine. - uc image ls myapp:1.2.3 -m machine1`, + # List images filtered by name pattern on specific machine. + uc image ls "myapp:1.*" -m machine1`, Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { if len(args) > 0 { @@ -102,7 +102,10 @@ func list(ctx context.Context, uncli *cli.CLI, opts listOptions) error { machines := cli.ExpandCommaSeparatedValues(opts.machines) - clusterImages, err := clusterClient.ListImages(ctx, api.ImageFilter{Machines: machines}) + clusterImages, err := clusterClient.ListImages(ctx, api.ImageFilter{ + Machines: machines, + Name: opts.nameFilter, + }) if err != nil { return fmt.Errorf("list images: %w", err) } @@ -156,8 +159,6 @@ func list(ctx context.Context, uncli *cli.CLI, opts listOptions) error { } } - rows = filterImagesByName(rows, opts.nameFilter) - if len(rows) == 0 { if opts.nameFilter != "" { fmt.Printf("No images matching '%s' found.\n", opts.nameFilter) @@ -181,37 +182,6 @@ func list(ctx context.Context, uncli *cli.CLI, opts listOptions) error { return nil } -// filterImagesByName filters the image rows by image name. It matches any tag of the image if only the name -// is provided. For example, if filter is "nginx", it matches "nginx", "nginx:latest", "nginx:1.25", etc. -// If filter is "nginx:latest", it matches only "nginx:latest". -func filterImagesByName(rows []imageRow, nameFilter string) []imageRow { - if nameFilter == "" { - return rows - } - - var filteredRows []imageRow - for _, row := range rows { - if row.name == "" { - continue - } - - if strings.Contains(nameFilter, ":") { - // Exact match if filter contains a tag. - if row.name == nameFilter { - filteredRows = append(filteredRows, row) - } - continue - } - - // Match by name prefix if filter does not contain a tag. - if strings.HasPrefix(row.name, nameFilter) && - len(row.name) > len(nameFilter) && row.name[len(nameFilter)] == ':' { - filteredRows = append(filteredRows, row) - } - } - return filteredRows -} - // 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) { var formattedPlatforms []string diff --git a/pkg/api/image.go b/pkg/api/image.go index 3372b756..1e8d616a 100644 --- a/pkg/api/image.go +++ b/pkg/api/image.go @@ -28,6 +28,9 @@ type ImageFilter struct { // Machines filters images to those present on the specified machines (names or IDs). // If empty, it matches images on all machines. Machines []string + // Name filters images by name (with or without tag). Accepts a wildcard pattern. + // If empty, it matches all image names. + Name string } // MachineRemoteImage represents an image in a remote registry fetched by a particular machine. diff --git a/pkg/client/image.go b/pkg/client/image.go index 4216587c..8f27d0d0 100644 --- a/pkg/client/image.go +++ b/pkg/client/image.go @@ -14,6 +14,7 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/docker/compose/v2/pkg/progress" "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/image" dockerclient "github.com/docker/docker/client" "github.com/docker/docker/pkg/jsonmessage" @@ -52,6 +53,12 @@ func (cli *Client) ListImages(ctx context.Context, filter api.ImageFilter) ([]ap } opts := image.ListOptions{Manifests: true} + if filter.Name != "" { + opts.Filters = filters.NewArgs( + filters.Arg("reference", filter.Name), + ) + } + optsBytes, err := json.Marshal(opts) if err != nil { return nil, fmt.Errorf("marshal options: %w", err)