image_builder.go 12 KB

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