github.com/containerd/Containerd@v1.4.13/image.go (about)

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