add support for image inspect with containerd-integration
- depends on https://github.com/moby/moby/pull/43939
- closes https://github.com/moby/moby/pull/44621

Upstreaming: 

- https://github.com/rumpl/moby/pull/8
- https://github.com/rumpl/moby/pull/122
- https://github.com/rumpl/moby/pull/27
- https://github.com/rumpl/moby/pull/55
- https://github.com/rumpl/moby/pull/68
- https://github.com/rumpl/moby/pull/88
- https://github.com/rumpl/moby/pull/114
- https://github.com/rumpl/moby/pull/90
- https://github.com/rumpl/moby/pull/93
- https://github.com/rumpl/moby/pull/98

These patches were selected by looking at the history for https://github.com/rumpl/moby/commits/dd-4.15.0/daemon/containerd/image.go, and only the relevant parts for this feature were included.

Quite a few of those are touching-up previous commits, so we should consider squashing (some of) them, but keeping them separate for now to somewhat help review.

```
p 85d6399836 add support for image inspect
p fa430d6686 Implement run using the containerd snapshotter
p d37b797d34 introduce GetImageOpts to manage image inspect data in backend
p 4543499f86 Introduce support for docker commit
p f1213b08bd Fix linting issues
p 2840f63ee6 GetImage to return image tags with details
p 75df7b7a05 list images matching digest to discover all tags
p d9b76c6682 c8d/prune: Handle filters, don't delete used
p a930139556 Add ExposedPorts and Volumes to the image returned
p 8df0a88265 Refactor resolving/getting images
p d58184802b Return the image ID on inspect
p 61ea993343 consider digest and ignore tag when both are set
p eaa810ab98 docker run --platform
p ab6918735d c8d/builder: store untagged images
p 960cff2c71 c8d/commit: Save as dangling name
p f7962b37ef Remove the call to resolveImage
p 9ac5d768c7 Handle multi-platform images when listing them
```

Also included https://github.com/rumpl/moby/pull/122

```
p a5a01b5cf7 daemon/containerd: fix compat for "docker run" with image that's not present
```

----

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



**- A picture of a cute animal (not mandatory but encouraged)**


daemon/containerd/image.go
package containerd

import (
	"context"
	"errors"

	imagetype "github.com/docker/docker/api/types/image"
	"github.com/docker/docker/errdefs"
	"github.com/docker/docker/image"
)

// 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"))
}

package containerd

import (
	"context"
	"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) (*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
}

Consider the API currently shows whatever image is currently there regardless of platform.

We could error out with like `ambiguous image reference: multiple platforms are available, you must specify a platform: available platforms <platform1>,<platform2>`. It would be nice, though, if the API's json error could list those platforms so clients can parse it nicely (without having to parse the error string) or pick the first one... I'm inclined to error in case of ambiguity here, though.

Alternatively... we could update the API to return a list of objects
The docker CLI at least already outputs a json array.
There wouldn't be much too change to support the updated API.

... or change it to return the image index (with the appropriate media type on the response headers).
This does mean larger changes would be needed on the client.