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