github.com/moby/docker@v26.1.3+incompatible/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 "github.com/containerd/containerd/platforms" 16 "github.com/containerd/log" 17 "github.com/distribution/reference" 18 "github.com/docker/docker/api/types/events" 19 "github.com/docker/docker/container" 20 "github.com/docker/docker/daemon/images" 21 "github.com/docker/docker/errdefs" 22 dockerarchive "github.com/docker/docker/pkg/archive" 23 "github.com/docker/docker/pkg/streamformatter" 24 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 25 "github.com/pkg/errors" 26 ) 27 28 func (i *ImageService) PerformWithBaseFS(ctx context.Context, c *container.Container, fn func(root string) error) error { 29 snapshotter := i.client.SnapshotService(c.Driver) 30 mounts, err := snapshotter.Mounts(ctx, c.ID) 31 if err != nil { 32 return err 33 } 34 path, err := i.refCountMounter.Mount(mounts, c.ID) 35 if err != nil { 36 return err 37 } 38 defer i.refCountMounter.Unmount(path) 39 40 return fn(path) 41 } 42 43 // ExportImage exports a list of images to the given output stream. The 44 // exported images are archived into a tar when written to the output 45 // stream. All images with the given tag and all versions containing 46 // the same tag are exported. names is the set of tags to export, and 47 // outStream is the writer which the images are written to. 48 // 49 // TODO(thaJeztah): produce JSON stream progress response and image events; see https://github.com/moby/moby/issues/43910 50 func (i *ImageService) ExportImage(ctx context.Context, names []string, outStream io.Writer) error { 51 platform := matchAllWithPreference(platforms.Default()) 52 opts := []archive.ExportOpt{ 53 archive.WithSkipNonDistributableBlobs(), 54 55 // This makes the exported archive also include `manifest.json` 56 // when the image is a manifest list. It is needed for backwards 57 // compatibility with Docker image format. 58 // The containerd will choose only one manifest for the `manifest.json`. 59 // Our preference is to have it point to the default platform. 60 // Example: 61 // Daemon is running on linux/arm64 62 // When we export linux/amd64 and linux/arm64, manifest.json will point to linux/arm64. 63 // When we export linux/amd64 only, manifest.json will point to linux/amd64. 64 // Note: This is only applicable if importing this archive into non-containerd Docker. 65 // Importing the same archive into containerd, will not restrict the platforms. 66 archive.WithPlatform(platform), 67 archive.WithSkipMissing(i.content), 68 } 69 70 leasesManager := i.client.LeasesService() 71 lease, err := leasesManager.Create(ctx, leases.WithRandomID()) 72 if err != nil { 73 return errdefs.System(err) 74 } 75 defer func() { 76 if err := leasesManager.Delete(ctx, lease); err != nil { 77 log.G(ctx).WithError(err).Warn("cleaning up lease") 78 } 79 }() 80 81 addLease := func(ctx context.Context, target ocispec.Descriptor) error { 82 return leaseContent(ctx, i.content, leasesManager, lease, target) 83 } 84 85 exportImage := func(ctx context.Context, target ocispec.Descriptor, ref reference.Named) error { 86 if err := addLease(ctx, target); err != nil { 87 return err 88 } 89 90 if ref != nil { 91 opts = append(opts, archive.WithManifest(target, ref.String())) 92 93 log.G(ctx).WithFields(log.Fields{ 94 "target": target, 95 "name": ref, 96 }).Debug("export image") 97 } else { 98 orgTarget := target 99 target.Annotations = make(map[string]string) 100 101 for k, v := range orgTarget.Annotations { 102 switch k { 103 case containerdimages.AnnotationImageName, ocispec.AnnotationRefName: 104 // Strip image name/tag annotations from the descriptor. 105 // Otherwise containerd will use it as name. 106 default: 107 target.Annotations[k] = v 108 } 109 } 110 111 opts = append(opts, archive.WithManifest(target)) 112 113 log.G(ctx).WithFields(log.Fields{ 114 "target": target, 115 }).Debug("export image without name") 116 } 117 118 i.LogImageEvent(target.Digest.String(), target.Digest.String(), events.ActionSave) 119 return nil 120 } 121 122 exportRepository := func(ctx context.Context, ref reference.Named) error { 123 imgs, err := i.getAllImagesWithRepository(ctx, ref) 124 if err != nil { 125 return errdefs.System(fmt.Errorf("failed to list all images from repository %s: %w", ref.Name(), err)) 126 } 127 128 if len(imgs) == 0 { 129 return images.ErrImageDoesNotExist{Ref: ref} 130 } 131 132 for _, img := range imgs { 133 ref, err := reference.ParseNamed(img.Name) 134 135 if err != nil { 136 log.G(ctx).WithFields(log.Fields{ 137 "image": img.Name, 138 "error": err, 139 }).Warn("couldn't parse image name as a valid named reference") 140 continue 141 } 142 143 if err := exportImage(ctx, img.Target, ref); err != nil { 144 return err 145 } 146 } 147 148 return nil 149 } 150 151 for _, name := range names { 152 target, resolveErr := i.resolveDescriptor(ctx, name) 153 154 // Check if the requested name is a truncated digest of the resolved descriptor. 155 // If yes, that means that the user specified a specific image ID so 156 // it's not referencing a repository. 157 specificDigestResolved := false 158 if resolveErr == nil { 159 nameWithoutDigestAlgorithm := strings.TrimPrefix(name, target.Digest.Algorithm().String()+":") 160 specificDigestResolved = strings.HasPrefix(target.Digest.Encoded(), nameWithoutDigestAlgorithm) 161 } 162 163 log.G(ctx).WithFields(log.Fields{ 164 "name": name, 165 "resolveErr": resolveErr, 166 "specificDigestResolved": specificDigestResolved, 167 }).Debug("export requested") 168 169 ref, refErr := reference.ParseNormalizedNamed(name) 170 171 if refErr == nil { 172 if _, ok := ref.(reference.Digested); ok { 173 specificDigestResolved = true 174 } 175 } 176 177 if resolveErr != nil || !specificDigestResolved { 178 // Name didn't resolve to anything, or name wasn't explicitly referencing a digest 179 if refErr == nil && reference.IsNameOnly(ref) { 180 // Reference is valid, but doesn't include a specific tag. 181 // Export all images with the same repository. 182 if err := exportRepository(ctx, ref); err != nil { 183 return err 184 } 185 continue 186 } 187 } 188 189 if resolveErr != nil { 190 return resolveErr 191 } 192 if refErr != nil { 193 return refErr 194 } 195 196 // If user exports a specific digest, it shouldn't have a tag. 197 if specificDigestResolved { 198 ref = nil 199 } 200 if err := exportImage(ctx, target, ref); err != nil { 201 return err 202 } 203 } 204 205 return i.client.Export(ctx, outStream, opts...) 206 } 207 208 // leaseContent will add a resource to the lease for each child of the descriptor making sure that it and 209 // its children won't be deleted while the lease exists 210 func leaseContent(ctx context.Context, store content.Store, leasesManager leases.Manager, lease leases.Lease, desc ocispec.Descriptor) error { 211 return containerdimages.Walk(ctx, containerdimages.HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { 212 _, err := store.Info(ctx, desc.Digest) 213 if err != nil { 214 if errors.Is(err, cerrdefs.ErrNotFound) { 215 return nil, nil 216 } 217 return nil, errdefs.System(err) 218 } 219 220 r := leases.Resource{ 221 ID: desc.Digest.String(), 222 Type: "content", 223 } 224 if err := leasesManager.AddResource(ctx, lease, r); err != nil { 225 return nil, errdefs.System(err) 226 } 227 228 return containerdimages.Children(ctx, store, desc) 229 }), desc) 230 } 231 232 // LoadImage uploads a set of images into the repository. This is the 233 // complement of ExportImage. The input stream is an uncompressed tar 234 // ball containing images and metadata. 235 func (i *ImageService) LoadImage(ctx context.Context, inTar io.ReadCloser, outStream io.Writer, quiet bool) error { 236 decompressed, err := dockerarchive.DecompressStream(inTar) 237 if err != nil { 238 return errors.Wrap(err, "failed to decompress input tar archive") 239 } 240 defer decompressed.Close() 241 242 opts := []containerd.ImportOpt{ 243 // TODO(vvoland): Allow user to pass platform 244 containerd.WithImportPlatform(platforms.All), 245 246 containerd.WithSkipMissing(), 247 248 // Create an additional image with dangling name for imported images... 249 containerd.WithDigestRef(danglingImageName), 250 // ... but only if they don't have a name or it's invalid. 251 containerd.WithSkipDigestRef(func(nameFromArchive string) bool { 252 if nameFromArchive == "" { 253 return false 254 } 255 _, err := reference.ParseNormalizedNamed(nameFromArchive) 256 return err == nil 257 }), 258 } 259 260 imgs, err := i.client.Import(ctx, decompressed, opts...) 261 if err != nil { 262 log.G(ctx).WithError(err).Debug("failed to import image to containerd") 263 return errdefs.System(err) 264 } 265 266 progress := streamformatter.NewStdoutWriter(outStream) 267 268 for _, img := range imgs { 269 name := img.Name 270 loadedMsg := "Loaded image" 271 272 if isDanglingImage(img) { 273 name = img.Target.Digest.String() 274 loadedMsg = "Loaded image ID" 275 } else if named, err := reference.ParseNormalizedNamed(img.Name); err == nil { 276 name = reference.FamiliarString(reference.TagNameOnly(named)) 277 } 278 279 err = i.walkImageManifests(ctx, img, func(platformImg *ImageManifest) error { 280 logger := log.G(ctx).WithFields(log.Fields{ 281 "image": name, 282 "manifest": platformImg.Target().Digest, 283 }) 284 285 if isPseudo, err := platformImg.IsPseudoImage(ctx); isPseudo || err != nil { 286 if err != nil { 287 logger.WithError(err).Warn("failed to read manifest") 288 } else { 289 logger.Debug("don't unpack non-image manifest") 290 } 291 return nil 292 } 293 294 unpacked, err := platformImg.IsUnpacked(ctx, i.snapshotter) 295 if err != nil { 296 logger.WithError(err).Warn("failed to check if image is unpacked") 297 return nil 298 } 299 300 if !unpacked { 301 err = platformImg.Unpack(ctx, i.snapshotter) 302 303 if err != nil { 304 return errdefs.System(err) 305 } 306 } 307 logger.WithField("alreadyUnpacked", unpacked).WithError(err).Debug("unpack") 308 return nil 309 }) 310 if err != nil { 311 return errors.Wrap(err, "failed to unpack loaded image") 312 } 313 314 fmt.Fprintf(progress, "%s: %s\n", loadedMsg, name) 315 i.LogImageEvent(img.Target.Digest.String(), img.Target.Digest.String(), events.ActionLoad) 316 } 317 318 return nil 319 }