Skip to content
Open
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
2 changes: 1 addition & 1 deletion cli/azd/extensions/azure.ai.finetune/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
16 changes: 16 additions & 0 deletions cli/azd/pkg/account/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
Expand Down
133 changes: 133 additions & 0 deletions cli/azd/pkg/account/subscriptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -143,6 +145,137 @@ 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{}
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the capacity is unknown but likely to be less than len(allLocations), consider using make([]Location, 0, len(allLocations)) to pre-allocate the slice with capacity to avoid reallocations during append operations.

Copilot uses AI. Check for mistakes.
for _, location := range allLocations {
supported, err := s.checkResourceTypesAvailability(
ctx, subscriptionId, tenantId, location.Name, options.ResourceTypes)
Comment on lines +175 to +177
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is going to be real expensive. For each location, we are querying every single resource type to see if the location should be filtered, i.e. Number of queries = number of locations x number of resource types

if err != nil {
// 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)
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Direct fmt.Printf calls bypass the structured logging system. Use the log package or a structured logger instead. This is inconsistent with the telemetry and logging infrastructure used elsewhere in azd.

This issue also appears in the following locations of the same file:

  • line 212

Copilot uses AI. Check for mistakes.
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 {
// 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]
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 {
Expand Down
24 changes: 24 additions & 0 deletions cli/azd/pkg/account/subscriptions_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
35 changes: 35 additions & 0 deletions cli/azd/pkg/azure/arm_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
Comment on lines +216 to +218
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This wouldn't work for any nested Bicep modules yet, which would require recursively looking at [resources == Microsoft.Resources/deployments].properties.template.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 {
Comment on lines +224 to +226
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This wouldn't handle existing resources, which aren't being created or updated by the current ARM template

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
}
Loading
Loading