Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion cli/azd/.vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
"go.testTimeout": "10m",
"go.lintTool": "golangci-lint",
"go.lintOnSave": "package",
"cSpell.import": ["cspell.yaml"]
"cSpell.import": [
"${workspaceFolder}/.vscode/cspell.yaml"
]
}

68 changes: 40 additions & 28 deletions cli/azd/cmd/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/azure/azure-dev/cli/azd/pkg/output"
"github.com/azure/azure-dev/cli/azd/pkg/tools"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)

func envCmd(rootOptions *commands.GlobalCommandOptions) *cobra.Command {
Expand Down Expand Up @@ -169,43 +170,54 @@ func envListCmd(rootOptions *commands.GlobalCommandOptions) *cobra.Command {
}

func envNewCmd(rootOptions *commands.GlobalCommandOptions) *cobra.Command {
actionFn := func(ctx context.Context, _ *cobra.Command, args []string, azdCtx *environment.AzdContext) error {
askOne := makeAskOne(rootOptions.NoPrompt)
azCli := commands.GetAzCliFromContext(ctx)

if err := ensureProject(azdCtx.ProjectPath()); err != nil {
return err
}

if err := tools.EnsureInstalled(ctx, azCli); err != nil {
return err
}

if len(args) == 1 {
rootOptions.EnvironmentName = args[0]
}

if _, err := createAndInitEnvironment(ctx, &rootOptions.EnvironmentName, azdCtx, askOne); err != nil {
return fmt.Errorf("creating new environment: %w", err)
}

if err := azdCtx.SetDefaultEnvironmentName(rootOptions.EnvironmentName); err != nil {
return fmt.Errorf("saving default environment: %w", err)
}

return nil
}
cmd := commands.Build(
commands.ActionFunc(actionFn),
&envNewAction{rootOptions: rootOptions},
rootOptions,
"new <environment>",
"Create a new environment.",
"",
)
cmd.Args = cobra.MaximumNArgs(1)
return cmd
}

type envNewAction struct {
rootOptions *commands.GlobalCommandOptions
subscription string
location string
}

func (en *envNewAction) SetupFlags(persis *pflag.FlagSet, local *pflag.FlagSet) {
local.StringVar(&en.subscription, "subscription", "", "Name or ID of an Azure subscription to use for the new environment")
local.StringVarP(&en.location, "location", "l", "", "Azure location for the new environment")
}

func (en *envNewAction) Run(ctx context.Context, _ *cobra.Command, args []string, azdCtx *environment.AzdContext) error {
if err := ensureProject(azdCtx.ProjectPath()); err != nil {
return err
}

azCli := commands.GetAzCliFromContext(ctx)
if err := tools.EnsureInstalled(ctx, azCli); err != nil {
return err
}

askOne := makeAskOne(en.rootOptions.NoPrompt)
envSpec := environmentSpec{
environmentName: en.rootOptions.EnvironmentName,
subscription: en.subscription,
location: en.location,
}
if _, err := createAndInitEnvironment(ctx, &envSpec, azdCtx, askOne); err != nil {
return fmt.Errorf("creating new environment: %w", err)
}

if err := azdCtx.SetDefaultEnvironmentName(envSpec.environmentName); err != nil {
return fmt.Errorf("saving default environment: %w", err)
}

return nil
}

func envRefreshCmd(rootOptions *commands.GlobalCommandOptions) *cobra.Command {
actionFn := func(ctx context.Context, cmd *cobra.Command, args []string, azdCtx *environment.AzdContext) error {
azCli := commands.GetAzCliFromContext(ctx)
Expand Down
13 changes: 11 additions & 2 deletions cli/azd/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ When a template is provided, the sample code is cloned to the current directory.
type initAction struct {
template templates.Template
templateBranch string
subscription string
location string
rootOptions *commands.GlobalCommandOptions
}

Expand All @@ -62,6 +64,8 @@ func (i *initAction) SetupFlags(
) {
local.StringVarP(&i.template.Name, "template", "t", "", "The template to use when you initialize the project. You can use Full URI, <owner>/<repository>, or <repository> if it's part of the azure-samples organization.")
local.StringVarP(&i.templateBranch, "branch", "b", "", "The template branch to initialize from.")
local.StringVar(&i.subscription, "subscription", "", "Name or ID of an Azure subscription to use for the new environment")
local.StringVarP(&i.location, "location", "l", "", "Azure location for the new environment")
}

func (i *initAction) Run(ctx context.Context, _ *cobra.Command, args []string, azdCtx *environment.AzdContext) error {
Expand Down Expand Up @@ -259,12 +263,17 @@ func (i *initAction) Run(ctx context.Context, _ *cobra.Command, args []string, a
}
}

_, err = createAndInitEnvironment(ctx, &i.rootOptions.EnvironmentName, azdCtx, askOne)
envSpec := environmentSpec{
environmentName: i.rootOptions.EnvironmentName,
subscription: i.subscription,
location: i.location,
}
_, err = createAndInitEnvironment(ctx, &envSpec, azdCtx, askOne)
if err != nil {
return fmt.Errorf("loading environment: %w", err)
}

if err := azdCtx.SetDefaultEnvironmentName(i.rootOptions.EnvironmentName); err != nil {
if err := azdCtx.SetDefaultEnvironmentName(envSpec.environmentName); err != nil {
return fmt.Errorf("saving default environment: %w", err)
}

Expand Down
147 changes: 86 additions & 61 deletions cli/azd/cmd/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ import (

type Asker func(p survey.Prompt, response interface{}) error

const (
manualSubscriptionEntryOption = "Other (enter manually)"
)

func invalidEnvironmentNameMsg(environmentName string) string {
return fmt.Sprintf("environment name '%s' is invalid (it should contain only alphanumeric characters and hyphens)\n", environmentName)
}

// ensureValidEnvironmentName ensures the environment name is valid, if it is not, an error is printed
// and the user is prompted for a new name.
func ensureValidEnvironmentName(environmentName *string, askOneFn Asker) error {
Expand All @@ -40,36 +48,43 @@ func ensureValidEnvironmentName(environmentName *string, askOneFn Asker) error {
}

if !environment.IsValidEnvironmentName(*environmentName) {
fmt.Printf("environment name '%s' is invalid (it should contain only alphanumeric characters and hyphens)\n", *environmentName)
fmt.Print(invalidEnvironmentNameMsg(*environmentName))
}
}

return nil
}

type environmentSpec struct {
environmentName string
subscription string
location string
}

// createEnvironment creates a new named environment. If an environment with this name already
// exists, and error is return.
func createAndInitEnvironment(ctx context.Context, environmentName *string, azdCtx *environment.AzdContext, askOne Asker) (environment.Environment, error) {
if *environmentName != "" && !environment.IsValidEnvironmentName(*environmentName) {
fmt.Printf("environment name '%s' is invalid (it should contain only alphanumeric characters and hyphens)\n", *environmentName)
return environment.Environment{}, fmt.Errorf("environment name '%s' is invalid (it should contain only alphanumeric characters and hyphens)", *environmentName)
func createAndInitEnvironment(ctx context.Context, envSpec *environmentSpec, azdCtx *environment.AzdContext, askOne Asker) (environment.Environment, error) {
if envSpec.environmentName != "" && !environment.IsValidEnvironmentName(envSpec.environmentName) {
errMsg := invalidEnvironmentNameMsg(envSpec.environmentName)
fmt.Print(errMsg)
return environment.Environment{}, fmt.Errorf(errMsg)
}

if err := ensureValidEnvironmentName(environmentName, askOne); err != nil {
if err := ensureValidEnvironmentName(&envSpec.environmentName, askOne); err != nil {
return environment.Environment{}, err
}

// Ensure the environment does not already exist:
env, err := azdCtx.GetEnvironment(*environmentName)
env, err := azdCtx.GetEnvironment(envSpec.environmentName)
switch {
case errors.Is(err, os.ErrNotExist):
case err != nil:
return environment.Environment{}, fmt.Errorf("checking for existing environment: %w", err)
case err == nil:
return environment.Environment{}, fmt.Errorf("environment '%s' already exists", *environmentName)
return environment.Environment{}, fmt.Errorf("environment '%s' already exists", envSpec.environmentName)
}

if err := ensureEnvironmentInitialized(ctx, *environmentName, &env, askOne); err != nil {
if err := ensureEnvironmentInitialized(ctx, *envSpec, &env, askOne); err != nil {
return environment.Environment{}, fmt.Errorf("initializing environment: %w", err)
}

Expand Down Expand Up @@ -134,7 +149,7 @@ func loadOrInitEnvironment(ctx context.Context, environmentName *string, azdCtx
return environment.Environment{}, err
}

if err := ensureEnvironmentInitialized(ctx, *environmentName, &env, askOne); err != nil {
if err := ensureEnvironmentInitialized(ctx, environmentSpec{environmentName: *environmentName}, &env, askOne); err != nil {
return environment.Environment{}, fmt.Errorf("initializing environment: %w", err)
}

Expand All @@ -148,8 +163,9 @@ func loadOrInitEnvironment(ctx context.Context, environmentName *string, azdCtx
}

// ensureEnvironmentInitialized ensures the environment is initialized, i.e. it contains values for `AZURE_ENV_NAME`, `AZURE_LOCATION`, `AZURE_SUBSCRIPTION_ID` and `AZURE_PRINCIPAL_ID`.
// prompts for any missing values
func ensureEnvironmentInitialized(ctx context.Context, environmentName string, env *environment.Environment, askOne Asker) error {
// It will use the values from the "environment spec" passed in, and prompt for any missing values as necessary.
// Existing environment value are left unchanged, even if the "spec" has different values.
func ensureEnvironmentInitialized(ctx context.Context, envSpec environmentSpec, env *environment.Environment, askOne Asker) error {
if env.Values == nil {
env.Values = make(map[string]string)
}
Expand All @@ -168,64 +184,37 @@ func ensureEnvironmentInitialized(ctx context.Context, environmentName string, e
return nil
}

if !hasEnvName {
env.SetEnvName(environmentName)
if !hasEnvName && envSpec.environmentName != "" {
env.SetEnvName(envSpec.environmentName)
}

if !hasSubID || !hasPrincipalID || !hasLocation {
needAzureInteraction := !hasSubID || !hasLocation || !hasPrincipalID
if needAzureInteraction {
if err := ensureLoggedIn(ctx); err != nil {
return fmt.Errorf("logging in: %w", err)
}
}

if !hasLocation {
var location string
if !hasLocation && envSpec.location != "" {
env.SetLocation(envSpec.location)
} else {
location, err := promptLocation(ctx, "Please select an Azure location to use:", askOne)
if err != nil {
return fmt.Errorf("prompting for location: %w", err)
}

env.Values[environment.LocationEnvVarName] = strings.TrimSpace(location)
env.SetLocation(strings.TrimSpace(location))
}

azCli := commands.GetAzCliFromContext(ctx)

if !hasSubID {
subscriptionInfos, err := azCli.ListAccounts(ctx)
if !hasSubID && envSpec.subscription != "" {
env.SetSubscriptionId(envSpec.subscription)
} else {
subscriptionOptions, defaultSubscription, err := getSubscriptionOptions(ctx)
if err != nil {
return fmt.Errorf("listing accounts: %w", err)
return err
}

sort.Sort(azureutil.Subs(subscriptionInfos))

// If `AZURE_SUBSCRIPTION_ID` is set in the environment, use it to influence
// the default option in our prompt. Fall back to the what the `az` CLI is
// configured to use if the environment variable is unset.
defaultSubscriptionId := os.Getenv(environment.SubscriptionIdEnvVarName)
if defaultSubscriptionId == "" {
for _, info := range subscriptionInfos {
if info.IsDefault {
defaultSubscriptionId = info.Id
}
}
}

var subscriptionOptions = make([]string, len(subscriptionInfos)+1)
var defaultSubscription string

for index, info := range subscriptionInfos {
subscriptionOptions[index] = fmt.Sprintf("%2d. %s (%s)", index+1, info.Name, info.Id)

if info.Id == defaultSubscriptionId {
defaultSubscription = subscriptionOptions[index]
}
}

subscriptionOptions[len(subscriptionOptions)-1] = "Other (enter manually)"

var subscriptionId string

for env.GetSubscriptionId() == "" {
var subscriptionId = ""
for subscriptionId == "" {
var subscriptionSelection string
err := askOne(&survey.Select{
Message: "Please select an Azure Subscription to use:",
Expand All @@ -235,28 +224,28 @@ func ensureEnvironmentInitialized(ctx context.Context, environmentName string, e
if err != nil {
return fmt.Errorf("reading subscription id: %w", err)
}
if subscriptionSelection == "Other (enter manually)" {

if subscriptionSelection == manualSubscriptionEntryOption {
err = askOne(&survey.Input{
Message: "Enter an Azure Subscription to use:",
}, &subscriptionId)
if err != nil {
return fmt.Errorf("reading subscription id: %w", err)
}
} else {
subscriptionId = subscriptionSelection[len(subscriptionSelection)-len("(059cdffa-0e5b-47d8-ad4b-f13fd9099f21)")+1 : len(subscriptionSelection)-1]
subscriptionId = subscriptionSelection[len(subscriptionSelection)-len("(00000000-0000-0000-0000-000000000000)")+1 : len(subscriptionSelection)-1]
}

env.Values[environment.SubscriptionIdEnvVarName] = strings.TrimSpace(subscriptionId)
}

env.SetSubscriptionId(strings.TrimSpace(subscriptionId))
}

if !hasPrincipalID {
principalId, err := azureutil.GetCurrentPrincipalId(ctx)
principalID, err := azureutil.GetCurrentPrincipalId(ctx)
if err != nil {
return fmt.Errorf("fetching current user information: %w", err)
}

env.Values[environment.PrincipalIdEnvVarName] = principalId
env.SetPrincipalId(principalID)
}

if err := env.Save(); err != nil {
Expand All @@ -266,6 +255,42 @@ func ensureEnvironmentInitialized(ctx context.Context, environmentName string, e
return nil
}

func getSubscriptionOptions(ctx context.Context) ([]string, string, error) {
azCli := commands.GetAzCliFromContext(ctx)
subscriptionInfos, err := azCli.ListAccounts(ctx)
if err != nil {
return nil, "", fmt.Errorf("listing accounts: %w", err)
}

sort.Sort(azureutil.Subs(subscriptionInfos))

// If `AZURE_SUBSCRIPTION_ID` is set in the environment, use it to influence
// the default option in our prompt. Fall back to the what the `az` CLI is
// configured to use if the environment variable is unset.
defaultSubscriptionId := os.Getenv(environment.SubscriptionIdEnvVarName)
if defaultSubscriptionId == "" {
for _, info := range subscriptionInfos {
if info.IsDefault {
defaultSubscriptionId = info.Id
}
}
}

var subscriptionOptions = make([]string, len(subscriptionInfos)+1)
var defaultSubscription string = ""

for index, info := range subscriptionInfos {
subscriptionOptions[index] = fmt.Sprintf("%2d. %s (%s)", index+1, info.Name, info.Id)

if info.Id == defaultSubscriptionId {
defaultSubscription = subscriptionOptions[index]
}
}

subscriptionOptions[len(subscriptionOptions)-1] = manualSubscriptionEntryOption
return subscriptionOptions, defaultSubscription, nil
}

func makeAskOne(noPrompt bool) Asker {
if noPrompt {
return askOneNoPrompt
Expand Down
8 changes: 8 additions & 0 deletions cli/azd/pkg/environment/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,11 @@ func (e *Environment) GetTenantId() string {
func (e *Environment) SetSubscriptionId(id string) {
e.Values[SubscriptionIdEnvVarName] = id
}

func (e *Environment) SetLocation(location string) {
e.Values[LocationEnvVarName] = location
}

func (e *Environment) SetPrincipalId(principalID string) {
e.Values[PrincipalIdEnvVarName] = principalID
}