Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions api/server/router/image/image_routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
211 changes: 208 additions & 3 deletions daemon/containerd/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
5 changes: 3 additions & 2 deletions daemon/images/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Comment on lines 26 to 28
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a follow-up; we need to check if we really need this error-type, as it already implements errdefs.NotFound, perhaps we don't need it (with the possible exception of the "special" No such ... prefix, which may be needed for older CLIs).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this one might not be needed, the only difference is that this error type formats the message with "No such image: %s", ref whereas the errdefs.NotFound doesn't have that extra data


func (e ErrImageDoesNotExist) Error() string {
ref := e.ref
ref := e.Ref
if named, ok := ref.(reference.Named); ok {
ref = reference.TagNameOnly(named)
}
Expand Down Expand Up @@ -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(),
Expand Down
9 changes: 9 additions & 0 deletions image/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
26 changes: 26 additions & 0 deletions pkg/platforms/platforms.go
Original file line number Diff line number Diff line change
@@ -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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is to live in a pkg package, we should make sure it's robust for any preferred matcher, including ones whose Less implementation may not necessarily sort platforms which Match less than platforms it would not.

Suggested change
return c.preferred.Less(p1, p2)
m1, m2 := c.preferred.Match(p1), c.preferred.Match(p2)
if m1 && m2 {
return c.preferred.Less(p1, p2)
}
return m1 // Not totally-ordered

}