From e6087cd0344c13d969a9ce9aad16fb2267080822 Mon Sep 17 00:00:00 2001 From: Jian Wu Date: Fri, 22 May 2026 11:18:20 +0800 Subject: [PATCH 1/2] fix: separate model deployment location from resource group location Introduce AZURE_AI_DEPLOYMENTS_LOCATION env var to decouple model/project deployment location from the resource group location (AZURE_LOCATION). When a user selects a model from a different region during init, only AZURE_AI_DEPLOYMENTS_LOCATION is updated, preserving AZURE_LOCATION for the resource group. This prevents provisioning failures when the Foundry project and resource group are in different regions. Fixes Azure/azure-dev#7670 Supersedes #7873 --- .../cmd/init_foundry_resources_helpers.go | 51 ++++++++++++++++--- .../internal/cmd/init_models.go | 6 +-- 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_foundry_resources_helpers.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_foundry_resources_helpers.go index 4b455ed0153..c16307a98dc 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_foundry_resources_helpers.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_foundry_resources_helpers.go @@ -62,6 +62,23 @@ func setEnvValue(ctx context.Context, azdClient *azdext.AzdClient, envName, key, return nil } +// getEnvValue retrieves a single environment variable value. Returns empty string if not found. +func getEnvValue(ctx context.Context, azdClient *azdext.AzdClient, envName, key string) (string, error) { + envValues, err := azdClient.Environment().GetValues(ctx, &azdext.GetEnvironmentRequest{ + Name: envName, + }) + if err != nil { + return "", fmt.Errorf("failed to get environment values: %w", err) + } + + for _, kv := range envValues.KeyValues { + if kv.Key == key { + return kv.Value, nil + } + } + return "", nil +} + // projectResourceIdRegex is the precompiled regex for parsing Foundry project ARM resource IDs. var projectResourceIdRegex = regexp.MustCompile( `^/subscriptions/([^/]+)/resourceGroups/([^/]+)/providers/Microsoft\.CognitiveServices/accounts/([^/]+)/projects/([^/]+)$`, @@ -661,11 +678,18 @@ func loadAzureContext( envValueMap[value.Key] = value.Value } + // Prefer AZURE_AI_DEPLOYMENTS_LOCATION for model/project operations; + // fall back to AZURE_LOCATION for backward compatibility. + location := envValueMap["AZURE_AI_DEPLOYMENTS_LOCATION"] + if location == "" { + location = envValueMap["AZURE_LOCATION"] + } + return &azdext.AzureContext{ Scope: &azdext.AzureScope{ TenantId: envValueMap["AZURE_TENANT_ID"], SubscriptionId: envValueMap["AZURE_SUBSCRIPTION_ID"], - Location: envValueMap["AZURE_LOCATION"], + Location: location, }, Resources: []string{}, }, nil @@ -770,11 +794,11 @@ func ensureLocation( } if azureContext.Scope.Location != "" && locationAllowed(azureContext.Scope.Location, allowedLocations) { - return nil + return setEnvValue(ctx, azdClient, envName, "AZURE_AI_DEPLOYMENTS_LOCATION", azureContext.Scope.Location) } if azureContext.Scope.Location != "" { fmt.Printf("%s", output.WithWarningFormat( - "The current AZURE_LOCATION '%s' is not supported for this agent setup. Please choose a different location.\n", + "The current location '%s' is not supported for this agent setup. Please choose a different location.\n", azureContext.Scope.Location, )) azureContext.Scope.Location = "" @@ -789,7 +813,13 @@ func ensureLocation( azureContext.Scope.Location = locationName - return setEnvValue(ctx, azdClient, envName, "AZURE_LOCATION", azureContext.Scope.Location) + // Set both AZURE_LOCATION (resource group) and AZURE_AI_DEPLOYMENTS_LOCATION (model/project resources) + // to the same value by default. AZURE_AI_DEPLOYMENTS_LOCATION can be changed independently later + // (e.g. during model selection) without affecting the resource group location. + if err := setEnvValue(ctx, azdClient, envName, "AZURE_LOCATION", azureContext.Scope.Location); err != nil { + return err + } + return setEnvValue(ctx, azdClient, envName, "AZURE_AI_DEPLOYMENTS_LOCATION", azureContext.Scope.Location) } // ensureSubscriptionAndLocation ensures both subscription and location are set. @@ -1148,8 +1178,17 @@ func selectFoundryProject( // Set location from the selected project azureContext.Scope.Location = selectedProject.Location - if err := setEnvValue(ctx, azdClient, envName, "AZURE_LOCATION", selectedProject.Location); err != nil { - return nil, fmt.Errorf("failed to set AZURE_LOCATION: %w", err) + if err := setEnvValue(ctx, azdClient, envName, "AZURE_AI_DEPLOYMENTS_LOCATION", selectedProject.Location); err != nil { + return nil, fmt.Errorf("failed to set AZURE_AI_DEPLOYMENTS_LOCATION: %w", err) + } + // Seed AZURE_LOCATION (used for resource group) only if not already set. + // If the user already has an existing RG in a different region, we must not + // overwrite it — ARM cannot change the location of an existing resource group. + currentLocation, _ := getEnvValue(ctx, azdClient, envName, "AZURE_LOCATION") + if currentLocation == "" { + if err := setEnvValue(ctx, azdClient, envName, "AZURE_LOCATION", selectedProject.Location); err != nil { + return nil, fmt.Errorf("failed to set AZURE_LOCATION: %w", err) + } } // Configure all Foundry project environment variables diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_models.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_models.go index a607d516cb4..5198598c75d 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_models.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_models.go @@ -82,11 +82,11 @@ func (a *modelSelector) updateEnvLocation(ctx context.Context, selectedLocation _, err := a.azdClient.Environment().SetValue(ctx, &azdext.SetEnvRequest{ EnvName: envName, - Key: "AZURE_LOCATION", + Key: "AZURE_AI_DEPLOYMENTS_LOCATION", Value: selectedLocation, }) if err != nil { - return fmt.Errorf("failed to update AZURE_LOCATION in azd environment: %w", err) + return fmt.Errorf("failed to update AZURE_AI_DEPLOYMENTS_LOCATION in azd environment: %w", err) } if a.azureContext == nil { @@ -97,7 +97,7 @@ func (a *modelSelector) updateEnvLocation(ctx context.Context, selectedLocation } a.azureContext.Scope.Location = selectedLocation - fmt.Println(output.WithSuccessFormat("Updated AZURE_LOCATION to '%s' in your azd environment.", selectedLocation)) + fmt.Println(output.WithSuccessFormat("Updated AZURE_AI_DEPLOYMENTS_LOCATION to '%s' in your azd environment.", selectedLocation)) return nil } From 3d5dbe7c175361ee537f89bc2da9d23eca7567d7 Mon Sep 17 00:00:00 2001 From: Jian Wu Date: Fri, 22 May 2026 15:23:23 +0800 Subject: [PATCH 2/2] Address review: defensive AZURE_LOCATION seed + updateEnvLocation test - ensureLocation fast-path now seeds AZURE_LOCATION when unset, preventing provisioning failures if only AZURE_AI_DEPLOYMENTS_LOCATION was configured. - Add TestUpdateEnvLocation covering env var persistence and azureContext update. --- .../cmd/init_foundry_resources_helpers.go | 10 +++- .../internal/cmd/init_models_test.go | 55 +++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_foundry_resources_helpers.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_foundry_resources_helpers.go index c16307a98dc..7e9aeb344cb 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_foundry_resources_helpers.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_foundry_resources_helpers.go @@ -794,7 +794,15 @@ func ensureLocation( } if azureContext.Scope.Location != "" && locationAllowed(azureContext.Scope.Location, allowedLocations) { - return setEnvValue(ctx, azdClient, envName, "AZURE_AI_DEPLOYMENTS_LOCATION", azureContext.Scope.Location) + if err := setEnvValue(ctx, azdClient, envName, "AZURE_AI_DEPLOYMENTS_LOCATION", azureContext.Scope.Location); err != nil { + return err + } + // Defensively seed AZURE_LOCATION (resource group location) if not already set, + // so that downstream provisioning always has a valid location. + if existing, _ := getEnvValue(ctx, azdClient, envName, "AZURE_LOCATION"); existing == "" { + return setEnvValue(ctx, azdClient, envName, "AZURE_LOCATION", azureContext.Scope.Location) + } + return nil } if azureContext.Scope.Location != "" { fmt.Printf("%s", output.WithWarningFormat( diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_models_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_models_test.go index 50759e17453..02f3894c3eb 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_models_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_models_test.go @@ -262,3 +262,58 @@ func TestPersistFirstDeploymentName(t *testing.T) { }) } } + +func TestUpdateEnvLocation(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + selectedLocation string + existingContext *azdext.AzureContext + wantLocation string // expected azureContext.Scope.Location after call + }{ + { + name: "sets AZURE_AI_DEPLOYMENTS_LOCATION and updates azureContext", + selectedLocation: "westus2", + existingContext: &azdext.AzureContext{Scope: &azdext.AzureScope{Location: "eastus"}}, + wantLocation: "westus2", + }, + { + name: "nil azureContext gets initialized", + selectedLocation: "swedencentral", + existingContext: nil, + wantLocation: "swedencentral", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + envName := "test-env" + envServer := &testEnvironmentServiceServer{ + values: map[string]map[string]string{ + envName: {}, + }, + } + azdClient := newTestAzdClient(t, envServer, &testWorkflowServiceServer{}) + + ms := &modelSelector{ + azdClient: azdClient, + environment: &azdext.Environment{Name: envName}, + azureContext: tt.existingContext, + } + + err := ms.updateEnvLocation(t.Context(), tt.selectedLocation) + require.NoError(t, err) + + // Verify env var was persisted + assert.Equal(t, tt.selectedLocation, envServer.values[envName]["AZURE_AI_DEPLOYMENTS_LOCATION"]) + + // Verify azureContext was updated + require.NotNil(t, ms.azureContext) + require.NotNil(t, ms.azureContext.Scope) + assert.Equal(t, tt.wantLocation, ms.azureContext.Scope.Location) + }) + } +}