image_builder.go 13 KB

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