diff --git a/daemon/containerd/image_list.go b/daemon/containerd/image_list.go index 600260a3fd6d3..80b2409015a3b 100644 --- a/daemon/containerd/image_list.go +++ b/daemon/containerd/image_list.go @@ -255,11 +255,13 @@ func (i *ImageService) imageSummary(ctx context.Context, img images.Image, platf target := img.Target() - chainIDs, err := img.RootFS(ctx) + diffIDs, err := img.RootFS(ctx) if err != nil { return err } + chainIDs := identity.ChainIDs(diffIDs) + ts, _, err := i.singlePlatformSize(ctx, img) if err != nil { return err @@ -650,6 +652,11 @@ func computeSharedSize(chainIDs []digest.Digest, layers map[digest.Digest]int, s } size, err := sizeFn(chainID) if err != nil { + // Several images might share the same layer and neither of them + // might be unpacked (for example if it's a non-host platform). + if cerrdefs.IsNotFound(err) { + continue + } return 0, err } sharedSize += size diff --git a/integration/image/list_test.go b/integration/image/list_test.go index b653802491795..54d725315f74f 100644 --- a/integration/image/list_test.go +++ b/integration/image/list_test.go @@ -10,10 +10,14 @@ import ( "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/image" "github.com/docker/docker/integration/internal/container" + "github.com/docker/docker/internal/testutils/specialimage" "github.com/docker/docker/testutil" + "github.com/docker/docker/testutil/daemon" "github.com/google/go-cmp/cmp/cmpopts" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" + "gotest.tools/v3/skip" ) // Regression : #38171 @@ -192,3 +196,34 @@ func TestAPIImagesFilters(t *testing.T) { }) } } + +// Verify that the size calculation operates on ChainIDs and not DiffIDs. +// This test calls an image list with two images that share one, top layer. +func TestAPIImagesListSizeShared(t *testing.T) { + skip.If(t, testEnv.DaemonInfo.OSType != "linux") + + ctx := setupTest(t) + + daemon := daemon.New(t) + daemon.Start(t) + defer daemon.Stop(t) + + client := daemon.NewClientT(t) + + specialimage.Load(ctx, t, client, func(dir string) (*ocispec.Index, error) { + return specialimage.MultiLayerCustom(dir, "multilayer:latest", []specialimage.SingleFileLayer{ + {Name: "bar", Content: []byte("2")}, + {Name: "foo", Content: []byte("1")}, + }) + }) + + specialimage.Load(ctx, t, client, func(dir string) (*ocispec.Index, error) { + return specialimage.MultiLayerCustom(dir, "multilayer2:latest", []specialimage.SingleFileLayer{ + {Name: "asdf", Content: []byte("3")}, + {Name: "foo", Content: []byte("1")}, + }) + }) + + _, err := client.ImageList(ctx, image.ListOptions{SharedSize: true}) + assert.NilError(t, err) +} diff --git a/internal/testutils/specialimage/multilayer.go b/internal/testutils/specialimage/multilayer.go index ee20380084252..b7352d019d958 100644 --- a/internal/testutils/specialimage/multilayer.go +++ b/internal/testutils/specialimage/multilayer.go @@ -16,20 +16,32 @@ import ( ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) +type SingleFileLayer struct { + Name string + Content []byte +} + func MultiLayer(dir string) (*ocispec.Index, error) { - const imageRef = "multilayer:latest" + return MultiLayerCustom(dir, "multilayer:latest", []SingleFileLayer{ + {Name: "foo", Content: []byte("1")}, + {Name: "bar", Content: []byte("2")}, + {Name: "hello", Content: []byte("world")}, + }) +} - layer1Desc, err := writeLayerWithOneFile(dir, "foo", []byte("1")) - if err != nil { - return nil, err - } - layer2Desc, err := writeLayerWithOneFile(dir, "bar", []byte("2")) - if err != nil { - return nil, err - } - layer3Desc, err := writeLayerWithOneFile(dir, "hello", []byte("world")) - if err != nil { - return nil, err +func MultiLayerCustom(dir string, imageRef string, layers []SingleFileLayer) (*ocispec.Index, error) { + var layerDescs []ocispec.Descriptor + var layerDgsts []digest.Digest + var layerBlobs []string + for _, layer := range layers { + layerDesc, err := writeLayerWithOneFile(dir, layer.Name, layer.Content) + if err != nil { + return nil, err + } + + layerDescs = append(layerDescs, layerDesc) + layerDgsts = append(layerDgsts, layerDesc.Digest) + layerBlobs = append(layerBlobs, blobPath(layerDesc)) } configDesc, err := writeJsonBlob(dir, ocispec.MediaTypeImageConfig, ocispec.Image{ @@ -39,7 +51,7 @@ func MultiLayer(dir string) (*ocispec.Index, error) { }, RootFS: ocispec.RootFS{ Type: "layers", - DiffIDs: []digest.Digest{layer1Desc.Digest, layer2Desc.Digest, layer3Desc.Digest}, + DiffIDs: layerDgsts, }, }) if err != nil { @@ -49,14 +61,14 @@ func MultiLayer(dir string) (*ocispec.Index, error) { manifest := ocispec.Manifest{ MediaType: ocispec.MediaTypeImageManifest, Config: configDesc, - Layers: []ocispec.Descriptor{layer1Desc, layer2Desc, layer3Desc}, + Layers: layerDescs, } legacyManifests := []manifestItem{ { Config: blobPath(configDesc), RepoTags: []string{imageRef}, - Layers: []string{blobPath(layer1Desc), blobPath(layer2Desc), blobPath(layer3Desc)}, + Layers: layerBlobs, }, } @@ -128,6 +140,7 @@ func writeLayerWithOneFile(dir string, filename string, content []byte) (ocispec if err != nil { return ocispec.Descriptor{}, err } + defer rd.Close() return writeBlob(dir, ocispec.MediaTypeImageLayer, rd) }