image_builder.go 14 KB

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