cli/azd/cmd/extension.go (737 lines of code) (raw):

// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. package cmd import ( "context" "errors" "fmt" "io" "strings" "text/tabwriter" "github.com/azure/azure-dev/cli/azd/cmd/actions" "github.com/azure/azure-dev/cli/azd/pkg/extensions" "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/spf13/cobra" ) // Register extension commands func extensionActions(root *actions.ActionDescriptor) *actions.ActionDescriptor { group := root.Add("extension", &actions.ActionDescriptorOptions{ Command: &cobra.Command{ Use: "extension", Aliases: []string{"ext"}, Short: fmt.Sprintf("Manage azd extensions. %s", output.WithWarningFormat("(Alpha)")), }, GroupingOptions: actions.CommandGroupOptions{ RootLevelHelp: actions.CmdGroupConfig, }, }) // azd extension list [--installed] group.Add("list", &actions.ActionDescriptorOptions{ Command: &cobra.Command{ Use: "list [--installed]", Short: "List available extensions.", }, OutputFormats: []output.Format{output.JsonFormat, output.TableFormat}, DefaultFormat: output.TableFormat, ActionResolver: newExtensionListAction, FlagsResolver: newExtensionListFlags, }) // azd extension show group.Add("show", &actions.ActionDescriptorOptions{ Command: &cobra.Command{ Use: "show <extension-name>", Short: "Show details for a specific extension.", Args: cobra.ExactArgs(1), }, OutputFormats: []output.Format{output.JsonFormat, output.NoneFormat}, DefaultFormat: output.NoneFormat, ActionResolver: newExtensionShowAction, }) // azd extension install <extension-name> group.Add("install", &actions.ActionDescriptorOptions{ Command: &cobra.Command{ Use: "install <extension-name>", Short: "Installs specified extensions.", }, ActionResolver: newExtensionInstallAction, FlagsResolver: newExtensionInstallFlags, }) // azd extension uninstall <extension-name> group.Add("uninstall", &actions.ActionDescriptorOptions{ Command: &cobra.Command{ Use: "uninstall <extension-name>", Short: "Uninstall specified extensions.", }, ActionResolver: newExtensionUninstallAction, FlagsResolver: newExtensionUninstallFlags, }) // azd extension upgrade <extension-name> group.Add("upgrade", &actions.ActionDescriptorOptions{ Command: &cobra.Command{ Use: "upgrade <extension-name>", Short: "Upgrade specified extensions.", }, ActionResolver: newExtensionUpgradeAction, FlagsResolver: newExtensionUpgradeFlags, }) sourceGroup := group.Add("source", &actions.ActionDescriptorOptions{ Command: &cobra.Command{ Use: "source", Short: "View and manage extension sources", }, }) sourceGroup.Add("list", &actions.ActionDescriptorOptions{ Command: &cobra.Command{ Use: "list", Short: "List extension sources", }, OutputFormats: []output.Format{output.JsonFormat, output.TableFormat}, DefaultFormat: output.TableFormat, ActionResolver: newExtensionSourceListAction, }) sourceGroup.Add("add", &actions.ActionDescriptorOptions{ Command: &cobra.Command{ Use: "add", Short: "Add an extension source with the specified name", }, ActionResolver: newExtensionSourceAddAction, FlagsResolver: newExtensionSourceAddFlags, OutputFormats: []output.Format{output.NoneFormat}, DefaultFormat: output.NoneFormat, }) sourceGroup.Add("remove", &actions.ActionDescriptorOptions{ Command: &cobra.Command{ Use: "remove <name>", Short: "Remove an extension source with the specified name", }, ActionResolver: newExtensionSourceRemoveAction, OutputFormats: []output.Format{output.NoneFormat}, DefaultFormat: output.NoneFormat, }) return group } type extensionListFlags struct { installed bool source string tags []string } func newExtensionListFlags(cmd *cobra.Command) *extensionListFlags { flags := &extensionListFlags{} cmd.Flags().BoolVar(&flags.installed, "installed", false, "List installed extensions") cmd.Flags().StringVar(&flags.source, "source", "", "Filter extensions by source") cmd.Flags().StringSliceVar(&flags.tags, "tags", nil, "Filter extensions by tags") return flags } // azd extension list [--installed] type extensionListAction struct { flags *extensionListFlags formatter output.Formatter console input.Console writer io.Writer sourceManager *extensions.SourceManager extensionManager *extensions.Manager } func newExtensionListAction( flags *extensionListFlags, formatter output.Formatter, console input.Console, writer io.Writer, sourceManager *extensions.SourceManager, extensionManager *extensions.Manager, ) actions.Action { return &extensionListAction{ flags: flags, formatter: formatter, console: console, writer: writer, sourceManager: sourceManager, extensionManager: extensionManager, } } type extensionListItem struct { Id string `json:"id"` Name string `json:"name"` Namespace string `json:"namespace"` Version string `json:"version"` Installed bool `json:"installed"` Source string `json:"source"` } func (a *extensionListAction) Run(ctx context.Context) (*actions.ActionResult, error) { options := &extensions.ListOptions{ Source: a.flags.source, Tags: a.flags.tags, } if options.Source != "" { if _, err := a.sourceManager.Get(ctx, options.Source); err != nil { return nil, fmt.Errorf("extension source '%s' not found: %w", options.Source, err) } } registryExtensions, err := a.extensionManager.ListFromRegistry(ctx, options) if err != nil { return nil, fmt.Errorf("failed listing extensions from registry: %w", err) } installedExtensions, err := a.extensionManager.ListInstalled() if err != nil { return nil, fmt.Errorf("failed listing installed extensions: %w", err) } extensionRows := []extensionListItem{} for _, extension := range registryExtensions { installedExtension, has := installedExtensions[extension.Id] installed := has && installedExtension.Source == extension.Source if a.flags.installed && !installed { continue } var version string if has { version = installedExtension.Version } else { version = extension.Versions[len(extension.Versions)-1].Version } extensionRows = append(extensionRows, extensionListItem{ Id: extension.Id, Name: extension.DisplayName, Namespace: extension.Namespace, Version: version, Source: extension.Source, Installed: installed, }) } if len(extensionRows) == 0 { if a.flags.installed { a.console.Message(ctx, output.WithWarningFormat("WARNING: No extensions installed.\n")) a.console.Message(ctx, fmt.Sprintf( "Run %s to install extensions.", output.WithHighLightFormat("azd extension install <extension-name>"), )) } else { a.console.Message(ctx, output.WithWarningFormat("WARNING: No extensions found in configured sources.\n")) a.console.Message(ctx, fmt.Sprintf( "Run %s to add a new extension source.", output.WithHighLightFormat("azd extension source add [flags]"), )) } return nil, nil } var formatErr error if a.formatter.Kind() == output.TableFormat { columns := []output.Column{ { Heading: "Id", ValueTemplate: "{{.Id}}", }, { Heading: "Name", ValueTemplate: "{{.Name}}", }, { Heading: "Version", ValueTemplate: `{{.Version}}`, }, { Heading: "Source", ValueTemplate: `{{.Source}}`, }, { Heading: "Installed", ValueTemplate: `{{.Installed}}`, }, } formatErr = a.formatter.Format(extensionRows, a.writer, output.TableFormatterOptions{ Columns: columns, }) } else { formatErr = a.formatter.Format(extensionRows, a.writer, nil) } return nil, formatErr } // azd extension show type extensionShowAction struct { args []string formatter output.Formatter writer io.Writer extensionManager *extensions.Manager } func newExtensionShowAction( args []string, formatter output.Formatter, writer io.Writer, extensionManager *extensions.Manager, ) actions.Action { return &extensionShowAction{ args: args, formatter: formatter, writer: writer, extensionManager: extensionManager, } } type extensionShowItem struct { Id string Namespace string Description string Tags []string LatestVersion string InstalledVersion string Usage string Examples []extensions.ExtensionExample } func (t *extensionShowItem) Display(writer io.Writer) error { tabs := tabwriter.NewWriter( writer, 0, output.TableTabSize, 1, output.TablePadCharacter, output.TableFlags) text := [][]string{ {"Id", ":", t.Id}, {"Namespace", ":", t.Namespace}, {"Description", ":", t.Description}, {"Latest Version", ":", t.LatestVersion}, {"Installed Version", ":", t.InstalledVersion}, {"Tags", ":", strings.Join(t.Tags, ", ")}, {"", "", ""}, {"Usage", ":", t.Usage}, {"Examples", ":", ""}, } for _, example := range t.Examples { text = append(text, []string{"", "", example.Usage}) } for _, line := range text { _, err := tabs.Write([]byte(strings.Join(line, "\t") + "\n")) if err != nil { return err } } return tabs.Flush() } func (a *extensionShowAction) Run(ctx context.Context) (*actions.ActionResult, error) { extensionId := a.args[0] registryExtension, err := a.extensionManager.GetFromRegistry(ctx, extensionId, nil) if err != nil { return nil, fmt.Errorf("failed to get extension details: %w", err) } latestVersion := registryExtension.Versions[len(registryExtension.Versions)-1] extensionDetails := extensionShowItem{ Id: registryExtension.Id, Namespace: registryExtension.Namespace, Description: registryExtension.DisplayName, Tags: registryExtension.Tags, LatestVersion: latestVersion.Version, Usage: latestVersion.Usage, Examples: latestVersion.Examples, InstalledVersion: "N/A", } installedExtension, err := a.extensionManager.GetInstalled( extensions.LookupOptions{Id: extensionId}, ) if err == nil { extensionDetails.InstalledVersion = installedExtension.Version } var formatErr error if a.formatter.Kind() == output.NoneFormat { formatErr = extensionDetails.Display(a.writer) } else { formatErr = a.formatter.Format(extensionDetails, a.writer, nil) } return nil, formatErr } type extensionInstallFlags struct { version string source string } func newExtensionInstallFlags(cmd *cobra.Command) *extensionInstallFlags { flags := &extensionInstallFlags{} cmd.Flags().StringVarP(&flags.source, "source", "s", "", "The extension source to use for installs") cmd.Flags().StringVarP(&flags.version, "version", "v", "", "The version of the extension to install") return flags } // azd extension install type extensionInstallAction struct { args []string flags *extensionInstallFlags console input.Console extensionManager *extensions.Manager } func newExtensionInstallAction( args []string, flags *extensionInstallFlags, console input.Console, extensionManager *extensions.Manager, ) actions.Action { return &extensionInstallAction{ args: args, flags: flags, console: console, extensionManager: extensionManager, } } func (a *extensionInstallAction) Run(ctx context.Context) (*actions.ActionResult, error) { a.console.MessageUxItem(ctx, &ux.MessageTitle{ Title: "Install an azd extension (azd extension install)", TitleNote: "Installs the specified extension onto the local machine", }) extensionIds := a.args if len(extensionIds) == 0 { return nil, fmt.Errorf("must specify an extension name") } if len(extensionIds) > 1 && a.flags.version != "" { return nil, fmt.Errorf("cannot specify --version flag when using multiple extensions") } for index, extensionId := range extensionIds { if index > 0 { a.console.Message(ctx, "") } stepMessage := fmt.Sprintf("Installing %s extension", output.WithHighLightFormat(extensionId)) a.console.ShowSpinner(ctx, stepMessage, input.Step) installed, err := a.extensionManager.GetInstalled(extensions.LookupOptions{ Id: extensionId, }) if err == nil { stepMessage += output.WithGrayFormat(" (version %s already installed)", installed.Version) a.console.StopSpinner(ctx, stepMessage, input.StepSkipped) continue } filterOptions := &extensions.FilterOptions{ Source: a.flags.source, Version: a.flags.version, } extensionVersion, err := a.extensionManager.Install(ctx, extensionId, filterOptions) if err != nil { a.console.StopSpinner(ctx, stepMessage, input.StepFailed) return nil, fmt.Errorf("failed to install extension: %w", err) } stepMessage += output.WithGrayFormat(" (%s)", extensionVersion.Version) a.console.StopSpinner(ctx, stepMessage, input.StepDone) a.console.Message(ctx, fmt.Sprintf(" %s %s", output.WithBold("Usage: "), extensionVersion.Usage)) a.console.Message(ctx, output.WithBold(" Examples:")) for _, example := range extensionVersion.Examples { a.console.Message(ctx, " "+output.WithHighLightFormat(example.Usage)) } } return &actions.ActionResult{ Message: &actions.ResultMessage{ Header: "Extension(s) installed successfully", }, }, nil } // azd extension uninstall type extensionUninstallFlags struct { all bool } func newExtensionUninstallFlags(cmd *cobra.Command) *extensionUninstallFlags { flags := &extensionUninstallFlags{} cmd.Flags().BoolVar(&flags.all, "all", false, "Uninstall all installed extensions") return flags } type extensionUninstallAction struct { args []string flags *extensionUninstallFlags console input.Console extensionManager *extensions.Manager } func newExtensionUninstallAction( args []string, flags *extensionUninstallFlags, console input.Console, extensionManager *extensions.Manager, ) actions.Action { return &extensionUninstallAction{ args: args, flags: flags, console: console, extensionManager: extensionManager, } } func (a *extensionUninstallAction) Run(ctx context.Context) (*actions.ActionResult, error) { if len(a.args) > 0 && a.flags.all { return nil, fmt.Errorf("cannot specify both an extension name and --all flag") } if len(a.args) == 0 && !a.flags.all { return nil, fmt.Errorf("must specify an extension name or use --all flag") } a.console.MessageUxItem(ctx, &ux.MessageTitle{ Title: "Uninstall an azd extension (azd extension uninstall)", TitleNote: "Uninstalls the specified extension from the local machine", }) extensionIds := a.args if a.flags.all { installed, err := a.extensionManager.ListInstalled() if err != nil { return nil, fmt.Errorf("failed to list installed extensions: %w", err) } extensionIds = make([]string, 0, len(installed)) for name := range installed { extensionIds = append(extensionIds, name) } } if len(extensionIds) == 0 { return nil, fmt.Errorf("no extensions to uninstall") } for _, extensionId := range extensionIds { stepMessage := fmt.Sprintf("Uninstalling %s extension", output.WithHighLightFormat(extensionId)) installed, err := a.extensionManager.GetInstalled(extensions.LookupOptions{ Id: extensionId, }) if err != nil { a.console.ShowSpinner(ctx, stepMessage, input.Step) a.console.StopSpinner(ctx, stepMessage, input.StepFailed) return nil, fmt.Errorf("failed to get installed extension: %w", err) } stepMessage += fmt.Sprintf(" (%s)", installed.Version) a.console.ShowSpinner(ctx, stepMessage, input.Step) if err := a.extensionManager.Uninstall(extensionId); err != nil { a.console.StopSpinner(ctx, stepMessage, input.StepFailed) return nil, fmt.Errorf("failed to uninstall extension: %w", err) } a.console.StopSpinner(ctx, stepMessage, input.StepDone) } return &actions.ActionResult{ Message: &actions.ResultMessage{ Header: "Extension(s) uninstalled successfully", }, }, nil } type extensionUpgradeFlags struct { version string source string all bool } func newExtensionUpgradeFlags(cmd *cobra.Command) *extensionUpgradeFlags { flags := &extensionUpgradeFlags{} cmd.Flags().StringVarP(&flags.version, "version", "v", "", "The version of the extension to upgrade to") cmd.Flags().StringVarP(&flags.source, "source", "s", "", "The extension source to use for upgrades") cmd.Flags().BoolVar(&flags.all, "all", false, "Upgrade all installed extensions") return flags } // azd extension upgrade type extensionUpgradeAction struct { args []string flags *extensionUpgradeFlags console input.Console extensionManager *extensions.Manager } func newExtensionUpgradeAction( args []string, flags *extensionUpgradeFlags, console input.Console, extensionManager *extensions.Manager, ) actions.Action { return &extensionUpgradeAction{ args: args, flags: flags, console: console, extensionManager: extensionManager, } } func (a *extensionUpgradeAction) Run(ctx context.Context) (*actions.ActionResult, error) { if len(a.args) > 0 && a.flags.all { return nil, fmt.Errorf("cannot specify both an extension name and --all flag") } if len(a.args) > 1 && a.flags.version != "" { return nil, fmt.Errorf("cannot specify --version flag when using multiple extensions") } if len(a.args) == 0 && !a.flags.all { return nil, fmt.Errorf("must specify an extension name or use --all flag") } a.console.MessageUxItem(ctx, &ux.MessageTitle{ Title: "Upgrade azd extensions (azd extension upgrade)", TitleNote: "Upgrades the specified extensions on the local machine", }) extensionIds := a.args if a.flags.all { installed, err := a.extensionManager.ListInstalled() if err != nil { return nil, fmt.Errorf("failed to list installed extensions: %w", err) } extensionIds = make([]string, 0, len(installed)) for name := range installed { extensionIds = append(extensionIds, name) } } if len(extensionIds) == 0 { return nil, fmt.Errorf("no extensions to upgrade") } for index, extensionId := range extensionIds { if index > 0 { a.console.Message(ctx, "") } stepMessage := fmt.Sprintf("Upgrading %s extension", output.WithHighLightFormat(extensionId)) a.console.ShowSpinner(ctx, stepMessage, input.Step) installed, err := a.extensionManager.GetInstalled(extensions.LookupOptions{ Id: extensionId, }) if err != nil { a.console.StopSpinner(ctx, stepMessage, input.StepFailed) return nil, fmt.Errorf("failed to get installed extension: %w", err) } filterOptions := &extensions.FilterOptions{ Source: a.flags.source, Version: a.flags.version, } extension, err := a.extensionManager.GetFromRegistry(ctx, extensionId, filterOptions) if err != nil { a.console.StopSpinner(ctx, stepMessage, input.StepFailed) return nil, fmt.Errorf("failed to get extension %s: %w", extensionId, err) } latestVersion := extension.Versions[len(extension.Versions)-1] if latestVersion.Version == installed.Version { stepMessage += output.WithGrayFormat(" (No upgrade available)") a.console.StopSpinner(ctx, stepMessage, input.StepSkipped) } else { extensionVersion, err := a.extensionManager.Upgrade(ctx, extensionId, filterOptions) if err != nil { return nil, fmt.Errorf("failed to upgrade extension: %w", err) } stepMessage += output.WithGrayFormat(" (%s)", extensionVersion.Version) a.console.StopSpinner(ctx, stepMessage, input.StepDone) a.console.Message(ctx, fmt.Sprintf(" %s %s", output.WithBold("Usage: "), extensionVersion.Usage)) a.console.Message(ctx, output.WithBold(" Examples:")) for _, example := range extensionVersion.Examples { a.console.Message(ctx, " "+output.WithHighLightFormat(example.Usage)) } } } return &actions.ActionResult{ Message: &actions.ResultMessage{ Header: "Extensions upgraded successfully", }, }, nil } type extensionSourceListAction struct { formatter output.Formatter writer io.Writer sourceManager *extensions.SourceManager } func newExtensionSourceListAction( formatter output.Formatter, writer io.Writer, sourceManager *extensions.SourceManager, ) actions.Action { return &extensionSourceListAction{ formatter: formatter, writer: writer, sourceManager: sourceManager, } } func (a *extensionSourceListAction) Run(ctx context.Context) (*actions.ActionResult, error) { sourceConfigs, err := a.sourceManager.List(ctx) if err != nil { return nil, fmt.Errorf("failed to list extension sources: %w", err) } if a.formatter.Kind() == output.TableFormat { columns := []output.Column{ { 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 } type extensionSourceAddFlags struct { name string location string kind string } func newExtensionSourceAddFlags(cmd *cobra.Command) *extensionSourceAddFlags { flags := &extensionSourceAddFlags{} cmd.Flags().StringVarP(&flags.name, "name", "n", "", "The name of the extension source") cmd.Flags().StringVarP(&flags.location, "location", "l", "", "The location of the extension source") cmd.Flags().StringVarP(&flags.kind, "type", "t", "", "The type of the extension source. Supported types are 'file' and 'url'") return flags } type extensionSourceAddAction struct { flags *extensionSourceAddFlags console input.Console sourceManager *extensions.SourceManager } func newExtensionSourceAddAction( flags *extensionSourceAddFlags, console input.Console, sourceManager *extensions.SourceManager, args []string, ) actions.Action { return &extensionSourceAddAction{ flags: flags, console: console, sourceManager: sourceManager, } } func (a *extensionSourceAddAction) Run(ctx context.Context) (*actions.ActionResult, error) { a.console.MessageUxItem(ctx, &ux.MessageTitle{ Title: "Add extension source (azd extension source add)", }) spinnerMessage := "Validating extension source" a.console.ShowSpinner(ctx, spinnerMessage, input.Step) sourceConfig := &extensions.SourceConfig{ Type: extensions.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, extensions.ErrSourceTypeInvalid) { return nil, fmt.Errorf( "extension source type '%s' is not supported. Supported types are %s", a.flags.kind, ux.ListAsText([]string{"'file'", "'url'"}), ) } return nil, fmt.Errorf("extension source validation failed: %w", err) } spinnerMessage = "Saving extension source" a.console.ShowSpinner(ctx, spinnerMessage, input.Step) err = a.sourceManager.Add(ctx, a.flags.name, sourceConfig) a.console.StopSpinner(ctx, spinnerMessage, input.GetStepResultFormat(err)) if err != nil { return nil, fmt.Errorf("failed adding extension source: %w", err) } return &actions.ActionResult{ Message: &actions.ResultMessage{ Header: fmt.Sprintf("Added azd extension source %s", a.flags.name), FollowUp: "Run `azd extension list` to see the available set of azd extensions.", }, }, nil } type extensionSourceRemoveAction struct { sourceManager *extensions.SourceManager console input.Console args []string } func newExtensionSourceRemoveAction( sourceManager *extensions.SourceManager, console input.Console, args []string, ) actions.Action { return &extensionSourceRemoveAction{ sourceManager: sourceManager, console: console, args: args, } } func (a *extensionSourceRemoveAction) Run(ctx context.Context) (*actions.ActionResult, error) { a.console.MessageUxItem(ctx, &ux.MessageTitle{ Title: "Remove extension source (azd extension source remove)", }) var key = strings.ToLower(a.args[0]) spinnerMessage := fmt.Sprintf("Removing extension 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 extension source: %w", err) } return &actions.ActionResult{ Message: &actions.ResultMessage{ Header: fmt.Sprintf("Removed azd extension source %s", key), FollowUp: fmt.Sprintf( "Add more extension sources by running %s", output.WithHighLightFormat("azd extension source add <key>"), ), }, }, nil }