From 6545a2222419954045cf4b80cc9f03f918e568af Mon Sep 17 00:00:00 2001 From: DCCooper <1866858@gmail.com> Date: Tue, 26 Oct 2021 14:21:02 +0800 Subject: [PATCH] daemon:finish daemon load separated image reason: support isula-build daemon side load separated image ABI change(daemon): none Load process changes: 1. construct image tarball at the beginning of load process - input: separated images - output: none - addition: new images in local storages Signed-off-by: DCCooper <1866858@gmail.com> Signed-off-by: lixiang --- daemon/load.go | 306 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 294 insertions(+), 12 deletions(-) diff --git a/daemon/load.go b/daemon/load.go index 2fb8e27d..41690abc 100644 --- a/daemon/load.go +++ b/daemon/load.go @@ -14,11 +14,16 @@ package daemon import ( + "io/ioutil" + "os" + "path/filepath" + "github.com/containers/image/v5/docker/tarfile" ociarchive "github.com/containers/image/v5/oci/archive" "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" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" @@ -33,30 +38,108 @@ import ( "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 { - path string - format string + path string + format string + logEntry *logrus.Entry + sep separatorLoad } -func (b *Backend) getLoadOptions(req *pb.LoadRequest) loadOptions { - return loadOptions{ +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()}), } + + // normal loadOptions + if !opt.sep.enabled { + 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) + if err != nil { + return loadOptions{}, err + } + opt.sep.appName = appImgName + + return opt, nil } // Load loads the image func (b *Backend) Load(req *pb.LoadRequest, stream pb.Control_LoadServer) error { - logrus.Info("LoadRequest received") + logrus.WithFields(logrus.Fields{ + "LoadID": req.GetLoadID(), + }).Info("LoadRequest received") var ( si *storage.Image repoTags [][]string - err error ) - opts := b.getLoadOptions(req) + opts, err := b.getLoadOptions(req) + if err != nil { + return errors.Wrap(err, "process load options failed") + } + + 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 cErr := util.CheckLoadFile(req.Path); cErr != nil { - return cErr + // 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) + return lErr + } } repoTags, err = tryToParseImageFormatFromTarball(b.daemon.opts.DataRoot, &opts) @@ -149,8 +232,13 @@ func getDockerRepoTagFromImageTar(systemContext *types.SystemContext, path strin // tmp dir will be removed after NewSourceFromFileWithContext tarfileSource, err := tarfile.NewSourceFromFileWithContext(systemContext, path) if err != nil { - return nil, errors.Wrapf(err, "failed to get the source of loading tar file") + return nil, errors.Wrap(err, "failed to get the source of loading tar file") } + defer func() { + if cErr := tarfileSource.Close(); cErr != nil { + logrus.Warnf("tar file source close failed: %v", cErr) + } + }() topLevelImageManifest, err := tarfileSource.LoadTarManifest() if err != nil || len(topLevelImageManifest) == 0 { @@ -172,12 +260,12 @@ func getOCIRepoTagFromImageTar(systemContext *types.SystemContext, path string) srcRef, err := alltransports.ParseImageName(exporter.FormatTransport(constant.OCIArchiveTransport, path)) if err != nil { - return nil, errors.Wrapf(err, "failed to parse image name of oci image format") + return nil, errors.Wrap(err, "failed to parse image name of oci image format") } tarManifest, err := ociarchive.LoadManifestDescriptorWithContext(systemContext, srcRef) if err != nil { - return nil, errors.Wrapf(err, "failed to load manifest descriptor of oci image format") + return nil, errors.Wrap(err, "failed to load manifest descriptor of oci image format") } // For now, we only support load single image in archive file @@ -187,3 +275,197 @@ func getOCIRepoTagFromImageTar(systemContext *types.SystemContext, path string) return [][]string{{}}, 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) + 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 + } + + // app image tarball can not be empty + if len(s.appPath) == 0 { + return errors.New("app image tarball path can not be empty") + } + if err := util.CheckSum(s.appPath, s.info.AppHash); err != nil { + return errors.Wrapf(err, "check sum for file %q failed", s.appPath) + } + + // base image tarball can not be empty + if len(s.basePath) == 0 { + return errors.New("base image tarball path can not be empty") + } + if err := util.CheckSum(s.basePath, s.info.BaseHash); err != nil { + return errors.Wrapf(err, "check sum for file %q failed", s.basePath) + } + + // lib image may be empty image + if len(s.libPath) != 0 { + if err := util.CheckSum(s.libPath, s.info.LibHash); err != nil { + return errors.Wrapf(err, "check sum for file %q failed", s.libPath) + } + } + + 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") + } + + // unpack base first and the later images will be moved here + if err := util.UnpackFile(s.basePath, s.tmpDir.base, archive.Gzip, false); err != nil { + return errors.Wrapf(err, "unpack base tarball %q failed", s.basePath) + } + + if err := util.UnpackFile(s.appPath, s.tmpDir.app, archive.Gzip, false); err != nil { + return errors.Wrapf(err, "unpack app tarball %q failed", s.appPath) + } + + if len(s.libPath) != 0 { + if err := util.UnpackFile(s.libPath, s.tmpDir.lib, archive.Gzip, false); err != nil { + return errors.Wrapf(err, "unpack lib tarball %q failed", s.libPath) + } + } + + 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 +} -- 2.27.0