image_builder.go 15 KB

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