From f485c7854679645e5bf5b3694b98c072a3b56da1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Jan 2026 16:03:06 +0000 Subject: [PATCH 1/5] Initial plan From dde24d1436c0cd8e6e1291b4da72a97a03ce8d26 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Jan 2026 16:18:58 +0000 Subject: [PATCH 2/5] Add location filtering by resource provider availability Co-authored-by: spboyer <7681382+spboyer@users.noreply.github.com> --- .../internal/utils/environment.go | 2 +- cli/azd/pkg/account/manager.go | 16 +++ cli/azd/pkg/account/subscriptions.go | 125 ++++++++++++++++++ cli/azd/pkg/account/subscriptions_manager.go | 24 ++++ cli/azd/pkg/azure/arm_template.go | 35 +++++ cli/azd/pkg/azureutil/location.go | 62 ++++++++- .../provisioning/bicep/bicep_provider.go | 14 +- .../pkg/infra/provisioning/bicep/prompt.go | 4 +- cli/azd/pkg/infra/provisioning/manager.go | 33 ++++- cli/azd/pkg/prompt/prompter.go | 30 +++++ 10 files changed, 333 insertions(+), 12 deletions(-) diff --git a/cli/azd/extensions/azure.ai.finetune/internal/utils/environment.go b/cli/azd/extensions/azure.ai.finetune/internal/utils/environment.go index 75c27f47251..3a260951378 100644 --- a/cli/azd/extensions/azure.ai.finetune/internal/utils/environment.go +++ b/cli/azd/extensions/azure.ai.finetune/internal/utils/environment.go @@ -18,7 +18,7 @@ const ( EnvAzureOpenAIProjectName = "AZURE_PROJECT_NAME" EnvAPIVersion = "AZURE_API_VERSION" EnvFinetuningRoute = "AZURE_FINETUNING_ROUTE" - EnvFinetuningTokenScope = "AZURE_FINETUNING_TOKEN_SCOPE" + EnvFinetuningTokenScope = "AZURE_FINETUNING_TOKEN_SCOPE" ) // GetEnvironmentValues retrieves Azure environment configuration from azd client. diff --git a/cli/azd/pkg/account/manager.go b/cli/azd/pkg/account/manager.go index 966632f3edc..a286eadb131 100644 --- a/cli/azd/pkg/account/manager.go +++ b/cli/azd/pkg/account/manager.go @@ -37,6 +37,7 @@ type Manager interface { GetSubscriptions(ctx context.Context) ([]Subscription, error) GetSubscriptionsWithDefaultSet(ctx context.Context) ([]Subscription, error) GetLocations(ctx context.Context, subscriptionId string) ([]Location, error) + GetLocationsWithFilter(ctx context.Context, subscriptionId string, resourceTypes []string) ([]Location, error) SetDefaultSubscription(ctx context.Context, subscriptionId string) (*Subscription, error) SetDefaultLocation(ctx context.Context, subscriptionId string, location string) (*Location, error) } @@ -150,6 +151,21 @@ func (m *manager) GetLocations(ctx context.Context, subscriptionId string) ([]Lo return locations, nil } +// GetLocationsWithFilter gets the available Azure locations for the specified Azure subscription, +// filtered by resource type availability. Only locations that support ALL specified resource types are returned. +func (m *manager) GetLocationsWithFilter( + ctx context.Context, + subscriptionId string, + resourceTypes []string, +) ([]Location, error) { + locations, err := m.subManager.ListLocationsWithFilter(ctx, subscriptionId, resourceTypes) + if err != nil { + return nil, fmt.Errorf("failed retrieving Azure locations for account '%s': %w", subscriptionId, err) + } + + return locations, nil +} + // Sets the default Azure subscription for the current logged in principal. func (m *manager) SetDefaultSubscription(ctx context.Context, subscriptionId string) (*Subscription, error) { subscription, err := m.subManager.GetSubscription(ctx, subscriptionId) diff --git a/cli/azd/pkg/account/subscriptions.go b/cli/azd/pkg/account/subscriptions.go index c1226e105eb..d7e2c3cf675 100644 --- a/cli/azd/pkg/account/subscriptions.go +++ b/cli/azd/pkg/account/subscriptions.go @@ -7,8 +7,10 @@ import ( "context" "fmt" "sort" + "strings" "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions" "github.com/azure/azure-dev/cli/azd/pkg/auth" "github.com/azure/azure-dev/cli/azd/pkg/compare" @@ -143,6 +145,129 @@ func (s *SubscriptionsService) ListSubscriptionLocations( return locations, nil } +// LocationFilterOptions provides filtering options for location queries +type LocationFilterOptions struct { + // ResourceTypes filters by specific resource types (e.g., "Microsoft.App/containerApps") + ResourceTypes []string +} + +// ListSubscriptionLocationsWithFilter lists physical locations with optional resource type filtering. +// When ResourceTypes are provided, only locations that support ALL specified resource types are returned. +func (s *SubscriptionsService) ListSubscriptionLocationsWithFilter( + ctx context.Context, + subscriptionId string, + tenantId string, + options *LocationFilterOptions, +) ([]Location, error) { + // Get all physical locations first + allLocations, err := s.ListSubscriptionLocations(ctx, subscriptionId, tenantId) + if err != nil { + return nil, err + } + + // If no filtering options or no resource types specified, return all locations + if options == nil || len(options.ResourceTypes) == 0 { + return allLocations, nil + } + + // Check resource type availability for each location + filteredLocations := []Location{} + for _, location := range allLocations { + supported, err := s.checkResourceTypesAvailability(ctx, subscriptionId, tenantId, location.Name, options.ResourceTypes) + if err != nil { + // Log error but continue with other locations + continue + } + + if supported { + filteredLocations = append(filteredLocations, location) + } + } + + return filteredLocations, nil +} + +// checkResourceTypesAvailability checks if all specified resource types are available in the given location. +func (s *SubscriptionsService) checkResourceTypesAvailability( + ctx context.Context, + subscriptionId string, + tenantId string, + locationName string, + resourceTypes []string, +) (bool, error) { + if len(resourceTypes) == 0 { + return true, nil + } + + // Group resource types by provider namespace + providerResourceTypes := make(map[string][]string) + for _, resourceType := range resourceTypes { + parts := strings.SplitN(resourceType, "/", 2) + if len(parts) != 2 { + continue // Skip invalid resource types + } + providerNamespace := parts[0] + typeName := parts[1] + providerResourceTypes[providerNamespace] = append(providerResourceTypes[providerNamespace], typeName) + } + + // Create providers client + cred, err := s.credentialProvider.GetTokenCredential(ctx, tenantId) + if err != nil { + return false, fmt.Errorf("getting credential: %w", err) + } + + providersClient, err := armresources.NewProvidersClient(subscriptionId, cred, s.armClientOptions) + if err != nil { + return false, fmt.Errorf("creating providers client: %w", err) + } + + // Check each provider's resource types + for providerNamespace, typeNames := range providerResourceTypes { + provider, err := providersClient.Get(ctx, providerNamespace, nil) + if err != nil { + return false, fmt.Errorf("getting provider %s: %w", providerNamespace, err) + } + + // Check if provider is registered + if provider.RegistrationState == nil || *provider.RegistrationState != "Registered" { + return false, nil + } + + // Check each resource type + for _, typeName := range typeNames { + found := false + if provider.ResourceTypes != nil { + for _, rt := range provider.ResourceTypes { + if rt.ResourceType != nil && *rt.ResourceType == typeName { + // Check if this location is supported + if rt.Locations != nil { + locationSupported := false + for _, loc := range rt.Locations { + if loc != nil && strings.EqualFold(*loc, locationName) { + locationSupported = true + break + } + } + if !locationSupported { + return false, nil + } + } + found = true + break + } + } + } + + if !found { + return false, nil + } + } + } + + return true, nil +} + func (s *SubscriptionsService) ListTenants(ctx context.Context) ([]armsubscriptions.TenantIDDescription, error) { client, err := s.createTenantsClient(ctx) if err != nil { diff --git a/cli/azd/pkg/account/subscriptions_manager.go b/cli/azd/pkg/account/subscriptions_manager.go index 494a89bbdd1..957d3281307 100644 --- a/cli/azd/pkg/account/subscriptions_manager.go +++ b/cli/azd/pkg/account/subscriptions_manager.go @@ -380,6 +380,30 @@ func (m *SubscriptionsManager) listLocations( return m.service.ListSubscriptionLocations(ctx, subscriptionId, tenantId) } +// ListLocationsWithFilter lists locations for a subscription with optional resource type filtering. +// When resourceTypes are provided, only locations that support ALL specified resource types are returned. +func (m *SubscriptionsManager) ListLocationsWithFilter( + ctx context.Context, + subscriptionId string, + resourceTypes []string, +) ([]Location, error) { + var err error + msg := "Retrieving locations..." + m.console.ShowSpinner(ctx, msg, input.Step) + defer m.console.StopSpinner(ctx, "", input.GetStepResultFormat(err)) + + tenantId, err := m.LookupTenant(ctx, subscriptionId) + if err != nil { + return nil, err + } + + options := &LocationFilterOptions{ + ResourceTypes: resourceTypes, + } + + return m.service.ListSubscriptionLocationsWithFilter(ctx, subscriptionId, tenantId, options) +} + func (m *SubscriptionsManager) getSubscription(ctx context.Context, subscriptionId string) (*Subscription, error) { tenantId, err := m.LookupTenant(ctx, subscriptionId) if err != nil { diff --git a/cli/azd/pkg/azure/arm_template.go b/cli/azd/pkg/azure/arm_template.go index 06a5d25c341..17902eb5dbc 100644 --- a/cli/azd/pkg/azure/arm_template.go +++ b/cli/azd/pkg/azure/arm_template.go @@ -202,3 +202,38 @@ type ArmTemplateOutput struct { Metadata map[string]any `json:"metadata"` Ref string `json:"$ref"` } + +// ArmTemplateResource represents a resource in an ARM template +type ArmTemplateResource struct { + Type string `json:"type"` + Name string `json:"name"` + Location any `json:"location,omitempty"` +} + +// ExtractResourceTypes extracts unique resource types from a compiled ARM template. +// Returns a list of resource types in the format "Microsoft.Provider/resourceType". +func ExtractResourceTypes(rawTemplate RawArmTemplate) ([]string, error) { + var templateWithResources struct { + Resources []ArmTemplateResource `json:"resources"` + } + + if err := json.Unmarshal(rawTemplate, &templateWithResources); err != nil { + return nil, fmt.Errorf("failed to unmarshal ARM template: %w", err) + } + + // Use a map to track unique resource types + uniqueTypes := make(map[string]struct{}) + for _, resource := range templateWithResources.Resources { + if resource.Type != "" { + uniqueTypes[resource.Type] = struct{}{} + } + } + + // Convert map to slice + resourceTypes := make([]string, 0, len(uniqueTypes)) + for resourceType := range uniqueTypes { + resourceTypes = append(resourceTypes, resourceType) + } + + return resourceTypes, nil +} diff --git a/cli/azd/pkg/azureutil/location.go b/cli/azd/pkg/azureutil/location.go index 28aa3aafdf9..71a86061d1c 100644 --- a/cli/azd/pkg/azureutil/location.go +++ b/cli/azd/pkg/azureutil/location.go @@ -27,7 +27,67 @@ func PromptLocationWithFilter( shouldDisplay func(account.Location) bool, defaultSelectedLocation *string, ) (string, error) { - allLocations, err := accountManager.GetLocations(ctx, subscriptionId) + return promptLocationWithFilterAndResourceTypes( + ctx, + subscriptionId, + message, + help, + console, + accountManager, + shouldDisplay, + defaultSelectedLocation, + nil, // No resource type filtering + ) +} + +// PromptLocationWithResourceTypeFilter asks the user to select a location from a list of supported azure locations +// for a given subscription, filtered by resource type availability. +// Only locations that support ALL specified resource types are presented. +func PromptLocationWithResourceTypeFilter( + ctx context.Context, + subscriptionId string, + message string, + help string, + console input.Console, + accountManager account.Manager, + shouldDisplay func(account.Location) bool, + defaultSelectedLocation *string, + resourceTypes []string, +) (string, error) { + return promptLocationWithFilterAndResourceTypes( + ctx, + subscriptionId, + message, + help, + console, + accountManager, + shouldDisplay, + defaultSelectedLocation, + resourceTypes, + ) +} + +func promptLocationWithFilterAndResourceTypes( + ctx context.Context, + subscriptionId string, + message string, + help string, + console input.Console, + accountManager account.Manager, + shouldDisplay func(account.Location) bool, + defaultSelectedLocation *string, + resourceTypes []string, +) (string, error) { + var allLocations []account.Location + var err error + + // Use filtered or unfiltered location retrieval based on resource types + if len(resourceTypes) > 0 { + allLocations, err = accountManager.GetLocationsWithFilter(ctx, subscriptionId, resourceTypes) + } else { + allLocations, err = accountManager.GetLocations(ctx, subscriptionId) + } + if err != nil { return "", fmt.Errorf("listing locations: %w", err) } diff --git a/cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go b/cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go index 1ae10dab1b1..e5b91fe69a9 100644 --- a/cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go +++ b/cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go @@ -84,6 +84,9 @@ type BicepProvider struct { // Internal state // compileBicepResult is cached to avoid recompiling the same bicep file multiple times in the same azd run. compileBicepMemoryCache *compileBicepResult + // extractedResourceTypes contains resource types extracted from the compiled bicep template. + // Used for filtering locations by resource provider availability. + extractedResourceTypes []string } // Name gets the name of the infra provider @@ -149,10 +152,19 @@ func (p *BicepProvider) EnsureEnv(ctx context.Context) error { return fmt.Errorf("%w%w", ErrEnsureEnvPreReqBicepCompileFailed, compileErr) } + // Extract resource types from compiled template for location filtering + resourceTypes, err := azure.ExtractResourceTypes(compileResult.RawArmTemplate) + if err != nil { + // If extraction fails, we continue without filtering + resourceTypes = []string{} + } + // Store for use during parameter prompting + p.extractedResourceTypes = resourceTypes + // 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 p.mode == bicepMode { - err := provisioning.EnsureSubscription( + err = provisioning.EnsureSubscription( ctx, p.envManager, p.env, p.prompters) if err != nil { return err diff --git a/cli/azd/pkg/infra/provisioning/bicep/prompt.go b/cli/azd/pkg/infra/provisioning/bicep/prompt.go index 34865314352..d6649fba5c2 100644 --- a/cli/azd/pkg/infra/provisioning/bicep/prompt.go +++ b/cli/azd/pkg/infra/provisioning/bicep/prompt.go @@ -276,10 +276,10 @@ func (p *BicepProvider) promptForParameter( allowedLocations = withQuotaLocations } - location, err := p.prompters.PromptLocation( + location, err := p.prompters.PromptLocationWithResourceTypes( ctx, p.env.GetSubscriptionId(), msg, func(loc account.Location) bool { return locationParameterFilterImpl(allowedLocations, loc) - }, defaultPromptValue(param)) + }, defaultPromptValue(param), p.extractedResourceTypes) if err != nil { return nil, err } diff --git a/cli/azd/pkg/infra/provisioning/manager.go b/cli/azd/pkg/infra/provisioning/manager.go index 306f9dd600e..14e1ab56d83 100644 --- a/cli/azd/pkg/infra/provisioning/manager.go +++ b/cli/azd/pkg/infra/provisioning/manager.go @@ -345,6 +345,9 @@ type EnsureSubscriptionAndLocationOptions struct { LocationFiler prompt.LocationFilterPredicate // SelectDefaultLocation is the default location that azd mark as selected when prompting the user for the location. SelectDefaultLocation *string + // ResourceTypes filters locations by resource type availability. Only locations that support ALL specified + // resource types are displayed. + ResourceTypes []string } // EnsureSubscriptionAndLocation ensures that that that subscription (AZURE_SUBSCRIPTION_ID) and location (AZURE_LOCATION) @@ -377,13 +380,29 @@ func EnsureSubscriptionAndLocation( location := env.GetLocation() if env.GetLocation() == "" { - loc, err := prompter.PromptLocation( - ctx, - env.GetSubscriptionId(), - "Select an Azure location to use:", - options.LocationFiler, - options.SelectDefaultLocation, - ) + var loc string + var err error + + // Use resource type filtering if provided + if len(options.ResourceTypes) > 0 { + loc, err = prompter.PromptLocationWithResourceTypes( + ctx, + env.GetSubscriptionId(), + "Select an Azure location to use:", + options.LocationFiler, + options.SelectDefaultLocation, + options.ResourceTypes, + ) + } else { + loc, err = prompter.PromptLocation( + ctx, + env.GetSubscriptionId(), + "Select an Azure location to use:", + options.LocationFiler, + options.SelectDefaultLocation, + ) + } + if err != nil { return err } diff --git a/cli/azd/pkg/prompt/prompter.go b/cli/azd/pkg/prompt/prompter.go index d6bc4589a77..4c5471472c9 100644 --- a/cli/azd/pkg/prompt/prompter.go +++ b/cli/azd/pkg/prompt/prompter.go @@ -34,6 +34,13 @@ type Prompter interface { msg string, filter LocationFilterPredicate, defaultLocation *string) (string, error) + PromptLocationWithResourceTypes( + ctx context.Context, + subId string, + msg string, + filter LocationFilterPredicate, + defaultLocation *string, + resourceTypes []string) (string, error) // PromptResourceGroup prompts for an existing or optionally a new resource group. // A location saved as AZURE_LOCATION is also prompted as part of creating a new resource group. @@ -127,6 +134,29 @@ func (p *DefaultPrompter) PromptLocation( return loc, nil } +func (p *DefaultPrompter) PromptLocationWithResourceTypes( + ctx context.Context, + subId string, + msg string, + filter LocationFilterPredicate, + defaultLocation *string, + resourceTypes []string, +) (string, error) { + loc, err := azureutil.PromptLocationWithResourceTypeFilter( + ctx, subId, msg, "", p.console, p.accountManager, filter, defaultLocation, resourceTypes) + if err != nil { + return "", err + } + + if !p.accountManager.HasDefaultLocation() { + if _, err := p.accountManager.SetDefaultLocation(ctx, subId, loc); err != nil { + log.Printf("failed setting default location. %v\n", err) + } + } + + return loc, nil +} + type PromptResourceOptions struct { // DisableCreateNew disables the option to create a new resource group. DisableCreateNew bool From ef97613def03111788adc4782ac50c73c5e4e2bf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Jan 2026 16:23:56 +0000 Subject: [PATCH 3/5] Add unit tests and fix linting issues Co-authored-by: spboyer <7681382+spboyer@users.noreply.github.com> --- cli/azd/pkg/account/subscriptions.go | 3 +- cli/azd/pkg/azure/arm_template_test.go | 166 ++++++++++++++++++ .../test/mocks/mockaccount/mock_manager.go | 10 ++ 3 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 cli/azd/pkg/azure/arm_template_test.go diff --git a/cli/azd/pkg/account/subscriptions.go b/cli/azd/pkg/account/subscriptions.go index d7e2c3cf675..1f1a47caabc 100644 --- a/cli/azd/pkg/account/subscriptions.go +++ b/cli/azd/pkg/account/subscriptions.go @@ -173,7 +173,8 @@ func (s *SubscriptionsService) ListSubscriptionLocationsWithFilter( // Check resource type availability for each location filteredLocations := []Location{} for _, location := range allLocations { - supported, err := s.checkResourceTypesAvailability(ctx, subscriptionId, tenantId, location.Name, options.ResourceTypes) + supported, err := s.checkResourceTypesAvailability( + ctx, subscriptionId, tenantId, location.Name, options.ResourceTypes) if err != nil { // Log error but continue with other locations continue diff --git a/cli/azd/pkg/azure/arm_template_test.go b/cli/azd/pkg/azure/arm_template_test.go new file mode 100644 index 00000000000..0eb55fecae8 --- /dev/null +++ b/cli/azd/pkg/azure/arm_template_test.go @@ -0,0 +1,166 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package azure + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestExtractResourceTypes(t *testing.T) { + tests := []struct { + name string + template string + expectedTypes []string + expectedError bool + }{ + { + name: "SimpleTemplate", + template: `{ + "resources": [ + { + "type": "Microsoft.App/containerApps", + "name": "myapp", + "location": "eastus" + }, + { + "type": "Microsoft.DBforPostgreSQL/flexibleServers", + "name": "mydb", + "location": "eastus" + } + ] + }`, + expectedTypes: []string{"Microsoft.App/containerApps", "Microsoft.DBforPostgreSQL/flexibleServers"}, + }, + { + name: "DuplicateResourceTypes", + template: `{ + "resources": [ + { + "type": "Microsoft.App/containerApps", + "name": "app1" + }, + { + "type": "Microsoft.App/containerApps", + "name": "app2" + }, + { + "type": "Microsoft.Storage/storageAccounts", + "name": "storage1" + } + ] + }`, + expectedTypes: []string{"Microsoft.App/containerApps", "Microsoft.Storage/storageAccounts"}, + }, + { + name: "EmptyResources", + template: `{ + "resources": [] + }`, + expectedTypes: []string{}, + }, + { + name: "NoResourcesField", + template: `{ + "parameters": {}, + "outputs": {} + }`, + expectedTypes: []string{}, + }, + { + name: "ResourceWithoutType", + template: `{ + "resources": [ + { + "name": "myresource" + }, + { + "type": "Microsoft.Web/staticSites", + "name": "webapp" + } + ] + }`, + expectedTypes: []string{"Microsoft.Web/staticSites"}, + }, + { + name: "InvalidJSON", + template: `{ + "resources": [ + { invalid json + ] + }`, + expectedError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rawTemplate := RawArmTemplate(tt.template) + resourceTypes, err := ExtractResourceTypes(rawTemplate) + + if tt.expectedError { + require.Error(t, err) + return + } + + require.NoError(t, err) + + // Sort both slices for comparison since order doesn't matter + assert.ElementsMatch(t, tt.expectedTypes, resourceTypes, + "Expected resource types %v, got %v", tt.expectedTypes, resourceTypes) + }) + } +} + +func TestExtractResourceTypesComplexTemplate(t *testing.T) { + // Test with a more realistic ARM template structure + template := map[string]interface{}{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": map[string]interface{}{ + "location": map[string]interface{}{ + "type": "string", + }, + }, + "resources": []interface{}{ + map[string]interface{}{ + "type": "Microsoft.App/containerApps", + "apiVersion": "2023-05-01", + "name": "[parameters('appName')]", + "location": "[parameters('location')]", + "properties": map[string]interface{}{ + "managedEnvironmentId": "[resourceId('Microsoft.App/managedEnvironments', 'env')]", + }, + }, + map[string]interface{}{ + "type": "Microsoft.App/managedEnvironments", + "apiVersion": "2023-05-01", + "name": "env", + "location": "[parameters('location')]", + }, + map[string]interface{}{ + "type": "Microsoft.KeyVault/vaults", + "apiVersion": "2023-02-01", + "name": "keyvault", + "location": "[parameters('location')]", + }, + }, + } + + templateJSON, err := json.Marshal(template) + require.NoError(t, err) + + rawTemplate := RawArmTemplate(templateJSON) + resourceTypes, err := ExtractResourceTypes(rawTemplate) + + require.NoError(t, err) + assert.ElementsMatch(t, []string{ + "Microsoft.App/containerApps", + "Microsoft.App/managedEnvironments", + "Microsoft.KeyVault/vaults", + }, resourceTypes) +} diff --git a/cli/azd/test/mocks/mockaccount/mock_manager.go b/cli/azd/test/mocks/mockaccount/mock_manager.go index fcbf8e79a87..d7481a41c54 100644 --- a/cli/azd/test/mocks/mockaccount/mock_manager.go +++ b/cli/azd/test/mocks/mockaccount/mock_manager.go @@ -72,6 +72,16 @@ func (a *MockAccountManager) GetLocations(ctx context.Context, subscriptionId st return a.Locations, nil } +func (a *MockAccountManager) GetLocationsWithFilter( + ctx context.Context, + subscriptionId string, + resourceTypes []string, +) ([]account.Location, error) { + // For mock purposes, just return all locations + // In a real implementation with more complex testing, this could filter based on resourceTypes + return a.Locations, nil +} + func (a *MockAccountManager) SetDefaultSubscription( ctx context.Context, subscriptionId string) (*account.Subscription, error) { a.DefaultSubscription = subscriptionId From 11c8e767bca7630eb52552ee882108fc22034ad1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Jan 2026 16:26:37 +0000 Subject: [PATCH 4/5] Address code review feedback: improve error handling and logging Co-authored-by: spboyer <7681382+spboyer@users.noreply.github.com> --- cli/azd/pkg/account/subscriptions.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/cli/azd/pkg/account/subscriptions.go b/cli/azd/pkg/account/subscriptions.go index 1f1a47caabc..9cc09a84983 100644 --- a/cli/azd/pkg/account/subscriptions.go +++ b/cli/azd/pkg/account/subscriptions.go @@ -176,7 +176,9 @@ func (s *SubscriptionsService) ListSubscriptionLocationsWithFilter( supported, err := s.checkResourceTypesAvailability( ctx, subscriptionId, tenantId, location.Name, options.ResourceTypes) if err != nil { - // Log error but continue with other locations + // Log error but continue with other locations to provide best-effort filtering. + // If all locations fail, an empty list will be returned, prompting the user to check permissions. + fmt.Printf("warning: failed to check resource availability for location %s: %v\n", location.Name, err) continue } @@ -205,7 +207,12 @@ func (s *SubscriptionsService) checkResourceTypesAvailability( for _, resourceType := range resourceTypes { parts := strings.SplitN(resourceType, "/", 2) if len(parts) != 2 { - continue // Skip invalid resource types + // Skip invalid resource types (should be in format "Provider/Type") + // This could indicate a template parsing issue, so log for debugging + fmt.Printf( + "warning: skipping invalid resource type format '%s' (expected 'Provider/Type')\n", + resourceType) + continue } providerNamespace := parts[0] typeName := parts[1] From bcc21c9a2ecbf131386ac773ece21bf370e73b3f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Jan 2026 16:30:13 +0000 Subject: [PATCH 5/5] Complete implementation - all checks passed Co-authored-by: spboyer <7681382+spboyer@users.noreply.github.com> --- cli/azd/extensions/azure.ai.finetune/go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/azd/extensions/azure.ai.finetune/go.mod b/cli/azd/extensions/azure.ai.finetune/go.mod index 245759a75d1..2fc5bb74aa9 100644 --- a/cli/azd/extensions/azure.ai.finetune/go.mod +++ b/cli/azd/extensions/azure.ai.finetune/go.mod @@ -10,6 +10,7 @@ require ( github.com/braydonk/yaml v0.9.0 github.com/fatih/color v1.18.0 github.com/openai/openai-go/v3 v3.2.0 + github.com/sethvargo/go-retry v0.3.0 github.com/spf13/cobra v1.10.1 ) @@ -61,7 +62,6 @@ require ( github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/sethvargo/go-retry v0.3.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/stretchr/testify v1.11.1 // indirect github.com/theckman/yacspin v0.13.12 // indirect