images: Extract ImageInspect from GetImage

Remove a special `Details` parameter from the `GetImage` options and
extract its behavior to a `ImageInspect` method as it was only used by
the `/images/{name}/json` endpoint (`docker image inspect`).

This makes it easier for the containerd image service to output an image
inspect output without having to use the same data structures as the
graphdrivers.

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
This commit is contained in:
Paweł Gronowski committed 2024-07-25 20:13:41 +02:00
1 parent 2b1097f080
commit cd11843df8
8 files changed
+278 -208

No files matched your search

+1
View File
@@ -26,6 +26,7 @@ type imageBackend interface {
ImageHistory(ctx context.Context, imageName string) ([]*image.HistoryResponseItem, error)
Images(ctx context.Context, opts image.ListOptions) ([]*image.Summary, error)
GetImage(ctx context.Context, refOrID string, options backend.GetImageOpts) (*dockerimage.Image, error)
ImageInspect(ctx context.Context, refOrID string, options backend.ImageInspectOpts) (*image.InspectResponse, error)
TagImage(ctx context.Context, id dockerimage.ID, newRef reference.Named) error
ImagesPrune(ctx context.Context, pruneFilters filters.Args) (*image.PruneReport, error)
}
+8 -73
View File
@@ -18,7 +18,6 @@ import (
"github.com/docker/docker/api/types/filters"
imagetypes "github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/registry"
"github.com/docker/docker/api/types/storage"
"github.com/docker/docker/api/types/versions"
"github.com/docker/docker/builder/remotecontext"
"github.com/docker/docker/dockerversion"
@@ -303,14 +302,18 @@ func (ir *imageRouter) deleteImages(ctx context.Context, w http.ResponseWriter,
}
func (ir *imageRouter) getImagesByName(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
img, err := ir.backend.GetImage(ctx, vars["name"], backend.GetImageOpts{Details: true})
imageInspect, err := ir.backend.ImageInspect(ctx, vars["name"], backend.ImageInspectOpts{})
if err != nil {
return err
}
imageInspect, err := ir.toImageInspect(img)
if err != nil {
return err
// Make sure we output empty arrays instead of nil. While Go nil slice is functionally equivalent to an empty slice,
// it matters for the JSON representation.
if imageInspect.RepoTags == nil {
imageInspect.RepoTags = []string{}
}
if imageInspect.RepoDigests == nil {
imageInspect.RepoDigests = []string{}
}
version := httputils.VersionFromContext(ctx)
@@ -330,74 +333,6 @@ func (ir *imageRouter) getImagesByName(ctx context.Context, w http.ResponseWrite
return httputils.WriteJSON(w, http.StatusOK, imageInspect)
}
func (ir *imageRouter) toImageInspect(img *image.Image) (*imagetypes.InspectResponse, error) {
var repoTags, repoDigests []string
for _, ref := range img.Details.References {
switch ref.(type) {
case reference.NamedTagged:
repoTags = append(repoTags, reference.FamiliarString(ref))
case reference.Canonical:
repoDigests = append(repoDigests, reference.FamiliarString(ref))
}
}
comment := img.Comment
if len(comment) == 0 && len(img.History) > 0 {
comment = img.History[len(img.History)-1].Comment
}
// Make sure we output empty arrays instead of nil.
if repoTags == nil {
repoTags = []string{}
}
if repoDigests == nil {
repoDigests = []string{}
}
var created string
if img.Created != nil {
created = img.Created.Format(time.RFC3339Nano)
}
return &imagetypes.InspectResponse{
ID: img.ID().String(),
RepoTags: repoTags,
RepoDigests: repoDigests,
Parent: img.Parent.String(),
Comment: comment,
Created: created,
Container: img.Container, //nolint:staticcheck // ignore SA1019: field is deprecated, but still set on API < v1.45.
ContainerConfig: &img.ContainerConfig, //nolint:staticcheck // ignore SA1019: field is deprecated, but still set on API < v1.45.
DockerVersion: img.DockerVersion,
Author: img.Author,
Config: img.Config,
Architecture: img.Architecture,
Variant: img.Variant,
Os: img.OperatingSystem(),
OsVersion: img.OSVersion,
Size: img.Details.Size,
GraphDriver: storage.DriverData{
Name: img.Details.Driver,
Data: img.Details.Metadata,
},
RootFS: rootFSToAPIType(img.RootFS),
Metadata: imagetypes.Metadata{
LastTagTime: img.Details.LastUpdated,
},
}, nil
}
func rootFSToAPIType(rootfs *image.RootFS) imagetypes.RootFS {
var layers []string
for _, l := range rootfs.DiffIDs {
layers = append(layers, l.String())
}
return imagetypes.RootFS{
Type: rootfs.Type,
Layers: layers,
}
}
func (ir *imageRouter) getImagesJSON(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
if err := httputils.ParseForm(r); err != nil {
return err
+3 -1
View File
@@ -141,9 +141,11 @@ type CreateImageConfig struct {
// from the backend.
type GetImageOpts struct {
Platform *ocispec.Platform
Details bool
}
// ImageInspectOpts holds parameters to inspect an image.
type ImageInspectOpts struct{}
// CommitConfig is the configuration for creating an image as part of a build.
type CommitConfig struct {
Author string
+1 -94
View File
@@ -7,8 +7,6 @@ import (
"sort"
"strconv"
"strings"
"sync/atomic"
"time"
containerdimages "github.com/containerd/containerd/images"
cerrdefs "github.com/containerd/errdefs"
@@ -23,7 +21,6 @@ import (
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"golang.org/x/sync/semaphore"
)
var errInconsistentData error = errors.New("consistency error: data changed during operation, retry")
@@ -44,8 +41,8 @@ func (i *ImageService) GetImage(ctx context.Context, refOrID string, options bac
if err != nil {
return nil, err
}
ociImage := presentImages[0]
ociImage := presentImages[0]
img := dockerOciImageToDockerImagePartial(image.ID(desc.Target.Digest), ociImage)
parent, err := i.getImageLabelByDigest(ctx, desc.Target.Digest, imageLabelClassicBuilderParent)
@@ -55,68 +52,6 @@ func (i *ImageService) GetImage(ctx context.Context, refOrID string, options bac
img.Parent = image.ID(parent)
}
if options.Details {
lastUpdated := time.Unix(0, 0)
size, err := i.size(ctx, desc.Target, platform)
if err != nil {
return nil, err
}
tagged, err := i.images.List(ctx, "target.digest=="+desc.Target.Digest.String())
if err != nil {
return nil, err
}
// Usually each image will result in 2 references (named and digested).
refs := make([]reference.Named, 0, len(tagged)*2)
for _, i := range tagged {
if i.UpdatedAt.After(lastUpdated) {
lastUpdated = i.UpdatedAt
}
if isDanglingImage(i) {
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(i.Name)
if err != nil {
// This is inconsistent with `docker image ls` which will
// still include the malformed name in RepoTags.
log.G(ctx).WithField("name", name).WithError(err).Error("failed to parse image name as reference")
continue
}
refs = append(refs, name)
if _, ok := name.(reference.Digested); ok {
// Image name already contains a digest, so no need to create a digested reference.
continue
}
digested, err := reference.WithDigest(reference.TrimNamed(name), desc.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
}
refs = append(refs, digested)
}
img.Details = &image.Details{
References: refs,
Size: size,
Metadata: nil,
Driver: i.snapshotter,
LastUpdated: lastUpdated,
}
}
return img, nil
}
@@ -231,34 +166,6 @@ func (i *ImageService) GetImageManifest(ctx context.Context, refOrID string, opt
return nil, errdefs.NotFound(errors.New("failed to find manifest"))
}
// 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 := containerdimages.LimitManifests(containerdimages.ChildrenHandler(cs), platform, 1)
var wh containerdimages.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 := containerdimages.Dispatch(ctx, wh, l, desc); err != nil {
return 0, err
}
return size.Load(), nil
}
// resolveDescriptor searches for a descriptor based on the given
// reference or identifier. Returns the descriptor of
// the image, which could be a manifest list, manifest, or config.
+158
View File
@@ -0,0 +1,158 @@
package containerd
import (
"context"
"sync/atomic"
"time"
containerdimages "github.com/containerd/containerd/images"
cerrdefs "github.com/containerd/errdefs"
"github.com/containerd/log"
"github.com/containerd/platforms"
"github.com/distribution/reference"
"github.com/docker/docker/api/types/backend"
imagetypes "github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/storage"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"golang.org/x/sync/semaphore"
)
func (i *ImageService) ImageInspect(ctx context.Context, refOrID string, _ backend.ImageInspectOpts) (*imagetypes.InspectResponse, error) {
img, err := i.GetImage(ctx, refOrID, backend.GetImageOpts{})
if err != nil {
return nil, err
}
lastUpdated := time.Unix(0, 0)
tagged, err := i.images.List(ctx, "target.digest=="+img.ImageID())
if err != nil {
return nil, err
}
// This could happen only if the image was deleted after the GetImage call above.
if len(tagged) == 0 {
return nil, errInconsistentData
}
platform := matchAllWithPreference(platforms.Default())
size, err := i.size(ctx, tagged[0].Target, platform)
if err != nil {
return nil, err
}
imgDgst := tagged[0].Target.Digest
repoTags := make([]string, 0, len(tagged))
repoDigests := make([]string, 0, len(tagged))
for _, i := range tagged {
if i.UpdatedAt.After(lastUpdated) {
lastUpdated = i.UpdatedAt
}
if isDanglingImage(i) {
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(i.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, i.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), imgDgst)
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))
}
var comment string
if len(comment) == 0 && len(img.History) > 0 {
comment = img.History[len(img.History)-1].Comment
}
var created string
if img.Created != nil {
created = img.Created.Format(time.RFC3339Nano)
}
var layers []string
for _, layer := range img.RootFS.DiffIDs {
layers = append(layers, layer.String())
}
return &imagetypes.InspectResponse{
ID: img.ImageID(),
RepoTags: repoTags,
RepoDigests: repoDigests,
Parent: img.Parent.String(),
Comment: comment,
Created: created,
DockerVersion: "",
Author: img.Author,
Config: img.Config,
Architecture: img.Architecture,
Variant: img.Variant,
Os: img.OS,
OsVersion: img.OSVersion,
Size: size,
GraphDriver: storage.DriverData{
Name: i.snapshotter,
Data: nil,
},
RootFS: imagetypes.RootFS{
Type: img.RootFS.Type,
Layers: layers,
},
Metadata: imagetypes.Metadata{
LastTagTime: lastUpdated,
},
}, nil
}
// 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 := containerdimages.LimitManifests(containerdimages.ChildrenHandler(cs), platform, 1)
var wh containerdimages.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 := containerdimages.Dispatch(ctx, wh, l, desc); err != nil {
return 0, err
}
return size.Load(), nil
}
+1
View File
@@ -43,6 +43,7 @@ type ImageService interface {
ImageHistory(ctx context.Context, name string) ([]*imagetype.HistoryResponseItem, error)
CommitImage(ctx context.Context, c backend.CommitConfig) (image.ID, error)
SquashImage(id, parent string) (string, error)
ImageInspect(ctx context.Context, refOrID string, opts backend.ImageInspectOpts) (*imagetype.InspectResponse, error)
// Containerd related methods
+2 -40
View File
@@ -16,7 +16,6 @@ import (
"github.com/docker/docker/api/types/backend"
"github.com/docker/docker/errdefs"
"github.com/docker/docker/image"
"github.com/docker/docker/layer"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
@@ -163,49 +162,12 @@ func (i *ImageService) manifestMatchesPlatform(ctx context.Context, img *image.I
return false, nil
}
// GetImage returns an image corresponding to the image referred to by refOrID.
func (i *ImageService) GetImage(ctx context.Context, refOrID string, options backend.GetImageOpts) (*image.Image, error) {
img, err := i.getImage(ctx, refOrID, options)
if err != nil {
return nil, err
}
if options.Details {
var size int64
var layerMetadata map[string]string
layerID := img.RootFS.ChainID()
if layerID != "" {
l, err := i.layerStore.Get(layerID)
if err != nil {
return nil, err
}
defer layer.ReleaseAndLog(i.layerStore, l)
size = l.Size()
layerMetadata, err = l.Metadata()
if err != nil {
return nil, err
}
}
lastUpdated, err := i.imageStore.GetLastUpdated(img.ID())
if err != nil {
return nil, err
}
img.Details = &image.Details{
References: i.referenceStore.References(img.ID().Digest()),
Size: size,
Metadata: layerMetadata,
Driver: i.layerStore.DriverName(),
LastUpdated: lastUpdated,
}
}
return img, nil
}
func (i *ImageService) GetImageManifest(ctx context.Context, refOrID string, options backend.GetImageOpts) (*ocispec.Descriptor, error) {
panic("not implemented")
}
func (i *ImageService) getImage(ctx context.Context, refOrID string, options backend.GetImageOpts) (retImg *image.Image, retErr error) {
// GetImage returns an image corresponding to the image referred to by refOrID.
func (i *ImageService) GetImage(ctx context.Context, refOrID string, options backend.GetImageOpts) (retImg *image.Image, retErr error) {
defer func() {
if retErr != nil || retImg == nil || options.Platform == nil {
return
+104
View File
@@ -0,0 +1,104 @@
package images
import (
"context"
"time"
"github.com/distribution/reference"
"github.com/docker/docker/api/types/backend"
imagetypes "github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/storage"
"github.com/docker/docker/image"
"github.com/docker/docker/layer"
)
func (i *ImageService) ImageInspect(ctx context.Context, refOrID string, _ backend.ImageInspectOpts) (*imagetypes.InspectResponse, error) {
img, err := i.GetImage(ctx, refOrID, backend.GetImageOpts{})
if err != nil {
return nil, err
}
size, layerMetadata, err := i.getLayerSizeAndMetadata(img)
if err != nil {
return nil, err
}
lastUpdated, err := i.imageStore.GetLastUpdated(img.ID())
if err != nil {
return nil, err
}
var repoTags, repoDigests []string
for _, ref := range i.referenceStore.References(img.ID().Digest()) {
switch ref.(type) {
case reference.NamedTagged:
repoTags = append(repoTags, reference.FamiliarString(ref))
case reference.Canonical:
repoDigests = append(repoDigests, reference.FamiliarString(ref))
}
}
comment := img.Comment
if len(comment) == 0 && len(img.History) > 0 {
comment = img.History[len(img.History)-1].Comment
}
var created string
if img.Created != nil {
created = img.Created.Format(time.RFC3339Nano)
}
var layers []string
for _, l := range img.RootFS.DiffIDs {
layers = append(layers, l.String())
}
return &imagetypes.InspectResponse{
ID: img.ID().String(),
RepoTags: repoTags,
RepoDigests: repoDigests,
Parent: img.Parent.String(),
Comment: comment,
Created: created,
Container: img.Container, //nolint:staticcheck // ignore SA1019: field is deprecated, but still set on API < v1.45.
ContainerConfig: &img.ContainerConfig, //nolint:staticcheck // ignore SA1019: field is deprecated, but still set on API < v1.45.
DockerVersion: img.DockerVersion,
Author: img.Author,
Config: img.Config,
Architecture: img.Architecture,
Variant: img.Variant,
Os: img.OperatingSystem(),
OsVersion: img.OSVersion,
Size: size,
GraphDriver: storage.DriverData{
Name: i.layerStore.DriverName(),
Data: layerMetadata,
},
RootFS: imagetypes.RootFS{
Type: img.RootFS.Type,
Layers: layers,
},
Metadata: imagetypes.Metadata{
LastTagTime: lastUpdated,
},
}, nil
}
func (i *ImageService) getLayerSizeAndMetadata(img *image.Image) (int64, map[string]string, error) {
var size int64
var layerMetadata map[string]string
layerID := img.RootFS.ChainID()
if layerID != "" {
l, err := i.layerStore.Get(layerID)
if err != nil {
return 0, nil, err
}
defer layer.ReleaseAndLog(i.layerStore, l)
size = l.Size()
layerMetadata, err = l.Metadata()
if err != nil {
return 0, nil, err
}
}
return size, layerMetadata, nil
}