cli/azd/pkg/templates/template_manager.go (190 lines of code) (raw):

// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. package templates import ( "context" "fmt" "log" "slices" "strings" "github.com/azure/azure-dev/cli/azd/pkg/input" ) var ( ErrTemplateNotFound = fmt.Errorf("template not found") ) type TemplateManager struct { sourceManager SourceManager sources []Source console input.Console } func NewTemplateManager(sourceManager SourceManager, console input.Console) (*TemplateManager, error) { return &TemplateManager{ sourceManager: sourceManager, console: console, }, nil } type ListOptions struct { Source string Tags []string } type sourceFilterPredicate func(config *SourceConfig) bool type templateFilterPredicate func(template *Template) bool // ListTemplates retrieves the list of templates in a deterministic order. func (tm *TemplateManager) ListTemplates(ctx context.Context, options *ListOptions) ([]*Template, error) { msg := "Retrieving templates..." tm.console.ShowSpinner(ctx, msg, input.Step) defer tm.console.StopSpinner(ctx, "", input.StepDone) allTemplates := []*Template{} var sourceFilterPredicate sourceFilterPredicate if options != nil && options.Source != "" { sourceFilterPredicate = func(config *SourceConfig) bool { return strings.EqualFold(config.Key, options.Source) } } var templateFilterPredicate templateFilterPredicate if options != nil && len(options.Tags) > 0 { // Find templates that match all the incoming tags templateFilterPredicate = func(template *Template) bool { match := false for _, optionTag := range options.Tags { match = slices.ContainsFunc(template.Tags, func(templateTag string) bool { return strings.EqualFold(optionTag, templateTag) }) if !match { break } } return match } } sources, err := tm.getSources(ctx, sourceFilterPredicate) if err != nil { return nil, fmt.Errorf("failed listing templates: %w", err) } for _, source := range sources { filteredTemplates := []*Template{} sourceTemplates, err := source.ListTemplates(ctx) if err != nil { return nil, fmt.Errorf("unable to list templates: %w", err) } for _, template := range sourceTemplates { if templateFilterPredicate == nil || templateFilterPredicate(template) { filteredTemplates = append(filteredTemplates, template) } } // Sort by source, then repository path and finally name slices.SortFunc(filteredTemplates, func(a *Template, b *Template) int { if a.Source != b.Source { return strings.Compare(a.Source, b.Source) } if a.RepositoryPath != b.RepositoryPath { return strings.Compare(a.RepositoryPath, b.RepositoryPath) } return strings.Compare(a.Name, b.Name) }) allTemplates = append(allTemplates, filteredTemplates...) } return allTemplates, nil } func (tm *TemplateManager) GetTemplate(ctx context.Context, path string) (*Template, error) { sources, err := tm.getSources(ctx, nil) if err != nil { return nil, fmt.Errorf("failed getting template sources: %w", err) } var match *Template var sourceErr error for _, source := range sources { template, err := source.GetTemplate(ctx, path) if err != nil { sourceErr = err } else if template != nil { match = template break } } if match != nil { return match, nil } if sourceErr != nil { return nil, fmt.Errorf("failed getting template: %w", sourceErr) } return nil, ErrTemplateNotFound } func (tm *TemplateManager) getSources(ctx context.Context, filter sourceFilterPredicate) ([]Source, error) { if tm.sources != nil { return tm.sources, nil } configs, err := tm.sourceManager.List(ctx) if err != nil { return nil, fmt.Errorf("failed parsing template sources: %w", err) } sources, err := tm.createSourcesFromConfig(ctx, configs, filter) if err != nil { return nil, fmt.Errorf("failed initializing template sources: %w", err) } tm.sources = sources return tm.sources, nil } func (tm *TemplateManager) createSourcesFromConfig( ctx context.Context, configs []*SourceConfig, filter sourceFilterPredicate, ) ([]Source, error) { sources := []Source{} for _, config := range configs { if filter != nil && !filter(config) { continue } source, err := tm.sourceManager.CreateSource(ctx, config) if err != nil { log.Printf("failed to create source: %s", err.Error()) continue } sources = append(sources, source) } return sources, nil } // PromptTemplate asks the user to select a template. func PromptTemplate( ctx context.Context, message string, templateManager *TemplateManager, console input.Console, options *ListOptions, ) (Template, error) { templates, err := templateManager.ListTemplates(ctx, options) if err != nil { return Template{}, fmt.Errorf("prompting for template: %w", err) } templateChoices := []*Template{} duplicateNames := []string{} // Check for duplicate template names for _, template := range templates { hasDuplicateName := slices.ContainsFunc(templateChoices, func(t *Template) bool { return t.Name == template.Name }) if hasDuplicateName { duplicateNames = append(duplicateNames, template.Name) } templateChoices = append(templateChoices, template) } templateNames := make([]string, 0, len(templates)+1) templateDetails := make([]string, 0, len(templates)+1) for _, template := range templates { templateChoice := template.Name // Disambiguate duplicate template names with source identifier if slices.Contains(duplicateNames, template.Name) { templateChoice += fmt.Sprintf(" (%s)", template.Source) } templateDetails = append(templateDetails, template.RepositoryPath) if slices.Contains(templateNames, templateChoice) { duplicateNames = append(duplicateNames, templateChoice) } templateNames = append(templateNames, templateChoice) } selected, err := console.Select(ctx, input.ConsoleOptions{ Message: message, Options: templateNames, OptionDetails: templateDetails, DefaultValue: templateNames[0], }) // separate this prompt from the next log console.Message(ctx, "") if err != nil { return Template{}, fmt.Errorf("prompting for template: %w", err) } template := templates[selected] log.Printf("Selected template: %s", fmt.Sprint(template.RepositoryPath)) return *template, nil }