From 1616a09b613b07e56926ae23b1ae710f9a2c64fe Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Wed, 6 Jul 2022 14:24:38 +0200 Subject: [PATCH] add support for image inspect with containerd-integration This is a squashed version of various PRs (or related code-changes) to implement image inspect with the containerd-integration; - add support for image inspect - introduce GetImageOpts to manage image inspect data in backend - GetImage to return image tags with details - list images matching digest to discover all tags - Add ExposedPorts and Volumes to the image returned - Refactor resolving/getting images - Return the image ID on inspect - consider digest and ignore tag when both are set - docker run --platform Signed-off-by: Djordje Lukic Signed-off-by: Nicolas De Loof Signed-off-by: Sebastiaan van Stijn --- api/server/router/image/image_routes.go | 3 +- daemon/containerd/image.go | 211 +++++++++++++++++++++++- daemon/images/image.go | 5 +- image/image.go | 9 + pkg/platforms/platforms.go | 26 +++ 5 files changed, 247 insertions(+), 7 deletions(-) create mode 100644 pkg/platforms/platforms.go diff --git a/api/server/router/image/image_routes.go b/api/server/router/image/image_routes.go index 16c9eccc7d..ab64b1511a 100644 --- a/api/server/router/image/image_routes.go +++ b/api/server/router/image/image_routes.go @@ -206,9 +206,8 @@ func (ir *imageRouter) getImagesByName(ctx context.Context, w http.ResponseWrite } func (ir *imageRouter) toImageInspect(img *image.Image) (*types.ImageInspect, error) { - refs := ir.referenceBackend.References(img.ID().Digest()) var repoTags, repoDigests []string - for _, ref := range refs { + for _, ref := range img.Details.References { switch ref.(type) { case reference.NamedTagged: repoTags = append(repoTags, reference.FamiliarString(ref)) diff --git a/daemon/containerd/image.go b/daemon/containerd/image.go index 0ba182661e..bf5a8232bb 100644 --- a/daemon/containerd/image.go +++ b/daemon/containerd/image.go @@ -2,14 +2,219 @@ package containerd import ( "context" - "errors" + "encoding/json" + "fmt" + "regexp" + "strconv" + "sync/atomic" + "time" + "github.com/containerd/containerd/content" + cerrdefs "github.com/containerd/containerd/errdefs" + containerdimages "github.com/containerd/containerd/images" + cplatforms "github.com/containerd/containerd/platforms" + "github.com/docker/distribution/reference" + containertypes "github.com/docker/docker/api/types/container" imagetype "github.com/docker/docker/api/types/image" + "github.com/docker/docker/daemon/images" "github.com/docker/docker/errdefs" "github.com/docker/docker/image" + "github.com/docker/docker/layer" + "github.com/docker/docker/pkg/platforms" + "github.com/docker/go-connections/nat" + "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/pkg/errors" + "golang.org/x/sync/semaphore" ) +var truncatedID = regexp.MustCompile(`^([a-f0-9]{4,64})$`) + // GetImage returns an image corresponding to the image referred to by refOrID. -func (i *ImageService) GetImage(ctx context.Context, refOrID string, options imagetype.GetImageOpts) (retImg *image.Image, retErr error) { - return nil, errdefs.NotImplemented(errors.New("not implemented")) +func (i *ImageService) GetImage(ctx context.Context, refOrID string, options imagetype.GetImageOpts) (*image.Image, error) { + desc, err := i.resolveDescriptor(ctx, refOrID) + if err != nil { + return nil, err + } + + platform := platforms.AllPlatformsWithPreference(cplatforms.Default()) + if options.Platform != nil { + platform = cplatforms.OnlyStrict(*options.Platform) + } + + cs := i.client.ContentStore() + conf, err := containerdimages.Config(ctx, cs, desc, platform) + if err != nil { + return nil, err + } + + imageConfigBytes, err := content.ReadBlob(ctx, cs, conf) + if err != nil { + return nil, err + } + + var ociimage ocispec.Image + if err := json.Unmarshal(imageConfigBytes, &ociimage); err != nil { + return nil, err + } + + rootfs := image.NewRootFS() + for _, id := range ociimage.RootFS.DiffIDs { + rootfs.Append(layer.DiffID(id)) + } + exposedPorts := make(nat.PortSet, len(ociimage.Config.ExposedPorts)) + for k, v := range ociimage.Config.ExposedPorts { + exposedPorts[nat.Port(k)] = v + } + + img := image.NewImage(image.ID(desc.Digest)) + img.V1Image = image.V1Image{ + ID: string(desc.Digest), + OS: ociimage.OS, + Architecture: ociimage.Architecture, + Config: &containertypes.Config{ + Entrypoint: ociimage.Config.Entrypoint, + Env: ociimage.Config.Env, + Cmd: ociimage.Config.Cmd, + User: ociimage.Config.User, + WorkingDir: ociimage.Config.WorkingDir, + ExposedPorts: exposedPorts, + Volumes: ociimage.Config.Volumes, + Labels: ociimage.Config.Labels, + StopSignal: ociimage.Config.StopSignal, + }, + } + + img.RootFS = rootfs + + if options.Details { + lastUpdated := time.Unix(0, 0) + size, err := i.size(ctx, desc, platform) + if err != nil { + return nil, err + } + + tagged, err := i.client.ImageService().List(ctx, "target.digest=="+desc.Digest.String()) + if err != nil { + return nil, err + } + tags := make([]reference.Named, 0, len(tagged)) + for _, i := range tagged { + if i.UpdatedAt.After(lastUpdated) { + lastUpdated = i.UpdatedAt + } + name, err := reference.ParseNamed(i.Name) + if err != nil { + return nil, err + } + tags = append(tags, name) + } + + img.Details = &image.Details{ + References: tags, + Size: size, + Metadata: nil, + Driver: i.snapshotter, + LastUpdated: lastUpdated, + } + } + + return img, nil +} + +// size returns the total size of the image's packed resources. +func (i *ImageService) size(ctx context.Context, desc ocispec.Descriptor, platform cplatforms.MatchComparer) (int64, error) { + var size int64 + + cs := i.client.ContentStore() + 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 + } + } + + atomic.AddInt64(&size, desc.Size) + + return children, nil + } + + l := semaphore.NewWeighted(3) + if err := containerdimages.Dispatch(ctx, wh, l, desc); err != nil { + return 0, err + } + + return size, 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. +func (i *ImageService) resolveDescriptor(ctx context.Context, refOrID string) (ocispec.Descriptor, error) { + parsed, err := reference.ParseAnyReference(refOrID) + if err != nil { + return ocispec.Descriptor{}, errdefs.InvalidParameter(err) + } + + is := i.client.ImageService() + + digested, ok := parsed.(reference.Digested) + if ok { + imgs, err := is.List(ctx, "target.digest=="+digested.Digest().String()) + if err != nil { + return ocispec.Descriptor{}, errors.Wrap(err, "failed to lookup digest") + } + if len(imgs) == 0 { + return ocispec.Descriptor{}, images.ErrImageDoesNotExist{Ref: parsed} + } + + return imgs[0].Target, nil + } + + ref := reference.TagNameOnly(parsed.(reference.Named)).String() + + // If the identifier could be a short ID, attempt to match + if truncatedID.MatchString(refOrID) { + filters := []string{ + fmt.Sprintf("name==%q", ref), // Or it could just look like one. + "target.digest~=" + strconv.Quote(fmt.Sprintf(`sha256:^%s[0-9a-fA-F]{%d}$`, regexp.QuoteMeta(refOrID), 64-len(refOrID))), + } + imgs, err := is.List(ctx, filters...) + if err != nil { + return ocispec.Descriptor{}, err + } + + if len(imgs) == 0 { + return ocispec.Descriptor{}, images.ErrImageDoesNotExist{Ref: parsed} + } + if len(imgs) > 1 { + digests := map[digest.Digest]struct{}{} + for _, img := range imgs { + if img.Name == ref { + return img.Target, nil + } + digests[img.Target.Digest] = struct{}{} + } + + if len(digests) > 1 { + return ocispec.Descriptor{}, errdefs.NotFound(errors.New("ambiguous reference")) + } + } + + return imgs[0].Target, nil + } + + img, err := is.Get(ctx, ref) + if err != nil { + // TODO(containerd): error translation can use common function + if !cerrdefs.IsNotFound(err) { + return ocispec.Descriptor{}, err + } + return ocispec.Descriptor{}, images.ErrImageDoesNotExist{Ref: parsed} + } + + return img.Target, nil } diff --git a/daemon/images/image.go b/daemon/images/image.go index a7b56e7941..0d42f8675e 100644 --- a/daemon/images/image.go +++ b/daemon/images/image.go @@ -24,11 +24,11 @@ import ( // ErrImageDoesNotExist is error returned when no image can be found for a reference. type ErrImageDoesNotExist struct { - ref reference.Reference + Ref reference.Reference } func (e ErrImageDoesNotExist) Error() string { - ref := e.ref + ref := e.Ref if named, ok := ref.(reference.Named); ok { ref = reference.TagNameOnly(named) } @@ -176,6 +176,7 @@ func (i *ImageService) GetImage(ctx context.Context, refOrID string, options ima return nil, err } img.Details = &image.Details{ + References: i.referenceStore.References(img.ID().Digest()), Size: size, Metadata: layerMetadata, Driver: i.layerStore.DriverName(), diff --git a/image/image.go b/image/image.go index 25179a1c8a..af9563eeee 100644 --- a/image/image.go +++ b/image/image.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "github.com/docker/distribution/reference" "github.com/docker/docker/api/types/container" "github.com/docker/docker/dockerversion" "github.com/docker/docker/layer" @@ -119,6 +120,7 @@ type Image struct { // Details provides additional image data type Details struct { + References []reference.Named Size int64 Metadata map[string]string Driver string @@ -199,6 +201,13 @@ type ChildConfig struct { Config *container.Config } +// NewImage creates a new image with the given ID +func NewImage(id ID) *Image { + return &Image{ + computedID: id, + } +} + // NewChildImage creates a new Image as a child of this image. func NewChildImage(img *Image, child ChildConfig, os string) *Image { isEmptyLayer := layer.IsEmpty(child.DiffID) diff --git a/pkg/platforms/platforms.go b/pkg/platforms/platforms.go new file mode 100644 index 0000000000..e232b4ddb2 --- /dev/null +++ b/pkg/platforms/platforms.go @@ -0,0 +1,26 @@ +package platforms + +import ( + cplatforms "github.com/containerd/containerd/platforms" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" +) + +type allPlatformsWithPreferenceMatcher struct { + preferred cplatforms.MatchComparer +} + +// AllPlatformsWithPreference will return a platform matcher that matches all +// platforms but will order platforms matching the preferred matcher first. +func AllPlatformsWithPreference(preferred cplatforms.MatchComparer) cplatforms.MatchComparer { + return allPlatformsWithPreferenceMatcher{ + preferred: preferred, + } +} + +func (c allPlatformsWithPreferenceMatcher) Match(_ ocispec.Platform) bool { + return true +} + +func (c allPlatformsWithPreferenceMatcher) Less(p1, p2 ocispec.Platform) bool { + return c.preferred.Less(p1, p2) +}