moby/image/tarexport/load.go
Sebastiaan van Stijn cff4f20c44
migrate to github.com/containerd/log v0.1.0
The github.com/containerd/containerd/log package was moved to a separate
module, which will also be used by upcoming (patch) releases of containerd.

This patch moves our own uses of the package to use the new module.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2023-10-11 17:52:23 +02:00

425 lines
10 KiB
Go

package tarexport // import "github.com/docker/docker/image/tarexport"
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"reflect"
"runtime"
"github.com/containerd/log"
"github.com/distribution/reference"
"github.com/docker/distribution"
"github.com/docker/docker/api/types/events"
"github.com/docker/docker/image"
v1 "github.com/docker/docker/image/v1"
"github.com/docker/docker/layer"
"github.com/docker/docker/pkg/archive"
"github.com/docker/docker/pkg/chrootarchive"
"github.com/docker/docker/pkg/progress"
"github.com/docker/docker/pkg/streamformatter"
"github.com/docker/docker/pkg/stringid"
"github.com/moby/sys/sequential"
"github.com/moby/sys/symlink"
"github.com/opencontainers/go-digest"
)
func (l *tarexporter) Load(inTar io.ReadCloser, outStream io.Writer, quiet bool) error {
var progressOutput progress.Output
if !quiet {
progressOutput = streamformatter.NewJSONProgressOutput(outStream, false)
}
outStream = streamformatter.NewStdoutWriter(outStream)
tmpDir, err := os.MkdirTemp("", "docker-import-")
if err != nil {
return err
}
defer os.RemoveAll(tmpDir)
if err := chrootarchive.Untar(inTar, tmpDir, nil); err != nil {
return err
}
// read manifest, if no file then load in legacy mode
manifestPath, err := safePath(tmpDir, manifestFileName)
if err != nil {
return err
}
manifestFile, err := os.Open(manifestPath)
if err != nil {
if os.IsNotExist(err) {
return l.legacyLoad(tmpDir, outStream, progressOutput)
}
return err
}
defer manifestFile.Close()
var manifest []manifestItem
if err := json.NewDecoder(manifestFile).Decode(&manifest); err != nil {
return err
}
if err := validateManifest(manifest); err != nil {
return err
}
var parentLinks []parentLink
var imageIDsStr string
var imageRefCount int
for _, m := range manifest {
configPath, err := safePath(tmpDir, m.Config)
if err != nil {
return err
}
config, err := os.ReadFile(configPath)
if err != nil {
return err
}
img, err := image.NewFromJSON(config)
if err != nil {
return err
}
if err := image.CheckOS(img.OperatingSystem()); err != nil {
return fmt.Errorf("cannot load %s image on %s", img.OperatingSystem(), runtime.GOOS)
}
rootFS := *img.RootFS
rootFS.DiffIDs = nil
if expected, actual := len(m.Layers), len(img.RootFS.DiffIDs); expected != actual {
return fmt.Errorf("invalid manifest, layers length mismatch: expected %d, got %d", expected, actual)
}
for i, diffID := range img.RootFS.DiffIDs {
layerPath, err := safePath(tmpDir, m.Layers[i])
if err != nil {
return err
}
r := rootFS
r.Append(diffID)
newLayer, err := l.lss.Get(r.ChainID())
if err != nil {
newLayer, err = l.loadLayer(layerPath, rootFS, diffID.String(), m.LayerSources[diffID], progressOutput)
if err != nil {
return err
}
}
defer layer.ReleaseAndLog(l.lss, newLayer)
if expected, actual := diffID, newLayer.DiffID(); expected != actual {
return fmt.Errorf("invalid diffID for layer %d: expected %q, got %q", i, expected, actual)
}
rootFS.Append(diffID)
}
imgID, err := l.is.Create(config)
if err != nil {
return err
}
imageIDsStr += fmt.Sprintf("Loaded image ID: %s\n", imgID)
imageRefCount = 0
for _, repoTag := range m.RepoTags {
named, err := reference.ParseNormalizedNamed(repoTag)
if err != nil {
return err
}
ref, ok := named.(reference.NamedTagged)
if !ok {
return fmt.Errorf("invalid tag %q", repoTag)
}
l.setLoadedTag(ref, imgID.Digest(), outStream)
outStream.Write([]byte(fmt.Sprintf("Loaded image: %s\n", reference.FamiliarString(ref))))
imageRefCount++
}
parentLinks = append(parentLinks, parentLink{imgID, m.Parent})
l.loggerImgEvent.LogImageEvent(imgID.String(), imgID.String(), events.ActionLoad)
}
for _, p := range validatedParentLinks(parentLinks) {
if p.parentID != "" {
if err := l.setParentID(p.id, p.parentID); err != nil {
return err
}
}
}
if imageRefCount == 0 {
outStream.Write([]byte(imageIDsStr))
}
return nil
}
func (l *tarexporter) setParentID(id, parentID image.ID) error {
img, err := l.is.Get(id)
if err != nil {
return err
}
parent, err := l.is.Get(parentID)
if err != nil {
return err
}
if !checkValidParent(img, parent) {
return fmt.Errorf("image %v is not a valid parent for %v", parent.ID(), img.ID())
}
return l.is.SetParent(id, parentID)
}
func (l *tarexporter) loadLayer(filename string, rootFS image.RootFS, id string, foreignSrc distribution.Descriptor, progressOutput progress.Output) (layer.Layer, error) {
// We use sequential file access to avoid depleting the standby list on Windows.
// On Linux, this equates to a regular os.Open.
rawTar, err := sequential.Open(filename)
if err != nil {
log.G(context.TODO()).Debugf("Error reading embedded tar: %v", err)
return nil, err
}
defer rawTar.Close()
var r io.Reader
if progressOutput != nil {
fileInfo, err := rawTar.Stat()
if err != nil {
log.G(context.TODO()).Debugf("Error statting file: %v", err)
return nil, err
}
r = progress.NewProgressReader(rawTar, progressOutput, fileInfo.Size(), stringid.TruncateID(id), "Loading layer")
} else {
r = rawTar
}
inflatedLayerData, err := archive.DecompressStream(r)
if err != nil {
return nil, err
}
defer inflatedLayerData.Close()
if ds, ok := l.lss.(layer.DescribableStore); ok {
return ds.RegisterWithDescriptor(inflatedLayerData, rootFS.ChainID(), foreignSrc)
}
return l.lss.Register(inflatedLayerData, rootFS.ChainID())
}
func (l *tarexporter) setLoadedTag(ref reference.Named, imgID digest.Digest, outStream io.Writer) error {
if prevID, err := l.rs.Get(ref); err == nil && prevID != imgID {
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
}
return l.rs.AddTag(ref, imgID, true)
}
func (l *tarexporter) legacyLoad(tmpDir string, outStream io.Writer, progressOutput progress.Output) error {
if runtime.GOOS == "windows" {
return errors.New("Windows does not support legacy loading of images")
}
legacyLoadedMap := make(map[string]image.ID)
dirs, err := os.ReadDir(tmpDir)
if err != nil {
return err
}
// every dir represents an image
for _, d := range dirs {
if d.IsDir() {
if err := l.legacyLoadImage(d.Name(), tmpDir, legacyLoadedMap, progressOutput); err != nil {
return err
}
}
}
// load tags from repositories file
repositoriesPath, err := safePath(tmpDir, legacyRepositoriesFileName)
if err != nil {
return err
}
repositoriesFile, err := os.Open(repositoriesPath)
if err != nil {
return err
}
defer repositoriesFile.Close()
repositories := make(map[string]map[string]string)
if err := json.NewDecoder(repositoriesFile).Decode(&repositories); err != nil {
return err
}
for name, tagMap := range repositories {
for tag, oldID := range tagMap {
imgID, ok := legacyLoadedMap[oldID]
if !ok {
return fmt.Errorf("invalid target ID: %v", oldID)
}
named, err := reference.ParseNormalizedNamed(name)
if err != nil {
return err
}
ref, err := reference.WithTag(named, tag)
if err != nil {
return err
}
l.setLoadedTag(ref, imgID.Digest(), outStream)
}
}
return nil
}
func (l *tarexporter) legacyLoadImage(oldID, sourceDir string, loadedMap map[string]image.ID, progressOutput progress.Output) error {
if _, loaded := loadedMap[oldID]; loaded {
return nil
}
configPath, err := safePath(sourceDir, filepath.Join(oldID, legacyConfigFileName))
if err != nil {
return err
}
imageJSON, err := os.ReadFile(configPath)
if err != nil {
log.G(context.TODO()).Debugf("Error reading json: %v", err)
return err
}
var img struct {
OS string
Parent string
}
if err := json.Unmarshal(imageJSON, &img); err != nil {
return err
}
if img.OS == "" {
img.OS = runtime.GOOS
}
if err := image.CheckOS(img.OS); err != nil {
return fmt.Errorf("cannot load %s image on %s", img.OS, runtime.GOOS)
}
var parentID image.ID
if img.Parent != "" {
for {
var loaded bool
if parentID, loaded = loadedMap[img.Parent]; !loaded {
if err := l.legacyLoadImage(img.Parent, sourceDir, loadedMap, progressOutput); err != nil {
return err
}
} else {
break
}
}
}
// todo: try to connect with migrate code
rootFS := image.NewRootFS()
var history []image.History
if parentID != "" {
parentImg, err := l.is.Get(parentID)
if err != nil {
return err
}
rootFS = parentImg.RootFS
history = parentImg.History
}
layerPath, err := safePath(sourceDir, filepath.Join(oldID, legacyLayerFileName))
if err != nil {
return err
}
newLayer, err := l.loadLayer(layerPath, *rootFS, oldID, distribution.Descriptor{}, progressOutput)
if err != nil {
return err
}
rootFS.Append(newLayer.DiffID())
h, err := v1.HistoryFromConfig(imageJSON, false)
if err != nil {
return err
}
history = append(history, h)
config, err := v1.MakeConfigFromV1Config(imageJSON, rootFS, history)
if err != nil {
return err
}
imgID, err := l.is.Create(config)
if err != nil {
return err
}
metadata, err := l.lss.Release(newLayer)
layer.LogReleaseMetadata(metadata)
if err != nil {
return err
}
if parentID != "" {
if err := l.is.SetParent(imgID, parentID); err != nil {
return err
}
}
loadedMap[oldID] = imgID
return nil
}
func safePath(base, path string) (string, error) {
return symlink.FollowSymlinkInScope(filepath.Join(base, path), base)
}
type parentLink struct {
id, parentID image.ID
}
func validatedParentLinks(pl []parentLink) (ret []parentLink) {
mainloop:
for i, p := range pl {
ret = append(ret, p)
for _, p2 := range pl {
if p2.id == p.parentID && p2.id != p.id {
continue mainloop
}
}
ret[i].parentID = ""
}
return
}
func checkValidParent(img, parent *image.Image) bool {
if len(img.History) == 0 && len(parent.History) == 0 {
return true // having history is not mandatory
}
if len(img.History)-len(parent.History) != 1 {
return false
}
for i, hP := range parent.History {
hC := img.History[i]
if (hP.Created == nil) != (hC.Created == nil) {
return false
}
if hP.Created != nil && !hP.Created.Equal(*hC.Created) {
return false
}
hC.Created = hP.Created
if !reflect.DeepEqual(hP, hC) {
return false
}
}
return true
}
func validateManifest(manifest []manifestItem) error {
// a nil manifest usually indicates a bug, so don't just silently fail.
// if someone really needs to pass an empty manifest, they can pass [].
if manifest == nil {
return errors.New("invalid manifest, manifest cannot be null (but can be [])")
}
return nil
}