mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
ee5228e799
After https://github.com/docker/docker/pull/28926, if saving multiple images which have common layers at same time, the common layers can't share in the tar archive because the hash ID changes because of the Create time. The Create time is used for pre v1.9 which treat each layer as a image and make no sense for after v1.10. To make the hash ID consistent and keep the image save from >1.10 working properly on pre v1.9, using a constant Create time `time.Unix(0,0)`. Signed-off-by: Lei Jitang <leijitang@huawei.com>
358 lines
8.7 KiB
Go
358 lines
8.7 KiB
Go
package tarexport
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/docker/distribution"
|
|
"github.com/docker/docker/image"
|
|
"github.com/docker/docker/image/v1"
|
|
"github.com/docker/docker/layer"
|
|
"github.com/docker/docker/pkg/archive"
|
|
"github.com/docker/docker/pkg/system"
|
|
"github.com/docker/docker/reference"
|
|
"github.com/opencontainers/go-digest"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
type imageDescriptor struct {
|
|
refs []reference.NamedTagged
|
|
layers []string
|
|
}
|
|
|
|
type saveSession struct {
|
|
*tarexporter
|
|
outDir string
|
|
images map[image.ID]*imageDescriptor
|
|
savedLayers map[string]struct{}
|
|
diffIDPaths map[layer.DiffID]string // cache every diffID blob to avoid duplicates
|
|
}
|
|
|
|
func (l *tarexporter) Save(names []string, outStream io.Writer) error {
|
|
images, err := l.parseNames(names)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return (&saveSession{tarexporter: l, images: images}).save(outStream)
|
|
}
|
|
|
|
func (l *tarexporter) parseNames(names []string) (map[image.ID]*imageDescriptor, error) {
|
|
imgDescr := make(map[image.ID]*imageDescriptor)
|
|
|
|
addAssoc := func(id image.ID, ref reference.Named) {
|
|
if _, ok := imgDescr[id]; !ok {
|
|
imgDescr[id] = &imageDescriptor{}
|
|
}
|
|
|
|
if ref != nil {
|
|
var tagged reference.NamedTagged
|
|
if _, ok := ref.(reference.Canonical); ok {
|
|
return
|
|
}
|
|
var ok bool
|
|
if tagged, ok = ref.(reference.NamedTagged); !ok {
|
|
var err error
|
|
if tagged, err = reference.WithTag(ref, reference.DefaultTag); err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
for _, t := range imgDescr[id].refs {
|
|
if tagged.String() == t.String() {
|
|
return
|
|
}
|
|
}
|
|
imgDescr[id].refs = append(imgDescr[id].refs, tagged)
|
|
}
|
|
}
|
|
|
|
for _, name := range names {
|
|
id, ref, err := reference.ParseIDOrReference(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if id != "" {
|
|
_, err := l.is.Get(image.IDFromDigest(id))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
addAssoc(image.IDFromDigest(id), nil)
|
|
continue
|
|
}
|
|
if ref.Name() == string(digest.Canonical) {
|
|
imgID, err := l.is.Search(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
addAssoc(imgID, nil)
|
|
continue
|
|
}
|
|
if reference.IsNameOnly(ref) {
|
|
assocs := l.rs.ReferencesByName(ref)
|
|
for _, assoc := range assocs {
|
|
addAssoc(image.IDFromDigest(assoc.ID), assoc.Ref)
|
|
}
|
|
if len(assocs) == 0 {
|
|
imgID, err := l.is.Search(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
addAssoc(imgID, nil)
|
|
}
|
|
continue
|
|
}
|
|
id, err = l.rs.Get(ref)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
addAssoc(image.IDFromDigest(id), ref)
|
|
|
|
}
|
|
return imgDescr, nil
|
|
}
|
|
|
|
func (s *saveSession) save(outStream io.Writer) error {
|
|
s.savedLayers = make(map[string]struct{})
|
|
s.diffIDPaths = make(map[layer.DiffID]string)
|
|
|
|
// get image json
|
|
tempDir, err := ioutil.TempDir("", "docker-export-")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
s.outDir = tempDir
|
|
reposLegacy := make(map[string]map[string]string)
|
|
|
|
var manifest []manifestItem
|
|
var parentLinks []parentLink
|
|
|
|
for id, imageDescr := range s.images {
|
|
foreignSrcs, err := s.saveImage(id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var repoTags []string
|
|
var layers []string
|
|
|
|
for _, ref := range imageDescr.refs {
|
|
if _, ok := reposLegacy[ref.Name()]; !ok {
|
|
reposLegacy[ref.Name()] = make(map[string]string)
|
|
}
|
|
reposLegacy[ref.Name()][ref.Tag()] = imageDescr.layers[len(imageDescr.layers)-1]
|
|
repoTags = append(repoTags, ref.String())
|
|
}
|
|
|
|
for _, l := range imageDescr.layers {
|
|
layers = append(layers, filepath.Join(l, legacyLayerFileName))
|
|
}
|
|
|
|
manifest = append(manifest, manifestItem{
|
|
Config: id.Digest().Hex() + ".json",
|
|
RepoTags: repoTags,
|
|
Layers: layers,
|
|
LayerSources: foreignSrcs,
|
|
})
|
|
|
|
parentID, _ := s.is.GetParent(id)
|
|
parentLinks = append(parentLinks, parentLink{id, parentID})
|
|
s.tarexporter.loggerImgEvent.LogImageEvent(id.String(), id.String(), "save")
|
|
}
|
|
|
|
for i, p := range validatedParentLinks(parentLinks) {
|
|
if p.parentID != "" {
|
|
manifest[i].Parent = p.parentID
|
|
}
|
|
}
|
|
|
|
if len(reposLegacy) > 0 {
|
|
reposFile := filepath.Join(tempDir, legacyRepositoriesFileName)
|
|
rf, err := os.OpenFile(reposFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := json.NewEncoder(rf).Encode(reposLegacy); err != nil {
|
|
rf.Close()
|
|
return err
|
|
}
|
|
|
|
rf.Close()
|
|
|
|
if err := system.Chtimes(reposFile, time.Unix(0, 0), time.Unix(0, 0)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
manifestFileName := filepath.Join(tempDir, manifestFileName)
|
|
f, err := os.OpenFile(manifestFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := json.NewEncoder(f).Encode(manifest); err != nil {
|
|
f.Close()
|
|
return err
|
|
}
|
|
|
|
f.Close()
|
|
|
|
if err := system.Chtimes(manifestFileName, time.Unix(0, 0), time.Unix(0, 0)); err != nil {
|
|
return err
|
|
}
|
|
|
|
fs, err := archive.Tar(tempDir, archive.Uncompressed)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer fs.Close()
|
|
|
|
_, err = io.Copy(outStream, fs)
|
|
return err
|
|
}
|
|
|
|
func (s *saveSession) saveImage(id image.ID) (map[layer.DiffID]distribution.Descriptor, error) {
|
|
img, err := s.is.Get(id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(img.RootFS.DiffIDs) == 0 {
|
|
return nil, fmt.Errorf("empty export - not implemented")
|
|
}
|
|
|
|
var parent digest.Digest
|
|
var layers []string
|
|
var foreignSrcs map[layer.DiffID]distribution.Descriptor
|
|
for i := range img.RootFS.DiffIDs {
|
|
v1Img := image.V1Image{
|
|
// This is for backward compatibility used for
|
|
// pre v1.9 docker.
|
|
Created: time.Unix(0, 0),
|
|
}
|
|
if i == len(img.RootFS.DiffIDs)-1 {
|
|
v1Img = img.V1Image
|
|
}
|
|
rootFS := *img.RootFS
|
|
rootFS.DiffIDs = rootFS.DiffIDs[:i+1]
|
|
v1ID, err := v1.CreateID(v1Img, rootFS.ChainID(), parent)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
v1Img.ID = v1ID.Hex()
|
|
if parent != "" {
|
|
v1Img.Parent = parent.Hex()
|
|
}
|
|
|
|
src, err := s.saveLayer(rootFS.ChainID(), v1Img, img.Created)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
layers = append(layers, v1Img.ID)
|
|
parent = v1ID
|
|
if src.Digest != "" {
|
|
if foreignSrcs == nil {
|
|
foreignSrcs = make(map[layer.DiffID]distribution.Descriptor)
|
|
}
|
|
foreignSrcs[img.RootFS.DiffIDs[i]] = src
|
|
}
|
|
}
|
|
|
|
configFile := filepath.Join(s.outDir, id.Digest().Hex()+".json")
|
|
if err := ioutil.WriteFile(configFile, img.RawJSON(), 0644); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := system.Chtimes(configFile, img.Created, img.Created); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
s.images[id].layers = layers
|
|
return foreignSrcs, nil
|
|
}
|
|
|
|
func (s *saveSession) saveLayer(id layer.ChainID, legacyImg image.V1Image, createdTime time.Time) (distribution.Descriptor, error) {
|
|
if _, exists := s.savedLayers[legacyImg.ID]; exists {
|
|
return distribution.Descriptor{}, nil
|
|
}
|
|
|
|
outDir := filepath.Join(s.outDir, legacyImg.ID)
|
|
if err := os.Mkdir(outDir, 0755); err != nil {
|
|
return distribution.Descriptor{}, err
|
|
}
|
|
|
|
// todo: why is this version file here?
|
|
if err := ioutil.WriteFile(filepath.Join(outDir, legacyVersionFileName), []byte("1.0"), 0644); err != nil {
|
|
return distribution.Descriptor{}, err
|
|
}
|
|
|
|
imageConfig, err := json.Marshal(legacyImg)
|
|
if err != nil {
|
|
return distribution.Descriptor{}, err
|
|
}
|
|
|
|
if err := ioutil.WriteFile(filepath.Join(outDir, legacyConfigFileName), imageConfig, 0644); err != nil {
|
|
return distribution.Descriptor{}, err
|
|
}
|
|
|
|
// serialize filesystem
|
|
layerPath := filepath.Join(outDir, legacyLayerFileName)
|
|
l, err := s.ls.Get(id)
|
|
if err != nil {
|
|
return distribution.Descriptor{}, err
|
|
}
|
|
defer layer.ReleaseAndLog(s.ls, l)
|
|
|
|
if oldPath, exists := s.diffIDPaths[l.DiffID()]; exists {
|
|
relPath, err := filepath.Rel(outDir, oldPath)
|
|
if err != nil {
|
|
return distribution.Descriptor{}, err
|
|
}
|
|
if err := os.Symlink(relPath, layerPath); err != nil {
|
|
return distribution.Descriptor{}, errors.Wrap(err, "error creating symlink while saving layer")
|
|
}
|
|
} else {
|
|
// Use system.CreateSequential rather than os.Create. This ensures sequential
|
|
// file access on Windows to avoid eating into MM standby list.
|
|
// On Linux, this equates to a regular os.Create.
|
|
tarFile, err := system.CreateSequential(layerPath)
|
|
if err != nil {
|
|
return distribution.Descriptor{}, err
|
|
}
|
|
defer tarFile.Close()
|
|
|
|
arch, err := l.TarStream()
|
|
if err != nil {
|
|
return distribution.Descriptor{}, err
|
|
}
|
|
defer arch.Close()
|
|
|
|
if _, err := io.Copy(tarFile, arch); err != nil {
|
|
return distribution.Descriptor{}, err
|
|
}
|
|
|
|
for _, fname := range []string{"", legacyVersionFileName, legacyConfigFileName, legacyLayerFileName} {
|
|
// todo: maybe save layer created timestamp?
|
|
if err := system.Chtimes(filepath.Join(outDir, fname), createdTime, createdTime); err != nil {
|
|
return distribution.Descriptor{}, err
|
|
}
|
|
}
|
|
|
|
s.diffIDPaths[l.DiffID()] = layerPath
|
|
}
|
|
s.savedLayers[legacyImg.ID] = struct{}{}
|
|
|
|
var src distribution.Descriptor
|
|
if fs, ok := l.(distribution.Describable); ok {
|
|
src = fs.Descriptor()
|
|
}
|
|
return src, nil
|
|
}
|