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 <djordje.lukic@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
parent
3e39ec60da
commit
1616a09b61
5 changed files with 247 additions and 7 deletions
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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)
|
||||
|
|
26
pkg/platforms/platforms.go
Normal file
26
pkg/platforms/platforms.go
Normal file
|
@ -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)
|
||||
}
|
Loading…
Reference in a new issue