cli/azd/cmd/cobra_builder.go (231 lines of code) (raw):

// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. package cmd import ( "fmt" "log" "slices" "strconv" "strings" "github.com/azure/azure-dev/cli/azd/cmd/actions" "github.com/azure/azure-dev/cli/azd/cmd/middleware" "github.com/azure/azure-dev/cli/azd/pkg/input" "github.com/azure/azure-dev/cli/azd/pkg/ioc" "github.com/azure/azure-dev/cli/azd/pkg/output" "github.com/azure/azure-dev/cli/azd/pkg/tools" "github.com/spf13/cobra" ) // CobraBuilder manages the construction of the cobra command tree from nested ActionDescriptors type CobraBuilder struct { container *ioc.NestedContainer } // Creates a new instance of the Cobra builder func NewCobraBuilder(container *ioc.NestedContainer) *CobraBuilder { return &CobraBuilder{ container: container, } } // Builds a cobra Command for the specified action descriptor func (cb *CobraBuilder) BuildCommand(descriptor *actions.ActionDescriptor) (*cobra.Command, error) { cmd := descriptor.Options.Command if cmd.Use == "" { cmd.Use = descriptor.Name } // Build the full command tree for _, childDescriptor := range descriptor.Children() { childCmd, err := cb.BuildCommand(childDescriptor) if err != nil { return nil, err } cmd.AddCommand(childCmd) } // Bind root command after command tree has been established // This ensures the command path is ready and consistent across all nested commands if descriptor.Parent() == nil { if err := cb.bindCommand(cmd, descriptor); err != nil { return nil, err } } // Configure action resolver for leaf commands if !cmd.HasSubCommands() { if err := cb.configureActionResolver(cmd, descriptor); err != nil { return nil, err } } return cmd, nil } // Configures the cobra command 'RunE' function to running the composed middleware and action for the // current action descriptor func (cb *CobraBuilder) configureActionResolver(cmd *cobra.Command, descriptor *actions.ActionDescriptor) error { // Dev Error: Either an action resolver or RunE must be set if descriptor.Options.ActionResolver == nil && cmd.RunE == nil { return fmt.Errorf( //nolint:lll "action descriptor for '%s' must be configured with either an ActionResolver or a Cobra RunE command", cmd.CommandPath(), ) } // Dev Error: Both action resolver and RunE have been defined if descriptor.Options.ActionResolver != nil && cmd.RunE != nil { return fmt.Errorf( //nolint:lll "action descriptor for '%s' must be configured with either an ActionResolver or a Cobra RunE command but NOT both", cmd.CommandPath(), ) } // Only bind command to action if an action resolver had been defined // and when a RunE hasn't already been set if descriptor.Options.ActionResolver == nil || cmd.RunE != nil { return nil } cmd.RunE = func(cmd *cobra.Command, args []string) error { // Register root go context that will be used for resolving singleton dependencies ctx := tools.WithInstalledCheckCache(cmd.Context()) ioc.RegisterInstance(cb.container, ctx) // Create new container scope for the current command cmdContainer, err := cb.container.NewScope() if err != nil { return fmt.Errorf("failed creating new scope for command, %w", err) } // Registers the following to enable injection into actions that require them ioc.RegisterInstance(cmdContainer, ctx) ioc.RegisterInstance(cmdContainer, cmd) ioc.RegisterInstance(cmdContainer, args) ioc.RegisterInstance(cmdContainer, cmdContainer) ioc.RegisterInstance[ioc.ServiceLocator](cmdContainer, cmdContainer) // Register any required middleware registered for the current action descriptor middlewareRunner := middleware.NewMiddlewareRunner(cmdContainer) if err := cb.registerMiddleware(middlewareRunner, descriptor); err != nil { return err } runOptions := &middleware.Options{ Name: cmd.Name(), CommandPath: cmd.CommandPath(), Aliases: cmd.Aliases, Flags: cmd.Flags(), Args: args, } // Set the container that should be used for resolving middleware components runOptions.WithContainer(cmdContainer) // Run the middleware chain with action actionName := createActionName(cmd) _, err = middlewareRunner.RunAction(ctx, runOptions, actionName) // At this point, we know that there might be an error, so we can silence cobra from showing it after us. cmd.SilenceErrors = true return err } return nil } // docsFlag is a flag with a custom parsing implementation which changes the default behavior for printing help // for all commands, when it is set as true. // docsFlag keeps a reference to the cobra command where it belongs so it can update it. // docsFlag also contains a callbacks to pull dependencies for the docs routine. type docsFlag struct { // reference to the command where the flag was added. command *cobra.Command consoleFn func() input.Console value bool defaultHelpFn func(*cobra.Command, []string) } // returns the flag value func (df *docsFlag) String() string { return fmt.Sprintf("%t", df.value) } // define flag type func (df *docsFlag) Type() string { return "bool" } // Set not only initialize the flag value, but it also turns the help flag true and defines the HelpFunc for the command. // This wiring forces cobra to react as it the --help flag was provided and stop the command early to run the HelpFunc. func (df *docsFlag) Set(value string) error { v, err := strconv.ParseBool(value) if err != nil { return fmt.Errorf("invalid value for boolean --docs parameter") } df.value = v if !df.value { return nil } // Setting help to true will make cobra to stop and call the HelpFunc if err = df.command.Flag("help").Value.Set("true"); err != nil { // dev-issue: help flag should be already been added when log.Panic("tried to set help after docs parameter: %w", err) } // keeping the default help function allows to set --help with higher priority and use it // in case of finding --docs and --help df.defaultHelpFn = df.command.HelpFunc() // set help func for doing docs df.command.SetHelpFunc(func(c *cobra.Command, args []string) { console := df.consoleFn() ctx := c.Context() ctx = tools.WithInstalledCheckCache(ctx) if slices.Contains(args, "--help") { df.defaultHelpFn(c, args) return } commandPath := strings.ReplaceAll(c.CommandPath(), " ", "-") commandDocsUrl := referenceDocumentationUrl + commandPath openWithDefaultBrowser(ctx, console, commandDocsUrl) }) return nil } // Binds the intersection of cobra command options and action descriptor options func (cb *CobraBuilder) bindCommand(cmd *cobra.Command, descriptor *actions.ActionDescriptor) error { actionName := createActionName(cmd) // Automatically adds a consistent help flag cmd.Flags().BoolP("help", "h", false, fmt.Sprintf("Gets help for %s.", cmd.Name())) // docs flags for all commands docsFlag := &docsFlag{ command: cmd, consoleFn: func() input.Console { var console input.Console if err := cb.container.Resolve(&console); err != nil { log.Panic("creating docs flag: %w", err) } return console }, } flag := cmd.Flags().VarPF( docsFlag, "docs", "", fmt.Sprintf("Opens the documentation for %s in your web browser.", cmd.CommandPath())) flag.NoOptDefVal = "true" // Consistently registers output formats for the descriptor if len(descriptor.Options.OutputFormats) > 0 { output.AddOutputParam(cmd, descriptor.Options.OutputFormats, descriptor.Options.DefaultFormat) } // Create, register and bind flags when required if descriptor.Options.FlagsResolver != nil { ioc.RegisterInstance(cb.container, cmd) // The flags resolver is constructed and bound to the cobra command via dependency injection // This allows flags to be options and support any set of required dependencies if err := cb.container.RegisterSingletonAndInvoke(descriptor.Options.FlagsResolver); err != nil { return fmt.Errorf( //nolint:lll "failed registering FlagsResolver for action '%s'. Ensure the resolver is a valid go function and resolves without error. %w", actionName, err, ) } } // Registers and bind action resolves when required // Action resolvers are essential go functions that create the instance of the required actions.Action // These functions are typically the constructor function for the action. ex) newDeployAction(...) // Action resolvers can take any number of dependencies and instantiated via the IoC container if descriptor.Options.ActionResolver != nil { if err := cb.container.RegisterNamedTransient(actionName, descriptor.Options.ActionResolver); err != nil { return fmt.Errorf( //nolint:lll "failed registering ActionResolver for action '%s'. Ensure the resolver is a valid go function and resolves without error. %w", actionName, err, ) } } // Bind flag completions // Since flags are lazily loaded we need to wait until after command flags are wired up before // any flag completion functions are registered for flag, completionFn := range descriptor.FlagCompletions() { if err := cmd.RegisterFlagCompletionFunc(flag, completionFn); err != nil { return fmt.Errorf("failed registering flag completion function for '%s', %w", flag, err) } } // Bind the child commands for the current descriptor for _, childDescriptor := range descriptor.Children() { childCmd := childDescriptor.Options.Command if err := cb.bindCommand(childCmd, childDescriptor); err != nil { return err } } if descriptor.Options.GroupingOptions.RootLevelHelp != actions.CmdGroupNone { if cmd.Annotations == nil { cmd.Annotations = make(map[string]string) } actions.SetGroupCommandAnnotation(cmd, descriptor.Options.GroupingOptions.RootLevelHelp) } // `generateCmdHelp` sets a default help section when `descriptor.Options.HelpOptions` is nil. // This call ensures all commands gets the same help formatting. cmd.SetHelpTemplate(generateCmdHelp(cmd, generateCmdHelpOptions{ Description: cmdHelpGenerator(descriptor.Options.HelpOptions.Description), Usage: cmdHelpGenerator(descriptor.Options.HelpOptions.Usage), Commands: cmdHelpGenerator(descriptor.Options.HelpOptions.Commands), Flags: cmdHelpGenerator(descriptor.Options.HelpOptions.Flags), Footer: cmdHelpGenerator(descriptor.Options.HelpOptions.Footer), })) return nil } // Registers all middleware components for the current command and any parent descriptors // Middleware components are insure to run in the order that they were registered from the // root registration, down through action groups and ultimately individual actions func (cb *CobraBuilder) registerMiddleware( middlewareRunner *middleware.MiddlewareRunner, descriptor *actions.ActionDescriptor, ) error { chain := []*actions.MiddlewareRegistration{} current := descriptor // Recursively loop through any action describer and their parents for { middleware := current.Middleware() for i := len(middleware) - 1; i > -1; i-- { registration := middleware[i] // Only use the middleware when the predicate resolves truthy or if not defined // Registration predicates are useful for when you want to selectively want to // register a middleware based on the descriptor options // Ex) Telemetry middleware registered for all actions except 'version' if registration.Predicate == nil || registration.Predicate(descriptor) { chain = append(chain, middleware[i]) } } if current.Parent() == nil { break } current = current.Parent() } // Register middleware in reverse order so middleware registered // higher up the command structure are resolved before lower registrations for i := len(chain) - 1; i > -1; i-- { registration := chain[i] if err := middlewareRunner.Use(registration.Name, registration.Resolver); err != nil { return err } } return nil } // Composes a consistent action name for the specified cobra command // ex) azd config list becomes 'azd-config-list-action' func createActionName(cmd *cobra.Command) string { actionName := cmd.CommandPath() actionName = strings.TrimSpace(actionName) actionName = strings.ReplaceAll(actionName, " ", "-") actionName = fmt.Sprintf("%s-action", actionName) return strings.ToLower(actionName) }