image_exporter.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. package containerd
  2. import (
  3. "context"
  4. "fmt"
  5. "io"
  6. "github.com/containerd/containerd"
  7. "github.com/containerd/containerd/content"
  8. cerrdefs "github.com/containerd/containerd/errdefs"
  9. containerdimages "github.com/containerd/containerd/images"
  10. "github.com/containerd/containerd/images/archive"
  11. "github.com/containerd/containerd/leases"
  12. "github.com/containerd/containerd/log"
  13. "github.com/containerd/containerd/mount"
  14. cplatforms "github.com/containerd/containerd/platforms"
  15. "github.com/docker/distribution/reference"
  16. "github.com/docker/docker/container"
  17. "github.com/docker/docker/errdefs"
  18. "github.com/docker/docker/pkg/platforms"
  19. "github.com/docker/docker/pkg/streamformatter"
  20. "github.com/opencontainers/image-spec/specs-go"
  21. ocispec "github.com/opencontainers/image-spec/specs-go/v1"
  22. "github.com/pkg/errors"
  23. "github.com/sirupsen/logrus"
  24. )
  25. func (i *ImageService) PerformWithBaseFS(ctx context.Context, c *container.Container, fn func(root string) error) error {
  26. snapshotter := i.client.SnapshotService(c.Driver)
  27. mounts, err := snapshotter.Mounts(ctx, c.ID)
  28. if err != nil {
  29. return err
  30. }
  31. return mount.WithTempMount(ctx, mounts, fn)
  32. }
  33. // ExportImage exports a list of images to the given output stream. The
  34. // exported images are archived into a tar when written to the output
  35. // stream. All images with the given tag and all versions containing
  36. // the same tag are exported. names is the set of tags to export, and
  37. // outStream is the writer which the images are written to.
  38. //
  39. // TODO(thaJeztah): produce JSON stream progress response and image events; see https://github.com/moby/moby/issues/43910
  40. func (i *ImageService) ExportImage(ctx context.Context, names []string, outStream io.Writer) error {
  41. platform := platforms.AllPlatformsWithPreference(cplatforms.Default())
  42. opts := []archive.ExportOpt{
  43. archive.WithSkipNonDistributableBlobs(),
  44. // This makes the exported archive also include `manifest.json`
  45. // when the image is a manifest list. It is needed for backwards
  46. // compatibility with Docker image format.
  47. // The containerd will choose only one manifest for the `manifest.json`.
  48. // Our preference is to have it point to the default platform.
  49. // Example:
  50. // Daemon is running on linux/arm64
  51. // When we export linux/amd64 and linux/arm64, manifest.json will point to linux/arm64.
  52. // When we export linux/amd64 only, manifest.json will point to linux/amd64.
  53. // Note: This is only applicable if importing this archive into non-containerd Docker.
  54. // Importing the same archive into containerd, will not restrict the platforms.
  55. archive.WithPlatform(platform),
  56. }
  57. contentStore := i.client.ContentStore()
  58. leasesManager := i.client.LeasesService()
  59. lease, err := leasesManager.Create(ctx, leases.WithRandomID())
  60. if err != nil {
  61. return errdefs.System(err)
  62. }
  63. defer func() {
  64. if err := leasesManager.Delete(ctx, lease); err != nil {
  65. log.G(ctx).WithError(err).Warn("cleaning up lease")
  66. }
  67. }()
  68. for _, name := range names {
  69. target, err := i.resolveDescriptor(ctx, name)
  70. if err != nil {
  71. return err
  72. }
  73. if err = leaseContent(ctx, contentStore, leasesManager, lease, target); err != nil {
  74. return err
  75. }
  76. // We may not have locally all the platforms that are specified in the index.
  77. // Export only those manifests that we have.
  78. // TODO(vvoland): Reconsider this when `--platform` is added.
  79. if containerdimages.IsIndexType(target.MediaType) {
  80. desc, err := i.getBestDescriptorForExport(ctx, target)
  81. if err != nil {
  82. return err
  83. }
  84. target = desc
  85. }
  86. if ref, err := reference.ParseNormalizedNamed(name); err == nil {
  87. ref = reference.TagNameOnly(ref)
  88. opts = append(opts, archive.WithManifest(target, ref.String()))
  89. log.G(ctx).WithFields(logrus.Fields{
  90. "target": target,
  91. "name": ref.String(),
  92. }).Debug("export image")
  93. } else {
  94. opts = append(opts, archive.WithManifest(target))
  95. log.G(ctx).WithFields(logrus.Fields{
  96. "target": target,
  97. }).Debug("export image without name")
  98. }
  99. }
  100. return i.client.Export(ctx, outStream, opts...)
  101. }
  102. // leaseContent will add a resource to the lease for each child of the descriptor making sure that it and
  103. // its children won't be deleted while the lease exists
  104. func leaseContent(ctx context.Context, store content.Store, leasesManager leases.Manager, lease leases.Lease, desc ocispec.Descriptor) error {
  105. return containerdimages.Walk(ctx, containerdimages.HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
  106. _, err := store.Info(ctx, desc.Digest)
  107. if err != nil {
  108. if errors.Is(err, cerrdefs.ErrNotFound) {
  109. return nil, nil
  110. }
  111. return nil, errdefs.System(err)
  112. }
  113. r := leases.Resource{
  114. ID: desc.Digest.String(),
  115. Type: "content",
  116. }
  117. if err := leasesManager.AddResource(ctx, lease, r); err != nil {
  118. return nil, errdefs.System(err)
  119. }
  120. return containerdimages.Children(ctx, store, desc)
  121. }), desc)
  122. }
  123. // LoadImage uploads a set of images into the repository. This is the
  124. // complement of ExportImage. The input stream is an uncompressed tar
  125. // ball containing images and metadata.
  126. func (i *ImageService) LoadImage(ctx context.Context, inTar io.ReadCloser, outStream io.Writer, quiet bool) error {
  127. opts := []containerd.ImportOpt{
  128. // TODO(vvoland): Allow user to pass platform
  129. containerd.WithImportPlatform(cplatforms.All),
  130. // Create an additional image with dangling name for imported images...
  131. containerd.WithDigestRef(danglingImageName),
  132. // ... but only if they don't have a name or it's invalid.
  133. containerd.WithSkipDigestRef(func(nameFromArchive string) bool {
  134. if nameFromArchive == "" {
  135. return false
  136. }
  137. _, err := reference.ParseNormalizedNamed(nameFromArchive)
  138. return err == nil
  139. }),
  140. }
  141. imgs, err := i.client.Import(ctx, inTar, opts...)
  142. if err != nil {
  143. log.G(ctx).WithError(err).Debug("failed to import image to containerd")
  144. return errdefs.System(err)
  145. }
  146. progress := streamformatter.NewStdoutWriter(outStream)
  147. for _, img := range imgs {
  148. name := img.Name
  149. loadedMsg := "Loaded image"
  150. if isDanglingImage(img) {
  151. name = img.Target.Digest.String()
  152. loadedMsg = "Loaded image ID"
  153. } else if named, err := reference.ParseNormalizedNamed(img.Name); err == nil {
  154. name = reference.FamiliarName(reference.TagNameOnly(named))
  155. }
  156. err = i.walkImageManifests(ctx, img, func(platformImg *ImageManifest) error {
  157. logger := log.G(ctx).WithFields(logrus.Fields{
  158. "image": name,
  159. "manifest": platformImg.Target().Digest,
  160. })
  161. if isPseudo, err := platformImg.IsPseudoImage(ctx); isPseudo || err != nil {
  162. if err != nil {
  163. logger.WithError(err).Warn("failed to read manifest")
  164. } else {
  165. logger.Debug("don't unpack non-image manifest")
  166. }
  167. return nil
  168. }
  169. unpacked, err := platformImg.IsUnpacked(ctx, i.snapshotter)
  170. if err != nil {
  171. logger.WithError(err).Warn("failed to check if image is unpacked")
  172. return nil
  173. }
  174. if !unpacked {
  175. err = platformImg.Unpack(ctx, i.snapshotter)
  176. if err != nil {
  177. return errdefs.System(err)
  178. }
  179. }
  180. logger.WithField("alreadyUnpacked", unpacked).WithError(err).Debug("unpack")
  181. return nil
  182. })
  183. if err != nil {
  184. return errors.Wrap(err, "failed to unpack loaded image")
  185. }
  186. fmt.Fprintf(progress, "%s: %s\n", loadedMsg, name)
  187. i.LogImageEvent(img.Target.Digest.String(), img.Target.Digest.String(), "load")
  188. }
  189. return nil
  190. }
  191. // getBestDescriptorForExport returns a descriptor which only references content available locally.
  192. // The returned descriptor can be:
  193. // - The same index descriptor - if all content is available
  194. // - Platform specific manifest - if only one manifest from the whole index is available
  195. // - Reduced index descriptor - if not all, but more than one manifest is available
  196. //
  197. // The reduced index descriptor is stored in the content store and may be garbage collected.
  198. // It's advised to pass a context with a lease that's long enough to cover usage of the blob.
  199. func (i *ImageService) getBestDescriptorForExport(ctx context.Context, indexDesc ocispec.Descriptor) (ocispec.Descriptor, error) {
  200. none := ocispec.Descriptor{}
  201. if !containerdimages.IsIndexType(indexDesc.MediaType) {
  202. err := fmt.Errorf("index/manifest-list descriptor expected, got: %s", indexDesc.MediaType)
  203. return none, errdefs.InvalidParameter(err)
  204. }
  205. store := i.client.ContentStore()
  206. children, err := containerdimages.Children(ctx, store, indexDesc)
  207. if err != nil {
  208. if cerrdefs.IsNotFound(err) {
  209. return none, errdefs.NotFound(err)
  210. }
  211. return none, errdefs.System(err)
  212. }
  213. // Check which platform manifests have all their blobs available.
  214. hasMissingManifests := false
  215. var presentManifests []ocispec.Descriptor
  216. for _, mfst := range children {
  217. if containerdimages.IsManifestType(mfst.MediaType) {
  218. available, _, _, missing, err := containerdimages.Check(ctx, store, mfst, nil)
  219. if err != nil {
  220. hasMissingManifests = true
  221. log.G(ctx).WithField("manifest", mfst.Digest).Warn("failed to check manifest's blob availability, won't export")
  222. continue
  223. }
  224. if available && len(missing) == 0 {
  225. presentManifests = append(presentManifests, mfst)
  226. log.G(ctx).WithField("manifest", mfst.Digest).Debug("manifest content present, will export")
  227. } else {
  228. hasMissingManifests = true
  229. log.G(ctx).WithFields(logrus.Fields{
  230. "manifest": mfst.Digest,
  231. "missing": missing,
  232. }).Debug("manifest is missing, won't export")
  233. }
  234. }
  235. }
  236. if !hasMissingManifests || len(children) == 0 {
  237. // If we have the full image, or it has no manifests, just export the original index.
  238. return indexDesc, nil
  239. } else if len(presentManifests) == 1 {
  240. // If only one platform is present, export that one manifest.
  241. return presentManifests[0], nil
  242. } else if len(presentManifests) == 0 {
  243. // Return error when none of the image's manifest is present.
  244. return none, errdefs.NotFound(fmt.Errorf("none of the manifests is fully present in the content store"))
  245. }
  246. // Create a new index which contains only the manifests we have in store.
  247. index := ocispec.Index{
  248. Versioned: specs.Versioned{
  249. SchemaVersion: 2,
  250. },
  251. MediaType: ocispec.MediaTypeImageIndex,
  252. Manifests: presentManifests,
  253. Annotations: indexDesc.Annotations,
  254. }
  255. reducedIndexDesc, err := storeJson(ctx, store, index.MediaType, index, nil)
  256. if err != nil {
  257. return none, err
  258. }
  259. return reducedIndexDesc, nil
  260. }