image_builder.go 19 KB

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