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