1/*
2   Copyright The containerd Authors.
3
4   Licensed under the Apache License, Version 2.0 (the "License");
5   you may not use this file except in compliance with the License.
6   You may obtain a copy of the License at
7
8       http://www.apache.org/licenses/LICENSE-2.0
9
10   Unless required by applicable law or agreed to in writing, software
11   distributed under the License is distributed on an "AS IS" BASIS,
12   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   See the License for the specific language governing permissions and
14   limitations under the License.
15*/
16
17package images
18
19import (
20	"context"
21	"encoding/json"
22	"sort"
23	"time"
24
25	"github.com/containerd/containerd/content"
26	"github.com/containerd/containerd/errdefs"
27	"github.com/containerd/containerd/log"
28	"github.com/containerd/containerd/platforms"
29	digest "github.com/opencontainers/go-digest"
30	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
31	"github.com/pkg/errors"
32)
33
34// Image provides the model for how containerd views container images.
35type Image struct {
36	// Name of the image.
37	//
38	// To be pulled, it must be a reference compatible with resolvers.
39	//
40	// This field is required.
41	Name string
42
43	// Labels provide runtime decoration for the image record.
44	//
45	// There is no default behavior for how these labels are propagated. They
46	// only decorate the static metadata object.
47	//
48	// This field is optional.
49	Labels map[string]string
50
51	// Target describes the root content for this image. Typically, this is
52	// a manifest, index or manifest list.
53	Target ocispec.Descriptor
54
55	CreatedAt, UpdatedAt time.Time
56}
57
58// DeleteOptions provide options on image delete
59type DeleteOptions struct {
60	Synchronous bool
61}
62
63// DeleteOpt allows configuring a delete operation
64type DeleteOpt func(context.Context, *DeleteOptions) error
65
66// SynchronousDelete is used to indicate that an image deletion and removal of
67// the image resources should occur synchronously before returning a result.
68func SynchronousDelete() DeleteOpt {
69	return func(ctx context.Context, o *DeleteOptions) error {
70		o.Synchronous = true
71		return nil
72	}
73}
74
75// Store and interact with images
76type Store interface {
77	Get(ctx context.Context, name string) (Image, error)
78	List(ctx context.Context, filters ...string) ([]Image, error)
79	Create(ctx context.Context, image Image) (Image, error)
80
81	// Update will replace the data in the store with the provided image. If
82	// one or more fieldpaths are provided, only those fields will be updated.
83	Update(ctx context.Context, image Image, fieldpaths ...string) (Image, error)
84
85	Delete(ctx context.Context, name string, opts ...DeleteOpt) error
86}
87
88// TODO(stevvooe): Many of these functions make strong platform assumptions,
89// which are untrue in a lot of cases. More refactoring must be done here to
90// make this work in all cases.
91
92// Config resolves the image configuration descriptor.
93//
94// The caller can then use the descriptor to resolve and process the
95// configuration of the image.
96func (image *Image) Config(ctx context.Context, provider content.Provider, platform platforms.MatchComparer) (ocispec.Descriptor, error) {
97	return Config(ctx, provider, image.Target, platform)
98}
99
100// RootFS returns the unpacked diffids that make up and images rootfs.
101//
102// These are used to verify that a set of layers unpacked to the expected
103// values.
104func (image *Image) RootFS(ctx context.Context, provider content.Provider, platform platforms.MatchComparer) ([]digest.Digest, error) {
105	desc, err := image.Config(ctx, provider, platform)
106	if err != nil {
107		return nil, err
108	}
109	return RootFS(ctx, provider, desc)
110}
111
112// Size returns the total size of an image's packed resources.
113func (image *Image) Size(ctx context.Context, provider content.Provider, platform platforms.MatchComparer) (int64, error) {
114	var size int64
115	return size, Walk(ctx, Handlers(HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
116		if desc.Size < 0 {
117			return nil, errors.Errorf("invalid size %v in %v (%v)", desc.Size, desc.Digest, desc.MediaType)
118		}
119		size += desc.Size
120		return nil, nil
121	}), LimitManifests(FilterPlatforms(ChildrenHandler(provider), platform), platform, 1)), image.Target)
122}
123
124type platformManifest struct {
125	p *ocispec.Platform
126	m *ocispec.Manifest
127}
128
129// Manifest resolves a manifest from the image for the given platform.
130//
131// When a manifest descriptor inside of a manifest index does not have
132// a platform defined, the platform from the image config is considered.
133//
134// If the descriptor points to a non-index manifest, then the manifest is
135// unmarshalled and returned without considering the platform inside of the
136// config.
137//
138// TODO(stevvooe): This violates the current platform agnostic approach to this
139// package by returning a specific manifest type. We'll need to refactor this
140// to return a manifest descriptor or decide that we want to bring the API in
141// this direction because this abstraction is not needed.`
142func Manifest(ctx context.Context, provider content.Provider, image ocispec.Descriptor, platform platforms.MatchComparer) (ocispec.Manifest, error) {
143	var (
144		limit    = 1
145		m        []platformManifest
146		wasIndex bool
147	)
148
149	if err := Walk(ctx, HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
150		switch desc.MediaType {
151		case MediaTypeDockerSchema2Manifest, ocispec.MediaTypeImageManifest:
152			p, err := content.ReadBlob(ctx, provider, desc)
153			if err != nil {
154				return nil, err
155			}
156
157			var manifest ocispec.Manifest
158			if err := json.Unmarshal(p, &manifest); err != nil {
159				return nil, err
160			}
161
162			if desc.Digest != image.Digest && platform != nil {
163				if desc.Platform != nil && !platform.Match(*desc.Platform) {
164					return nil, nil
165				}
166
167				if desc.Platform == nil {
168					p, err := content.ReadBlob(ctx, provider, manifest.Config)
169					if err != nil {
170						return nil, err
171					}
172
173					var image ocispec.Image
174					if err := json.Unmarshal(p, &image); err != nil {
175						return nil, err
176					}
177
178					if !platform.Match(platforms.Normalize(ocispec.Platform{OS: image.OS, Architecture: image.Architecture})) {
179						return nil, nil
180					}
181
182				}
183			}
184
185			m = append(m, platformManifest{
186				p: desc.Platform,
187				m: &manifest,
188			})
189
190			return nil, nil
191		case MediaTypeDockerSchema2ManifestList, ocispec.MediaTypeImageIndex:
192			p, err := content.ReadBlob(ctx, provider, desc)
193			if err != nil {
194				return nil, err
195			}
196
197			var idx ocispec.Index
198			if err := json.Unmarshal(p, &idx); err != nil {
199				return nil, err
200			}
201
202			if platform == nil {
203				return idx.Manifests, nil
204			}
205
206			var descs []ocispec.Descriptor
207			for _, d := range idx.Manifests {
208				if d.Platform == nil || platform.Match(*d.Platform) {
209					descs = append(descs, d)
210				}
211			}
212
213			sort.SliceStable(descs, func(i, j int) bool {
214				if descs[i].Platform == nil {
215					return false
216				}
217				if descs[j].Platform == nil {
218					return true
219				}
220				return platform.Less(*descs[i].Platform, *descs[j].Platform)
221			})
222
223			wasIndex = true
224
225			if len(descs) > limit {
226				return descs[:limit], nil
227			}
228			return descs, nil
229		}
230		return nil, errors.Wrapf(errdefs.ErrNotFound, "unexpected media type %v for %v", desc.MediaType, desc.Digest)
231	}), image); err != nil {
232		return ocispec.Manifest{}, err
233	}
234
235	if len(m) == 0 {
236		err := errors.Wrapf(errdefs.ErrNotFound, "manifest %v", image.Digest)
237		if wasIndex {
238			err = errors.Wrapf(errdefs.ErrNotFound, "no match for platform in manifest %v", image.Digest)
239		}
240		return ocispec.Manifest{}, err
241	}
242	return *m[0].m, nil
243}
244
245// Config resolves the image configuration descriptor using a content provided
246// to resolve child resources on the image.
247//
248// The caller can then use the descriptor to resolve and process the
249// configuration of the image.
250func Config(ctx context.Context, provider content.Provider, image ocispec.Descriptor, platform platforms.MatchComparer) (ocispec.Descriptor, error) {
251	manifest, err := Manifest(ctx, provider, image, platform)
252	if err != nil {
253		return ocispec.Descriptor{}, err
254	}
255	return manifest.Config, err
256}
257
258// Platforms returns one or more platforms supported by the image.
259func Platforms(ctx context.Context, provider content.Provider, image ocispec.Descriptor) ([]ocispec.Platform, error) {
260	var platformSpecs []ocispec.Platform
261	return platformSpecs, Walk(ctx, Handlers(HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
262		if desc.Platform != nil {
263			platformSpecs = append(platformSpecs, *desc.Platform)
264			return nil, ErrSkipDesc
265		}
266
267		switch desc.MediaType {
268		case MediaTypeDockerSchema2Config, ocispec.MediaTypeImageConfig:
269			p, err := content.ReadBlob(ctx, provider, desc)
270			if err != nil {
271				return nil, err
272			}
273
274			var image ocispec.Image
275			if err := json.Unmarshal(p, &image); err != nil {
276				return nil, err
277			}
278
279			platformSpecs = append(platformSpecs,
280				platforms.Normalize(ocispec.Platform{OS: image.OS, Architecture: image.Architecture}))
281		}
282		return nil, nil
283	}), ChildrenHandler(provider)), image)
284}
285
286// Check returns nil if the all components of an image are available in the
287// provider for the specified platform.
288//
289// If available is true, the caller can assume that required represents the
290// complete set of content required for the image.
291//
292// missing will have the components that are part of required but not avaiiable
293// in the provider.
294//
295// If there is a problem resolving content, an error will be returned.
296func Check(ctx context.Context, provider content.Provider, image ocispec.Descriptor, platform platforms.MatchComparer) (available bool, required, present, missing []ocispec.Descriptor, err error) {
297	mfst, err := Manifest(ctx, provider, image, platform)
298	if err != nil {
299		if errdefs.IsNotFound(err) {
300			return false, []ocispec.Descriptor{image}, nil, []ocispec.Descriptor{image}, nil
301		}
302
303		return false, nil, nil, nil, errors.Wrapf(err, "failed to check image %v", image.Digest)
304	}
305
306	// TODO(stevvooe): It is possible that referenced conponents could have
307	// children, but this is rare. For now, we ignore this and only verify
308	// that manifest components are present.
309	required = append([]ocispec.Descriptor{mfst.Config}, mfst.Layers...)
310
311	for _, desc := range required {
312		ra, err := provider.ReaderAt(ctx, desc)
313		if err != nil {
314			if errdefs.IsNotFound(err) {
315				missing = append(missing, desc)
316				continue
317			} else {
318				return false, nil, nil, nil, errors.Wrapf(err, "failed to check image %v", desc.Digest)
319			}
320		}
321		ra.Close()
322		present = append(present, desc)
323
324	}
325
326	return true, required, present, missing, nil
327}
328
329// Children returns the immediate children of content described by the descriptor.
330func Children(ctx context.Context, provider content.Provider, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
331	var descs []ocispec.Descriptor
332	switch desc.MediaType {
333	case MediaTypeDockerSchema2Manifest, ocispec.MediaTypeImageManifest:
334		p, err := content.ReadBlob(ctx, provider, desc)
335		if err != nil {
336			return nil, err
337		}
338
339		// TODO(stevvooe): We just assume oci manifest, for now. There may be
340		// subtle differences from the docker version.
341		var manifest ocispec.Manifest
342		if err := json.Unmarshal(p, &manifest); err != nil {
343			return nil, err
344		}
345
346		descs = append(descs, manifest.Config)
347		descs = append(descs, manifest.Layers...)
348	case MediaTypeDockerSchema2ManifestList, ocispec.MediaTypeImageIndex:
349		p, err := content.ReadBlob(ctx, provider, desc)
350		if err != nil {
351			return nil, err
352		}
353
354		var index ocispec.Index
355		if err := json.Unmarshal(p, &index); err != nil {
356			return nil, err
357		}
358
359		descs = append(descs, index.Manifests...)
360	default:
361		if IsLayerType(desc.MediaType) || IsKnownConfig(desc.MediaType) {
362			// childless data types.
363			return nil, nil
364		}
365		log.G(ctx).Warnf("encountered unknown type %v; children may not be fetched", desc.MediaType)
366	}
367
368	return descs, nil
369}
370
371// RootFS returns the unpacked diffids that make up and images rootfs.
372//
373// These are used to verify that a set of layers unpacked to the expected
374// values.
375func RootFS(ctx context.Context, provider content.Provider, configDesc ocispec.Descriptor) ([]digest.Digest, error) {
376	p, err := content.ReadBlob(ctx, provider, configDesc)
377	if err != nil {
378		return nil, err
379	}
380
381	var config ocispec.Image
382	if err := json.Unmarshal(p, &config); err != nil {
383		return nil, err
384	}
385	return config.RootFS.DiffIDs, nil
386}
387