From 621db5fb6a478a04b36259548f17617aa7f7b789 Mon Sep 17 00:00:00 2001 From: KK <68334452+healthjyk@users.noreply.github.com> Date: Wed, 10 May 2023 19:31:05 +0800 Subject: [PATCH] add templates as sub cmd of init, used to list templates, support json format (#334) Co-authored-by: healthjyk --- pkg/cmd/init/init.go | 48 +++++++- pkg/cmd/init/options.go | 222 +++++++++++++++++++++++++++-------- pkg/cmd/init/options_test.go | 12 +- pkg/scaffold/templates.go | 6 +- 4 files changed, 234 insertions(+), 54 deletions(-) diff --git a/pkg/cmd/init/init.go b/pkg/cmd/init/init.go index b8022e19..b1f7623d 100644 --- a/pkg/cmd/init/init.go +++ b/pkg/cmd/init/init.go @@ -12,7 +12,7 @@ var ( initShort = `Initialize KCL file structure and base codes for a new project` initLong = ` - kusion init command helps you to generate an scaffolding KCL project. + kusion init command helps you to generate a scaffolding KCL project. Try "kusion init" to simply get a demo project.` initExample = ` @@ -27,6 +27,19 @@ var ( # Initialize a new KCL project from local directory kusion init /path/to/templates` + + templatesShort = `List Templates used to initialize a new project` + + templatesLong = ` + kusion init templates command helps you get the templates which are used + to generate a scaffolding KCL project.` + + templatesExample = ` + # Get name and description of internal templates + kusion init templates + + # Get templates from specific templates location + kusion init templates https://github.com// --online=true` ) func NewCmdInit() *cobra.Command { @@ -52,7 +65,7 @@ func NewCmdInit() *cobra.Command { cmd.Flags().BoolVar( &o.Force, "force", false, i18n.T("Forces content to be generated even if it would change existing files")) - cmd.Flags().BoolVar( + cmd.PersistentFlags().BoolVar( &o.Online, "online", false, i18n.T("Use locally cached templates without making any network requests")) cmd.Flags().BoolVar( @@ -61,5 +74,36 @@ func NewCmdInit() *cobra.Command { cmd.Flags().StringVar( &o.CustomParamsJSON, "custom-params", "", i18n.T("Custom params in JSON string; if not empty, kusion will skip prompt process and use it as template default value")) + + templatesCmd := newCmdTemplates() + cmd.AddCommand(templatesCmd) + return cmd +} + +func newCmdTemplates() *cobra.Command { + o := NewTemplatesOptions() + cmd := &cobra.Command{ + Use: "templates", + Short: i18n.T(templatesShort), + Long: templates.LongDesc(i18n.T(templatesLong)), + Example: templates.Examples(i18n.T(templatesExample)), + DisableFlagsInUseLine: true, + RunE: func(cmd *cobra.Command, args []string) (err error) { + defer util.RecoverErr(&err) + online, err := cmd.InheritedFlags().GetBool("online") + if err != nil { + return err + } + util.CheckErr(o.Complete(args, online)) + util.CheckErr(o.Validate()) + util.CheckErr(o.Run()) + return + }, + } + + cmd.Flags().StringVarP( + &o.Output, "output", "o", "", + i18n.T("The output format, only support json if specified; if not specified, print template name and description")) + return cmd } diff --git a/pkg/cmd/init/options.go b/pkg/cmd/init/options.go index e43df7b6..ed581d75 100644 --- a/pkg/cmd/init/options.go +++ b/pkg/cmd/init/options.go @@ -15,6 +15,8 @@ import ( "kusionstack.io/kusion/pkg/scaffold" ) +const jsonOutput = "json" + type InitOptions struct { TemplateNameOrURL string Online bool @@ -30,22 +32,13 @@ func NewInitOptions() *InitOptions { func (o *InitOptions) Complete(args []string) error { if o.Online { // use online templates, official link or user-specified link - if len(args) > 0 { - // user-specified link - o.TemplateNameOrURL = args[0] - } + o.TemplateNameOrURL = getURL(args) } else { // use offline templates, internal templates or user-specified local dir - if len(args) > 0 { - // user-specified local dir - o.TemplateNameOrURL = args[0] - } else { - // use internal templates - internalTemplateDir, err := scaffold.GetTemplateDir(scaffold.InternalTemplateDir) - if err != nil { - return err - } - o.TemplateNameOrURL = internalTemplateDir + path, err := getPath(args) + if err != nil { + return err } + o.TemplateNameOrURL = path } return nil } @@ -55,40 +48,25 @@ func (o *InitOptions) Validate() error { return nil } // offline mode may need to generate templates - internalTemplateDir, err := scaffold.GetTemplateDir(scaffold.InternalTemplateDir) - if err != nil { + if err := validatePath(o.TemplateNameOrURL); err != nil { return err } - // gen internal templates first before using it - if internalTemplateDir == o.TemplateNameOrURL { - _, err := os.Stat(o.TemplateNameOrURL) - if os.IsNotExist(err) { - return scaffold.GenInternalTemplates() - } - } return nil } func (o *InitOptions) Run() error { // Retrieve the template repo. - repo, err := scaffold.RetrieveTemplates(o.TemplateNameOrURL, o.Online) + repo, err := retrieveTemplateRepo(o.TemplateNameOrURL, o.Online) if err != nil { return err } - defer func() { - if err := repo.Delete(); err != nil { - log.Warnf("Explicitly ignoring and discarding error: %v", err) - } - }() + defer deleteTemplateRepo(repo) // List the templates from the repo. - templates, err := repo.Templates() + templates, err := getTemplates(repo) if err != nil { return err } - if len(templates) == 0 { - return errors.New("no templates") - } // Choose template var template scaffold.Template @@ -203,6 +181,165 @@ func (o *InitOptions) Run() error { return nil } +type TemplatesOptions struct { + Online bool + URL string + Path string + Output string +} + +func NewTemplatesOptions() *TemplatesOptions { + return &TemplatesOptions{} +} + +func (o *TemplatesOptions) Complete(args []string, online bool) error { + o.Online = online + if o.Online { + o.URL = getURL(args) + } else { + if path, err := getPath(args); err != nil { + return err + } else { + o.Path = path + } + } + return nil +} + +func (o *TemplatesOptions) Validate() error { + if o.Output != "" && o.Output != jsonOutput { + return errors.New("invalid output type, supported types: json") + } + if !o.Online { + if err := validatePath(o.Path); err != nil { + return err + } + } + return nil +} + +func (o *TemplatesOptions) Run() error { + var templateName string + if o.Online { + templateName = o.URL + } else { + templateName = o.Path + } + // retrieve template repo + repo, err := retrieveTemplateRepo(templateName, o.Online) + if err != nil { + return err + } + defer deleteTemplateRepo(repo) + + // get templates from repo, and print it + templates, err := getTemplates(repo) + if err != nil { + return err + } + templateOutputs, err := fmtTemplatesOutput(templates, o.Output == jsonOutput) + if err != nil { + return err + } + for _, output := range templateOutputs { + pterm.Println(output) + } + return nil +} + +// getURL parses url from args, called when --online is true. +func getURL(args []string) string { + if len(args) > 0 { + // user-specified link + return args[0] + } + return "" // use official link +} + +// getPath parses path from args, if not specified, use default InternalTemplateDir, +// called when --online is false. +func getPath(args []string) (string, error) { + if len(args) > 0 { + // user-specified local dir + return args[0], nil + } else { + // use internal templates + internalTemplateDir, err := scaffold.GetTemplateDir(scaffold.InternalTemplateDir) + if err != nil { + return "", err + } + return internalTemplateDir, nil + } +} + +// validatePath checks the path is valid or not. +func validatePath(path string) error { + // offline mode may need to generate templates + internalTemplateDir, err := scaffold.GetTemplateDir(scaffold.InternalTemplateDir) + if err != nil { + return err + } + // gen internal templates first before using it + if internalTemplateDir == path { + if _, err = os.Stat(path); os.IsNotExist(err) { + return scaffold.GenInternalTemplates() + } + } + return nil +} + +// retrieveTemplateRepo gets template repos from online or local, with specified url or path. +func retrieveTemplateRepo(templateName string, online bool) (scaffold.TemplateRepository, error) { + return scaffold.RetrieveTemplates(templateName, online) +} + +// deleteTemplateRepo is used to delete the files of the template repos, log warn if failed. +func deleteTemplateRepo(repo scaffold.TemplateRepository) { + if err := repo.Delete(); err != nil { + log.Warnf("Explicitly ignoring and discarding error: %w", err) + } +} + +// getTemplates get templates from template repo. +func getTemplates(repo scaffold.TemplateRepository) ([]scaffold.Template, error) { + // List the templates from the repo. + templates, err := repo.Templates() + if err != nil { + return nil, err + } + if len(templates) == 0 { + return nil, errors.New("no templates") + } + return templates, nil +} + +// fmtTemplatesOutput is used to format the templates output, in text or json. +func fmtTemplatesOutput(templates []scaffold.Template, jsonFmt bool) ([]string, error) { + var outputs []string + if jsonFmt { + output, err := json.Marshal(templates) + if err != nil { + return nil, fmt.Errorf("failed to json marshal templates as %w", err) + } + outputs = append(outputs, string(output)) + } else { + // Find the longest name length. Used to add padding between the name and description. + maxNameLength := 0 + for _, template := range templates { + if len(template.Name) > maxNameLength { + maxNameLength = len(template.Name) + } + } + // Create the option string that combines the name, padding, and description. + for _, template := range templates { + output := fmt.Sprintf(fmt.Sprintf("%%%ds %%s", -maxNameLength), template.Name, template.Description) + outputs = append(outputs, output) + } + } + + return outputs, nil +} + // chooseTemplate will prompt the user to choose amongst the available templates. func chooseTemplate(templates []scaffold.Template) (scaffold.Template, error) { const chooseTemplateErr = "no template selected; please use `kusion init` to choose one" @@ -236,24 +373,11 @@ func chooseTemplate(templates []scaffold.Template) (scaffold.Template, error) { // templatesToOptionArrayAndMap returns an array of option strings and a map of option strings to templates. // Each option string is made up of the template name and description with some padding in between. func templatesToOptionArrayAndMap(templates []scaffold.Template) ([]string, map[string]scaffold.Template) { - // Find the longest name length. Used to add padding between the name and description. - maxNameLength := 0 - for _, template := range templates { - if len(template.Name) > maxNameLength { - maxNameLength = len(template.Name) - } - } - // Build the array and map. - options := []string{} + options, _ := fmtTemplatesOutput(templates, false) nameToTemplateMap := make(map[string]scaffold.Template) - for _, template := range templates { - // Create the option string that combines the name, padding, and description. - option := fmt.Sprintf(fmt.Sprintf("%%%ds %%s", -maxNameLength), template.Name, template.Description) - - // Add it to the array and map. - options = append(options, option) - nameToTemplateMap[option] = template + for i, template := range templates { + nameToTemplateMap[options[i]] = template } sort.Strings(options) diff --git a/pkg/cmd/init/options_test.go b/pkg/cmd/init/options_test.go index 575cef01..bc7ca75e 100644 --- a/pkg/cmd/init/options_test.go +++ b/pkg/cmd/init/options_test.go @@ -41,7 +41,17 @@ func TestRun(t *testing.T) { assert.Nil(t, err) err = o.Run() assert.Nil(t, err) - os.RemoveAll(o.ProjectName) + _ = os.RemoveAll(o.ProjectName) + }) + + t.Run("init templates from official url", func(t *testing.T) { + o := &TemplatesOptions{ + Output: jsonOutput, + } + err := o.Complete(nil, true) + assert.Nil(t, err) + err = o.Run() + assert.Nil(t, err) }) } diff --git a/pkg/scaffold/templates.go b/pkg/scaffold/templates.go index 87899396..b22dc96d 100644 --- a/pkg/scaffold/templates.go +++ b/pkg/scaffold/templates.go @@ -125,8 +125,10 @@ func LoadTemplate(path string) (Template, error) { // Template represents a project template. type Template struct { - Dir string // The directory containing kusion.yaml. - Name string // The name of the template. + // The directory containing kusion.yaml. + Dir string `json:"dir,omitempty" yaml:"dir,omitempty"` + // The name of the template. + Name string `json:"name,omitempty" yaml:"name,omitempty"` *ProjectTemplate }