Skip to content

Commit

Permalink
Prompt for resources with optional resourceType
Browse files Browse the repository at this point in the history
Resolves Azure#4530
  • Loading branch information
heaths committed Nov 9, 2024
1 parent f030c4c commit d14ad7b
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 0 deletions.
43 changes: 43 additions & 0 deletions cli/azd/pkg/azapi/resource_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ type ListResourceGroupResourcesOptions struct {
Filter *string
}

type ListResourcesOptions struct {
ResourceType string
}

type ResourceService struct {
credentialProvider account.SubscriptionCredentialProvider
armClientOptions *arm.ClientOptions
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions cli/azd/pkg/azure/arm_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
10 changes: 10 additions & 0 deletions cli/azd/pkg/infra/provisioning/bicep/prompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,16 @@ func (p *BicepProvider) promptForParameter(
}
value = genValue
}
} else if paramType == provisioning.ParameterTypeString &&
azdMetadata.Type != nil &&
*azdMetadata.Type == azure.AzdMetadataTypeResource {

resourceId, err := p.prompters.PromptResource(ctx, p.env.GetSubscriptionId(), msg, *azdMetadata.ResourceType)
if err != nil {
return nil, err
}

value = resourceId
} else if param.AllowedValues != nil {
options := make([]string, 0, len(*param.AllowedValues))
for _, option := range *param.AllowedValues {
Expand Down
42 changes: 42 additions & 0 deletions cli/azd/pkg/prompt/prompter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, subId string, msg string, resourceType string) (string, error)
PromptResourceGroup(ctx context.Context) (string, error)
}

Expand Down Expand Up @@ -111,6 +112,47 @@ 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.
// 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,
subId string,
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)
}

slices.SortFunc(resources, func(a, b *azapi.Resource) int {
return strings.Compare(a.Name, b.Name)
})

// TODO: Add `optional` field to allow something like "Create a new resource" (similar to resources groups below) 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)
Expand Down

0 comments on commit d14ad7b

Please sign in to comment.