From 8bb2cb6f3904f13d0010cc207e9b00bafe043805 Mon Sep 17 00:00:00 2001 From: DCCooper <1866858@gmail.com> Date: Tue, 26 Oct 2021 14:19:10 +0800 Subject: [PATCH 02/16] cli:finish client save separated image reason: support isula-build client side process info for save separated image ABI change:(client) - --dest: destination file direcotry to store seprated image - --base: base image name of separated images - --lib: lib image name of separated images - --rename: rename json file path of separated images Signed-off-by: DCCooper <1866858@gmail.com> --- cmd/cli/save.go | 121 ++++++++++++++++++++++++++++++++++++++---------- util/common.go | 24 ++++++++++ 2 files changed, 121 insertions(+), 24 deletions(-) diff --git a/cmd/cli/save.go b/cmd/cli/save.go index cb78ecfb..4d22798a 100644 --- a/cmd/cli/save.go +++ b/cmd/cli/save.go @@ -29,8 +29,17 @@ import ( "isula.org/isula-build/util" ) +type separatorSaveOption struct { + baseImgName string + libImageName string + renameFile string + destPath string + enabled bool +} + type saveOptions struct { images []string + sep separatorSaveOption path string saveID string format string @@ -41,7 +50,9 @@ var saveOpts saveOptions const ( saveExample = `isula-build ctr-img save busybox:latest -o busybox.tar isula-build ctr-img save 21c3e96ac411 -o myimage.tar -isula-build ctr-img save busybox:latest alpine:3.9 -o all.tar` +isula-build ctr-img save busybox:latest alpine:3.9 -o all.tar +isula-build ctr-img save app:latest app1:latest -d Images +isula-build ctr-img save app:latest app1:latest -d Images -b busybox:latest -l lib:latest -r rename.json` ) // NewSaveCmd cmd for container image saving @@ -54,6 +65,10 @@ func NewSaveCmd() *cobra.Command { } saveCmd.PersistentFlags().StringVarP(&saveOpts.path, "output", "o", "", "Path to save the tarball") + saveCmd.PersistentFlags().StringVarP(&saveOpts.sep.destPath, "dest", "d", "Images", "Destination file directory to store separated images") + saveCmd.PersistentFlags().StringVarP(&saveOpts.sep.baseImgName, "base", "b", "", "Base image name of separated images") + saveCmd.PersistentFlags().StringVarP(&saveOpts.sep.libImageName, "lib", "l", "", "Lib image name of separated images") + saveCmd.PersistentFlags().StringVarP(&saveOpts.sep.renameFile, "rename", "r", "", "Rename json file path of separated images") if util.CheckCliExperimentalEnabled() { saveCmd.PersistentFlags().StringVarP(&saveOpts.format, "format", "f", "oci", "Format of image saving to local tarball") } else { @@ -67,16 +82,7 @@ func saveCommand(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - if len(args) == 0 { - return errors.New("save accepts at least one image") - } - if saveOpts.format == constant.OCITransport && len(args) >= 2 { - return errors.New("oci image format now only supports saving single image") - } - if err := util.CheckImageFormat(saveOpts.format); err != nil { - return err - } - if err := checkSavePath(); err != nil { + if err := saveOpts.checkSaveOpts(args); err != nil { return err } @@ -88,25 +94,79 @@ func saveCommand(cmd *cobra.Command, args []string) error { return runSave(ctx, cli, args) } -func checkSavePath() error { - if len(saveOpts.path) == 0 { - return errors.New("output path should not be empty") +func (sep *separatorSaveOption) check(pwd string) error { + if len(sep.baseImgName) != 0 { + if !util.IsValidImageName(sep.baseImgName) { + return errors.Errorf("invalid base image name %s", sep.baseImgName) + } } + if len(sep.libImageName) != 0 { + if !util.IsValidImageName(sep.libImageName) { + return errors.Errorf("invalid lib image name %s", sep.libImageName) + } + } + if len(sep.destPath) == 0 { + sep.destPath = "Images" + } + if !filepath.IsAbs(sep.destPath) { + sep.destPath = util.MakeAbsolute(sep.destPath, pwd) + } + if util.IsExist(sep.destPath) { + return errors.Errorf("output file already exist: %q, try to remove existing tarball or rename output file", sep.destPath) + } + if len(sep.renameFile) != 0 { + if !filepath.IsAbs(sep.renameFile) { + sep.renameFile = util.MakeAbsolute(sep.renameFile, pwd) + } + } + + return nil +} - if strings.Contains(saveOpts.path, ":") { - return errors.Errorf("colon in path %q is not supported", saveOpts.path) +func (opt *saveOptions) checkSaveOpts(args []string) error { + if len(args) == 0 { + return errors.New("save accepts at least one image") } - if !filepath.IsAbs(saveOpts.path) { - pwd, err := os.Getwd() - if err != nil { - return errors.New("get current path failed") + if strings.Contains(opt.path, ":") || strings.Contains(opt.sep.destPath, ":") { + return errors.Errorf("colon in path %q is not supported", opt.path) + } + pwd, err := os.Getwd() + if err != nil { + return errors.New("get current path failed") + } + + // normal save + if !opt.sep.isEnabled() { + // only check oci format when doing normal save operation + if opt.format == constant.OCITransport && len(args) >= 2 { + return errors.New("oci image format now only supports saving single image") + } + if err := util.CheckImageFormat(opt.format); err != nil { + return err + } + if len(opt.path) == 0 { + return errors.New("output path should not be empty") } - saveOpts.path = util.MakeAbsolute(saveOpts.path, pwd) + if !filepath.IsAbs(opt.path) { + opt.path = util.MakeAbsolute(opt.path, pwd) + } + if util.IsExist(opt.path) { + return errors.Errorf("output file already exist: %q, try to remove existing tarball or rename output file", opt.path) + } + return nil } - if util.IsExist(saveOpts.path) { - return errors.Errorf("output file already exist: %q, try to remove existing tarball or rename output file", saveOpts.path) + // separator save + opt.sep.enabled = true + if len(opt.path) != 0 { + return errors.New("conflict options between -o and [-b -l -r]") + } + // separate image only support docker image spec + opt.format = constant.DockerTransport + + if err := opt.sep.check(pwd); err != nil { + return err } return nil @@ -116,11 +176,20 @@ func runSave(ctx context.Context, cli Cli, args []string) error { saveOpts.saveID = util.GenerateNonCryptoID()[:constant.DefaultIDLen] saveOpts.images = args + sep := &pb.SeparatorSave{ + Base: saveOpts.sep.baseImgName, + Lib: saveOpts.sep.libImageName, + Rename: saveOpts.sep.renameFile, + Dest: saveOpts.sep.destPath, + Enabled: saveOpts.sep.enabled, + } + saveStream, err := cli.Client().Save(ctx, &pb.SaveRequest{ Images: saveOpts.images, Path: saveOpts.path, SaveID: saveOpts.saveID, Format: saveOpts.format, + Sep: sep, }) if err != nil { return err @@ -137,7 +206,11 @@ func runSave(ctx context.Context, cli Cli, args []string) error { fmt.Printf("Save success with image: %s\n", saveOpts.images) return nil } - return errors.Errorf("save image failed: %v", err) + return errors.Errorf("save image failed: %v", err.Error()) } } } + +func (sep *separatorSaveOption) isEnabled() bool { + return util.AnyFlagSet(sep.baseImgName, sep.libImageName, sep.renameFile) +} diff --git a/util/common.go b/util/common.go index 00b1b941..4782b2ec 100644 --- a/util/common.go +++ b/util/common.go @@ -21,6 +21,7 @@ import ( "strings" securejoin "github.com/cyphar/filepath-securejoin" + "github.com/docker/distribution/reference" "github.com/pkg/errors" "github.com/spf13/cobra" "golang.org/x/sys/unix" @@ -184,3 +185,26 @@ func FormatSize(size, base float64) string { func CheckCliExperimentalEnabled() bool { return os.Getenv("ISULABUILD_CLI_EXPERIMENTAL") == "enabled" } + +// IsValidImageName will check the validity of image name +func IsValidImageName(name string) bool { + ref, err := reference.ParseNormalizedNamed(name) + if err != nil { + return false + } + if _, canonical := ref.(reference.Canonical); canonical { + return false + } + return true +} + +// AnyFlagSet is a checker to indicate there exist flag's length not empty +// If all flags are empty, will return false +func AnyFlagSet(flags ...string) bool { + for _, flag := range flags { + if len(flag) != 0 { + return true + } + } + return false +} -- 2.27.0