This commit is contained in:
Paweł Gronowski 2024-04-18 13:31:19 +02:00 committed by GitHub
commit d31bf07ab8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 255 additions and 29 deletions

View file

@ -407,9 +407,10 @@ func (ir *imageRouter) getImagesJSON(ctx context.Context, w http.ResponseWriter,
}
images, err := ir.backend.Images(ctx, imagetypes.ListOptions{
All: httputils.BoolValue(r, "all"),
Filters: imageFilters,
SharedSize: sharedSize,
All: httputils.BoolValue(r, "all"),
Filters: imageFilters,
SharedSize: sharedSize,
ContainerCount: true,
})
if err != nil {
return err

View file

@ -1988,6 +1988,19 @@ definitions:
x-nullable: false
type: "integer"
example: 2
PlatformImages:
description: |
Platform-specific images available for this image.
Only present with the containerd integration enabled.
WARNING: This is experimental and may change at any time without any backward
compatibility.
type: "array"
x-nullable: false
x-omitempty: true
items:
$ref: "#/definitions/PlatformImage"
AuthConfig:
type: "object"
@ -6200,6 +6213,50 @@ definitions:
additionalProperties:
type: "string"
PlatformImage:
x-nullable: false
required: [Id, Descriptor, Available, Platform, ContentSize, UnpackedSize, Containers]
description: |
PlatformImage represents a platform-specific image that is part of a
multi-platform image.
type: "object"
properties:
Id:
description: |
Content-addressable ID of an image derived from the platform-specific
image manifest.
type: "string"
example: "sha256:95869fbcf224d947ace8d61d0e931d49e31bb7fc67fffbbe9c3198c33aa8e93f"
Descriptor:
$ref: "#/definitions/OCIDescriptor"
Available:
description: Indicates whether the image is locally available.
type: "boolean"
example: true
Platform:
$ref: "#/definitions/OCIPlatform"
ContentSize:
description: |
The size of the available distributable (possibly compressed) image content
in bytes.
type: "integer"
format: "int64"
example: 3987495
UnpackedSize:
description: |
The size of the unpacked and uncompressed image content (needed for
the image to be useable by containers) in bytes.
type: "integer"
format: "int64"
example: 3987495
Containers:
description: |
The number of containers that are using this specific platform image.
type: "integer"
format: "int64"
example: 2
paths:
/containers/json:
get:

View file

@ -0,0 +1,43 @@
package image
import (
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
type PlatformImage struct {
// ID is the content-addressable ID of an image and is the same as the
// digest of the platform-specific image manifest.
//
// Required: true
ID string `json:"Id"`
// Descriptor is the OCI descriptor of the image.
//
// Required: true
Descriptor ocispec.Descriptor `json:"Descriptor"`
// Available indicates whether the image is locally available.
//
// Required: true
Available bool `json:"Available"`
// Platform is the platform of the image
//
// Required: true
Platform ocispec.Platform `json:"Platform"`
// ContentSize is the size of all the locally available distributable content size.
//
// Required: true
ContentSize int64 `json:"ContentSize"`
// UnpackedSize is the size of the image when unpacked.
//
// Required: true
UnpackedSize int64 `json:"UnpackedSize"`
// Containers is the number of containers created from this image.
//
// Required: true
Containers int64 `json:"Containers"`
}

View file

@ -47,6 +47,15 @@ type Summary struct {
// Required: true
ParentID string `json:"ParentId"`
// Platform-specific images available for this image.
//
// Only present with the containerd integration enabled.
//
// WARNING: This is experimental and may change at any time without any backward
// compatibility.
//
PlatformImages []PlatformImage `json:"PlatformImages,omitempty"`
// List of content-addressable digests of locally available image manifests
// that the image is referenced from. Multiple manifests can refer to the
// same image.

View file

@ -210,6 +210,7 @@ func (i *ImageService) Images(ctx context.Context, opts imagetypes.ListOptions)
func (i *ImageService) imageSummary(ctx context.Context, img images.Image, platformMatcher platforms.MatchComparer,
opts imagetypes.ListOptions, tagsByDigest map[digest.Digest][]string,
) (_ *imagetypes.Summary, allChainIDs []digest.Digest, _ error) {
var platformImages []imagetypes.PlatformImage
// Total size of the image including all its platform
var totalSize int64
@ -224,9 +225,20 @@ func (i *ImageService) imageSummary(ctx context.Context, img images.Image, platf
var best *ImageManifest
var bestPlatform ocispec.Platform
err := i.walkImageManifests(ctx, img, func(img *ImageManifest) error {
err := i.walkReachableImageManifests(ctx, img, func(img *ImageManifest) error {
target := img.Target()
if isPseudo, err := img.IsPseudoImage(ctx); isPseudo || err != nil {
return nil
log.G(ctx).WithFields(log.Fields{
"error": err,
"image": img.Name(),
"digest": target.Digest,
"isPseudo": isPseudo,
}).Debug("skipping pseudo image")
if !errdefs.IsNotFound(err) {
return nil
}
}
available, err := img.CheckContentAvailable(ctx)
@ -239,7 +251,19 @@ func (i *ImageService) imageSummary(ctx context.Context, img images.Image, platf
return nil
}
platformSummary := imagetypes.PlatformImage{
ID: target.Digest.String(),
Available: available,
Descriptor: target,
Containers: -1,
}
if target.Platform != nil {
platformSummary.Platform = *target.Platform
}
if !available {
platformImages = append(platformImages, platformSummary)
return nil
}
@ -253,7 +277,9 @@ func (i *ImageService) imageSummary(ctx context.Context, img images.Image, platf
return err
}
target := img.Target()
if target.Platform == nil {
platformSummary.Platform = dockerImage.Platform
}
diffIDs, err := img.RootFS(ctx)
if err != nil {
@ -262,40 +288,45 @@ func (i *ImageService) imageSummary(ctx context.Context, img images.Image, platf
chainIDs := identity.ChainIDs(diffIDs)
ts, _, err := i.singlePlatformSize(ctx, img)
unpackedSize, contentSize, err := i.singlePlatformSize(ctx, img)
if err != nil {
return err
}
totalSize += ts
totalSize += unpackedSize + contentSize
allChainsIDs = append(allChainsIDs, chainIDs...)
platformSummary.ContentSize = contentSize
platformSummary.UnpackedSize = unpackedSize
if opts.ContainerCount {
i.containers.ApplyAll(func(c *container.Container) {
if c.ImageManifest != nil && c.ImageManifest.Digest == target.Digest {
containersCount++
}
})
}
var platform ocispec.Platform
if target.Platform != nil {
platform = *target.Platform
} else {
platform = dockerImage.Platform
platformSummary.Containers = containersCount
}
// Filter out platforms that don't match the requested platform. Do it
// after the size, container count and chainIDs are summed up to have
// the single combined entry still represent the whole multi-platform
// image.
if !platformMatcher.Match(platform) {
if !platformMatcher.Match(platformSummary.Platform) {
return nil
}
if best == nil || platformMatcher.Less(platform, bestPlatform) {
// If the platform is available, prepend it to the list of platforms
// otherwise append it at the end.
if platformSummary.Available {
platformImages = append([]imagetypes.PlatformImage{platformSummary}, platformImages...)
} else {
platformImages = append(platformImages, platformSummary)
}
if best == nil || platformMatcher.Less(platformSummary.Platform, bestPlatform) {
best = img
bestPlatform = platform
bestPlatform = platformSummary.Platform
}
return nil
@ -317,6 +348,7 @@ func (i *ImageService) imageSummary(ctx context.Context, img images.Image, platf
return nil, nil, err
}
image.Size = totalSize
image.PlatformImages = platformImages
if opts.ContainerCount {
image.Containers = containersCount
@ -324,7 +356,7 @@ func (i *ImageService) imageSummary(ctx context.Context, img images.Image, platf
return image, allChainsIDs, nil
}
func (i *ImageService) singlePlatformSize(ctx context.Context, imgMfst *ImageManifest) (totalSize int64, contentSize int64, _ error) {
func (i *ImageService) singlePlatformSize(ctx context.Context, imgMfst *ImageManifest) (unpackedSize int64, contentSize int64, _ error) {
// TODO(thaJeztah): do we need to take multiple snapshotters into account? See https://github.com/moby/moby/issues/45273
snapshotter := i.snapshotterService(i.snapshotter)
@ -350,10 +382,7 @@ func (i *ImageService) singlePlatformSize(ctx context.Context, imgMfst *ImageMan
return -1, -1, err
}
// totalSize is the size of the image's packed layers and snapshots
// (unpacked layers) combined.
totalSize = contentSize + unpackedUsage.Size
return totalSize, contentSize, nil
return unpackedUsage.Size, contentSize, nil
}
func (i *ImageService) singlePlatformImage(ctx context.Context, contentStore content.Store, repoTags []string, imageManifest *ImageManifest) (*imagetypes.Summary, error) {
@ -395,11 +424,15 @@ func (i *ImageService) singlePlatformImage(ctx context.Context, contentStore con
return nil, err
}
totalSize, _, err := i.singlePlatformSize(ctx, imageManifest)
unpackedSize, contentSize, err := i.singlePlatformSize(ctx, imageManifest)
if err != nil {
return nil, errors.Wrapf(err, "failed to calculate size of image %s", imageManifest.Name())
}
// totalSize is the size of the image's packed layers and snapshots
// (unpacked layers) combined.
totalSize := contentSize + unpackedSize
summary := &imagetypes.Summary{
ParentID: rawImg.Labels[imageLabelClassicBuilderParent],
ID: target.String(),

View file

@ -6,6 +6,7 @@ import (
"github.com/containerd/containerd"
"github.com/containerd/containerd/content"
cerrdefs "github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/images"
containerdimages "github.com/containerd/containerd/images"
"github.com/containerd/containerd/platforms"
@ -48,6 +49,51 @@ func (i *ImageService) walkImageManifests(ctx context.Context, img containerdima
return errNotManifestOrIndex
}
// walkReachableImageManifests calls the handler for each manifest in the
// multiplatform image that can be reached from the given image.
// The image might not be present locally, but its descriptor is known.
// The image implements the containerd.Image interface, but all operations act
// on the specific manifest instead of the index.
func (i *ImageService) walkReachableImageManifests(ctx context.Context, img containerdimages.Image, handler func(img *ImageManifest) error) error {
desc := img.Target
handleManifest := func(ctx context.Context, d ocispec.Descriptor) error {
platformImg, err := i.NewImageManifest(ctx, img, d)
if err != nil {
if err == errNotManifest {
return nil
}
return err
}
return handler(platformImg)
}
if containerdimages.IsManifestType(desc.MediaType) {
return handleManifest(ctx, desc)
}
if containerdimages.IsIndexType(desc.MediaType) {
return containerdimages.Walk(ctx, containerdimages.HandlerFunc(
func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
err := handleManifest(ctx, desc)
if err != nil {
return nil, err
}
c, err := containerdimages.Children(ctx, i.content, desc)
if err != nil {
if cerrdefs.IsNotFound(err) {
return nil, nil
}
return nil, err
}
return c, nil
}), desc)
}
return errNotManifestOrIndex
}
type ImageManifest struct {
containerd.Image
@ -78,21 +124,31 @@ func (im *ImageManifest) Metadata() containerdimages.Image {
return md
}
func (im *ImageManifest) IsAttestation() bool {
// Quick check for buildkit attestation manifests
// https://github.com/moby/buildkit/blob/v0.11.4/docs/attestations/attestation-storage.md
// This would have also been caught by the layer check below, but it requires
// an additional content read and deserialization of Manifest.
if _, has := im.Target().Annotations[attestation.DockerAnnotationReferenceType]; has {
return true
}
return false
}
// IsPseudoImage returns false if the manifest has no layers or any of its layers is a known image layer.
// Some manifests use the image media type for compatibility, even if they are not a real image.
func (im *ImageManifest) IsPseudoImage(ctx context.Context) (bool, error) {
desc := im.Target()
// Quick check for buildkit attestation manifests
// https://github.com/moby/buildkit/blob/v0.11.4/docs/attestations/attestation-storage.md
// This would have also been caught by the layer check below, but it requires
// an additional content read and deserialization of Manifest.
if _, has := desc.Annotations[attestation.DockerAnnotationReferenceType]; has {
if im.IsAttestation() {
return true, nil
}
mfst, err := im.Manifest(ctx)
if err != nil {
if cerrdefs.IsNotFound(err) {
return false, errdefs.NotFound(errors.Wrapf(err, "failed to read manifest %v", desc.Digest))
}
return true, err
}
if len(mfst.Layers) == 0 {
@ -149,3 +205,21 @@ func readManifest(ctx context.Context, store content.Provider, desc ocispec.Desc
return mfst, nil
}
// ImagePlatform returns the platform of the image manifest.
// If the manifest list doesn't have a platform filled, it will be read from the config.
func (m *ImageManifest) ImagePlatform(ctx context.Context) (ocispec.Platform, error) {
target := m.Target()
if target.Platform != nil {
return *target.Platform, nil
}
configDesc, err := m.Config(ctx)
if err != nil {
return ocispec.Platform{}, err
}
var out ocispec.Platform
err = readConfig(ctx, m.ContentStore(), configDesc, &out)
return out, err
}

View file

@ -13,6 +13,15 @@ keywords: "API, Docker, rcli, REST, documentation"
will be rejected.
-->
## v1.46 API changes
[Docker Engine API v1.46](https://docs.docker.com/engine/api/v1.46/) documentation
* `GET /images/json` response now includes `PlatformImages` field, which contains
information about the platform-specific manifests available for the image.
WARNING: This is experimental and may change at any time without any backward
compatibility.
## v1.45 API changes
[Docker Engine API v1.45](https://docs.docker.com/engine/api/v1.45/) documentation