diff --git a/pkg/cmd/mod/mod.go b/pkg/cmd/mod/mod.go index 3bdccf12..5009a2eb 100644 --- a/pkg/cmd/mod/mod.go +++ b/pkg/cmd/mod/mod.go @@ -24,7 +24,7 @@ func NewCmdMod(streams genericclioptions.IOStreams) *cobra.Command { } // add subcommands - cmd.AddCommand(NewCmdInit(streams)) + cmd.AddCommand(NewCmdInit()) cmd.AddCommand(NewCmdPush(streams)) return cmd diff --git a/pkg/cmd/mod/mod_init.go b/pkg/cmd/mod/mod_init.go index 83b62939..717511d7 100644 --- a/pkg/cmd/mod/mod_init.go +++ b/pkg/cmd/mod/mod_init.go @@ -1,28 +1,109 @@ package mod import ( + "fmt" + "os" + "path" + + "github.com/go-git/go-git/v5/plumbing" + "github.com/pulumi/pulumi/sdk/v3/go/common/util/gitutil" "github.com/spf13/cobra" - "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/kubectl/pkg/util/templates" + + "kusionstack.io/kusion/pkg/cmd/util" + "kusionstack.io/kusion/pkg/util/i18n" ) -type InitModOptions struct{} +type InitOptions struct { + Name string + Path string + TemplateURL string +} + +var example = i18n.T(`# Create a kusion module template in the current directory + kusion mod init my-app + + # Init a kusion module at the specified Path + kusion mod init my-app ./modules + + # Init a module from a remote git template repository + kusion mod init my-app --template https://github.com//`) +var short = i18n.T("Create a kusion module along with common files and directories in the current directory") -var ( - initLong = `` - initExample = `` +const ( + defaultTemplateURL = "https://github.com/KusionStack/kusion-module-scaffolding.git" + defaultBranch = "main" ) // NewCmdInit returns an initialized Command instance for the 'mod init' sub command -func NewCmdInit(streams genericclioptions.IOStreams) *cobra.Command { +func NewCmdInit() *cobra.Command { + o := &InitOptions{} + cmd := &cobra.Command{ - Use: "", - DisableFlagsInUseLine: true, - Short: "", - Long: initLong, - Example: initExample, - Run: func(cmd *cobra.Command, args []string) { + Use: "init [MODULE NAME] [PATH]", + Short: short, + Example: templates.Examples(example), + RunE: func(cmd *cobra.Command, args []string) (err error) { + defer util.RecoverErr(&err) + util.CheckErr(o.Validate(args)) + util.CheckErr(o.Run()) + return }, } + cmd.Flags().StringVar(&o.TemplateURL, "template", "", i18n.T("Initialize with specified template")) return cmd } + +func (o *InitOptions) Validate(args []string) error { + // get the module Name + if len(args) < 1 { + return fmt.Errorf("module Name is empty") + } + o.Name = args[0] + + // get the Path + if len(args) == 2 { + o.Path = args[1] + } else { + // default to the current directory + o.Path, _ = os.Getwd() + } + + // create the module directory if not exists + fs, err := os.Stat(o.Path) + if err != nil { + return fmt.Errorf("failed to create module directory: %w", err) + } else if !fs.IsDir() { + return fmt.Errorf("path is not a directory") + } else if os.IsNotExist(err) { + if err = os.MkdirAll(o.Path, os.ModePerm); err != nil { + return fmt.Errorf("failed to create module directory: %v", err) + } + } + return nil +} + +func (o *InitOptions) Run() error { + if o.TemplateURL == "" { + o.TemplateURL = defaultTemplateURL + } + + // remove existing directory + dir := path.Join(o.Path, o.Name) + if err := os.RemoveAll(dir); err != nil { + return fmt.Errorf("failed to remove existing directory: %v", err) + } + if err := os.MkdirAll(dir, os.ModePerm); err != nil { + return err + } + + // clone templates repo + branch := plumbing.NewBranchReferenceName(defaultBranch) + err := gitutil.GitCloneOrPull(o.TemplateURL, branch, dir, false) + if err != nil { + return fmt.Errorf("failed to clone git repo:%s, %w", o.TemplateURL, err) + } + fmt.Printf("initialized module %s successfully\n", o.Name) + return nil +} diff --git a/pkg/cmd/mod/mod_init_test.go b/pkg/cmd/mod/mod_init_test.go new file mode 100644 index 00000000..ddeab965 --- /dev/null +++ b/pkg/cmd/mod/mod_init_test.go @@ -0,0 +1,60 @@ +package mod + +import ( + "os" + "testing" + + "github.com/bytedance/mockey" + "github.com/stretchr/testify/assert" + + "github.com/pulumi/pulumi/sdk/v3/go/common/util/gitutil" +) + +func TestValidateWithEmptyModuleName(t *testing.T) { + o := &InitOptions{} + err := o.Validate([]string{}) + assert.Error(t, err) + assert.Equal(t, "module Name is empty", err.Error()) +} + +func TestValidateWithNonExistentPath(t *testing.T) { + o := &InitOptions{} + err := o.Validate([]string{"my-app", "/non/existent/Path"}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to create module directory") +} + +func TestValidateWithExistingPath(t *testing.T) { + o := &InitOptions{} + err := o.Validate([]string{"my-app", os.TempDir()}) + assert.NoError(t, err) +} + +func TestRunWithEmptyTemplateURL(t *testing.T) { + o := &InitOptions{} + o.Name = "my-app" + o.Path = os.TempDir() + err := o.Run() + assert.NoError(t, err) +} + +func TestRunWithInvalidTemplateURL(t *testing.T) { + o := &InitOptions{} + o.Name = "my-app" + o.Path = os.TempDir() + o.TemplateURL = "invalid-url" + err := o.Run() + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to clone git repo") +} + +func TestNewCmdInit(t *testing.T) { + t.Run("validate error", func(t *testing.T) { + // mock git clone + mockey.Mock(gitutil.GitCloneOrPull).Return(nil).Build() + cmd := NewCmdInit() + cmd.SetArgs([]string{"fakeName"}) + err := cmd.Execute() + assert.Nil(t, err) + }) +}