github.com/moby/docker@v26.1.3+incompatible/daemon/containerd/image_pull.go (about) 1 package containerd 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "os" 8 "strings" 9 "time" 10 11 "github.com/containerd/containerd" 12 cerrdefs "github.com/containerd/containerd/errdefs" 13 "github.com/containerd/containerd/images" 14 "github.com/containerd/containerd/pkg/snapshotters" 15 "github.com/containerd/containerd/platforms" 16 "github.com/containerd/containerd/remotes/docker" 17 "github.com/containerd/log" 18 "github.com/distribution/reference" 19 "github.com/docker/docker/api/types/events" 20 registrytypes "github.com/docker/docker/api/types/registry" 21 dimages "github.com/docker/docker/daemon/images" 22 "github.com/docker/docker/distribution" 23 "github.com/docker/docker/errdefs" 24 "github.com/docker/docker/internal/compatcontext" 25 "github.com/docker/docker/pkg/progress" 26 "github.com/docker/docker/pkg/streamformatter" 27 "github.com/docker/docker/pkg/stringid" 28 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 29 "github.com/pkg/errors" 30 ) 31 32 // PullImage initiates a pull operation. baseRef is the image to pull. 33 // If reference is not tagged, all tags are pulled. 34 func (i *ImageService) PullImage(ctx context.Context, baseRef reference.Named, platform *ocispec.Platform, metaHeaders map[string][]string, authConfig *registrytypes.AuthConfig, outStream io.Writer) (retErr error) { 35 start := time.Now() 36 defer func() { 37 if retErr == nil { 38 dimages.ImageActions.WithValues("pull").UpdateSince(start) 39 } 40 }() 41 out := streamformatter.NewJSONProgressOutput(outStream, false) 42 43 if !reference.IsNameOnly(baseRef) { 44 return i.pullTag(ctx, baseRef, platform, metaHeaders, authConfig, out) 45 } 46 47 tags, err := distribution.Tags(ctx, baseRef, &distribution.Config{ 48 RegistryService: i.registryService, 49 MetaHeaders: metaHeaders, 50 AuthConfig: authConfig, 51 }) 52 if err != nil { 53 return err 54 } 55 56 for _, tag := range tags { 57 ref, err := reference.WithTag(baseRef, tag) 58 if err != nil { 59 log.G(ctx).WithFields(log.Fields{ 60 "tag": tag, 61 "baseRef": baseRef, 62 }).Warn("invalid tag, won't pull") 63 continue 64 } 65 66 if err := i.pullTag(ctx, ref, platform, metaHeaders, authConfig, out); err != nil { 67 return fmt.Errorf("error pulling %s: %w", ref, err) 68 } 69 } 70 71 return nil 72 } 73 74 func (i *ImageService) pullTag(ctx context.Context, ref reference.Named, platform *ocispec.Platform, metaHeaders map[string][]string, authConfig *registrytypes.AuthConfig, out progress.Output) error { 75 var opts []containerd.RemoteOpt 76 if platform != nil { 77 opts = append(opts, containerd.WithPlatform(platforms.Format(*platform))) 78 } 79 80 resolver, _ := i.newResolverFromAuthConfig(ctx, authConfig, ref) 81 opts = append(opts, containerd.WithResolver(resolver)) 82 83 old, err := i.resolveDescriptor(ctx, ref.String()) 84 if err != nil && !errdefs.IsNotFound(err) { 85 return err 86 } 87 p := platforms.Default() 88 if platform != nil { 89 p = platforms.Only(*platform) 90 } 91 92 jobs := newJobs() 93 h := images.HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { 94 if images.IsLayerType(desc.MediaType) { 95 jobs.Add(desc) 96 } 97 return nil, nil 98 }) 99 opts = append(opts, containerd.WithImageHandler(h)) 100 101 pp := pullProgress{store: i.content, showExists: true} 102 finishProgress := jobs.showProgress(ctx, out, pp) 103 104 var outNewImg *containerd.Image 105 defer func() { 106 finishProgress() 107 108 // Send final status message after the progress updater has finished. 109 // Otherwise the layer/manifest progress messages may arrive AFTER the 110 // status message have been sent, so they won't update the previous 111 // progress leaving stale progress like: 112 // 70f5ac315c5a: Downloading [> ] 0B/3.19kB 113 // Digest: sha256:4f53e2564790c8e7856ec08e384732aa38dc43c52f02952483e3f003afbf23db 114 // 70f5ac315c5a: Download complete 115 // Status: Downloaded newer image for hello-world:latest 116 // docker.io/library/hello-world:latest 117 if outNewImg != nil { 118 img := *outNewImg 119 progress.Message(out, "", "Digest: "+img.Target().Digest.String()) 120 writeStatus(out, reference.FamiliarString(ref), old.Digest != img.Target().Digest) 121 } 122 }() 123 124 var sentPullingFrom, sentSchema1Deprecation bool 125 ah := images.HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { 126 if desc.MediaType == images.MediaTypeDockerSchema1Manifest && !sentSchema1Deprecation { 127 err := distribution.DeprecatedSchema1ImageError(ref) 128 if os.Getenv("DOCKER_ENABLE_DEPRECATED_PULL_SCHEMA_1_IMAGE") == "" { 129 log.G(context.TODO()).Warn(err.Error()) 130 return nil, err 131 } 132 progress.Message(out, "", err.Error()) 133 sentSchema1Deprecation = true 134 } 135 if images.IsLayerType(desc.MediaType) { 136 id := stringid.TruncateID(desc.Digest.String()) 137 progress.Update(out, id, "Pulling fs layer") 138 } 139 if images.IsManifestType(desc.MediaType) { 140 if !sentPullingFrom { 141 var tagOrDigest string 142 if tagged, ok := ref.(reference.Tagged); ok { 143 tagOrDigest = tagged.Tag() 144 } else { 145 tagOrDigest = ref.String() 146 } 147 progress.Message(out, tagOrDigest, "Pulling from "+reference.Path(ref)) 148 sentPullingFrom = true 149 } 150 151 available, _, _, missing, err := images.Check(ctx, i.content, desc, p) 152 if err != nil { 153 return nil, err 154 } 155 // If we already have all the contents pull shouldn't show any layer 156 // download progress, not even a "Already present" message. 157 if available && len(missing) == 0 { 158 pp.hideLayers = true 159 } 160 } 161 return nil, nil 162 }) 163 opts = append(opts, containerd.WithImageHandler(ah)) 164 165 opts = append(opts, containerd.WithPullUnpack) 166 // TODO(thaJeztah): we may have to pass the snapshotter to use if the pull is part of a "docker run" (container create -> pull image if missing). See https://github.com/moby/moby/issues/45273 167 opts = append(opts, containerd.WithPullSnapshotter(i.snapshotter)) 168 169 // AppendInfoHandlerWrapper will annotate the image with basic information like manifest and layer digests as labels; 170 // this information is used to enable remote snapshotters like nydus and stargz to query a registry. 171 infoHandler := snapshotters.AppendInfoHandlerWrapper(ref.String()) 172 opts = append(opts, containerd.WithImageHandlerWrapper(infoHandler)) 173 174 // Allow pulling application/vnd.docker.distribution.manifest.v1+prettyjws images 175 // by converting them to OCI manifests. 176 opts = append(opts, containerd.WithSchema1Conversion) //nolint:staticcheck // Ignore SA1019: containerd.WithSchema1Conversion is deprecated: use Schema 2 or OCI images. 177 178 img, err := i.client.Pull(ctx, ref.String(), opts...) 179 if err != nil { 180 if errors.Is(err, docker.ErrInvalidAuthorization) { 181 // Match error returned by containerd. 182 // https://github.com/containerd/containerd/blob/v1.7.8/remotes/docker/authorizer.go#L189-L191 183 if strings.Contains(err.Error(), "no basic auth credentials") { 184 return err 185 } 186 return errdefs.NotFound(fmt.Errorf("pull access denied for %s, repository does not exist or may require 'docker login'", reference.FamiliarName(ref))) 187 } 188 return err 189 } 190 191 logger := log.G(ctx).WithFields(log.Fields{ 192 "digest": img.Target().Digest, 193 "remote": ref.String(), 194 }) 195 logger.Info("image pulled") 196 197 // The pull succeeded, so try to remove any dangling image we have for this target 198 err = i.images.Delete(compatcontext.WithoutCancel(ctx), danglingImageName(img.Target().Digest)) 199 if err != nil && !cerrdefs.IsNotFound(err) { 200 // Image pull succeeded, but cleaning up the dangling image failed. Ignore the 201 // error to not mark the pull as failed. 202 logger.WithError(err).Warn("unexpected error while removing outdated dangling image reference") 203 } 204 205 i.LogImageEvent(reference.FamiliarString(ref), reference.FamiliarName(ref), events.ActionPull) 206 outNewImg = &img 207 return nil 208 } 209 210 // writeStatus writes a status message to out. If newerDownloaded is true, the 211 // status message indicates that a newer image was downloaded. Otherwise, it 212 // indicates that the image is up to date. requestedTag is the tag the message 213 // will refer to. 214 func writeStatus(out progress.Output, requestedTag string, newerDownloaded bool) { 215 if newerDownloaded { 216 progress.Message(out, "", "Status: Downloaded newer image for "+requestedTag) 217 } else { 218 progress.Message(out, "", "Status: Image is up to date for "+requestedTag) 219 } 220 }