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  }