github.com/containerd/containerd@v22.0.0-20200918172823-438c87b8e050+incompatible/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  	"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.
    35  type 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
    59  type DeleteOptions struct {
    60  	Synchronous bool
    61  }
    62  
    63  // DeleteOpt allows configuring a delete operation
    64  type 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.
    68  func 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
    76  type 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.
    96  func (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.
   104  func (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.
   113  func (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  
   124  type 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.`
   142  func 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.
   250  func 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.
   259  func 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.
   296  func 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.
   330  func 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).Debugf("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.
   375  func 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  }