github.com/moby/docker@v26.1.3+incompatible/daemon/containerd/image_import.go (about) 1 package containerd 2 3 import ( 4 "bufio" 5 "bytes" 6 "context" 7 "encoding/json" 8 "fmt" 9 "io" 10 "time" 11 12 "github.com/containerd/containerd/content" 13 cerrdefs "github.com/containerd/containerd/errdefs" 14 "github.com/containerd/containerd/images" 15 "github.com/containerd/containerd/platforms" 16 "github.com/containerd/log" 17 "github.com/distribution/reference" 18 "github.com/docker/docker/api/types/container" 19 "github.com/docker/docker/api/types/events" 20 "github.com/docker/docker/builder/dockerfile" 21 "github.com/docker/docker/errdefs" 22 "github.com/docker/docker/image" 23 "github.com/docker/docker/internal/compatcontext" 24 "github.com/docker/docker/pkg/archive" 25 "github.com/docker/docker/pkg/pools" 26 "github.com/google/uuid" 27 imagespec "github.com/moby/docker-image-spec/specs-go/v1" 28 "github.com/opencontainers/go-digest" 29 "github.com/opencontainers/image-spec/specs-go" 30 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 31 "github.com/pkg/errors" 32 ) 33 34 // ImportImage imports an image, getting the archived layer data from layerReader. 35 // Layer archive is imported as-is if the compression is gzip or zstd. 36 // Uncompressed, xz and bzip2 archives are recompressed into gzip. 37 // The image is tagged with the given reference. 38 // If the platform is nil, the default host platform is used. 39 // The message is used as the history comment. 40 // Image configuration is derived from the dockerfile instructions in changes. 41 func (i *ImageService) ImportImage(ctx context.Context, ref reference.Named, platform *ocispec.Platform, msg string, layerReader io.Reader, changes []string) (image.ID, error) { 42 refString := "" 43 if ref != nil { 44 refString = ref.String() 45 } 46 logger := log.G(ctx).WithField("ref", refString) 47 48 ctx, release, err := i.client.WithLease(ctx) 49 if err != nil { 50 return "", errdefs.System(err) 51 } 52 defer func() { 53 if err := release(compatcontext.WithoutCancel(ctx)); err != nil { 54 logger.WithError(err).Warn("failed to release lease created for import") 55 } 56 }() 57 58 if platform == nil { 59 def := platforms.DefaultSpec() 60 platform = &def 61 } 62 63 imageConfig, err := dockerfile.BuildFromConfig(ctx, &container.Config{}, changes, platform.OS) 64 if err != nil { 65 logger.WithError(err).Debug("failed to process changes") 66 return "", errdefs.InvalidParameter(err) 67 } 68 69 cs := i.content 70 71 compressedDigest, uncompressedDigest, mt, err := saveArchive(ctx, cs, layerReader) 72 if err != nil { 73 logger.WithError(err).Debug("failed to write layer blob") 74 return "", err 75 } 76 logger = logger.WithFields(log.Fields{ 77 "compressedDigest": compressedDigest, 78 "uncompressedDigest": uncompressedDigest, 79 }) 80 81 size, err := fillUncompressedLabel(ctx, cs, compressedDigest, uncompressedDigest) 82 if err != nil { 83 logger.WithError(err).Debug("failed to set uncompressed label on the compressed blob") 84 return "", err 85 } 86 87 compressedRootfsDesc := ocispec.Descriptor{ 88 MediaType: mt, 89 Digest: compressedDigest, 90 Size: size, 91 } 92 93 dockerCfg := containerConfigToDockerOCIImageConfig(imageConfig) 94 createdAt := time.Now() 95 config := imagespec.DockerOCIImage{ 96 Image: ocispec.Image{ 97 Platform: *platform, 98 Created: &createdAt, 99 Author: "", 100 RootFS: ocispec.RootFS{ 101 Type: "layers", 102 DiffIDs: []digest.Digest{uncompressedDigest}, 103 }, 104 History: []ocispec.History{ 105 { 106 Created: &createdAt, 107 CreatedBy: "", 108 Author: "", 109 Comment: msg, 110 EmptyLayer: false, 111 }, 112 }, 113 }, 114 Config: dockerCfg, 115 } 116 configDesc, err := storeJson(ctx, cs, ocispec.MediaTypeImageConfig, config, nil) 117 if err != nil { 118 return "", err 119 } 120 121 manifest := ocispec.Manifest{ 122 MediaType: ocispec.MediaTypeImageManifest, 123 Versioned: specs.Versioned{ 124 SchemaVersion: 2, 125 }, 126 Config: configDesc, 127 Layers: []ocispec.Descriptor{ 128 compressedRootfsDesc, 129 }, 130 } 131 manifestDesc, err := storeJson(ctx, cs, ocispec.MediaTypeImageManifest, manifest, map[string]string{ 132 "containerd.io/gc.ref.content.config": configDesc.Digest.String(), 133 "containerd.io/gc.ref.content.l.0": compressedDigest.String(), 134 }) 135 if err != nil { 136 return "", err 137 } 138 139 id := image.ID(manifestDesc.Digest.String()) 140 img := images.Image{ 141 Name: refString, 142 Target: manifestDesc, 143 CreatedAt: createdAt, 144 } 145 if img.Name == "" { 146 img.Name = danglingImageName(manifestDesc.Digest) 147 } 148 149 err = i.saveImage(ctx, img) 150 if err != nil { 151 logger.WithError(err).Debug("failed to save image") 152 return "", err 153 } 154 155 err = i.unpackImage(ctx, i.StorageDriver(), img, manifestDesc) 156 if err != nil { 157 logger.WithError(err).Debug("failed to unpack image") 158 } else { 159 i.LogImageEvent(id.String(), id.String(), events.ActionImport) 160 } 161 162 return id, err 163 } 164 165 // saveArchive saves the archive from bufRd to the content store, compressing it if necessary. 166 // Returns compressed blob digest, digest of the uncompressed data and media type of the stored blob. 167 func saveArchive(ctx context.Context, cs content.Store, layerReader io.Reader) (digest.Digest, digest.Digest, string, error) { 168 // Wrap the reader in buffered reader to allow peeks. 169 p := pools.BufioReader32KPool 170 bufRd := p.Get(layerReader) 171 defer p.Put(bufRd) 172 173 compression, err := detectCompression(bufRd) 174 if err != nil { 175 return "", "", "", err 176 } 177 178 var uncompressedReader io.Reader = bufRd 179 switch compression { 180 case archive.Gzip, archive.Zstd: 181 // If the input is already a compressed layer, just save it as is. 182 mediaType := ocispec.MediaTypeImageLayerGzip 183 if compression == archive.Zstd { 184 mediaType = ocispec.MediaTypeImageLayerZstd 185 } 186 187 compressedDigest, uncompressedDigest, err := writeCompressedBlob(ctx, cs, mediaType, bufRd) 188 if err != nil { 189 return "", "", "", err 190 } 191 192 return compressedDigest, uncompressedDigest, mediaType, nil 193 case archive.Bzip2, archive.Xz: 194 r, err := archive.DecompressStream(bufRd) 195 if err != nil { 196 return "", "", "", errdefs.InvalidParameter(err) 197 } 198 defer r.Close() 199 uncompressedReader = r 200 fallthrough 201 case archive.Uncompressed: 202 mediaType := ocispec.MediaTypeImageLayerGzip 203 compression := archive.Gzip 204 205 compressedDigest, uncompressedDigest, err := compressAndWriteBlob(ctx, cs, compression, mediaType, uncompressedReader) 206 if err != nil { 207 return "", "", "", err 208 } 209 210 return compressedDigest, uncompressedDigest, mediaType, nil 211 } 212 213 return "", "", "", errdefs.InvalidParameter(errors.New("unsupported archive compression")) 214 } 215 216 // writeCompressedBlob writes the blob and simultaneously computes the digest of the uncompressed data. 217 func writeCompressedBlob(ctx context.Context, cs content.Store, mediaType string, bufRd *bufio.Reader) (digest.Digest, digest.Digest, error) { 218 pr, pw := io.Pipe() 219 defer pw.Close() 220 defer pr.Close() 221 222 c := make(chan digest.Digest) 223 // Start copying the blob to the content store from the pipe and tee it to the pipe. 224 go func() { 225 compressedDigest, err := writeBlobAndReturnDigest(ctx, cs, mediaType, io.TeeReader(bufRd, pw)) 226 pw.CloseWithError(err) 227 c <- compressedDigest 228 }() 229 230 digester := digest.Canonical.Digester() 231 232 // Decompress the piped blob. 233 decompressedStream, err := archive.DecompressStream(pr) 234 if err == nil { 235 // Feed the digester with decompressed data. 236 _, err = io.Copy(digester.Hash(), decompressedStream) 237 decompressedStream.Close() 238 } 239 pr.CloseWithError(err) 240 241 compressedDigest := <-c 242 if err != nil { 243 if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { 244 return "", "", errdefs.Cancelled(err) 245 } 246 return "", "", errdefs.System(err) 247 } 248 249 uncompressedDigest := digester.Digest() 250 return compressedDigest, uncompressedDigest, nil 251 } 252 253 // compressAndWriteBlob compresses the uncompressedReader and stores it in the content store. 254 func compressAndWriteBlob(ctx context.Context, cs content.Store, compression archive.Compression, mediaType string, uncompressedLayerReader io.Reader) (digest.Digest, digest.Digest, error) { 255 pr, pw := io.Pipe() 256 defer pr.Close() 257 defer pw.Close() 258 259 compressor, err := archive.CompressStream(pw, compression) 260 if err != nil { 261 return "", "", errdefs.InvalidParameter(err) 262 } 263 264 writeChan := make(chan digest.Digest) 265 // Start copying the blob to the content store from the pipe. 266 go func() { 267 dgst, err := writeBlobAndReturnDigest(ctx, cs, mediaType, pr) 268 pr.CloseWithError(err) 269 writeChan <- dgst 270 }() 271 272 // Copy archive to the pipe and tee it to a digester. 273 // This will feed the pipe the above goroutine is reading from. 274 uncompressedDigester := digest.Canonical.Digester() 275 readFromInputAndDigest := io.TeeReader(uncompressedLayerReader, uncompressedDigester.Hash()) 276 _, err = io.Copy(compressor, readFromInputAndDigest) 277 compressor.Close() 278 pw.CloseWithError(err) 279 280 compressedDigest := <-writeChan 281 if err != nil { 282 if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { 283 return "", "", errdefs.Cancelled(err) 284 } 285 return "", "", errdefs.System(err) 286 } 287 288 return compressedDigest, uncompressedDigester.Digest(), err 289 } 290 291 // writeBlobAndReturnDigest writes a blob to the content store and returns the digest. 292 func writeBlobAndReturnDigest(ctx context.Context, cs content.Store, mt string, reader io.Reader) (digest.Digest, error) { 293 digester := digest.Canonical.Digester() 294 if err := content.WriteBlob(ctx, cs, uuid.New().String(), io.TeeReader(reader, digester.Hash()), ocispec.Descriptor{MediaType: mt}); err != nil { 295 return "", errdefs.System(err) 296 } 297 return digester.Digest(), nil 298 } 299 300 // saveImage creates an image in the ImageService or updates it if it exists. 301 func (i *ImageService) saveImage(ctx context.Context, img images.Image) error { 302 if _, err := i.images.Update(ctx, img); err != nil { 303 if cerrdefs.IsNotFound(err) { 304 if _, err := i.images.Create(ctx, img); err != nil { 305 return errdefs.Unknown(err) 306 } 307 } else { 308 return errdefs.Unknown(err) 309 } 310 } 311 312 return nil 313 } 314 315 // unpackImage unpacks the platform-specific manifest of a image into the snapshotter. 316 func (i *ImageService) unpackImage(ctx context.Context, snapshotter string, img images.Image, manifestDesc ocispec.Descriptor) error { 317 c8dImg, err := i.NewImageManifest(ctx, img, manifestDesc) 318 if err != nil { 319 return err 320 } 321 322 if err := c8dImg.Unpack(ctx, snapshotter); err != nil { 323 if !cerrdefs.IsAlreadyExists(err) { 324 return errdefs.System(fmt.Errorf("failed to unpack image: %w", err)) 325 } 326 } 327 328 return nil 329 } 330 331 // detectCompression dectects the reader compression type. 332 func detectCompression(bufRd *bufio.Reader) (archive.Compression, error) { 333 bs, err := bufRd.Peek(10) 334 if err != nil && err != io.EOF { 335 // Note: we'll ignore any io.EOF error because there are some odd 336 // cases where the layer.tar file will be empty (zero bytes) and 337 // that results in an io.EOF from the Peek() call. So, in those 338 // cases we'll just treat it as a non-compressed stream and 339 // that means just create an empty layer. 340 // See Issue 18170 341 return archive.Uncompressed, errdefs.Unknown(err) 342 } 343 344 return archive.DetectCompression(bs), nil 345 } 346 347 // fillUncompressedLabel sets the uncompressed digest label on the compressed blob metadata 348 // and returns the compressed blob size. 349 func fillUncompressedLabel(ctx context.Context, cs content.Store, compressedDigest digest.Digest, uncompressedDigest digest.Digest) (int64, error) { 350 info, err := cs.Info(ctx, compressedDigest) 351 if err != nil { 352 return 0, errdefs.Unknown(errors.Wrapf(err, "couldn't open previously written blob")) 353 } 354 size := info.Size 355 info.Labels = map[string]string{"containerd.io/uncompressed": uncompressedDigest.String()} 356 357 _, err = cs.Update(ctx, info, "labels.*") 358 if err != nil { 359 return 0, errdefs.System(errors.Wrapf(err, "couldn't set uncompressed label")) 360 } 361 return size, nil 362 } 363 364 // storeJson marshals the provided object as json and stores it. 365 func storeJson(ctx context.Context, cs content.Ingester, mt string, obj interface{}, labels map[string]string) (ocispec.Descriptor, error) { 366 configData, err := json.Marshal(obj) 367 if err != nil { 368 return ocispec.Descriptor{}, errdefs.InvalidParameter(err) 369 } 370 configDigest := digest.FromBytes(configData) 371 if err != nil { 372 return ocispec.Descriptor{}, errdefs.InvalidParameter(err) 373 } 374 desc := ocispec.Descriptor{ 375 MediaType: mt, 376 Digest: configDigest, 377 Size: int64(len(configData)), 378 } 379 380 var opts []content.Opt 381 if labels != nil { 382 opts = append(opts, content.WithLabels(labels)) 383 } 384 385 err = content.WriteBlob(ctx, cs, configDigest.String(), bytes.NewReader(configData), desc, opts...) 386 if err != nil { 387 return ocispec.Descriptor{}, errdefs.System(err) 388 } 389 return desc, nil 390 }