Skip to content

Commit

Permalink
c8d/list: Embed platform specific information
Browse files Browse the repository at this point in the history
Add the PlatformImages field to `ImageSummary` which describes each
platform-specific manifest in that image.

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
  • Loading branch information
vvoland committed Apr 4, 2024
1 parent 9fa7678 commit fc27649
Show file tree
Hide file tree
Showing 7 changed files with 221 additions and 24 deletions.
7 changes: 4 additions & 3 deletions api/server/router/image/image_routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
53 changes: 53 additions & 0 deletions api/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1988,6 +1988,17 @@ 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"
items:
$ref: "#/definitions/PlatformImage"

AuthConfig:
type: "object"
Expand Down Expand Up @@ -6200,6 +6211,48 @@ definitions:
additionalProperties:
type: "string"

PlatformImage:
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:
Expand Down
43 changes: 43 additions & 0 deletions api/types/image/platform_image.go
Original file line number Diff line number Diff line change
@@ -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"`
}
9 changes: 9 additions & 0 deletions api/types/image/summary.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
75 changes: 54 additions & 21 deletions daemon/containerd/image_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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
}

Expand All @@ -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 {
Expand All @@ -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
Expand All @@ -317,14 +348,15 @@ 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
}
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)

Expand All @@ -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) {
Expand Down Expand Up @@ -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(),
Expand Down
49 changes: 49 additions & 0 deletions daemon/containerd/image_manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -93,6 +139,9 @@ func (im *ImageManifest) IsPseudoImage(ctx context.Context) (bool, error) {

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 {
Expand Down
9 changes: 9 additions & 0 deletions docs/api/version-history.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit fc27649

Please sign in to comment.