0f7c9cd27e
Both of these were deprecated in 55f675811a
,
but the format of the GoDoc comments didn't follow the correct format, which
caused them not being picked up by tools as "deprecated".
This patch updates uses in the codebase to use the alternatives.
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
414 lines
11 KiB
Go
414 lines
11 KiB
Go
package tarexport // import "github.com/docker/docker/image/tarexport"
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/docker/distribution"
|
|
"github.com/docker/distribution/reference"
|
|
"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/system"
|
|
"github.com/moby/sys/sequential"
|
|
"github.com/opencontainers/go-digest"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
type imageDescriptor struct {
|
|
refs []reference.NamedTagged
|
|
layers []string
|
|
image *image.Image
|
|
layerRef layer.Layer
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// Release all the image top layer references
|
|
defer l.releaseLayerReferences(images)
|
|
return (&saveSession{tarexporter: l, images: images}).save(outStream)
|
|
}
|
|
|
|
// parseNames will parse the image names to a map which contains image.ID to *imageDescriptor.
|
|
// Each imageDescriptor holds an image top layer reference named 'layerRef'. It is taken here, should be released later.
|
|
func (l *tarexporter) parseNames(names []string) (desc map[image.ID]*imageDescriptor, rErr error) {
|
|
imgDescr := make(map[image.ID]*imageDescriptor)
|
|
defer func() {
|
|
if rErr != nil {
|
|
l.releaseLayerReferences(imgDescr)
|
|
}
|
|
}()
|
|
|
|
addAssoc := func(id image.ID, ref reference.Named) error {
|
|
if _, ok := imgDescr[id]; !ok {
|
|
descr := &imageDescriptor{}
|
|
if err := l.takeLayerReference(id, descr); err != nil {
|
|
return err
|
|
}
|
|
imgDescr[id] = descr
|
|
}
|
|
|
|
if ref != nil {
|
|
if _, ok := ref.(reference.Canonical); ok {
|
|
return nil
|
|
}
|
|
tagged, ok := reference.TagNameOnly(ref).(reference.NamedTagged)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
for _, t := range imgDescr[id].refs {
|
|
if tagged.String() == t.String() {
|
|
return nil
|
|
}
|
|
}
|
|
imgDescr[id].refs = append(imgDescr[id].refs, tagged)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
for _, name := range names {
|
|
ref, err := reference.ParseAnyReference(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
namedRef, ok := ref.(reference.Named)
|
|
if !ok {
|
|
// Check if digest ID reference
|
|
if digested, ok := ref.(reference.Digested); ok {
|
|
id := image.IDFromDigest(digested.Digest())
|
|
if err := addAssoc(id, nil); err != nil {
|
|
return nil, err
|
|
}
|
|
continue
|
|
}
|
|
return nil, errors.Errorf("invalid reference: %v", name)
|
|
}
|
|
|
|
if reference.FamiliarName(namedRef) == string(digest.Canonical) {
|
|
imgID, err := l.is.Search(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := addAssoc(imgID, nil); err != nil {
|
|
return nil, err
|
|
}
|
|
continue
|
|
}
|
|
if reference.IsNameOnly(namedRef) {
|
|
assocs := l.rs.ReferencesByName(namedRef)
|
|
for _, assoc := range assocs {
|
|
if err := addAssoc(image.IDFromDigest(assoc.ID), assoc.Ref); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if len(assocs) == 0 {
|
|
imgID, err := l.is.Search(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := addAssoc(imgID, nil); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
id, err := l.rs.Get(namedRef)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := addAssoc(image.IDFromDigest(id), namedRef); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return imgDescr, nil
|
|
}
|
|
|
|
// takeLayerReference will take/Get the image top layer reference
|
|
func (l *tarexporter) takeLayerReference(id image.ID, imgDescr *imageDescriptor) error {
|
|
img, err := l.is.Get(id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if os := img.OperatingSystem(); !system.IsOSSupported(os) {
|
|
return fmt.Errorf("os %q is not supported", os)
|
|
}
|
|
imgDescr.image = img
|
|
topLayerID := img.RootFS.ChainID()
|
|
if topLayerID == "" {
|
|
return nil
|
|
}
|
|
layer, err := l.lss.Get(topLayerID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
imgDescr.layerRef = layer
|
|
return nil
|
|
}
|
|
|
|
// releaseLayerReferences will release all the image top layer references
|
|
func (l *tarexporter) releaseLayerReferences(imgDescr map[image.ID]*imageDescriptor) error {
|
|
for _, descr := range imgDescr {
|
|
if descr.layerRef != nil {
|
|
l.lss.Release(descr.layerRef)
|
|
}
|
|
}
|
|
return 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 := os.MkdirTemp("", "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 {
|
|
familiarName := reference.FamiliarName(ref)
|
|
if _, ok := reposLegacy[familiarName]; !ok {
|
|
reposLegacy[familiarName] = make(map[string]string)
|
|
}
|
|
reposLegacy[familiarName][ref.Tag()] = imageDescr.layers[len(imageDescr.layers)-1]
|
|
repoTags = append(repoTags, reference.FamiliarString(ref))
|
|
}
|
|
|
|
for _, l := range imageDescr.layers {
|
|
// IMPORTANT: We use path, not filepath here to ensure the layers
|
|
// in the manifest use Unix-style forward-slashes.
|
|
layers = append(layers, path.Join(l, legacyLayerFileName))
|
|
}
|
|
|
|
manifest = append(manifest, manifestItem{
|
|
Config: id.Digest().Encoded() + ".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 := s.images[id].image
|
|
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.Encoded()
|
|
if parent != "" {
|
|
v1Img.Parent = parent.Encoded()
|
|
}
|
|
|
|
v1Img.OS = img.OS
|
|
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().Encoded()+".json")
|
|
if err := os.WriteFile(configFile, img.RawJSON(), 0o644); 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 := os.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 := os.WriteFile(filepath.Join(outDir, legacyConfigFileName), imageConfig, 0644); err != nil {
|
|
return distribution.Descriptor{}, err
|
|
}
|
|
|
|
// serialize filesystem
|
|
layerPath := filepath.Join(outDir, legacyLayerFileName)
|
|
l, err := s.lss.Get(id)
|
|
if err != nil {
|
|
return distribution.Descriptor{}, err
|
|
}
|
|
defer layer.ReleaseAndLog(s.lss, 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 {
|
|
// We use sequential file access to avoid depleting the standby list on
|
|
// Windows. On Linux, this equates to a regular os.Create.
|
|
tarFile, err := sequential.Create(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
|
|
}
|