github.com/moby/docker@v26.1.3+incompatible/daemon/containerd/image_push.go (about) 1 package containerd 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "strings" 8 "sync" 9 "time" 10 11 "github.com/containerd/containerd/content" 12 cerrdefs "github.com/containerd/containerd/errdefs" 13 "github.com/containerd/containerd/images" 14 containerdimages "github.com/containerd/containerd/images" 15 containerdlabels "github.com/containerd/containerd/labels" 16 "github.com/containerd/containerd/platforms" 17 "github.com/containerd/containerd/remotes" 18 "github.com/containerd/containerd/remotes/docker" 19 "github.com/containerd/log" 20 "github.com/distribution/reference" 21 "github.com/docker/docker/api/types/events" 22 "github.com/docker/docker/api/types/registry" 23 dimages "github.com/docker/docker/daemon/images" 24 "github.com/docker/docker/errdefs" 25 "github.com/docker/docker/internal/compatcontext" 26 "github.com/docker/docker/pkg/progress" 27 "github.com/docker/docker/pkg/streamformatter" 28 "github.com/opencontainers/go-digest" 29 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 30 "github.com/pkg/errors" 31 "golang.org/x/sync/semaphore" 32 ) 33 34 // PushImage initiates a push operation of the image pointed to by sourceRef. 35 // If reference is untagged, all tags from the reference repository are pushed. 36 // Image manifest (or index) is pushed as is, which will probably fail if you 37 // don't have all content referenced by the index. 38 // Cross-repo mounts will be attempted for non-existing blobs. 39 // 40 // It will also add distribution source labels to the pushed content 41 // pointing to the new target repository. This will allow subsequent pushes 42 // to perform cross-repo mounts of the shared content when pushing to a different 43 // repository on the same registry. 44 func (i *ImageService) PushImage(ctx context.Context, sourceRef reference.Named, metaHeaders map[string][]string, authConfig *registry.AuthConfig, outStream io.Writer) (retErr error) { 45 start := time.Now() 46 defer func() { 47 if retErr == nil { 48 dimages.ImageActions.WithValues("push").UpdateSince(start) 49 } 50 }() 51 out := streamformatter.NewJSONProgressOutput(outStream, false) 52 progress.Messagef(out, "", "The push refers to repository [%s]", sourceRef.Name()) 53 54 if _, tagged := sourceRef.(reference.Tagged); !tagged { 55 if _, digested := sourceRef.(reference.Digested); !digested { 56 // Image is not tagged nor digested, that means all tags push was requested. 57 58 // Find all images with the same repository. 59 imgs, err := i.getAllImagesWithRepository(ctx, sourceRef) 60 if err != nil { 61 return err 62 } 63 64 if len(imgs) == 0 { 65 return fmt.Errorf("An image does not exist locally with the tag: %s", reference.FamiliarName(sourceRef)) 66 } 67 68 for _, img := range imgs { 69 named, err := reference.ParseNamed(img.Name) 70 if err != nil { 71 // This shouldn't happen, but log a warning just in case. 72 log.G(ctx).WithFields(log.Fields{ 73 "image": img.Name, 74 "sourceRef": sourceRef, 75 }).Warn("refusing to push an invalid tag") 76 continue 77 } 78 79 if err := i.pushRef(ctx, named, metaHeaders, authConfig, out); err != nil { 80 return err 81 } 82 } 83 84 return nil 85 } 86 } 87 88 return i.pushRef(ctx, sourceRef, metaHeaders, authConfig, out) 89 } 90 91 func (i *ImageService) pushRef(ctx context.Context, targetRef reference.Named, metaHeaders map[string][]string, authConfig *registry.AuthConfig, out progress.Output) (retErr error) { 92 leasedCtx, release, err := i.client.WithLease(ctx) 93 if err != nil { 94 return err 95 } 96 defer func() { 97 if err := release(compatcontext.WithoutCancel(leasedCtx)); err != nil { 98 log.G(ctx).WithField("image", targetRef).WithError(err).Warn("failed to release lease created for push") 99 } 100 }() 101 102 img, err := i.images.Get(ctx, targetRef.String()) 103 if err != nil { 104 if cerrdefs.IsNotFound(err) { 105 return errdefs.NotFound(fmt.Errorf("tag does not exist: %s", reference.FamiliarString(targetRef))) 106 } 107 return errdefs.NotFound(err) 108 } 109 110 target := img.Target 111 store := i.content 112 113 resolver, tracker := i.newResolverFromAuthConfig(ctx, authConfig, targetRef) 114 pp := pushProgress{Tracker: tracker} 115 jobsQueue := newJobs() 116 finishProgress := jobsQueue.showProgress(ctx, out, combinedProgress([]progressUpdater{ 117 &pp, 118 pullProgress{showExists: false, store: store}, 119 })) 120 defer func() { 121 finishProgress() 122 if retErr == nil { 123 if tagged, ok := targetRef.(reference.Tagged); ok { 124 progress.Messagef(out, "", "%s: digest: %s size: %d", tagged.Tag(), target.Digest, img.Target.Size) 125 } 126 } 127 }() 128 129 var limiter *semaphore.Weighted = nil // TODO: Respect max concurrent downloads/uploads 130 131 mountableBlobs, err := findMissingMountable(ctx, store, jobsQueue, target, targetRef, limiter) 132 if err != nil { 133 return err 134 } 135 136 // Create a store which fakes the local existence of possibly mountable blobs. 137 // Otherwise they can't be pushed at all. 138 realStore := store 139 wrapped := wrapWithFakeMountableBlobs(store, mountableBlobs) 140 store = wrapped 141 142 pusher, err := resolver.Pusher(ctx, targetRef.String()) 143 if err != nil { 144 return err 145 } 146 147 addLayerJobs := containerdimages.HandlerFunc( 148 func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { 149 switch { 150 case containerdimages.IsIndexType(desc.MediaType), 151 containerdimages.IsManifestType(desc.MediaType), 152 containerdimages.IsConfigType(desc.MediaType): 153 default: 154 jobsQueue.Add(desc) 155 } 156 157 return nil, nil 158 }, 159 ) 160 161 handlerWrapper := func(h images.Handler) images.Handler { 162 return containerdimages.Handlers(addLayerJobs, h) 163 } 164 165 err = remotes.PushContent(ctx, pusher, target, store, limiter, platforms.All, handlerWrapper) 166 if err != nil { 167 if containerdimages.IsIndexType(target.MediaType) && cerrdefs.IsNotFound(err) { 168 return errdefs.NotFound(fmt.Errorf( 169 "missing content: %w\n"+ 170 "Note: You're trying to push a manifest list/index which "+ 171 "references multiple platform specific manifests, but not all of them are available locally "+ 172 "or available to the remote repository.\n"+ 173 "Make sure you have all the referenced content and try again.", 174 err)) 175 } 176 return err 177 } 178 179 appendDistributionSourceLabel(ctx, realStore, targetRef, target) 180 181 i.LogImageEvent(reference.FamiliarString(targetRef), reference.FamiliarName(targetRef), events.ActionPush) 182 183 return nil 184 } 185 186 func appendDistributionSourceLabel(ctx context.Context, realStore content.Store, targetRef reference.Named, target ocispec.Descriptor) { 187 appendSource, err := docker.AppendDistributionSourceLabel(realStore, targetRef.String()) 188 if err != nil { 189 // This shouldn't happen at this point because the reference would have to be invalid 190 // and if it was, then it would error out earlier. 191 log.G(ctx).WithError(err).Warn("failed to create an handler that appends distribution source label to pushed content") 192 return 193 } 194 195 handler := presentChildrenHandler(realStore, appendSource) 196 if err := containerdimages.Dispatch(ctx, handler, nil, target); err != nil { 197 // Shouldn't happen, but even if it would fail, then make it only a warning 198 // because it doesn't affect the pushed data. 199 log.G(ctx).WithError(err).Warn("failed to append distribution source labels to pushed content") 200 } 201 } 202 203 // findMissingMountable will walk the target descriptor recursively and return 204 // missing contents with their distribution source which could potentially 205 // be cross-repo mounted. 206 func findMissingMountable(ctx context.Context, store content.Store, queue *jobs, 207 target ocispec.Descriptor, targetRef reference.Named, limiter *semaphore.Weighted, 208 ) (map[digest.Digest]distributionSource, error) { 209 mountableBlobs := map[digest.Digest]distributionSource{} 210 var mutex sync.Mutex 211 212 sources, err := getDigestSources(ctx, store, target.Digest) 213 if err != nil { 214 if !errdefs.IsNotFound(err) { 215 return nil, err 216 } 217 log.G(ctx).WithField("target", target).Debug("distribution source label not found") 218 return mountableBlobs, nil 219 } 220 221 handler := func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { 222 _, err := store.Info(ctx, desc.Digest) 223 if err != nil { 224 if !cerrdefs.IsNotFound(err) { 225 return nil, errdefs.System(errors.Wrapf(err, "failed to get metadata of content %s", desc.Digest.String())) 226 } 227 228 for _, source := range sources { 229 if canBeMounted(desc.MediaType, targetRef, source) { 230 mutex.Lock() 231 mountableBlobs[desc.Digest] = source 232 mutex.Unlock() 233 queue.Add(desc) 234 break 235 } 236 } 237 return nil, nil 238 } 239 240 return containerdimages.Children(ctx, store, desc) 241 } 242 243 err = containerdimages.Dispatch(ctx, containerdimages.HandlerFunc(handler), limiter, target) 244 if err != nil { 245 return nil, err 246 } 247 248 return mountableBlobs, nil 249 } 250 251 func getDigestSources(ctx context.Context, store content.Manager, digest digest.Digest) ([]distributionSource, error) { 252 info, err := store.Info(ctx, digest) 253 if err != nil { 254 if cerrdefs.IsNotFound(err) { 255 return nil, errdefs.NotFound(err) 256 } 257 return nil, errdefs.System(err) 258 } 259 260 sources := extractDistributionSources(info.Labels) 261 if sources == nil { 262 return nil, errdefs.NotFound(fmt.Errorf("label %q is not attached to %s", containerdlabels.LabelDistributionSource, digest.String())) 263 } 264 265 return sources, nil 266 } 267 268 func extractDistributionSources(labels map[string]string) []distributionSource { 269 var sources []distributionSource 270 271 // Check if this blob has a distributionSource label 272 // if yes, read it as source 273 for k, v := range labels { 274 if reg := strings.TrimPrefix(k, containerdlabels.LabelDistributionSource); reg != k { 275 for _, repo := range strings.Split(v, ",") { 276 ref, err := reference.ParseNamed(reg + "/" + repo) 277 if err != nil { 278 continue 279 } 280 281 sources = append(sources, distributionSource{ 282 registryRef: ref, 283 }) 284 } 285 } 286 } 287 288 return sources 289 } 290 291 type distributionSource struct { 292 registryRef reference.Named 293 } 294 295 // ToAnnotation returns key and value 296 func (source distributionSource) ToAnnotation() (string, string) { 297 domain := reference.Domain(source.registryRef) 298 v := reference.Path(source.registryRef) 299 return containerdlabels.LabelDistributionSource + domain, v 300 } 301 302 func (source distributionSource) GetReference(dgst digest.Digest) (reference.Named, error) { 303 return reference.WithDigest(source.registryRef, dgst) 304 } 305 306 // canBeMounted returns if the content with given media type can be cross-repo 307 // mounted when pushing it to a remote reference ref. 308 func canBeMounted(mediaType string, targetRef reference.Named, source distributionSource) bool { 309 if containerdimages.IsManifestType(mediaType) { 310 return false 311 } 312 if containerdimages.IsIndexType(mediaType) { 313 return false 314 } 315 316 reg := reference.Domain(targetRef) 317 // Remove :port suffix from domain 318 // containerd distribution source label doesn't store port 319 if portIdx := strings.LastIndex(reg, ":"); portIdx != -1 { 320 reg = reg[:portIdx] 321 } 322 323 // If the source registry is the same as the one we are pushing to 324 // then the cross-repo mount will work. 325 return reg == reference.Domain(source.registryRef) 326 }