Skip to content

compose: existing support #4943

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

Merged
merged 13 commits into from
Apr 1, 2025
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