Skip to content

Commit

Permalink
Extensions: Auto install extensions during azd init command (#4771)
Browse files Browse the repository at this point in the history
- Extensions can now be referenced directly within azure.yaml under the requiredVersions.extensions configuration element.
- Referenced extensions are automatically installed during azd init command when the extensions alpha feature is enabled.
- Extension dependencies now support semver constraints
- Minor updates on azd extension commands
  • Loading branch information
wbreza authored Feb 7, 2025
1 parent d3678b8 commit 445214c
Show file tree
Hide file tree
Showing 10 changed files with 387 additions and 64 deletions.
64 changes: 47 additions & 17 deletions cli/azd/cmd/extension.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ func newExtensionListFlags(cmd *cobra.Command) *extensionListFlags {
type extensionListAction struct {
flags *extensionListFlags
formatter output.Formatter
console input.Console
writer io.Writer
sourceManager *extensions.SourceManager
extensionManager *extensions.Manager
Expand All @@ -154,25 +155,28 @@ type extensionListAction struct {
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
Name string
Namespace string
Version string
Installed bool
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) {
Expand Down Expand Up @@ -217,10 +221,29 @@ func (a *extensionListAction) Run(ctx context.Context) (*actions.ActionResult, e
Name: extension.DisplayName,
Namespace: extension.Namespace,
Version: version,
Source: extension.Source,
Installed: installedExtensions[extension.Id] != nil,
})
}

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 {
Expand All @@ -237,6 +260,10 @@ func (a *extensionListAction) Run(ctx context.Context) (*actions.ActionResult, e
Heading: "Version",
ValueTemplate: `{{.Version}}`,
},
{
Heading: "Source",
ValueTemplate: `{{.Source}}`,
},
{
Heading: "Installed",
ValueTemplate: `{{.Installed}}`,
Expand Down Expand Up @@ -276,8 +303,10 @@ func newExtensionShowAction(
}

type extensionShowItem struct {
Name string
Id string
Namespace string
Description string
Tags []string
LatestVersion string
InstalledVersion string
Usage string
Expand All @@ -293,10 +322,12 @@ func (t *extensionShowItem) Display(writer io.Writer) error {
output.TablePadCharacter,
output.TableFlags)
text := [][]string{
{"Name", ":", t.Name},
{"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", ":", ""},
Expand Down Expand Up @@ -326,8 +357,10 @@ func (a *extensionShowAction) Run(ctx context.Context) (*actions.ActionResult, e
latestVersion := registryExtension.Versions[len(registryExtension.Versions)-1]

extensionDetails := extensionShowItem{
Name: registryExtension.Id,
Id: registryExtension.Id,
Namespace: registryExtension.Namespace,
Description: registryExtension.DisplayName,
Tags: registryExtension.Tags,
LatestVersion: latestVersion.Version,
Usage: latestVersion.Usage,
Examples: latestVersion.Examples,
Expand Down Expand Up @@ -715,9 +748,10 @@ type extensionSourceAddFlags struct {

func newExtensionSourceAddFlags(cmd *cobra.Command) *extensionSourceAddFlags {
flags := &extensionSourceAddFlags{}
cmd.Flags().StringVar(&flags.name, "name", "", "The name of the extension source")
cmd.Flags().StringVar(&flags.location, "location", "", "The location of the extension source")
cmd.Flags().StringVar(&flags.kind, "king", "", "The type of the extension source")
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
}
Expand All @@ -726,7 +760,6 @@ type extensionSourceAddAction struct {
flags *extensionSourceAddFlags
console input.Console
sourceManager *extensions.SourceManager
args []string
}

func newExtensionSourceAddAction(
Expand All @@ -739,7 +772,6 @@ func newExtensionSourceAddAction(
flags: flags,
console: console,
sourceManager: sourceManager,
args: args,
}
}

Expand All @@ -748,8 +780,6 @@ func (a *extensionSourceAddAction) Run(ctx context.Context) (*actions.ActionResu
Title: "Add extension source (azd extension source add)",
})

var name = strings.ToLower(a.args[0])

spinnerMessage := "Validating extension source"
a.console.ShowSpinner(ctx, spinnerMessage, input.Step)

Expand Down Expand Up @@ -777,15 +807,15 @@ func (a *extensionSourceAddAction) Run(ctx context.Context) (*actions.ActionResu
spinnerMessage = "Saving extension source"
a.console.ShowSpinner(ctx, spinnerMessage, input.Step)

err = a.sourceManager.Add(ctx, name, sourceConfig)
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", name),
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
Expand Down
96 changes: 77 additions & 19 deletions cli/azd/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/azure/azure-dev/cli/azd/pkg/environment"
"github.com/azure/azure-dev/cli/azd/pkg/environment/azdcontext"
"github.com/azure/azure-dev/cli/azd/pkg/exec"
"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/lazy"
"github.com/azure/azure-dev/cli/azd/pkg/output"
Expand Down Expand Up @@ -101,15 +102,16 @@ func (i *initFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandOpt
}

type initAction struct {
lazyAzdCtx *lazy.Lazy[*azdcontext.AzdContext]
lazyEnvManager *lazy.Lazy[environment.Manager]
console input.Console
cmdRun exec.CommandRunner
gitCli *git.Cli
flags *initFlags
repoInitializer *repository.Initializer
templateManager *templates.TemplateManager
featuresManager *alpha.FeatureManager
lazyAzdCtx *lazy.Lazy[*azdcontext.AzdContext]
lazyEnvManager *lazy.Lazy[environment.Manager]
console input.Console
cmdRun exec.CommandRunner
gitCli *git.Cli
flags *initFlags
repoInitializer *repository.Initializer
templateManager *templates.TemplateManager
featuresManager *alpha.FeatureManager
extensionsManager *extensions.Manager
}

func newInitAction(
Expand All @@ -121,17 +123,20 @@ func newInitAction(
flags *initFlags,
repoInitializer *repository.Initializer,
templateManager *templates.TemplateManager,
featuresManager *alpha.FeatureManager) actions.Action {
featuresManager *alpha.FeatureManager,
extensionsManager *extensions.Manager,
) actions.Action {
return &initAction{
lazyAzdCtx: lazyAzdCtx,
lazyEnvManager: lazyEnvManager,
console: console,
cmdRun: cmdRun,
gitCli: gitCli,
flags: flags,
repoInitializer: repoInitializer,
templateManager: templateManager,
featuresManager: featuresManager,
lazyAzdCtx: lazyAzdCtx,
lazyEnvManager: lazyEnvManager,
console: console,
cmdRun: cmdRun,
gitCli: gitCli,
flags: flags,
repoInitializer: repoInitializer,
templateManager: templateManager,
featuresManager: featuresManager,
extensionsManager: extensionsManager,
}
}

Expand Down Expand Up @@ -303,6 +308,10 @@ func (i *initAction) Run(ctx context.Context) (*actions.ActionResult, error) {
panic("unhandled init type")
}

if err := i.initializeExtensions(ctx, azdCtx); err != nil {
return nil, fmt.Errorf("initializing project extensions: %w", err)
}

return &actions.ActionResult{
Message: &actions.ResultMessage{
Header: header,
Expand Down Expand Up @@ -453,6 +462,55 @@ func (i *initAction) initializeEnv(
return env, nil
}

// initializeExtensions installs extensions specified in the project config
func (i *initAction) initializeExtensions(ctx context.Context, azdCtx *azdcontext.AzdContext) error {
if !i.featuresManager.IsEnabled(extensions.FeatureExtensions) {
return nil
}

projectConfig, err := project.Load(ctx, azdCtx.ProjectPath())
if err != nil {
return fmt.Errorf("loading project config: %w", err)
}

installedExtensions, err := i.extensionsManager.ListInstalled()
if err != nil {
return fmt.Errorf("listing installed extensions: %w", err)
}

if projectConfig.RequiredVersions != nil && len(projectConfig.RequiredVersions.Extensions) > 0 {
i.console.Message(ctx, "\nInstalling required extensions...")
}

for extensionId, versionConstraint := range projectConfig.RequiredVersions.Extensions {
stepMessage := fmt.Sprintf("Installing %s extension", output.WithHighLightFormat(extensionId))
i.console.ShowSpinner(ctx, stepMessage, input.Step)

installed, isInstalled := installedExtensions[extensionId]
if isInstalled {
stepMessage += output.WithGrayFormat(" (version %s already installed)", installed.Version)
i.console.StopSpinner(ctx, stepMessage, input.StepSkipped)
continue
} else {
installConstraint := "latest"
if versionConstraint != nil {
installConstraint = *versionConstraint
}

extensionVersion, err := i.extensionsManager.Install(ctx, extensionId, installConstraint)
if err != nil {
i.console.StopSpinner(ctx, stepMessage, input.StepFailed)
return fmt.Errorf("installing extension %s: %w", extensionId, err)
}

stepMessage += output.WithGrayFormat(" (%s)", extensionVersion.Version)
i.console.StopSpinner(ctx, stepMessage, input.StepDone)
}
}

return nil
}

func getCmdInitHelpDescription(*cobra.Command) string {
return generateCmdHelpDescription("Initialize a new application in your current directory.",
[]string{
Expand Down
3 changes: 1 addition & 2 deletions cli/azd/extensions/registry.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,7 @@
},
"version": {
"type": "string",
"description": "Required version of the dependency. Must follow semantic versioning.",
"pattern": "^\\d+\\.\\d+\\.\\d+$"
"description": "Required version of the dependency. Supports semantic versioning constraints."
}
},
"required": [
Expand Down
Loading

0 comments on commit 445214c

Please sign in to comment.