isula-build/patch/0099-Refactor-refactor-image-separator-related.patch

2344 lines
71 KiB
Diff
Raw Normal View History

From c4f400ec1cfaf65d3e83dbd796f1f2b00574ba6e Mon Sep 17 00:00:00 2001
From: Lu Jingxiao <lujingxiao@huawei.com>
Date: Fri, 10 Dec 2021 13:36:26 +0800
Subject: [PATCH] Refactor: refactor image separator related
Signed-off-by: Lu Jingxiao <lujingxiao@huawei.com>
---
cmd/cli/save.go | 4 +-
cmd/daemon/main.go | 7 +-
constant.go | 26 +-
daemon/daemon.go | 5 +-
daemon/import.go | 4 +-
daemon/load.go | 295 ++-------------
daemon/load_test.go | 2 +-
daemon/save.go | 649 +--------------------------------
daemon/separator/image_info.go | 235 ++++++++++++
daemon/separator/load.go | 283 ++++++++++++++
daemon/separator/save.go | 407 +++++++++++++++++++++
daemon/separator/utils.go | 78 ++++
12 files changed, 1074 insertions(+), 921 deletions(-)
create mode 100644 daemon/separator/image_info.go
create mode 100644 daemon/separator/load.go
create mode 100644 daemon/separator/save.go
create mode 100644 daemon/separator/utils.go
diff --git a/cmd/cli/save.go b/cmd/cli/save.go
index 5a63e02..a30681f 100644
--- a/cmd/cli/save.go
+++ b/cmd/cli/save.go
@@ -144,8 +144,8 @@ func (opt *saveOptions) checkSaveOpts(args []string) error {
}
// separate image only support docker image spec
opt.format = constant.DockerTransport
- if err := opt.sep.check(pwd); err != nil {
- return err
+ if cerr := opt.sep.check(pwd); cerr != nil {
+ return cerr
}
opt.sep.enabled = true
diff --git a/cmd/daemon/main.go b/cmd/daemon/main.go
index 3665f6b..3cecbf9 100644
--- a/cmd/daemon/main.go
+++ b/cmd/daemon/main.go
@@ -36,10 +36,7 @@ import (
"isula.org/isula-build/util"
)
-const (
- lockFileName = "isula-builder.lock"
- dataRootTmpDirPrefix = "tmp"
-)
+const lockFileName = "isula-builder.lock"
var daemonOpts daemon.Options
@@ -331,7 +328,7 @@ func setupWorkingDirectories() error {
return errors.Errorf("runroot(%q) and dataroot(%q) must be different paths", daemonOpts.RunRoot, daemonOpts.DataRoot)
}
- buildTmpDir, err := securejoin.SecureJoin(daemonOpts.DataRoot, dataRootTmpDirPrefix)
+ buildTmpDir, err := securejoin.SecureJoin(daemonOpts.DataRoot, constant.DataRootTmpDirPrefix)
if err != nil {
return err
}
diff --git a/constant.go b/constant.go
index 4d1596a..5af4fe2 100644
--- a/constant.go
+++ b/constant.go
@@ -31,14 +31,20 @@ const (
RegistryDirPath = ConfigRoot + "registries.d"
// AuthFilePath is authentication file used for registry connection
AuthFilePath = ConfigRoot + "auth.json"
- // DefaultGRPCAddress is the local unix socket used by isula-builder
- DefaultGRPCAddress = "unix:///var/run/isula_build.sock"
- // UnixPrefix is the prefix used on defined an unix sock
- UnixPrefix = "unix://"
+ // DefaultCertRoot is path of certification used for registry connection
+ DefaultCertRoot = ConfigRoot + "certs.d"
+
// DefaultDataRoot is the default persistent data root used by isula-builder
DefaultDataRoot = "/var/lib/isula-build"
// DefaultRunRoot is the default run root used by isula-builder
DefaultRunRoot = "/var/run/isula-build"
+ // UnixPrefix is the prefix used on defined an unix sock
+ UnixPrefix = "unix://"
+ // DefaultGRPCAddress is the local unix socket used by isula-builder
+ DefaultGRPCAddress = UnixPrefix + "/var/run/isula_build.sock"
+ // DataRootTmpDirPrefix is the dir for storing temporary items using during images building
+ DataRootTmpDirPrefix = "tmp"
+
// DefaultSharedDirMode is dir perm mode with higher permission
DefaultSharedDirMode = 0755
// DefaultSharedFileMode is file perm mode with higher permission
@@ -53,12 +59,14 @@ const (
DefaultReadOnlyFileMode = 0400
// DefaultUmask is the working umask of isula-builder as a process, not for users
DefaultUmask = 0022
- // CliLogBufferLen is log channel buffer size
- CliLogBufferLen = 8
+
// HostsFilePath is the path of file hosts
HostsFilePath = "/etc/hosts"
// ResolvFilePath is the path of file resolv.conf
ResolvFilePath = "/etc/resolv.conf"
+
+ // CliLogBufferLen is log channel buffer size
+ CliLogBufferLen = 8
// MaxFileSize is the maximum file size allowed, set 1M
MaxFileSize = 1024 * 1024
// DefaultHTTPTimeout includes the total time of dial, TLS handshake, request, resp headers and body
@@ -67,14 +75,12 @@ const (
DefaultFailedCode = 1
// DefaultIDLen is the ID length for image ID and build ID
DefaultIDLen = 12
- // DefaultCertRoot is path of certification used for registry connection
- DefaultCertRoot = ConfigRoot + "certs.d"
+
// LayoutTime is the time format used to parse time from a string
LayoutTime = "2006-01-02 15:04:05"
// BuildContainerImageType is the default build type
BuildContainerImageType = "ctr-img"
- // BufferSize is default buffer size for file transportation
- BufferSize = 32 * 1024
+
// DockerTransport used to export docker image format images to registry
DockerTransport = "docker"
// DockerArchiveTransport used to export docker image format images to local tarball
diff --git a/daemon/daemon.go b/daemon/daemon.go
index 4e0435a..1847dbb 100644
--- a/daemon/daemon.go
+++ b/daemon/daemon.go
@@ -28,6 +28,7 @@ import (
"github.com/sirupsen/logrus"
"golang.org/x/sys/unix"
+ constant "isula.org/isula-build"
pb "isula.org/isula-build/api/services"
"isula.org/isula-build/builder"
"isula.org/isula-build/pkg/gc"
@@ -37,8 +38,6 @@ import (
"isula.org/isula-build/util"
)
-const dataRootTmpDirPrefix = "tmp"
-
// Options carries the options configured to daemon
type Options struct {
Debug bool
@@ -137,7 +136,7 @@ func (d *Daemon) NewBuilder(ctx context.Context, req *pb.BuildRequest) (b builde
runDir string
)
// buildDir is used to set directory which is used to store tmp data
- buildDir, err = securejoin.SecureJoin(d.opts.DataRoot, filepath.Join(dataRootTmpDirPrefix, req.BuildID))
+ buildDir, err = securejoin.SecureJoin(d.opts.DataRoot, filepath.Join(constant.DataRootTmpDirPrefix, req.BuildID))
if err != nil {
return nil, err
}
diff --git a/daemon/import.go b/daemon/import.go
index 2da36be..21ab729 100644
--- a/daemon/import.go
+++ b/daemon/import.go
@@ -87,7 +87,7 @@ func (b *Backend) Import(req *pb.ImportRequest, stream pb.Control_ImportServer)
log := logger.NewCliLogger(constant.CliLogBufferLen)
imageCopyOptions := image.NewImageCopyOptions(log)
- tmpDir, err = securejoin.SecureJoin(b.daemon.opts.DataRoot, filepath.Join(dataRootTmpDirPrefix, importID))
+ tmpDir, err = securejoin.SecureJoin(b.daemon.opts.DataRoot, filepath.Join(constant.DataRootTmpDirPrefix, importID))
if err != nil {
return err
}
@@ -97,7 +97,7 @@ func (b *Backend) Import(req *pb.ImportRequest, stream pb.Control_ImportServer)
}
defer func() {
if rErr := os.RemoveAll(tmpDir); rErr != nil {
- logEntry.Errorf("Failed to remove import temporary dir %q, err: %v", filepath.Join(dataRootTmpDirPrefix, importID), rErr)
+ logEntry.Errorf("Failed to remove import temporary dir %q, err: %v", filepath.Join(constant.DataRootTmpDirPrefix, importID), rErr)
}
}()
diff --git a/daemon/load.go b/daemon/load.go
index 894159b..1ee025b 100644
--- a/daemon/load.go
+++ b/daemon/load.go
@@ -15,9 +15,7 @@ package daemon
import (
"context"
- "io/ioutil"
"os"
- "path/filepath"
"strings"
"github.com/containers/image/v5/docker/tarfile"
@@ -25,7 +23,6 @@ import (
"github.com/containers/image/v5/transports/alltransports"
"github.com/containers/image/v5/types"
"github.com/containers/storage"
- "github.com/containers/storage/pkg/archive"
securejoin "github.com/cyphar/filepath-securejoin"
digest "github.com/opencontainers/go-digest"
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
@@ -35,89 +32,47 @@ import (
constant "isula.org/isula-build"
pb "isula.org/isula-build/api/services"
+ "isula.org/isula-build/daemon/separator"
"isula.org/isula-build/exporter"
"isula.org/isula-build/image"
"isula.org/isula-build/pkg/logger"
"isula.org/isula-build/util"
)
-const (
- tmpBaseDirName = "base"
- tmpAppDirName = "app"
- tmpLibDirName = "lib"
- unionCompressedTarName = "all.tar.gz"
-)
-
-type loadImageTmpDir struct {
- app string
- base string
- lib string
- root string
-}
-
-type separatorLoad struct {
- log *logrus.Entry
- tmpDir loadImageTmpDir
- info tarballInfo
- appName string
- basePath string
- appPath string
- libPath string
- dir string
- skipCheck bool
- enabled bool
-}
-
-type loadOptions struct {
- logEntry *logrus.Entry
- path string
- format string
- sep separatorLoad
-}
-
type singleImage struct {
index int
id string
nameTag []string
}
-func (b *Backend) getLoadOptions(req *pb.LoadRequest) (loadOptions, error) {
- var opt = loadOptions{
- path: req.GetPath(),
- sep: separatorLoad{
- appName: req.GetSep().GetApp(),
- basePath: req.GetSep().GetBase(),
- libPath: req.GetSep().GetLib(),
- dir: req.GetSep().GetDir(),
- skipCheck: req.GetSep().GetSkipCheck(),
- enabled: req.GetSep().GetEnabled(),
- },
- logEntry: logrus.WithFields(logrus.Fields{"LoadID": req.GetLoadID()}),
+// LoadOptions stores the options for image loading
+type LoadOptions struct {
+ LogEntry *logrus.Entry
+ path string
+ format string
+ sep separator.Loader
+}
+
+func (b *Backend) getLoadOptions(req *pb.LoadRequest) (LoadOptions, error) {
+ var err error
+ var opt = LoadOptions{
+ path: req.GetPath(),
+ LogEntry: logrus.WithFields(logrus.Fields{"LoadID": req.GetLoadID()}),
}
- // normal loadOptions
- if !opt.sep.enabled {
- if err := util.CheckLoadFile(opt.path); err != nil {
- return loadOptions{}, err
+ // normal image loading
+ if !req.GetSep().GetEnabled() {
+ if err = util.CheckLoadFile(opt.path); err != nil {
+ return LoadOptions{}, err
}
return opt, nil
}
- // load separated images
- // log is used for sep methods
- opt.sep.log = opt.logEntry
- tmpRoot := filepath.Join(b.daemon.opts.DataRoot, filepath.Join(dataRootTmpDirPrefix, req.GetLoadID()))
- opt.sep.tmpDir.root = tmpRoot
- opt.sep.tmpDir.base = filepath.Join(tmpRoot, tmpBaseDirName)
- opt.sep.tmpDir.app = filepath.Join(tmpRoot, tmpAppDirName)
- opt.sep.tmpDir.lib = filepath.Join(tmpRoot, tmpLibDirName)
-
- // check image name and add "latest" tag if not present
- _, appImgName, err := image.GetNamedTaggedReference(opt.sep.appName)
+ // separated images loading
+ opt.sep, err = separator.GetSepLoadOptions(req, opt.LogEntry, b.daemon.opts.DataRoot)
if err != nil {
- return loadOptions{}, err
+ return LoadOptions{}, err
}
- opt.sep.appName = appImgName
return opt, nil
}
@@ -136,15 +91,16 @@ func (b *Backend) Load(req *pb.LoadRequest, stream pb.Control_LoadServer) error
}
defer func() {
- if tErr := os.RemoveAll(opts.sep.tmpDir.root); tErr != nil {
- opts.logEntry.Warnf("Removing load tmp directory %q failed: %v", opts.sep.tmpDir.root, tErr)
+ if tErr := os.RemoveAll(opts.sep.TmpDirRoot()); tErr != nil {
+ opts.LogEntry.Warnf("Removing load tmp directory %q failed: %v", opts.sep.TmpDirRoot(), tErr)
}
}()
// construct separated images
- if opts.sep.enabled {
- if lErr := loadSeparatedImage(&opts); lErr != nil {
- opts.logEntry.Errorf("Load separated image for %s failed: %v", opts.sep.appName, lErr)
+ if opts.sep.Enabled() {
+ var lErr error
+ if opts.path, lErr = opts.sep.LoadSeparatedImage(); lErr != nil {
+ opts.LogEntry.Errorf("Load separated image for %s failed: %v", opts.sep.AppName(), lErr)
return lErr
}
}
@@ -206,9 +162,9 @@ func (b *Backend) Load(req *pb.LoadRequest, stream pb.Control_LoadServer) error
return nil
}
-func tryToParseImageFormatFromTarball(dataRoot string, opts *loadOptions) ([]singleImage, error) {
+func tryToParseImageFormatFromTarball(dataRoot string, opts *LoadOptions) ([]singleImage, error) {
// tmp dir will be removed after NewSourceFromFileWithContext
- tmpDir, err := securejoin.SecureJoin(dataRoot, dataRootTmpDirPrefix)
+ tmpDir, err := securejoin.SecureJoin(dataRoot, constant.DataRootTmpDirPrefix)
if err != nil {
return nil, err
}
@@ -323,196 +279,3 @@ func getLoadedImageID(imageRef types.ImageReference, systemContext *types.System
return "@" + imageDigest.Encoded(), nil
}
-
-func loadSeparatedImage(opt *loadOptions) error {
- s := &opt.sep
- s.log.Infof("Starting load separated image %s", s.appName)
-
- // load manifest file to get tarball info
- if err := s.getTarballInfo(); err != nil {
- return errors.Wrap(err, "failed to get tarball info")
- }
- if err := s.constructTarballInfo(); err != nil {
- return err
- }
- // checksum for image tarballs
- if err := s.tarballCheckSum(); err != nil {
- return err
- }
- // process image tarballs and get final constructed image tarball
- tarPath, err := s.processTarballs()
- if err != nil {
- return err
- }
- opt.path = tarPath
-
- return nil
-}
-
-func (s *separatorLoad) getTarballInfo() error {
- manifest, err := securejoin.SecureJoin(s.dir, manifestFile)
- if err != nil {
- return errors.Wrap(err, "join manifest file path failed")
- }
-
- var t = make(map[string]tarballInfo, 1)
- if err = util.LoadJSONFile(manifest, &t); err != nil {
- return errors.Wrap(err, "load manifest file failed")
- }
-
- tarball, ok := t[s.appName]
- if !ok {
- return errors.Errorf("failed to find app image %s", s.appName)
- }
- s.info = tarball
-
- return nil
-}
-
-func (s *separatorLoad) constructTarballInfo() (err error) {
- s.log.Infof("Construct image tarball info for %s", s.appName)
- // fill up path for separator
- // this case should not happened since client side already check this flag
- if len(s.appName) == 0 {
- return errors.New("app image name should not be empty")
- }
- s.appPath, err = securejoin.SecureJoin(s.dir, s.info.AppTarName)
- if err != nil {
- return err
- }
-
- if len(s.basePath) == 0 {
- if len(s.info.BaseTarName) == 0 {
- return errors.Errorf("base image %s tarball can not be empty", s.info.BaseImageName)
- }
- s.log.Info("Base image path is empty, use path from manifest")
- s.basePath, err = securejoin.SecureJoin(s.dir, s.info.BaseTarName)
- if err != nil {
- return err
- }
- }
- if len(s.libPath) == 0 && len(s.info.LibTarName) != 0 {
- s.log.Info("Lib image path is empty, use path from manifest")
- s.libPath, err = securejoin.SecureJoin(s.dir, s.info.LibTarName)
- if err != nil {
- return err
- }
- }
-
- return nil
-}
-
-func (s *separatorLoad) tarballCheckSum() error {
- if s.skipCheck {
- s.log.Info("Skip checksum for tarballs")
- return nil
- }
-
- type checkInfo struct {
- path string
- hash string
- str string
- canBeEmpty bool
- }
- checkLen := 3
- var checkList = make([]checkInfo, 0, checkLen)
- checkList = append(checkList, checkInfo{path: s.basePath, hash: s.info.BaseHash, canBeEmpty: false, str: "base image"})
- checkList = append(checkList, checkInfo{path: s.libPath, hash: s.info.LibHash, canBeEmpty: true, str: "lib image"})
- checkList = append(checkList, checkInfo{path: s.appPath, hash: s.info.AppHash, canBeEmpty: false, str: "app image"})
- for _, p := range checkList {
- if len(p.path) == 0 && !p.canBeEmpty {
- return errors.Errorf("%s tarball path can not be empty", p.str)
- }
- if len(p.path) != 0 {
- if err := util.CheckSum(p.path, p.hash); err != nil {
- return errors.Wrapf(err, "check sum for file %q failed", p.path)
- }
- }
- }
-
- return nil
-}
-
-func (s *separatorLoad) processTarballs() (string, error) {
- if err := s.unpackTarballs(); err != nil {
- return "", err
- }
-
- if err := s.reconstructImage(); err != nil {
- return "", err
- }
-
- // pack app image to tarball
- tarPath := filepath.Join(s.tmpDir.root, unionCompressedTarName)
- if err := util.PackFiles(s.tmpDir.base, tarPath, archive.Gzip, true); err != nil {
- return "", err
- }
-
- return tarPath, nil
-}
-
-func (s *separatorLoad) unpackTarballs() error {
- if err := s.makeTempDir(); err != nil {
- return errors.Wrap(err, "failed to make temporary directories")
- }
-
- type unpackInfo struct{ path, dir, str string }
- unpackLen := 3
- var unpackList = make([]unpackInfo, 0, unpackLen)
- unpackList = append(unpackList, unpackInfo{path: s.basePath, dir: s.tmpDir.base, str: "base image"})
- unpackList = append(unpackList, unpackInfo{path: s.appPath, dir: s.tmpDir.app, str: "app image"})
- unpackList = append(unpackList, unpackInfo{path: s.libPath, dir: s.tmpDir.lib, str: "lib image"})
-
- for _, p := range unpackList {
- if len(p.path) != 0 {
- if err := util.UnpackFile(p.path, p.dir, archive.Gzip, false); err != nil {
- return errors.Wrapf(err, "unpack %s tarball %q failed", p.str, p.path)
- }
- }
- }
-
- return nil
-}
-
-func (s *separatorLoad) reconstructImage() error {
- files, err := ioutil.ReadDir(s.tmpDir.app)
- if err != nil {
- return err
- }
-
- for _, f := range files {
- src := filepath.Join(s.tmpDir.app, f.Name())
- dest := filepath.Join(s.tmpDir.base, f.Name())
- if err := os.Rename(src, dest); err != nil {
- return errors.Wrapf(err, "reconstruct app file %q failed", s.info.AppTarName)
- }
- }
-
- if len(s.libPath) != 0 {
- files, err := ioutil.ReadDir(s.tmpDir.lib)
- if err != nil {
- return err
- }
-
- for _, f := range files {
- src := filepath.Join(s.tmpDir.lib, f.Name())
- dest := filepath.Join(s.tmpDir.base, f.Name())
- if err := os.Rename(src, dest); err != nil {
- return errors.Wrapf(err, "reconstruct lib file %q failed", s.info.LibTarName)
- }
- }
- }
-
- return nil
-}
-
-func (s *separatorLoad) makeTempDir() error {
- dirs := []string{s.tmpDir.root, s.tmpDir.app, s.tmpDir.base, s.tmpDir.lib}
- for _, dir := range dirs {
- if err := os.MkdirAll(dir, constant.DefaultRootDirMode); err != nil {
- return err
- }
- }
-
- return nil
-}
diff --git a/daemon/load_test.go b/daemon/load_test.go
index 860b897..5e1a42b 100644
--- a/daemon/load_test.go
+++ b/daemon/load_test.go
@@ -314,7 +314,7 @@ func TestLoadMultipleImages(t *testing.T) {
defer clean(dir)
path := dir.Join(loadedTarFile)
- repoTags, err := tryToParseImageFormatFromTarball(daemon.opts.DataRoot, &loadOptions{path: path})
+ repoTags, err := tryToParseImageFormatFromTarball(daemon.opts.DataRoot, &LoadOptions{path: path})
assert.NilError(t, err)
assert.Equal(t, repoTags[0].nameTag[0], "registry.example.com/sayhello:first")
assert.Equal(t, repoTags[1].nameTag[0], "registry.example.com/sayhello:second")
diff --git a/daemon/save.go b/daemon/save.go
index 7a110bd..708fab3 100644
--- a/daemon/save.go
+++ b/daemon/save.go
@@ -15,24 +15,19 @@ package daemon
import (
"context"
- "encoding/json"
- "fmt"
- "io/ioutil"
"os"
"path/filepath"
- "sort"
"strings"
"github.com/containers/image/v5/docker/reference"
"github.com/containers/image/v5/types"
- "github.com/containers/storage/pkg/archive"
- "github.com/docker/docker/pkg/ioutils"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"golang.org/x/sync/errgroup"
constant "isula.org/isula-build"
pb "isula.org/isula-build/api/services"
+ "isula.org/isula-build/daemon/separator"
"isula.org/isula-build/exporter"
savedocker "isula.org/isula-build/exporter/docker/archive"
"isula.org/isula-build/image"
@@ -41,28 +36,13 @@ import (
"isula.org/isula-build/util"
)
-const (
- manifestDataFile = "manifest.json"
- manifestFile = "manifest"
- repositoriesFile = "repositories"
- baseTarNameSuffix = "_base_image.tar.gz"
- appTarNameSuffix = "_app_image.tar.gz"
- libTarNameSuffix = "_lib_image.tar.gz"
- untarTempDirName = "untar"
- baseUntarTempDirName = "base_images"
- appUntarTempDirName = "app_images"
- libUntarTempDirName = "lib_images"
- unionTarName = "all.tar"
- layerTarName = "layer.tar"
- tarSuffix = ".tar"
-)
-
type savedImage struct {
exist bool
tags []reference.NamedTagged
}
-type saveOptions struct {
+// SaveOptions stores the options for saving images
+type SaveOptions struct {
sysCtx *types.SystemContext
localStore *store.Store
logger *logger.Logger
@@ -73,77 +53,11 @@ type saveOptions struct {
oriImgList []string
finalImageOrdered []string
finalImageSet map[string]*savedImage
- sep separatorSave
-}
-
-type separatorSave struct {
- log *logrus.Entry
- renameData []renames
- tmpDir imageTmpDir
- base string
- lib string
- dest string
- renameFile string
- enabled bool
-}
-
-type renames struct {
- Name string `json:"name"`
- Rename string `json:"rename"`
-}
-
-type imageTmpDir struct {
- app string
- base string
- lib string
- untar string
- root string
-}
-
-type layer struct {
- all []string
- base []string
- lib []string
- app []string
-}
-
-type imageInfo struct {
- layers layer
- repoTags []string
- config string
- name string
- tag string
- nameTag string
- topLayer string
-}
-
-// imageManifest return image's manifest info
-type imageManifest struct {
- Config string `json:"Config"`
- RepoTags []string `json:"RepoTags"`
- Layers []string `json:"Layers"`
- // Not shown in the json file
- HashMap map[string]string `json:"-"`
+ sep separator.Saver
}
-type imageLayersMap map[string]string
-
-type tarballInfo struct {
- AppTarName string `json:"app"`
- AppHash string `json:"appHash"`
- AppLayers []string `json:"appLayers"`
- LibTarName string `json:"lib"`
- LibHash string `json:"libHash"`
- LibImageName string `json:"libImageName"`
- LibLayers []string `json:"libLayers"`
- BaseTarName string `json:"base"`
- BaseHash string `json:"baseHash"`
- BaseImageName string `json:"baseImageName"`
- BaseLayer string `json:"baseLayer"`
-}
-
-func (b *Backend) getSaveOptions(req *pb.SaveRequest) saveOptions {
- var opt = saveOptions{
+func (b *Backend) getSaveOptions(req *pb.SaveRequest) SaveOptions {
+ var opt = SaveOptions{
sysCtx: image.GetSystemContext(),
localStore: b.daemon.localStore,
saveID: req.GetSaveID(),
@@ -160,24 +74,7 @@ func (b *Backend) getSaveOptions(req *pb.SaveRequest) saveOptions {
return opt
}
- opt.sep = separatorSave{
- base: req.GetSep().GetBase(),
- lib: req.GetSep().GetLib(),
- dest: req.GetSep().GetDest(),
- log: logrus.WithFields(logrus.Fields{"SaveID": req.GetSaveID()}),
- enabled: req.GetSep().GetEnabled(),
- renameFile: req.GetSep().GetRename(),
- }
-
- // save separated image
- tmpRoot := filepath.Join(b.daemon.opts.DataRoot, filepath.Join(dataRootTmpDirPrefix, req.GetSaveID()))
- untar := filepath.Join(tmpRoot, untarTempDirName)
- appDir := filepath.Join(tmpRoot, appUntarTempDirName)
- baseDir := filepath.Join(tmpRoot, baseUntarTempDirName)
- libDir := filepath.Join(tmpRoot, libUntarTempDirName)
-
- opt.sep.tmpDir = imageTmpDir{app: appDir, base: baseDir, lib: libDir, untar: untar, root: tmpRoot}
- opt.outputPath = filepath.Join(untar, unionTarName)
+ opt.sep, opt.outputPath = separator.GetSepSaveOptions(req, opt.logEntry, b.daemon.opts.DataRoot)
return opt
}
@@ -213,15 +110,14 @@ func (b *Backend) Save(req *pb.SaveRequest, stream pb.Control_SaveServer) (err e
return err
}
- // separatorSave found
- if opts.sep.enabled {
- return separateImage(opts)
+ if opts.sep.Enabled() {
+ return opts.sep.SeparateImage(opts.localStore, opts.oriImgList, opts.outputPath)
}
return nil
}
-func exportHandler(ctx context.Context, opts *saveOptions) func() error {
+func exportHandler(ctx context.Context, opts *SaveOptions) func() error {
return func() error {
defer func() {
opts.logger.CloseContent()
@@ -278,7 +174,7 @@ func messageHandler(stream pb.Control_SaveServer, cliLogger *logger.Logger) func
}
}
-func (opts *saveOptions) manage() error {
+func (opts *SaveOptions) manage() error {
if err := opts.checkImageNameIsID(); err != nil {
return err
}
@@ -288,29 +184,22 @@ func (opts *saveOptions) manage() error {
if err := opts.filterImageName(); err != nil {
return err
}
- if err := opts.loadRenameFile(); err != nil {
+ if err := opts.sep.LoadRenameFile(); err != nil {
return err
}
return nil
}
-func (opts *saveOptions) checkImageNameIsID() error {
+func (opts *SaveOptions) checkImageNameIsID() error {
imageNames := opts.oriImgList
- if opts.sep.enabled {
- if len(opts.sep.base) != 0 {
- imageNames = append(imageNames, opts.sep.base)
- }
- if len(opts.sep.lib) != 0 {
- imageNames = append(imageNames, opts.sep.lib)
- }
- }
+ imageNames = append(imageNames, opts.sep.ImageNames()...)
for _, name := range imageNames {
_, img, err := image.FindImage(opts.localStore, name)
if err != nil {
return errors.Wrapf(err, "check image name failed when finding image name %q", name)
}
- if strings.HasPrefix(img.ID, name) && opts.sep.enabled {
+ if strings.HasPrefix(img.ID, name) && opts.sep.Enabled() {
return errors.Errorf("using image ID %q as image name to save separated image is not allowed", name)
}
}
@@ -318,7 +207,7 @@ func (opts *saveOptions) checkImageNameIsID() error {
return nil
}
-func (opts *saveOptions) setFormat() error {
+func (opts *SaveOptions) setFormat() error {
switch opts.format {
case constant.DockerTransport:
opts.format = constant.DockerArchiveTransport
@@ -331,7 +220,7 @@ func (opts *saveOptions) setFormat() error {
return nil
}
-func (opts *saveOptions) filterImageName() error {
+func (opts *SaveOptions) filterImageName() error {
if opts.format == constant.OCIArchiveTransport {
opts.finalImageOrdered = opts.oriImgList
return nil
@@ -367,507 +256,3 @@ func (opts *saveOptions) filterImageName() error {
return nil
}
-
-func (opts *saveOptions) loadRenameFile() error {
- if len(opts.sep.renameFile) != 0 {
- var reName []renames
- if err := util.LoadJSONFile(opts.sep.renameFile, &reName); err != nil {
- return errors.Wrap(err, "check rename file failed")
- }
- opts.sep.renameData = reName
- }
-
- return nil
-}
-
-func (s *separatorSave) getLayerHashFromStorage(store *store.Store, name string) ([]string, error) {
- if len(name) == 0 {
- return nil, nil
- }
- _, img, err := image.FindImage(store, name)
- if err != nil {
- return nil, err
- }
-
- layer, err := store.Layer(img.TopLayer)
- if err != nil {
- return nil, errors.Wrapf(err, "failed to get top layer for image %s", name)
- }
-
- var layers []string
- // add each layer in the layers until reach the root layer
- for layer != nil {
- fields := strings.Split(layer.UncompressedDigest.String(), ":")
- if len(fields) != 2 {
- return nil, errors.Errorf("error format of layer of image %s", name)
- }
- layers = append(layers, fields[1])
- if layer.Parent == "" {
- break
- }
- layer, err = store.Layer(layer.Parent)
- if err != nil {
- return nil, errors.Wrapf(err, "unable to read layer %q", layer.Parent)
- }
- }
-
- return layers, nil
-}
-
-// process physic file
-func (s *separatorSave) constructLayerMap() (map[string]string, error) {
- path := s.tmpDir.untar
- files, rErr := ioutil.ReadDir(path)
- if rErr != nil {
- return nil, rErr
- }
-
- var layerMap = make(map[string]string, len(files))
- // process layer's file
- for _, file := range files {
- if file.IsDir() {
- layerFile := filepath.Join(path, file.Name(), layerTarName)
- oriFile, err := os.Readlink(layerFile)
- if err != nil {
- return nil, err
- }
- physicFile := filepath.Join(path, file.Name(), oriFile)
- layerMap[filepath.Base(physicFile)] = filepath.Join(file.Name(), layerTarName)
- if err := os.Rename(physicFile, layerFile); err != nil {
- return nil, err
- }
- }
- }
-
- return layerMap, nil
-}
-
-func getLayerHashFromTar(layerMap map[string]string, layer []string) map[string]string {
- hashMap := make(map[string]string, len(layer))
- // first reverse map since it's <k-v> is unique
- revMap := make(map[string]string, len(layerMap))
- for k, v := range layerMap {
- revMap[v] = k
- }
- for _, l := range layer {
- if v, ok := revMap[l]; ok {
- // format is like xxx(hash): xxx/layer.tar
- hashMap[strings.TrimSuffix(v, tarSuffix)] = l
- }
- }
-
- return hashMap
-}
-
-func (s *separatorSave) adjustLayers() ([]imageManifest, error) {
- s.log.Info("Adjusting layers for saving separated image")
-
- layerMap, err := s.constructLayerMap()
- if err != nil {
- s.log.Errorf("Process layers failed: %v", err)
- return nil, err
- }
-
- // process manifest file
- var man []imageManifest
- if lErr := util.LoadJSONFile(filepath.Join(s.tmpDir.untar, manifestDataFile), &man); lErr != nil {
- return nil, lErr
- }
-
- for i, img := range man {
- layers := make([]string, len(img.Layers))
- for i, layer := range img.Layers {
- layers[i] = layerMap[layer]
- }
- man[i].Layers = layers
- man[i].HashMap = getLayerHashFromTar(layerMap, layers)
- }
- buf, err := json.Marshal(&man)
- if err != nil {
- return nil, err
- }
- if err := ioutils.AtomicWriteFile(manifestFile, buf, constant.DefaultSharedFileMode); err != nil {
- return nil, err
- }
-
- return man, nil
-}
-
-func separateImage(opt saveOptions) (err error) {
- s := &opt.sep
- s.log.Infof("Start saving separated images %v", opt.oriImgList)
-
- if err = os.MkdirAll(s.dest, constant.DefaultRootDirMode); err != nil {
- return err
- }
-
- defer func() {
- if tErr := os.RemoveAll(s.tmpDir.root); tErr != nil && !os.IsNotExist(tErr) {
- s.log.Warnf("Removing save tmp directory %q failed: %v", s.tmpDir.root, tErr)
- }
- if err != nil {
- if rErr := os.RemoveAll(s.dest); rErr != nil && !os.IsNotExist(rErr) {
- s.log.Warnf("Removing save dest directory %q failed: %v", s.dest, rErr)
- }
- }
- }()
- if err = util.UnpackFile(opt.outputPath, s.tmpDir.untar, archive.Gzip, true); err != nil {
- return errors.Wrapf(err, "unpack %q failed", opt.outputPath)
- }
- manifest, aErr := s.adjustLayers()
- if aErr != nil {
- return errors.Wrap(aErr, "adjust layers failed")
- }
-
- imgInfos, cErr := s.constructImageInfos(manifest, opt.localStore)
- if cErr != nil {
- return errors.Wrap(cErr, "process image infos failed")
- }
-
- if err = s.processImageLayers(imgInfos); err != nil {
- return err
- }
-
- return nil
-}
-
-func (s *separatorSave) processImageLayers(imgInfos map[string]imageInfo) error {
- s.log.Info("Processing image layers")
- var (
- tarballs = make(map[string]tarballInfo)
- baseImagesMap = make(imageLayersMap, 1)
- libImagesMap = make(imageLayersMap, 1)
- appImagesMap = make(imageLayersMap, 1)
- )
- var sortedKey []string
- for k := range imgInfos {
- sortedKey = append(sortedKey, k)
- }
- sort.Strings(sortedKey)
- for _, k := range sortedKey {
- info := imgInfos[k]
- if err := s.clearTempDirs(); err != nil {
- return errors.Wrap(err, "clear tmp dirs failed")
- }
- var t tarballInfo
- // process base
- if err := info.processBaseImg(s, baseImagesMap, &t); err != nil {
- return errors.Wrapf(err, "process base images %s failed", info.nameTag)
- }
- // process lib
- if err := info.processLibImg(s, libImagesMap, &t); err != nil {
- return errors.Wrapf(err, "process lib images %s failed", info.nameTag)
- }
- // process app
- if err := info.processAppImg(s, appImagesMap, &t); err != nil {
- return errors.Wrapf(err, "process app images %s failed", info.nameTag)
- }
- tarballs[info.nameTag] = t
- }
- buf, err := json.Marshal(&tarballs)
- if err != nil {
- return err
- }
- // manifest file
- manifestFile := filepath.Join(s.dest, manifestFile)
- if err := ioutils.AtomicWriteFile(manifestFile, buf, constant.DefaultRootFileMode); err != nil {
- return err
- }
-
- s.log.Info("Save separated image succeed")
- return nil
-}
-
-func (s *separatorSave) clearTempDirs() error {
- dirs := []string{s.tmpDir.base, s.tmpDir.app, s.tmpDir.lib}
- for _, dir := range dirs {
- if err := os.RemoveAll(dir); err != nil {
- return err
- }
- if err := os.MkdirAll(dir, constant.DefaultRootDirMode); err != nil {
- return err
- }
- }
- return nil
-}
-
-// processTarName will trim the prefix of image name like example.io/library/myapp:v1
-// after processed, the name will be myapp_v1_suffix
-// mind: suffix here should not contain path separator
-func (info imageInfo) processTarName(suffix string) string {
- originNames := strings.Split(info.name, string(os.PathSeparator))
- originTags := strings.Split(info.tag, string(os.PathSeparator))
- // get the last element of the list, which mast be the right name without prefix
- name := originNames[len(originNames)-1]
- tag := originTags[len(originTags)-1]
-
- return fmt.Sprintf("%s_%s%s", name, tag, suffix)
-}
-
-func (info *imageInfo) processBaseImg(sep *separatorSave, baseImagesMap map[string]string, tarball *tarballInfo) error {
- // process base
- tarball.BaseImageName = sep.base
- if len(info.layers.base) != 0 {
- sep.log.Infof("Base image %s has %d layers", sep.base, len(info.layers.base))
- tarball.BaseLayer = info.layers.base[0]
- }
- for _, layerID := range info.layers.base {
- if baseImg, ok := baseImagesMap[layerID]; !ok {
- srcLayerPath := filepath.Join(sep.tmpDir.untar, layerID)
- destLayerPath := filepath.Join(sep.tmpDir.base, layerID)
- if err := os.Rename(srcLayerPath, destLayerPath); err != nil {
- return err
- }
- baseTarName := info.processTarName(baseTarNameSuffix)
- baseTarName = sep.rename(baseTarName)
- baseTarPath := filepath.Join(sep.dest, baseTarName)
- if err := util.PackFiles(sep.tmpDir.base, baseTarPath, archive.Gzip, true); err != nil {
- return err
- }
- baseImagesMap[layerID] = baseTarPath
- tarball.BaseTarName = baseTarName
- digest, err := util.SHA256Sum(baseTarPath)
- if err != nil {
- return errors.Wrapf(err, "check sum for new base image %s failed", baseTarName)
- }
- tarball.BaseHash = digest
- } else {
- tarball.BaseTarName = filepath.Base(baseImg)
- digest, err := util.SHA256Sum(baseImg)
- if err != nil {
- return errors.Wrapf(err, "check sum for reuse base image %s failed", baseImg)
- }
- tarball.BaseHash = digest
- }
- }
-
- return nil
-}
-
-func (info *imageInfo) processLibImg(sep *separatorSave, libImagesMap map[string]string, tarball *tarballInfo) error {
- // process lib
- if info.layers.lib == nil {
- return nil
- }
-
- tarball.LibImageName = sep.lib
- sep.log.Infof("Lib image %s has %d layers", sep.lib, len(info.layers.lib))
- for _, layerID := range info.layers.lib {
- tarball.LibLayers = append(tarball.LibLayers, layerID)
- if libImg, ok := libImagesMap[layerID]; !ok {
- srcLayerPath := filepath.Join(sep.tmpDir.untar, layerID)
- destLayerPath := filepath.Join(sep.tmpDir.lib, layerID)
- if err := os.Rename(srcLayerPath, destLayerPath); err != nil {
- return err
- }
- libTarName := info.processTarName(libTarNameSuffix)
- libTarName = sep.rename(libTarName)
- libTarPath := filepath.Join(sep.dest, libTarName)
- if err := util.PackFiles(sep.tmpDir.lib, libTarPath, archive.Gzip, true); err != nil {
- return err
- }
- libImagesMap[layerID] = libTarPath
- tarball.LibTarName = libTarName
- digest, err := util.SHA256Sum(libTarPath)
- if err != nil {
- return errors.Wrapf(err, "check sum for lib image %s failed", sep.lib)
- }
- tarball.LibHash = digest
- } else {
- tarball.LibTarName = filepath.Base(libImg)
- digest, err := util.SHA256Sum(libImg)
- if err != nil {
- return errors.Wrapf(err, "check sum for lib image %s failed", sep.lib)
- }
- tarball.LibHash = digest
- }
- }
-
- return nil
-}
-
-func (info *imageInfo) processAppImg(sep *separatorSave, appImagesMap map[string]string, tarball *tarballInfo) error {
- // process app
- sep.log.Infof("App image %s has %d layers", info.nameTag, len(info.layers.app))
- appTarName := info.processTarName(appTarNameSuffix)
- appTarName = sep.rename(appTarName)
- appTarPath := filepath.Join(sep.dest, appTarName)
- for _, layerID := range info.layers.app {
- srcLayerPath := filepath.Join(sep.tmpDir.untar, layerID)
- destLayerPath := filepath.Join(sep.tmpDir.app, layerID)
- if err := os.Rename(srcLayerPath, destLayerPath); err != nil {
- if appImg, ok := appImagesMap[layerID]; ok {
- return errors.Errorf("lib layers %s already saved in %s for image %s",
- layerID, appImg, info.nameTag)
- }
- }
- appImagesMap[layerID] = appTarPath
- tarball.AppLayers = append(tarball.AppLayers, layerID)
- }
- // create config file
- if err := info.createManifestFile(sep); err != nil {
- return err
- }
- if err := info.createRepositoriesFile(sep); err != nil {
- return err
- }
-
- srcConfigPath := filepath.Join(sep.tmpDir.untar, info.config)
- destConfigPath := filepath.Join(sep.tmpDir.app, info.config)
- if err := os.Rename(srcConfigPath, destConfigPath); err != nil {
- return err
- }
-
- if err := util.PackFiles(sep.tmpDir.app, appTarPath, archive.Gzip, true); err != nil {
- return err
- }
- tarball.AppTarName = appTarName
- digest, err := util.SHA256Sum(appTarPath)
- if err != nil {
- return errors.Wrapf(err, "check sum for app image %s failed", info.nameTag)
- }
- tarball.AppHash = digest
-
- return nil
-}
-
-func (info imageInfo) createRepositoriesFile(sep *separatorSave) error {
- // create repositories
- type repoItem map[string]string
- repo := make(map[string]repoItem, 1)
- item := make(repoItem, 1)
- if _, ok := item[info.tag]; !ok {
- item[info.tag] = info.topLayer
- }
- repo[info.name] = item
- buf, err := json.Marshal(repo)
- if err != nil {
- return err
- }
- repositoryFile := filepath.Join(sep.tmpDir.app, repositoriesFile)
- if err := ioutils.AtomicWriteFile(repositoryFile, buf, constant.DefaultRootFileMode); err != nil {
- return err
- }
- return nil
-}
-
-func (info imageInfo) createManifestFile(sep *separatorSave) error {
- // create manifest.json
- var s = imageManifest{
- Config: info.config,
- Layers: info.layers.all,
- RepoTags: info.repoTags,
- }
- var m []imageManifest
- m = append(m, s)
- buf, err := json.Marshal(&m)
- if err != nil {
- return err
- }
- data := filepath.Join(sep.tmpDir.app, manifestDataFile)
- if err := ioutils.AtomicWriteFile(data, buf, constant.DefaultRootFileMode); err != nil {
- return err
- }
- return nil
-}
-
-func getLayersID(layer []string) []string {
- var after = make([]string, len(layer))
- for i, v := range layer {
- after[i] = strings.Split(v, "/")[0]
- }
- return after
-}
-
-func (s *separatorSave) constructSingleImgInfo(mani imageManifest, store *store.Store) (imageInfo, error) {
- var libLayers, appLayers []string
- // image name should not be empty here
- if len(mani.RepoTags) == 0 {
- return imageInfo{}, errors.New("image name and tag is empty")
- }
- // if there is more than one repoTag, will use first one as image name
- imageRepoFields := strings.Split(mani.RepoTags[0], ":")
- imageLayers := getLayersID(mani.Layers)
-
- libs, bases, err := s.checkLayersHash(mani.HashMap, store)
- if err != nil {
- return imageInfo{}, errors.Wrap(err, "compare layers failed")
- }
- baseLayers := imageLayers[0:len(bases)]
- if len(libs) != 0 {
- libLayers = imageLayers[len(bases):len(libs)]
- appLayers = imageLayers[len(libs):]
- } else {
- libLayers = nil
- appLayers = imageLayers[len(bases):]
- }
-
- return imageInfo{
- config: mani.Config,
- repoTags: mani.RepoTags,
- nameTag: mani.RepoTags[0],
- name: strings.Join(imageRepoFields[0:len(imageRepoFields)-1], ":"),
- tag: imageRepoFields[len(imageRepoFields)-1],
- layers: layer{app: appLayers, lib: libLayers, base: baseLayers, all: mani.Layers},
- topLayer: imageLayers[len(imageLayers)-1],
- }, nil
-}
-
-func (s *separatorSave) checkLayersHash(layerHashMap map[string]string, store *store.Store) ([]string, []string, error) {
- libHash, err := s.getLayerHashFromStorage(store, s.lib)
- if err != nil {
- return nil, nil, errors.Wrapf(err, "get lib image %s layers failed", s.lib)
- }
- baseHash, err := s.getLayerHashFromStorage(store, s.base)
- if err != nil {
- return nil, nil, errors.Wrapf(err, "get base image %s layers failed", s.base)
- }
- if len(baseHash) > 1 {
- return nil, nil, errors.Errorf("number of base layers %d more than one", len(baseHash))
- }
- if len(libHash) >= len(layerHashMap) || len(baseHash) >= len(layerHashMap) {
- return nil, nil, errors.Errorf("number of base or lib layers is equal or greater than saved app layers")
- }
-
- for _, l := range libHash {
- if _, ok := layerHashMap[l]; !ok {
- return nil, nil, errors.Errorf("dismatch checksum for lib image %s", s.lib)
- }
- }
- for _, b := range baseHash {
- if _, ok := layerHashMap[b]; !ok {
- return nil, nil, errors.Errorf("dismatch checksum for base image %s", s.base)
- }
- }
-
- return libHash, baseHash, nil
-}
-
-func (s *separatorSave) constructImageInfos(manifest []imageManifest, store *store.Store) (map[string]imageInfo, error) {
- s.log.Info("Constructing image info")
-
- var imgInfos = make(map[string]imageInfo, 1)
- for _, mani := range manifest {
- imgInfo, err := s.constructSingleImgInfo(mani, store)
- if err != nil {
- s.log.Errorf("Constructing image info failed: %v", err)
- return nil, errors.Wrap(err, "construct image info failed")
- }
- if _, ok := imgInfos[imgInfo.nameTag]; !ok {
- imgInfos[imgInfo.nameTag] = imgInfo
- }
- }
- return imgInfos, nil
-}
-
-func (s *separatorSave) rename(name string) string {
- if len(s.renameData) != 0 {
- s.log.Info("Renaming image tarballs")
- for _, item := range s.renameData {
- if item.Name == name {
- return item.Rename
- }
- }
- }
- return name
-}
diff --git a/daemon/separator/image_info.go b/daemon/separator/image_info.go
new file mode 100644
index 0000000..53bcd7a
--- /dev/null
+++ b/daemon/separator/image_info.go
@@ -0,0 +1,235 @@
+// Copyright (c) Huawei Technologies Co., Ltd. 2020. All rights reserved.
+// isula-build licensed under the Mulan PSL v2.
+// You can use this software according to the terms and conditions of the Mulan PSL v2.
+// You may obtain a copy of Mulan PSL v2 at:
+// http://license.coscl.org.cn/MulanPSL2
+// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR
+// PURPOSE.
+// See the Mulan PSL v2 for more details.
+// Author: Xiang Li
+// Create: 2021-12-09
+// Description: This file is handling image info for image separator
+
+package separator
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/containers/storage/pkg/archive"
+ "github.com/docker/docker/pkg/ioutils"
+ "github.com/pkg/errors"
+
+ constant "isula.org/isula-build"
+ "isula.org/isula-build/util"
+)
+
+type layer struct {
+ all []string
+ base []string
+ lib []string
+ app []string
+}
+
+type imageInfo struct {
+ layers layer
+ repoTags []string
+ config string
+ name string
+ tag string
+ nameTag string
+ topLayer string
+}
+
+// processTarName will trim the prefix of image name like example.io/library/myapp:v1
+// after processed, the name will be myapp_v1_suffix
+// mind: suffix here should not contain path separator
+func (info *imageInfo) processTarName(suffix string) string {
+ originNames := strings.Split(info.name, string(os.PathSeparator))
+ originTags := strings.Split(info.tag, string(os.PathSeparator))
+ // get the last element of the list, which mast be the right name without prefix
+ name := originNames[len(originNames)-1]
+ tag := originTags[len(originTags)-1]
+
+ return fmt.Sprintf("%s_%s%s", name, tag, suffix)
+}
+
+func (info *imageInfo) processBaseImg(sep *Saver, baseImagesMap map[string]string, tarball *tarballInfo) error {
+ // process base
+ tarball.BaseImageName = sep.base
+ if len(info.layers.base) != 0 {
+ sep.log.Infof("Base image %s has %d layers", sep.base, len(info.layers.base))
+ tarball.BaseLayer = info.layers.base[0]
+ }
+ for _, layerID := range info.layers.base {
+ if baseImg, ok := baseImagesMap[layerID]; !ok {
+ srcLayerPath := filepath.Join(sep.tmpDir.untar, layerID)
+ destLayerPath := filepath.Join(sep.tmpDir.base, layerID)
+ if err := os.Rename(srcLayerPath, destLayerPath); err != nil {
+ return err
+ }
+ baseTarName := info.processTarName(baseTarNameSuffix)
+ baseTarName = sep.getRename(baseTarName)
+ baseTarPath := filepath.Join(sep.dest, baseTarName)
+ if err := util.PackFiles(sep.tmpDir.base, baseTarPath, archive.Gzip, true); err != nil {
+ return err
+ }
+ baseImagesMap[layerID] = baseTarPath
+ tarball.BaseTarName = baseTarName
+ digest, err := util.SHA256Sum(baseTarPath)
+ if err != nil {
+ return errors.Wrapf(err, "check sum for new base image %s failed", baseTarName)
+ }
+ tarball.BaseHash = digest
+ } else {
+ tarball.BaseTarName = filepath.Base(baseImg)
+ digest, err := util.SHA256Sum(baseImg)
+ if err != nil {
+ return errors.Wrapf(err, "check sum for reuse base image %s failed", baseImg)
+ }
+ tarball.BaseHash = digest
+ }
+ }
+
+ return nil
+}
+
+func (info *imageInfo) processLibImg(sep *Saver, libImagesMap map[string]string, tarball *tarballInfo) error {
+ // process lib
+ if info.layers.lib == nil {
+ return nil
+ }
+
+ tarball.LibImageName = sep.lib
+ sep.log.Infof("Lib image %s has %d layers", sep.lib, len(info.layers.lib))
+ for _, layerID := range info.layers.lib {
+ tarball.LibLayers = append(tarball.LibLayers, layerID)
+ if libImg, ok := libImagesMap[layerID]; !ok {
+ srcLayerPath := filepath.Join(sep.tmpDir.untar, layerID)
+ destLayerPath := filepath.Join(sep.tmpDir.lib, layerID)
+ if err := os.Rename(srcLayerPath, destLayerPath); err != nil {
+ return err
+ }
+ libTarName := info.processTarName(libTarNameSuffix)
+ libTarName = sep.getRename(libTarName)
+ libTarPath := filepath.Join(sep.dest, libTarName)
+ if err := util.PackFiles(sep.tmpDir.lib, libTarPath, archive.Gzip, true); err != nil {
+ return err
+ }
+ libImagesMap[layerID] = libTarPath
+ tarball.LibTarName = libTarName
+ digest, err := util.SHA256Sum(libTarPath)
+ if err != nil {
+ return errors.Wrapf(err, "check sum for lib image %s failed", sep.lib)
+ }
+ tarball.LibHash = digest
+ } else {
+ tarball.LibTarName = filepath.Base(libImg)
+ digest, err := util.SHA256Sum(libImg)
+ if err != nil {
+ return errors.Wrapf(err, "check sum for lib image %s failed", sep.lib)
+ }
+ tarball.LibHash = digest
+ }
+ }
+
+ return nil
+}
+
+func (info *imageInfo) processAppImg(sep *Saver, appImagesMap map[string]string, tarball *tarballInfo) error {
+ // process app
+ sep.log.Infof("App image %s has %d layers", info.nameTag, len(info.layers.app))
+ appTarName := info.processTarName(appTarNameSuffix)
+ appTarName = sep.getRename(appTarName)
+ appTarPath := filepath.Join(sep.dest, appTarName)
+ for _, layerID := range info.layers.app {
+ srcLayerPath := filepath.Join(sep.tmpDir.untar, layerID)
+ destLayerPath := filepath.Join(sep.tmpDir.app, layerID)
+ if err := os.Rename(srcLayerPath, destLayerPath); err != nil {
+ if appImg, ok := appImagesMap[layerID]; ok {
+ return errors.Errorf("lib layers %s already saved in %s for image %s",
+ layerID, appImg, info.nameTag)
+ }
+ }
+ appImagesMap[layerID] = appTarPath
+ tarball.AppLayers = append(tarball.AppLayers, layerID)
+ }
+ // create config file
+ if err := info.createManifestFile(sep); err != nil {
+ return err
+ }
+ if err := info.createRepositoriesFile(sep); err != nil {
+ return err
+ }
+
+ srcConfigPath := filepath.Join(sep.tmpDir.untar, info.config)
+ destConfigPath := filepath.Join(sep.tmpDir.app, info.config)
+ if err := os.Rename(srcConfigPath, destConfigPath); err != nil {
+ return err
+ }
+
+ if err := util.PackFiles(sep.tmpDir.app, appTarPath, archive.Gzip, true); err != nil {
+ return err
+ }
+ tarball.AppTarName = appTarName
+ digest, err := util.SHA256Sum(appTarPath)
+ if err != nil {
+ return errors.Wrapf(err, "check sum for app image %s failed", info.nameTag)
+ }
+ tarball.AppHash = digest
+
+ return nil
+}
+
+func (info *imageInfo) createRepositoriesFile(sep *Saver) error {
+ // create repositories
+ type repoItem map[string]string
+ repo := make(map[string]repoItem, 1)
+ item := make(repoItem, 1)
+ if _, ok := item[info.tag]; !ok {
+ item[info.tag] = info.topLayer
+ }
+ repo[info.name] = item
+ buf, err := json.Marshal(repo)
+ if err != nil {
+ return err
+ }
+ repositoryFile := filepath.Join(sep.tmpDir.app, repositoriesFile)
+ if err := ioutils.AtomicWriteFile(repositoryFile, buf, constant.DefaultRootFileMode); err != nil {
+ return err
+ }
+ return nil
+}
+
+// imageManifest return image's manifest info
+type imageManifest struct {
+ Config string `json:"Config"`
+ RepoTags []string `json:"RepoTags"`
+ Layers []string `json:"Layers"`
+ // Not shown in the json file
+ HashMap map[string]string `json:"-"`
+}
+
+func (info *imageInfo) createManifestFile(sep *Saver) error {
+ // create manifest.json
+ var s = imageManifest{
+ Config: info.config,
+ Layers: info.layers.all,
+ RepoTags: info.repoTags,
+ }
+ var m []imageManifest
+ m = append(m, s)
+ buf, err := json.Marshal(&m)
+ if err != nil {
+ return err
+ }
+ data := filepath.Join(sep.tmpDir.app, manifestDataFile)
+ if err := ioutils.AtomicWriteFile(data, buf, constant.DefaultRootFileMode); err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/daemon/separator/load.go b/daemon/separator/load.go
new file mode 100644
index 0000000..d02dccf
--- /dev/null
+++ b/daemon/separator/load.go
@@ -0,0 +1,283 @@
+// Copyright (c) Huawei Technologies Co., Ltd. 2020. All rights reserved.
+// isula-build licensed under the Mulan PSL v2.
+// You can use this software according to the terms and conditions of the Mulan PSL v2.
+// You may obtain a copy of Mulan PSL v2 at:
+// http://license.coscl.org.cn/MulanPSL2
+// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR
+// PURPOSE.
+// See the Mulan PSL v2 for more details.
+// Author: Xiang Li
+// Create: 2021-12-09
+// Description: This file is handling "load" part for image separator at server side
+
+package separator
+
+import (
+ "io/ioutil"
+ "os"
+ "path/filepath"
+
+ "github.com/containers/storage/pkg/archive"
+ filepath_securejoin "github.com/cyphar/filepath-securejoin"
+ "github.com/pkg/errors"
+ "github.com/sirupsen/logrus"
+
+ constant "isula.org/isula-build"
+ pb "isula.org/isula-build/api/services"
+ "isula.org/isula-build/image"
+ "isula.org/isula-build/util"
+)
+
+type loadImageTmpDir struct {
+ app string
+ base string
+ lib string
+ root string
+}
+
+// Loader the main instance for loading separated images
+type Loader struct {
+ log *logrus.Entry
+ tmpDir loadImageTmpDir
+ info tarballInfo
+ appName string
+ basePath string
+ appPath string
+ libPath string
+ dir string
+ skipCheck bool
+ enabled bool
+}
+
+// GetSepLoadOptions returns Loader instance from LoadRequest
+func GetSepLoadOptions(req *pb.LoadRequest, logEntry *logrus.Entry, dataRoot string) (Loader, error) {
+ var tmpRoot = filepath.Join(dataRoot, filepath.Join(constant.DataRootTmpDirPrefix, req.GetLoadID()))
+ var sep = Loader{
+ appName: req.GetSep().GetApp(),
+ basePath: req.GetSep().GetBase(),
+ libPath: req.GetSep().GetLib(),
+ dir: req.GetSep().GetDir(),
+ log: logEntry,
+ tmpDir: loadImageTmpDir{
+ root: tmpRoot,
+ base: filepath.Join(tmpRoot, tmpBaseDirName),
+ lib: filepath.Join(tmpRoot, tmpLibDirName),
+ app: filepath.Join(tmpRoot, tmpAppDirName),
+ },
+ skipCheck: req.GetSep().GetSkipCheck(),
+ enabled: req.GetSep().GetEnabled(),
+ }
+
+ // check image name and add "latest" tag if not present
+ _, appImgName, err := image.GetNamedTaggedReference(sep.appName)
+ if err != nil {
+ return Loader{}, err
+ }
+ if len(appImgName) == 0 {
+ return Loader{}, errors.New("app image name should not be empty")
+ }
+ sep.appName = appImgName
+ return sep, nil
+}
+
+// LoadSeparatedImage the main method of Loader, tries to load the separated images, and returns the path of the
+// reconstructed image tarball for later handling
+func (l *Loader) LoadSeparatedImage() (string, error) {
+ l.log.Infof("Starting load separated image %s", l.appName)
+
+ // load manifest file to get tarball info
+ if err := l.getTarballInfo(); err != nil {
+ return "", errors.Wrap(err, "failed to get tarball info")
+ }
+ if err := l.constructLayerPath(); err != nil {
+ return "", err
+ }
+ // checksum for image tarballs
+ if err := l.tarballCheckSum(); err != nil {
+ return "", err
+ }
+ // process image tarballs and get final constructed image tarball
+ return l.processTarballs()
+}
+
+func (l *Loader) getTarballInfo() error {
+ manifest, err := filepath_securejoin.SecureJoin(l.dir, manifestFile)
+ if err != nil {
+ return errors.Wrap(err, "join manifest file path failed")
+ }
+
+ var t = make(map[string]tarballInfo, 1)
+ if err = util.LoadJSONFile(manifest, &t); err != nil {
+ return errors.Wrap(err, "load manifest file failed")
+ }
+
+ tarball, ok := t[l.appName]
+ if !ok {
+ return errors.Errorf("failed to find app image %s", l.appName)
+ }
+ if len(tarball.AppTarName) == 0 {
+ return errors.Errorf("app image %s tarball can not be empty", tarball.AppTarName)
+ }
+ if len(tarball.BaseTarName) == 0 {
+ return errors.Errorf("base image %s tarball can not be empty", tarball.BaseImageName)
+ }
+ l.info = tarball
+
+ return nil
+}
+
+func (l *Loader) joinPath(path, tarName, str string, canBeEmpty bool) (string, error) {
+ if len(path) != 0 {
+ return path, nil
+ }
+ l.log.Infof("%s image path is empty, use path from manifest", str)
+ return filepath_securejoin.SecureJoin(l.dir, tarName)
+}
+
+func (l *Loader) constructLayerPath() error {
+ l.log.Infof("Construct image layer pathes for %s\n", l.appName)
+
+ var err error
+ if l.basePath, err = l.joinPath(l.basePath, l.info.BaseTarName, "Base", false); err != nil {
+ return err
+ }
+ if l.libPath, err = l.joinPath(l.libPath, l.info.LibTarName, "Lib", true); err != nil {
+ return err
+ }
+ if l.appPath, err = l.joinPath(l.appPath, l.info.AppTarName, "App", false); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (l *Loader) tarballCheckSum() error {
+ if l.skipCheck {
+ l.log.Info("Skip checksum for tarballs")
+ return nil
+ }
+
+ type checkInfo struct {
+ path string
+ hash string
+ str string
+ canBeEmpty bool
+ }
+ checkLen := 3
+ var checkList = make([]checkInfo, 0, checkLen)
+ checkList = append(checkList, checkInfo{path: l.basePath, hash: l.info.BaseHash, canBeEmpty: false, str: "base"})
+ checkList = append(checkList, checkInfo{path: l.libPath, hash: l.info.LibHash, canBeEmpty: true, str: "lib"})
+ checkList = append(checkList, checkInfo{path: l.appPath, hash: l.info.AppHash, canBeEmpty: false, str: "app"})
+ for _, p := range checkList {
+ if len(p.path) == 0 && !p.canBeEmpty {
+ return errors.Errorf("%s image tarball path can not be empty", p.str)
+ }
+ if len(p.path) != 0 {
+ if err := util.CheckSum(p.path, p.hash); err != nil {
+ return errors.Wrapf(err, "check sum for file %q failed", p.path)
+ }
+ }
+ }
+
+ return nil
+}
+
+func (l *Loader) processTarballs() (string, error) {
+ if err := l.unpackTarballs(); err != nil {
+ return "", err
+ }
+
+ if err := l.reconstructImage(); err != nil {
+ return "", err
+ }
+
+ // pack app image to tarball
+ tarPath := filepath.Join(l.tmpDir.root, unionCompressedTarName)
+ if err := util.PackFiles(l.tmpDir.base, tarPath, archive.Gzip, true); err != nil {
+ return "", err
+ }
+
+ return tarPath, nil
+}
+
+func (l *Loader) unpackTarballs() error {
+ if err := l.makeTempDir(); err != nil {
+ return errors.Wrap(err, "failed to make temporary directories")
+ }
+
+ type unpackInfo struct{ path, dir, str string }
+ unpackLen := 3
+ var unpackList = make([]unpackInfo, 0, unpackLen)
+ unpackList = append(unpackList, unpackInfo{path: l.basePath, dir: l.tmpDir.base, str: "base"})
+ unpackList = append(unpackList, unpackInfo{path: l.libPath, dir: l.tmpDir.lib, str: "lib"})
+ unpackList = append(unpackList, unpackInfo{path: l.appPath, dir: l.tmpDir.app, str: "app"})
+
+ for _, p := range unpackList {
+ if len(p.path) != 0 {
+ if err := util.UnpackFile(p.path, p.dir, archive.Gzip, false); err != nil {
+ return errors.Wrapf(err, "unpack %s image tarball %q failed", p.str, p.path)
+ }
+ }
+ }
+
+ return nil
+}
+
+func (l *Loader) reconstructImage() error {
+ files, err := ioutil.ReadDir(l.tmpDir.app)
+ if err != nil {
+ return err
+ }
+
+ for _, f := range files {
+ src := filepath.Join(l.tmpDir.app, f.Name())
+ dest := filepath.Join(l.tmpDir.base, f.Name())
+ if err := os.Rename(src, dest); err != nil {
+ return errors.Wrapf(err, "reconstruct app file %q failed", l.info.AppTarName)
+ }
+ }
+
+ if len(l.libPath) != 0 {
+ files, err := ioutil.ReadDir(l.tmpDir.lib)
+ if err != nil {
+ return err
+ }
+
+ for _, f := range files {
+ src := filepath.Join(l.tmpDir.lib, f.Name())
+ dest := filepath.Join(l.tmpDir.base, f.Name())
+ if err := os.Rename(src, dest); err != nil {
+ return errors.Wrapf(err, "reconstruct lib file %q failed", l.info.LibTarName)
+ }
+ }
+ }
+
+ return nil
+}
+
+func (l *Loader) makeTempDir() error {
+ dirs := []string{l.tmpDir.root, l.tmpDir.app, l.tmpDir.base, l.tmpDir.lib}
+ for _, dir := range dirs {
+ if err := os.MkdirAll(dir, constant.DefaultRootDirMode); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// AppName returns the AppName of Loader
+func (l *Loader) AppName() string {
+ return l.appName
+}
+
+// TmpDirRoot returns the tmpDir.root of Loader
+func (l *Loader) TmpDirRoot() string {
+ return l.tmpDir.root
+}
+
+// Enabled returns whether separated-image feature is enabled
+func (l *Loader) Enabled() bool {
+ return l.enabled
+}
diff --git a/daemon/separator/save.go b/daemon/separator/save.go
new file mode 100644
index 0000000..a455335
--- /dev/null
+++ b/daemon/separator/save.go
@@ -0,0 +1,407 @@
+// Copyright (c) Huawei Technologies Co., Ltd. 2020. All rights reserved.
+// isula-build licensed under the Mulan PSL v2.
+// You can use this software according to the terms and conditions of the Mulan PSL v2.
+// You may obtain a copy of Mulan PSL v2 at:
+// http://license.coscl.org.cn/MulanPSL2
+// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR
+// PURPOSE.
+// See the Mulan PSL v2 for more details.
+// Author: Xiang Li
+// Create: 2021-12-09
+// Description: This file is handling "save" part for image separator at server side
+
+package separator
+
+import (
+ "encoding/json"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+
+ "github.com/containers/storage/pkg/archive"
+ "github.com/docker/docker/pkg/ioutils"
+ "github.com/pkg/errors"
+ "github.com/sirupsen/logrus"
+
+ constant "isula.org/isula-build"
+ pb "isula.org/isula-build/api/services"
+ "isula.org/isula-build/image"
+ "isula.org/isula-build/store"
+ "isula.org/isula-build/util"
+)
+
+// Saver the main instance for saving separated images
+type Saver struct {
+ log *logrus.Entry
+ renameData []renames
+ tmpDir imageTmpDir
+ base string
+ lib string
+ dest string
+ renameFile string
+ enabled bool
+}
+
+type renames struct {
+ OriName string `json:"name"`
+ NewName string `json:"rename"`
+}
+
+type imageTmpDir struct {
+ app string
+ base string
+ lib string
+ untar string
+ root string
+}
+
+type imageLayersMap map[string]string
+
+// GetSepSaveOptions returns Save instance from SaveRequest
+func GetSepSaveOptions(req *pb.SaveRequest, logEntry *logrus.Entry, dataRoot string) (Saver, string) {
+ var tmpRoot = filepath.Join(dataRoot, filepath.Join(constant.DataRootTmpDirPrefix, req.GetSaveID()))
+ var sep = Saver{
+ base: req.GetSep().GetBase(),
+ lib: req.GetSep().GetLib(),
+ dest: req.GetSep().GetDest(),
+ log: logEntry,
+ enabled: req.GetSep().GetEnabled(),
+ renameFile: req.GetSep().GetRename(),
+
+ tmpDir: imageTmpDir{
+ root: tmpRoot,
+ untar: filepath.Join(tmpRoot, untarTempDirName),
+ base: filepath.Join(tmpRoot, baseUntarTempDirName),
+ lib: filepath.Join(tmpRoot, libUntarTempDirName),
+ app: filepath.Join(tmpRoot, appUntarTempDirName),
+ },
+ }
+
+ return sep, filepath.Join(sep.tmpDir.untar, unionTarName)
+}
+
+// SeparateImage the main method of Saver, tries to separated the listed images to pieces
+func (s *Saver) SeparateImage(localStore *store.Store, oriImgList []string, outputPath string) (err error) {
+ s.log.Infof("Start saving separated images %v", oriImgList)
+
+ if err = os.MkdirAll(s.dest, constant.DefaultRootDirMode); err != nil {
+ return err
+ }
+
+ defer func() {
+ if tErr := os.RemoveAll(s.tmpDir.root); tErr != nil && !os.IsNotExist(tErr) {
+ s.log.Warnf("Removing save tmp directory %q failed: %v", s.tmpDir.root, tErr)
+ }
+ if err != nil {
+ if rErr := os.RemoveAll(s.dest); rErr != nil && !os.IsNotExist(rErr) {
+ s.log.Warnf("Removing save dest directory %q failed: %v", s.dest, rErr)
+ }
+ }
+ }()
+ if err = util.UnpackFile(outputPath, s.tmpDir.untar, archive.Gzip, true); err != nil {
+ return errors.Wrapf(err, "unpack %q failed", outputPath)
+ }
+ manifest, aErr := s.adjustLayers()
+ if aErr != nil {
+ return errors.Wrap(aErr, "adjust layers failed")
+ }
+
+ imgInfos, cErr := s.constructImageInfos(manifest, localStore)
+ if cErr != nil {
+ return errors.Wrap(cErr, "process image infos failed")
+ }
+
+ if err = s.processImageLayers(imgInfos); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (s *Saver) getLayerHashFromStorage(store *store.Store, name string) ([]string, error) {
+ if len(name) == 0 {
+ return nil, nil
+ }
+ _, img, err := image.FindImage(store, name)
+ if err != nil {
+ return nil, err
+ }
+
+ layer, err := store.Layer(img.TopLayer)
+ if err != nil {
+ return nil, errors.Wrapf(err, "failed to get top layer for image %s", name)
+ }
+
+ var layers []string
+ // add each layer in the layers until reach the root layer
+ for layer != nil {
+ fields := strings.Split(layer.UncompressedDigest.String(), ":")
+ if len(fields) != 2 {
+ return nil, errors.Errorf("error format of layer of image %s", name)
+ }
+ layers = append(layers, fields[1])
+ if layer.Parent == "" {
+ break
+ }
+ layer, err = store.Layer(layer.Parent)
+ if err != nil {
+ return nil, errors.Wrapf(err, "unable to read layer %q", layer.Parent)
+ }
+ }
+
+ return layers, nil
+}
+
+// process physic file
+func (s *Saver) constructLayerMap() (map[string]string, error) {
+ path := s.tmpDir.untar
+ files, rErr := ioutil.ReadDir(path)
+ if rErr != nil {
+ return nil, rErr
+ }
+
+ var layerMap = make(map[string]string, len(files))
+ // process layer's file
+ for _, file := range files {
+ if file.IsDir() {
+ layerFile := filepath.Join(path, file.Name(), layerTarName)
+ oriFile, err := os.Readlink(layerFile)
+ if err != nil {
+ return nil, err
+ }
+ physicFile := filepath.Join(path, file.Name(), oriFile)
+ layerMap[filepath.Base(physicFile)] = filepath.Join(file.Name(), layerTarName)
+ if err := os.Rename(physicFile, layerFile); err != nil {
+ return nil, err
+ }
+ }
+ }
+
+ return layerMap, nil
+}
+
+func (s *Saver) adjustLayers() ([]imageManifest, error) {
+ s.log.Info("Adjusting layers for saving separated image")
+
+ layerMap, err := s.constructLayerMap()
+ if err != nil {
+ s.log.Errorf("Process layers failed: %v", err)
+ return nil, err
+ }
+
+ // process manifest file
+ var man []imageManifest
+ if lErr := util.LoadJSONFile(filepath.Join(s.tmpDir.untar, manifestDataFile), &man); lErr != nil {
+ return nil, lErr
+ }
+
+ for i, img := range man {
+ layers := make([]string, len(img.Layers))
+ for i, layer := range img.Layers {
+ layers[i] = layerMap[layer]
+ }
+ man[i].Layers = layers
+ man[i].HashMap = getLayerHashFromTar(layerMap, layers)
+ }
+ buf, err := json.Marshal(&man)
+ if err != nil {
+ return nil, err
+ }
+ if err := ioutils.AtomicWriteFile(manifestFile, buf, constant.DefaultSharedFileMode); err != nil {
+ return nil, err
+ }
+
+ return man, nil
+}
+
+func (s *Saver) processImageLayers(imgInfos map[string]imageInfo) error {
+ s.log.Info("Processing image layers")
+ var (
+ tarballs = make(map[string]tarballInfo)
+ baseImagesMap = make(imageLayersMap, 1)
+ libImagesMap = make(imageLayersMap, 1)
+ appImagesMap = make(imageLayersMap, 1)
+ )
+ var sortedKey []string
+ for k := range imgInfos {
+ sortedKey = append(sortedKey, k)
+ }
+ sort.Strings(sortedKey)
+ for _, k := range sortedKey {
+ info := imgInfos[k]
+ if err := s.clearTempDirs(); err != nil {
+ return errors.Wrap(err, "clear tmp dirs failed")
+ }
+ var t tarballInfo
+ // process base
+ if err := info.processBaseImg(s, baseImagesMap, &t); err != nil {
+ return errors.Wrapf(err, "process base images %s failed", info.nameTag)
+ }
+ // process lib
+ if err := info.processLibImg(s, libImagesMap, &t); err != nil {
+ return errors.Wrapf(err, "process lib images %s failed", info.nameTag)
+ }
+ // process app
+ if err := info.processAppImg(s, appImagesMap, &t); err != nil {
+ return errors.Wrapf(err, "process app images %s failed", info.nameTag)
+ }
+ tarballs[info.nameTag] = t
+ }
+ buf, err := json.Marshal(&tarballs)
+ if err != nil {
+ return err
+ }
+ // manifest file
+ manifestFile := filepath.Join(s.dest, manifestFile)
+ if err := ioutils.AtomicWriteFile(manifestFile, buf, constant.DefaultRootFileMode); err != nil {
+ return err
+ }
+
+ s.log.Info("Save separated image succeed")
+ return nil
+}
+
+func (s *Saver) clearTempDirs() error {
+ dirs := []string{s.tmpDir.base, s.tmpDir.app, s.tmpDir.lib}
+ for _, dir := range dirs {
+ if err := os.RemoveAll(dir); err != nil {
+ return err
+ }
+ if err := os.MkdirAll(dir, constant.DefaultRootDirMode); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// ImageNames returns the images names of Saver
+func (s *Saver) ImageNames() []string {
+ var names = make([]string, 0, 2)
+ if !s.enabled {
+ return []string{}
+ }
+ if len(s.base) != 0 {
+ names = append(names, s.base)
+ }
+ if len(s.lib) != 0 {
+ names = append(names, s.lib)
+ }
+ return names
+}
+
+func (s *Saver) constructSingleImgInfo(mani imageManifest, store *store.Store) (imageInfo, error) {
+ var libLayers, appLayers []string
+ // image name should not be empty here
+ if len(mani.RepoTags) == 0 {
+ return imageInfo{}, errors.New("image name and tag is empty")
+ }
+ // if there is more than one repoTag, will use first one as image name
+ imageRepoFields := strings.Split(mani.RepoTags[0], ":")
+ imageLayers := getLayersID(mani.Layers)
+
+ libs, bases, err := s.checkLayersHash(mani.HashMap, store)
+ if err != nil {
+ return imageInfo{}, errors.Wrap(err, "compare layers failed")
+ }
+ baseLayers := imageLayers[0:len(bases)]
+ if len(libs) != 0 {
+ libLayers = imageLayers[len(bases):len(libs)]
+ appLayers = imageLayers[len(libs):]
+ } else {
+ libLayers = nil
+ appLayers = imageLayers[len(bases):]
+ }
+
+ return imageInfo{
+ config: mani.Config,
+ repoTags: mani.RepoTags,
+ nameTag: mani.RepoTags[0],
+ name: strings.Join(imageRepoFields[0:len(imageRepoFields)-1], ":"),
+ tag: imageRepoFields[len(imageRepoFields)-1],
+ layers: layer{app: appLayers, lib: libLayers, base: baseLayers, all: mani.Layers},
+ topLayer: imageLayers[len(imageLayers)-1],
+ }, nil
+}
+
+func (s *Saver) checkLayersHash(layerHashMap map[string]string, store *store.Store) ([]string, []string, error) {
+ libHash, err := s.getLayerHashFromStorage(store, s.lib)
+ if err != nil {
+ return nil, nil, errors.Wrapf(err, "get lib image %s layers failed", s.lib)
+ }
+ baseHash, err := s.getLayerHashFromStorage(store, s.base)
+ if err != nil {
+ return nil, nil, errors.Wrapf(err, "get base image %s layers failed", s.base)
+ }
+ if len(baseHash) > 1 {
+ return nil, nil, errors.Errorf("number of base layers %d more than one", len(baseHash))
+ }
+ if len(libHash) >= len(layerHashMap) || len(baseHash) >= len(layerHashMap) {
+ return nil, nil, errors.Errorf("number of base or lib layers is equal or greater than saved app layers")
+ }
+
+ for _, l := range libHash {
+ if _, ok := layerHashMap[l]; !ok {
+ return nil, nil, errors.Errorf("dismatch checksum for lib image %s", s.lib)
+ }
+ }
+ for _, b := range baseHash {
+ if _, ok := layerHashMap[b]; !ok {
+ return nil, nil, errors.Errorf("dismatch checksum for base image %s", s.base)
+ }
+ }
+
+ return libHash, baseHash, nil
+}
+
+func (s *Saver) constructImageInfos(manifest []imageManifest, store *store.Store) (map[string]imageInfo, error) {
+ s.log.Info("Constructing image info")
+
+ var imgInfos = make(map[string]imageInfo, 1)
+ for _, mani := range manifest {
+ imgInfo, err := s.constructSingleImgInfo(mani, store)
+ if err != nil {
+ s.log.Errorf("Constructing image info failed: %v", err)
+ return nil, errors.Wrap(err, "construct image info failed")
+ }
+ if _, ok := imgInfos[imgInfo.nameTag]; !ok {
+ imgInfos[imgInfo.nameTag] = imgInfo
+ }
+ }
+ return imgInfos, nil
+}
+
+// LoadRenameFile Saver tries to load the specified rename json
+func (s *Saver) LoadRenameFile() error {
+ if len(s.renameFile) == 0 {
+ return nil
+ }
+
+ var reName []renames
+ if err := util.LoadJSONFile(s.renameFile, &reName); err != nil {
+ return errors.Wrap(err, "check rename file failed")
+ }
+ s.renameData = reName
+ return nil
+}
+
+func (s *Saver) getRename(name string) string {
+ if len(s.renameData) == 0 {
+ return name
+ }
+
+ for _, item := range s.renameData {
+ if item.OriName == name {
+ s.log.Infof("Renaming image tarballs for %s to %s\n", name, item.NewName)
+ return item.NewName
+ }
+ }
+ return name
+}
+
+// Enabled returns whether separated-image feature is enabled
+func (s *Saver) Enabled() bool {
+ return s.enabled
+}
diff --git a/daemon/separator/utils.go b/daemon/separator/utils.go
new file mode 100644
index 0000000..fb05f58
--- /dev/null
+++ b/daemon/separator/utils.go
@@ -0,0 +1,78 @@
+// Copyright (c) Huawei Technologies Co., Ltd. 2020. All rights reserved.
+// isula-build licensed under the Mulan PSL v2.
+// You can use this software according to the terms and conditions of the Mulan PSL v2.
+// You may obtain a copy of Mulan PSL v2 at:
+// http://license.coscl.org.cn/MulanPSL2
+// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR
+// PURPOSE.
+// See the Mulan PSL v2 for more details.
+// Author: Xiang Li
+// Create: 2021-12-09
+// Description: This file is utils for image separator
+
+package separator
+
+import "strings"
+
+const (
+ manifestDataFile = "manifest.json"
+ manifestFile = "manifest"
+ repositoriesFile = "repositories"
+
+ tmpBaseDirName = "base"
+ tmpAppDirName = "app"
+ tmpLibDirName = "lib"
+ baseUntarTempDirName = "base_images"
+ appUntarTempDirName = "app_images"
+ libUntarTempDirName = "lib_images"
+ baseTarNameSuffix = "_base_image.tar.gz"
+ appTarNameSuffix = "_app_image.tar.gz"
+ libTarNameSuffix = "_lib_image.tar.gz"
+
+ unionTarName = "all.tar"
+ unionCompressedTarName = "all.tar.gz"
+
+ untarTempDirName = "untar"
+ layerTarName = "layer.tar"
+ tarSuffix = ".tar"
+)
+
+type tarballInfo struct {
+ AppTarName string `json:"app"`
+ AppHash string `json:"appHash"`
+ AppLayers []string `json:"appLayers"`
+ LibTarName string `json:"lib"`
+ LibHash string `json:"libHash"`
+ LibImageName string `json:"libImageName"`
+ LibLayers []string `json:"libLayers"`
+ BaseTarName string `json:"base"`
+ BaseHash string `json:"baseHash"`
+ BaseImageName string `json:"baseImageName"`
+ BaseLayer string `json:"baseLayer"`
+}
+
+func getLayerHashFromTar(layerMap map[string]string, layer []string) map[string]string {
+ hashMap := make(map[string]string, len(layer))
+ // first reverse map since it's <k-v> is unique
+ revMap := make(map[string]string, len(layerMap))
+ for k, v := range layerMap {
+ revMap[v] = k
+ }
+ for _, l := range layer {
+ if v, ok := revMap[l]; ok {
+ // format is like xxx(hash): xxx/layer.tar
+ hashMap[strings.TrimSuffix(v, tarSuffix)] = l
+ }
+ }
+
+ return hashMap
+}
+
+func getLayersID(layer []string) []string {
+ var after = make([]string, len(layer))
+ for i, v := range layer {
+ after[i] = strings.Split(v, "/")[0]
+ }
+ return after
+}
--
2.31.0.windows.1