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 containerd
18
19import (
20	"context"
21	"fmt"
22	"strings"
23	"sync/atomic"
24
25	"github.com/containerd/containerd/content"
26	"github.com/containerd/containerd/diff"
27	"github.com/containerd/containerd/errdefs"
28	"github.com/containerd/containerd/images"
29	"github.com/containerd/containerd/platforms"
30	"github.com/containerd/containerd/rootfs"
31	"github.com/containerd/containerd/snapshots"
32	"github.com/opencontainers/go-digest"
33	"github.com/opencontainers/image-spec/identity"
34	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
35	"github.com/pkg/errors"
36	"golang.org/x/sync/semaphore"
37)
38
39// Image describes an image used by containers
40type Image interface {
41	// Name of the image
42	Name() string
43	// Target descriptor for the image content
44	Target() ocispec.Descriptor
45	// Labels of the image
46	Labels() map[string]string
47	// Unpack unpacks the image's content into a snapshot
48	Unpack(context.Context, string, ...UnpackOpt) error
49	// RootFS returns the unpacked diffids that make up images rootfs.
50	RootFS(ctx context.Context) ([]digest.Digest, error)
51	// Size returns the total size of the image's packed resources.
52	Size(ctx context.Context) (int64, error)
53	// Usage returns a usage calculation for the image.
54	Usage(context.Context, ...UsageOpt) (int64, error)
55	// Config descriptor for the image.
56	Config(ctx context.Context) (ocispec.Descriptor, error)
57	// IsUnpacked returns whether or not an image is unpacked.
58	IsUnpacked(context.Context, string) (bool, error)
59	// ContentStore provides a content store which contains image blob data
60	ContentStore() content.Store
61}
62
63type usageOptions struct {
64	manifestLimit *int
65	manifestOnly  bool
66	snapshots     bool
67}
68
69// UsageOpt is used to configure the usage calculation
70type UsageOpt func(*usageOptions) error
71
72// WithUsageManifestLimit sets the limit to the number of manifests which will
73// be walked for usage. Setting this value to 0 will require all manifests to
74// be walked, returning ErrNotFound if manifests are missing.
75// NOTE: By default all manifests which exist will be walked
76// and any non-existent manifests and their subobjects will be ignored.
77func WithUsageManifestLimit(i int) UsageOpt {
78	// If 0 then don't filter any manifests
79	// By default limits to current platform
80	return func(o *usageOptions) error {
81		o.manifestLimit = &i
82		return nil
83	}
84}
85
86// WithSnapshotUsage will check for referenced snapshots from the image objects
87// and include the snapshot size in the total usage.
88func WithSnapshotUsage() UsageOpt {
89	return func(o *usageOptions) error {
90		o.snapshots = true
91		return nil
92	}
93}
94
95// WithManifestUsage is used to get the usage for an image based on what is
96// reported by the manifests rather than what exists in the content store.
97// NOTE: This function is best used with the manifest limit set to get a
98// consistent value, otherwise non-existent manifests will be excluded.
99func WithManifestUsage() UsageOpt {
100	return func(o *usageOptions) error {
101		o.manifestOnly = true
102		return nil
103	}
104}
105
106var _ = (Image)(&image{})
107
108// NewImage returns a client image object from the metadata image
109func NewImage(client *Client, i images.Image) Image {
110	return &image{
111		client:   client,
112		i:        i,
113		platform: client.platform,
114	}
115}
116
117// NewImageWithPlatform returns a client image object from the metadata image
118func NewImageWithPlatform(client *Client, i images.Image, platform platforms.MatchComparer) Image {
119	return &image{
120		client:   client,
121		i:        i,
122		platform: platform,
123	}
124}
125
126type image struct {
127	client *Client
128
129	i        images.Image
130	platform platforms.MatchComparer
131}
132
133func (i *image) Name() string {
134	return i.i.Name
135}
136
137func (i *image) Target() ocispec.Descriptor {
138	return i.i.Target
139}
140
141func (i *image) Labels() map[string]string {
142	return i.i.Labels
143}
144
145func (i *image) RootFS(ctx context.Context) ([]digest.Digest, error) {
146	provider := i.client.ContentStore()
147	return i.i.RootFS(ctx, provider, i.platform)
148}
149
150func (i *image) Size(ctx context.Context) (int64, error) {
151	return i.Usage(ctx, WithUsageManifestLimit(1), WithManifestUsage())
152}
153
154func (i *image) Usage(ctx context.Context, opts ...UsageOpt) (int64, error) {
155	var config usageOptions
156	for _, opt := range opts {
157		if err := opt(&config); err != nil {
158			return 0, err
159		}
160	}
161
162	var (
163		provider  = i.client.ContentStore()
164		handler   = images.ChildrenHandler(provider)
165		size      int64
166		mustExist bool
167	)
168
169	if config.manifestLimit != nil {
170		handler = images.LimitManifests(handler, i.platform, *config.manifestLimit)
171		mustExist = true
172	}
173
174	var wh images.HandlerFunc = func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
175		var usage int64
176		children, err := handler(ctx, desc)
177		if err != nil {
178			if !errdefs.IsNotFound(err) || mustExist {
179				return nil, err
180			}
181			if !config.manifestOnly {
182				// Do not count size of non-existent objects
183				desc.Size = 0
184			}
185		} else if config.snapshots || !config.manifestOnly {
186			info, err := provider.Info(ctx, desc.Digest)
187			if err != nil {
188				if !errdefs.IsNotFound(err) {
189					return nil, err
190				}
191				if !config.manifestOnly {
192					// Do not count size of non-existent objects
193					desc.Size = 0
194				}
195			} else if info.Size > desc.Size {
196				// Count actual usage, Size may be unset or -1
197				desc.Size = info.Size
198			}
199
200			for k, v := range info.Labels {
201				const prefix = "containerd.io/gc.ref.snapshot."
202				if !strings.HasPrefix(k, prefix) {
203					continue
204				}
205
206				sn := i.client.SnapshotService(k[len(prefix):])
207				if sn == nil {
208					continue
209				}
210
211				u, err := sn.Usage(ctx, v)
212				if err != nil {
213					if !errdefs.IsNotFound(err) && !errdefs.IsInvalidArgument(err) {
214						return nil, err
215					}
216				} else {
217					usage += u.Size
218				}
219			}
220		}
221
222		// Ignore unknown sizes. Generally unknown sizes should
223		// never be set in manifests, however, the usage
224		// calculation does not need to enforce this.
225		if desc.Size >= 0 {
226			usage += desc.Size
227		}
228
229		atomic.AddInt64(&size, usage)
230
231		return children, nil
232	}
233
234	l := semaphore.NewWeighted(3)
235	if err := images.Dispatch(ctx, wh, l, i.i.Target); err != nil {
236		return 0, err
237	}
238
239	return size, nil
240}
241
242func (i *image) Config(ctx context.Context) (ocispec.Descriptor, error) {
243	provider := i.client.ContentStore()
244	return i.i.Config(ctx, provider, i.platform)
245}
246
247func (i *image) IsUnpacked(ctx context.Context, snapshotterName string) (bool, error) {
248	sn, err := i.client.getSnapshotter(ctx, snapshotterName)
249	if err != nil {
250		return false, err
251	}
252	cs := i.client.ContentStore()
253
254	diffs, err := i.i.RootFS(ctx, cs, i.platform)
255	if err != nil {
256		return false, err
257	}
258
259	chainID := identity.ChainID(diffs)
260	_, err = sn.Stat(ctx, chainID.String())
261	if err == nil {
262		return true, nil
263	} else if !errdefs.IsNotFound(err) {
264		return false, err
265	}
266
267	return false, nil
268}
269
270// UnpackConfig provides configuration for the unpack of an image
271type UnpackConfig struct {
272	// ApplyOpts for applying a diff to a snapshotter
273	ApplyOpts []diff.ApplyOpt
274	// SnapshotOpts for configuring a snapshotter
275	SnapshotOpts []snapshots.Opt
276}
277
278// UnpackOpt provides configuration for unpack
279type UnpackOpt func(context.Context, *UnpackConfig) error
280
281func (i *image) Unpack(ctx context.Context, snapshotterName string, opts ...UnpackOpt) error {
282	ctx, done, err := i.client.WithLease(ctx)
283	if err != nil {
284		return err
285	}
286	defer done(ctx)
287
288	var config UnpackConfig
289	for _, o := range opts {
290		if err := o(ctx, &config); err != nil {
291			return err
292		}
293	}
294
295	layers, err := i.getLayers(ctx, i.platform)
296	if err != nil {
297		return err
298	}
299
300	var (
301		a  = i.client.DiffService()
302		cs = i.client.ContentStore()
303
304		chain    []digest.Digest
305		unpacked bool
306	)
307	snapshotterName, err = i.client.resolveSnapshotterName(ctx, snapshotterName)
308	if err != nil {
309		return err
310	}
311	sn, err := i.client.getSnapshotter(ctx, snapshotterName)
312	if err != nil {
313		return err
314	}
315	for _, layer := range layers {
316		unpacked, err = rootfs.ApplyLayerWithOpts(ctx, layer, chain, sn, a, config.SnapshotOpts, config.ApplyOpts)
317		if err != nil {
318			return err
319		}
320
321		if unpacked {
322			// Set the uncompressed label after the uncompressed
323			// digest has been verified through apply.
324			cinfo := content.Info{
325				Digest: layer.Blob.Digest,
326				Labels: map[string]string{
327					"containerd.io/uncompressed": layer.Diff.Digest.String(),
328				},
329			}
330			if _, err := cs.Update(ctx, cinfo, "labels.containerd.io/uncompressed"); err != nil {
331				return err
332			}
333		}
334
335		chain = append(chain, layer.Diff.Digest)
336	}
337
338	desc, err := i.i.Config(ctx, cs, i.platform)
339	if err != nil {
340		return err
341	}
342
343	rootfs := identity.ChainID(chain).String()
344
345	cinfo := content.Info{
346		Digest: desc.Digest,
347		Labels: map[string]string{
348			fmt.Sprintf("containerd.io/gc.ref.snapshot.%s", snapshotterName): rootfs,
349		},
350	}
351
352	_, err = cs.Update(ctx, cinfo, fmt.Sprintf("labels.containerd.io/gc.ref.snapshot.%s", snapshotterName))
353	return err
354}
355
356func (i *image) getLayers(ctx context.Context, platform platforms.MatchComparer) ([]rootfs.Layer, error) {
357	cs := i.client.ContentStore()
358
359	manifest, err := images.Manifest(ctx, cs, i.i.Target, platform)
360	if err != nil {
361		return nil, err
362	}
363
364	diffIDs, err := i.i.RootFS(ctx, cs, platform)
365	if err != nil {
366		return nil, errors.Wrap(err, "failed to resolve rootfs")
367	}
368	if len(diffIDs) != len(manifest.Layers) {
369		return nil, errors.Errorf("mismatched image rootfs and manifest layers")
370	}
371	layers := make([]rootfs.Layer, len(diffIDs))
372	for i := range diffIDs {
373		layers[i].Diff = ocispec.Descriptor{
374			// TODO: derive media type from compressed type
375			MediaType: ocispec.MediaTypeImageLayer,
376			Digest:    diffIDs[i],
377		}
378		layers[i].Blob = manifest.Layers[i]
379	}
380	return layers, nil
381}
382
383func (i *image) ContentStore() content.Store {
384	return i.client.ContentStore()
385}
386