image_exporter.go 13 KB

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