Skip to content
23 changes: 23 additions & 0 deletions cli/azd/internal/cmd/add/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ type AddAction struct {
alphaManager *alpha.FeatureManager
creds account.SubscriptionCredentialProvider
rm infra.ResourceManager
resourceService *azapi.ResourceService
armClientOptions *arm.ClientOptions
prompter prompt.Prompter
console input.Console
Expand Down Expand Up @@ -118,6 +119,11 @@ func (a *AddAction) Run(ctx context.Context) (*actions.ActionResult, error) {
resourceToAdd = r
}

resourceToAdd, err = a.ConfigureLive(ctx, resourceToAdd, a.console, promptOpts)
if err != nil {
return nil, err
}

resourceToAdd, err = Configure(ctx, resourceToAdd, a.console, promptOpts)
if err != nil {
return nil, err
Expand Down Expand Up @@ -259,6 +265,21 @@ func (a *AddAction) Run(ctx context.Context) (*actions.ActionResult, error) {
return nil, fmt.Errorf("closing file: %w", err)
}

envModified := false
for _, resource := range resourcesToAdd {
if resource.ResourceId != "" {
a.env.DotenvSet(infra.ResourceIdName(resource.Name), resource.ResourceId)
envModified = true
}
}

if envModified {
err = a.envManager.Save(ctx, a.env)
if err != nil {
return nil, fmt.Errorf("saving environment: %w", err)
}
}

a.console.MessageUxItem(ctx, &ux.ActionResult{
SuccessMessage: "azure.yaml updated.",
})
Expand Down Expand Up @@ -414,6 +435,7 @@ func NewAddAction(
creds account.SubscriptionCredentialProvider,
prompter prompt.Prompter,
rm infra.ResourceManager,
resourceService *azapi.ResourceService,
armClientOptions *arm.ClientOptions,
azd workflow.AzdCommandRunner,
accountManager account.Manager,
Expand All @@ -428,6 +450,7 @@ func NewAddAction(
env: env,
prompter: prompter,
rm: rm,
resourceService: resourceService,
armClientOptions: armClientOptions,
creds: creds,
azd: azd,
Expand Down
36 changes: 36 additions & 0 deletions cli/azd/internal/cmd/add/add_configure.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,38 @@ var DbMap = map[appdetect.DatabaseDep]project.ResourceType{
type PromptOptions struct {
// PrjConfig is the current project configuration.
PrjConfig *project.ProjectConfig

// ExistingId is the ID of an existing resource.
// This is only used to configure the resource with an existing resource.
ExistingId string
}

// ConfigureLive fills in the fields for a resource by first querying live Azure for information.
//
// This is used in addition to Configure currently.
func (a *AddAction) ConfigureLive(
ctx context.Context,
r *project.ResourceConfig,
console input.Console,
p PromptOptions) (*project.ResourceConfig, error) {
if r.Existing {
return r, nil
}

var err error

switch r.Type {
case project.ResourceTypeAiProject:
r, err = a.promptAiModel(console, ctx, r, p)
case project.ResourceTypeOpenAiModel:
r, err = a.promptOpenAi(console, ctx, r, p)
}

if err != nil {
return nil, err
}

return r, nil
}

// Configure fills in the fields for a resource.
Expand All @@ -36,6 +68,10 @@ func Configure(
r *project.ResourceConfig,
console input.Console,
p PromptOptions) (*project.ResourceConfig, error) {
if r.Existing {
return ConfigureExisting(ctx, r, console, p)
}

switch r.Type {
case project.ResourceTypeHostContainerApp:
return fillUses(ctx, r, console, p)
Expand Down
61 changes: 61 additions & 0 deletions cli/azd/internal/cmd/add/add_configure_existing.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package add

import (
"context"

"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
"github.com/azure/azure-dev/cli/azd/internal/names"
"github.com/azure/azure-dev/cli/azd/pkg/input"
"github.com/azure/azure-dev/cli/azd/pkg/project"
)

// ConfigureExisting prompts the user to configure details for an existing resource.
func ConfigureExisting(
ctx context.Context,
r *project.ResourceConfig,
console input.Console,
p PromptOptions) (*project.ResourceConfig, error) {
if r.Name == "" {
resourceId, err := arm.ParseResourceID(r.ResourceId)
if err != nil {
return nil, err
}

for {
name, err := console.Prompt(ctx, input.ConsoleOptions{
Message: "What should we call this resource?",
Help: "This name will be used to identify the resource in your project. " +
"It will also be used to prefix environment variables by default.",
DefaultValue: names.LabelName(resourceId.Name),
})
if err != nil {
return nil, err
}

if err := names.ValidateLabelName(name); err != nil {
console.Message(ctx, err.Error())
continue
}

r.Name = name
break
}
}

return r, nil
}

// resourceType returns the resource type for the given Azure resource type.
func resourceType(azureResourceType string) project.ResourceType {
resourceTypes := project.AllResourceTypes()
for _, resourceType := range resourceTypes {
if resourceType.AzureResourceType() == azureResourceType {
return resourceType
}
}

return project.ResourceType("")
}
12 changes: 11 additions & 1 deletion cli/azd/internal/cmd/add/add_preview.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"text/tabwriter"

"github.com/azure/azure-dev/cli/azd/internal/scaffold"
"github.com/azure/azure-dev/cli/azd/pkg/environment"
"github.com/azure/azure-dev/cli/azd/pkg/infra/provisioning"
"github.com/azure/azure-dev/cli/azd/pkg/input"
"github.com/azure/azure-dev/cli/azd/pkg/output"
Expand All @@ -35,6 +36,10 @@ func Metadata(r *project.ResourceConfig) metaDisplay {
// transform to standard variables
prefix := res.StandardVarPrefix

if r.Existing {
prefix += "_" + environment.Key(r.Name)
}

// host resources are special and prefixed with the name
if strings.HasPrefix(string(r.Type), "host.") {
prefix = strings.ToUpper(r.Name)
Expand Down Expand Up @@ -90,7 +95,12 @@ func (a *AddAction) previewProvision(
fmt.Fprintln(w, "b Name\tResource type")
for _, res := range resourcesToAdd {
meta := Metadata(res)
fmt.Fprintf(w, "+ %s\t%s\n", res.Name, meta.ResourceType)
status := ""
if res.Existing {
status = " (existing)"
}

fmt.Fprintf(w, "+ %s\t%s%s\n", res.Name, meta.ResourceType, status)
}

w.Flush()
Expand Down
157 changes: 157 additions & 0 deletions cli/azd/internal/cmd/add/add_select.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ import (
"slices"
"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/azure/azure-dev/cli/azd/internal/scaffold"
"github.com/azure/azure-dev/cli/azd/pkg/azapi"
"github.com/azure/azure-dev/cli/azd/pkg/infra"
"github.com/azure/azure-dev/cli/azd/pkg/input"
"github.com/azure/azure-dev/cli/azd/pkg/project"
)
Expand All @@ -36,6 +42,7 @@ func (a *AddAction) selectMenu() []Menu {
{Namespace: "messaging", Label: "Messaging", SelectResource: selectMessaging},
{Namespace: "storage", Label: "Storage account", SelectResource: selectStorage},
{Namespace: "keyvault", Label: "Key Vault", SelectResource: selectKeyVault},
{Namespace: "existing", Label: "~Existing resource", SelectResource: a.selectExistingResource},
}
}

Expand Down Expand Up @@ -135,3 +142,153 @@ func selectKeyVault(console input.Console, ctx context.Context, p PromptOptions)
r.Type = project.ResourceTypeKeyVault
return r, nil
}

func (a *AddAction) selectExistingResource(
console input.Console,
ctx context.Context,
p PromptOptions) (*project.ResourceConfig, error) {
res := &project.ResourceConfig{}
res.Existing = true

if p.ExistingId == "" {
all := a.selectMenu()
selectMenu := make([]Menu, 0, len(all))
for _, menu := range all {
if menu.Namespace == "existing" {
continue
}

if menu.Namespace == "host" || // host resources are not yet supported
menu.Namespace == "db" { // db resources are not yet supported
continue
}

selectMenu = append(selectMenu, menu)

}

slices.SortFunc(selectMenu, func(a, b Menu) int {
return strings.Compare(a.Label, b.Label)
})

selections := make([]string, 0, len(selectMenu))
for _, menu := range selectMenu {
selections = append(selections, menu.Label)
}
idx, err := a.console.Select(ctx, input.ConsoleOptions{
Message: "Which type of existing resource?",
Options: selections,
})
if err != nil {
return nil, err
}

selected := selectMenu[idx]

r, err := selected.SelectResource(a.console, ctx, p)
if err != nil {
return nil, err
}

azureResourceType := r.Type.AzureResourceType()
resourceMeta, ok := scaffold.ResourceMetaFromType(azureResourceType)
if ok && resourceMeta.ParentForEval != "" {
azureResourceType = resourceMeta.ParentForEval
}

managedResourceIds := make([]string, 0, len(p.PrjConfig.Resources))
env := a.env.Dotenv()

for res, resCfg := range p.PrjConfig.Resources {
if resCfg.Type != r.Type {
continue
}

if resId, ok := env[infra.ResourceIdName(res)]; ok {
managedResourceIds = append(managedResourceIds, resId)
}
}

resourceId, err := a.promptResource(
ctx,
fmt.Sprintf("Which %s resource?", r.Type.String()),
azureResourceType,
managedResourceIds)
if err != nil {
return nil, fmt.Errorf("prompting for resource: %w", err)
}

if resourceId == "" {
return nil, fmt.Errorf("no resources of type '%s' were found", azureResourceType)
}

res.Type = r.Type
res.ResourceId = resourceId
} else {
resourceId, err := arm.ParseResourceID(p.ExistingId)
if err != nil {
return nil, err
}

azureResourceType := resourceId.ResourceType.String()
resourceType := resourceType(azureResourceType)
if resourceType == "" {
return nil, fmt.Errorf("resource type '%s' is not currently supported", azureResourceType)
}

res.Type = resourceType
res.ResourceId = resourceId.String()
}

return res, nil
}

func (a *AddAction) promptResource(
ctx context.Context,
msg string,
resourceType string,
excludeResourceIds []string,
) (string, error) {
options := armresources.ClientListOptions{
Filter: to.Ptr(fmt.Sprintf("resourceType eq '%s'", resourceType)),
}

a.console.ShowSpinner(ctx, "Listing resources...", input.Step)
allResources, err := a.resourceService.ListSubscriptionResources(ctx, a.env.GetSubscriptionId(), &options)
if err != nil {
return "", fmt.Errorf("listing resources: %w", err)
}

resources := make([]*azapi.ResourceExtended, 0, len(allResources))
for _, resource := range allResources {
if slices.Contains(excludeResourceIds, resource.Id) {
continue
}

resources = append(resources, resource)
}

if len(resources) == 0 {
return "", nil
}
a.console.StopSpinner(ctx, "", input.StepDone)

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

choices := make([]string, len(resources))
for idx, resource := range resources {
choices[idx] = fmt.Sprintf("%d. %s (%s)", idx+1, resource.Name, resource.Location)
}

choice, err := a.console.Select(ctx, input.ConsoleOptions{
Message: msg,
Options: choices,
})
if err != nil {
return "", fmt.Errorf("selecting resource: %w", err)
}

return resources[choice].Id, nil
}
Loading