include platform and creation date listing image used by running compose application
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
This commit is contained in:
parent
f4fc010d6b
commit
eb3074bbda
@ -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"
|
||||
@ -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
|
||||
@ -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
go.mod
2
go.mod
@ -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
|
||||
@ -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
|
||||
|
@ -24,6 +24,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/containerd/platforms"
|
||||
"github.com/docker/cli/opts"
|
||||
)
|
||||
|
||||
@ -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
|
||||
@ -536,9 +537,9 @@ type ContainerProcSummary struct {
|
||||
// ImageSummary holds container image description
|
||||
type ImageSummary struct {
|
||||
ID string
|
||||
ContainerName string
|
||||
Repository string
|
||||
Tag string
|
||||
Platform platforms.Platform
|
||||
Size int64
|
||||
LastTagTime time.Time
|
||||
}
|
||||
|
@ -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,
|
||||
@ -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()
|
||||
}
|
||||
return summary, nil
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
err = eg.Wait()
|
||||
return summary, err
|
||||
}
|
||||
|
||||
func (s *composeService) getImageSummaries(ctx context.Context, repoTags []string) (map[string]api.ImageSummary, error) {
|
||||
|
@ -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"
|
||||
@ -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")
|
||||
@ -54,24 +56,21 @@ func TestImages(t *testing.T) {
|
||||
|
||||
images, err := tested.Images(ctx, strings.ToLower(testProject), compose.ImagesOptions{})
|
||||
|
||||
expected := []compose.ImageSummary{
|
||||
{
|
||||
expected := map[string]compose.ImageSummary{
|
||||
"123": {
|
||||
ID: "image1",
|
||||
ContainerName: "123",
|
||||
Repository: "foo",
|
||||
Tag: "1",
|
||||
Size: 12345,
|
||||
},
|
||||
{
|
||||
"456": {
|
||||
ID: "image2",
|
||||
ContainerName: "456",
|
||||
Repository: "bar",
|
||||
Tag: "2",
|
||||
Size: 67890,
|
||||
},
|
||||
{
|
||||
"789": {
|
||||
ID: "image1",
|
||||
ContainerName: "789",
|
||||
Repository: "foo",
|
||||
Tag: "1",
|
||||
Size: 12345,
|
||||
|
@ -5,7 +5,6 @@
|
||||
//
|
||||
// mockgen -destination pkg/mocks/mock_docker_api.go -package mocks github.com/docker/docker/client APIClient
|
||||
//
|
||||
|
||||
// Package mocks is a generated GoMock package.
|
||||
package mocks
|
||||
|
||||
|
@ -5,7 +5,6 @@
|
||||
//
|
||||
// mockgen -destination pkg/mocks/mock_docker_cli.go -package mocks github.com/docker/cli/cli/command Cli
|
||||
//
|
||||
|
||||
// Package mocks is a generated GoMock package.
|
||||
package mocks
|
||||
|
||||
@ -16,12 +15,8 @@ import (
|
||||
configfile "github.com/docker/cli/cli/config/configfile"
|
||||
docker "github.com/docker/cli/cli/context/docker"
|
||||
store "github.com/docker/cli/cli/context/store"
|
||||
store0 "github.com/docker/cli/cli/manifest/store"
|
||||
client "github.com/docker/cli/cli/registry/client"
|
||||
streams "github.com/docker/cli/cli/streams"
|
||||
trust "github.com/docker/cli/cli/trust"
|
||||
client0 "github.com/docker/docker/client"
|
||||
client1 "github.com/theupdateframework/notary/client"
|
||||
client "github.com/docker/docker/client"
|
||||
metric "go.opentelemetry.io/otel/metric"
|
||||
resource "go.opentelemetry.io/otel/sdk/resource"
|
||||
trace "go.opentelemetry.io/otel/trace"
|
||||
@ -85,10 +80,10 @@ func (mr *MockCliMockRecorder) BuildKitEnabled() *gomock.Call {
|
||||
}
|
||||
|
||||
// Client mocks base method.
|
||||
func (m *MockCli) Client() client0.APIClient {
|
||||
func (m *MockCli) Client() client.APIClient {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Client")
|
||||
ret0, _ := ret[0].(client0.APIClient)
|
||||
ret0, _ := ret[0].(client.APIClient)
|
||||
return ret0
|
||||
}
|
||||
|
||||
@ -224,20 +219,6 @@ func (mr *MockCliMockRecorder) In() *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "In", reflect.TypeOf((*MockCli)(nil).In))
|
||||
}
|
||||
|
||||
// ManifestStore mocks base method.
|
||||
func (m *MockCli) ManifestStore() store0.Store {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ManifestStore")
|
||||
ret0, _ := ret[0].(store0.Store)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// ManifestStore indicates an expected call of ManifestStore.
|
||||
func (mr *MockCliMockRecorder) ManifestStore() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ManifestStore", reflect.TypeOf((*MockCli)(nil).ManifestStore))
|
||||
}
|
||||
|
||||
// MeterProvider mocks base method.
|
||||
func (m *MockCli) MeterProvider() metric.MeterProvider {
|
||||
m.ctrl.T.Helper()
|
||||
@ -252,21 +233,6 @@ func (mr *MockCliMockRecorder) MeterProvider() *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MeterProvider", reflect.TypeOf((*MockCli)(nil).MeterProvider))
|
||||
}
|
||||
|
||||
// NotaryClient mocks base method.
|
||||
func (m *MockCli) NotaryClient(arg0 trust.ImageRefAndAuth, arg1 []string) (client1.Repository, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "NotaryClient", arg0, arg1)
|
||||
ret0, _ := ret[0].(client1.Repository)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// NotaryClient indicates an expected call of NotaryClient.
|
||||
func (mr *MockCliMockRecorder) NotaryClient(arg0, arg1 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NotaryClient", reflect.TypeOf((*MockCli)(nil).NotaryClient), arg0, arg1)
|
||||
}
|
||||
|
||||
// Out mocks base method.
|
||||
func (m *MockCli) Out() *streams.Out {
|
||||
m.ctrl.T.Helper()
|
||||
@ -281,20 +247,6 @@ func (mr *MockCliMockRecorder) Out() *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Out", reflect.TypeOf((*MockCli)(nil).Out))
|
||||
}
|
||||
|
||||
// RegistryClient mocks base method.
|
||||
func (m *MockCli) RegistryClient(arg0 bool) client.RegistryClient {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "RegistryClient", arg0)
|
||||
ret0, _ := ret[0].(client.RegistryClient)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// RegistryClient indicates an expected call of RegistryClient.
|
||||
func (mr *MockCliMockRecorder) RegistryClient(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegistryClient", reflect.TypeOf((*MockCli)(nil).RegistryClient), arg0)
|
||||
}
|
||||
|
||||
// Resource mocks base method.
|
||||
func (m *MockCli) Resource() *resource.Resource {
|
||||
m.ctrl.T.Helper()
|
||||
|
@ -5,7 +5,6 @@
|
||||
//
|
||||
// mockgen -destination pkg/mocks/mock_docker_compose_api.go -package mocks -source=./pkg/api/api.go Service
|
||||
//
|
||||
|
||||
// Package mocks is a generated GoMock package.
|
||||
package mocks
|
||||
|
||||
@ -199,10 +198,10 @@ func (mr *MockServiceMockRecorder) Generate(ctx, options any) *gomock.Call {
|
||||
}
|
||||
|
||||
// Images mocks base method.
|
||||
func (m *MockService) Images(ctx context.Context, projectName string, options api.ImagesOptions) ([]api.ImageSummary, error) {
|
||||
func (m *MockService) Images(ctx context.Context, projectName string, options api.ImagesOptions) (map[string]api.ImageSummary, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Images", ctx, projectName, options)
|
||||
ret0, _ := ret[0].([]api.ImageSummary)
|
||||
ret0, _ := ret[0].(map[string]api.ImageSummary)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user