From 5749a92be53a3e8a135b4f7e59e8fd6d470fbd55 Mon Sep 17 00:00:00 2001
From: DCCooper <1866858@gmail.com>
Date: Tue, 26 Oct 2021 14:20:07 +0800
Subject: [PATCH 05/16] cli:finish client load separated image

reason: support isula-build client side process info for load separated
image
ABI change:(client)
- --input: name of app images when load separated images
- --dir: path to separated images' tarball directory
- --base: base image tarball path of separated images
- --lib: lib image tarball path of separated images
- --no-check: skip sha256 check sum for legacy separated images loading

Signed-off-by: DCCooper <1866858@gmail.com>
---
 cmd/cli/load.go      | 113 ++++++++++++++++++++++++++++++++++++++++---
 cmd/cli/load_test.go |  50 +++++++++++++++++--
 cmd/cli/mock.go      |  10 ++++
 3 files changed, 160 insertions(+), 13 deletions(-)

diff --git a/cmd/cli/load.go b/cmd/cli/load.go
index 16e90a26..2a9df772 100644
--- a/cmd/cli/load.go
+++ b/cmd/cli/load.go
@@ -25,18 +25,32 @@ import (
 	"github.com/pkg/errors"
 	"github.com/spf13/cobra"
 
+	constant "isula.org/isula-build"
 	pb "isula.org/isula-build/api/services"
 	"isula.org/isula-build/util"
 )
 
+type separatorLoadOption struct {
+	app       string
+	base      string
+	lib       string
+	dir       string
+	skipCheck bool
+	enabled   bool
+}
+
 type loadOptions struct {
-	path string
+	path   string
+	loadID string
+	sep    separatorLoadOption
 }
 
 var loadOpts loadOptions
 
 const (
-	loadExample = `isula-build ctr-img load -i busybox.tar`
+	loadExample = `isula-build ctr-img load -i busybox.tar
+isula-build ctr-img load -i app:latest -d /home/Images
+isula-build ctr-img load -i app:latest -d /home/Images -b /home/Images/base.tar.gz -l /home/Images/lib.tar.gz`
 )
 
 // NewLoadCmd returns image load command
@@ -49,12 +63,20 @@ func NewLoadCmd() *cobra.Command {
 		RunE:    loadCommand,
 	}
 
