
This change allows implementations to resolve the location of the actual data using OCI descriptor fields such as MediaType. No OCI descriptor field is written to the store. No change on gRPC API. Signed-off-by: Akihiro Suda <suda.akihiro@lab.ntt.co.jp>
384 lines
12 KiB
Go
384 lines
12 KiB
Go
/*
|
|
Copyright The containerd Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package images
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/containerd/containerd/content"
|
|
"github.com/containerd/containerd/errdefs"
|
|
"github.com/containerd/containerd/log"
|
|
"github.com/containerd/containerd/platforms"
|
|
digest "github.com/opencontainers/go-digest"
|
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
// Image provides the model for how containerd views container images.
|
|
type Image struct {
|
|
// Name of the image.
|
|
//
|
|
// To be pulled, it must be a reference compatible with resolvers.
|
|
//
|
|
// This field is required.
|
|
Name string
|
|
|
|
// Labels provide runtime decoration for the image record.
|
|
//
|
|
// There is no default behavior for how these labels are propagated. They
|
|
// only decorate the static metadata object.
|
|
//
|
|
// This field is optional.
|
|
Labels map[string]string
|
|
|
|
// Target describes the root content for this image. Typically, this is
|
|
// a manifest, index or manifest list.
|
|
Target ocispec.Descriptor
|
|
|
|
CreatedAt, UpdatedAt time.Time
|
|
}
|
|
|
|
// DeleteOptions provide options on image delete
|
|
type DeleteOptions struct {
|
|
Synchronous bool
|
|
}
|
|
|
|
// DeleteOpt allows configuring a delete operation
|
|
type DeleteOpt func(context.Context, *DeleteOptions) error
|
|
|
|
// SynchronousDelete is used to indicate that an image deletion and removal of
|
|
// the image resources should occur synchronously before returning a result.
|
|
func SynchronousDelete() DeleteOpt {
|
|
return func(ctx context.Context, o *DeleteOptions) error {
|
|
o.Synchronous = true
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Store and interact with images
|
|
type Store interface {
|
|
Get(ctx context.Context, name string) (Image, error)
|
|
List(ctx context.Context, filters ...string) ([]Image, error)
|
|
Create(ctx context.Context, image Image) (Image, error)
|
|
|
|
// Update will replace the data in the store with the provided image. If
|
|
// one or more fieldpaths are provided, only those fields will be updated.
|
|
Update(ctx context.Context, image Image, fieldpaths ...string) (Image, error)
|
|
|
|
Delete(ctx context.Context, name string, opts ...DeleteOpt) error
|
|
}
|
|
|
|
// TODO(stevvooe): Many of these functions make strong platform assumptions,
|
|
// which are untrue in a lot of cases. More refactoring must be done here to
|
|
// make this work in all cases.
|
|
|
|
// Config resolves the image configuration descriptor.
|
|
//
|
|
// The caller can then use the descriptor to resolve and process the
|
|
// configuration of the image.
|
|
func (image *Image) Config(ctx context.Context, provider content.Provider, platform string) (ocispec.Descriptor, error) {
|
|
return Config(ctx, provider, image.Target, platform)
|
|
}
|
|
|
|
// RootFS returns the unpacked diffids that make up and images rootfs.
|
|
//
|
|
// These are used to verify that a set of layers unpacked to the expected
|
|
// values.
|
|
func (image *Image) RootFS(ctx context.Context, provider content.Provider, platform string) ([]digest.Digest, error) {
|
|
desc, err := image.Config(ctx, provider, platform)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return RootFS(ctx, provider, desc)
|
|
}
|
|
|
|
// Size returns the total size of an image's packed resources.
|
|
func (image *Image) Size(ctx context.Context, provider content.Provider, platform string) (int64, error) {
|
|
var size int64
|
|
return size, Walk(ctx, Handlers(HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
|
|
if desc.Size < 0 {
|
|
return nil, errors.Errorf("invalid size %v in %v (%v)", desc.Size, desc.Digest, desc.MediaType)
|
|
}
|
|
size += desc.Size
|
|
return nil, nil
|
|
}), FilterPlatforms(ChildrenHandler(provider), platform)), image.Target)
|
|
}
|
|
|
|
// Manifest resolves a manifest from the image for the given platform.
|
|
//
|
|
// TODO(stevvooe): This violates the current platform agnostic approach to this
|
|
// package by returning a specific manifest type. We'll need to refactor this
|
|
// to return a manifest descriptor or decide that we want to bring the API in
|
|
// this direction because this abstraction is not needed.`
|
|
func Manifest(ctx context.Context, provider content.Provider, image ocispec.Descriptor, platform string) (ocispec.Manifest, error) {
|
|
var (
|
|
matcher platforms.Matcher
|
|
m *ocispec.Manifest
|
|
)
|
|
if platform != "" {
|
|
p, err := platforms.Parse(platform)
|
|
if err != nil {
|
|
return ocispec.Manifest{}, err
|
|
}
|
|
matcher = platforms.NewMatcher(p)
|
|
}
|
|
|
|
if err := Walk(ctx, HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
|
|
switch desc.MediaType {
|
|
case MediaTypeDockerSchema2Manifest, ocispec.MediaTypeImageManifest:
|
|
p, err := content.ReadBlob(ctx, provider, desc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var manifest ocispec.Manifest
|
|
if err := json.Unmarshal(p, &manifest); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if platform != "" {
|
|
if desc.Platform != nil && !matcher.Match(*desc.Platform) {
|
|
return nil, nil
|
|
}
|
|
|
|
if desc.Platform == nil {
|
|
p, err := content.ReadBlob(ctx, provider, manifest.Config)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var image ocispec.Image
|
|
if err := json.Unmarshal(p, &image); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !matcher.Match(platforms.Normalize(ocispec.Platform{OS: image.OS, Architecture: image.Architecture})) {
|
|
return nil, nil
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
m = &manifest
|
|
|
|
return nil, nil
|
|
case MediaTypeDockerSchema2ManifestList, ocispec.MediaTypeImageIndex:
|
|
p, err := content.ReadBlob(ctx, provider, desc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var idx ocispec.Index
|
|
if err := json.Unmarshal(p, &idx); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if platform == "" {
|
|
return idx.Manifests, nil
|
|
}
|
|
|
|
var descs []ocispec.Descriptor
|
|
for _, d := range idx.Manifests {
|
|
if d.Platform == nil || matcher.Match(*d.Platform) {
|
|
descs = append(descs, d)
|
|
}
|
|
}
|
|
|
|
return descs, nil
|
|
|
|
}
|
|
return nil, errors.Wrapf(errdefs.ErrNotFound, "unexpected media type %v for %v", desc.MediaType, desc.Digest)
|
|
}), image); err != nil {
|
|
return ocispec.Manifest{}, err
|
|
}
|
|
|
|
if m == nil {
|
|
return ocispec.Manifest{}, errors.Wrapf(errdefs.ErrNotFound, "manifest %v", image.Digest)
|
|
}
|
|
|
|
return *m, nil
|
|
}
|
|
|
|
// Config resolves the image configuration descriptor using a content provided
|
|
// to resolve child resources on the image.
|
|
//
|
|
// The caller can then use the descriptor to resolve and process the
|
|
// configuration of the image.
|
|
func Config(ctx context.Context, provider content.Provider, image ocispec.Descriptor, platform string) (ocispec.Descriptor, error) {
|
|
manifest, err := Manifest(ctx, provider, image, platform)
|
|
if err != nil {
|
|
return ocispec.Descriptor{}, err
|
|
}
|
|
return manifest.Config, err
|
|
}
|
|
|
|
// Platforms returns one or more platforms supported by the image.
|
|
func Platforms(ctx context.Context, provider content.Provider, image ocispec.Descriptor) ([]ocispec.Platform, error) {
|
|
var platformSpecs []ocispec.Platform
|
|
return platformSpecs, Walk(ctx, Handlers(HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
|
|
if desc.Platform != nil {
|
|
platformSpecs = append(platformSpecs, *desc.Platform)
|
|
return nil, ErrSkipDesc
|
|
}
|
|
|
|
switch desc.MediaType {
|
|
case MediaTypeDockerSchema2Config, ocispec.MediaTypeImageConfig:
|
|
p, err := content.ReadBlob(ctx, provider, desc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var image ocispec.Image
|
|
if err := json.Unmarshal(p, &image); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
platformSpecs = append(platformSpecs,
|
|
platforms.Normalize(ocispec.Platform{OS: image.OS, Architecture: image.Architecture}))
|
|
}
|
|
return nil, nil
|
|
}), ChildrenHandler(provider)), image)
|
|
}
|
|
|
|
// Check returns nil if the all components of an image are available in the
|
|
// provider for the specified platform.
|
|
//
|
|
// If available is true, the caller can assume that required represents the
|
|
// complete set of content required for the image.
|
|
//
|
|
// missing will have the components that are part of required but not avaiiable
|
|
// in the provider.
|
|
//
|
|
// If there is a problem resolving content, an error will be returned.
|
|
func Check(ctx context.Context, provider content.Provider, image ocispec.Descriptor, platform string) (available bool, required, present, missing []ocispec.Descriptor, err error) {
|
|
mfst, err := Manifest(ctx, provider, image, platform)
|
|
if err != nil {
|
|
if errdefs.IsNotFound(err) {
|
|
return false, []ocispec.Descriptor{image}, nil, []ocispec.Descriptor{image}, nil
|
|
}
|
|
|
|
return false, nil, nil, nil, errors.Wrapf(err, "failed to check image %v", image.Digest)
|
|
}
|
|
|
|
// TODO(stevvooe): It is possible that referenced conponents could have
|
|
// children, but this is rare. For now, we ignore this and only verify
|
|
// that manifest components are present.
|
|
required = append([]ocispec.Descriptor{mfst.Config}, mfst.Layers...)
|
|
|
|
for _, desc := range required {
|
|
ra, err := provider.ReaderAt(ctx, desc)
|
|
if err != nil {
|
|
if errdefs.IsNotFound(err) {
|
|
missing = append(missing, desc)
|
|
continue
|
|
} else {
|
|
return false, nil, nil, nil, errors.Wrapf(err, "failed to check image %v", desc.Digest)
|
|
}
|
|
}
|
|
ra.Close()
|
|
present = append(present, desc)
|
|
|
|
}
|
|
|
|
return true, required, present, missing, nil
|
|
}
|
|
|
|
// Children returns the immediate children of content described by the descriptor.
|
|
func Children(ctx context.Context, provider content.Provider, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
|
|
var descs []ocispec.Descriptor
|
|
switch desc.MediaType {
|
|
case MediaTypeDockerSchema2Manifest, ocispec.MediaTypeImageManifest:
|
|
p, err := content.ReadBlob(ctx, provider, desc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// TODO(stevvooe): We just assume oci manifest, for now. There may be
|
|
// subtle differences from the docker version.
|
|
var manifest ocispec.Manifest
|
|
if err := json.Unmarshal(p, &manifest); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
descs = append(descs, manifest.Config)
|
|
descs = append(descs, manifest.Layers...)
|
|
case MediaTypeDockerSchema2ManifestList, ocispec.MediaTypeImageIndex:
|
|
p, err := content.ReadBlob(ctx, provider, desc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var index ocispec.Index
|
|
if err := json.Unmarshal(p, &index); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
descs = append(descs, index.Manifests...)
|
|
case MediaTypeDockerSchema2Layer, MediaTypeDockerSchema2LayerGzip,
|
|
MediaTypeDockerSchema2LayerForeign, MediaTypeDockerSchema2LayerForeignGzip,
|
|
MediaTypeDockerSchema2Config, ocispec.MediaTypeImageConfig,
|
|
ocispec.MediaTypeImageLayer, ocispec.MediaTypeImageLayerGzip,
|
|
ocispec.MediaTypeImageLayerNonDistributable, ocispec.MediaTypeImageLayerNonDistributableGzip,
|
|
MediaTypeContainerd1Checkpoint, MediaTypeContainerd1CheckpointConfig:
|
|
// childless data types.
|
|
return nil, nil
|
|
default:
|
|
log.G(ctx).Warnf("encountered unknown type %v; children may not be fetched", desc.MediaType)
|
|
}
|
|
|
|
return descs, nil
|
|
}
|
|
|
|
// RootFS returns the unpacked diffids that make up and images rootfs.
|
|
//
|
|
// These are used to verify that a set of layers unpacked to the expected
|
|
// values.
|
|
func RootFS(ctx context.Context, provider content.Provider, configDesc ocispec.Descriptor) ([]digest.Digest, error) {
|
|
p, err := content.ReadBlob(ctx, provider, configDesc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var config ocispec.Image
|
|
if err := json.Unmarshal(p, &config); err != nil {
|
|
return nil, err
|
|
}
|
|
return config.RootFS.DiffIDs, nil
|
|
}
|
|
|
|
// IsCompressedDiff returns true if mediaType is a known compressed diff media type.
|
|
// It returns false if the media type is a diff, but not compressed. If the media type
|
|
// is not a known diff type, it returns errdefs.ErrNotImplemented
|
|
func IsCompressedDiff(ctx context.Context, mediaType string) (bool, error) {
|
|
switch mediaType {
|
|
case ocispec.MediaTypeImageLayer, MediaTypeDockerSchema2Layer:
|
|
case ocispec.MediaTypeImageLayerGzip, MediaTypeDockerSchema2LayerGzip:
|
|
return true, nil
|
|
default:
|
|
// Still apply all generic media types *.tar[.+]gzip and *.tar
|
|
if strings.HasSuffix(mediaType, ".tar.gzip") || strings.HasSuffix(mediaType, ".tar+gzip") {
|
|
return true, nil
|
|
} else if !strings.HasSuffix(mediaType, ".tar") {
|
|
return false, errdefs.ErrNotImplemented
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|