From fc276495e3764f4322bfef395ad2978b6a736feb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Gronowski?= Date: Thu, 7 Mar 2024 20:56:00 +0100 Subject: [PATCH] c8d/list: Embed platform specific information MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the PlatformImages field to `ImageSummary` which describes each platform-specific manifest in that image. Signed-off-by: Paweł Gronowski --- api/server/router/image/image_routes.go | 7 ++- api/swagger.yaml | 53 +++++++++++++++++ api/types/image/platform_image.go | 43 ++++++++++++++ api/types/image/summary.go | 9 +++ daemon/containerd/image_list.go | 75 ++++++++++++++++++------- daemon/containerd/image_manifest.go | 49 ++++++++++++++++ docs/api/version-history.md | 9 +++ 7 files changed, 221 insertions(+), 24 deletions(-) create mode 100644 api/types/image/platform_image.go diff --git a/api/server/router/image/image_routes.go b/api/server/router/image/image_routes.go index b941cab69f001..9540dee818eb8 100644 --- a/api/server/router/image/image_routes.go +++ b/api/server/router/image/image_routes.go @@ -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 diff --git a/api/swagger.yaml b/api/swagger.yaml index 5677340dbd53f..a517453db34a4 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -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" @@ -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: diff --git a/api/types/image/platform_image.go b/api/types/image/platform_image.go new file mode 100644 index 0000000000000..a9f6849ac7631 --- /dev/null +++ b/api/types/image/platform_image.go @@ -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"` +} diff --git a/api/types/image/summary.go b/api/types/image/summary.go index f1e3e2ef018f8..4ebbdeadf7047 100644 --- a/api/types/image/summary.go +++ b/api/types/image/summary.go @@ -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. diff --git a/daemon/containerd/image_list.go b/daemon/containerd/image_list.go index 80b2409015a3b..baa5da5e349c2 100644 --- a/daemon/containerd/image_list.go +++ b/daemon/containerd/image_list.go @@ -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(), diff --git a/daemon/containerd/image_manifest.go b/daemon/containerd/image_manifest.go index f4fe77e444309..dbc6e1db0eb14 100644 --- a/daemon/containerd/image_manifest.go +++ b/daemon/containerd/image_manifest.go @@ -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 @@ -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 { diff --git a/docs/api/version-history.md b/docs/api/version-history.md index 6942c65e79add..52afa237743c4 100644 --- a/docs/api/version-history.md +++ b/docs/api/version-history.md @@ -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