image_builder.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617
  1. package containerd
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/json"
  6. "errors"
  7. "fmt"
  8. "io"
  9. "os"
  10. "runtime"
  11. "time"
  12. "github.com/containerd/containerd"
  13. "github.com/containerd/containerd/content"
  14. cerrdefs "github.com/containerd/containerd/errdefs"
  15. containerdimages "github.com/containerd/containerd/images"
  16. "github.com/containerd/containerd/leases"
  17. "github.com/containerd/containerd/mount"
  18. "github.com/containerd/containerd/platforms"
  19. "github.com/containerd/containerd/rootfs"
  20. "github.com/containerd/log"
  21. "github.com/distribution/reference"
  22. "github.com/docker/docker/api/types/backend"
  23. "github.com/docker/docker/api/types/container"
  24. imagetypes "github.com/docker/docker/api/types/image"
  25. "github.com/docker/docker/api/types/registry"
  26. "github.com/docker/docker/builder"
  27. "github.com/docker/docker/errdefs"
  28. dimage "github.com/docker/docker/image"
  29. imagespec "github.com/docker/docker/image/spec/specs-go/v1"
  30. "github.com/docker/docker/internal/compatcontext"
  31. "github.com/docker/docker/layer"
  32. "github.com/docker/docker/pkg/archive"
  33. "github.com/docker/docker/pkg/progress"
  34. "github.com/docker/docker/pkg/streamformatter"
  35. "github.com/docker/docker/pkg/stringid"
  36. registrypkg "github.com/docker/docker/registry"
  37. "github.com/opencontainers/go-digest"
  38. "github.com/opencontainers/image-spec/identity"
  39. "github.com/opencontainers/image-spec/specs-go"
  40. ocispec "github.com/opencontainers/image-spec/specs-go/v1"
  41. )
  42. const (
  43. // Digest of the image which was the base image of the committed container.
  44. imageLabelClassicBuilderParent = "org.mobyproject.image.parent"
  45. // "1" means that the image was created directly from the "FROM scratch".
  46. imageLabelClassicBuilderFromScratch = "org.mobyproject.image.fromscratch"
  47. // digest of the ContainerConfig stored in the content store.
  48. imageLabelClassicBuilderContainerConfig = "org.mobyproject.image.containerconfig"
  49. )
  50. const (
  51. // gc.ref label that associates the ContainerConfig content blob with the
  52. // corresponding Config content.
  53. contentLabelGcRefContainerConfig = "containerd.io/gc.ref.content.moby/container.config"
  54. // Digest of the image this ContainerConfig blobs describes.
  55. // Only ContainerConfig content should be labelled with it.
  56. contentLabelClassicBuilderImage = "org.mobyproject.content.image"
  57. )
  58. // GetImageAndReleasableLayer returns an image and releaseable layer for a
  59. // reference or ID. Every call to GetImageAndReleasableLayer MUST call
  60. // releasableLayer.Release() to prevent leaking of layers.
  61. func (i *ImageService) GetImageAndReleasableLayer(ctx context.Context, refOrID string, opts backend.GetImageAndLayerOptions) (builder.Image, builder.ROLayer, error) {
  62. if refOrID == "" { // FROM scratch
  63. if runtime.GOOS == "windows" {
  64. return nil, nil, fmt.Errorf(`"FROM scratch" is not supported on Windows`)
  65. }
  66. if opts.Platform != nil {
  67. if err := dimage.CheckOS(opts.Platform.OS); err != nil {
  68. return nil, nil, err
  69. }
  70. }
  71. return nil, &rolayer{
  72. c: i.client,
  73. snapshotter: i.snapshotter,
  74. }, nil
  75. }
  76. if opts.PullOption != backend.PullOptionForcePull {
  77. // TODO(laurazard): same as below
  78. img, err := i.GetImage(ctx, refOrID, imagetypes.GetImageOpts{Platform: opts.Platform})
  79. if err != nil && opts.PullOption == backend.PullOptionNoPull {
  80. return nil, nil, err
  81. }
  82. imgDesc, err := i.resolveDescriptor(ctx, refOrID)
  83. if err != nil && !errdefs.IsNotFound(err) {
  84. return nil, nil, err
  85. }
  86. if img != nil {
  87. if err := dimage.CheckOS(img.OperatingSystem()); err != nil {
  88. return nil, nil, err
  89. }
  90. roLayer, err := newROLayerForImage(ctx, &imgDesc, i, opts.Platform)
  91. if err != nil {
  92. return nil, nil, err
  93. }
  94. return img, roLayer, nil
  95. }
  96. }
  97. ctx, _, err := i.client.WithLease(ctx, leases.WithRandomID(), leases.WithExpiration(1*time.Hour))
  98. if err != nil {
  99. return nil, nil, fmt.Errorf("failed to create lease for commit: %w", err)
  100. }
  101. // TODO(laurazard): do we really need a new method here to pull the image?
  102. imgDesc, err := i.pullForBuilder(ctx, refOrID, opts.AuthConfig, opts.Output, opts.Platform)
  103. if err != nil {
  104. return nil, nil, err
  105. }
  106. // TODO(laurazard): pullForBuilder should return whatever we
  107. // need here instead of having to go and get it again
  108. img, err := i.GetImage(ctx, refOrID, imagetypes.GetImageOpts{
  109. Platform: opts.Platform,
  110. })
  111. if err != nil {
  112. return nil, nil, err
  113. }
  114. roLayer, err := newROLayerForImage(ctx, imgDesc, i, opts.Platform)
  115. if err != nil {
  116. return nil, nil, err
  117. }
  118. return img, roLayer, nil
  119. }
  120. func (i *ImageService) pullForBuilder(ctx context.Context, name string, authConfigs map[string]registry.AuthConfig, output io.Writer, platform *ocispec.Platform) (*ocispec.Descriptor, error) {
  121. ref, err := reference.ParseNormalizedNamed(name)
  122. if err != nil {
  123. return nil, err
  124. }
  125. pullRegistryAuth := &registry.AuthConfig{}
  126. if len(authConfigs) > 0 {
  127. // The request came with a full auth config, use it
  128. repoInfo, err := i.registryService.ResolveRepository(ref)
  129. if err != nil {
  130. return nil, err
  131. }
  132. resolvedConfig := registrypkg.ResolveAuthConfig(authConfigs, repoInfo.Index)
  133. pullRegistryAuth = &resolvedConfig
  134. }
  135. if err := i.PullImage(ctx, reference.TagNameOnly(ref), platform, nil, pullRegistryAuth, output); err != nil {
  136. return nil, err
  137. }
  138. img, err := i.GetImage(ctx, name, imagetypes.GetImageOpts{Platform: platform})
  139. if err != nil {
  140. if errdefs.IsNotFound(err) && img != nil && platform != nil {
  141. imgPlat := ocispec.Platform{
  142. OS: img.OS,
  143. Architecture: img.BaseImgArch(),
  144. Variant: img.BaseImgVariant(),
  145. }
  146. p := *platform
  147. if !platforms.Only(p).Match(imgPlat) {
  148. po := streamformatter.NewJSONProgressOutput(output, false)
  149. progress.Messagef(po, "", `
  150. WARNING: Pulled image with specified platform (%s), but the resulting image's configured platform (%s) does not match.
  151. This is most likely caused by a bug in the build system that created the fetched image (%s).
  152. Please notify the image author to correct the configuration.`,
  153. platforms.Format(p), platforms.Format(imgPlat), name,
  154. )
  155. 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.")
  156. }
  157. } else {
  158. return nil, err
  159. }
  160. }
  161. if err := dimage.CheckOS(img.OperatingSystem()); err != nil {
  162. return nil, err
  163. }
  164. imgDesc, err := i.resolveDescriptor(ctx, name)
  165. if err != nil {
  166. return nil, err
  167. }
  168. return &imgDesc, err
  169. }
  170. func newROLayerForImage(ctx context.Context, imgDesc *ocispec.Descriptor, i *ImageService, platform *ocispec.Platform) (builder.ROLayer, error) {
  171. if imgDesc == nil {
  172. return nil, fmt.Errorf("can't make an RO layer for a nil image :'(")
  173. }
  174. platMatcher := platforms.Default()
  175. if platform != nil {
  176. platMatcher = platforms.Only(*platform)
  177. }
  178. confDesc, err := containerdimages.Config(ctx, i.client.ContentStore(), *imgDesc, platMatcher)
  179. if err != nil {
  180. return nil, err
  181. }
  182. diffIDs, err := containerdimages.RootFS(ctx, i.client.ContentStore(), confDesc)
  183. if err != nil {
  184. return nil, err
  185. }
  186. // TODO(vvoland): Check if image is unpacked, and unpack it if it's not.
  187. imageSnapshotID := identity.ChainID(diffIDs).String()
  188. snapshotter := i.StorageDriver()
  189. _, lease, err := createLease(ctx, i.client.LeasesService())
  190. if err != nil {
  191. return nil, errdefs.System(fmt.Errorf("failed to lease image snapshot %s: %w", imageSnapshotID, err))
  192. }
  193. return &rolayer{
  194. key: imageSnapshotID,
  195. c: i.client,
  196. snapshotter: snapshotter,
  197. diffID: "", // Image RO layer doesn't have a diff.
  198. contentStoreDigest: "",
  199. lease: &lease,
  200. }, nil
  201. }
  202. func createLease(ctx context.Context, lm leases.Manager) (context.Context, leases.Lease, error) {
  203. lease, err := lm.Create(ctx,
  204. leases.WithExpiration(time.Hour*24),
  205. leases.WithLabels(map[string]string{
  206. "org.mobyproject.lease.classicbuilder": "true",
  207. }),
  208. )
  209. if err != nil {
  210. return nil, leases.Lease{}, fmt.Errorf("failed to create a lease for snapshot: %w", err)
  211. }
  212. return leases.WithLease(ctx, lease.ID), lease, nil
  213. }
  214. type rolayer struct {
  215. key string
  216. c *containerd.Client
  217. snapshotter string
  218. diffID layer.DiffID
  219. contentStoreDigest digest.Digest
  220. lease *leases.Lease
  221. }
  222. func (rl *rolayer) ContentStoreDigest() digest.Digest {
  223. return rl.contentStoreDigest
  224. }
  225. func (rl *rolayer) DiffID() layer.DiffID {
  226. if rl.diffID == "" {
  227. return layer.DigestSHA256EmptyTar
  228. }
  229. return rl.diffID
  230. }
  231. func (rl *rolayer) Release() error {
  232. if rl.lease != nil {
  233. lm := rl.c.LeasesService()
  234. err := lm.Delete(context.TODO(), *rl.lease)
  235. if err != nil {
  236. return err
  237. }
  238. rl.lease = nil
  239. }
  240. return nil
  241. }
  242. // NewRWLayer creates a new read-write layer for the builder
  243. func (rl *rolayer) NewRWLayer() (_ builder.RWLayer, outErr error) {
  244. snapshotter := rl.c.SnapshotService(rl.snapshotter)
  245. key := stringid.GenerateRandomID()
  246. ctx, lease, err := createLease(context.TODO(), rl.c.LeasesService())
  247. if err != nil {
  248. return nil, err
  249. }
  250. defer func() {
  251. if outErr != nil {
  252. if err := rl.c.LeasesService().Delete(ctx, lease); err != nil {
  253. log.G(ctx).WithError(err).Warn("failed to remove lease after NewRWLayer error")
  254. }
  255. }
  256. }()
  257. mounts, err := snapshotter.Prepare(ctx, key, rl.key)
  258. if err != nil {
  259. return nil, err
  260. }
  261. root, err := os.MkdirTemp(os.TempDir(), "rootfs-mount")
  262. if err != nil {
  263. return nil, err
  264. }
  265. if err := mount.All(mounts, root); err != nil {
  266. return nil, err
  267. }
  268. return &rwlayer{
  269. key: key,
  270. parent: rl.key,
  271. c: rl.c,
  272. snapshotter: rl.snapshotter,
  273. root: root,
  274. lease: &lease,
  275. }, nil
  276. }
  277. type rwlayer struct {
  278. key string
  279. parent string
  280. c *containerd.Client
  281. snapshotter string
  282. root string
  283. lease *leases.Lease
  284. }
  285. func (rw *rwlayer) Root() string {
  286. return rw.root
  287. }
  288. func (rw *rwlayer) Commit() (_ builder.ROLayer, outErr error) {
  289. snapshotter := rw.c.SnapshotService(rw.snapshotter)
  290. key := stringid.GenerateRandomID()
  291. lm := rw.c.LeasesService()
  292. ctx, lease, err := createLease(context.TODO(), lm)
  293. if err != nil {
  294. return nil, err
  295. }
  296. defer func() {
  297. if outErr != nil {
  298. if err := lm.Delete(ctx, lease); err != nil {
  299. log.G(ctx).WithError(err).Warn("failed to remove lease after NewRWLayer error")
  300. }
  301. }
  302. }()
  303. err = snapshotter.Commit(ctx, key, rw.key)
  304. if err != nil && !cerrdefs.IsAlreadyExists(err) {
  305. return nil, err
  306. }
  307. differ := rw.c.DiffService()
  308. desc, err := rootfs.CreateDiff(ctx, key, snapshotter, differ)
  309. if err != nil {
  310. return nil, err
  311. }
  312. info, err := rw.c.ContentStore().Info(ctx, desc.Digest)
  313. if err != nil {
  314. return nil, err
  315. }
  316. diffIDStr, ok := info.Labels["containerd.io/uncompressed"]
  317. if !ok {
  318. return nil, fmt.Errorf("invalid differ response with no diffID")
  319. }
  320. diffID, err := digest.Parse(diffIDStr)
  321. if err != nil {
  322. return nil, err
  323. }
  324. return &rolayer{
  325. key: key,
  326. c: rw.c,
  327. snapshotter: rw.snapshotter,
  328. diffID: layer.DiffID(diffID),
  329. contentStoreDigest: desc.Digest,
  330. lease: &lease,
  331. }, nil
  332. }
  333. func (rw *rwlayer) Release() (outErr error) {
  334. if rw.root == "" { // nothing to release
  335. return nil
  336. }
  337. if err := mount.UnmountAll(rw.root, 0); err != nil && !errors.Is(err, os.ErrNotExist) {
  338. log.G(context.TODO()).WithError(err).WithField("root", rw.root).Error("failed to unmount RWLayer")
  339. return err
  340. }
  341. if err := os.Remove(rw.root); err != nil && !errors.Is(err, os.ErrNotExist) {
  342. log.G(context.TODO()).WithError(err).WithField("dir", rw.root).Error("failed to remove mount temp dir")
  343. return err
  344. }
  345. rw.root = ""
  346. if rw.lease != nil {
  347. lm := rw.c.LeasesService()
  348. err := lm.Delete(context.TODO(), *rw.lease)
  349. if err != nil {
  350. log.G(context.TODO()).WithError(err).Warn("failed to delete lease when releasing RWLayer")
  351. } else {
  352. rw.lease = nil
  353. }
  354. }
  355. return nil
  356. }
  357. // CreateImage creates a new image by adding a config and ID to the image store.
  358. // This is similar to LoadImage() except that it receives JSON encoded bytes of
  359. // an image instead of a tar archive.
  360. func (i *ImageService) CreateImage(ctx context.Context, config []byte, parent string, layerDigest digest.Digest) (builder.Image, error) {
  361. imgToCreate, err := dimage.NewFromJSON(config)
  362. if err != nil {
  363. return nil, err
  364. }
  365. ociImgToCreate := dockerImageToDockerOCIImage(*imgToCreate)
  366. var layers []ocispec.Descriptor
  367. var parentDigest digest.Digest
  368. // if the image has a parent, we need to start with the parents layers descriptors
  369. if parent != "" {
  370. parentDesc, err := i.resolveDescriptor(ctx, parent)
  371. if err != nil {
  372. return nil, err
  373. }
  374. parentImageManifest, err := containerdimages.Manifest(ctx, i.client.ContentStore(), parentDesc, platforms.Default())
  375. if err != nil {
  376. return nil, err
  377. }
  378. layers = parentImageManifest.Layers
  379. parentDigest = parentDesc.Digest
  380. }
  381. cs := i.client.ContentStore()
  382. ra, err := cs.ReaderAt(ctx, ocispec.Descriptor{Digest: layerDigest})
  383. if err != nil {
  384. return nil, fmt.Errorf("failed to read diff archive: %w", err)
  385. }
  386. defer ra.Close()
  387. empty, err := archive.IsEmpty(content.NewReader(ra))
  388. if err != nil {
  389. return nil, fmt.Errorf("failed to check if archive is empty: %w", err)
  390. }
  391. if !empty {
  392. info, err := cs.Info(ctx, layerDigest)
  393. if err != nil {
  394. return nil, err
  395. }
  396. layers = append(layers, ocispec.Descriptor{
  397. MediaType: containerdimages.MediaTypeDockerSchema2LayerGzip,
  398. Digest: layerDigest,
  399. Size: info.Size,
  400. })
  401. }
  402. createdImageId, err := i.createImageOCI(ctx, ociImgToCreate, parentDigest, layers, imgToCreate.ContainerConfig)
  403. if err != nil {
  404. return nil, err
  405. }
  406. return dimage.Clone(imgToCreate, createdImageId), nil
  407. }
  408. func (i *ImageService) createImageOCI(ctx context.Context, imgToCreate imagespec.DockerOCIImage,
  409. parentDigest digest.Digest, layers []ocispec.Descriptor,
  410. containerConfig container.Config,
  411. ) (dimage.ID, error) {
  412. // Necessary to prevent the contents from being GC'd
  413. // between writing them here and creating an image
  414. ctx, release, err := i.client.WithLease(ctx, leases.WithRandomID(), leases.WithExpiration(1*time.Hour))
  415. if err != nil {
  416. return "", err
  417. }
  418. defer func() {
  419. if err := release(compatcontext.WithoutCancel(ctx)); err != nil {
  420. log.G(ctx).WithError(err).Warn("failed to release lease created for create")
  421. }
  422. }()
  423. manifestDesc, ccDesc, err := writeContentsForImage(ctx, i.snapshotter, i.client.ContentStore(), imgToCreate, layers, containerConfig)
  424. if err != nil {
  425. return "", err
  426. }
  427. img := containerdimages.Image{
  428. Name: danglingImageName(manifestDesc.Digest),
  429. Target: manifestDesc,
  430. CreatedAt: time.Now(),
  431. Labels: map[string]string{
  432. imageLabelClassicBuilderParent: parentDigest.String(),
  433. imageLabelClassicBuilderContainerConfig: ccDesc.Digest.String(),
  434. },
  435. }
  436. if parentDigest == "" {
  437. img.Labels[imageLabelClassicBuilderFromScratch] = "1"
  438. }
  439. createdImage, err := i.client.ImageService().Update(ctx, img)
  440. if err != nil {
  441. if !cerrdefs.IsNotFound(err) {
  442. return "", err
  443. }
  444. if createdImage, err = i.client.ImageService().Create(ctx, img); err != nil {
  445. return "", fmt.Errorf("failed to create new image: %w", err)
  446. }
  447. }
  448. if err := i.unpackImage(ctx, i.StorageDriver(), img, manifestDesc); err != nil {
  449. return "", err
  450. }
  451. return dimage.ID(createdImage.Target.Digest), nil
  452. }
  453. // writeContentsForImage will commit oci image config and manifest into containerd's content store.
  454. func writeContentsForImage(ctx context.Context, snName string, cs content.Store,
  455. newConfig imagespec.DockerOCIImage, layers []ocispec.Descriptor,
  456. containerConfig container.Config,
  457. ) (
  458. manifestDesc ocispec.Descriptor,
  459. containerConfigDesc ocispec.Descriptor,
  460. _ error,
  461. ) {
  462. newConfigJSON, err := json.Marshal(newConfig)
  463. if err != nil {
  464. return ocispec.Descriptor{}, ocispec.Descriptor{}, err
  465. }
  466. configDesc := ocispec.Descriptor{
  467. MediaType: ocispec.MediaTypeImageConfig,
  468. Digest: digest.FromBytes(newConfigJSON),
  469. Size: int64(len(newConfigJSON)),
  470. }
  471. newMfst := struct {
  472. MediaType string `json:"mediaType,omitempty"`
  473. ocispec.Manifest
  474. }{
  475. MediaType: ocispec.MediaTypeImageManifest,
  476. Manifest: ocispec.Manifest{
  477. Versioned: specs.Versioned{
  478. SchemaVersion: 2,
  479. },
  480. Config: configDesc,
  481. Layers: layers,
  482. },
  483. }
  484. newMfstJSON, err := json.MarshalIndent(newMfst, "", " ")
  485. if err != nil {
  486. return ocispec.Descriptor{}, ocispec.Descriptor{}, err
  487. }
  488. newMfstDesc := ocispec.Descriptor{
  489. MediaType: ocispec.MediaTypeImageManifest,
  490. Digest: digest.FromBytes(newMfstJSON),
  491. Size: int64(len(newMfstJSON)),
  492. }
  493. // new manifest should reference the layers and config content
  494. labels := map[string]string{
  495. "containerd.io/gc.ref.content.0": configDesc.Digest.String(),
  496. }
  497. for i, l := range layers {
  498. labels[fmt.Sprintf("containerd.io/gc.ref.content.%d", i+1)] = l.Digest.String()
  499. }
  500. err = content.WriteBlob(ctx, cs, newMfstDesc.Digest.String(), bytes.NewReader(newMfstJSON), newMfstDesc, content.WithLabels(labels))
  501. if err != nil {
  502. return ocispec.Descriptor{}, ocispec.Descriptor{}, err
  503. }
  504. ccDesc, err := saveContainerConfig(ctx, cs, newMfstDesc.Digest, containerConfig)
  505. if err != nil {
  506. return ocispec.Descriptor{}, ocispec.Descriptor{}, err
  507. }
  508. // config should reference to snapshotter and container config
  509. labelOpt := content.WithLabels(map[string]string{
  510. fmt.Sprintf("containerd.io/gc.ref.snapshot.%s", snName): identity.ChainID(newConfig.RootFS.DiffIDs).String(),
  511. contentLabelGcRefContainerConfig: ccDesc.Digest.String(),
  512. })
  513. err = content.WriteBlob(ctx, cs, configDesc.Digest.String(), bytes.NewReader(newConfigJSON), configDesc, labelOpt)
  514. if err != nil {
  515. return ocispec.Descriptor{}, ocispec.Descriptor{}, err
  516. }
  517. return newMfstDesc, ccDesc, nil
  518. }
  519. // saveContainerConfig serializes the given ContainerConfig into a json and
  520. // stores it in the content store and returns its descriptor.
  521. func saveContainerConfig(ctx context.Context, content content.Ingester, imgID digest.Digest, containerConfig container.Config) (ocispec.Descriptor, error) {
  522. containerConfigDesc, err := storeJson(ctx, content,
  523. "application/vnd.docker.container.image.v1+json", containerConfig,
  524. map[string]string{contentLabelClassicBuilderImage: imgID.String()},
  525. )
  526. if err != nil {
  527. return ocispec.Descriptor{}, err
  528. }
  529. return containerConfigDesc, nil
  530. }