image_builder.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  1. package containerd
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "io"
  7. "os"
  8. "runtime"
  9. "time"
  10. "github.com/containerd/containerd"
  11. "github.com/containerd/containerd/content"
  12. cerrdefs "github.com/containerd/containerd/errdefs"
  13. "github.com/containerd/containerd/leases"
  14. "github.com/containerd/containerd/mount"
  15. "github.com/containerd/containerd/platforms"
  16. "github.com/containerd/containerd/rootfs"
  17. "github.com/distribution/reference"
  18. "github.com/docker/docker/api/types/backend"
  19. imagetypes "github.com/docker/docker/api/types/image"
  20. "github.com/docker/docker/api/types/registry"
  21. registrypkg "github.com/docker/docker/registry"
  22. // "github.com/docker/docker/api/types/container"
  23. containerdimages "github.com/containerd/containerd/images"
  24. "github.com/containerd/containerd/log"
  25. "github.com/docker/docker/api/types/image"
  26. "github.com/docker/docker/builder"
  27. "github.com/docker/docker/errdefs"
  28. dimage "github.com/docker/docker/image"
  29. "github.com/docker/docker/layer"
  30. "github.com/docker/docker/pkg/archive"
  31. "github.com/docker/docker/pkg/progress"
  32. "github.com/docker/docker/pkg/streamformatter"
  33. "github.com/docker/docker/pkg/stringid"
  34. "github.com/opencontainers/go-digest"
  35. "github.com/opencontainers/image-spec/identity"
  36. ocispec "github.com/opencontainers/image-spec/specs-go/v1"
  37. )
  38. const imageLabelClassicBuilderParent = "org.mobyproject.image.parent"
  39. // GetImageAndReleasableLayer returns an image and releaseable layer for a
  40. // reference or ID. Every call to GetImageAndReleasableLayer MUST call
  41. // releasableLayer.Release() to prevent leaking of layers.
  42. func (i *ImageService) GetImageAndReleasableLayer(ctx context.Context, refOrID string, opts backend.GetImageAndLayerOptions) (builder.Image, builder.ROLayer, error) {
  43. if refOrID == "" { // FROM scratch
  44. if runtime.GOOS == "windows" {
  45. return nil, nil, fmt.Errorf(`"FROM scratch" is not supported on Windows`)
  46. }
  47. if opts.Platform != nil {
  48. if err := dimage.CheckOS(opts.Platform.OS); err != nil {
  49. return nil, nil, err
  50. }
  51. }
  52. return nil, &rolayer{
  53. c: i.client,
  54. snapshotter: i.snapshotter,
  55. }, nil
  56. }
  57. if opts.PullOption != backend.PullOptionForcePull {
  58. // TODO(laurazard): same as below
  59. img, err := i.GetImage(ctx, refOrID, image.GetImageOpts{Platform: opts.Platform})
  60. if err != nil && opts.PullOption == backend.PullOptionNoPull {
  61. return nil, nil, err
  62. }
  63. imgDesc, err := i.resolveDescriptor(ctx, refOrID)
  64. if err != nil && !errdefs.IsNotFound(err) {
  65. return nil, nil, err
  66. }
  67. if img != nil {
  68. if err := dimage.CheckOS(img.OperatingSystem()); err != nil {
  69. return nil, nil, err
  70. }
  71. roLayer, err := newROLayerForImage(ctx, &imgDesc, i, opts.Platform)
  72. if err != nil {
  73. return nil, nil, err
  74. }
  75. return img, roLayer, nil
  76. }
  77. }
  78. ctx, _, err := i.client.WithLease(ctx, leases.WithRandomID(), leases.WithExpiration(1*time.Hour))
  79. if err != nil {
  80. return nil, nil, fmt.Errorf("failed to create lease for commit: %w", err)
  81. }
  82. // TODO(laurazard): do we really need a new method here to pull the image?
  83. imgDesc, err := i.pullForBuilder(ctx, refOrID, opts.AuthConfig, opts.Output, opts.Platform)
  84. if err != nil {
  85. return nil, nil, err
  86. }
  87. // TODO(laurazard): pullForBuilder should return whatever we
  88. // need here instead of having to go and get it again
  89. img, err := i.GetImage(ctx, refOrID, imagetypes.GetImageOpts{
  90. Platform: opts.Platform,
  91. })
  92. if err != nil {
  93. return nil, nil, err
  94. }
  95. roLayer, err := newROLayerForImage(ctx, imgDesc, i, opts.Platform)
  96. if err != nil {
  97. return nil, nil, err
  98. }
  99. return img, roLayer, nil
  100. }
  101. func (i *ImageService) pullForBuilder(ctx context.Context, name string, authConfigs map[string]registry.AuthConfig, output io.Writer, platform *ocispec.Platform) (*ocispec.Descriptor, error) {
  102. ref, err := reference.ParseNormalizedNamed(name)
  103. if err != nil {
  104. return nil, err
  105. }
  106. taggedRef := reference.TagNameOnly(ref)
  107. pullRegistryAuth := &registry.AuthConfig{}
  108. if len(authConfigs) > 0 {
  109. // The request came with a full auth config, use it
  110. repoInfo, err := i.registryService.ResolveRepository(ref)
  111. if err != nil {
  112. return nil, err
  113. }
  114. resolvedConfig := registrypkg.ResolveAuthConfig(authConfigs, repoInfo.Index)
  115. pullRegistryAuth = &resolvedConfig
  116. }
  117. if err := i.PullImage(ctx, ref.Name(), taggedRef.(reference.NamedTagged).Tag(), platform, nil, pullRegistryAuth, output); err != nil {
  118. return nil, err
  119. }
  120. img, err := i.GetImage(ctx, name, imagetypes.GetImageOpts{Platform: platform})
  121. if err != nil {
  122. if errdefs.IsNotFound(err) && img != nil && platform != nil {
  123. imgPlat := ocispec.Platform{
  124. OS: img.OS,
  125. Architecture: img.BaseImgArch(),
  126. Variant: img.BaseImgVariant(),
  127. }
  128. p := *platform
  129. if !platforms.Only(p).Match(imgPlat) {
  130. po := streamformatter.NewJSONProgressOutput(output, false)
  131. progress.Messagef(po, "", `
  132. WARNING: Pulled image with specified platform (%s), but the resulting image's configured platform (%s) does not match.
  133. This is most likely caused by a bug in the build system that created the fetched image (%s).
  134. Please notify the image author to correct the configuration.`,
  135. platforms.Format(p), platforms.Format(imgPlat), name,
  136. )
  137. log.G(ctx).WithError(err).WithField("image", name).Warn("Ignoring error about platform mismatch where the manifest list points to an image whose configuration does not match the platform in the manifest.")
  138. }
  139. } else {
  140. return nil, err
  141. }
  142. }
  143. if err := dimage.CheckOS(img.OperatingSystem()); err != nil {
  144. return nil, err
  145. }
  146. imgDesc, err := i.resolveDescriptor(ctx, name)
  147. if err != nil {
  148. return nil, err
  149. }
  150. return &imgDesc, err
  151. }
  152. func newROLayerForImage(ctx context.Context, imgDesc *ocispec.Descriptor, i *ImageService, platform *ocispec.Platform) (builder.ROLayer, error) {
  153. if imgDesc == nil {
  154. return nil, fmt.Errorf("can't make an RO layer for a nil image :'(")
  155. }
  156. platMatcher := platforms.Default()
  157. if platform != nil {
  158. platMatcher = platforms.Only(*platform)
  159. }
  160. confDesc, err := containerdimages.Config(ctx, i.client.ContentStore(), *imgDesc, platMatcher)
  161. if err != nil {
  162. return nil, err
  163. }
  164. diffIDs, err := containerdimages.RootFS(ctx, i.client.ContentStore(), confDesc)
  165. if err != nil {
  166. return nil, err
  167. }
  168. // TODO(vvoland): Check if image is unpacked, and unpack it if it's not.
  169. imageSnapshotID := identity.ChainID(diffIDs).String()
  170. snapshotter := i.StorageDriver()
  171. _, lease, err := createLease(ctx, i.client.LeasesService())
  172. if err != nil {
  173. return nil, errdefs.System(fmt.Errorf("failed to lease image snapshot %s: %w", imageSnapshotID, err))
  174. }
  175. return &rolayer{
  176. key: imageSnapshotID,
  177. c: i.client,
  178. snapshotter: snapshotter,
  179. diffID: "", // Image RO layer doesn't have a diff.
  180. contentStoreDigest: "",
  181. lease: &lease,
  182. }, nil
  183. }
  184. func createLease(ctx context.Context, lm leases.Manager) (context.Context, leases.Lease, error) {
  185. lease, err := lm.Create(ctx,
  186. leases.WithExpiration(time.Hour*24),
  187. leases.WithLabels(map[string]string{
  188. "org.mobyproject.lease.classicbuilder": "true",
  189. }),
  190. )
  191. if err != nil {
  192. return nil, leases.Lease{}, fmt.Errorf("failed to create a lease for snapshot: %w", err)
  193. }
  194. return leases.WithLease(ctx, lease.ID), lease, nil
  195. }
  196. type rolayer struct {
  197. key string
  198. c *containerd.Client
  199. snapshotter string
  200. diffID digest.Digest
  201. contentStoreDigest digest.Digest
  202. lease *leases.Lease
  203. }
  204. func (rl *rolayer) ContentStoreDigest() digest.Digest {
  205. return rl.contentStoreDigest
  206. }
  207. func (rl *rolayer) DiffID() layer.DiffID {
  208. if rl.diffID == "" {
  209. return layer.DigestSHA256EmptyTar
  210. }
  211. return layer.DiffID(rl.diffID)
  212. }
  213. func (rl *rolayer) Release() error {
  214. if rl.lease != nil {
  215. lm := rl.c.LeasesService()
  216. err := lm.Delete(context.TODO(), *rl.lease)
  217. if err != nil {
  218. return err
  219. }
  220. rl.lease = nil
  221. }
  222. return nil
  223. }
  224. // NewRWLayer creates a new read-write layer for the builder
  225. func (rl *rolayer) NewRWLayer() (_ builder.RWLayer, outErr error) {
  226. snapshotter := rl.c.SnapshotService(rl.snapshotter)
  227. key := stringid.GenerateRandomID()
  228. ctx, lease, err := createLease(context.TODO(), rl.c.LeasesService())
  229. if err != nil {
  230. return nil, err
  231. }
  232. defer func() {
  233. if outErr != nil {
  234. if err := rl.c.LeasesService().Delete(ctx, lease); err != nil {
  235. log.G(ctx).WithError(err).Warn("failed to remove lease after NewRWLayer error")
  236. }
  237. }
  238. }()
  239. mounts, err := snapshotter.Prepare(ctx, key, rl.key)
  240. if err != nil {
  241. return nil, err
  242. }
  243. root, err := os.MkdirTemp(os.TempDir(), "rootfs-mount")
  244. if err != nil {
  245. return nil, err
  246. }
  247. if err := mount.All(mounts, root); err != nil {
  248. return nil, err
  249. }
  250. return &rwlayer{
  251. key: key,
  252. parent: rl.key,
  253. c: rl.c,
  254. snapshotter: rl.snapshotter,
  255. root: root,
  256. lease: &lease,
  257. }, nil
  258. }
  259. type rwlayer struct {
  260. key string
  261. parent string
  262. c *containerd.Client
  263. snapshotter string
  264. root string
  265. lease *leases.Lease
  266. }
  267. func (rw *rwlayer) Root() string {
  268. return rw.root
  269. }
  270. func (rw *rwlayer) Commit() (_ builder.ROLayer, outErr error) {
  271. snapshotter := rw.c.SnapshotService(rw.snapshotter)
  272. key := stringid.GenerateRandomID()
  273. lm := rw.c.LeasesService()
  274. ctx, lease, err := createLease(context.TODO(), lm)
  275. if err != nil {
  276. return nil, err
  277. }
  278. defer func() {
  279. if outErr != nil {
  280. if err := lm.Delete(ctx, lease); err != nil {
  281. log.G(ctx).WithError(err).Warn("failed to remove lease after NewRWLayer error")
  282. }
  283. }
  284. }()
  285. err = snapshotter.Commit(ctx, key, rw.key)
  286. if err != nil && !cerrdefs.IsAlreadyExists(err) {
  287. return nil, err
  288. }
  289. differ := rw.c.DiffService()
  290. desc, err := rootfs.CreateDiff(ctx, key, snapshotter, differ)
  291. if err != nil {
  292. return nil, err
  293. }
  294. info, err := rw.c.ContentStore().Info(ctx, desc.Digest)
  295. if err != nil {
  296. return nil, err
  297. }
  298. diffIDStr, ok := info.Labels["containerd.io/uncompressed"]
  299. if !ok {
  300. return nil, fmt.Errorf("invalid differ response with no diffID")
  301. }
  302. diffID, err := digest.Parse(diffIDStr)
  303. if err != nil {
  304. return nil, err
  305. }
  306. return &rolayer{
  307. key: key,
  308. c: rw.c,
  309. snapshotter: rw.snapshotter,
  310. diffID: diffID,
  311. contentStoreDigest: desc.Digest,
  312. lease: &lease,
  313. }, nil
  314. }
  315. func (rw *rwlayer) Release() (outErr error) {
  316. if rw.root == "" { // nothing to release
  317. return nil
  318. }
  319. if err := mount.UnmountAll(rw.root, 0); err != nil && !errors.Is(err, os.ErrNotExist) {
  320. log.G(context.TODO()).WithError(err).WithField("root", rw.root).Error("failed to unmount ROLayer")
  321. return err
  322. }
  323. if err := os.Remove(rw.root); err != nil && !errors.Is(err, os.ErrNotExist) {
  324. log.G(context.TODO()).WithError(err).WithField("dir", rw.root).Error("failed to remove mount temp dir")
  325. return err
  326. }
  327. rw.root = ""
  328. if rw.lease != nil {
  329. lm := rw.c.LeasesService()
  330. err := lm.Delete(context.TODO(), *rw.lease)
  331. if err != nil {
  332. log.G(context.TODO()).WithError(err).Warn("failed to delete lease when releasing RWLayer")
  333. } else {
  334. rw.lease = nil
  335. }
  336. }
  337. return nil
  338. }
  339. // CreateImage creates a new image by adding a config and ID to the image store.
  340. // This is similar to LoadImage() except that it receives JSON encoded bytes of
  341. // an image instead of a tar archive.
  342. func (i *ImageService) CreateImage(ctx context.Context, config []byte, parent string, layerDigest digest.Digest) (builder.Image, error) {
  343. imgToCreate, err := dimage.NewFromJSON(config)
  344. if err != nil {
  345. return nil, err
  346. }
  347. ociImgToCreate := dockerImageToDockerOCIImage(*imgToCreate)
  348. var layers []ocispec.Descriptor
  349. var parentDigest digest.Digest
  350. // if the image has a parent, we need to start with the parents layers descriptors
  351. if parent != "" {
  352. parentDesc, err := i.resolveDescriptor(ctx, parent)
  353. if err != nil {
  354. return nil, err
  355. }
  356. parentImageManifest, err := containerdimages.Manifest(ctx, i.client.ContentStore(), parentDesc, platforms.Default())
  357. if err != nil {
  358. return nil, err
  359. }
  360. layers = parentImageManifest.Layers
  361. parentDigest = parentDesc.Digest
  362. }
  363. cs := i.client.ContentStore()
  364. ra, err := cs.ReaderAt(ctx, ocispec.Descriptor{Digest: layerDigest})
  365. if err != nil {
  366. return nil, fmt.Errorf("failed to read diff archive: %w", err)
  367. }
  368. defer ra.Close()
  369. empty, err := archive.IsEmpty(content.NewReader(ra))
  370. if err != nil {
  371. return nil, fmt.Errorf("failed to check if archive is empty: %w", err)
  372. }
  373. if !empty {
  374. info, err := cs.Info(ctx, layerDigest)
  375. if err != nil {
  376. return nil, err
  377. }
  378. layers = append(layers, ocispec.Descriptor{
  379. MediaType: containerdimages.MediaTypeDockerSchema2LayerGzip,
  380. Digest: layerDigest,
  381. Size: info.Size,
  382. })
  383. }
  384. // necessary to prevent the contents from being GC'd
  385. // between writing them here and creating an image
  386. ctx, release, err := i.client.WithLease(ctx, leases.WithRandomID(), leases.WithExpiration(1*time.Hour))
  387. if err != nil {
  388. return nil, err
  389. }
  390. defer func() {
  391. if err := release(ctx); err != nil {
  392. log.G(ctx).WithError(err).Warn("failed to release lease created for create")
  393. }
  394. }()
  395. commitManifestDesc, err := writeContentsForImage(ctx, i.snapshotter, i.client.ContentStore(), ociImgToCreate, layers)
  396. if err != nil {
  397. return nil, err
  398. }
  399. // image create
  400. img := containerdimages.Image{
  401. Name: danglingImageName(commitManifestDesc.Digest),
  402. Target: commitManifestDesc,
  403. CreatedAt: time.Now(),
  404. Labels: map[string]string{
  405. imageLabelClassicBuilderParent: parentDigest.String(),
  406. },
  407. }
  408. createdImage, err := i.client.ImageService().Update(ctx, img)
  409. if err != nil {
  410. if !cerrdefs.IsNotFound(err) {
  411. return nil, err
  412. }
  413. if createdImage, err = i.client.ImageService().Create(ctx, img); err != nil {
  414. return nil, fmt.Errorf("failed to create new image: %w", err)
  415. }
  416. }
  417. if err := i.unpackImage(ctx, i.StorageDriver(), img, commitManifestDesc); err != nil {
  418. return nil, err
  419. }
  420. newImage := dimage.Clone(imgToCreate, dimage.ID(createdImage.Target.Digest))
  421. return newImage, nil
  422. }