github.com/moby/docker@v26.1.3+incompatible/image/tarexport/save.go (about) 1 package tarexport // import "github.com/docker/docker/image/tarexport" 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "io" 8 "os" 9 "path" 10 "path/filepath" 11 "time" 12 13 "github.com/containerd/containerd/images" 14 "github.com/containerd/log" 15 "github.com/distribution/reference" 16 "github.com/docker/distribution" 17 "github.com/docker/docker/api/types/events" 18 "github.com/docker/docker/image" 19 v1 "github.com/docker/docker/image/v1" 20 "github.com/docker/docker/layer" 21 "github.com/docker/docker/pkg/archive" 22 "github.com/docker/docker/pkg/system" 23 "github.com/moby/sys/sequential" 24 "github.com/opencontainers/go-digest" 25 "github.com/opencontainers/image-spec/specs-go" 26 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 27 "github.com/pkg/errors" 28 ) 29 30 type imageDescriptor struct { 31 refs []reference.NamedTagged 32 layers []layer.DiffID 33 image *image.Image 34 layerRef layer.Layer 35 } 36 37 type saveSession struct { 38 *tarexporter 39 outDir string 40 images map[image.ID]*imageDescriptor 41 savedLayers map[layer.DiffID]distribution.Descriptor 42 savedConfigs map[string]struct{} 43 } 44 45 func (l *tarexporter) Save(names []string, outStream io.Writer) error { 46 images, err := l.parseNames(names) 47 if err != nil { 48 return err 49 } 50 51 // Release all the image top layer references 52 defer l.releaseLayerReferences(images) 53 return (&saveSession{tarexporter: l, images: images}).save(outStream) 54 } 55 56 // parseNames will parse the image names to a map which contains image.ID to *imageDescriptor. 57 // Each imageDescriptor holds an image top layer reference named 'layerRef'. It is taken here, should be released later. 58 func (l *tarexporter) parseNames(names []string) (desc map[image.ID]*imageDescriptor, rErr error) { 59 imgDescr := make(map[image.ID]*imageDescriptor) 60 defer func() { 61 if rErr != nil { 62 l.releaseLayerReferences(imgDescr) 63 } 64 }() 65 66 addAssoc := func(id image.ID, ref reference.Named) error { 67 if _, ok := imgDescr[id]; !ok { 68 descr := &imageDescriptor{} 69 if err := l.takeLayerReference(id, descr); err != nil { 70 return err 71 } 72 imgDescr[id] = descr 73 } 74 75 if ref != nil { 76 if _, ok := ref.(reference.Canonical); ok { 77 return nil 78 } 79 tagged, ok := reference.TagNameOnly(ref).(reference.NamedTagged) 80 if !ok { 81 return nil 82 } 83 84 for _, t := range imgDescr[id].refs { 85 if tagged.String() == t.String() { 86 return nil 87 } 88 } 89 imgDescr[id].refs = append(imgDescr[id].refs, tagged) 90 } 91 return nil 92 } 93 94 for _, name := range names { 95 ref, err := reference.ParseAnyReference(name) 96 if err != nil { 97 return nil, err 98 } 99 namedRef, ok := ref.(reference.Named) 100 if !ok { 101 // Check if digest ID reference 102 if digested, ok := ref.(reference.Digested); ok { 103 if err := addAssoc(image.ID(digested.Digest()), nil); err != nil { 104 return nil, err 105 } 106 continue 107 } 108 return nil, errors.Errorf("invalid reference: %v", name) 109 } 110 111 if reference.FamiliarName(namedRef) == string(digest.Canonical) { 112 imgID, err := l.is.Search(name) 113 if err != nil { 114 return nil, err 115 } 116 if err := addAssoc(imgID, nil); err != nil { 117 return nil, err 118 } 119 continue 120 } 121 if reference.IsNameOnly(namedRef) { 122 assocs := l.rs.ReferencesByName(namedRef) 123 for _, assoc := range assocs { 124 if err := addAssoc(image.ID(assoc.ID), assoc.Ref); err != nil { 125 return nil, err 126 } 127 } 128 if len(assocs) == 0 { 129 imgID, err := l.is.Search(name) 130 if err != nil { 131 return nil, err 132 } 133 if err := addAssoc(imgID, nil); err != nil { 134 return nil, err 135 } 136 } 137 continue 138 } 139 id, err := l.rs.Get(namedRef) 140 if err != nil { 141 return nil, err 142 } 143 if err := addAssoc(image.ID(id), namedRef); err != nil { 144 return nil, err 145 } 146 } 147 return imgDescr, nil 148 } 149 150 // takeLayerReference will take/Get the image top layer reference 151 func (l *tarexporter) takeLayerReference(id image.ID, imgDescr *imageDescriptor) error { 152 img, err := l.is.Get(id) 153 if err != nil { 154 return err 155 } 156 if err := image.CheckOS(img.OperatingSystem()); err != nil { 157 return fmt.Errorf("os %q is not supported", img.OperatingSystem()) 158 } 159 imgDescr.image = img 160 topLayerID := img.RootFS.ChainID() 161 if topLayerID == "" { 162 return nil 163 } 164 layer, err := l.lss.Get(topLayerID) 165 if err != nil { 166 return err 167 } 168 imgDescr.layerRef = layer 169 return nil 170 } 171 172 // releaseLayerReferences will release all the image top layer references 173 func (l *tarexporter) releaseLayerReferences(imgDescr map[image.ID]*imageDescriptor) error { 174 for _, descr := range imgDescr { 175 if descr.layerRef != nil { 176 l.lss.Release(descr.layerRef) 177 } 178 } 179 return nil 180 } 181 182 func (s *saveSession) save(outStream io.Writer) error { 183 s.savedConfigs = make(map[string]struct{}) 184 s.savedLayers = make(map[layer.DiffID]distribution.Descriptor) 185 186 // get image json 187 tempDir, err := os.MkdirTemp("", "docker-export-") 188 if err != nil { 189 return err 190 } 191 defer os.RemoveAll(tempDir) 192 193 s.outDir = tempDir 194 reposLegacy := make(map[string]map[string]string) 195 196 var manifest []manifestItem 197 var parentLinks []parentLink 198 199 var manifestDescriptors []ocispec.Descriptor 200 201 for id, imageDescr := range s.images { 202 foreignSrcs, err := s.saveImage(id) 203 if err != nil { 204 return err 205 } 206 207 var ( 208 repoTags []string 209 layers []string 210 foreign = make([]ocispec.Descriptor, 0, len(foreignSrcs)) 211 ) 212 213 // Layers in manifest must follow the actual layer order from config. 214 for _, l := range imageDescr.layers { 215 desc := foreignSrcs[l] 216 foreign = append(foreign, ocispec.Descriptor{ 217 MediaType: desc.MediaType, 218 Digest: desc.Digest, 219 Size: desc.Size, 220 URLs: desc.URLs, 221 Annotations: desc.Annotations, 222 Platform: desc.Platform, 223 }) 224 } 225 226 m := ocispec.Manifest{ 227 Versioned: specs.Versioned{ 228 SchemaVersion: 2, 229 }, 230 MediaType: ocispec.MediaTypeImageManifest, 231 Config: ocispec.Descriptor{ 232 MediaType: ocispec.MediaTypeImageConfig, 233 Digest: digest.Digest(imageDescr.image.ID()), 234 Size: int64(len(imageDescr.image.RawJSON())), 235 }, 236 Layers: foreign, 237 } 238 239 data, err := json.Marshal(m) 240 if err != nil { 241 return errors.Wrap(err, "error marshaling manifest") 242 } 243 dgst := digest.FromBytes(data) 244 245 mFile := filepath.Join(s.outDir, ocispec.ImageBlobsDir, dgst.Algorithm().String(), dgst.Encoded()) 246 if err := os.MkdirAll(filepath.Dir(mFile), 0o755); err != nil { 247 return errors.Wrap(err, "error creating blob directory") 248 } 249 if err := system.Chtimes(filepath.Dir(mFile), time.Unix(0, 0), time.Unix(0, 0)); err != nil { 250 return errors.Wrap(err, "error setting blob directory timestamps") 251 } 252 if err := os.WriteFile(mFile, data, 0o644); err != nil { 253 return errors.Wrap(err, "error writing oci manifest file") 254 } 255 if err := system.Chtimes(mFile, time.Unix(0, 0), time.Unix(0, 0)); err != nil { 256 return errors.Wrap(err, "error setting blob directory timestamps") 257 } 258 size := int64(len(data)) 259 260 untaggedMfstDesc := ocispec.Descriptor{ 261 MediaType: ocispec.MediaTypeImageManifest, 262 Digest: dgst, 263 Size: size, 264 Platform: m.Config.Platform, 265 } 266 for _, ref := range imageDescr.refs { 267 familiarName := reference.FamiliarName(ref) 268 if _, ok := reposLegacy[familiarName]; !ok { 269 reposLegacy[familiarName] = make(map[string]string) 270 } 271 reposLegacy[familiarName][ref.Tag()] = digest.Digest(imageDescr.layers[len(imageDescr.layers)-1]).Encoded() 272 repoTags = append(repoTags, reference.FamiliarString(ref)) 273 274 taggedManifest := untaggedMfstDesc 275 taggedManifest.Annotations = map[string]string{ 276 images.AnnotationImageName: ref.String(), 277 ocispec.AnnotationRefName: ref.Tag(), 278 } 279 manifestDescriptors = append(manifestDescriptors, taggedManifest) 280 } 281 282 // If no ref was assigned, make sure still add the image is still included in index.json. 283 if len(manifestDescriptors) == 0 { 284 manifestDescriptors = append(manifestDescriptors, untaggedMfstDesc) 285 } 286 287 for _, l := range imageDescr.layers { 288 // IMPORTANT: We use path, not filepath here to ensure the layers 289 // in the manifest use Unix-style forward-slashes. 290 lDgst := digest.Digest(l) 291 layers = append(layers, path.Join(ocispec.ImageBlobsDir, lDgst.Algorithm().String(), lDgst.Encoded())) 292 } 293 294 manifest = append(manifest, manifestItem{ 295 Config: path.Join(ocispec.ImageBlobsDir, id.Digest().Algorithm().String(), id.Digest().Encoded()), 296 RepoTags: repoTags, 297 Layers: layers, 298 LayerSources: foreignSrcs, 299 }) 300 301 parentID, _ := s.is.GetParent(id) 302 parentLinks = append(parentLinks, parentLink{id, parentID}) 303 s.tarexporter.loggerImgEvent.LogImageEvent(id.String(), id.String(), events.ActionSave) 304 } 305 306 for i, p := range validatedParentLinks(parentLinks) { 307 if p.parentID != "" { 308 manifest[i].Parent = p.parentID 309 } 310 } 311 312 if len(reposLegacy) > 0 { 313 reposFile := filepath.Join(tempDir, legacyRepositoriesFileName) 314 rf, err := os.OpenFile(reposFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o644) 315 if err != nil { 316 return err 317 } 318 319 if err := json.NewEncoder(rf).Encode(reposLegacy); err != nil { 320 rf.Close() 321 return err 322 } 323 324 rf.Close() 325 326 if err := system.Chtimes(reposFile, time.Unix(0, 0), time.Unix(0, 0)); err != nil { 327 return err 328 } 329 } 330 331 manifestPath := filepath.Join(tempDir, manifestFileName) 332 f, err := os.OpenFile(manifestPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o644) 333 if err != nil { 334 return err 335 } 336 337 if err := json.NewEncoder(f).Encode(manifest); err != nil { 338 f.Close() 339 return err 340 } 341 342 f.Close() 343 344 if err := system.Chtimes(manifestPath, time.Unix(0, 0), time.Unix(0, 0)); err != nil { 345 return err 346 } 347 348 const ociLayoutContent = `{"imageLayoutVersion": "` + ocispec.ImageLayoutVersion + `"}` 349 layoutPath := filepath.Join(tempDir, ocispec.ImageLayoutFile) 350 if err := os.WriteFile(layoutPath, []byte(ociLayoutContent), 0o644); err != nil { 351 return errors.Wrap(err, "error writing oci layout file") 352 } 353 if err := system.Chtimes(layoutPath, time.Unix(0, 0), time.Unix(0, 0)); err != nil { 354 return errors.Wrap(err, "error setting oci layout file timestamps") 355 } 356 357 data, err := json.Marshal(ocispec.Index{ 358 Versioned: specs.Versioned{ 359 SchemaVersion: 2, 360 }, 361 MediaType: ocispec.MediaTypeImageIndex, 362 Manifests: manifestDescriptors, 363 }) 364 if err != nil { 365 return errors.Wrap(err, "error marshaling oci index") 366 } 367 368 idxFile := filepath.Join(s.outDir, ocispec.ImageIndexFile) 369 if err := os.WriteFile(idxFile, data, 0o644); err != nil { 370 return errors.Wrap(err, "error writing oci index file") 371 } 372 373 fs, err := archive.Tar(tempDir, archive.Uncompressed) 374 if err != nil { 375 return err 376 } 377 defer fs.Close() 378 379 _, err = io.Copy(outStream, fs) 380 return err 381 } 382 383 func (s *saveSession) saveImage(id image.ID) (map[layer.DiffID]distribution.Descriptor, error) { 384 img := s.images[id].image 385 if len(img.RootFS.DiffIDs) == 0 { 386 return nil, fmt.Errorf("empty export - not implemented") 387 } 388 389 var parent digest.Digest 390 var layers []layer.DiffID 391 var foreignSrcs map[layer.DiffID]distribution.Descriptor 392 for i, diffID := range img.RootFS.DiffIDs { 393 v1ImgCreated := time.Unix(0, 0) 394 v1Img := image.V1Image{ 395 // This is for backward compatibility used for 396 // pre v1.9 docker. 397 Created: &v1ImgCreated, 398 } 399 if i == len(img.RootFS.DiffIDs)-1 { 400 v1Img = img.V1Image 401 } 402 rootFS := *img.RootFS 403 rootFS.DiffIDs = rootFS.DiffIDs[:i+1] 404 v1ID, err := v1.CreateID(v1Img, rootFS.ChainID(), parent) 405 if err != nil { 406 return nil, err 407 } 408 409 v1Img.ID = v1ID.Encoded() 410 if parent != "" { 411 v1Img.Parent = parent.Encoded() 412 } 413 414 v1Img.OS = img.OS 415 src, err := s.saveConfigAndLayer(rootFS.ChainID(), v1Img, img.Created) 416 if err != nil { 417 return nil, err 418 } 419 420 layers = append(layers, diffID) 421 parent = v1ID 422 if src.Digest != "" { 423 if foreignSrcs == nil { 424 foreignSrcs = make(map[layer.DiffID]distribution.Descriptor) 425 } 426 foreignSrcs[img.RootFS.DiffIDs[i]] = src 427 } 428 } 429 430 data := img.RawJSON() 431 dgst := digest.FromBytes(data) 432 433 blobDir := filepath.Join(s.outDir, ocispec.ImageBlobsDir, dgst.Algorithm().String()) 434 if err := os.MkdirAll(blobDir, 0o755); err != nil { 435 return nil, err 436 } 437 if img.Created != nil { 438 if err := system.Chtimes(blobDir, *img.Created, *img.Created); err != nil { 439 return nil, err 440 } 441 if err := system.Chtimes(filepath.Dir(blobDir), *img.Created, *img.Created); err != nil { 442 return nil, err 443 } 444 } 445 446 configFile := filepath.Join(blobDir, dgst.Encoded()) 447 if err := os.WriteFile(configFile, img.RawJSON(), 0o644); err != nil { 448 return nil, err 449 } 450 if img.Created != nil { 451 if err := system.Chtimes(configFile, *img.Created, *img.Created); err != nil { 452 return nil, err 453 } 454 } 455 456 s.images[id].layers = layers 457 return foreignSrcs, nil 458 } 459 460 func (s *saveSession) saveConfigAndLayer(id layer.ChainID, legacyImg image.V1Image, createdTime *time.Time) (distribution.Descriptor, error) { 461 outDir := filepath.Join(s.outDir, ocispec.ImageBlobsDir) 462 463 if _, ok := s.savedConfigs[legacyImg.ID]; !ok { 464 if err := s.saveConfig(legacyImg, outDir, createdTime); err != nil { 465 return distribution.Descriptor{}, err 466 } 467 } 468 469 // serialize filesystem 470 l, err := s.lss.Get(id) 471 if err != nil { 472 return distribution.Descriptor{}, err 473 } 474 475 lDiffID := l.DiffID() 476 lDgst := digest.Digest(lDiffID) 477 if _, ok := s.savedLayers[lDiffID]; ok { 478 return s.savedLayers[lDiffID], nil 479 } 480 layerPath := filepath.Join(outDir, lDgst.Algorithm().String(), lDgst.Encoded()) 481 defer layer.ReleaseAndLog(s.lss, l) 482 483 if _, err = os.Stat(layerPath); err == nil { 484 // This is should not happen. If the layer path was already created, we should have returned early. 485 // Log a warning an proceed to recreate the archive. 486 log.G(context.TODO()).WithFields(log.Fields{ 487 "layerPath": layerPath, 488 "id": id, 489 "lDgst": lDgst, 490 }).Warn("LayerPath already exists but the descriptor is not cached") 491 } else if !os.IsNotExist(err) { 492 return distribution.Descriptor{}, err 493 } 494 495 // We use sequential file access to avoid depleting the standby list on 496 // Windows. On Linux, this equates to a regular os.Create. 497 if err := os.MkdirAll(filepath.Dir(layerPath), 0o755); err != nil { 498 return distribution.Descriptor{}, errors.Wrap(err, "could not create layer dir parent") 499 } 500 tarFile, err := sequential.Create(layerPath) 501 if err != nil { 502 return distribution.Descriptor{}, errors.Wrap(err, "error creating layer file") 503 } 504 defer tarFile.Close() 505 506 arch, err := l.TarStream() 507 if err != nil { 508 return distribution.Descriptor{}, err 509 } 510 defer arch.Close() 511 512 digester := digest.Canonical.Digester() 513 digestedArch := io.TeeReader(arch, digester.Hash()) 514 515 tarSize, err := io.Copy(tarFile, digestedArch) 516 if err != nil { 517 return distribution.Descriptor{}, err 518 } 519 520 tarDigest := digester.Digest() 521 if lDgst != tarDigest { 522 log.G(context.TODO()).WithFields(log.Fields{ 523 "layerDigest": lDgst, 524 "actualDigest": tarDigest, 525 }).Warn("layer digest doesn't match its tar archive digest") 526 527 lDgst = digester.Digest() 528 layerPath = filepath.Join(outDir, lDgst.Algorithm().String(), lDgst.Encoded()) 529 } 530 531 if createdTime != nil { 532 for _, fname := range []string{outDir, layerPath} { 533 // todo: maybe save layer created timestamp? 534 if err := system.Chtimes(fname, *createdTime, *createdTime); err != nil { 535 return distribution.Descriptor{}, errors.Wrap(err, "could not set layer timestamp") 536 } 537 } 538 } 539 540 var desc distribution.Descriptor 541 if fs, ok := l.(distribution.Describable); ok { 542 desc = fs.Descriptor() 543 } 544 545 if desc.Digest == "" { 546 desc.Digest = tarDigest 547 desc.Size = tarSize 548 } 549 if desc.MediaType == "" { 550 desc.MediaType = ocispec.MediaTypeImageLayer 551 } 552 s.savedLayers[lDiffID] = desc 553 554 return desc, nil 555 } 556 557 func (s *saveSession) saveConfig(legacyImg image.V1Image, outDir string, createdTime *time.Time) error { 558 imageConfig, err := json.Marshal(legacyImg) 559 if err != nil { 560 return err 561 } 562 563 cfgDgst := digest.FromBytes(imageConfig) 564 configPath := filepath.Join(outDir, cfgDgst.Algorithm().String(), cfgDgst.Encoded()) 565 if err := os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil { 566 return errors.Wrap(err, "could not create layer dir parent") 567 } 568 569 if err := os.WriteFile(configPath, imageConfig, 0o644); err != nil { 570 return err 571 } 572 573 if createdTime != nil { 574 if err := system.Chtimes(configPath, *createdTime, *createdTime); err != nil { 575 return errors.Wrap(err, "could not set config timestamp") 576 } 577 } 578 579 s.savedConfigs[legacyImg.ID] = struct{}{} 580 return nil 581 }