Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions cmd/compose/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ import (
"context"
"fmt"
"io"
"maps"
"slices"
"sort"
"strings"
"time"

"github.com/containerd/platforms"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/go-units"
Expand Down Expand Up @@ -86,13 +88,10 @@ func runImages(ctx context.Context, dockerCli command.Cli, backend api.Service,
return nil
}

sort.Slice(images, func(i, j int) bool {
return images[i].ContainerName < images[j].ContainerName
})

return formatter.Print(images, opts.Format, dockerCli.Out(),
func(w io.Writer) {
for _, img := range images {
for _, container := range slices.Sorted(maps.Keys(images)) {
img := images[container]
id := stringid.TruncateID(img.ID)
size := units.HumanSizeWithPrecision(float64(img.Size), 3)
repo := img.Repository
Expand All @@ -103,8 +102,10 @@ func runImages(ctx context.Context, dockerCli command.Cli, backend api.Service,
if tag == "" {
tag = "<none>"
}
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", img.ContainerName, repo, tag, id, size)
created := units.HumanDuration(time.Now().UTC().Sub(img.LastTagTime)) + " ago"
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
container, repo, tag, platforms.Format(img.Platform), id, size, created)
}
},
"CONTAINER", "REPOSITORY", "TAG", "IMAGE ID", "SIZE")
"CONTAINER", "REPOSITORY", "TAG", "PLATFORM", "IMAGE ID", "SIZE", "CREATED")
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ require (
github.com/spf13/cobra v1.9.1
github.com/spf13/pflag v1.0.6
github.com/stretchr/testify v1.10.0
github.com/theupdateframework/notary v0.7.0
github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0
go.opentelemetry.io/otel v1.35.0
Expand Down Expand Up @@ -161,6 +160,7 @@ require (
github.com/secure-systems-lab/go-securesystemslib v0.4.0 // indirect
github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b // indirect
github.com/shibumi/go-pathspec v1.3.0 // indirect
github.com/theupdateframework/notary v0.7.0 // indirect
github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323 // indirect
github.com/tonistiigi/fsutil v0.0.0-20250417144416-3f76f8130144 // indirect
github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4 // indirect
Expand Down
15 changes: 8 additions & 7 deletions pkg/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"time"

"github.com/compose-spec/compose-go/v2/types"
"github.com/containerd/platforms"
"github.com/docker/cli/opts"
)

Expand Down Expand Up @@ -78,7 +79,7 @@ type Service interface {
// Publish executes the equivalent to a `compose publish`
Publish(ctx context.Context, project *types.Project, repository string, options PublishOptions) error
// Images executes the equivalent of a `compose images`
Images(ctx context.Context, projectName string, options ImagesOptions) ([]ImageSummary, error)
Images(ctx context.Context, projectName string, options ImagesOptions) (map[string]ImageSummary, error)
// MaxConcurrency defines upper limit for concurrent operations against engine API
MaxConcurrency(parallel int)
// DryRunMode defines if dry run applies to the command
Expand Down Expand Up @@ -535,12 +536,12 @@ type ContainerProcSummary struct {

// ImageSummary holds container image description
type ImageSummary struct {
ID string
ContainerName string
Repository string
Tag string
Size int64
LastTagTime time.Time
ID string
Repository string
Tag string
Platform platforms.Platform
Size int64
LastTagTime time.Time
}

// ServiceStatus hold status about a service
Expand Down
71 changes: 54 additions & 17 deletions pkg/compose/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,18 @@ import (
"sync"

cerrdefs "github.com/containerd/errdefs"
"github.com/containerd/platforms"
"github.com/distribution/reference"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/versions"
"github.com/docker/docker/client"
"golang.org/x/sync/errgroup"

"github.com/docker/compose/v2/pkg/api"
)

func (s *composeService) Images(ctx context.Context, projectName string, options api.ImagesOptions) ([]api.ImageSummary, error) {
func (s *composeService) Images(ctx context.Context, projectName string, options api.ImagesOptions) (map[string]api.ImageSummary, error) {
projectName = strings.ToLower(projectName)
allContainers, err := s.apiClient().ContainerList(ctx, container.ListOptions{
All: true,
Expand All @@ -53,27 +56,61 @@ func (s *composeService) Images(ctx context.Context, projectName string, options
containers = allContainers
}

images := []string{}
for _, c := range containers {
if !slices.Contains(images, c.Image) {
images = append(images, c.Image)
}
}
imageSummaries, err := s.getImageSummaries(ctx, images)
version, err := s.RuntimeVersion(ctx)
if err != nil {
return nil, err
}
summary := make([]api.ImageSummary, len(containers))
for i, c := range containers {
img, ok := imageSummaries[c.Image]
if !ok {
return nil, fmt.Errorf("failed to retrieve image for container %s", getCanonicalContainerName(c))
}
withPlatform := versions.GreaterThanOrEqualTo(version, "1.49")

summary := map[string]api.ImageSummary{}
var mux sync.Mutex
eg, ctx := errgroup.WithContext(ctx)
for _, c := range containers {
eg.Go(func() error {
image, err := s.apiClient().ImageInspect(ctx, c.Image)
if err != nil {
return err
}
id := image.ID // platform-specific image ID can't be combined with image tag, see https://github.com/moby/moby/issues/49995

if withPlatform && c.ImageManifestDescriptor != nil && c.ImageManifestDescriptor.Platform != nil {
image, err = s.apiClient().ImageInspect(ctx, c.Image, client.ImageInspectWithPlatform(c.ImageManifestDescriptor.Platform))
if err != nil {
return err
}
}

summary[i] = img
summary[i].ContainerName = getCanonicalContainerName(c)
var repository, tag string
ref, err := reference.ParseDockerRef(c.Image)
if err == nil {
// ParseDockerRef will reject a local image ID
repository = reference.FamiliarName(ref)
if tagged, ok := ref.(reference.Tagged); ok {
tag = tagged.Tag()
}
}

mux.Lock()
defer mux.Unlock()
summary[getCanonicalContainerName(c)] = api.ImageSummary{
ID: id,
Repository: repository,
Tag: tag,
Platform: platforms.Platform{
Architecture: image.Architecture,
OS: image.Os,
OSVersion: image.OsVersion,
Variant: image.Variant,
},
Size: image.Size,
LastTagTime: image.Metadata.LastTagTime,
}
return nil
})
}
return summary, nil

err = eg.Wait()
return summary, err
}

func (s *composeService) getImageSummaries(ctx context.Context, repoTags []string) (map[string]api.ImageSummary, error) {
Expand Down
39 changes: 19 additions & 20 deletions pkg/compose/images_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"strings"
"testing"

"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/image"
Expand All @@ -42,9 +43,10 @@ func TestImages(t *testing.T) {
ctx := context.Background()
args := filters.NewArgs(projectFilter(strings.ToLower(testProject)))
listOpts := container.ListOptions{All: true, Filters: args}
api.EXPECT().ServerVersion(gomock.Any()).Return(types.Version{APIVersion: "1.96"}, nil).AnyTimes()
image1 := imageInspect("image1", "foo:1", 12345)
image2 := imageInspect("image2", "bar:2", 67890)
api.EXPECT().ImageInspect(anyCancellableContext(), "foo:1").Return(image1, nil)
api.EXPECT().ImageInspect(anyCancellableContext(), "foo:1").Return(image1, nil).MaxTimes(2)
api.EXPECT().ImageInspect(anyCancellableContext(), "bar:2").Return(image2, nil)
c1 := containerDetail("service1", "123", "running", "foo:1")
c2 := containerDetail("service1", "456", "running", "bar:2")
Expand All @@ -54,27 +56,24 @@ func TestImages(t *testing.T) {

images, err := tested.Images(ctx, strings.ToLower(testProject), compose.ImagesOptions{})

expected := []compose.ImageSummary{
{
ID: "image1",
ContainerName: "123",
Repository: "foo",
Tag: "1",
Size: 12345,
expected := map[string]compose.ImageSummary{
"123": {
ID: "image1",
Repository: "foo",
Tag: "1",
Size: 12345,
},
{
ID: "image2",
ContainerName: "456",
Repository: "bar",
Tag: "2",
Size: 67890,
"456": {
ID: "image2",
Repository: "bar",
Tag: "2",
Size: 67890,
},
{
ID: "image1",
ContainerName: "789",
Repository: "foo",
Tag: "1",
Size: 12345,
"789": {
ID: "image1",
Repository: "foo",
Tag: "1",
Size: 12345,
},
}
assert.NilError(t, err)
Expand Down
1 change: 0 additions & 1 deletion pkg/mocks/mock_docker_api.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

54 changes: 3 additions & 51 deletions pkg/mocks/mock_docker_cli.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 2 additions & 3 deletions pkg/mocks/mock_docker_compose_api.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.