-
Notifications
You must be signed in to change notification settings - Fork 267
Filter location prompts by resource provider availability #6502
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
f485c78
dde24d1
ef97613
11c8e76
bcc21c9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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,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{} | ||
| for _, location := range allLocations { | ||
| supported, err := s.checkResourceTypesAvailability( | ||
| ctx, subscriptionId, tenantId, location.Name, options.ResourceTypes) | ||
|
Comment on lines
+175
to
+177
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
||
| 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 { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
|
||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This wouldn't handle |
||
| 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 | ||
| } | ||
There was a problem hiding this comment.
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 usingmake([]Location, 0, len(allLocations))to pre-allocate the slice with capacity to avoid reallocations during append operations.