diff --git a/cli/azd/pkg/azapi/resource_service.go b/cli/azd/pkg/azapi/resource_service.go index 9a9dde8fd64..9e4514ad93e 100644 --- a/cli/azd/pkg/azapi/resource_service.go +++ b/cli/azd/pkg/azapi/resource_service.go @@ -42,6 +42,10 @@ type ListResourceGroupResourcesOptions struct { Filter *string } +type ListResourcesOptions struct { + ResourceType string +} + type ResourceService struct { credentialProvider account.SubscriptionCredentialProvider armClientOptions *arm.ClientOptions @@ -167,6 +171,45 @@ func (rs *ResourceService) ListResourceGroup( return groups, nil } +// ListResources returns a slice of resources - optionally filtered on fields in `ListResourcesOptions` - including the +// ID, Name, Type, and Location of each resource. +func (rs *ResourceService) ListResources( + ctx context.Context, + subscriptionId string, + listOptions *ListResourcesOptions, +) ([]*Resource, error) { + client, err := rs.createResourcesClient(ctx, subscriptionId) + if err != nil { + return nil, err + } + + options := armresources.ClientListOptions{} + if listOptions != nil && listOptions.ResourceType != "" { + filter := fmt.Sprintf("resourceType eq '%s'", listOptions.ResourceType) + options.Filter = &filter + } + + resources := []*Resource{} + pager := client.NewListPager(&options) + + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, err + } + + for _, resource := range page.ResourceListResult.Value { + resources = append(resources, &Resource{ + Id: *resource.ID, + Name: *resource.Name, + Type: *resource.Type, + Location: *resource.Location, + }) + } + } + return resources, nil +} + func (rs *ResourceService) CreateOrUpdateResourceGroup( ctx context.Context, subscriptionId string, diff --git a/cli/azd/pkg/azure/arm_template.go b/cli/azd/pkg/azure/arm_template.go index 153d352f439..9618982c75a 100644 --- a/cli/azd/pkg/azure/arm_template.go +++ b/cli/azd/pkg/azure/arm_template.go @@ -102,11 +102,13 @@ type AzdMetadataType string const AzdMetadataTypeLocation AzdMetadataType = "location" const AzdMetadataTypeGenerate AzdMetadataType = "generate" const AzdMetadataTypeGenerateOrManual AzdMetadataType = "generateOrManual" +const AzdMetadataTypeResource AzdMetadataType = "resource" type AzdMetadata struct { Type *AzdMetadataType `json:"type,omitempty"` AutoGenerateConfig *AutoGenInput `json:"config,omitempty"` DefaultValueExpr *string `json:"defaultValueExpr,omitempty"` + ResourceType *string `json:"resourceType,omitempty"` } // Description returns the value of the "Description" string metadata for this parameter or empty if it can not be found. diff --git a/cli/azd/pkg/infra/provisioning/bicep/prompt.go b/cli/azd/pkg/infra/provisioning/bicep/prompt.go index fb85295bf54..641b7647faf 100644 --- a/cli/azd/pkg/infra/provisioning/bicep/prompt.go +++ b/cli/azd/pkg/infra/provisioning/bicep/prompt.go @@ -143,6 +143,30 @@ func (p *BicepProvider) promptForParameter( } value = genValue } + } else if paramType == provisioning.ParameterTypeString && + azdMetadata.Type != nil && + *azdMetadata.Type == azure.AzdMetadataTypeResource { + + // Require a resource type for templates. It would be practically useless without knowing the type e.g., + // you couldn't use it in a `reference()` or `resourceId()` template function, nor could you use it in an + // `existing` resource Bicep template. + if azdMetadata.ResourceType == nil || *azdMetadata.ResourceType == "" { + return nil, fmt.Errorf("resourceType required for parameter '%s'", key) + } + + resourceId, err := p.prompters.PromptResource(ctx, msg, *azdMetadata.ResourceType) + if err != nil { + return nil, err + } + if resourceId == "" { + return nil, fmt.Errorf( + "no resources of type '%s' were found for parameter '%s'", + *azdMetadata.ResourceType, + key, + ) + } + + value = resourceId } else if param.AllowedValues != nil { options := make([]string, 0, len(*param.AllowedValues)) for _, option := range *param.AllowedValues { diff --git a/cli/azd/pkg/prompt/prompter.go b/cli/azd/pkg/prompt/prompter.go index 8ad6f5e8532..08607de7baf 100644 --- a/cli/azd/pkg/prompt/prompter.go +++ b/cli/azd/pkg/prompt/prompter.go @@ -26,6 +26,7 @@ type LocationFilterPredicate func(loc account.Location) bool type Prompter interface { PromptSubscription(ctx context.Context, msg string) (subscriptionId string, err error) PromptLocation(ctx context.Context, subId string, msg string, filter LocationFilterPredicate) (string, error) + PromptResource(ctx context.Context, msg string, resourceType string) (string, error) PromptResourceGroup(ctx context.Context) (string, error) } @@ -111,6 +112,51 @@ func (p *DefaultPrompter) PromptLocation( return loc, nil } +// PromptResource uses the console (or external) prompter to allow the user to select a resource +// of optional type `resourceType`. The selected resource Name will be returned. +// If no resources of that type were found, an empty string is returned. +// +// The Name can be used with the ARM or Bicep template function `reference` or in an existing resource template's name +// to get provisioned state data from a resource, or passed to the `resourceId` function to get the full resource ID +// if you know the `resourceType`. +func (p *DefaultPrompter) PromptResource( + ctx context.Context, + msg string, + resourceType string, +) (string, error) { + options := azapi.ListResourcesOptions{ + ResourceType: resourceType, + } + resources, err := p.resourceService.ListResources(ctx, p.env.GetSubscriptionId(), &options) + if err != nil { + return "", fmt.Errorf("listing resources: %w", err) + } + if len(resources) == 0 { + return "", nil + } + + slices.SortFunc(resources, func(a, b *azapi.Resource) int { + return strings.Compare(a.Name, b.Name) + }) + + // TODO: Add `optional` field to allow "None" and return ""? + choices := make([]string, len(resources)) + for idx, resource := range resources { + // TODO: Get location display names from account manager instead? + choices[idx] = fmt.Sprintf("%d. %s (%s)", idx+1, resource.Name, resource.Location) + } + + choice, err := p.console.Select(ctx, input.ConsoleOptions{ + Message: msg, + Options: choices, + }) + if err != nil { + return "", fmt.Errorf("selecting resource: %w", err) + } + + return resources[choice].Name, nil +} + func (p *DefaultPrompter) PromptResourceGroup(ctx context.Context) (string, error) { // Get current resource groups groups, err := p.resourceService.ListResourceGroup(ctx, p.env.GetSubscriptionId(), nil)