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 }