image_builder.go 14 KB

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