cli/azd/cmd/root.go (396 lines of code) (raw):

// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. package cmd import ( "errors" "fmt" "log" "os" "strings" "github.com/azure/azure-dev/cli/azd/cmd/actions" "github.com/azure/azure-dev/cli/azd/cmd/middleware" // Importing for infrastructure provider plugin registrations "github.com/azure/azure-dev/cli/azd/pkg/alpha" "github.com/azure/azure-dev/cli/azd/pkg/azd" "github.com/azure/azure-dev/cli/azd/pkg/extensions" "github.com/azure/azure-dev/cli/azd/pkg/ioc" "github.com/azure/azure-dev/cli/azd/pkg/platform" "github.com/azure/azure-dev/cli/azd/internal" "github.com/azure/azure-dev/cli/azd/internal/cmd" "github.com/azure/azure-dev/cli/azd/internal/cmd/add" "github.com/azure/azure-dev/cli/azd/internal/cmd/show" "github.com/azure/azure-dev/cli/azd/internal/telemetry" "github.com/azure/azure-dev/cli/azd/pkg/output" "github.com/spf13/cobra" ) // Creates the root Cobra command for AZD. // staticHelp - False, except for running for doc generation // middlewareChain - nil, except for running unit tests // rootContainer - The IoC container to use for registering and resolving dependencies. If nil is provided, a new // container empty will be created. func NewRootCmd( staticHelp bool, middlewareChain []*actions.MiddlewareRegistration, rootContainer *ioc.NestedContainer, ) *cobra.Command { prevDir := "" opts := &internal.GlobalCommandOptions{GenerateStaticHelp: staticHelp} opts.EnableTelemetry = telemetry.IsTelemetryEnabled() productName := "The Azure Developer CLI" if opts.GenerateStaticHelp { productName = "The Azure Developer CLI (`azd`)" } rootCmd := &cobra.Command{ Use: "azd", Short: fmt.Sprintf("%s is an open-source tool that helps onboard and manage your application on Azure", productName), PersistentPreRunE: func(cmd *cobra.Command, args []string) error { // If there was a platform configuration error report it to the user until it is resolved // Using fmt.Printf directly here since we can't leverage our IoC container to resolve a console instance if errors.Is(platform.Error, platform.ErrPlatformNotSupported) { fmt.Print(output.WithWarningFormat("WARNING: %s\n\n", platform.Error.Error())) } if opts.Cwd != "" { current, err := os.Getwd() if err != nil { return err } prevDir = current if err := os.Chdir(opts.Cwd); err != nil { return fmt.Errorf("failed to change directory to %s: %w", opts.Cwd, err) } } return nil }, PersistentPostRunE: func(cmd *cobra.Command, args []string) error { // This is just for cleanliness and making writing tests simpler since // we can just remove the entire project folder afterwards. // In practical execution, this wouldn't affect much, since the CLI is exiting. if prevDir != "" { return os.Chdir(prevDir) } return nil }, SilenceUsage: true, DisableAutoGenTag: true, } rootCmd.CompletionOptions.HiddenDefaultCmd = true root := actions.NewActionDescriptor("azd", &actions.ActionDescriptorOptions{ Command: rootCmd, FlagsResolver: func(cmd *cobra.Command) *internal.GlobalCommandOptions { rootCmd.PersistentFlags().StringVarP(&opts.Cwd, "cwd", "C", "", "Sets the current working directory.") rootCmd.PersistentFlags(). BoolVar(&opts.EnableDebugLogging, "debug", false, "Enables debugging and diagnostics logging.") rootCmd.PersistentFlags(). BoolVar( &opts.NoPrompt, "no-prompt", false, "Accepts the default value instead of prompting, or it fails if there is no default.") // The telemetry system is responsible for reading these flags value and using it to configure the telemetry // system, but we still need to add it to our flag set so that when we parse the command line with Cobra we // don't error due to an "unknown flag". var traceLogFile string var traceLogEndpoint string rootCmd.PersistentFlags().StringVar(&traceLogFile, "trace-log-file", "", "Write a diagnostics trace to a file.") _ = rootCmd.PersistentFlags().MarkHidden("trace-log-file") rootCmd.PersistentFlags().StringVar( &traceLogEndpoint, "trace-log-url", "", "Send traces to an Open Telemetry compatible endpoint.") _ = rootCmd.PersistentFlags().MarkHidden("trace-log-url") return opts }, }) configActions(root, opts) envActions(root) infraActions(root) pipelineActions(root) telemetryActions(root) templatesActions(root) authActions(root) hooksActions(root) root.Add("version", &actions.ActionDescriptorOptions{ Command: &cobra.Command{ Short: "Print the version number of Azure Developer CLI.", }, ActionResolver: newVersionAction, FlagsResolver: newVersionFlags, DisableTelemetry: true, OutputFormats: []output.Format{output.JsonFormat, output.NoneFormat}, DefaultFormat: output.NoneFormat, GroupingOptions: actions.CommandGroupOptions{ RootLevelHelp: actions.CmdGroupAbout, }, }) root.Add("vs-server", &actions.ActionDescriptorOptions{ Command: newVsServerCmd(), FlagsResolver: newVsServerFlags, ActionResolver: newVsServerAction, OutputFormats: []output.Format{output.NoneFormat}, DefaultFormat: output.NoneFormat, }) root.Add("show", &actions.ActionDescriptorOptions{ Command: show.NewShowCmd(), FlagsResolver: show.NewShowFlags, ActionResolver: show.NewShowAction, OutputFormats: []output.Format{output.JsonFormat, output.NoneFormat}, DefaultFormat: output.NoneFormat, GroupingOptions: actions.CommandGroupOptions{ RootLevelHelp: actions.CmdGroupMonitor, }, }) //deprecate:cmd hide login login := newLoginCmd("") login.Hidden = true root.Add("login", &actions.ActionDescriptorOptions{ Command: login, FlagsResolver: newLoginFlags, ActionResolver: newLoginAction, OutputFormats: []output.Format{output.JsonFormat, output.NoneFormat}, DefaultFormat: output.NoneFormat, }) //deprecate:cmd hide logout logout := newLogoutCmd("") logout.Hidden = true root.Add("logout", &actions.ActionDescriptorOptions{ Command: logout, ActionResolver: newLogoutAction, }) root.Add("init", &actions.ActionDescriptorOptions{ Command: newInitCmd(), FlagsResolver: newInitFlags, ActionResolver: newInitAction, HelpOptions: actions.ActionHelpOptions{ Description: getCmdInitHelpDescription, Footer: getCmdInitHelpFooter, }, GroupingOptions: actions.CommandGroupOptions{ RootLevelHelp: actions.CmdGroupConfig, }, }) root. Add("restore", &actions.ActionDescriptorOptions{ Command: newRestoreCmd(), FlagsResolver: newRestoreFlags, ActionResolver: newRestoreAction, OutputFormats: []output.Format{output.JsonFormat, output.NoneFormat}, DefaultFormat: output.NoneFormat, HelpOptions: actions.ActionHelpOptions{ Description: getCmdRestoreHelpDescription, Footer: getCmdRestoreHelpFooter, }, GroupingOptions: actions.CommandGroupOptions{ RootLevelHelp: actions.CmdGroupConfig, }, }). UseMiddleware("hooks", middleware.NewHooksMiddleware). UseMiddleware("extensions", middleware.NewExtensionsMiddleware) root. Add("build", &actions.ActionDescriptorOptions{ Command: newBuildCmd(), FlagsResolver: newBuildFlags, ActionResolver: newBuildAction, OutputFormats: []output.Format{output.JsonFormat, output.NoneFormat}, DefaultFormat: output.NoneFormat, }). UseMiddleware("hooks", middleware.NewHooksMiddleware). UseMiddleware("extensions", middleware.NewExtensionsMiddleware) root. Add("provision", &actions.ActionDescriptorOptions{ Command: cmd.NewProvisionCmd(), FlagsResolver: cmd.NewProvisionFlags, ActionResolver: cmd.NewProvisionAction, OutputFormats: []output.Format{output.JsonFormat, output.NoneFormat}, DefaultFormat: output.NoneFormat, HelpOptions: actions.ActionHelpOptions{ Description: cmd.GetCmdProvisionHelpDescription, Footer: getCmdHelpDefaultFooter, }, GroupingOptions: actions.CommandGroupOptions{ RootLevelHelp: actions.CmdGroupManage, }, }). UseMiddlewareWhen("hooks", middleware.NewHooksMiddleware, func(descriptor *actions.ActionDescriptor) bool { if onPreview, _ := descriptor.Options.Command.Flags().GetBool("preview"); onPreview { log.Println("Skipping provision hooks due to preview flag.") return false } return true }). UseMiddlewareWhen("extensions", middleware.NewExtensionsMiddleware, func(descriptor *actions.ActionDescriptor) bool { if onPreview, _ := descriptor.Options.Command.Flags().GetBool("preview"); onPreview { log.Println("Skipping provision hooks due to preview flag.") return false } return true }) root. Add("package", &actions.ActionDescriptorOptions{ Command: newPackageCmd(), FlagsResolver: newPackageFlags, ActionResolver: newPackageAction, OutputFormats: []output.Format{output.JsonFormat, output.NoneFormat}, DefaultFormat: output.NoneFormat, HelpOptions: actions.ActionHelpOptions{ Description: getCmdPackageHelpDescription, Footer: getCmdPackageHelpFooter, }, GroupingOptions: actions.CommandGroupOptions{ RootLevelHelp: actions.CmdGroupManage, }, }). UseMiddleware("hooks", middleware.NewHooksMiddleware). UseMiddleware("extensions", middleware.NewExtensionsMiddleware) root. Add("deploy", &actions.ActionDescriptorOptions{ Command: cmd.NewDeployCmd(), FlagsResolver: cmd.NewDeployFlags, ActionResolver: cmd.NewDeployAction, OutputFormats: []output.Format{output.JsonFormat, output.NoneFormat}, DefaultFormat: output.NoneFormat, HelpOptions: actions.ActionHelpOptions{ Description: cmd.GetCmdDeployHelpDescription, Footer: cmd.GetCmdDeployHelpFooter, }, GroupingOptions: actions.CommandGroupOptions{ RootLevelHelp: actions.CmdGroupManage, }, }). UseMiddleware("hooks", middleware.NewHooksMiddleware). UseMiddleware("extensions", middleware.NewExtensionsMiddleware) root. Add("up", &actions.ActionDescriptorOptions{ Command: newUpCmd(), FlagsResolver: newUpFlags, ActionResolver: newUpAction, OutputFormats: []output.Format{output.JsonFormat, output.NoneFormat}, DefaultFormat: output.NoneFormat, HelpOptions: actions.ActionHelpOptions{ Description: getCmdUpHelpDescription, }, GroupingOptions: actions.CommandGroupOptions{ RootLevelHelp: actions.CmdGroupManage, }, }). UseMiddleware("hooks", middleware.NewHooksMiddleware). UseMiddleware("extensions", middleware.NewExtensionsMiddleware) root.Add("monitor", &actions.ActionDescriptorOptions{ Command: newMonitorCmd(), FlagsResolver: newMonitorFlags, ActionResolver: newMonitorAction, HelpOptions: actions.ActionHelpOptions{ Description: getCmdMonitorHelpDescription, Footer: getCmdMonitorHelpFooter, }, GroupingOptions: actions.CommandGroupOptions{ RootLevelHelp: actions.CmdGroupMonitor, }, }) root. Add("down", &actions.ActionDescriptorOptions{ Command: newDownCmd(), FlagsResolver: newDownFlags, ActionResolver: newDownAction, OutputFormats: []output.Format{output.JsonFormat, output.NoneFormat}, DefaultFormat: output.NoneFormat, HelpOptions: actions.ActionHelpOptions{ Description: getCmdDownHelpDescription, Footer: getCmdDownHelpFooter, }, GroupingOptions: actions.CommandGroupOptions{ RootLevelHelp: actions.CmdGroupManage, }, }). UseMiddleware("hooks", middleware.NewHooksMiddleware). UseMiddleware("extensions", middleware.NewExtensionsMiddleware) root. Add("add", &actions.ActionDescriptorOptions{ Command: add.NewAddCmd(), ActionResolver: add.NewAddAction, }) // Register any global middleware defined by the caller if len(middlewareChain) > 0 { for _, registration := range middlewareChain { root.UseMiddlewareWhen(registration.Name, registration.Resolver, registration.Predicate) } } // Global middleware registration root. UseMiddleware("debug", middleware.NewDebugMiddleware). UseMiddleware("ux", middleware.NewUxMiddleware). UseMiddlewareWhen("telemetry", middleware.NewTelemetryMiddleware, func(descriptor *actions.ActionDescriptor) bool { return !descriptor.Options.DisableTelemetry }) // Register common dependencies for the IoC rootContainer if rootContainer == nil { rootContainer = ioc.NewNestedContainer(nil) } ioc.RegisterNamedInstance(rootContainer, "root-cmd", rootCmd) registerCommonDependencies(rootContainer) // Conditionally register the 'extension' commands if the feature is enabled err := rootContainer.Invoke(func(alphaFeatureManager *alpha.FeatureManager, extensionManager *extensions.Manager) error { if alphaFeatureManager.IsEnabled(extensions.FeatureExtensions) { // Enables the "extension (ext)" command group. extensionActions(root) // Enables custom extension commands installedExtensions, err := extensionManager.ListInstalled() if err != nil { return fmt.Errorf("Failed to get installed extensions: %w", err) } // Bind custom extension commands for extensions that expose the capability for _, ext := range installedExtensions { if ext.HasCapability(extensions.CustomCommandCapability) { if err := bindExtension(rootContainer, root, ext); err != nil { return fmt.Errorf("Failed to bind extension commands: %w", err) } } } } return nil }) if err != nil { panic(err) } // Initialize the platform specific components for the IoC container // Only container resolution errors will return an error // Invalid configurations will fall back to default platform if _, err := platform.Initialize(rootContainer, azd.PlatformKindDefault); err != nil { panic(err) } // Compose the hierarchy of action descriptions into cobra commands var cobraBuilder *CobraBuilder if err := rootContainer.Resolve(&cobraBuilder); err != nil { panic(err) } cmd, err := cobraBuilder.BuildCommand(root) if err != nil { // If their is a container registration issue or similar we'll get an error at this point // Error descriptions should be clear enough to resolve the issue panic(err) } // The help template has to be set after calling `BuildCommand()` to ensure the command tree is built cmd.SetHelpTemplate(generateCmdHelp( cmd, generateCmdHelpOptions{ Description: getCmdHelpDefaultDescription, Commands: func(c *cobra.Command) string { return getCmdHelpGroupedCommands(getCmdRootHelpCommands(c)) }, Footer: getCmdRootHelpFooter, })) return cmd } func getCmdRootHelpFooter(cmd *cobra.Command) string { return fmt.Sprintf("%s\n%s\n%s\n\n%s\n\n%s", output.WithBold("%s", output.WithUnderline("Deploying a sample application")), "Initialize from a sample application by running the "+ output.WithHighLightFormat("azd init --template ")+ output.WithWarningFormat("[%s]", "template name")+" command in an empty directory.", "Then, run "+output.WithHighLightFormat("azd up")+" to get the application up-and-running in Azure.", "To view a curated list of sample templates, run "+ output.WithHighLightFormat("azd template list")+".\n"+ "To view all available sample templates, including those submitted by the azd community, visit: "+ output.WithLinkFormat("https://azure.github.io/awesome-azd")+".", getCmdHelpDefaultFooter(cmd), ) } func getCmdRootHelpCommands(cmd *cobra.Command) (result string) { childrenCommands := cmd.Commands() groups := actions.GetGroupAnnotations() var commandGroups = make(map[string][]string, len(groups)) // stores the longes line len max := 0 for _, childCommand := range childrenCommands { // we rely on commands annotations for command grouping. Commands w/o annotation are ignored. if childCommand.Annotations == nil { continue } groupType, found := actions.GetGroupCommandAnnotation(childCommand) if !found { continue } commandName := childCommand.Name() commandNameLen := len(commandName) if commandNameLen > max { max = commandNameLen } commandGroups[groupType] = append(commandGroups[groupType], fmt.Sprintf("%s%s%s", commandName, endOfTitleSentinel, childCommand.Short)) } // align all lines for id := range commandGroups { alignTitles(commandGroups[id], max) } var paragraph []string for _, title := range groups { groupCommands := commandGroups[string(title)] if len(groupCommands) == 0 { continue } paragraph = append(paragraph, fmt.Sprintf(" %s\n %s\n", output.WithBold("%s", string(title)), strings.Join(commandGroups[string(title)], "\n "))) } return strings.Join(paragraph, "\n") }