diff --git a/cli/azd/cmd/env.go b/cli/azd/cmd/env.go index 2d2163f75c6..d26557096ce 100644 --- a/cli/azd/cmd/env.go +++ b/cli/azd/cmd/env.go @@ -896,7 +896,7 @@ func (f *envNewFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandO &f.subscription, "subscription", "", - "Name or ID of an Azure subscription to use for the new environment", + "ID of an Azure subscription to use for the new environment", ) local.StringVarP(&f.location, "location", "l", "", "Azure location for the new environment") diff --git a/cli/azd/cmd/init.go b/cli/azd/cmd/init.go index aa8ced7ee95..9e74c0d3060 100644 --- a/cli/azd/cmd/init.go +++ b/cli/azd/cmd/init.go @@ -99,7 +99,7 @@ func (i *initFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandOpt "subscription", "s", "", - "Name or ID of an Azure subscription to use for the new environment", + "ID of an Azure subscription to use for the new environment", ) local.BoolVarP( &i.fromCode, diff --git a/cli/azd/cmd/testdata/TestFigSpec.ts b/cli/azd/cmd/testdata/TestFigSpec.ts index 57584b6d5b5..6fbc54fde59 100644 --- a/cli/azd/cmd/testdata/TestFigSpec.ts +++ b/cli/azd/cmd/testdata/TestFigSpec.ts @@ -1221,7 +1221,7 @@ const completionSpec: Fig.Spec = { }, { name: ['--subscription'], - description: 'Name or ID of an Azure subscription to use for the new environment', + description: 'ID of an Azure subscription to use for the new environment', args: [ { name: 'subscription', @@ -1678,7 +1678,7 @@ const completionSpec: Fig.Spec = { }, { name: ['--subscription', '-s'], - description: 'Name or ID of an Azure subscription to use for the new environment', + description: 'ID of an Azure subscription to use for the new environment', args: [ { name: 'subscription', @@ -2054,6 +2054,15 @@ const completionSpec: Fig.Spec = { }, ], }, + { + name: ['--location', '-l'], + description: 'Azure location for the new environment', + args: [ + { + name: 'location', + }, + ], + }, { name: ['--no-state'], description: '(Bicep only) Forces a fresh deployment based on current Bicep template files, ignoring any stored deployment state.', @@ -2062,6 +2071,15 @@ const completionSpec: Fig.Spec = { name: ['--preview'], description: 'Preview changes to Azure resources.', }, + { + name: ['--subscription'], + description: 'ID of an Azure subscription to use for the new environment', + args: [ + { + name: 'subscription', + }, + ], + }, ], args: { name: 'layer', @@ -2262,6 +2280,24 @@ const completionSpec: Fig.Spec = { }, ], }, + { + name: ['--location', '-l'], + description: 'Azure location for the new environment', + args: [ + { + name: 'location', + }, + ], + }, + { + name: ['--subscription'], + description: 'ID of an Azure subscription to use for the new environment', + args: [ + { + name: 'subscription', + }, + ], + }, ], }, { diff --git a/cli/azd/cmd/testdata/TestUsage-azd-env-new.snap b/cli/azd/cmd/testdata/TestUsage-azd-env-new.snap index 91ae8cdbbd8..847ed048966 100644 --- a/cli/azd/cmd/testdata/TestUsage-azd-env-new.snap +++ b/cli/azd/cmd/testdata/TestUsage-azd-env-new.snap @@ -6,7 +6,7 @@ Usage Flags -l, --location string : Azure location for the new environment - --subscription string : Name or ID of an Azure subscription to use for the new environment + --subscription string : ID of an Azure subscription to use for the new environment Global Flags -C, --cwd string : Sets the current working directory. diff --git a/cli/azd/cmd/testdata/TestUsage-azd-init.snap b/cli/azd/cmd/testdata/TestUsage-azd-init.snap index 2b7e020c7b5..d1f9d0ca2f6 100644 --- a/cli/azd/cmd/testdata/TestUsage-azd-init.snap +++ b/cli/azd/cmd/testdata/TestUsage-azd-init.snap @@ -14,7 +14,7 @@ Flags --from-code : Initializes a new application from your existing code. -l, --location string : Azure location for the new environment -m, --minimal : Initializes a minimal project. - -s, --subscription string : Name or ID of an Azure subscription to use for the new environment + -s, --subscription string : ID of an Azure subscription to use for the new environment -t, --template string : Initializes a new application from a template. You can use Full URI, /, or if it's part of the azure-samples organization. --up : Provision and deploy to Azure after initializing the project from a template. diff --git a/cli/azd/cmd/testdata/TestUsage-azd-provision.snap b/cli/azd/cmd/testdata/TestUsage-azd-provision.snap index e9772773942..66f4ef16db9 100644 --- a/cli/azd/cmd/testdata/TestUsage-azd-provision.snap +++ b/cli/azd/cmd/testdata/TestUsage-azd-provision.snap @@ -12,9 +12,11 @@ Usage azd provision [] [flags] Flags - -e, --environment string : The name of the environment to use. - --no-state : (Bicep only) Forces a fresh deployment based on current Bicep template files, ignoring any stored deployment state. - --preview : Preview changes to Azure resources. + -e, --environment string : The name of the environment to use. + -l, --location string : Azure location for the new environment + --no-state : (Bicep only) Forces a fresh deployment based on current Bicep template files, ignoring any stored deployment state. + --preview : Preview changes to Azure resources. + --subscription string : ID of an Azure subscription to use for the new environment Global Flags -C, --cwd string : Sets the current working directory. diff --git a/cli/azd/cmd/testdata/TestUsage-azd-up.snap b/cli/azd/cmd/testdata/TestUsage-azd-up.snap index dfccf2941ab..bd2694a3a41 100644 --- a/cli/azd/cmd/testdata/TestUsage-azd-up.snap +++ b/cli/azd/cmd/testdata/TestUsage-azd-up.snap @@ -20,7 +20,9 @@ Usage azd up [flags] Flags - -e, --environment string : The name of the environment to use. + -e, --environment string : The name of the environment to use. + -l, --location string : Azure location for the new environment + --subscription string : ID of an Azure subscription to use for the new environment Global Flags -C, --cwd string : Sets the current working directory. diff --git a/cli/azd/cmd/up.go b/cli/azd/cmd/up.go index df9f8bea434..7a99489c1a0 100644 --- a/cli/azd/cmd/up.go +++ b/cli/azd/cmd/up.go @@ -103,6 +103,34 @@ func newUpAction( } func (u *upAction) Run(ctx context.Context) (*actions.ActionResult, error) { + // Apply --subscription and --location flags to the environment before provisioning + updatedEnv := false + if flagSub := u.flags.ProvisionFlags.Subscription(); flagSub != "" { + if existing := u.env.GetSubscriptionId(); existing != "" && existing != flagSub { + return nil, fmt.Errorf( + "cannot change subscription for existing environment '%s' (current: %s, requested: %s). "+ + "Create a new environment with 'azd env new' instead", + u.env.Name(), existing, flagSub) + } + u.env.SetSubscriptionId(flagSub) + updatedEnv = true + } + if flagLoc := u.flags.ProvisionFlags.Location(); flagLoc != "" { + if existing := u.env.GetLocation(); existing != "" && existing != flagLoc { + return nil, fmt.Errorf( + "cannot change location for existing environment '%s' (current: %s, requested: %s). "+ + "Create a new environment with 'azd env new' instead", + u.env.Name(), existing, flagLoc) + } + u.env.SetLocation(flagLoc) + updatedEnv = true + } + if updatedEnv { + if err := u.envManager.Save(ctx, u.env); err != nil { + return nil, fmt.Errorf("saving environment: %w", err) + } + } + infra, err := u.importManager.ProjectInfrastructure(ctx, u.projectConfig) if err != nil { return nil, err diff --git a/cli/azd/internal/cmd/provision.go b/cli/azd/internal/cmd/provision.go index de30fcb5ced..c47a0353a13 100644 --- a/cli/azd/internal/cmd/provision.go +++ b/cli/azd/internal/cmd/provision.go @@ -34,6 +34,8 @@ type ProvisionFlags struct { noProgress bool preview bool ignoreDeploymentState bool + subscription string + location string global *internal.GlobalCommandOptions *internal.EnvFlag } @@ -55,9 +57,26 @@ func (i *ProvisionFlags) BindNonCommon(local *pflag.FlagSet, global *internal.Gl local.BoolVar(&i.noProgress, "no-progress", false, "Suppresses progress information.") //deprecate:Flag hide --no-progress _ = local.MarkHidden("no-progress") + local.StringVar( + &i.subscription, + "subscription", + "", + "ID of an Azure subscription to use for the new environment", + ) + local.StringVarP(&i.location, "location", "l", "", "Azure location for the new environment") i.global = global } +// Subscription returns the value of the --subscription flag. +func (i *ProvisionFlags) Subscription() string { + return i.subscription +} + +// Location returns the value of the --location flag. +func (i *ProvisionFlags) Location() string { + return i.location +} + func (i *ProvisionFlags) bindCommon(local *pflag.FlagSet, global *internal.GlobalCommandOptions) { local.BoolVar(&i.preview, "preview", false, "Preview changes to Azure resources.") local.BoolVar( @@ -199,6 +218,34 @@ func (p *ProvisionAction) Run(ctx context.Context) (*actions.ActionResult, error return nil, err } + // Apply --subscription and --location flags to the environment before provisioning + envChanged := false + if p.flags.subscription != "" { + if existing := p.env.GetSubscriptionId(); existing != "" && existing != p.flags.subscription { + return nil, fmt.Errorf( + "cannot change subscription for existing environment '%s' (current: %s, requested: %s). "+ + "Create a new environment with 'azd env new' instead", + p.env.Name(), existing, p.flags.subscription) + } + p.env.SetSubscriptionId(p.flags.subscription) + envChanged = true + } + if p.flags.location != "" { + if existing := p.env.GetLocation(); existing != "" && existing != p.flags.location { + return nil, fmt.Errorf( + "cannot change location for existing environment '%s' (current: %s, requested: %s). "+ + "Create a new environment with 'azd env new' instead", + p.env.Name(), existing, p.flags.location) + } + p.env.SetLocation(p.flags.location) + envChanged = true + } + if envChanged { + if err := p.envManager.Save(ctx, p.env); err != nil { + return nil, fmt.Errorf("saving environment: %w", err) + } + } + infra, err := p.importManager.ProjectInfrastructure(ctx, p.projectConfig) if err != nil { return nil, err diff --git a/cli/azd/internal/cmd/provision_guard_test.go b/cli/azd/internal/cmd/provision_guard_test.go new file mode 100644 index 00000000000..af3df3417a8 --- /dev/null +++ b/cli/azd/internal/cmd/provision_guard_test.go @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package cmd + +import ( + "fmt" + "testing" + + "github.com/azure/azure-dev/cli/azd/pkg/environment" + "github.com/stretchr/testify/require" +) + +func Test_EnvFlagGuard(t *testing.T) { + tests := []struct { + name string + existingSub string + existingLoc string + flagSub string + flagLoc string + wantSubErr bool + wantLocErr bool + wantSubErrMsg string + wantLocErrMsg string + }{ + { + name: "EmptyEnv_NewFlags_Allowed", + existingSub: "", + existingLoc: "", + flagSub: "new-sub-id", + flagLoc: "eastus", + }, + { + name: "SameValues_NoOp", + existingSub: "sub-123", + existingLoc: "westus2", + flagSub: "sub-123", + flagLoc: "westus2", + }, + { + name: "DifferentSub_Error", + existingSub: "sub-123", + existingLoc: "westus2", + flagSub: "sub-456", + flagLoc: "", + wantSubErr: true, + wantSubErrMsg: "cannot change subscription", + }, + { + name: "DifferentLoc_Error", + existingSub: "sub-123", + existingLoc: "westus2", + flagSub: "", + flagLoc: "eastus", + wantLocErr: true, + wantLocErrMsg: "cannot change location", + }, + { + name: "DifferentBoth_SubErrorFirst", + existingSub: "sub-123", + existingLoc: "westus2", + flagSub: "sub-456", + flagLoc: "eastus", + wantSubErr: true, + wantSubErrMsg: "cannot change subscription", + }, + { + name: "NoFlags_NoChange", + existingSub: "sub-123", + existingLoc: "westus2", + flagSub: "", + flagLoc: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + env := environment.New("test-env") + if tt.existingSub != "" { + env.SetSubscriptionId(tt.existingSub) + } + if tt.existingLoc != "" { + env.SetLocation(tt.existingLoc) + } + + // Simulate the guard logic from provision.go + var subErr, locErr error + if tt.flagSub != "" { + if existing := env.GetSubscriptionId(); existing != "" && existing != tt.flagSub { + subErr = fmt.Errorf( + "cannot change subscription for existing environment '%s' (current: %s, requested: %s). "+ + "Create a new environment with 'azd env new' instead", + env.Name(), existing, tt.flagSub) + } + } + if subErr == nil && tt.flagLoc != "" { + if existing := env.GetLocation(); existing != "" && existing != tt.flagLoc { + locErr = fmt.Errorf( + "cannot change location for existing environment '%s' (current: %s, requested: %s). "+ + "Create a new environment with 'azd env new' instead", + env.Name(), existing, tt.flagLoc) + } + } + + if tt.wantSubErr { + require.Error(t, subErr) + require.Contains(t, subErr.Error(), tt.wantSubErrMsg) + } else { + require.NoError(t, subErr) + } + + if tt.wantLocErr { + require.Error(t, locErr) + require.Contains(t, locErr.Error(), tt.wantLocErrMsg) + } else { + require.NoError(t, locErr) + } + }) + } +}