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