github.com/containers/libpod@v1.9.4-0.20220419124438-4284fd425507/libpod/image/pull.go (about)

     1  package image
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"path/filepath"
     8  	"strings"
     9  
    10  	cp "github.com/containers/image/v5/copy"
    11  	"github.com/containers/image/v5/directory"
    12  	"github.com/containers/image/v5/docker"
    13  	dockerarchive "github.com/containers/image/v5/docker/archive"
    14  	"github.com/containers/image/v5/docker/tarfile"
    15  	ociarchive "github.com/containers/image/v5/oci/archive"
    16  	oci "github.com/containers/image/v5/oci/layout"
    17  	is "github.com/containers/image/v5/storage"
    18  	"github.com/containers/image/v5/transports"
    19  	"github.com/containers/image/v5/transports/alltransports"
    20  	"github.com/containers/image/v5/types"
    21  	"github.com/containers/libpod/libpod/events"
    22  	"github.com/containers/libpod/pkg/registries"
    23  	"github.com/hashicorp/go-multierror"
    24  	"github.com/opentracing/opentracing-go"
    25  	"github.com/pkg/errors"
    26  	"github.com/sirupsen/logrus"
    27  )
    28  
    29  var (
    30  	// DockerArchive is the transport we prepend to an image name
    31  	// when saving to docker-archive
    32  	DockerArchive = dockerarchive.Transport.Name()
    33  	// OCIArchive is the transport we prepend to an image name
    34  	// when saving to oci-archive
    35  	OCIArchive = ociarchive.Transport.Name()
    36  	// DirTransport is the transport for pushing and pulling
    37  	// images to and from a directory
    38  	DirTransport = directory.Transport.Name()
    39  	// DockerTransport is the transport for docker registries
    40  	DockerTransport = docker.Transport.Name()
    41  	// OCIDirTransport is the transport for pushing and pulling
    42  	// images to and from a directory containing an OCI image
    43  	OCIDirTransport = oci.Transport.Name()
    44  	// AtomicTransport is the transport for atomic registries
    45  	AtomicTransport = "atomic"
    46  	// DefaultTransport is a prefix that we apply to an image name
    47  	// NOTE: This is a string prefix, not actually a transport name usable for transports.Get();
    48  	// and because syntaxes of image names are transport-dependent, the prefix is not really interchangeable;
    49  	// each user implicitly assumes the appended string is a Docker-like reference.
    50  	DefaultTransport = DockerTransport + "://"
    51  	// DefaultLocalRegistry is the default local registry for local image operations
    52  	// Remote pulls will still use defined registries
    53  	DefaultLocalRegistry = "localhost"
    54  )
    55  
    56  // pullRefPair records a pair of prepared image references to pull.
    57  type pullRefPair struct {
    58  	image  string
    59  	srcRef types.ImageReference
    60  	dstRef types.ImageReference
    61  }
    62  
    63  // pullGoal represents the prepared image references and decided behavior to be executed by imagePull
    64  type pullGoal struct {
    65  	refPairs             []pullRefPair
    66  	pullAllPairs         bool     // Pull all refPairs instead of stopping on first success.
    67  	usedSearchRegistries bool     // refPairs construction has depended on registries.GetRegistries()
    68  	searchedRegistries   []string // The list of search registries used; set only if usedSearchRegistries
    69  }
    70  
    71  // singlePullRefPairGoal returns a no-frills pull goal for the specified reference pair.
    72  func singlePullRefPairGoal(rp pullRefPair) *pullGoal {
    73  	return &pullGoal{
    74  		refPairs:             []pullRefPair{rp},
    75  		pullAllPairs:         false, // Does not really make a difference.
    76  		usedSearchRegistries: false,
    77  		searchedRegistries:   nil,
    78  	}
    79  }
    80  
    81  func (ir *Runtime) getPullRefPair(srcRef types.ImageReference, destName string) (pullRefPair, error) {
    82  	decomposedDest, err := decompose(destName)
    83  	if err == nil && !decomposedDest.hasRegistry {
    84  		// If the image doesn't have a registry, set it as the default repo
    85  		ref, err := decomposedDest.referenceWithRegistry(DefaultLocalRegistry)
    86  		if err != nil {
    87  			return pullRefPair{}, err
    88  		}
    89  		destName = ref.String()
    90  	}
    91  
    92  	reference := destName
    93  	if srcRef.DockerReference() != nil {
    94  		reference = srcRef.DockerReference().String()
    95  	}
    96  	destRef, err := is.Transport.ParseStoreReference(ir.store, reference)
    97  	if err != nil {
    98  		return pullRefPair{}, errors.Wrapf(err, "error parsing dest reference name %#v", destName)
    99  	}
   100  	return pullRefPair{
   101  		image:  destName,
   102  		srcRef: srcRef,
   103  		dstRef: destRef,
   104  	}, nil
   105  }
   106  
   107  // getSinglePullRefPairGoal calls getPullRefPair with the specified parameters, and returns a single-pair goal for the return value.
   108  func (ir *Runtime) getSinglePullRefPairGoal(srcRef types.ImageReference, destName string) (*pullGoal, error) {
   109  	rp, err := ir.getPullRefPair(srcRef, destName)
   110  	if err != nil {
   111  		return nil, err
   112  	}
   113  	return singlePullRefPairGoal(rp), nil
   114  }
   115  
   116  // pullGoalFromImageReference returns a pull goal for a single ImageReference, depending on the used transport.
   117  func (ir *Runtime) pullGoalFromImageReference(ctx context.Context, srcRef types.ImageReference, imgName string, sc *types.SystemContext) (*pullGoal, error) {
   118  	span, _ := opentracing.StartSpanFromContext(ctx, "pullGoalFromImageReference")
   119  	defer span.Finish()
   120  
   121  	// supports pulling from docker-archive, oci, and registries
   122  	switch srcRef.Transport().Name() {
   123  	case DockerArchive:
   124  		archivePath := srcRef.StringWithinTransport()
   125  		tarSource, err := tarfile.NewSourceFromFile(archivePath)
   126  		if err != nil {
   127  			return nil, err
   128  		}
   129  		defer tarSource.Close()
   130  		manifest, err := tarSource.LoadTarManifest()
   131  
   132  		if err != nil {
   133  			return nil, errors.Wrapf(err, "error retrieving manifest.json")
   134  		}
   135  		// to pull the first image stored in the tar file
   136  		if len(manifest) == 0 {
   137  			// use the hex of the digest if no manifest is found
   138  			reference, err := getImageDigest(ctx, srcRef, sc)
   139  			if err != nil {
   140  				return nil, err
   141  			}
   142  			return ir.getSinglePullRefPairGoal(srcRef, reference)
   143  		}
   144  
   145  		if len(manifest[0].RepoTags) == 0 {
   146  			// If the input image has no repotags, we need to feed it a dest anyways
   147  			digest, err := getImageDigest(ctx, srcRef, sc)
   148  			if err != nil {
   149  				return nil, err
   150  			}
   151  			return ir.getSinglePullRefPairGoal(srcRef, digest)
   152  		}
   153  
   154  		// Need to load in all the repo tags from the manifest
   155  		res := []pullRefPair{}
   156  		for _, dst := range manifest[0].RepoTags {
   157  			//check if image exists and gives a warning of untagging
   158  			localImage, err := ir.NewFromLocal(dst)
   159  			imageID := strings.TrimSuffix(manifest[0].Config, ".json")
   160  			if err == nil && imageID != localImage.ID() {
   161  				logrus.Errorf("the image %s already exists, renaming the old one with ID %s to empty string", dst, localImage.ID())
   162  			}
   163  
   164  			pullInfo, err := ir.getPullRefPair(srcRef, dst)
   165  			if err != nil {
   166  				return nil, err
   167  			}
   168  			res = append(res, pullInfo)
   169  		}
   170  		return &pullGoal{
   171  			refPairs:             res,
   172  			pullAllPairs:         true,
   173  			usedSearchRegistries: false,
   174  			searchedRegistries:   nil,
   175  		}, nil
   176  
   177  	case OCIArchive:
   178  		// retrieve the manifest from index.json to access the image name
   179  		manifest, err := ociarchive.LoadManifestDescriptor(srcRef)
   180  		if err != nil {
   181  			return nil, errors.Wrapf(err, "error loading manifest for %q", srcRef)
   182  		}
   183  		var dest string
   184  		if manifest.Annotations == nil || manifest.Annotations["org.opencontainers.image.ref.name"] == "" {
   185  			// If the input image has no image.ref.name, we need to feed it a dest anyways
   186  			// use the hex of the digest
   187  			dest, err = getImageDigest(ctx, srcRef, sc)
   188  			if err != nil {
   189  				return nil, errors.Wrapf(err, "error getting image digest; image reference not found")
   190  			}
   191  		} else {
   192  			dest = manifest.Annotations["org.opencontainers.image.ref.name"]
   193  		}
   194  		return ir.getSinglePullRefPairGoal(srcRef, dest)
   195  
   196  	case DirTransport:
   197  		image := toLocalImageName(srcRef.StringWithinTransport())
   198  		return ir.getSinglePullRefPairGoal(srcRef, image)
   199  
   200  	case OCIDirTransport:
   201  		split := strings.SplitN(srcRef.StringWithinTransport(), ":", 2)
   202  		image := toLocalImageName(split[0])
   203  		return ir.getSinglePullRefPairGoal(srcRef, image)
   204  
   205  	default:
   206  		return ir.getSinglePullRefPairGoal(srcRef, imgName)
   207  	}
   208  }
   209  
   210  // toLocalImageName converts an image name into a 'localhost/' prefixed one
   211  func toLocalImageName(imageName string) string {
   212  	return fmt.Sprintf(
   213  		"%s/%s",
   214  		DefaultLocalRegistry,
   215  		strings.TrimLeft(imageName, "/"),
   216  	)
   217  }
   218  
   219  // pullImageFromHeuristicSource pulls an image based on inputName, which is heuristically parsed and may involve configured registries.
   220  // Use pullImageFromReference if the source is known precisely.
   221  func (ir *Runtime) pullImageFromHeuristicSource(ctx context.Context, inputName string, writer io.Writer, authfile, signaturePolicyPath string, signingOptions SigningOptions, dockerOptions *DockerRegistryOptions, label *string) ([]string, error) {
   222  	span, _ := opentracing.StartSpanFromContext(ctx, "pullImageFromHeuristicSource")
   223  	defer span.Finish()
   224  
   225  	var goal *pullGoal
   226  	sc := GetSystemContext(signaturePolicyPath, authfile, false)
   227  	if dockerOptions != nil {
   228  		sc.OSChoice = dockerOptions.OSChoice
   229  		sc.ArchitectureChoice = dockerOptions.ArchitectureChoice
   230  	}
   231  	sc.BlobInfoCacheDir = filepath.Join(ir.store.GraphRoot(), "cache")
   232  	srcRef, err := alltransports.ParseImageName(inputName)
   233  	if err != nil {
   234  		// We might be pulling with an unqualified image reference in which case
   235  		// we need to make sure that we're not using any other transport.
   236  		srcTransport := alltransports.TransportFromImageName(inputName)
   237  		if srcTransport != nil && srcTransport.Name() != DockerTransport {
   238  			return nil, err
   239  		}
   240  		goal, err = ir.pullGoalFromPossiblyUnqualifiedName(inputName)
   241  		if err != nil {
   242  			return nil, errors.Wrap(err, "error getting default registries to try")
   243  		}
   244  	} else {
   245  		goal, err = ir.pullGoalFromImageReference(ctx, srcRef, inputName, sc)
   246  		if err != nil {
   247  			return nil, errors.Wrapf(err, "error determining pull goal for image %q", inputName)
   248  		}
   249  	}
   250  	return ir.doPullImage(ctx, sc, *goal, writer, signingOptions, dockerOptions, label)
   251  }
   252  
   253  // pullImageFromReference pulls an image from a types.imageReference.
   254  func (ir *Runtime) pullImageFromReference(ctx context.Context, srcRef types.ImageReference, writer io.Writer, authfile, signaturePolicyPath string, signingOptions SigningOptions, dockerOptions *DockerRegistryOptions) ([]string, error) {
   255  	span, _ := opentracing.StartSpanFromContext(ctx, "pullImageFromReference")
   256  	defer span.Finish()
   257  
   258  	sc := GetSystemContext(signaturePolicyPath, authfile, false)
   259  	if dockerOptions != nil {
   260  		sc.OSChoice = dockerOptions.OSChoice
   261  		sc.ArchitectureChoice = dockerOptions.ArchitectureChoice
   262  	}
   263  	goal, err := ir.pullGoalFromImageReference(ctx, srcRef, transports.ImageName(srcRef), sc)
   264  	if err != nil {
   265  		return nil, errors.Wrapf(err, "error determining pull goal for image %q", transports.ImageName(srcRef))
   266  	}
   267  	return ir.doPullImage(ctx, sc, *goal, writer, signingOptions, dockerOptions, nil)
   268  }
   269  
   270  func cleanErrorMessage(err error) string {
   271  	errMessage := strings.TrimPrefix(errors.Cause(err).Error(), "errors:\n")
   272  	errMessage = strings.Split(errMessage, "\n")[0]
   273  	return fmt.Sprintf("  %s\n", errMessage)
   274  }
   275  
   276  // doPullImage is an internal helper interpreting pullGoal. Almost everyone should call one of the callers of doPullImage instead.
   277  func (ir *Runtime) doPullImage(ctx context.Context, sc *types.SystemContext, goal pullGoal, writer io.Writer, signingOptions SigningOptions, dockerOptions *DockerRegistryOptions, label *string) ([]string, error) {
   278  	span, _ := opentracing.StartSpanFromContext(ctx, "doPullImage")
   279  	defer span.Finish()
   280  
   281  	policyContext, err := getPolicyContext(sc)
   282  	if err != nil {
   283  		return nil, err
   284  	}
   285  	defer func() {
   286  		if err := policyContext.Destroy(); err != nil {
   287  			logrus.Errorf("failed to destroy policy context: %q", err)
   288  		}
   289  	}()
   290  
   291  	systemRegistriesConfPath := registries.SystemRegistriesConfPath()
   292  
   293  	var (
   294  		images     []string
   295  		pullErrors *multierror.Error
   296  	)
   297  
   298  	for _, imageInfo := range goal.refPairs {
   299  		copyOptions := getCopyOptions(sc, writer, dockerOptions, nil, signingOptions, "", nil)
   300  		copyOptions.SourceCtx.SystemRegistriesConfPath = systemRegistriesConfPath // FIXME: Set this more globally.  Probably no reason not to have it in every types.SystemContext, and to compute the value just once in one place.
   301  		// Print the following statement only when pulling from a docker or atomic registry
   302  		if writer != nil && (imageInfo.srcRef.Transport().Name() == DockerTransport || imageInfo.srcRef.Transport().Name() == AtomicTransport) {
   303  			if _, err := io.WriteString(writer, fmt.Sprintf("Trying to pull %s...\n", imageInfo.image)); err != nil {
   304  				return nil, err
   305  			}
   306  		}
   307  		// If the label is not nil, check if the label exists and if not, return err
   308  		if label != nil {
   309  			if err := checkRemoteImageForLabel(ctx, *label, imageInfo, sc); err != nil {
   310  				return nil, err
   311  			}
   312  		}
   313  
   314  		_, err = cp.Image(ctx, policyContext, imageInfo.dstRef, imageInfo.srcRef, copyOptions)
   315  		if err != nil {
   316  			pullErrors = multierror.Append(pullErrors, err)
   317  			logrus.Debugf("Error pulling image ref %s: %v", imageInfo.srcRef.StringWithinTransport(), err)
   318  			if writer != nil {
   319  				_, _ = io.WriteString(writer, cleanErrorMessage(err))
   320  			}
   321  		} else {
   322  			if !goal.pullAllPairs {
   323  				ir.newImageEvent(events.Pull, "")
   324  				return []string{imageInfo.image}, nil
   325  			}
   326  			images = append(images, imageInfo.image)
   327  		}
   328  	}
   329  	// If no image was found, we should handle.  Lets be nicer to the user and see if we can figure out why.
   330  	if len(images) == 0 {
   331  		if goal.usedSearchRegistries && len(goal.searchedRegistries) == 0 {
   332  			return nil, errors.Errorf("image name provided is a short name and no search registries are defined in the registries config file.")
   333  		}
   334  		// If the image passed in was fully-qualified, we will have 1 refpair.  Bc the image is fq'd, we don't need to yap about registries.
   335  		if !goal.usedSearchRegistries {
   336  			if pullErrors != nil && len(pullErrors.Errors) > 0 { // this should always be true
   337  				return nil, errors.Wrap(pullErrors.Errors[0], "unable to pull image")
   338  			}
   339  			return nil, errors.Errorf("unable to pull image, or you do not have pull access")
   340  		}
   341  		return nil, pullErrors
   342  	}
   343  	if len(images) > 0 {
   344  		ir.newImageEvent(events.Pull, images[0])
   345  	}
   346  	return images, nil
   347  }
   348  
   349  // pullGoalFromPossiblyUnqualifiedName looks at inputName and determines the possible
   350  // image references to try pulling in combination with the registries.conf file as well
   351  func (ir *Runtime) pullGoalFromPossiblyUnqualifiedName(inputName string) (*pullGoal, error) {
   352  	decomposedImage, err := decompose(inputName)
   353  	if err != nil {
   354  		return nil, err
   355  	}
   356  
   357  	if decomposedImage.hasRegistry {
   358  		srcRef, err := docker.ParseReference("//" + inputName)
   359  		if err != nil {
   360  			return nil, errors.Wrapf(err, "unable to parse '%s'", inputName)
   361  		}
   362  		return ir.getSinglePullRefPairGoal(srcRef, inputName)
   363  	}
   364  
   365  	searchRegistries, err := registries.GetRegistries()
   366  	if err != nil {
   367  		return nil, err
   368  	}
   369  	var refPairs []pullRefPair
   370  	for _, registry := range searchRegistries {
   371  		ref, err := decomposedImage.referenceWithRegistry(registry)
   372  		if err != nil {
   373  			return nil, err
   374  		}
   375  		imageName := ref.String()
   376  		srcRef, err := docker.ParseReference("//" + imageName)
   377  		if err != nil {
   378  			return nil, errors.Wrapf(err, "unable to parse '%s'", imageName)
   379  		}
   380  		ps, err := ir.getPullRefPair(srcRef, imageName)
   381  		if err != nil {
   382  			return nil, err
   383  		}
   384  		refPairs = append(refPairs, ps)
   385  	}
   386  	return &pullGoal{
   387  		refPairs:             refPairs,
   388  		pullAllPairs:         false,
   389  		usedSearchRegistries: true,
   390  		searchedRegistries:   searchRegistries,
   391  	}, nil
   392  }
   393  
   394  // checkRemoteImageForLabel checks if the remote image has a specific label. if the label exists, we
   395  // return nil, else we return an error
   396  func checkRemoteImageForLabel(ctx context.Context, label string, imageInfo pullRefPair, sc *types.SystemContext) error {
   397  	labelImage, err := imageInfo.srcRef.NewImage(ctx, sc)
   398  	if err != nil {
   399  		return err
   400  	}
   401  	remoteInspect, err := labelImage.Inspect(ctx)
   402  	if err != nil {
   403  		return err
   404  	}
   405  	// Labels are case insensitive; so we iterate instead of simple lookup
   406  	for k := range remoteInspect.Labels {
   407  		if strings.ToLower(label) == strings.ToLower(k) {
   408  			return nil
   409  		}
   410  	}
   411  	return errors.Errorf("%s has no label %s in %q", imageInfo.image, label, remoteInspect.Labels)
   412  }