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
5 changes: 3 additions & 2 deletions cli/azd/cmd/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,8 @@ func (e *envSetSecretAction) Run(ctx context.Context) (*actions.ActionResult, er
}

if willCreateNewKvAccount {
location, err := e.prompter.PromptLocation(ctx, subId, "Select the location to create the Key Vault", nil)
location, err := e.prompter.PromptLocation(
ctx, subId, "Select the location to create the Key Vault", nil, nil)
if err != nil {
return nil, fmt.Errorf("prompting for Key Vault location: %w", err)
}
Expand Down Expand Up @@ -788,7 +789,7 @@ func (ef *envRefreshAction) Run(ctx context.Context) (*actions.ActionResult, err
if errors.Is(err, bicep.ErrEnsureEnvPreReqBicepCompileFailed) {
// If bicep is not available, we continue to prompt for subscription and location unfiltered
err = provisioning.EnsureSubscriptionAndLocation(ctx, ef.envManager, ef.env, ef.prompters,
func(_ account.Location) bool { return true })
provisioning.EnsureSubscriptionAndLocationOptions{})
if err != nil {
return nil, err
}
Expand Down
3 changes: 2 additions & 1 deletion cli/azd/cmd/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ func (u *upAction) Run(ctx context.Context) (*actions.ActionResult, error) {
err = u.provisioningManager.Initialize(ctx, u.projectConfig.Path, infra.Options)
if errors.Is(err, bicep.ErrEnsureEnvPreReqBicepCompileFailed) {
// If bicep is not available, we continue to prompt for subscription and location unfiltered
err = provisioning.EnsureSubscriptionAndLocation(ctx, u.envManager, u.env, u.prompters, nil)
err = provisioning.EnsureSubscriptionAndLocation(
ctx, u.envManager, u.env, u.prompters, provisioning.EnsureSubscriptionAndLocationOptions{})
if err != nil {
return nil, err
}
Expand Down
3 changes: 2 additions & 1 deletion cli/azd/internal/cmd/add/add_preview.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ func (a *AddAction) previewProvision(
usedBy []string,
) error {
a.console.ShowSpinner(ctx, "Previewing changes....", input.Step)
err := provisioning.EnsureSubscriptionAndLocation(ctx, a.envManager, a.env, a.prompter, nil)
err := provisioning.EnsureSubscriptionAndLocation(
ctx, a.envManager, a.env, a.prompter, provisioning.EnsureSubscriptionAndLocationOptions{})
if err != nil {
return err
}
Expand Down
3 changes: 2 additions & 1 deletion cli/azd/internal/cmd/add/add_select_ai.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ func (a *AddAction) selectOpenAi(

var allModels []ModelList
for {
err = provisioning.EnsureSubscriptionAndLocation(ctx, a.envManager, a.env, a.prompter, nil)
err = provisioning.EnsureSubscriptionAndLocation(
ctx, a.envManager, a.env, a.prompter, provisioning.EnsureSubscriptionAndLocationOptions{})
if err != nil {
return nil, err
}
Expand Down
1 change: 1 addition & 0 deletions cli/azd/pkg/azure/arm_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ type AzdMetadata struct {
Type *AzdMetadataType `json:"type,omitempty"`
AutoGenerateConfig *AutoGenInput `json:"config,omitempty"`
DefaultValueExpr *string `json:"defaultValueExpr,omitempty"`
Default *string `json:"default,omitempty"`
Comment thread
vhvb1989 marked this conversation as resolved.
}

// Description returns the value of the "Description" string metadata for this parameter or empty if it can not be found.
Expand Down
11 changes: 11 additions & 0 deletions cli/azd/pkg/azureutil/location.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ func PromptLocationWithFilter(
console input.Console,
accountManager account.Manager,
shouldDisplay func(account.Location) bool,
defaultSelectedLocation *string,
) (string, error) {
allLocations, err := accountManager.GetLocations(ctx, subscriptionId)
if err != nil {
Expand All @@ -47,10 +48,20 @@ func PromptLocationWithFilter(
strings.ToLower(a.RegionalDisplayName), strings.ToLower(b.RegionalDisplayName))
})

// Default location selection.
// The order of precedence for selecting the default location is as follows:
// 1. The location set in the system environment. (AZURE_LOCATION) -> CI/CD strategy
// 2. Parameter passed to the function. (defaultSelectedLocation != nil)
// 3. The location set in the azd config. -> CI/CD strategy

// Allow the environment variable `AZURE_LOCATION` to control the default value for the location
// selection.
defaultLocation := os.Getenv(environment.LocationEnvVarName)

if defaultLocation == "" && defaultSelectedLocation != nil {
defaultLocation = *defaultSelectedLocation
}

// If no location is set in the process environment, see what the azd config default is.
if defaultLocation == "" {
defaultLocation = accountManager.GetDefaultLocationName(ctx)
Expand Down
62 changes: 50 additions & 12 deletions cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@ func (p *BicepProvider) EnsureEnv(ctx context.Context) error {
// for .bicepparam, we first prompt for environment values before calling compiling bicepparam file
// which can reference these values
if isBicepParamFile(modulePath) {
if err := provisioning.EnsureSubscriptionAndLocation(ctx, p.envManager, p.env, p.prompters, nil); err != nil {
if err := provisioning.EnsureSubscriptionAndLocation(
ctx, p.envManager, p.env, p.prompters, provisioning.EnsureSubscriptionAndLocationOptions{}); err != nil {
return err
}
}
Expand All @@ -130,21 +131,23 @@ func (p *BicepProvider) EnsureEnv(ctx context.Context) error {
return fmt.Errorf("%w%w", ErrEnsureEnvPreReqBicepCompileFailed, compileErr)
}

// for .bicep, azd must load a parameters.json file and create the ArmParameters
// for .bicep, azd must load a parameters.json file and create the ArmParameters so we know if the are filters
// to apply for location (using the allowedValues or the location azd metadata)
if isBicepFile(modulePath) {
locationParam, locationParamDefined := compileResult.Template.Parameters["location"]
var filterLocation = func(loc account.Location) bool {
if locationParam, defined := compileResult.Template.Parameters["location"]; defined {
if locationParam.AllowedValues != nil {
return slices.IndexFunc(*locationParam.AllowedValues, func(allowedValue any) bool {
allowedValueString, goodCast := allowedValue.(string)
return goodCast && loc.Name == allowedValueString
}) != -1
}
}
return true
return locationParameterFilterImpl(locationParam, loc)
}
var defaultLocationToSelect *string
if locationParamDefined {
defaultLocationToSelect = defaultPromptValue(locationParam)
}

err := provisioning.EnsureSubscriptionAndLocation(ctx, p.envManager, p.env, p.prompters, filterLocation)
err := provisioning.EnsureSubscriptionAndLocation(
ctx, p.envManager, p.env, p.prompters, provisioning.EnsureSubscriptionAndLocationOptions{
LocationFiler: filterLocation,
SelectDefaultLocation: defaultLocationToSelect,
})
if err != nil {
return err
}
Expand Down Expand Up @@ -176,6 +179,41 @@ func (p *BicepProvider) EnsureEnv(ctx context.Context) error {
return nil
}

func locationParameterFilterImpl(param azure.ArmTemplateParameterDefinition, location account.Location) bool {
if param.AllowedValues == nil {
return true
}

return slices.IndexFunc(*param.AllowedValues, func(v any) bool {
s, ok := v.(string)
return ok && location.Name == s
}) != -1
}

// defaultPromptValue resolves if there is an intention from a location parameter to use a default location.
//
// If the parameter has AzdMetadataTypeLocation, with a default location set, the default location is returned.
// If the parameter has AllowedValues, the first option value is returned.
// Otherwise, nil is returned to indicate no user-provided default value.
func defaultPromptValue(locationParam azure.ArmTemplateParameterDefinition) *string {
azdMetadata, has := locationParam.AzdMetadata()
if has &&
azdMetadata.Type != nil && *azdMetadata.Type == azure.AzdMetadataTypeLocation &&
azdMetadata.Default != nil {
// Metadata using location type and a default location. This is the highest priority.
return azdMetadata.Default
}

if locationParam.AllowedValues != nil {
firstOption, castOk := (*locationParam.AllowedValues)[0].(string)
// if cast doesn't work, we don't have a default location
if castOk {
return &firstOption
}
}
return nil
}

func (p *BicepProvider) LastDeployment(ctx context.Context) (*azapi.ResourceDeployment, error) {
modulePath := p.modulePath()
compileResult, err := p.compileBicep(ctx, modulePath)
Expand Down
51 changes: 51 additions & 0 deletions cli/azd/pkg/infra/provisioning/bicep/bicep_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1355,3 +1355,54 @@ func TestInputsParameter(t *testing.T) {

require.Equal(t, expectedInputsUpdated, inputsUpdated)
}
func TestDefaultLocationToSelectFn(t *testing.T) {
t.Run("NoAllowedValuesOrMetadata", func(t *testing.T) {
param := azure.ArmTemplateParameterDefinition{}
result := defaultPromptValue(param)
require.Nil(t, result)
})

t.Run("AllowedValuesOnly", func(t *testing.T) {
param := azure.ArmTemplateParameterDefinition{
AllowedValues: &[]any{"eastus", "westus"},
}
result := defaultPromptValue(param)
require.NotNil(t, result)
require.Equal(t, "eastus", *result)
})

t.Run("MetadataOnly", func(t *testing.T) {
defaultLocation := "centralus"
param := azure.ArmTemplateParameterDefinition{
Metadata: map[string]json.RawMessage{
"azd": json.RawMessage(`{"type": "location", "default": "centralus"}`),
},
}
result := defaultPromptValue(param)
require.NotNil(t, result)
require.Equal(t, defaultLocation, *result)
})

t.Run("AllowedValuesAndMetadata", func(t *testing.T) {
defaultLocation := "centralus"
param := azure.ArmTemplateParameterDefinition{
AllowedValues: &[]any{"eastus", "westus"},
Metadata: map[string]json.RawMessage{
"azd": json.RawMessage(`{"type": "location", "default": "centralus"}`),
},
}
result := defaultPromptValue(param)
require.NotNil(t, result)
require.Equal(t, defaultLocation, *result)
})

t.Run("InvalidMetadata", func(t *testing.T) {
param := azure.ArmTemplateParameterDefinition{
Metadata: map[string]json.RawMessage{
"azd": json.RawMessage(`{"type": "location"}`),
},
}
result := defaultPromptValue(param)
require.Nil(t, result)
})
}
13 changes: 2 additions & 11 deletions cli/azd/pkg/infra/provisioning/bicep/prompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"context"
"encoding/json"
"fmt"
"slices"
"strconv"

"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
Expand Down Expand Up @@ -97,16 +96,8 @@ func (p *BicepProvider) promptForParameter(
*azdMetadata.Type == azure.AzdMetadataTypeLocation {

location, err := p.prompters.PromptLocation(ctx, p.env.GetSubscriptionId(), msg, func(loc account.Location) bool {
if param.AllowedValues == nil {
return true
}

return slices.IndexFunc(*param.AllowedValues, func(v any) bool {
s, ok := v.(string)
return ok && loc.Name == s
}) != -1
},
)
return locationParameterFilterImpl(param, loc)
}, defaultPromptValue(param))
if err != nil {
return nil, err
}
Expand Down
12 changes: 10 additions & 2 deletions cli/azd/pkg/infra/provisioning/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,13 @@ func (m *Manager) UpdateEnvironment(
return nil
}

type EnsureSubscriptionAndLocationOptions struct {
// LocationFilterPredicate is a function to filter the locations being displayed if prompting the user for the location.
LocationFiler prompt.LocationFilterPredicate
// SelectDefaultLocation is the default location that azd mark as selected when prompting the user for the location.
SelectDefaultLocation *string
}

// EnsureSubscriptionAndLocation ensures that that that subscription (AZURE_SUBSCRIPTION_ID) and location (AZURE_LOCATION)
// variables are set in the environment, prompting the user for the values if they do not exist.
// locationFilter, when non-nil, filters the locations being displayed.
Expand All @@ -340,7 +347,7 @@ func EnsureSubscriptionAndLocation(
envManager environment.Manager,
env *environment.Environment,
prompter prompt.Prompter,
locationFiler prompt.LocationFilterPredicate,
options EnsureSubscriptionAndLocationOptions,
) error {
subId := env.GetSubscriptionId()
if subId == "" {
Expand All @@ -366,7 +373,8 @@ func EnsureSubscriptionAndLocation(
ctx,
env.GetSubscriptionId(),
"Select an Azure location to use:",
locationFiler,
options.LocationFiler,
options.SelectDefaultLocation,
)
if err != nil {
return err
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ func (t *TerraformProvider) EnsureEnv(ctx context.Context) error {
t.envManager,
t.env,
t.prompters,
nil,
provisioning.EnsureSubscriptionAndLocationOptions{},
)
}

Expand Down
3 changes: 1 addition & 2 deletions cli/azd/pkg/infra/provisioning/test/test_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"context"
"errors"

"github.com/azure/azure-dev/cli/azd/pkg/account"
"github.com/azure/azure-dev/cli/azd/pkg/environment"
"github.com/azure/azure-dev/cli/azd/pkg/infra/provisioning"
"github.com/azure/azure-dev/cli/azd/pkg/input"
Expand Down Expand Up @@ -54,7 +53,7 @@ func (t *TestProvider) EnsureEnv(ctx context.Context) error {
t.envManager,
t.env,
t.prompters,
func(_ account.Location) bool { return true },
provisioning.EnsureSubscriptionAndLocationOptions{},
)
}

Expand Down
10 changes: 8 additions & 2 deletions cli/azd/pkg/prompt/prompter.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ type LocationFilterPredicate func(loc account.Location) bool

type Prompter interface {
PromptSubscription(ctx context.Context, msg string) (subscriptionId string, err error)
PromptLocation(ctx context.Context, subId string, msg string, filter LocationFilterPredicate) (string, error)
PromptLocation(
ctx context.Context,
subId string,
msg string,
filter LocationFilterPredicate,
defaultLocation *string) (string, error)
PromptResourceGroup(ctx context.Context) (string, error)
PromptResourceGroupFrom(
ctx context.Context, subscriptionId string, location string, options PromptResourceGroupFromOptions) (string, error)
Expand Down Expand Up @@ -101,8 +106,9 @@ func (p *DefaultPrompter) PromptLocation(
subId string,
msg string,
filter LocationFilterPredicate,
defaultLocation *string,
) (string, error) {
loc, err := azureutil.PromptLocationWithFilter(ctx, subId, msg, "", p.console, p.accountManager, filter)
loc, err := azureutil.PromptLocationWithFilter(ctx, subId, msg, "", p.console, p.accountManager, filter, defaultLocation)
if err != nil {
return "", err
}
Expand Down