diff --git a/api/server/router/image/image_routes.go b/api/server/router/image/image_routes.go index 08a8e04ea3..bdf3705589 100644 --- a/api/server/router/image/image_routes.go +++ b/api/server/router/image/image_routes.go @@ -397,6 +397,7 @@ func (ir *imageRouter) getImagesJSON(ctx context.Context, w http.ResponseWriter, useNone := versions.LessThan(version, "1.43") withVirtualSize := versions.LessThan(version, "1.44") + noDescriptor := versions.LessThan(version, "1.48") for _, img := range images { if useNone { if len(img.RepoTags) == 0 && len(img.RepoDigests) == 0 { @@ -414,6 +415,9 @@ func (ir *imageRouter) getImagesJSON(ctx context.Context, w http.ResponseWriter, if withVirtualSize { img.VirtualSize = img.Size //nolint:staticcheck // ignore SA1019: field is deprecated, but still set on API < v1.44. } + if noDescriptor { + img.Descriptor = nil + } } return httputils.WriteJSON(w, http.StatusOK, images) diff --git a/api/swagger.yaml b/api/swagger.yaml index 6a771341c6..0167c8e9ce 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -2278,6 +2278,18 @@ definitions: x-omitempty: true items: $ref: "#/definitions/ImageManifestSummary" + Descriptor: + description: | + Descriptor is an OCI descriptor of the image target. + In case of a multi-platform image, this descriptor points to the OCI index + or a manifest list. + + This field is only present if the daemon provides a multi-platform image store. + + WARNING: This is experimental and may change at any time without any backward + compatibility. + x-nullable: true + $ref: "#/definitions/OCIDescriptor" AuthConfig: type: "object" diff --git a/api/types/image/summary.go b/api/types/image/summary.go index e87e216a28..c5ae6ab9ca 100644 --- a/api/types/image/summary.go +++ b/api/types/image/summary.go @@ -1,5 +1,7 @@ package image +import ocispec "github.com/opencontainers/image-spec/specs-go/v1" + type Summary struct { // Number of containers using this image. Includes both stopped and running @@ -42,6 +44,13 @@ type Summary struct { // Required: true ParentID string `json:"ParentId"` + // Descriptor is the OCI descriptor of the image target. + // It's only set if the daemon provides a multi-platform image store. + // + // WARNING: This is experimental and may change at any time without any backward + // compatibility. + Descriptor *ocispec.Descriptor `json:"Descriptor,omitempty"` + // Manifests is a list of image manifests available in this image. It // provides a more detailed view of the platform-specific image manifests or // other image-attached data like build attestations. diff --git a/daemon/containerd/image_list.go b/daemon/containerd/image_list.go index 2dd73c8d3a..7aa1c4c2ae 100644 --- a/daemon/containerd/image_list.go +++ b/daemon/containerd/image_list.go @@ -393,6 +393,7 @@ func (i *ImageService) imageSummary(ctx context.Context, img images.Image, platf // consider both "0" and "nil" to be "empty". SharedSize: -1, Containers: -1, + Descriptor: &target, }, nil, nil } @@ -402,6 +403,8 @@ func (i *ImageService) imageSummary(ctx context.Context, img images.Image, platf } image.Size = totalSize image.Manifests = manifestSummaries + target := img.Target + image.Descriptor = &target if opts.ContainerCount { image.Containers = containersCount diff --git a/daemon/containerd/image_list_test.go b/daemon/containerd/image_list_test.go index a1f0418fd9..83efcd5f58 100644 --- a/daemon/containerd/image_list_test.go +++ b/daemon/containerd/image_list_test.go @@ -187,20 +187,18 @@ func TestImageList(t *testing.T) { blobsDir := t.TempDir() - multilayer, err := specialimage.MultiLayer(blobsDir) - assert.NilError(t, err) + toContainerdImage := func(t *testing.T, imageFunc specialimage.SpecialImageFunc) images.Image { + idx, err := imageFunc(blobsDir) + assert.NilError(t, err) - twoplatform, err := specialimage.TwoPlatform(blobsDir) - assert.NilError(t, err) + return imagesFromIndex(idx)[0] + } - emptyIndex, err := specialimage.EmptyIndex(blobsDir) - assert.NilError(t, err) - - configTarget, err := specialimage.ConfigTarget(blobsDir) - assert.NilError(t, err) - - textplain, err := specialimage.TextPlain(blobsDir) - assert.NilError(t, err) + multilayer := toContainerdImage(t, specialimage.MultiLayer) + twoplatform := toContainerdImage(t, specialimage.TwoPlatform) + emptyIndex := toContainerdImage(t, specialimage.EmptyIndex) + configTarget := toContainerdImage(t, specialimage.ConfigTarget) + textplain := toContainerdImage(t, specialimage.TextPlain) cs := &blobsDirContentStore{blobs: filepath.Join(blobsDir, "blobs/sha256")} @@ -213,11 +211,14 @@ func TestImageList(t *testing.T) { }{ { name: "one multi-layer image", - images: imagesFromIndex(multilayer), + images: []images.Image{multilayer}, check: func(t *testing.T, all []*imagetypes.Summary) { assert.Check(t, is.Len(all, 1)) - assert.Check(t, is.Equal(all[0].ID, multilayer.Manifests[0].Digest.String())) + if assert.Check(t, all[0].Descriptor != nil) { + assert.Check(t, is.DeepEqual(*all[0].Descriptor, multilayer.Target)) + } + assert.Check(t, is.Equal(all[0].ID, multilayer.Target.Digest.String())) assert.Check(t, is.DeepEqual(all[0].RepoTags, []string{"multilayer:latest"})) assert.Check(t, is.Len(all[0].Manifests, 1)) @@ -227,11 +228,15 @@ func TestImageList(t *testing.T) { }, { name: "one image with two platforms is still one entry", - images: imagesFromIndex(twoplatform), + images: []images.Image{twoplatform}, check: func(t *testing.T, all []*imagetypes.Summary) { assert.Check(t, is.Len(all, 1)) - assert.Check(t, is.Equal(all[0].ID, twoplatform.Manifests[0].Digest.String())) + if assert.Check(t, all[0].Descriptor != nil) { + assert.Check(t, is.DeepEqual(*all[0].Descriptor, twoplatform.Target)) + } + assert.Check(t, is.Equal(all[0].ID, twoplatform.Target.Digest.String())) + assert.Check(t, is.DeepEqual(all[0].RepoTags, []string{"twoplatform:latest"})) i := all[0] @@ -249,14 +254,22 @@ func TestImageList(t *testing.T) { }, { name: "two images are two entries", - images: imagesFromIndex(multilayer, twoplatform), + images: []images.Image{multilayer, twoplatform}, check: func(t *testing.T, all []*imagetypes.Summary) { assert.Check(t, is.Len(all, 2)) - assert.Check(t, is.Equal(all[0].ID, multilayer.Manifests[0].Digest.String())) + if assert.Check(t, all[0].Descriptor != nil) { + assert.Check(t, is.DeepEqual(*all[0].Descriptor, multilayer.Target)) + } + assert.Check(t, is.Equal(all[0].ID, multilayer.Target.Digest.String())) + assert.Check(t, is.DeepEqual(all[0].RepoTags, []string{"multilayer:latest"})) - assert.Check(t, is.Equal(all[1].ID, twoplatform.Manifests[0].Digest.String())) + if assert.Check(t, all[1].Descriptor != nil) { + assert.Check(t, is.DeepEqual(*all[1].Descriptor, twoplatform.Target)) + } + assert.Check(t, is.Equal(all[1].ID, twoplatform.Target.Digest.String())) + assert.Check(t, is.DeepEqual(all[1].RepoTags, []string{"twoplatform:latest"})) assert.Check(t, is.Len(all[0].Manifests, 1)) @@ -270,14 +283,14 @@ func TestImageList(t *testing.T) { }, { name: "three images, one is an empty index", - images: imagesFromIndex(multilayer, emptyIndex, twoplatform), + images: []images.Image{multilayer, emptyIndex, twoplatform}, check: func(t *testing.T, all []*imagetypes.Summary) { assert.Check(t, is.Len(all, 3)) }, }, { name: "one good image, second has config as a target", - images: imagesFromIndex(multilayer, configTarget), + images: []images.Image{multilayer, configTarget}, check: func(t *testing.T, all []*imagetypes.Summary) { assert.Check(t, is.Len(all, 2)) @@ -285,19 +298,29 @@ func TestImageList(t *testing.T) { return slices.Contains(all[i].RepoTags, "multilayer:latest") }) - assert.Check(t, is.Equal(all[0].ID, multilayer.Manifests[0].Digest.String())) + if assert.Check(t, all[0].Descriptor != nil) { + assert.Check(t, is.DeepEqual(*all[0].Descriptor, multilayer.Target)) + } + assert.Check(t, is.Equal(all[0].ID, multilayer.Target.Digest.String())) + assert.Check(t, is.Len(all[0].Manifests, 1)) - assert.Check(t, is.Equal(all[1].ID, configTarget.Manifests[0].Digest.String())) + if assert.Check(t, all[1].Descriptor != nil) { + assert.Check(t, is.DeepEqual(*all[1].Descriptor, configTarget.Target)) + } assert.Check(t, is.Len(all[1].Manifests, 0)) }, }, { name: "a non-container image manifest", - images: imagesFromIndex(textplain), + images: []images.Image{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())) + + if assert.Check(t, all[0].Descriptor != nil) { + assert.Check(t, is.DeepEqual(*all[0].Descriptor, textplain.Target)) + } + assert.Check(t, is.Equal(all[0].ID, textplain.Target.Digest.String())) assert.Assert(t, is.Len(all[0].Manifests, 0)) }, diff --git a/docs/api/version-history.md b/docs/api/version-history.md index 148f513e3d..66ca77c41c 100644 --- a/docs/api/version-history.md +++ b/docs/api/version-history.md @@ -34,6 +34,12 @@ keywords: "API, Docker, rcli, REST, documentation" and will be omitted in API v1.49. * `Sysctls` in `HostConfig` (top level `--sysctl` settings) for `eth0` are no longer migrated to `DriverOpts`, as described in the changes for v1.46. +* `GET /images/json` response now includes `Descriptor` field, which contains + an OCI descriptor of the image target. + The new field will only be populated if the daemon provides a multi-platform + image store. + WARNING: This is experimental and may change at any time without any backward + compatibility. ## v1.47 API changes