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