github.com/Prakhar-Agarwal-byte/moby@v0.0.0-20231027092010-a14e3e8ab87e/daemon/containerd/image_exporter.go (about) 1 package containerd 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "strings" 8 9 "github.com/containerd/containerd" 10 "github.com/containerd/containerd/content" 11 cerrdefs "github.com/containerd/containerd/errdefs" 12 containerdimages "github.com/containerd/containerd/images" 13 "github.com/containerd/containerd/images/archive" 14 "github.com/containerd/containerd/leases" 15 cplatforms "github.com/containerd/containerd/platforms" 16 "github.com/containerd/log" 17 "github.com/distribution/reference" 18 "github.com/Prakhar-Agarwal-byte/moby/api/types/events" 19 "github.com/Prakhar-Agarwal-byte/moby/container" 20 "github.com/Prakhar-Agarwal-byte/moby/daemon/images" 21 "github.com/Prakhar-Agarwal-byte/moby/errdefs" 22 dockerarchive "github.com/Prakhar-Agarwal-byte/moby/pkg/archive" 23 "github.com/Prakhar-Agarwal-byte/moby/pkg/platforms" 24 "github.com/Prakhar-Agarwal-byte/moby/pkg/streamformatter" 25 "github.com/opencontainers/image-spec/specs-go" 26 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 27 "github.com/pkg/errors" 28 ) 29 30 func (i *ImageService) PerformWithBaseFS(ctx context.Context, c *container.Container, fn func(root string) error) error { 31 snapshotter := i.client.SnapshotService(c.Driver) 32 mounts, err := snapshotter.Mounts(ctx, c.ID) 33 if err != nil { 34 return err 35 } 36 path, err := i.refCountMounter.Mount(mounts, c.ID) 37 if err != nil { 38 return err 39 } 40 defer i.refCountMounter.Unmount(path) 41 42 return fn(path) 43 } 44 45 // ExportImage exports a list of images to the given output stream. The 46 // exported images are archived into a tar when written to the output 47 // stream. All images with the given tag and all versions containing 48 // the same tag are exported. names is the set of tags to export, and 49 // outStream is the writer which the images are written to. 50 // 51 // TODO(thaJeztah): produce JSON stream progress response and image events; see https://github.com/moby/moby/issues/43910 52 func (i *ImageService) ExportImage(ctx context.Context, names []string, outStream io.Writer) error { 53 platform := platforms.AllPlatformsWithPreference(cplatforms.Default()) 54 opts := []archive.ExportOpt{ 55 archive.WithSkipNonDistributableBlobs(), 56 57 // This makes the exported archive also include `manifest.json` 58 // when the image is a manifest list. It is needed for backwards 59 // compatibility with Docker image format. 60 // The containerd will choose only one manifest for the `manifest.json`. 61 // Our preference is to have it point to the default platform. 62 // Example: 63 // Daemon is running on linux/arm64 64 // When we export linux/amd64 and linux/arm64, manifest.json will point to linux/arm64. 65 // When we export linux/amd64 only, manifest.json will point to linux/amd64. 66 // Note: This is only applicable if importing this archive into non-containerd Docker. 67 // Importing the same archive into containerd, will not restrict the platforms. 68 archive.WithPlatform(platform), 69 } 70 71 contentStore := i.client.ContentStore() 72 leasesManager := i.client.LeasesService() 73 lease, err := leasesManager.Create(ctx, leases.WithRandomID()) 74 if err != nil { 75 return errdefs.System(err) 76 } 77 defer func() { 78 if err := leasesManager.Delete(ctx, lease); err != nil { 79 log.G(ctx).WithError(err).Warn("cleaning up lease") 80 } 81 }() 82 83 addLease := func(ctx context.Context, target ocispec.Descriptor) error { 84 return leaseContent(ctx, contentStore, leasesManager, lease, target) 85 } 86 87 exportImage := func(ctx context.Context, target ocispec.Descriptor, ref reference.Named) error { 88 if err := addLease(ctx, target); err != nil { 89 return err 90 } 91 92 // We may not have locally all the platforms that are specified in the index. 93 // Export only those manifests that we have. 94 // TODO(vvoland): Reconsider this when `--platform` is added. 95 if containerdimages.IsIndexType(target.MediaType) { 96 desc, err := i.getBestDescriptorForExport(ctx, target) 97 if err != nil { 98 return err 99 } 100 target = desc 101 } 102 103 if ref != nil { 104 opts = append(opts, archive.WithManifest(target, ref.String())) 105 106 log.G(ctx).WithFields(log.Fields{ 107 "target": target, 108 "name": ref, 109 }).Debug("export image") 110 } else { 111 orgTarget := target 112 target.Annotations = make(map[string]string) 113 114 for k, v := range orgTarget.Annotations { 115 switch k { 116 case containerdimages.AnnotationImageName, ocispec.AnnotationRefName: 117 // Strip image name/tag annotations from the descriptor. 118 // Otherwise containerd will use it as name. 119 default: 120 target.Annotations[k] = v 121 } 122 } 123 124 opts = append(opts, archive.WithManifest(target)) 125 126 log.G(ctx).WithFields(log.Fields{ 127 "target": target, 128 }).Debug("export image without name") 129 } 130 131 i.LogImageEvent(target.Digest.String(), target.Digest.String(), events.ActionSave) 132 return nil 133 } 134 135 exportRepository := func(ctx context.Context, ref reference.Named) error { 136 imgs, err := i.getAllImagesWithRepository(ctx, ref) 137 if err != nil { 138 return errdefs.System(fmt.Errorf("failed to list all images from repository %s: %w", ref.Name(), err)) 139 } 140 141 if len(imgs) == 0 { 142 return images.ErrImageDoesNotExist{Ref: ref} 143 } 144 145 for _, img := range imgs { 146 ref, err := reference.ParseNamed(img.Name) 147 148 if err != nil { 149 log.G(ctx).WithFields(log.Fields{ 150 "image": img.Name, 151 "error": err, 152 }).Warn("couldn't parse image name as a valid named reference") 153 continue 154 } 155 156 if err := exportImage(ctx, img.Target, ref); err != nil { 157 return err 158 } 159 } 160 161 return nil 162 } 163 164 for _, name := range names { 165 target, resolveErr := i.resolveDescriptor(ctx, name) 166 167 // Check if the requested name is a truncated digest of the resolved descriptor. 168 // If yes, that means that the user specified a specific image ID so 169 // it's not referencing a repository. 170 specificDigestResolved := false 171 if resolveErr == nil { 172 nameWithoutDigestAlgorithm := strings.TrimPrefix(name, target.Digest.Algorithm().String()+":") 173 specificDigestResolved = strings.HasPrefix(target.Digest.Encoded(), nameWithoutDigestAlgorithm) 174 } 175 176 log.G(ctx).WithFields(log.Fields{ 177 "name": name, 178 "resolveErr": resolveErr, 179 "specificDigestResolved": specificDigestResolved, 180 }).Debug("export requested") 181 182 ref, refErr := reference.ParseNormalizedNamed(name) 183 184 if resolveErr != nil || !specificDigestResolved { 185 // Name didn't resolve to anything, or name wasn't explicitly referencing a digest 186 if refErr == nil && reference.IsNameOnly(ref) { 187 // Reference is valid, but doesn't include a specific tag. 188 // Export all images with the same repository. 189 if err := exportRepository(ctx, ref); err != nil { 190 return err 191 } 192 continue 193 } 194 } 195 196 if resolveErr != nil { 197 return resolveErr 198 } 199 if refErr != nil { 200 return refErr 201 } 202 203 // If user exports a specific digest, it shouldn't have a tag. 204 if specificDigestResolved { 205 ref = nil 206 } 207 if err := exportImage(ctx, target, ref); err != nil { 208 return err 209 } 210 } 211 212 return i.client.Export(ctx, outStream, opts...) 213 } 214 215 // leaseContent will add a resource to the lease for each child of the descriptor making sure that it and 216 // its children won't be deleted while the lease exists 217 func leaseContent(ctx context.Context, store content.Store, leasesManager leases.Manager, lease leases.Lease, desc ocispec.Descriptor) error { 218 return containerdimages.Walk(ctx, containerdimages.HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { 219 _, err := store.Info(ctx, desc.Digest) 220 if err != nil { 221 if errors.Is(err, cerrdefs.ErrNotFound) { 222 return nil, nil 223 } 224 return nil, errdefs.System(err) 225 } 226 227 r := leases.Resource{ 228 ID: desc.Digest.String(), 229 Type: "content", 230 } 231 if err := leasesManager.AddResource(ctx, lease, r); err != nil { 232 return nil, errdefs.System(err) 233 } 234 235 return containerdimages.Children(ctx, store, desc) 236 }), desc) 237 } 238 239 // LoadImage uploads a set of images into the repository. This is the 240 // complement of ExportImage. The input stream is an uncompressed tar 241 // ball containing images and metadata. 242 func (i *ImageService) LoadImage(ctx context.Context, inTar io.ReadCloser, outStream io.Writer, quiet bool) error { 243 decompressed, err := dockerarchive.DecompressStream(inTar) 244 if err != nil { 245 return errors.Wrap(err, "failed to decompress input tar archive") 246 } 247 defer decompressed.Close() 248 249 opts := []containerd.ImportOpt{ 250 // TODO(vvoland): Allow user to pass platform 251 containerd.WithImportPlatform(cplatforms.All), 252 253 // Create an additional image with dangling name for imported images... 254 containerd.WithDigestRef(danglingImageName), 255 // ... but only if they don't have a name or it's invalid. 256 containerd.WithSkipDigestRef(func(nameFromArchive string) bool { 257 if nameFromArchive == "" { 258 return false 259 } 260 _, err := reference.ParseNormalizedNamed(nameFromArchive) 261 return err == nil 262 }), 263 } 264 265 imgs, err := i.client.Import(ctx, decompressed, opts...) 266 if err != nil { 267 log.G(ctx).WithError(err).Debug("failed to import image to containerd") 268 return errdefs.System(err) 269 } 270 271 progress := streamformatter.NewStdoutWriter(outStream) 272 273 for _, img := range imgs { 274 name := img.Name 275 loadedMsg := "Loaded image" 276 277 if isDanglingImage(img) { 278 name = img.Target.Digest.String() 279 loadedMsg = "Loaded image ID" 280 } else if named, err := reference.ParseNormalizedNamed(img.Name); err == nil { 281 name = reference.FamiliarString(reference.TagNameOnly(named)) 282 } 283 284 err = i.walkImageManifests(ctx, img, func(platformImg *ImageManifest) error { 285 logger := log.G(ctx).WithFields(log.Fields{ 286 "image": name, 287 "manifest": platformImg.Target().Digest, 288 }) 289 290 if isPseudo, err := platformImg.IsPseudoImage(ctx); isPseudo || err != nil { 291 if err != nil { 292 logger.WithError(err).Warn("failed to read manifest") 293 } else { 294 logger.Debug("don't unpack non-image manifest") 295 } 296 return nil 297 } 298 299 unpacked, err := platformImg.IsUnpacked(ctx, i.snapshotter) 300 if err != nil { 301 logger.WithError(err).Warn("failed to check if image is unpacked") 302 return nil 303 } 304 305 if !unpacked { 306 err = platformImg.Unpack(ctx, i.snapshotter) 307 308 if err != nil { 309 return errdefs.System(err) 310 } 311 } 312 logger.WithField("alreadyUnpacked", unpacked).WithError(err).Debug("unpack") 313 return nil 314 }) 315 if err != nil { 316 return errors.Wrap(err, "failed to unpack loaded image") 317 } 318 319 fmt.Fprintf(progress, "%s: %s\n", loadedMsg, name) 320 i.LogImageEvent(img.Target.Digest.String(), img.Target.Digest.String(), events.ActionLoad) 321 } 322 323 return nil 324 } 325 326 // getBestDescriptorForExport returns a descriptor which only references content available locally. 327 // The returned descriptor can be: 328 // - The same index descriptor - if all content is available 329 // - Platform specific manifest - if only one manifest from the whole index is available 330 // - Reduced index descriptor - if not all, but more than one manifest is available 331 // 332 // The reduced index descriptor is stored in the content store and may be garbage collected. 333 // It's advised to pass a context with a lease that's long enough to cover usage of the blob. 334 func (i *ImageService) getBestDescriptorForExport(ctx context.Context, indexDesc ocispec.Descriptor) (ocispec.Descriptor, error) { 335 none := ocispec.Descriptor{} 336 337 if !containerdimages.IsIndexType(indexDesc.MediaType) { 338 err := fmt.Errorf("index/manifest-list descriptor expected, got: %s", indexDesc.MediaType) 339 return none, errdefs.InvalidParameter(err) 340 } 341 store := i.client.ContentStore() 342 children, err := containerdimages.Children(ctx, store, indexDesc) 343 if err != nil { 344 if cerrdefs.IsNotFound(err) { 345 return none, errdefs.NotFound(err) 346 } 347 return none, errdefs.System(err) 348 } 349 350 // Check which platform manifests have all their blobs available. 351 hasMissingManifests := false 352 var presentManifests []ocispec.Descriptor 353 for _, mfst := range children { 354 if containerdimages.IsManifestType(mfst.MediaType) { 355 available, _, _, missing, err := containerdimages.Check(ctx, store, mfst, nil) 356 if err != nil { 357 hasMissingManifests = true 358 log.G(ctx).WithField("manifest", mfst.Digest).Warn("failed to check manifest's blob availability, won't export") 359 continue 360 } 361 362 if available && len(missing) == 0 { 363 presentManifests = append(presentManifests, mfst) 364 log.G(ctx).WithField("manifest", mfst.Digest).Debug("manifest content present, will export") 365 } else { 366 hasMissingManifests = true 367 log.G(ctx).WithFields(log.Fields{ 368 "manifest": mfst.Digest, 369 "missing": missing, 370 }).Debug("manifest is missing, won't export") 371 } 372 } 373 } 374 375 if !hasMissingManifests || len(children) == 0 { 376 // If we have the full image, or it has no manifests, just export the original index. 377 return indexDesc, nil 378 } else if len(presentManifests) == 1 { 379 // If only one platform is present, export that one manifest. 380 return presentManifests[0], nil 381 } else if len(presentManifests) == 0 { 382 // Return error when none of the image's manifest is present. 383 return none, errdefs.NotFound(fmt.Errorf("none of the manifests is fully present in the content store")) 384 } 385 386 // Create a new index which contains only the manifests we have in store. 387 index := ocispec.Index{ 388 Versioned: specs.Versioned{ 389 SchemaVersion: 2, 390 }, 391 MediaType: ocispec.MediaTypeImageIndex, 392 Manifests: presentManifests, 393 Annotations: indexDesc.Annotations, 394 } 395 396 reducedIndexDesc, err := storeJson(ctx, store, index.MediaType, index, nil) 397 if err != nil { 398 return none, err 399 } 400 401 return reducedIndexDesc, nil 402 }