Skip to content

Commit 1616a09

Browse files
ndeloofrumpl
authored andcommitted
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 <[email protected]> Signed-off-by: Nicolas De Loof <[email protected]> Signed-off-by: Sebastiaan van Stijn <[email protected]>
1 parent 3e39ec6 commit 1616a09

5 files changed

Lines changed: 247 additions & 7 deletions

File tree

api/server/router/image/image_routes.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,9 +206,8 @@ func (ir *imageRouter) getImagesByName(ctx context.Context, w http.ResponseWrite
206206
}
207207

208208
func (ir *imageRouter) toImageInspect(img *image.Image) (*types.ImageInspect, error) {
209-
refs := ir.referenceBackend.References(img.ID().Digest())
210209
var repoTags, repoDigests []string
211-
for _, ref := range refs {
210+
for _, ref := range img.Details.References {
212211
switch ref.(type) {
213212
case reference.NamedTagged:
214213
repoTags = append(repoTags, reference.FamiliarString(ref))

daemon/containerd/image.go

Lines changed: 208 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,219 @@ package containerd
22

33
import (
44
"context"
5-
"errors"
5+
"encoding/json"
6+
"fmt"
7+
"regexp"
8+
"strconv"
9+
"sync/atomic"
10+
"time"
611

12+
"github.com/containerd/containerd/content"
13+
cerrdefs "github.com/containerd/containerd/errdefs"
14+
containerdimages "github.com/containerd/containerd/images"
15+
cplatforms "github.com/containerd/containerd/platforms"
16+
"github.com/docker/distribution/reference"
17+
containertypes "github.com/docker/docker/api/types/container"
718
imagetype "github.com/docker/docker/api/types/image"
19+
"github.com/docker/docker/daemon/images"
820
"github.com/docker/docker/errdefs"
921
"github.com/docker/docker/image"
22+
"github.com/docker/docker/layer"
23+
"github.com/docker/docker/pkg/platforms"
24+
"github.com/docker/go-connections/nat"
25+
"github.com/opencontainers/go-digest"
26+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
27+
"github.com/pkg/errors"
28+
"golang.org/x/sync/semaphore"
1029
)
1130

31+
var truncatedID = regexp.MustCompile(`^([a-f0-9]{4,64})$`)
32+
1233
// GetImage returns an image corresponding to the image referred to by refOrID.
13-
func (i *ImageService) GetImage(ctx context.Context, refOrID string, options imagetype.GetImageOpts) (retImg *image.Image, retErr error) {
14-
return nil, errdefs.NotImplemented(errors.New("not implemented"))
34+
func (i *ImageService) GetImage(ctx context.Context, refOrID string, options imagetype.GetImageOpts) (*image.Image, error) {
35+
desc, err := i.resolveDescriptor(ctx, refOrID)
36+
if err != nil {
37+
return nil, err
38+
}
39+
40+
platform := platforms.AllPlatformsWithPreference(cplatforms.Default())
41+
if options.Platform != nil {
42+
platform = cplatforms.OnlyStrict(*options.Platform)
43+
}
44+
45+
cs := i.client.ContentStore()
46+
conf, err := containerdimages.Config(ctx, cs, desc, platform)
47+
if err != nil {
48+
return nil, err
49+
}
50+
51+
imageConfigBytes, err := content.ReadBlob(ctx, cs, conf)
52+
if err != nil {
53+
return nil, err
54+
}
55+
56+
var ociimage ocispec.Image
57+
if err := json.Unmarshal(imageConfigBytes, &ociimage); err != nil {
58+
return nil, err
59+
}
60+
61+
rootfs := image.NewRootFS()
62+
for _, id := range ociimage.RootFS.DiffIDs {
63+
rootfs.Append(layer.DiffID(id))
64+
}
65+
exposedPorts := make(nat.PortSet, len(ociimage.Config.ExposedPorts))
66+
for k, v := range ociimage.Config.ExposedPorts {
67+
exposedPorts[nat.Port(k)] = v
68+
}
69+
70+
img := image.NewImage(image.ID(desc.Digest))
71+
img.V1Image = image.V1Image{
72+
ID: string(desc.Digest),
73+
OS: ociimage.OS,
74+
Architecture: ociimage.Architecture,
75+
Config: &containertypes.Config{
76+
Entrypoint: ociimage.Config.Entrypoint,
77+
Env: ociimage.Config.Env,
78+
Cmd: ociimage.Config.Cmd,
79+
User: ociimage.Config.User,
80+
WorkingDir: ociimage.Config.WorkingDir,
81+
ExposedPorts: exposedPorts,
82+
Volumes: ociimage.Config.Volumes,
83+
Labels: ociimage.Config.Labels,
84+
StopSignal: ociimage.Config.StopSignal,
85+
},
86+
}
87+
88+
img.RootFS = rootfs
89+
90+
if options.Details {
91+
lastUpdated := time.Unix(0, 0)
92+
size, err := i.size(ctx, desc, platform)
93+
if err != nil {
94+
return nil, err
95+
}
96+
97+
tagged, err := i.client.ImageService().List(ctx, "target.digest=="+desc.Digest.String())
98+
if err != nil {
99+
return nil, err
100+
}
101+
tags := make([]reference.Named, 0, len(tagged))
102+
for _, i := range tagged {
103+
if i.UpdatedAt.After(lastUpdated) {
104+
lastUpdated = i.UpdatedAt
105+
}
106+
name, err := reference.ParseNamed(i.Name)
107+
if err != nil {
108+
return nil, err
109+
}
110+
tags = append(tags, name)
111+
}
112+
113+
img.Details = &image.Details{
114+
References: tags,
115+
Size: size,
116+
Metadata: nil,
117+
Driver: i.snapshotter,
118+
LastUpdated: lastUpdated,
119+
}
120+
}
121+
122+
return img, nil
123+
}
124+
125+
// size returns the total size of the image's packed resources.
126+
func (i *ImageService) size(ctx context.Context, desc ocispec.Descriptor, platform cplatforms.MatchComparer) (int64, error) {
127+
var size int64
128+
129+
cs := i.client.ContentStore()
130+
handler := containerdimages.LimitManifests(containerdimages.ChildrenHandler(cs), platform, 1)
131+
132+
var wh containerdimages.HandlerFunc = func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
133+
children, err := handler(ctx, desc)
134+
if err != nil {
135+
if !cerrdefs.IsNotFound(err) {
136+
return nil, err
137+
}
138+
}
139+
140+
atomic.AddInt64(&size, desc.Size)
141+
142+
return children, nil
143+
}
144+
145+
l := semaphore.NewWeighted(3)
146+
if err := containerdimages.Dispatch(ctx, wh, l, desc); err != nil {
147+
return 0, err
148+
}
149+
150+
return size, nil
151+
}
152+
153+
// resolveDescriptor searches for a descriptor based on the given
154+
// reference or identifier. Returns the descriptor of
155+
// the image, which could be a manifest list, manifest, or config.
156+
func (i *ImageService) resolveDescriptor(ctx context.Context, refOrID string) (ocispec.Descriptor, error) {
157+
parsed, err := reference.ParseAnyReference(refOrID)
158+
if err != nil {
159+
return ocispec.Descriptor{}, errdefs.InvalidParameter(err)
160+
}
161+
162+
is := i.client.ImageService()
163+
164+
digested, ok := parsed.(reference.Digested)
165+
if ok {
166+
imgs, err := is.List(ctx, "target.digest=="+digested.Digest().String())
167+
if err != nil {
168+
return ocispec.Descriptor{}, errors.Wrap(err, "failed to lookup digest")
169+
}
170+
if len(imgs) == 0 {
171+
return ocispec.Descriptor{}, images.ErrImageDoesNotExist{Ref: parsed}
172+
}
173+
174+
return imgs[0].Target, nil
175+
}
176+
177+
ref := reference.TagNameOnly(parsed.(reference.Named)).String()
178+
179+
// If the identifier could be a short ID, attempt to match
180+
if truncatedID.MatchString(refOrID) {
181+
filters := []string{
182+
fmt.Sprintf("name==%q", ref), // Or it could just look like one.
183+
"target.digest~=" + strconv.Quote(fmt.Sprintf(`sha256:^%s[0-9a-fA-F]{%d}$`, regexp.QuoteMeta(refOrID), 64-len(refOrID))),
184+
}
185+
imgs, err := is.List(ctx, filters...)
186+
if err != nil {
187+
return ocispec.Descriptor{}, err
188+
}
189+
190+
if len(imgs) == 0 {
191+
return ocispec.Descriptor{}, images.ErrImageDoesNotExist{Ref: parsed}
192+
}
193+
if len(imgs) > 1 {
194+
digests := map[digest.Digest]struct{}{}
195+
for _, img := range imgs {
196+
if img.Name == ref {
197+
return img.Target, nil
198+
}
199+
digests[img.Target.Digest] = struct{}{}
200+
}
201+
202+
if len(digests) > 1 {
203+
return ocispec.Descriptor{}, errdefs.NotFound(errors.New("ambiguous reference"))
204+
}
205+
}
206+
207+
return imgs[0].Target, nil
208+
}
209+
210+
img, err := is.Get(ctx, ref)
211+
if err != nil {
212+
// TODO(containerd): error translation can use common function
213+
if !cerrdefs.IsNotFound(err) {
214+
return ocispec.Descriptor{}, err
215+
}
216+
return ocispec.Descriptor{}, images.ErrImageDoesNotExist{Ref: parsed}
217+
}
218+
219+
return img.Target, nil
15220
}

daemon/images/image.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ import (
2424

2525
// ErrImageDoesNotExist is error returned when no image can be found for a reference.
2626
type ErrImageDoesNotExist struct {
27-
ref reference.Reference
27+
Ref reference.Reference
2828
}
2929

3030
func (e ErrImageDoesNotExist) Error() string {
31-
ref := e.ref
31+
ref := e.Ref
3232
if named, ok := ref.(reference.Named); ok {
3333
ref = reference.TagNameOnly(named)
3434
}
@@ -176,6 +176,7 @@ func (i *ImageService) GetImage(ctx context.Context, refOrID string, options ima
176176
return nil, err
177177
}
178178
img.Details = &image.Details{
179+
References: i.referenceStore.References(img.ID().Digest()),
179180
Size: size,
180181
Metadata: layerMetadata,
181182
Driver: i.layerStore.DriverName(),

image/image.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"strings"
1010
"time"
1111

12+
"github.com/docker/distribution/reference"
1213
"github.com/docker/docker/api/types/container"
1314
"github.com/docker/docker/dockerversion"
1415
"github.com/docker/docker/layer"
@@ -119,6 +120,7 @@ type Image struct {
119120

120121
// Details provides additional image data
121122
type Details struct {
123+
References []reference.Named
122124
Size int64
123125
Metadata map[string]string
124126
Driver string
@@ -199,6 +201,13 @@ type ChildConfig struct {
199201
Config *container.Config
200202
}
201203

204+
// NewImage creates a new image with the given ID
205+
func NewImage(id ID) *Image {
206+
return &Image{
207+
computedID: id,
208+
}
209+
}
210+
202211
// NewChildImage creates a new Image as a child of this image.
203212
func NewChildImage(img *Image, child ChildConfig, os string) *Image {
204213
isEmptyLayer := layer.IsEmpty(child.DiffID)

pkg/platforms/platforms.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package platforms
2+
3+
import (
4+
cplatforms "github.com/containerd/containerd/platforms"
5+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
6+
)
7+
8+
type allPlatformsWithPreferenceMatcher struct {
9+
preferred cplatforms.MatchComparer
10+
}
11+
12+
// AllPlatformsWithPreference will return a platform matcher that matches all
13+
// platforms but will order platforms matching the preferred matcher first.
14+
func AllPlatformsWithPreference(preferred cplatforms.MatchComparer) cplatforms.MatchComparer {
15+
return allPlatformsWithPreferenceMatcher{
16+
preferred: preferred,
17+
}
18+
}
19+
20+
func (c allPlatformsWithPreferenceMatcher) Match(_ ocispec.Platform) bool {
21+
return true
22+
}
23+
24+
func (c allPlatformsWithPreferenceMatcher) Less(p1, p2 ocispec.Platform) bool {
25+
return c.preferred.Less(p1, p2)
26+
}

0 commit comments

Comments
 (0)