-	loadCmd.PersistentFlags().StringVarP(&loadOpts.path, "input", "i", "", "Path to local tarball")
+	loadCmd.PersistentFlags().StringVarP(&loadOpts.path, "input", "i", "", "Path to local tarball(or app image name when load separated images)")
+	loadCmd.PersistentFlags().StringVarP(&loadOpts.sep.dir, "dir", "d", "", "Path to separated image tarballs directory")
+	loadCmd.PersistentFlags().StringVarP(&loadOpts.sep.base, "base", "b", "", "Base image tarball path of separated images")
+	loadCmd.PersistentFlags().StringVarP(&loadOpts.sep.lib, "lib", "l", "", "Library image tarball path of separated images")
+	loadCmd.PersistentFlags().BoolVarP(&loadOpts.sep.skipCheck, "no-check", "", false, "Skip sha256 check sum for legacy separated images loading")
 
 	return loadCmd
 }
 
 func loadCommand(cmd *cobra.Command, args []string) error {
+	if err := loadOpts.checkLoadOpts(); err != nil {
+		return errors.Wrapf(err, "check load options failed")
+	}
+
 	ctx := context.Background()
 	cli, err := NewClient(ctx)
 	if err != nil {
@@ -65,14 +87,20 @@ func loadCommand(cmd *cobra.Command, args []string) error {
 }
 
 func runLoad(ctx context.Context, cli Cli) error {
-	var err error
-
-	if loadOpts.path, err = resolveLoadPath(loadOpts.path); err != nil {
-		return err
+	loadOpts.loadID = util.GenerateNonCryptoID()[:constant.DefaultIDLen]
+	sep := &pb.SeparatorLoad{
+		App:       loadOpts.sep.app,
+		Dir:       loadOpts.sep.dir,
+		Base:      loadOpts.sep.base,
+		Lib:       loadOpts.sep.lib,
+		SkipCheck: loadOpts.sep.skipCheck,
+		Enabled:   loadOpts.sep.enabled,
 	}
 
 	resp, err := cli.Client().Load(ctx, &pb.LoadRequest{
-		Path: loadOpts.path,
+		Path:   loadOpts.path,
+		LoadID: loadOpts.loadID,
+		Sep:    sep,
 	})
 	if err != nil {
 		return err
@@ -114,3 +142,72 @@ func resolveLoadPath(path string) (string, error) {
 
 	return path, nil
 }
+
+func (opt *loadOptions) checkLoadOpts() error {
+	// normal load
+	if !opt.sep.isEnabled() {
+		path, err := resolveLoadPath(opt.path)
+		if err != nil {
+			return err
+		}
+		opt.path = path
+
+		return nil
+	}
+
+	// load separated image
+	opt.sep.enabled = true
+	if len(opt.path) == 0 {
+		return errors.New("app image should not be empty")
+	}
+
+	// Use opt.path as app image name when operating separated images
+	// this can be mark as a switch for handling separated images
+	opt.sep.app = opt.path
+
+	if err := opt.sep.check(); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (sep *separatorLoadOption) isEnabled() bool {
+	return util.AnyFlagSet(sep.dir, sep.base, sep.lib, sep.app)
+}
+
+func (sep *separatorLoadOption) check() error {
+	pwd, err := os.Getwd()
+	if err != nil {
+		return errors.New("get current path failed")
+	}
+	if !util.IsValidImageName(sep.app) {
+		return errors.Errorf("invalid image name: %s", sep.app)
+	}
+
+	if len(sep.base) != 0 {
+		path, err := resolveLoadPath(sep.base)
+		if err != nil {
+			return errors.Wrap(err, "resolve base tarball path failed")
+		}
+		sep.base = path
+	}
+	if len(sep.lib) != 0 {
+		path, err := resolveLoadPath(sep.lib)
+		if err != nil {
+			return errors.Wrap(err, "resolve lib tarball path failed")
+		}
+		sep.lib = path
+	}
+	if len(sep.dir) == 0 {
+		return errors.New("image tarball directory should not be empty")
+	}
+	if !filepath.IsAbs(sep.dir) {
+		sep.dir = util.MakeAbsolute(sep.dir, pwd)
+	}
+	if !util.IsExist(sep.dir) {
+		return errors.Errorf("image tarball directory %s is not exist", sep.dir)
+	}
+
+	return nil
+}
diff --git a/cmd/cli/load_test.go b/cmd/cli/load_test.go
index 9c753e23..b7bf2a57 100644
--- a/cmd/cli/load_test.go
+++ b/cmd/cli/load_test.go
@@ -15,19 +15,59 @@ package main
 
 import (
 	"context"
+	"io/ioutil"
 	"path/filepath"
 	"testing"
 
 	"gotest.tools/v3/assert"
 	"gotest.tools/v3/fs"
+	constant "isula.org/isula-build"
 )
 
 func TestLoadCmd(t *testing.T) {
-	cmd := NewLoadCmd()
-	err := cmd.Execute()
-	assert.Equal(t, err != nil, true)
-	err = loadCommand(cmd, nil)
-	assert.ErrorContains(t, err, "isula_build")
+	tmpDir := fs.NewFile(t, t.Name())
+	err := ioutil.WriteFile(tmpDir.Path(), []byte("This is test file"), constant.DefaultSharedFileMode)
+	assert.NilError(t, err)
+	defer tmpDir.Remove()
+
+	type testcase struct {
+		name      string
+		path      string
+		errString string
+		args      []string
+		wantErr   bool
+		sep       separatorLoadOption
+	}
+	// For normal cases, default err is "invalid socket path: unix:///var/run/isula_build.sock".
+	// As daemon is not running as we run unit test.
+	var testcases = []testcase{
+		{
+			name:      "TC1 - normal case",
+			path:      tmpDir.Path(),
+			errString: "isula_build.sock",
+			wantErr:   true,
+		},
+	}
+
+	for _, tc := range testcases {
+		t.Run(tc.name, func(t *testing.T) {
+			loadCmd := NewLoadCmd()
+			loadOpts = loadOptions{
+				path: tc.path,
+				sep:  tc.sep,
+			}
+			err := loadCmd.Execute()
+			assert.Equal(t, err != nil, true)
+
+			err = loadCommand(loadCmd, tc.args)
+			if tc.wantErr {
+				assert.ErrorContains(t, err, tc.errString)
+			}
+			if !tc.wantErr {
+				assert.NilError(t, err)
+			}
+		})
+	}
 }
 
 func TestRunLoad(t *testing.T) {
diff --git a/cmd/cli/mock.go b/cmd/cli/mock.go
index 2ae07d56..142c87fa 100644
--- a/cmd/cli/mock.go
+++ b/cmd/cli/mock.go
@@ -318,6 +318,16 @@ func (f *mockDaemon) importImage(_ context.Context, opts ...grpc.CallOption) (pb
 
 func (f *mockDaemon) load(_ context.Context, in *pb.LoadRequest, opts ...grpc.CallOption) (pb.Control_LoadClient, error) {
 	f.loadReq = in
+	path := f.loadReq.Path
+	sep := f.loadReq.Sep
+	if !sep.Enabled {
+		if path == "" {
+			return &mockLoadClient{}, errors.Errorf("tarball path should not be empty")
+		}
+		_, err := resolveLoadPath(path)
+		return &mockLoadClient{}, err
+	}
+
 	return &mockLoadClient{}, nil
 }
 
-- 
2.27.0

