github.com/rish1988/moby@v25.0.2+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 imgPlat := imageDescr.image.Platform() 227 228 m := ocispec.Manifest{ 229 Versioned: specs.Versioned{ 230 SchemaVersion: 2, 231 }, 232 MediaType: ocispec.MediaTypeImageManifest, 233 Config: ocispec.Descriptor{ 234 MediaType: ocispec.MediaTypeImageConfig, 235 Digest: digest.Digest(imageDescr.image.ID()), 236 Size: int64(len(imageDescr.image.RawJSON())), 237 Platform: &imgPlat, 238 }, 239 Layers: foreign, 240 } 241 242 data, err := json.Marshal(m) 243 if err != nil { 244 return errors.Wrap(err, "error marshaling manifest") 245 } 246 dgst := digest.FromBytes(data) 247 248 mFile := filepath.Join(s.outDir, ocispec.ImageBlobsDir, dgst.Algorithm().String(), dgst.Encoded()) 249 if err := os.MkdirAll(filepath.Dir(mFile), 0o755); err != nil { 250 return errors.Wrap(err, "error creating blob directory") 251 } 252 if err := system.Chtimes(filepath.Dir(mFile), time.Unix(0, 0), time.Unix(0, 0)); err != nil { 253 return errors.Wrap(err, "error setting blob directory timestamps") 254 } 255 if err := os.WriteFile(mFile, data, 0o644); err != nil { 256 return errors.Wrap(err, "error writing oci manifest file") 257 } 258 if err := system.Chtimes(mFile, time.Unix(0, 0), time.Unix(0, 0)); err != nil { 259 return errors.Wrap(err, "error setting blob directory timestamps") 260 } 261 size := int64(len(data)) 262 263 for _, ref := range imageDescr.refs { 264 familiarName := reference.FamiliarName(ref) 265 if _, ok := reposLegacy[familiarName]; !ok { 266 reposLegacy[familiarName] = make(map[string]string) 267 } 268 reposLegacy[familiarName][ref.Tag()] = digest.Digest(imageDescr.layers[len(imageDescr.layers)-1]).Encoded() 269 repoTags = append(repoTags, reference.FamiliarString(ref)) 270 271 manifestDescriptors = append(manifestDescriptors, ocispec.Descriptor{ 272 MediaType: ocispec.MediaTypeImageManifest, 273 Digest: dgst, 274 Size: size, 275 Platform: m.Config.Platform, 276 Annotations: map[string]string{ 277 images.AnnotationImageName: ref.String(), 278 ocispec.AnnotationRefName: ref.Tag(), 279 }, 280 }) 281 } 282 283 for _, l := range imageDescr.layers { 284 // IMPORTANT: We use path, not filepath here to ensure the layers 285 // in the manifest use Unix-style forward-slashes. 286 lDgst := digest.Digest(l) 287 layers = append(layers, path.Join(ocispec.ImageBlobsDir, lDgst.Algorithm().String(), lDgst.Encoded())) 288 } 289 290 manifest = append(manifest, manifestItem{ 291 Config: path.Join(ocispec.ImageBlobsDir, id.Digest().Algorithm().String(), id.Digest().Encoded()), 292 RepoTags: repoTags, 293 Layers: layers, 294 LayerSources: foreignSrcs, 295 }) 296 297 parentID, _ := s.is.GetParent(id) 298 parentLinks = append(parentLinks, parentLink{id, parentID}) 299 s.tarexporter.loggerImgEvent.LogImageEvent(id.String(), id.String(), events.ActionSave) 300 } 301 302 for i, p := range validatedParentLinks(parentLinks) { 303 if p.parentID != "" { 304 manifest[i].Parent = p.parentID 305 } 306 } 307 308 if len(reposLegacy) > 0 { 309 reposFile := filepath.Join(tempDir, legacyRepositoriesFileName) 310 rf, err := os.OpenFile(reposFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o644) 311 if err != nil { 312 return err 313 } 314 315 if err := json.NewEncoder(rf).Encode(reposLegacy); err != nil { 316 rf.Close() 317 return err 318 } 319 320 rf.Close() 321 322 if err := system.Chtimes(reposFile, time.Unix(0, 0), time.Unix(0, 0)); err != nil { 323 return err 324 } 325 } 326 327 manifestPath := filepath.Join(tempDir, manifestFileName) 328 f, err := os.OpenFile(manifestPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o644) 329 if err != nil { 330 return err 331 } 332 333 if err := json.NewEncoder(f).Encode(manifest); err != nil { 334 f.Close() 335 return err 336 } 337 338 f.Close() 339 340 if err := system.Chtimes(manifestPath, time.Unix(0, 0), time.Unix(0, 0)); err != nil { 341 return err 342 } 343 344 const ociLayoutContent = `{"imageLayoutVersion": "` + ocispec.ImageLayoutVersion + `"}` 345 layoutPath := filepath.Join(tempDir, ocispec.ImageLayoutFile) 346 if err := os.WriteFile(layoutPath, []byte(ociLayoutContent), 0o644); err != nil { 347 return errors.Wrap(err, "error writing oci layout file") 348 } 349 if err := system.Chtimes(layoutPath, time.Unix(0, 0), time.Unix(0, 0)); err != nil { 350 return errors.Wrap(err, "error setting oci layout file timestamps") 351 } 352 353 data, err := json.Marshal(ocispec.Index{ 354 Versioned: specs.Versioned{ 355 SchemaVersion: 2, 356 }, 357 MediaType: ocispec.MediaTypeImageIndex, 358 Manifests: manifestDescriptors, 359 }) 360 if err != nil { 361 return errors.Wrap(err, "error marshaling oci index") 362 } 363 364 idxFile := filepath.Join(s.outDir, ocispec.ImageIndexFile) 365 if err := os.WriteFile(idxFile, data, 0o644); err != nil { 366 return errors.Wrap(err, "error writing oci index file") 367 } 368 369 fs, err := archive.Tar(tempDir, archive.Uncompressed) 370 if err != nil { 371 return err 372 } 373 defer fs.Close() 374 375 _, err = io.Copy(outStream, fs) 376 return err 377 } 378 379 func (s *saveSession) saveImage(id image.ID) (map[layer.DiffID]distribution.Descriptor, error) { 380 img := s.images[id].image 381 if len(img.RootFS.DiffIDs) == 0 { 382 return nil, fmt.Errorf("empty export - not implemented") 383 } 384 385 var parent digest.Digest 386 var layers []layer.DiffID 387 var foreignSrcs map[layer.DiffID]distribution.Descriptor 388 for i, diffID := range img.RootFS.DiffIDs { 389 v1ImgCreated := time.Unix(0, 0) 390 v1Img := image.V1Image{ 391 // This is for backward compatibility used for 392 // pre v1.9 docker. 393 Created: &v1ImgCreated, 394 } 395 if i == len(img.RootFS.DiffIDs)-1 { 396 v1Img = img.V1Image 397 } 398 rootFS := *img.RootFS 399 rootFS.DiffIDs = rootFS.DiffIDs[:i+1] 400 v1ID, err := v1.CreateID(v1Img, rootFS.ChainID(), parent) 401 if err != nil { 402 return nil, err 403 } 404 405 v1Img.ID = v1ID.Encoded() 406 if parent != "" { 407 v1Img.Parent = parent.Encoded() 408 } 409 410 v1Img.OS = img.OS 411 src, err := s.saveConfigAndLayer(rootFS.ChainID(), v1Img, img.Created) 412 if err != nil { 413 return nil, err 414 } 415 416 layers = append(layers, diffID) 417 parent = v1ID 418 if src.Digest != "" { 419 if foreignSrcs == nil { 420 foreignSrcs = make(map[layer.DiffID]distribution.Descriptor) 421 } 422 foreignSrcs[img.RootFS.DiffIDs[i]] = src 423 } 424 } 425 426 data := img.RawJSON() 427 dgst := digest.FromBytes(data) 428 429 blobDir := filepath.Join(s.outDir, ocispec.ImageBlobsDir, dgst.Algorithm().String()) 430 if err := os.MkdirAll(blobDir, 0o755); err != nil { 431 return nil, err 432 } 433 if img.Created != nil { 434 if err := system.Chtimes(blobDir, *img.Created, *img.Created); err != nil { 435 return nil, err 436 } 437 if err := system.Chtimes(filepath.Dir(blobDir), *img.Created, *img.Created); err != nil { 438 return nil, err 439 } 440 } 441 442 configFile := filepath.Join(blobDir, dgst.Encoded()) 443 if err := os.WriteFile(configFile, img.RawJSON(), 0o644); err != nil { 444 return nil, err 445 } 446 if img.Created != nil { 447 if err := system.Chtimes(configFile, *img.Created, *img.Created); err != nil { 448 return nil, err 449 } 450 } 451 452 s.images[id].layers = layers 453 return foreignSrcs, nil 454 } 455 456 func (s *saveSession) saveConfigAndLayer(id layer.ChainID, legacyImg image.V1Image, createdTime *time.Time) (distribution.Descriptor, error) { 457 outDir := filepath.Join(s.outDir, ocispec.ImageBlobsDir) 458 459 if _, ok := s.savedConfigs[legacyImg.ID]; !ok { 460 if err := s.saveConfig(legacyImg, outDir, createdTime); err != nil { 461 return distribution.Descriptor{}, err 462 } 463 } 464 465 // serialize filesystem 466 l, err := s.lss.Get(id) 467 if err != nil { 468 return distribution.Descriptor{}, err 469 } 470 471 lDiffID := l.DiffID() 472 lDgst := digest.Digest(lDiffID) 473 if _, ok := s.savedLayers[lDiffID]; ok { 474 return s.savedLayers[lDiffID], nil 475 } 476 layerPath := filepath.Join(outDir, lDgst.Algorithm().String(), lDgst.Encoded()) 477 defer layer.ReleaseAndLog(s.lss, l) 478 479 if _, err = os.Stat(layerPath); err == nil { 480 // This is should not happen. If the layer path was already created, we should have returned early. 481 // Log a warning an proceed to recreate the archive. 482 log.G(context.TODO()).WithFields(log.Fields{ 483 "layerPath": layerPath, 484 "id": id, 485 "lDgst": lDgst, 486 }).Warn("LayerPath already exists but the descriptor is not cached") 487 } else if !os.IsNotExist(err) { 488 return distribution.Descriptor{}, err 489 } 490 491 // We use sequential file access to avoid depleting the standby list on 492 // Windows. On Linux, this equates to a regular os.Create. 493 if err := os.MkdirAll(filepath.Dir(layerPath), 0o755); err != nil { 494 return distribution.Descriptor{}, errors.Wrap(err, "could not create layer dir parent") 495 } 496 tarFile, err := sequential.Create(layerPath) 497 if err != nil { 498 return distribution.Descriptor{}, errors.Wrap(err, "error creating layer file") 499 } 500 defer tarFile.Close() 501 502 arch, err := l.TarStream() 503 if err != nil { 504 return distribution.Descriptor{}, err 505 } 506 defer arch.Close() 507 508 digester := digest.Canonical.Digester() 509 digestedArch := io.TeeReader(arch, digester.Hash()) 510 511 tarSize, err := io.Copy(tarFile, digestedArch) 512 if err != nil { 513 return distribution.Descriptor{}, err 514 } 515 516 tarDigest := digester.Digest() 517 if lDgst != tarDigest { 518 log.G(context.TODO()).WithFields(log.Fields{ 519 "layerDigest": lDgst, 520 "actualDigest": tarDigest, 521 }).Warn("layer digest doesn't match its tar archive digest") 522 523 lDgst = digester.Digest() 524 layerPath = filepath.Join(outDir, lDgst.Algorithm().String(), lDgst.Encoded()) 525 } 526 527 if createdTime != nil { 528 for _, fname := range []string{outDir, layerPath} { 529 // todo: maybe save layer created timestamp? 530 if err := system.Chtimes(fname, *createdTime, *createdTime); err != nil { 531 return distribution.Descriptor{}, errors.Wrap(err, "could not set layer timestamp") 532 } 533 } 534 } 535 536 var desc distribution.Descriptor 537 if fs, ok := l.(distribution.Describable); ok { 538 desc = fs.Descriptor() 539 } 540 541 if desc.Digest == "" { 542 desc.Digest = tarDigest 543 desc.Size = tarSize 544 } 545 if desc.MediaType == "" { 546 desc.MediaType = ocispec.MediaTypeImageLayer 547 } 548 s.savedLayers[lDiffID] = desc 549 550 return desc, nil 551 } 552 553 func (s *saveSession) saveConfig(legacyImg image.V1Image, outDir string, createdTime *time.Time) error { 554 imageConfig, err := json.Marshal(legacyImg) 555 if err != nil { 556 return err 557 } 558 559 cfgDgst := digest.FromBytes(imageConfig) 560 configPath := filepath.Join(outDir, cfgDgst.Algorithm().String(), cfgDgst.Encoded()) 561 if err := os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil { 562 return errors.Wrap(err, "could not create layer dir parent") 563 } 564 565 if err := os.WriteFile(configPath, imageConfig, 0o644); err != nil { 566 return err 567 } 568 569 if createdTime != nil { 570 if err := system.Chtimes(configPath, *createdTime, *createdTime); err != nil { 571 return errors.Wrap(err, "could not set config timestamp") 572 } 573 } 574 575 s.savedConfigs[legacyImg.ID] = struct{}{} 576 return nil 577 }