From 933bc8a6589164f9bc25e9d2909bd76031c3fa6a Mon Sep 17 00:00:00 2001 From: fengxsong Date: Fri, 30 Jun 2023 16:59:25 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20save=20and=20load=20subcommand=20su?= =?UTF-8?q?pport=20multiple=20images=20in=20a=20single=20tar=E2=80=A6=20(#?= =?UTF-8?q?3442)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: save and load subcommand support multiple images in a single tar Signed-off-by: fengxsong * fix: golangci-lint Signed-off-by: fengxsong * fix(main): fix e2e error Signed-off-by: cuisongliu * fix(main): fix e2e error Signed-off-by: cuisongliu --------- Signed-off-by: fengxsong Signed-off-by: cuisongliu Co-authored-by: cuisongliu --- pkg/buildah/build.go | 4 +- pkg/buildah/constants.go | 6 +- pkg/buildah/imagesaver.go | 6 +- pkg/buildah/load.go | 106 +++++++++++++++++++------ pkg/buildah/merge.go | 2 +- pkg/buildah/save.go | 89 +++++++++++++++++---- test/e2e/images_test.go | 12 +++ test/e2e/run_other_test.go | 2 +- test/e2e/suites/operators/image.go | 4 + test/e2e/suites/operators/interface.go | 1 + test/e2e/testhelper/cmd/sealosCmd.go | 7 +- 11 files changed, 191 insertions(+), 48 deletions(-) diff --git a/pkg/buildah/build.go b/pkg/buildah/build.go index b0e071d84e3..d1731813e7d 100644 --- a/pkg/buildah/build.go +++ b/pkg/buildah/build.go @@ -42,7 +42,7 @@ func newBuildCommand() *cobra.Command { fromAndBudResults := buildahcli.FromAndBudResults{} userNSResults := buildahcli.UserNSResults{} namespaceResults := buildahcli.NameSpaceResults{} - sopts := saveOptions{} + sopts := saverOptions{} buildCommand := &cobra.Command{ Use: "build [CONTEXT]", @@ -90,7 +90,7 @@ func newBuildCommand() *cobra.Command { return buildCommand } -func buildCmd(c *cobra.Command, inputArgs []string, sopts saveOptions, iopts buildahcli.BuildOptions) error { +func buildCmd(c *cobra.Command, inputArgs []string, sopts saverOptions, iopts buildahcli.BuildOptions) error { if flagChanged(c, "logfile") { logfile, err := os.OpenFile(iopts.Logfile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600) if err != nil { diff --git a/pkg/buildah/constants.go b/pkg/buildah/constants.go index 4fe0362d102..c23d2f529ef 100644 --- a/pkg/buildah/constants.go +++ b/pkg/buildah/constants.go @@ -31,8 +31,10 @@ import ( ) const ( - OCIArchive string = "oci-archive" - DockerArchive string = "docker-archive" + OCIArchive string = "oci-archive" + OCIManifestDir string = "oci-dir" + DockerArchive string = "docker-archive" + DockerManifestDir string = "docker-dir" ) var DefaultTransport = OCIArchive diff --git a/pkg/buildah/imagesaver.go b/pkg/buildah/imagesaver.go index 2201c1d62b6..fbd192c6b83 100644 --- a/pkg/buildah/imagesaver.go +++ b/pkg/buildah/imagesaver.go @@ -35,17 +35,17 @@ import ( "github.com/labring/sealos/pkg/utils/logger" ) -type saveOptions struct { +type saverOptions struct { maxPullProcs int enabled bool } -func (opts *saveOptions) RegisterFlags(fs *pflag.FlagSet) { +func (opts *saverOptions) RegisterFlags(fs *pflag.FlagSet) { fs.IntVar(&opts.maxPullProcs, "max-pull-procs", 5, "maximum number of goroutines for pulling") fs.BoolVar(&opts.enabled, "save-image", true, "store images that parsed from the specific directories") } -func runSaveImages(contextDir string, platforms []v1.Platform, sys *types.SystemContext, opts *saveOptions) error { +func runSaveImages(contextDir string, platforms []v1.Platform, sys *types.SystemContext, opts *saverOptions) error { if !opts.enabled { logger.Warn("save-image is disabled, skip pulling images") return nil diff --git a/pkg/buildah/load.go b/pkg/buildah/load.go index c9dff8a5ac6..c2ce2d54d6e 100644 --- a/pkg/buildah/load.go +++ b/pkg/buildah/load.go @@ -12,44 +12,104 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Mostly copy from github.com/containers/podman + package buildah import ( + "errors" "fmt" - "runtime" + "io" + "os" + "strings" - "github.com/containers/buildah/pkg/parse" + "github.com/containers/common/libimage" + "github.com/containers/common/pkg/config" + "github.com/containers/common/pkg/download" "github.com/spf13/cobra" + "github.com/spf13/pflag" + "golang.org/x/term" ) +type loadOptions struct { + input string + quiet bool +} + +func (o *loadOptions) RegisterFlags(fs *pflag.FlagSet) { + fs.StringVarP(&o.input, "input", "i", "", "load images from specified tar archive file, default(stdin)") + fs.BoolVarP(&o.quiet, "quiet", "q", false, "suppress the output") +} + func newLoadCommand() *cobra.Command { - var ( - opts = newDefaultPullOptions() - archiveName string - transport string - ) + var opts = &loadOptions{} loadCommand := &cobra.Command{ Use: "load", - Short: "Load image from archive file", - RunE: func(cmd *cobra.Command, _ []string) error { - if err := ValidateTransport(transport); err != nil { - return err - } - return pullCmd(cmd, []string{fmt.Sprintf("%s:%s", transport, archiveName)}, opts) + Short: "Load image(s) from archive file", + RunE: func(cmd *cobra.Command, args []string) error { + return load(cmd, args, opts) }, Example: fmt.Sprintf(`%[1]s load -i kubernetes.tar`, rootCmd.CommandPath()), } loadCommand.SetUsageTemplate(UsageTemplate()) - fs := loadCommand.Flags() - fs.String("os", runtime.GOOS, "prefer `OS` instead of the running OS for choosing images") - fs.String("arch", runtime.GOARCH, "prefer `ARCH` instead of the architecture of the machine for choosing images") - fs.StringSlice("platform", []string{parse.DefaultPlatform()}, "prefer OS/ARCH instead of the current operating system and architecture for choosing images") - fs.String("variant", "", "override the `variant` of the specified image") - fs.StringVarP(&archiveName, "input", "i", "", "load image from tar archive file") - fs.StringVarP(&transport, "transport", "t", OCIArchive, - fmt.Sprintf("load image transport from tar archive file. (available options are %s, %s)", OCIArchive, DockerArchive)) - _ = markFlagsHidden(fs, flagsAssociatedWithPlatform()...) - _ = loadCommand.MarkFlagRequired("input") + opts.RegisterFlags(loadCommand.Flags()) + return loadCommand } + +func load(cmd *cobra.Command, _ []string, loadOpts *loadOptions) error { + if len(loadOpts.input) > 0 { + // Download the input file if needed. + if strings.HasPrefix(loadOpts.input, "https://") || strings.HasPrefix(loadOpts.input, "http://") { + containerConfig, err := config.Default() + if err != nil { + return err + } + tmpdir, err := containerConfig.ImageCopyTmpDir() + if err != nil { + return err + } + tmpfile, err := download.FromURL(tmpdir, loadOpts.input) + if err != nil { + return err + } + defer os.Remove(tmpfile) + loadOpts.input = tmpfile + } + + if _, err := os.Stat(loadOpts.input); err != nil { + return err + } + } else { + if term.IsTerminal(int(os.Stdin.Fd())) { + return errors.New("cannot read from terminal, use command-line redirection or the --input flag") + } + outFile, err := os.CreateTemp("", rootCmd.Name()) + if err != nil { + return fmt.Errorf("creating file %v", err) + } + defer os.Remove(outFile.Name()) + defer outFile.Close() + + _, err = io.Copy(outFile, os.Stdin) + if err != nil { + return fmt.Errorf("copying file %v", err) + } + loadOpts.input = outFile.Name() + } + r, err := getRuntime(cmd) + if err != nil { + return err + } + loadOptions := &libimage.LoadOptions{} + if !loadOpts.quiet { + loadOptions.Writer = os.Stderr + } + loadedImages, err := r.Load(getContext(), loadOpts.input, loadOptions) + if err != nil { + return err + } + fmt.Println("Loaded image: " + strings.Join(loadedImages, "\nLoaded image: ")) + return nil +} diff --git a/pkg/buildah/merge.go b/pkg/buildah/merge.go index 09fce7170e6..7f4930468ca 100644 --- a/pkg/buildah/merge.go +++ b/pkg/buildah/merge.go @@ -43,7 +43,7 @@ func newMergeCommand() *cobra.Command { userNSResults := buildahcli.UserNSResults{} namespaceResults := buildahcli.NameSpaceResults{} buildahInfo := &buildah.BuilderInfo{} - sopts := saveOptions{} + sopts := saverOptions{} mergeCommand := &cobra.Command{ Use: "merge", Short: "merge multiple images into one", diff --git a/pkg/buildah/save.go b/pkg/buildah/save.go index 8fa31fadf10..4fefda496d2 100644 --- a/pkg/buildah/save.go +++ b/pkg/buildah/save.go @@ -12,39 +12,98 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Mostly copy from github.com/containers/podman + package buildah import ( + "errors" "fmt" + "os" + "strings" + "github.com/containers/common/libimage" "github.com/spf13/cobra" + "github.com/spf13/pflag" ) +type saveOptions struct { + compress bool + quiet bool + multiImageArchive bool + ociAcceptUncompressedLayers bool + format string + output string +} + +func (o *saveOptions) RegisterFlags(fs *pflag.FlagSet) { + fs.BoolVar(&o.compress, "compress", false, "compress tarball image layers when saving to a directory using the 'dir' transport. (default is same compression type as source)") + fs.BoolVarP(&o.quiet, "quiet", "q", false, "suppress the output") + fs.BoolVarP(&o.multiImageArchive, "multi-image-archive", "m", false, "interpret additional arguments as images not tags and create a multi-image-archive (only for docker-archive)") + fs.BoolVar(&o.ociAcceptUncompressedLayers, "uncompressed", false, "Accept uncompressed layers when copying OCI images") + fs.StringVar(&o.format, "format", OCIArchive, "save image to oci-archive, oci-dir (directory with oci manifest type), "+ + "docker-archive, docker-dir (directory with v2s2 manifest type)") + fs.StringVarP(&o.output, "output", "o", "", "write to a specified file (default: stdout, which must be redirected)") +} + +func (o *saveOptions) Validate() error { + if strings.Contains(o.output, ":") { + return fmt.Errorf("invalid filename (should not contain ':') %q", o.output) + } + return nil +} + func newSaveCommand() *cobra.Command { - var ( - opts = newDefaultPushOptions() - archiveName string - transport string - ) + var opts = &saveOptions{} saveCommand := &cobra.Command{ Use: "save", Short: "Save image into archive file", + Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - if err := ValidateTransport(transport); err != nil { - return err - } - return pushCmd(cmd, []string{ - args[0], - fmt.Sprintf("%s:%s:%s", transport, archiveName, args[0]), - }, opts) + return runSave(cmd, args, opts) }, Example: fmt.Sprintf(`%[1]s save -o kubernetes.tar labring/kubernetes:latest`, rootCmd.CommandPath()), } saveCommand.SetUsageTemplate(UsageTemplate()) - saveCommand.Flags().StringVarP(&archiveName, "output", "o", "", "save image into tar archive file") + opts.RegisterFlags(saveCommand.Flags()) _ = saveCommand.MarkFlagRequired("output") - saveCommand.Flags().StringVarP(&transport, "transport", "t", OCIArchive, - fmt.Sprintf("save image transport to tar archive file. (available options are %s, %s)", OCIArchive, DockerArchive)) + return saveCommand } + +func runSave(cmd *cobra.Command, args []string, saveOpts *saveOptions) error { + var ( + tags []string + ) + if flagChanged(cmd, "compress") && saveOpts.format != DockerManifestDir { + return errors.New("--compress can only be set when --format is 'docker-dir'") + } + + if err := saveOpts.Validate(); err != nil { + return err + } + if len(args) > 1 { + tags = args[1:] + } + + r, err := getRuntime(cmd) + if err != nil { + return err + } + saveOptions := &libimage.SaveOptions{} + saveOptions.DirForceCompress = saveOpts.compress + saveOptions.OciAcceptUncompressedLayers = saveOpts.ociAcceptUncompressedLayers + + if !saveOpts.quiet { + saveOptions.Writer = os.Stderr + } + + names := []string{args[0]} + if saveOpts.multiImageArchive { + names = append(names, tags...) + } else { + saveOptions.AdditionalTags = tags + } + return r.Save(getContext(), names, saveOpts.format, saveOpts.output, saveOptions) +} diff --git a/test/e2e/images_test.go b/test/e2e/images_test.go index 4547186d936..8ab70ee2c10 100644 --- a/test/e2e/images_test.go +++ b/test/e2e/images_test.go @@ -69,6 +69,18 @@ var _ = Describe("E2E_sealos_images_test", func() { err = fakeClient.Image.LoadImage("k8s.tar") utils.CheckErr(err, fmt.Sprintf("failed to load image k8s.tar: %v", err)) }) + + It("images SaveMultiImage", func() { + err = fakeClient.Image.PullImage("docker.io/labring/kubernetes:v1.20.1", "labring/helm:v3.8.2") + utils.CheckErr(err, fmt.Sprintf("failed to pull images: %v", err)) + err = fakeClient.Image.SaveMultiImage("k8s-multi.tar", "docker.io/labring/kubernetes:v1.20.1", "labring/helm:v3.8.2") + utils.CheckErr(err, fmt.Sprintf("failed to SaveMultiImage : %v", err)) + }) + It("images load multi image", func() { + err = fakeClient.Image.LoadImage("k8s-multi.tar") + utils.CheckErr(err, fmt.Sprintf("failed to load multi image k8s.tar: %v", err)) + }) + It("images merge image", func() { err = fakeClient.Image.Merge("new:0.1.0", []string{"docker.io/labring/kubernetes:v1.20.1", "labring/helm:v3.8.2"}) utils.CheckErr(err, fmt.Sprintf("failed to merge image new:0.1.0: %v", err)) diff --git a/test/e2e/run_other_test.go b/test/e2e/run_other_test.go index 4c51a2acba5..a9ee6d4f9a0 100644 --- a/test/e2e/run_other_test.go +++ b/test/e2e/run_other_test.go @@ -50,7 +50,7 @@ var _ = Describe("E2E_sealos_run_other_test", func() { utils.CheckErr(err, fmt.Sprintf("failed to Run new cluster for single using tar: %v", err)) err = fakeClient.Cluster.Run("labring/helm:v3.8.2") utils.CheckErr(err, fmt.Sprintf("failed to running image for helm: %v", err)) - newImages := []string{"localhost/labring/kubernetes:v1.25.0", "labring/helm:v3.8.2"} + newImages := []string{"docker.io/labring/kubernetes:v1.25.0", "labring/helm:v3.8.2"} fakeCheckInterface, err = checkers.NewFakeGroupClient("default", &checkers.FakeOpts{Images: newImages}) utils.CheckErr(err, fmt.Sprintf("failed to get cluster interface: %v", err)) err = fakeCheckInterface.Verify() diff --git a/test/e2e/suites/operators/image.go b/test/e2e/suites/operators/image.go index 44ce80ad001..bd5829d3b18 100644 --- a/test/e2e/suites/operators/image.go +++ b/test/e2e/suites/operators/image.go @@ -61,6 +61,10 @@ func (f *fakeImageClient) SaveImage(name, file string) error { return f.SealosCmd.ImageSave(name, file, "") } +func (f *fakeImageClient) SaveMultiImage(file string, name ...string) error { + return f.SealosCmd.ImageMultiSave(file, name...) +} + func (f *fakeImageClient) LoadImage(file string) error { return f.SealosCmd.ImageLoad(file) } diff --git a/test/e2e/suites/operators/interface.go b/test/e2e/suites/operators/interface.go index 6ef7accdd13..f6ee1ac70c8 100644 --- a/test/e2e/suites/operators/interface.go +++ b/test/e2e/suites/operators/interface.go @@ -41,6 +41,7 @@ type FakeImageInterface interface { DockerArchiveImage(name string) error OCIArchiveImage(name string) error SaveImage(name, file string) error + SaveMultiImage(file string, name ...string) error TagImage(name, newName string) error LoadImage(file string) error Create(name string, short bool) ([]byte, error) diff --git a/test/e2e/testhelper/cmd/sealosCmd.go b/test/e2e/testhelper/cmd/sealosCmd.go index e463a3803f6..4e67103f2c1 100644 --- a/test/e2e/testhelper/cmd/sealosCmd.go +++ b/test/e2e/testhelper/cmd/sealosCmd.go @@ -151,7 +151,12 @@ func (s *SealosCmd) ImageSave(image string, path string, archive string) error { if archive == "" { return s.Executor.AsyncExec(s.BinPath, "save", "-o", path, image) } - return s.Executor.AsyncExec(s.BinPath, "save", "-o", path, "-t", archive, image) + return s.Executor.AsyncExec(s.BinPath, "save", "-o", path, "--format", archive, image) +} + +func (s *SealosCmd) ImageMultiSave(path string, name ...string) error { + param := append([]string{"save", "-m", "--format", "docker-archive", "-o", path}, name...) + return s.Executor.AsyncExec(s.BinPath, param...) } func (s *SealosCmd) ImageLoad(path string) error {