github.com/containerd/nerdctl@v1.7.7/pkg/imgutil/imgutil.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 imgutil
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"fmt"
    23  	"io"
    24  	"reflect"
    25  
    26  	"github.com/containerd/containerd"
    27  	"github.com/containerd/containerd/content"
    28  	"github.com/containerd/containerd/images"
    29  	"github.com/containerd/containerd/remotes"
    30  	"github.com/containerd/containerd/snapshots"
    31  	"github.com/containerd/imgcrypt"
    32  	"github.com/containerd/imgcrypt/images/encryption"
    33  	"github.com/containerd/log"
    34  	"github.com/containerd/nerdctl/pkg/errutil"
    35  	"github.com/containerd/nerdctl/pkg/idutil/imagewalker"
    36  	"github.com/containerd/nerdctl/pkg/imgutil/dockerconfigresolver"
    37  	"github.com/containerd/nerdctl/pkg/imgutil/pull"
    38  	"github.com/containerd/platforms"
    39  	refdocker "github.com/distribution/reference"
    40  	"github.com/docker/docker/errdefs"
    41  	"github.com/opencontainers/image-spec/identity"
    42  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    43  )
    44  
    45  // EnsuredImage contains the image existed in containerd and its metadata.
    46  type EnsuredImage struct {
    47  	Ref         string
    48  	Image       containerd.Image
    49  	ImageConfig ocispec.ImageConfig
    50  	Snapshotter string
    51  	Remote      bool // true for stargz or overlaybd
    52  }
    53  
    54  // PullMode is either one of "always", "missing", "never"
    55  type PullMode = string
    56  
    57  // GetExistingImage returns the specified image if exists in containerd. Return errdefs.NotFound() if not exists.
    58  func GetExistingImage(ctx context.Context, client *containerd.Client, snapshotter, rawRef string, platform ocispec.Platform) (*EnsuredImage, error) {
    59  	var res *EnsuredImage
    60  	imagewalker := &imagewalker.ImageWalker{
    61  		Client: client,
    62  		OnFound: func(ctx context.Context, found imagewalker.Found) error {
    63  			if res != nil {
    64  				return nil
    65  			}
    66  			image := containerd.NewImageWithPlatform(client, found.Image, platforms.OnlyStrict(platform))
    67  			imgConfig, err := getImageConfig(ctx, image)
    68  			if err != nil {
    69  				// Image found but blob not found for foreign arch
    70  				// Ignore err and return nil, so that the walker can visit the next candidate.
    71  				return nil
    72  			}
    73  			res = &EnsuredImage{
    74  				Ref:         found.Image.Name,
    75  				Image:       image,
    76  				ImageConfig: *imgConfig,
    77  				Snapshotter: snapshotter,
    78  				Remote:      getSnapshotterOpts(snapshotter).isRemote(),
    79  			}
    80  			if unpacked, err := image.IsUnpacked(ctx, snapshotter); err == nil && !unpacked {
    81  				if err := image.Unpack(ctx, snapshotter); err != nil {
    82  					return err
    83  				}
    84  			}
    85  			return nil
    86  		},
    87  	}
    88  	count, err := imagewalker.Walk(ctx, rawRef)
    89  	if err != nil {
    90  		return nil, err
    91  	}
    92  	if count == 0 {
    93  		return nil, errdefs.NotFound(fmt.Errorf("got count 0 after walking"))
    94  	}
    95  	if res == nil {
    96  		return nil, errdefs.NotFound(fmt.Errorf("got nil res after walking"))
    97  	}
    98  	return res, nil
    99  }
   100  
   101  // EnsureImage ensures the image.
   102  //
   103  // # When insecure is set, skips verifying certs, and also falls back to HTTP when the registry does not speak HTTPS
   104  //
   105  // FIXME: this func has too many args
   106  func EnsureImage(ctx context.Context, client *containerd.Client, stdout, stderr io.Writer, snapshotter, rawRef string, mode PullMode, insecure bool, hostsDirs []string, ocispecPlatforms []ocispec.Platform, unpack *bool, quiet bool, rFlags RemoteSnapshotterFlags) (*EnsuredImage, error) {
   107  	switch mode {
   108  	case "always", "missing", "never":
   109  		// NOP
   110  	default:
   111  		return nil, fmt.Errorf("unexpected pull mode: %q", mode)
   112  	}
   113  
   114  	// if not `always` pull and given one platform and image found locally, return existing image directly.
   115  	if mode != "always" && len(ocispecPlatforms) == 1 {
   116  		if res, err := GetExistingImage(ctx, client, snapshotter, rawRef, ocispecPlatforms[0]); err == nil {
   117  			return res, nil
   118  		} else if !errdefs.IsNotFound(err) {
   119  			return nil, err
   120  		}
   121  	}
   122  
   123  	if mode == "never" {
   124  		return nil, fmt.Errorf("image not available: %q", rawRef)
   125  	}
   126  
   127  	named, err := refdocker.ParseDockerRef(rawRef)
   128  	if err != nil {
   129  		return nil, err
   130  	}
   131  	ref := named.String()
   132  	refDomain := refdocker.Domain(named)
   133  
   134  	var dOpts []dockerconfigresolver.Opt
   135  	if insecure {
   136  		log.G(ctx).Warnf("skipping verifying HTTPS certs for %q", refDomain)
   137  		dOpts = append(dOpts, dockerconfigresolver.WithSkipVerifyCerts(true))
   138  	}
   139  	dOpts = append(dOpts, dockerconfigresolver.WithHostsDirs(hostsDirs))
   140  	resolver, err := dockerconfigresolver.New(ctx, refDomain, dOpts...)
   141  	if err != nil {
   142  		return nil, err
   143  	}
   144  
   145  	img, err := PullImage(ctx, client, stdout, stderr, snapshotter, resolver, ref, ocispecPlatforms, unpack, quiet, rFlags)
   146  	if err != nil {
   147  		// In some circumstance (e.g. people just use 80 port to support pure http), the error will contain message like "dial tcp <port>: connection refused".
   148  		if !errutil.IsErrHTTPResponseToHTTPSClient(err) && !errutil.IsErrConnectionRefused(err) {
   149  			return nil, err
   150  		}
   151  		if insecure {
   152  			log.G(ctx).WithError(err).Warnf("server %q does not seem to support HTTPS, falling back to plain HTTP", refDomain)
   153  			dOpts = append(dOpts, dockerconfigresolver.WithPlainHTTP(true))
   154  			resolver, err = dockerconfigresolver.New(ctx, refDomain, dOpts...)
   155  			if err != nil {
   156  				return nil, err
   157  			}
   158  			return PullImage(ctx, client, stdout, stderr, snapshotter, resolver, ref, ocispecPlatforms, unpack, quiet, rFlags)
   159  		}
   160  		log.G(ctx).WithError(err).Errorf("server %q does not seem to support HTTPS", refDomain)
   161  		log.G(ctx).Info("Hint: you may want to try --insecure-registry to allow plain HTTP (if you are in a trusted network)")
   162  		return nil, err
   163  
   164  	}
   165  	return img, nil
   166  }
   167  
   168  // ResolveDigest resolves `rawRef` and returns its descriptor digest.
   169  func ResolveDigest(ctx context.Context, rawRef string, insecure bool, hostsDirs []string) (string, error) {
   170  	named, err := refdocker.ParseDockerRef(rawRef)
   171  	if err != nil {
   172  		return "", err
   173  	}
   174  	ref := named.String()
   175  	refDomain := refdocker.Domain(named)
   176  
   177  	var dOpts []dockerconfigresolver.Opt
   178  	if insecure {
   179  		log.G(ctx).Warnf("skipping verifying HTTPS certs for %q", refDomain)
   180  		dOpts = append(dOpts, dockerconfigresolver.WithSkipVerifyCerts(true))
   181  	}
   182  	dOpts = append(dOpts, dockerconfigresolver.WithHostsDirs(hostsDirs))
   183  	resolver, err := dockerconfigresolver.New(ctx, refDomain, dOpts...)
   184  	if err != nil {
   185  		return "", err
   186  	}
   187  
   188  	_, desc, err := resolver.Resolve(ctx, ref)
   189  	if err != nil {
   190  		return "", err
   191  	}
   192  
   193  	return desc.Digest.String(), nil
   194  }
   195  
   196  // PullImage pulls an image using the specified resolver.
   197  func PullImage(ctx context.Context, client *containerd.Client, stdout, stderr io.Writer, snapshotter string, resolver remotes.Resolver, ref string, ocispecPlatforms []ocispec.Platform, unpack *bool, quiet bool, rFlags RemoteSnapshotterFlags) (*EnsuredImage, error) {
   198  	ctx, done, err := client.WithLease(ctx)
   199  	if err != nil {
   200  		return nil, err
   201  	}
   202  	defer done(ctx)
   203  
   204  	var containerdImage containerd.Image
   205  	config := &pull.Config{
   206  		Resolver:   resolver,
   207  		RemoteOpts: []containerd.RemoteOpt{},
   208  		Platforms:  ocispecPlatforms, // empty for all-platforms
   209  	}
   210  	if !quiet {
   211  		config.ProgressOutput = stderr
   212  	}
   213  
   214  	// unpack(B) if given 1 platform unless specified by `unpack`
   215  	unpackB := len(ocispecPlatforms) == 1
   216  	if unpack != nil {
   217  		unpackB = *unpack
   218  		if unpackB && len(ocispecPlatforms) != 1 {
   219  			return nil, fmt.Errorf("unpacking requires a single platform to be specified (e.g., --platform=amd64)")
   220  		}
   221  	}
   222  
   223  	snOpt := getSnapshotterOpts(snapshotter)
   224  	if unpackB {
   225  		log.G(ctx).Debugf("The image will be unpacked for platform %q, snapshotter %q.", ocispecPlatforms[0], snapshotter)
   226  		imgcryptPayload := imgcrypt.Payload{}
   227  		imgcryptUnpackOpt := encryption.WithUnpackConfigApplyOpts(encryption.WithDecryptedUnpack(&imgcryptPayload))
   228  		config.RemoteOpts = append(config.RemoteOpts,
   229  			containerd.WithPullUnpack,
   230  			containerd.WithUnpackOpts([]containerd.UnpackOpt{imgcryptUnpackOpt}))
   231  
   232  		// different remote snapshotters will update pull.Config separately
   233  		snOpt.apply(config, ref, rFlags)
   234  	} else {
   235  		log.G(ctx).Debugf("The image will not be unpacked. Platforms=%v.", ocispecPlatforms)
   236  	}
   237  
   238  	containerdImage, err = pull.Pull(ctx, client, ref, config)
   239  	if err != nil {
   240  		return nil, err
   241  	}
   242  	imgConfig, err := getImageConfig(ctx, containerdImage)
   243  	if err != nil {
   244  		return nil, err
   245  	}
   246  	res := &EnsuredImage{
   247  		Ref:         ref,
   248  		Image:       containerdImage,
   249  		ImageConfig: *imgConfig,
   250  		Snapshotter: snapshotter,
   251  		Remote:      snOpt.isRemote(),
   252  	}
   253  	return res, nil
   254  
   255  }
   256  
   257  func getImageConfig(ctx context.Context, image containerd.Image) (*ocispec.ImageConfig, error) {
   258  	desc, err := image.Config(ctx)
   259  	if err != nil {
   260  		return nil, err
   261  	}
   262  	switch desc.MediaType {
   263  	case ocispec.MediaTypeImageConfig, images.MediaTypeDockerSchema2Config:
   264  		var ocispecImage ocispec.Image
   265  		b, err := content.ReadBlob(ctx, image.ContentStore(), desc)
   266  		if err != nil {
   267  			return nil, err
   268  		}
   269  
   270  		if err := json.Unmarshal(b, &ocispecImage); err != nil {
   271  			return nil, err
   272  		}
   273  		return &ocispecImage.Config, nil
   274  	default:
   275  		return nil, fmt.Errorf("unknown media type %q", desc.MediaType)
   276  	}
   277  }
   278  
   279  // ReadIndex returns image index, or nil for non-indexed image.
   280  func ReadIndex(ctx context.Context, img containerd.Image) (*ocispec.Index, *ocispec.Descriptor, error) {
   281  	desc := img.Target()
   282  	if !images.IsIndexType(desc.MediaType) {
   283  		return nil, nil, nil
   284  	}
   285  	b, err := content.ReadBlob(ctx, img.ContentStore(), desc)
   286  	if err != nil {
   287  		return nil, &desc, err
   288  	}
   289  	var idx ocispec.Index
   290  	if err := json.Unmarshal(b, &idx); err != nil {
   291  		return nil, &desc, err
   292  	}
   293  
   294  	return &idx, &desc, nil
   295  }
   296  
   297  // ReadManifest returns the manifest for img.platform, or nil if no manifest was found.
   298  func ReadManifest(ctx context.Context, img containerd.Image) (*ocispec.Manifest, *ocispec.Descriptor, error) {
   299  	cs := img.ContentStore()
   300  	targetDesc := img.Target()
   301  	if images.IsManifestType(targetDesc.MediaType) {
   302  		b, err := content.ReadBlob(ctx, img.ContentStore(), targetDesc)
   303  		if err != nil {
   304  			return nil, &targetDesc, err
   305  		}
   306  		var mani ocispec.Manifest
   307  		if err := json.Unmarshal(b, &mani); err != nil {
   308  			return nil, &targetDesc, err
   309  		}
   310  		return &mani, &targetDesc, nil
   311  	}
   312  	if images.IsIndexType(targetDesc.MediaType) {
   313  		idx, _, err := ReadIndex(ctx, img)
   314  		if err != nil {
   315  			return nil, nil, err
   316  		}
   317  		configDesc, err := img.Config(ctx) // aware of img.platform
   318  		if err != nil {
   319  			return nil, nil, err
   320  		}
   321  		// We can't access the private `img.platform` variable.
   322  		// So, we find the manifest object by comparing the config desc.
   323  		for _, maniDesc := range idx.Manifests {
   324  			maniDesc := maniDesc
   325  			// ignore non-nil err
   326  			if b, err := content.ReadBlob(ctx, cs, maniDesc); err == nil {
   327  				var mani ocispec.Manifest
   328  				if err := json.Unmarshal(b, &mani); err != nil {
   329  					return nil, nil, err
   330  				}
   331  				if reflect.DeepEqual(configDesc, mani.Config) {
   332  					return &mani, &maniDesc, nil
   333  				}
   334  			}
   335  		}
   336  	}
   337  	// no manifest was found
   338  	return nil, nil, nil
   339  }
   340  
   341  // ReadImageConfig reads the config spec (`application/vnd.oci.image.config.v1+json`) for img.platform from content store.
   342  func ReadImageConfig(ctx context.Context, img containerd.Image) (ocispec.Image, ocispec.Descriptor, error) {
   343  	var config ocispec.Image
   344  
   345  	configDesc, err := img.Config(ctx) // aware of img.platform
   346  	if err != nil {
   347  		return config, configDesc, err
   348  	}
   349  	p, err := content.ReadBlob(ctx, img.ContentStore(), configDesc)
   350  	if err != nil {
   351  		return config, configDesc, err
   352  	}
   353  	if err := json.Unmarshal(p, &config); err != nil {
   354  		return config, configDesc, err
   355  	}
   356  	return config, configDesc, nil
   357  }
   358  
   359  // ParseRepoTag parses raw `imgName` to repository and tag.
   360  func ParseRepoTag(imgName string) (string, string) {
   361  	log.L.Debugf("raw image name=%q", imgName)
   362  
   363  	ref, err := refdocker.ParseDockerRef(imgName)
   364  	if err != nil {
   365  		log.L.WithError(err).Debugf("unparsable image name %q", imgName)
   366  		return "", ""
   367  	}
   368  
   369  	var tag string
   370  
   371  	if tagged, ok := ref.(refdocker.Tagged); ok {
   372  		tag = tagged.Tag()
   373  	}
   374  	repository := refdocker.FamiliarName(ref)
   375  
   376  	return repository, tag
   377  }
   378  
   379  type snapshotKey string
   380  
   381  // recursive function to calculate total usage of key's parent
   382  func (key snapshotKey) add(ctx context.Context, s snapshots.Snapshotter, usage *snapshots.Usage) error {
   383  	if key == "" {
   384  		return nil
   385  	}
   386  	u, err := s.Usage(ctx, string(key))
   387  	if err != nil {
   388  		return err
   389  	}
   390  
   391  	usage.Add(u)
   392  
   393  	info, err := s.Stat(ctx, string(key))
   394  	if err != nil {
   395  		return err
   396  	}
   397  
   398  	key = snapshotKey(info.Parent)
   399  	return key.add(ctx, s, usage)
   400  }
   401  
   402  // UnpackedImageSize is the size of the unpacked snapshots.
   403  // Does not contain the size of the blobs in the content store. (Corresponds to Docker).
   404  func UnpackedImageSize(ctx context.Context, s snapshots.Snapshotter, img containerd.Image) (int64, error) {
   405  	diffIDs, err := img.RootFS(ctx)
   406  	if err != nil {
   407  		return 0, err
   408  	}
   409  
   410  	chainID := identity.ChainID(diffIDs).String()
   411  	usage, err := s.Usage(ctx, chainID)
   412  	if err != nil {
   413  		if errdefs.IsNotFound(err) {
   414  			log.G(ctx).WithError(err).Debugf("image %q seems not unpacked", img.Name())
   415  			return 0, nil
   416  		}
   417  		return 0, err
   418  	}
   419  
   420  	info, err := s.Stat(ctx, chainID)
   421  	if err != nil {
   422  		return 0, err
   423  	}
   424  
   425  	//add ChainID's parent usage to the total usage
   426  	if err := snapshotKey(info.Parent).add(ctx, s, &usage); err != nil {
   427  		return 0, err
   428  	}
   429  	return usage.Size, nil
   430  }