Files
moby/daemon/containerd/image_inspect.go
Sebastiaan van Stijn 15bef6ff1a daemon/server: move GetImageOpts, ImageInspectOpts to imagebackend
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-10-05 13:41:22 +02:00

199 lines
5.5 KiB
Go

package containerd
import (
"context"
"sync/atomic"
"time"
c8dimages "github.com/containerd/containerd/v2/core/images"
cerrdefs "github.com/containerd/errdefs"
"github.com/containerd/log"
"github.com/containerd/platforms"
"github.com/distribution/reference"
imagespec "github.com/moby/docker-image-spec/specs-go/v1"
imagetypes "github.com/moby/moby/api/types/image"
"github.com/moby/moby/v2/daemon/server/imagebackend"
"github.com/moby/moby/v2/internal/sliceutil"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"golang.org/x/sync/semaphore"
)
func (i *ImageService) ImageInspect(ctx context.Context, refOrID string, opts imagebackend.ImageInspectOpts) (*imagetypes.InspectResponse, error) {
requestedPlatform := opts.Platform
c8dImg, err := i.resolveImage(ctx, refOrID)
if err != nil {
return nil, err
}
target := c8dImg.Target
tagged, err := i.images.List(ctx, "target.digest=="+target.Digest.String())
if err != nil {
return nil, err
}
// This could happen only if the image was deleted after the resolveImage call above.
if len(tagged) == 0 {
return nil, errInconsistentData
}
lastUpdated := time.Unix(0, 0)
for _, i := range tagged {
if i.UpdatedAt.After(lastUpdated) {
lastUpdated = i.UpdatedAt
}
}
platform := i.matchRequestedOrDefault(platforms.OnlyStrict, requestedPlatform)
size, err := i.size(ctx, target, platform)
if err != nil {
return nil, err
}
multi, err := i.multiPlatformSummary(ctx, c8dImg, platform)
if err != nil {
return nil, err
}
if multi.Best == nil && requestedPlatform != nil {
return nil, &errPlatformNotFound{
imageRef: refOrID,
wanted: *requestedPlatform,
}
}
var img imagespec.DockerOCIImage
if multi.Best != nil {
if err := multi.Best.ReadConfig(ctx, &img); err != nil {
return nil, err
}
}
parent, err := i.getImageLabelByDigest(ctx, target.Digest, imageLabelClassicBuilderParent)
if err != nil {
log.G(ctx).WithError(err).Warn("failed to determine Parent property")
}
var manifests []imagetypes.ManifestSummary
if opts.Manifests {
manifests = multi.Manifests
}
repoTags, repoDigests := collectRepoTagsAndDigests(ctx, tagged)
if requestedPlatform != nil {
target = multi.Best.Target()
}
resp := &imagetypes.InspectResponse{
ID: target.Digest.String(),
RepoTags: repoTags,
Descriptor: &target,
RepoDigests: repoDigests,
Parent: parent,
DockerVersion: "",
Size: size,
Manifests: manifests,
Metadata: imagetypes.Metadata{
LastTagTime: lastUpdated,
},
}
if multi.Best != nil {
imgConfig := img.Config
resp.Author = img.Author
resp.Config = &imgConfig
resp.Architecture = img.Architecture
resp.Variant = img.Variant
resp.Os = img.OS
resp.OsVersion = img.OSVersion
if len(img.History) > 0 {
resp.Comment = img.History[len(img.History)-1].Comment
}
if img.Created != nil {
resp.Created = img.Created.Format(time.RFC3339Nano)
}
resp.RootFS = imagetypes.RootFS{
Type: img.RootFS.Type,
}
for _, layer := range img.RootFS.DiffIDs {
resp.RootFS.Layers = append(resp.RootFS.Layers, layer.String())
}
}
return resp, nil
}
func collectRepoTagsAndDigests(ctx context.Context, tagged []c8dimages.Image) (repoTags []string, repoDigests []string) {
repoTags = make([]string, 0, len(tagged))
repoDigests = make([]string, 0, len(tagged))
for _, img := range tagged {
if isDanglingImage(img) {
if len(tagged) > 1 {
// This is unexpected - dangling image should be deleted
// as soon as another image with the same target is created.
// Log a warning, but don't error out the whole operation.
log.G(ctx).WithField("refs", tagged).Warn("multiple images have the same target, but one of them is still dangling")
}
continue
}
name, err := reference.ParseNamed(img.Name)
if err != nil {
log.G(ctx).WithField("name", name).WithError(err).Error("failed to parse image name as reference")
// Include the malformed name in RepoTags to be consistent with `docker image ls`.
repoTags = append(repoTags, img.Name)
continue
}
repoTags = append(repoTags, reference.FamiliarString(name))
if _, ok := name.(reference.Digested); ok {
repoDigests = append(repoDigests, reference.FamiliarString(name))
// Image name is a digested reference already, so no need to create a digested reference.
continue
}
digested, err := reference.WithDigest(reference.TrimNamed(name), img.Target.Digest)
if err != nil {
// This could only happen if digest is invalid, but considering that
// we get it from the Descriptor it's highly unlikely.
// Log error just in case.
log.G(ctx).WithError(err).Error("failed to create digested reference")
continue
}
repoDigests = append(repoDigests, reference.FamiliarString(digested))
}
return sliceutil.Dedup(repoTags), sliceutil.Dedup(repoDigests)
}
// size returns the total size of the image's packed resources.
func (i *ImageService) size(ctx context.Context, desc ocispec.Descriptor, platform platforms.MatchComparer) (int64, error) {
var size atomic.Int64
cs := i.content
handler := c8dimages.LimitManifests(c8dimages.ChildrenHandler(cs), platform, 1)
var wh c8dimages.HandlerFunc = func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
children, err := handler(ctx, desc)
if err != nil {
if !cerrdefs.IsNotFound(err) {
return nil, err
}
}
size.Add(desc.Size)
return children, nil
}
l := semaphore.NewWeighted(3)
if err := c8dimages.Dispatch(ctx, wh, l, desc); err != nil {
return 0, err
}
return size.Load(), nil
}