From 8865854fa4a46e5917b246abc33f4ff1bfdaa025 Mon Sep 17 00:00:00 2001 From: Shayne Boyer Date: Wed, 18 Feb 2026 12:23:43 -0500 Subject: [PATCH 1/3] Add --subscription and --location flags to provision and up commands Fixes #6774 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../cmd/testdata/TestUsage-azd-provision.snap | 8 +++-- cli/azd/cmd/testdata/TestUsage-azd-up.snap | 4 ++- cli/azd/cmd/up.go | 14 ++++++++ cli/azd/internal/cmd/provision.go | 33 +++++++++++++++++++ 4 files changed, 55 insertions(+), 4 deletions(-) diff --git a/cli/azd/cmd/testdata/TestUsage-azd-provision.snap b/cli/azd/cmd/testdata/TestUsage-azd-provision.snap index e9772773942..fb08a4cf9f4 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 : Name or 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..e94f14e71c6 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 : Name or 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..266a781f74b 100644 --- a/cli/azd/cmd/up.go +++ b/cli/azd/cmd/up.go @@ -103,6 +103,20 @@ func newUpAction( } func (u *upAction) Run(ctx context.Context) (*actions.ActionResult, error) { + // Apply --subscription and --location flags to the environment before provisioning + if u.flags.ProvisionFlags.Subscription() != "" { + u.env.SetSubscriptionId(u.flags.ProvisionFlags.Subscription()) + if err := u.envManager.Save(ctx, u.env); err != nil { + return nil, fmt.Errorf("saving subscription id: %w", err) + } + } + if u.flags.ProvisionFlags.Location() != "" { + u.env.SetLocation(u.flags.ProvisionFlags.Location()) + if err := u.envManager.Save(ctx, u.env); err != nil { + return nil, fmt.Errorf("saving location: %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..e4a856fa4f6 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", + "", + "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") 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,20 @@ func (p *ProvisionAction) Run(ctx context.Context) (*actions.ActionResult, error return nil, err } + // Apply --subscription and --location flags to the environment before provisioning + if p.flags.subscription != "" { + p.env.SetSubscriptionId(p.flags.subscription) + if err := p.envManager.Save(ctx, p.env); err != nil { + return nil, fmt.Errorf("saving subscription id: %w", err) + } + } + if p.flags.location != "" { + p.env.SetLocation(p.flags.location) + if err := p.envManager.Save(ctx, p.env); err != nil { + return nil, fmt.Errorf("saving location: %w", err) + } + } + infra, err := p.importManager.ProjectInfrastructure(ctx, p.projectConfig) if err != nil { return nil, err From 3afdea33bdc6b28a026ac9c8fa0d595f4662eaef Mon Sep 17 00:00:00 2001 From: Shayne Boyer Date: Wed, 18 Feb 2026 12:43:01 -0500 Subject: [PATCH 2/3] Address review feedback: fix flag description and consolidate env saves - Change --subscription flag help text from 'Name or ID' to 'ID' since no name-to-ID resolution is performed - Consolidate separate Save calls into a single Save after setting both subscription and location values in provision.go and up.go - Update snapshot test data to reflect the description change Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/azd/cmd/env.go | 2 +- cli/azd/cmd/init.go | 2 +- cli/azd/cmd/testdata/TestFigSpec.ts | 40 ++++++++++++++++++- .../cmd/testdata/TestUsage-azd-env-new.snap | 2 +- cli/azd/cmd/testdata/TestUsage-azd-init.snap | 2 +- .../cmd/testdata/TestUsage-azd-provision.snap | 2 +- cli/azd/cmd/testdata/TestUsage-azd-up.snap | 2 +- cli/azd/cmd/up.go | 10 +++-- cli/azd/internal/cmd/provision.go | 12 +++--- 9 files changed, 57 insertions(+), 17 deletions(-) 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 fb08a4cf9f4..66f4ef16db9 100644 --- a/cli/azd/cmd/testdata/TestUsage-azd-provision.snap +++ b/cli/azd/cmd/testdata/TestUsage-azd-provision.snap @@ -16,7 +16,7 @@ Flags -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 : 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-up.snap b/cli/azd/cmd/testdata/TestUsage-azd-up.snap index e94f14e71c6..bd2694a3a41 100644 --- a/cli/azd/cmd/testdata/TestUsage-azd-up.snap +++ b/cli/azd/cmd/testdata/TestUsage-azd-up.snap @@ -22,7 +22,7 @@ Usage Flags -e, --environment string : The name of the environment to use. -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/up.go b/cli/azd/cmd/up.go index 266a781f74b..60c038f7b66 100644 --- a/cli/azd/cmd/up.go +++ b/cli/azd/cmd/up.go @@ -104,16 +104,18 @@ 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 u.flags.ProvisionFlags.Subscription() != "" { u.env.SetSubscriptionId(u.flags.ProvisionFlags.Subscription()) - if err := u.envManager.Save(ctx, u.env); err != nil { - return nil, fmt.Errorf("saving subscription id: %w", err) - } + updatedEnv = true } if u.flags.ProvisionFlags.Location() != "" { u.env.SetLocation(u.flags.ProvisionFlags.Location()) + updatedEnv = true + } + if updatedEnv { if err := u.envManager.Save(ctx, u.env); err != nil { - return nil, fmt.Errorf("saving location: %w", err) + return nil, fmt.Errorf("saving environment: %w", err) } } diff --git a/cli/azd/internal/cmd/provision.go b/cli/azd/internal/cmd/provision.go index e4a856fa4f6..5a2f67a1475 100644 --- a/cli/azd/internal/cmd/provision.go +++ b/cli/azd/internal/cmd/provision.go @@ -61,7 +61,7 @@ func (i *ProvisionFlags) BindNonCommon(local *pflag.FlagSet, global *internal.Gl &i.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(&i.location, "location", "l", "", "Azure location for the new environment") i.global = global @@ -219,16 +219,18 @@ func (p *ProvisionAction) Run(ctx context.Context) (*actions.ActionResult, error } // Apply --subscription and --location flags to the environment before provisioning + envChanged := false if p.flags.subscription != "" { p.env.SetSubscriptionId(p.flags.subscription) - if err := p.envManager.Save(ctx, p.env); err != nil { - return nil, fmt.Errorf("saving subscription id: %w", err) - } + envChanged = true } if 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 location: %w", err) + return nil, fmt.Errorf("saving environment: %w", err) } } From 1f3368eac82c6e4bcb198b1b60f2672f3901e2bb Mon Sep 17 00:00:00 2001 From: Shayne Boyer Date: Wed, 18 Feb 2026 15:19:08 -0500 Subject: [PATCH 3/3] Guard against changing subscription/location on existing environments Return an error when --subscription or --location flags attempt to change values that are already set in the environment. This prevents accidentally leaking previously provisioned resources by re-provisioning in a different subscription or location. Same value = no-op (allowed), empty value = first time setting (allowed), different value = error with clear guidance to create a new environment. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/azd/cmd/up.go | 20 +++- cli/azd/internal/cmd/provision.go | 12 ++ cli/azd/internal/cmd/provision_guard_test.go | 120 +++++++++++++++++++ 3 files changed, 148 insertions(+), 4 deletions(-) create mode 100644 cli/azd/internal/cmd/provision_guard_test.go diff --git a/cli/azd/cmd/up.go b/cli/azd/cmd/up.go index 60c038f7b66..7a99489c1a0 100644 --- a/cli/azd/cmd/up.go +++ b/cli/azd/cmd/up.go @@ -105,12 +105,24 @@ 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 u.flags.ProvisionFlags.Subscription() != "" { - u.env.SetSubscriptionId(u.flags.ProvisionFlags.Subscription()) + 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 u.flags.ProvisionFlags.Location() != "" { - u.env.SetLocation(u.flags.ProvisionFlags.Location()) + 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 { diff --git a/cli/azd/internal/cmd/provision.go b/cli/azd/internal/cmd/provision.go index 5a2f67a1475..c47a0353a13 100644 --- a/cli/azd/internal/cmd/provision.go +++ b/cli/azd/internal/cmd/provision.go @@ -221,10 +221,22 @@ func (p *ProvisionAction) Run(ctx context.Context) (*actions.ActionResult, error // 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 } 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) + } + }) + } +}