github.com/docker/engine@v22.0.0-20211208180946-d456264580cf+incompatible/image/tarexport/load.go (about)

     1  package tarexport // import "github.com/docker/docker/image/tarexport"
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"path/filepath"
    10  	"runtime"
    11  
    12  	"github.com/docker/distribution"
    13  	"github.com/docker/distribution/reference"
    14  	"github.com/docker/docker/image"
    15  	v1 "github.com/docker/docker/image/v1"
    16  	"github.com/docker/docker/layer"
    17  	"github.com/docker/docker/pkg/archive"
    18  	"github.com/docker/docker/pkg/chrootarchive"
    19  	"github.com/docker/docker/pkg/progress"
    20  	"github.com/docker/docker/pkg/streamformatter"
    21  	"github.com/docker/docker/pkg/stringid"
    22  	"github.com/docker/docker/pkg/system"
    23  	"github.com/moby/sys/symlink"
    24  	digest "github.com/opencontainers/go-digest"
    25  	"github.com/sirupsen/logrus"
    26  )
    27  
    28  func (l *tarexporter) Load(inTar io.ReadCloser, outStream io.Writer, quiet bool) error {
    29  	var progressOutput progress.Output
    30  	if !quiet {
    31  		progressOutput = streamformatter.NewJSONProgressOutput(outStream, false)
    32  	}
    33  	outStream = streamformatter.NewStdoutWriter(outStream)
    34  
    35  	tmpDir, err := os.MkdirTemp("", "docker-import-")
    36  	if err != nil {
    37  		return err
    38  	}
    39  	defer os.RemoveAll(tmpDir)
    40  
    41  	if err := chrootarchive.Untar(inTar, tmpDir, nil); err != nil {
    42  		return err
    43  	}
    44  	// read manifest, if no file then load in legacy mode
    45  	manifestPath, err := safePath(tmpDir, manifestFileName)
    46  	if err != nil {
    47  		return err
    48  	}
    49  	manifestFile, err := os.Open(manifestPath)
    50  	if err != nil {
    51  		if os.IsNotExist(err) {
    52  			return l.legacyLoad(tmpDir, outStream, progressOutput)
    53  		}
    54  		return err
    55  	}
    56  	defer manifestFile.Close()
    57  
    58  	var manifest []manifestItem
    59  	if err := json.NewDecoder(manifestFile).Decode(&manifest); err != nil {
    60  		return err
    61  	}
    62  
    63  	if err := validateManifest(manifest); err != nil {
    64  		return err
    65  	}
    66  
    67  	var parentLinks []parentLink
    68  	var imageIDsStr string
    69  	var imageRefCount int
    70  
    71  	for _, m := range manifest {
    72  		configPath, err := safePath(tmpDir, m.Config)
    73  		if err != nil {
    74  			return err
    75  		}
    76  		config, err := os.ReadFile(configPath)
    77  		if err != nil {
    78  			return err
    79  		}
    80  		img, err := image.NewFromJSON(config)
    81  		if err != nil {
    82  			return err
    83  		}
    84  		if !system.IsOSSupported(img.OperatingSystem()) {
    85  			return fmt.Errorf("cannot load %s image on %s", img.OperatingSystem(), runtime.GOOS)
    86  		}
    87  		rootFS := *img.RootFS
    88  		rootFS.DiffIDs = nil
    89  
    90  		if expected, actual := len(m.Layers), len(img.RootFS.DiffIDs); expected != actual {
    91  			return fmt.Errorf("invalid manifest, layers length mismatch: expected %d, got %d", expected, actual)
    92  		}
    93  
    94  		for i, diffID := range img.RootFS.DiffIDs {
    95  			layerPath, err := safePath(tmpDir, m.Layers[i])
    96  			if err != nil {
    97  				return err
    98  			}
    99  			r := rootFS
   100  			r.Append(diffID)
   101  			newLayer, err := l.lss.Get(r.ChainID())
   102  			if err != nil {
   103  				newLayer, err = l.loadLayer(layerPath, rootFS, diffID.String(), m.LayerSources[diffID], progressOutput)
   104  				if err != nil {
   105  					return err
   106  				}
   107  			}
   108  			defer layer.ReleaseAndLog(l.lss, newLayer)
   109  			if expected, actual := diffID, newLayer.DiffID(); expected != actual {
   110  				return fmt.Errorf("invalid diffID for layer %d: expected %q, got %q", i, expected, actual)
   111  			}
   112  			rootFS.Append(diffID)
   113  		}
   114  
   115  		imgID, err := l.is.Create(config)
   116  		if err != nil {
   117  			return err
   118  		}
   119  		imageIDsStr += fmt.Sprintf("Loaded image ID: %s\n", imgID)
   120  
   121  		imageRefCount = 0
   122  		for _, repoTag := range m.RepoTags {
   123  			named, err := reference.ParseNormalizedNamed(repoTag)
   124  			if err != nil {
   125  				return err
   126  			}
   127  			ref, ok := named.(reference.NamedTagged)
   128  			if !ok {
   129  				return fmt.Errorf("invalid tag %q", repoTag)
   130  			}
   131  			l.setLoadedTag(ref, imgID.Digest(), outStream)
   132  			outStream.Write([]byte(fmt.Sprintf("Loaded image: %s\n", reference.FamiliarString(ref))))
   133  			imageRefCount++
   134  		}
   135  
   136  		parentLinks = append(parentLinks, parentLink{imgID, m.Parent})
   137  		l.loggerImgEvent.LogImageEvent(imgID.String(), imgID.String(), "load")
   138  	}
   139  
   140  	for _, p := range validatedParentLinks(parentLinks) {
   141  		if p.parentID != "" {
   142  			if err := l.setParentID(p.id, p.parentID); err != nil {
   143  				return err
   144  			}
   145  		}
   146  	}
   147  
   148  	if imageRefCount == 0 {
   149  		outStream.Write([]byte(imageIDsStr))
   150  	}
   151  
   152  	return nil
   153  }
   154  
   155  func (l *tarexporter) setParentID(id, parentID image.ID) error {
   156  	img, err := l.is.Get(id)
   157  	if err != nil {
   158  		return err
   159  	}
   160  	parent, err := l.is.Get(parentID)
   161  	if err != nil {
   162  		return err
   163  	}
   164  	if !checkValidParent(img, parent) {
   165  		return fmt.Errorf("image %v is not a valid parent for %v", parent.ID(), img.ID())
   166  	}
   167  	return l.is.SetParent(id, parentID)
   168  }
   169  
   170  func (l *tarexporter) loadLayer(filename string, rootFS image.RootFS, id string, foreignSrc distribution.Descriptor, progressOutput progress.Output) (layer.Layer, error) {
   171  	// We use system.OpenSequential to use sequential file access on Windows, avoiding
   172  	// depleting the standby list. On Linux, this equates to a regular os.Open.
   173  	rawTar, err := system.OpenSequential(filename)
   174  	if err != nil {
   175  		logrus.Debugf("Error reading embedded tar: %v", err)
   176  		return nil, err
   177  	}
   178  	defer rawTar.Close()
   179  
   180  	var r io.Reader
   181  	if progressOutput != nil {
   182  		fileInfo, err := rawTar.Stat()
   183  		if err != nil {
   184  			logrus.Debugf("Error statting file: %v", err)
   185  			return nil, err
   186  		}
   187  
   188  		r = progress.NewProgressReader(rawTar, progressOutput, fileInfo.Size(), stringid.TruncateID(id), "Loading layer")
   189  	} else {
   190  		r = rawTar
   191  	}
   192  
   193  	inflatedLayerData, err := archive.DecompressStream(r)
   194  	if err != nil {
   195  		return nil, err
   196  	}
   197  	defer inflatedLayerData.Close()
   198  
   199  	if ds, ok := l.lss.(layer.DescribableStore); ok {
   200  		return ds.RegisterWithDescriptor(inflatedLayerData, rootFS.ChainID(), foreignSrc)
   201  	}
   202  	return l.lss.Register(inflatedLayerData, rootFS.ChainID())
   203  }
   204  
   205  func (l *tarexporter) setLoadedTag(ref reference.Named, imgID digest.Digest, outStream io.Writer) error {
   206  	if prevID, err := l.rs.Get(ref); err == nil && prevID != imgID {
   207  		fmt.Fprintf(outStream, "The image %s already exists, renaming the old one with ID %s to empty string\n", reference.FamiliarString(ref), string(prevID)) // todo: this message is wrong in case of multiple tags
   208  	}
   209  
   210  	return l.rs.AddTag(ref, imgID, true)
   211  }
   212  
   213  func (l *tarexporter) legacyLoad(tmpDir string, outStream io.Writer, progressOutput progress.Output) error {
   214  	if runtime.GOOS == "windows" {
   215  		return errors.New("Windows does not support legacy loading of images")
   216  	}
   217  
   218  	legacyLoadedMap := make(map[string]image.ID)
   219  
   220  	dirs, err := os.ReadDir(tmpDir)
   221  	if err != nil {
   222  		return err
   223  	}
   224  
   225  	// every dir represents an image
   226  	for _, d := range dirs {
   227  		if d.IsDir() {
   228  			if err := l.legacyLoadImage(d.Name(), tmpDir, legacyLoadedMap, progressOutput); err != nil {
   229  				return err
   230  			}
   231  		}
   232  	}
   233  
   234  	// load tags from repositories file
   235  	repositoriesPath, err := safePath(tmpDir, legacyRepositoriesFileName)
   236  	if err != nil {
   237  		return err
   238  	}
   239  	repositoriesFile, err := os.Open(repositoriesPath)
   240  	if err != nil {
   241  		return err
   242  	}
   243  	defer repositoriesFile.Close()
   244  
   245  	repositories := make(map[string]map[string]string)
   246  	if err := json.NewDecoder(repositoriesFile).Decode(&repositories); err != nil {
   247  		return err
   248  	}
   249  
   250  	for name, tagMap := range repositories {
   251  		for tag, oldID := range tagMap {
   252  			imgID, ok := legacyLoadedMap[oldID]
   253  			if !ok {
   254  				return fmt.Errorf("invalid target ID: %v", oldID)
   255  			}
   256  			named, err := reference.ParseNormalizedNamed(name)
   257  			if err != nil {
   258  				return err
   259  			}
   260  			ref, err := reference.WithTag(named, tag)
   261  			if err != nil {
   262  				return err
   263  			}
   264  			l.setLoadedTag(ref, imgID.Digest(), outStream)
   265  		}
   266  	}
   267  
   268  	return nil
   269  }
   270  
   271  func (l *tarexporter) legacyLoadImage(oldID, sourceDir string, loadedMap map[string]image.ID, progressOutput progress.Output) error {
   272  	if _, loaded := loadedMap[oldID]; loaded {
   273  		return nil
   274  	}
   275  	configPath, err := safePath(sourceDir, filepath.Join(oldID, legacyConfigFileName))
   276  	if err != nil {
   277  		return err
   278  	}
   279  	imageJSON, err := os.ReadFile(configPath)
   280  	if err != nil {
   281  		logrus.Debugf("Error reading json: %v", err)
   282  		return err
   283  	}
   284  
   285  	var img struct {
   286  		OS     string
   287  		Parent string
   288  	}
   289  	if err := json.Unmarshal(imageJSON, &img); err != nil {
   290  		return err
   291  	}
   292  
   293  	if img.OS == "" {
   294  		img.OS = runtime.GOOS
   295  	}
   296  	if !system.IsOSSupported(img.OS) {
   297  		return fmt.Errorf("cannot load %s image on %s", img.OS, runtime.GOOS)
   298  	}
   299  
   300  	var parentID image.ID
   301  	if img.Parent != "" {
   302  		for {
   303  			var loaded bool
   304  			if parentID, loaded = loadedMap[img.Parent]; !loaded {
   305  				if err := l.legacyLoadImage(img.Parent, sourceDir, loadedMap, progressOutput); err != nil {
   306  					return err
   307  				}
   308  			} else {
   309  				break
   310  			}
   311  		}
   312  	}
   313  
   314  	// todo: try to connect with migrate code
   315  	rootFS := image.NewRootFS()
   316  	var history []image.History
   317  
   318  	if parentID != "" {
   319  		parentImg, err := l.is.Get(parentID)
   320  		if err != nil {
   321  			return err
   322  		}
   323  
   324  		rootFS = parentImg.RootFS
   325  		history = parentImg.History
   326  	}
   327  
   328  	layerPath, err := safePath(sourceDir, filepath.Join(oldID, legacyLayerFileName))
   329  	if err != nil {
   330  		return err
   331  	}
   332  	newLayer, err := l.loadLayer(layerPath, *rootFS, oldID, distribution.Descriptor{}, progressOutput)
   333  	if err != nil {
   334  		return err
   335  	}
   336  	rootFS.Append(newLayer.DiffID())
   337  
   338  	h, err := v1.HistoryFromConfig(imageJSON, false)
   339  	if err != nil {
   340  		return err
   341  	}
   342  	history = append(history, h)
   343  
   344  	config, err := v1.MakeConfigFromV1Config(imageJSON, rootFS, history)
   345  	if err != nil {
   346  		return err
   347  	}
   348  	imgID, err := l.is.Create(config)
   349  	if err != nil {
   350  		return err
   351  	}
   352  
   353  	metadata, err := l.lss.Release(newLayer)
   354  	layer.LogReleaseMetadata(metadata)
   355  	if err != nil {
   356  		return err
   357  	}
   358  
   359  	if parentID != "" {
   360  		if err := l.is.SetParent(imgID, parentID); err != nil {
   361  			return err
   362  		}
   363  	}
   364  
   365  	loadedMap[oldID] = imgID
   366  	return nil
   367  }
   368  
   369  func safePath(base, path string) (string, error) {
   370  	return symlink.FollowSymlinkInScope(filepath.Join(base, path), base)
   371  }
   372  
   373  type parentLink struct {
   374  	id, parentID image.ID
   375  }
   376  
   377  func validatedParentLinks(pl []parentLink) (ret []parentLink) {
   378  mainloop:
   379  	for i, p := range pl {
   380  		ret = append(ret, p)
   381  		for _, p2 := range pl {
   382  			if p2.id == p.parentID && p2.id != p.id {
   383  				continue mainloop
   384  			}
   385  		}
   386  		ret[i].parentID = ""
   387  	}
   388  	return
   389  }
   390  
   391  func checkValidParent(img, parent *image.Image) bool {
   392  	if len(img.History) == 0 && len(parent.History) == 0 {
   393  		return true // having history is not mandatory
   394  	}
   395  	if len(img.History)-len(parent.History) != 1 {
   396  		return false
   397  	}
   398  	for i, h := range parent.History {
   399  		if !h.Equal(img.History[i]) {
   400  			return false
   401  		}
   402  	}
   403  	return true
   404  }
   405  
   406  func validateManifest(manifest []manifestItem) error {
   407  	// a nil manifest usually indicates a bug, so don't just silently fail.
   408  	// if someone really needs to pass an empty manifest, they can pass [].
   409  	if manifest == nil {
   410  		return errors.New("invalid manifest, manifest cannot be null (but can be [])")
   411  	}
   412  
   413  	return nil
   414  }