image_builder.go 16 KB

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