Skip to content

Commit

Permalink
Prompt user to select or create new resource
Browse files Browse the repository at this point in the history
  • Loading branch information
wbreza committed Oct 9, 2024
1 parent e18ead3 commit c6e5b15
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 3 deletions.
38 changes: 38 additions & 0 deletions cli/azd/pkg/azapi/resource_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,44 @@ func (rs *ResourceService) GetResource(
}, nil
}

func (rs *ResourceService) ListSubscriptionResources(
ctx context.Context,
subscriptionId string,
listOptions *armresources.ClientListOptions,
) ([]*Resource, error) {
client, err := rs.createResourcesClient(ctx, subscriptionId)
if err != nil {
return nil, err
}

// Filter expression on the underlying REST API are different from --query param in az cli.
// https://learn.microsoft.com/en-us/rest/api/resources/resources/list-by-resource-group#uri-parameters
options := armresources.ClientListOptions{}
if listOptions != nil && *listOptions.Filter != "" {
options.Filter = listOptions.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) ListResourceGroupResources(
ctx context.Context,
subscriptionId string,
Expand Down
25 changes: 22 additions & 3 deletions cli/azd/pkg/azure/arm_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,16 +97,35 @@ type AutoGenInput struct {
MinSpecial *uint `json:"minSpecial,omitempty"`
}

// ResourceInputMetadata is set on ARM/Bicep parameter properties
// This metadata is used to generate a resource picker in the CLI
type ResourceInputMetadata struct {
DisplayName string `json:"displayName"`
Description string `json:"description"`
Type string `json:"type"`
}

// OptionalResource is used to represent a resource that may or may not exist in the Azure subscription.
// This value is used as an input value to the ARM/Bicep parameter for parameters with azd type resource metadata.
type OptionalResource struct {
Name string `json:"name"`
SubscriptionId string `json:"subscriptionId"`
ResourceGroup string `json:"resourceGroup"`
Exists bool `json:"exists"`
}

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"`
Type *AzdMetadataType `json:"type,omitempty"`
AutoGenerateConfig *AutoGenInput `json:"config,omitempty"`
DefaultValueExpr *string `json:"defaultValueExpr,omitempty"`
Resource *ResourceInputMetadata `json:"resource,omitempty"`
}

// Description returns the value of the "Description" string metadata for this parameter or empty if it can not be found.
Expand Down
46 changes: 46 additions & 0 deletions cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -1880,6 +1880,52 @@ func (p *BicepProvider) ensureParameters(
continue
}

// Prompt user to specify an existing resource to link to the template
if hasMetadata &&
azdMetadata.Type != nil && *azdMetadata.Type == azure.AzdMetadataTypeResource &&
azdMetadata.Resource != nil {

// Prompt user to select an existing resource to link to the template
selectedResource, err := p.prompters.PromptResource(ctx, prompt.PromptResourceOptions{
ResourceType: azdMetadata.Resource.Type,
DisplayName: azdMetadata.Resource.DisplayName,
Description: azdMetadata.Resource.Description,
})

if err != nil {
return nil, fmt.Errorf("prompting for resource: %w", err)
}

armResourceValue := azure.OptionalResource{}

// User opted to create a new resource
if selectedResource.Id == "" {
armResourceValue.Exists = false
armResourceValue.Name = selectedResource.Name
armResourceValue.SubscriptionId = p.env.GetSubscriptionId()
armResourceValue.ResourceGroup = p.env.Getenv(environment.ResourceGroupEnvVarName)
} else {
// User selected an existing resource
parsedResource, err := arm.ParseResourceID(selectedResource.Id)
if err != nil {
return nil, fmt.Errorf("parsing resource id: %w", err)
}

armResourceValue.Exists = true
armResourceValue.Name = parsedResource.Name
armResourceValue.SubscriptionId = parsedResource.SubscriptionID
armResourceValue.ResourceGroup = parsedResource.ResourceGroupName
}

configuredParameters[key] = azure.ArmParameterValue{
Value: armResourceValue,
}

mustSetParamAsConfig(key, armResourceValue, p.env.Config, false)
configModified = true
continue
}

// No saved value for this required parameter, we'll need to prompt for it.
parameterPrompts = append(parameterPrompts, struct {
key string
Expand Down
70 changes: 70 additions & 0 deletions cli/azd/pkg/prompt/prompter.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ import (
"fmt"
"log"
"os"
"path/filepath"
"slices"
"strconv"
"strings"

"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"
"github.com/MakeNowJust/heredoc/v2"
"github.com/azure/azure-dev/cli/azd/pkg/account"
"github.com/azure/azure-dev/cli/azd/pkg/azapi"
Expand All @@ -27,6 +30,7 @@ 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)
PromptResourceGroup(ctx context.Context) (string, error)
PromptResource(ctx context.Context, options PromptResourceOptions) (*azapi.Resource, error)
}

type DefaultPrompter struct {
Expand Down Expand Up @@ -160,6 +164,72 @@ func (p *DefaultPrompter) PromptResourceGroup(ctx context.Context) (string, erro
return name, nil
}

type PromptResourceOptions struct {
ResourceType string
DisplayName string
Description string
}

// PromptResource prompts the user to select a resource with the specified resource type.
// If the user selects to create a new resource, the user will be prompted to enter a name for the new resource.
// This new resource is intended to be created in the Bicep deployment
func (p *DefaultPrompter) PromptResource(ctx context.Context, options PromptResourceOptions) (*azapi.Resource, error) {
resourceListOptions := armresources.ClientListOptions{
Filter: to.Ptr(fmt.Sprintf("resourceType eq '%s'", options.ResourceType)),
}

if options.DisplayName == "" {
options.DisplayName = filepath.Base(options.ResourceType)
}

resourceTypeDisplayName := strings.ToLower(options.DisplayName)

resources, err := p.resourceService.ListSubscriptionResources(ctx, p.env.GetSubscriptionId(), &resourceListOptions)
if err != nil {
return nil, fmt.Errorf("listing subscription resources: %w", err)
}

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

choices := make([]string, len(resources)+1)
choices[0] = fmt.Sprintf("Create a new %s", resourceTypeDisplayName)

for idx, resource := range resources {
parsedResource, err := arm.ParseResourceID(*&resource.Id)

Check failure on line 200 in cli/azd/pkg/prompt/prompter.go

View workflow job for this annotation

GitHub Actions / azd-lint (ubuntu-latest)

SA4001: *&x will be simplified to x. It will not copy x. (staticcheck)
if err != nil {
return nil, fmt.Errorf("parsing resource id: %w", err)
}

choices[idx+1] = fmt.Sprintf("%d. %s (Resource Group: %s)", idx+1, resource.Name, parsedResource.ResourceGroupName)
}

selectedIndex, err := p.console.Select(ctx, input.ConsoleOptions{
Message: fmt.Sprintf("Select a %s to use:", resourceTypeDisplayName),
Options: choices,
Help: options.Description,
})
if err != nil {
return nil, fmt.Errorf("selecting %s: %w", resourceTypeDisplayName, err)
}

if selectedIndex > 0 {
return resources[selectedIndex-1], nil
}

name, err := p.console.Prompt(ctx, input.ConsoleOptions{
Message: fmt.Sprintf("Enter a name for the new %s:", resourceTypeDisplayName),
})
if err != nil {
return nil, fmt.Errorf("prompting for %s name: %w", resourceTypeDisplayName, err)
}

return &azapi.Resource{
Name: name,
}, nil
}

func (p *DefaultPrompter) getSubscriptionOptions(ctx context.Context) ([]string, []string, any, error) {
subscriptionInfos, err := p.accountManager.GetSubscriptions(ctx)
if err != nil {
Expand Down

0 comments on commit c6e5b15

Please sign in to comment.