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