diff --git a/daemon/containerd/image_list.go b/daemon/containerd/image_list.go index 23c3e26b48281..976763f59e7aa 100644 --- a/daemon/containerd/image_list.go +++ b/daemon/containerd/image_list.go @@ -393,17 +393,25 @@ func (i *ImageService) imageSummary(ctx context.Context, img images.Image, platf "error": err, "image": img.Name, }).Warn("unexpected image target (neither a manifest nor index)") - return nil, nil, nil + } else { + return nil, nil, err } - return nil, nil, err } if best == nil { - // TODO we should probably show *something* for images we've pulled - // but are 100% shallow or an empty manifest list/index - // ("tianon/scratch:index" is an empty example image index and - // "tianon/scratch:list" is an empty example manifest list) - return nil, nil, nil + target := img.Target + return &imagetypes.Summary{ + ID: target.Digest.String(), + RepoDigests: []string{target.Digest.String()}, + RepoTags: tagsByDigest[target.Digest], + Size: totalSize, + // -1 indicates that the value has not been set (avoids ambiguity + // between 0 (default) and "not set". We cannot use a pointer (nil) + // for this, as the JSON representation uses "omitempty", which would + // consider both "0" and "nil" to be "empty". + SharedSize: -1, + Containers: -1, + }, nil, nil } image, err := i.singlePlatformImage(ctx, i.content, tagsByDigest[best.RealTarget.Digest], best) diff --git a/daemon/containerd/image_list_test.go b/daemon/containerd/image_list_test.go index 344d73bae3571..f753ba72586ae 100644 --- a/daemon/containerd/image_list_test.go +++ b/daemon/containerd/image_list_test.go @@ -7,6 +7,7 @@ import ( "math/rand" "os" "path/filepath" + "slices" "sort" "strconv" "testing" @@ -206,6 +207,9 @@ func TestImageList(t *testing.T) { configTarget, err := specialimage.ConfigTarget(blobsDir) assert.NilError(t, err) + textplain, err := specialimage.TextPlain(blobsDir) + assert.NilError(t, err) + cs := &blobsDirContentStore{blobs: filepath.Join(blobsDir, "blobs/sha256")} for _, tc := range []struct { @@ -276,17 +280,34 @@ func TestImageList(t *testing.T) { name: "three images, one is an empty index", images: imagesFromIndex(multilayer, emptyIndex, twoplatform), check: func(t *testing.T, all []*imagetypes.Summary) { - assert.Check(t, is.Len(all, 2)) + assert.Check(t, is.Len(all, 3)) }, }, { - // Make sure an invalid image target doesn't break the whole operation name: "one good image, second has config as a target", images: imagesFromIndex(multilayer, configTarget), check: func(t *testing.T, all []*imagetypes.Summary) { - assert.Check(t, is.Len(all, 1)) + assert.Check(t, is.Len(all, 2)) + + sort.Slice(all, func(i, j int) bool { + return slices.Contains(all[i].RepoTags, "multilayer:latest") + }) assert.Check(t, is.Equal(all[0].ID, multilayer.Manifests[0].Digest.String())) + assert.Check(t, is.Len(all[0].Manifests, 1)) + + assert.Check(t, is.Equal(all[1].ID, configTarget.Manifests[0].Digest.String())) + assert.Check(t, is.Len(all[1].Manifests, 0)) + }, + }, + { + name: "a non-container image manifest", + images: imagesFromIndex(textplain), + check: func(t *testing.T, all []*imagetypes.Summary) { + assert.Check(t, is.Len(all, 1)) + assert.Check(t, is.Equal(all[0].ID, textplain.Manifests[0].Digest.String())) + + assert.Assert(t, is.Len(all[0].Manifests, 0)) }, }, } { diff --git a/internal/testutils/specialimage/textplain.go b/internal/testutils/specialimage/textplain.go new file mode 100644 index 0000000000000..fbfc8f733db0f --- /dev/null +++ b/internal/testutils/specialimage/textplain.go @@ -0,0 +1,39 @@ +package specialimage + +import ( + "strings" + + "github.com/distribution/reference" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" +) + +// TextPlain creates an non-container image that only contains a text/plain blob. +func TextPlain(dir string) (*ocispec.Index, error) { + ref, err := reference.ParseNormalizedNamed("tianon/test:text-plain") + if err != nil { + return nil, err + } + + emptyJsonDesc, err := writeBlob(dir, "text/plain", strings.NewReader("{}")) + if err != nil { + return nil, err + } + + configDesc := emptyJsonDesc + configDesc.MediaType = "application/vnd.oci.empty.v1+json" + + desc, err := writeJsonBlob(dir, ocispec.MediaTypeImageManifest, ocispec.Manifest{ + Config: configDesc, + Layers: []ocispec.Descriptor{ + emptyJsonDesc, + }, + }) + if err != nil { + return nil, err + } + desc.Annotations = map[string]string{ + "io.containerd.image.name": ref.String(), + } + + return ociImage(dir, nil, desc) +}