image_builder.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633
  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. // Unmount the layer, required by the containerd windows snapshotter.
  304. // The windowsfilter graphdriver does this inside its own Diff method.
  305. //
  306. // The only place that calls this in-tree is (b *Builder) exportImage and
  307. // that is called from the end of (b *Builder) performCopy which has a
  308. // `defer rwLayer.Release()` pending.
  309. //
  310. // After the snapshotter.Commit the source snapshot is deleted anyway and
  311. // it shouldn't be accessed afterwards.
  312. if rw.root != "" {
  313. if err := mount.UnmountAll(rw.root, 0); err != nil && !errors.Is(err, os.ErrNotExist) {
  314. log.G(ctx).WithError(err).WithField("root", rw.root).Error("failed to unmount RWLayer")
  315. return nil, err
  316. }
  317. }
  318. err = snapshotter.Commit(ctx, key, rw.key)
  319. if err != nil && !cerrdefs.IsAlreadyExists(err) {
  320. return nil, err
  321. }
  322. differ := rw.c.DiffService()
  323. desc, err := rootfs.CreateDiff(ctx, key, snapshotter, differ)
  324. if err != nil {
  325. return nil, err
  326. }
  327. info, err := rw.c.ContentStore().Info(ctx, desc.Digest)
  328. if err != nil {
  329. return nil, err
  330. }
  331. diffIDStr, ok := info.Labels["containerd.io/uncompressed"]
  332. if !ok {
  333. return nil, fmt.Errorf("invalid differ response with no diffID")
  334. }
  335. diffID, err := digest.Parse(diffIDStr)
  336. if err != nil {
  337. return nil, err
  338. }
  339. return &rolayer{
  340. key: key,
  341. c: rw.c,
  342. snapshotter: rw.snapshotter,
  343. diffID: layer.DiffID(diffID),
  344. contentStoreDigest: desc.Digest,
  345. lease: &lease,
  346. }, nil
  347. }
  348. func (rw *rwlayer) Release() (outErr error) {
  349. if rw.root == "" { // nothing to release
  350. return nil
  351. }
  352. if err := mount.UnmountAll(rw.root, 0); err != nil && !errors.Is(err, os.ErrNotExist) {
  353. log.G(context.TODO()).WithError(err).WithField("root", rw.root).Error("failed to unmount RWLayer")
  354. return err
  355. }
  356. if err := os.Remove(rw.root); err != nil && !errors.Is(err, os.ErrNotExist) {
  357. log.G(context.TODO()).WithError(err).WithField("dir", rw.root).Error("failed to remove mount temp dir")
  358. return err
  359. }
  360. rw.root = ""
  361. if rw.lease != nil {
  362. lm := rw.c.LeasesService()
  363. err := lm.Delete(context.TODO(), *rw.lease)
  364. if err != nil {
  365. log.G(context.TODO()).WithError(err).Warn("failed to delete lease when releasing RWLayer")
  366. } else {
  367. rw.lease = nil
  368. }
  369. }
  370. return nil
  371. }
  372. // CreateImage creates a new image by adding a config and ID to the image store.
  373. // This is similar to LoadImage() except that it receives JSON encoded bytes of
  374. // an image instead of a tar archive.
  375. func (i *ImageService) CreateImage(ctx context.Context, config []byte, parent string, layerDigest digest.Digest) (builder.Image, error) {
  376. imgToCreate, err := dimage.NewFromJSON(config)
  377. if err != nil {
  378. return nil, err
  379. }
  380. ociImgToCreate := dockerImageToDockerOCIImage(*imgToCreate)
  381. var layers []ocispec.Descriptor
  382. var parentDigest digest.Digest
  383. // if the image has a parent, we need to start with the parents layers descriptors
  384. if parent != "" {
  385. parentDesc, err := i.resolveDescriptor(ctx, parent)
  386. if err != nil {
  387. return nil, err
  388. }
  389. parentImageManifest, err := containerdimages.Manifest(ctx, i.client.ContentStore(), parentDesc, platforms.Default())
  390. if err != nil {
  391. return nil, err
  392. }
  393. layers = parentImageManifest.Layers
  394. parentDigest = parentDesc.Digest
  395. }
  396. cs := i.client.ContentStore()
  397. ra, err := cs.ReaderAt(ctx, ocispec.Descriptor{Digest: layerDigest})
  398. if err != nil {
  399. return nil, fmt.Errorf("failed to read diff archive: %w", err)
  400. }
  401. defer ra.Close()
  402. empty, err := archive.IsEmpty(content.NewReader(ra))
  403. if err != nil {
  404. return nil, fmt.Errorf("failed to check if archive is empty: %w", err)
  405. }
  406. if !empty {
  407. info, err := cs.Info(ctx, layerDigest)
  408. if err != nil {
  409. return nil, err
  410. }
  411. layers = append(layers, ocispec.Descriptor{
  412. MediaType: containerdimages.MediaTypeDockerSchema2LayerGzip,
  413. Digest: layerDigest,
  414. Size: info.Size,
  415. })
  416. }
  417. createdImageId, err := i.createImageOCI(ctx, ociImgToCreate, parentDigest, layers, imgToCreate.ContainerConfig)
  418. if err != nil {
  419. return nil, err
  420. }
  421. return dimage.Clone(imgToCreate, createdImageId), nil
  422. }
  423. func (i *ImageService) createImageOCI(ctx context.Context, imgToCreate imagespec.DockerOCIImage,
  424. parentDigest digest.Digest, layers []ocispec.Descriptor,
  425. containerConfig container.Config,
  426. ) (dimage.ID, error) {
  427. // Necessary to prevent the contents from being GC'd
  428. // between writing them here and creating an image
  429. ctx, release, err := i.client.WithLease(ctx, leases.WithRandomID(), leases.WithExpiration(1*time.Hour))
  430. if err != nil {
  431. return "", err
  432. }
  433. defer func() {
  434. if err := release(compatcontext.WithoutCancel(ctx)); err != nil {
  435. log.G(ctx).WithError(err).Warn("failed to release lease created for create")
  436. }
  437. }()
  438. manifestDesc, ccDesc, err := writeContentsForImage(ctx, i.snapshotter, i.client.ContentStore(), imgToCreate, layers, containerConfig)
  439. if err != nil {
  440. return "", err
  441. }
  442. img := containerdimages.Image{
  443. Name: danglingImageName(manifestDesc.Digest),
  444. Target: manifestDesc,
  445. CreatedAt: time.Now(),
  446. Labels: map[string]string{
  447. imageLabelClassicBuilderParent: parentDigest.String(),
  448. imageLabelClassicBuilderContainerConfig: ccDesc.Digest.String(),
  449. },
  450. }
  451. if parentDigest == "" {
  452. img.Labels[imageLabelClassicBuilderFromScratch] = "1"
  453. }
  454. createdImage, err := i.client.ImageService().Update(ctx, img)
  455. if err != nil {
  456. if !cerrdefs.IsNotFound(err) {
  457. return "", err
  458. }
  459. if createdImage, err = i.client.ImageService().Create(ctx, img); err != nil {
  460. return "", fmt.Errorf("failed to create new image: %w", err)
  461. }
  462. }
  463. if err := i.unpackImage(ctx, i.StorageDriver(), img, manifestDesc); err != nil {
  464. return "", err
  465. }
  466. return dimage.ID(createdImage.Target.Digest), nil
  467. }
  468. // writeContentsForImage will commit oci image config and manifest into containerd's content store.
  469. func writeContentsForImage(ctx context.Context, snName string, cs content.Store,
  470. newConfig imagespec.DockerOCIImage, layers []ocispec.Descriptor,
  471. containerConfig container.Config,
  472. ) (
  473. manifestDesc ocispec.Descriptor,
  474. containerConfigDesc ocispec.Descriptor,
  475. _ error,
  476. ) {
  477. newConfigJSON, err := json.Marshal(newConfig)
  478. if err != nil {
  479. return ocispec.Descriptor{}, ocispec.Descriptor{}, err
  480. }
  481. configDesc := ocispec.Descriptor{
  482. MediaType: ocispec.MediaTypeImageConfig,
  483. Digest: digest.FromBytes(newConfigJSON),
  484. Size: int64(len(newConfigJSON)),
  485. }
  486. newMfst := struct {
  487. MediaType string `json:"mediaType,omitempty"`
  488. ocispec.Manifest
  489. }{
  490. MediaType: ocispec.MediaTypeImageManifest,
  491. Manifest: ocispec.Manifest{
  492. Versioned: specs.Versioned{
  493. SchemaVersion: 2,
  494. },
  495. Config: configDesc,
  496. Layers: layers,
  497. },
  498. }
  499. newMfstJSON, err := json.MarshalIndent(newMfst, "", " ")
  500. if err != nil {
  501. return ocispec.Descriptor{}, ocispec.Descriptor{}, err
  502. }
  503. newMfstDesc := ocispec.Descriptor{
  504. MediaType: ocispec.MediaTypeImageManifest,
  505. Digest: digest.FromBytes(newMfstJSON),
  506. Size: int64(len(newMfstJSON)),
  507. }
  508. // new manifest should reference the layers and config content
  509. labels := map[string]string{
  510. "containerd.io/gc.ref.content.0": configDesc.Digest.String(),
  511. }
  512. for i, l := range layers {
  513. labels[fmt.Sprintf("containerd.io/gc.ref.content.%d", i+1)] = l.Digest.String()
  514. }
  515. err = content.WriteBlob(ctx, cs, newMfstDesc.Digest.String(), bytes.NewReader(newMfstJSON), newMfstDesc, content.WithLabels(labels))
  516. if err != nil {
  517. return ocispec.Descriptor{}, ocispec.Descriptor{}, err
  518. }
  519. ccDesc, err := saveContainerConfig(ctx, cs, newMfstDesc.Digest, containerConfig)
  520. if err != nil {
  521. return ocispec.Descriptor{}, ocispec.Descriptor{}, err
  522. }
  523. // config should reference to snapshotter and container config
  524. labelOpt := content.WithLabels(map[string]string{
  525. fmt.Sprintf("containerd.io/gc.ref.snapshot.%s", snName): identity.ChainID(newConfig.RootFS.DiffIDs).String(),
  526. contentLabelGcRefContainerConfig: ccDesc.Digest.String(),
  527. })
  528. err = content.WriteBlob(ctx, cs, configDesc.Digest.String(), bytes.NewReader(newConfigJSON), configDesc, labelOpt)
  529. if err != nil {
  530. return ocispec.Descriptor{}, ocispec.Descriptor{}, err
  531. }
  532. return newMfstDesc, ccDesc, nil
  533. }
  534. // saveContainerConfig serializes the given ContainerConfig into a json and
  535. // stores it in the content store and returns its descriptor.
  536. func saveContainerConfig(ctx context.Context, content content.Ingester, imgID digest.Digest, containerConfig container.Config) (ocispec.Descriptor, error) {
  537. containerConfigDesc, err := storeJson(ctx, content,
  538. "application/vnd.docker.container.image.v1+json", containerConfig,
  539. map[string]string{contentLabelClassicBuilderImage: imgID.String()},
  540. )
  541. if err != nil {
  542. return ocispec.Descriptor{}, err
  543. }
  544. return containerConfigDesc, nil
  545. }