cli/azd/cmd/templates.go (486 lines of code) (raw):

// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. package cmd import ( "context" "errors" "fmt" "io" "strings" "github.com/azure/azure-dev/cli/azd/cmd/actions" "github.com/azure/azure-dev/cli/azd/pkg/input" "github.com/azure/azure-dev/cli/azd/pkg/output" "github.com/azure/azure-dev/cli/azd/pkg/output/ux" "github.com/azure/azure-dev/cli/azd/pkg/templates" "github.com/spf13/cobra" ) func templatesActions(root *actions.ActionDescriptor) *actions.ActionDescriptor { group := root.Add("template", &actions.ActionDescriptorOptions{ Command: &cobra.Command{ Short: fmt.Sprintf("Find and view template details. %s", output.WithWarningFormat("(Beta)")), }, HelpOptions: actions.ActionHelpOptions{ Description: getCmdTemplateHelpDescription, Footer: getCmdTemplateHelpFooter, }, GroupingOptions: actions.CommandGroupOptions{ RootLevelHelp: actions.CmdGroupConfig, }, }) group.Add("list", &actions.ActionDescriptorOptions{ Command: newTemplateListCmd(), ActionResolver: newTemplateListAction, FlagsResolver: newTemplateListFlags, OutputFormats: []output.Format{output.JsonFormat, output.TableFormat}, DefaultFormat: output.TableFormat, }) group.Add("show", &actions.ActionDescriptorOptions{ Command: newTemplateShowCmd(), ActionResolver: newTemplateShowAction, OutputFormats: []output.Format{output.JsonFormat, output.NoneFormat}, DefaultFormat: output.NoneFormat, }) _ = templateSourceActions(group) return group } type templateListFlags struct { source string tags []string } func newTemplateListFlags(cmd *cobra.Command) *templateListFlags { flags := &templateListFlags{} cmd.Flags().StringVarP(&flags.source, "source", "s", "", "Filters templates by source.") cmd.Flags().StringSliceVarP( &flags.tags, "filter", "f", []string{}, "The tag(s) used to filter template results. Supports comma-separated values.", ) return flags } func newTemplateListCmd() *cobra.Command { return &cobra.Command{ Use: "list", Short: fmt.Sprintf("Show list of sample azd templates. %s", output.WithWarningFormat("(Beta)")), Aliases: []string{"ls"}, } } type templateListAction struct { flags *templateListFlags formatter output.Formatter writer io.Writer templateManager *templates.TemplateManager } func newTemplateListAction( flags *templateListFlags, formatter output.Formatter, writer io.Writer, templateManager *templates.TemplateManager, ) actions.Action { return &templateListAction{ flags: flags, formatter: formatter, writer: writer, templateManager: templateManager, } } func (tl *templateListAction) Run(ctx context.Context) (*actions.ActionResult, error) { options := &templates.ListOptions{ Source: tl.flags.source, Tags: tl.flags.tags, } listedTemplates, err := tl.templateManager.ListTemplates(ctx, options) if err != nil { return nil, err } if tl.formatter.Kind() == output.TableFormat { columns := []output.Column{ { Heading: "Name", ValueTemplate: `{{if ne .Name ""}}{{.Name}}{{else}}{{.Title}}{{end}}`, }, { Heading: "Source", ValueTemplate: "{{.Source}}", }, { Heading: "Repository Path", ValueTemplate: `{{if ne .RepositoryPath ""}}{{.RepositoryPath}}{{else}}{{.RepoSource}}{{end}}`, Transformer: templates.Hyperlink, }, } err = tl.formatter.Format(listedTemplates, tl.writer, output.TableFormatterOptions{ Columns: columns, }) } else { err = tl.formatter.Format(listedTemplates, tl.writer, nil) } return nil, err } type templateShowAction struct { formatter output.Formatter writer io.Writer templateManager *templates.TemplateManager path string } func newTemplateShowAction( formatter output.Formatter, writer io.Writer, templateManager *templates.TemplateManager, args []string, ) actions.Action { return &templateShowAction{ formatter: formatter, writer: writer, templateManager: templateManager, path: args[0], } } func (a *templateShowAction) Run(ctx context.Context) (*actions.ActionResult, error) { matchingTemplate, err := a.templateManager.GetTemplate(ctx, a.path) if err != nil { return nil, err } if a.formatter.Kind() == output.NoneFormat { err = matchingTemplate.Display(a.writer) } else { err = a.formatter.Format(matchingTemplate, a.writer, nil) } return nil, err } func newTemplateShowCmd() *cobra.Command { return &cobra.Command{ Use: "show <template>", Short: fmt.Sprintf("Show details for a given template. %s", output.WithWarningFormat("(Beta)")), Args: cobra.ExactArgs(1), } } func getCmdTemplateHelpDescription(*cobra.Command) string { return generateCmdHelpDescription( fmt.Sprintf( "View details of your current template or browse a list of curated sample templates. %s", output.WithWarningFormat("(Beta)")), []string{ formatHelpNote(fmt.Sprintf("The azd CLI includes a curated list of sample templates viewable by running %s.", output.WithHighLightFormat("azd template list"))), formatHelpNote(fmt.Sprintf("To view all available sample templates, including those submitted by the azd"+ " community visit: %s.", output.WithLinkFormat("https://azure.github.io/awesome-azd"))), formatHelpNote(fmt.Sprintf("Running %s without a template will prompt you to start with a minimal"+ " template or select from our curated list of samples.", output.WithHighLightFormat("azd init"))), }) } func getCmdTemplateSourceAddHelpDescription(*cobra.Command) string { return generateCmdHelpDescription( fmt.Sprintf("Adds an azd template source with the specified key. %s\n", output.WithWarningFormat("(Beta)"))+ "The key can be any value that uniquely identifies the template source, with well-known values being:", []string{ formatHelpNote("default: Default templates"), formatHelpNote("awesome-azd: Templates from https://aka.ms/awesome-azd"), }) } func getCmdTemplateSourceHelpDescription(*cobra.Command) string { return generateCmdHelpDescription( fmt.Sprintf( "View and manage azd template sources used within %s and %s experiences. %s", output.WithHighLightFormat("azd template list"), output.WithHighLightFormat("azd init"), output.WithWarningFormat("(Beta)")), []string{ formatHelpNote("Template sources allow customizing the list of available templates to include additional" + " local or remote files and urls."), formatHelpNote(fmt.Sprintf("Running %s without a template will prompt you to start with a minimal"+ " template or select from a template from your registered template sources.", output.WithHighLightFormat("azd init"))), }) } func getCmdTemplateSourceAddHelpFooter(*cobra.Command) string { return generateCmdHelpSamplesBlock(map[string]string{ "Add templates from awesome-azd source": output.WithHighLightFormat( "azd template source add awesome-azd", ), "Add default azd templates source.": output.WithHighLightFormat( "azd template source add default", ), "Add templates from a GitHub repository": output.WithHighLightFormat( "azd template source add <key> --type gh --location <GitHub URL>", ), "Add templates from a public url": output.WithHighLightFormat( "azd template source add <key> --type url --location https://example.com/templates.json", ), "Add templates from a file path": output.WithHighLightFormat( "azd template source add <key> --type file --location /path/to/templates.json", ), }) } // templateSourceActions creates the 'source' command group with child actions func templateSourceActions(root *actions.ActionDescriptor) *actions.ActionDescriptor { group := root.Add("source", &actions.ActionDescriptorOptions{ Command: &cobra.Command{ Short: fmt.Sprintf("View and manage template sources. %s", output.WithWarningFormat("(Beta)")), }, HelpOptions: actions.ActionHelpOptions{ Description: getCmdTemplateSourceHelpDescription, Footer: getCmdTemplateSourceHelpFooter, }, }) group.Add("list", &actions.ActionDescriptorOptions{ Command: newTemplateSourceListCmd(), ActionResolver: newTemplateSourceListAction, OutputFormats: []output.Format{output.JsonFormat, output.TableFormat}, DefaultFormat: output.TableFormat, }) group.Add("add", &actions.ActionDescriptorOptions{ Command: newTemplateSourceAddCmd(), ActionResolver: newTemplateSourceAddAction, FlagsResolver: newTemplateSourceAddFlags, OutputFormats: []output.Format{output.NoneFormat}, DefaultFormat: output.NoneFormat, HelpOptions: actions.ActionHelpOptions{ Description: getCmdTemplateSourceAddHelpDescription, Footer: getCmdTemplateSourceAddHelpFooter, }, }) group.Add("remove", &actions.ActionDescriptorOptions{ Command: newTemplateSourceRemoveCmd(), ActionResolver: newTemplateSourceRemoveAction, OutputFormats: []output.Format{output.NoneFormat}, DefaultFormat: output.NoneFormat, }) return group } func newTemplateSourceListCmd() *cobra.Command { return &cobra.Command{ Use: "list", Short: fmt.Sprintf("Lists the configured azd template sources. %s", output.WithWarningFormat("(Beta)")), Aliases: []string{"ls"}, } } type templateSourceListAction struct { formatter output.Formatter writer io.Writer sourceManager templates.SourceManager } func newTemplateSourceListAction( formatter output.Formatter, writer io.Writer, sourceManager templates.SourceManager, ) actions.Action { return &templateSourceListAction{ formatter: formatter, writer: writer, sourceManager: sourceManager, } } func (a *templateSourceListAction) Run(ctx context.Context) (*actions.ActionResult, error) { sourceConfigs, err := a.sourceManager.List(ctx) if err != nil { return nil, fmt.Errorf("failed to list template sources: %w", err) } if a.formatter.Kind() == output.TableFormat { columns := []output.Column{ { Heading: "Key", ValueTemplate: "{{.Key}}", }, { Heading: "Name", ValueTemplate: "{{.Name}}", }, { Heading: "Type", ValueTemplate: "{{.Type}}", }, { Heading: "Location", ValueTemplate: "{{.Location}}", }, } err = a.formatter.Format(sourceConfigs, a.writer, output.TableFormatterOptions{ Columns: columns, }) } else { err = a.formatter.Format(sourceConfigs, a.writer, nil) } return nil, err } func newTemplateSourceAddCmd() *cobra.Command { return &cobra.Command{ Use: "add <key>", Short: fmt.Sprintf("Adds an azd template source with the specified key. %s", output.WithWarningFormat("(Beta)")), Long: "The key can be any value that uniquely identifies the template source, with well-known values being:\n" + " ・default: Default templates\n" + " ・awesome-azd: Templates from https://aka.ms/awesome-azd", Args: cobra.ExactArgs(1), } } type templateSourceAddFlags struct { name string location string kind string } func newTemplateSourceAddFlags(cmd *cobra.Command) *templateSourceAddFlags { flags := &templateSourceAddFlags{} cmd.Flags().StringVarP(&flags.kind, "type", "t", "", "Kind of the template source. Supported types are "+ "'file', 'url' and 'gh'.") cmd.Flags().StringVarP(&flags.location, "location", "l", "", "Location of the template source. "+ "Required when using type flag.") cmd.Flags().StringVarP(&flags.name, "name", "n", "", "Display name of the template source.") return flags } type templateSourceAddAction struct { flags *templateSourceAddFlags console input.Console sourceManager templates.SourceManager args []string } func newTemplateSourceAddAction( flags *templateSourceAddFlags, console input.Console, sourceManager templates.SourceManager, args []string, ) actions.Action { return &templateSourceAddAction{ flags: flags, console: console, sourceManager: sourceManager, args: args, } } func (a *templateSourceAddAction) Run(ctx context.Context) (*actions.ActionResult, error) { a.console.MessageUxItem(ctx, &ux.MessageTitle{ Title: "Add template source (azd template source add)", }) var key = strings.ToLower(a.args[0]) sourceConfig := &templates.SourceConfig{} spinnerMessage := "Validating template source" a.console.ShowSpinner(ctx, spinnerMessage, input.Step) // Don't allow source type since they can only be added with known key like 'default' or 'awesome-azd' for _, wellKnownSource := range templates.WellKnownSources { if wellKnownSource.Type == templates.SourceKind(strings.ToLower(a.flags.kind)) { a.console.StopSpinner(ctx, spinnerMessage, input.StepFailed) return nil, fmt.Errorf( "'%s' is a known key. It can't be used as type for the custom key '%s'. "+ "For custom key, supported types are %s. "+ "If you are trying to add the known source '%s', "+ "run `azd template source add %s` (w/o the --type flag). ", a.flags.kind, key, ux.ListAsText([]string{"'file'", "'url'", "'gh'"}), a.flags.kind, a.flags.kind, ) } } if _, ok := templates.WellKnownSources[key]; !ok { sourceConfig = &templates.SourceConfig{ Key: key, Type: templates.SourceKind(a.flags.kind), Location: a.flags.location, Name: a.flags.name, } // Validate the custom source config _, err := a.sourceManager.CreateSource(ctx, sourceConfig) a.console.StopSpinner(ctx, spinnerMessage, input.GetStepResultFormat(err)) if err != nil { if errors.Is(err, templates.ErrSourceTypeInvalid) { return nil, fmt.Errorf( "template source type '%s' is not supported. Supported types are %s", a.flags.kind, ux.ListAsText([]string{"'file'", "'url'", "'gh'"}), ) } return nil, fmt.Errorf("template source validation failed: %w", err) } } spinnerMessage = "Saving template source" a.console.ShowSpinner(ctx, spinnerMessage, input.Step) err := a.sourceManager.Add(ctx, key, sourceConfig) a.console.StopSpinner(ctx, spinnerMessage, input.GetStepResultFormat(err)) if err != nil { return nil, fmt.Errorf("failed adding template source: %w", err) } return &actions.ActionResult{ Message: &actions.ResultMessage{ Header: fmt.Sprintf("Added azd template source %s", key), FollowUp: "Run `azd template list` to see the available set of azd templates.", }, }, nil } func newTemplateSourceRemoveCmd() *cobra.Command { return &cobra.Command{ Use: "remove <key>", Short: fmt.Sprintf("Removes the specified azd template source %s", output.WithWarningFormat("(Beta)")), Args: cobra.ExactArgs(1), } } type templateSourceRemoveAction struct { sourceManager templates.SourceManager console input.Console args []string } func newTemplateSourceRemoveAction( sourceManager templates.SourceManager, console input.Console, args []string, ) actions.Action { return &templateSourceRemoveAction{ sourceManager: sourceManager, console: console, args: args, } } func (a *templateSourceRemoveAction) Run(ctx context.Context) (*actions.ActionResult, error) { a.console.MessageUxItem(ctx, &ux.MessageTitle{ Title: "Remove template source (azd template source remove)", }) var key = strings.ToLower(a.args[0]) spinnerMessage := fmt.Sprintf("Removing template source (%s)", key) a.console.ShowSpinner(ctx, spinnerMessage, input.Step) err := a.sourceManager.Remove(ctx, key) a.console.StopSpinner(ctx, spinnerMessage, input.GetStepResultFormat(err)) if err != nil { return nil, fmt.Errorf("failed removing template source: %w", err) } return &actions.ActionResult{ Message: &actions.ResultMessage{ Header: fmt.Sprintf("Removed azd template source %s", key), FollowUp: fmt.Sprintf( "Add more template sources by running %s", output.WithHighLightFormat("azd template source add <key>"), ), }, }, nil } func getCmdTemplateHelpFooter(*cobra.Command) string { return generateCmdHelpSamplesBlock(map[string]string{ "View a list of all azd templates across template sources.": output.WithHighLightFormat( "azd template list", ), "View a list of azd templates for a specific template source.": output.WithHighLightFormat( "azd template list --source <key>", ), "View the details of an azd template.": output.WithHighLightFormat( "azd template show <template-name>", ), }) } func getCmdTemplateSourceHelpFooter(*cobra.Command) string { return generateCmdHelpSamplesBlock(map[string]string{ "View a list of registered azd template sources.": output.WithHighLightFormat( "azd template source list", ), "Enable the Awesome Azd template source.": output.WithHighLightFormat( "azd template source add awesome-azd", ), "Add a new file template source.": output.WithHighLightFormat( "azd template source add <key> --type file --location <path>", ), "Add a new url template source.": output.WithHighLightFormat( "azd template source add <key> --type url --location <url>", ), "Add a new GitHub template source.": output.WithHighLightFormat( "azd template source add <key> --type gh --location <GitHub URL>", ), "Remove a previously registered template source.": output.WithHighLightFormat( "azd template source remove <key>", ), }) }