in cli/azd/internal/cmd/add/add.go [63:388]
func (a *AddAction) Run(ctx context.Context) (*actions.ActionResult, error) {
if !a.alphaManager.IsEnabled(composeFeature) {
return nil, fmt.Errorf(
"compose is currently under alpha support and must be explicitly enabled."+
" Run `%s` to enable this feature", alpha.GetEnableCommand(composeFeature),
)
}
prjConfig, err := project.Load(ctx, a.azdCtx.ProjectPath())
if err != nil {
return nil, err
}
// Having a subscription is required for any azd compose (add)
err = provisioning.EnsureSubscription(ctx, a.envManager, a.env, a.prompter)
if err != nil {
return nil, err
}
selectMenu := a.selectMenu()
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: "What would you like to add?",
Options: selections,
})
if err != nil {
return nil, err
}
selected := selectMenu[idx]
resourceToAdd := &project.ResourceConfig{}
var serviceToAdd *project.ServiceConfig
promptOpts := PromptOptions{PrjConfig: prjConfig}
r, err := selected.SelectResource(a.console, ctx, promptOpts)
if err != nil {
return nil, err
}
resourceToAdd = r
if strings.EqualFold(selected.Namespace, "host") {
svc, r, err := a.configureHost(a.console, ctx, promptOpts, r.Type)
if err != nil {
return nil, err
}
serviceToAdd = svc
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
}
usedBy, err := promptUsedBy(ctx, resourceToAdd, a.console, promptOpts)
if err != nil {
return nil, err
}
if r, exists := prjConfig.Resources[resourceToAdd.Name]; exists && r.Type != project.ResourceTypeAiProject {
log.Panicf("unhandled validation: resource with name %s already exists", resourceToAdd.Name)
}
if serviceToAdd != nil {
if _, exists := prjConfig.Services[serviceToAdd.Name]; exists {
log.Panicf("unhandled validation: service with name %s already exists", serviceToAdd.Name)
}
}
file, err := os.OpenFile(a.azdCtx.ProjectPath(), os.O_RDWR, osutil.PermissionFile)
if err != nil {
return nil, fmt.Errorf("reading project file: %w", err)
}
defer file.Close()
decoder := yaml.NewDecoder(file)
decoder.SetScanBlockScalarAsLiteral(true)
var doc yaml.Node
err = decoder.Decode(&doc)
if err != nil {
return nil, fmt.Errorf("failed to decode: %w", err)
}
if serviceToAdd != nil {
serviceNode, err := yamlnode.Encode(serviceToAdd)
if err != nil {
panic(fmt.Sprintf("encoding yaml node: %v", err))
}
err = yamlnode.Set(&doc, fmt.Sprintf("services?.%s", serviceToAdd.Name), serviceNode)
if err != nil {
return nil, fmt.Errorf("adding service: %w", err)
}
}
resourcesToAdd := []*project.ResourceConfig{resourceToAdd}
dependentResources := project.DependentResourcesOf(resourceToAdd)
requiredByMessages := make([]string, 0)
// Find any dependent resources that are not already in the project
for _, dep := range dependentResources {
if prjConfig.Resources[dep.Name] == nil {
resourcesToAdd = append(resourcesToAdd, dep)
requiredByMessages = append(requiredByMessages,
fmt.Sprintf("(%s is required by %s)",
output.WithHighLightFormat(dep.Name),
output.WithHighLightFormat(resourceToAdd.Name)))
}
}
// Add resource and any non-existing dependent resources
for _, resource := range resourcesToAdd {
resourceNode, err := yamlnode.Encode(resource)
if err != nil {
panic(fmt.Sprintf("encoding resource yaml node: %v", err))
}
err = yamlnode.Set(&doc, fmt.Sprintf("resources?.%s", resource.Name), resourceNode)
if err != nil {
return nil, fmt.Errorf("setting resource: %w", err)
}
}
for _, svc := range usedBy {
err = yamlnode.Append(&doc, fmt.Sprintf("resources.%s.uses[]?", svc), &yaml.Node{
Kind: yaml.ScalarNode,
Value: resourceToAdd.Name,
})
if err != nil {
return nil, fmt.Errorf("appending resource: %w", err)
}
}
new, err := yaml.Marshal(&doc)
if err != nil {
return nil, fmt.Errorf("marshalling yaml: %w", err)
}
newCfg, err := project.Parse(ctx, string(new))
if err != nil {
return nil, fmt.Errorf("re-parsing yaml: %w", err)
}
a.console.Message(ctx, fmt.Sprintf("\nPreviewing changes to %s:\n", output.WithHighLightFormat("azure.yaml")))
diffString, diffErr := DiffBlocks(prjConfig.Resources, newCfg.Resources)
if diffErr != nil {
a.console.Message(ctx, "Preview unavailable. Pass --debug for more details.\n")
log.Printf("add-diff: preview failed: %v", diffErr)
} else {
a.console.Message(ctx, diffString)
if len(requiredByMessages) > 0 {
for _, msg := range requiredByMessages {
a.console.Message(ctx, msg)
}
a.console.Message(ctx, "")
}
}
confirm, err := a.console.Confirm(ctx, input.ConsoleOptions{
Message: "Accept changes to azure.yaml?",
DefaultValue: true,
})
if err != nil || !confirm {
return nil, err
}
// Write modified YAML back to file
err = file.Truncate(0)
if err != nil {
return nil, fmt.Errorf("truncating file: %w", err)
}
_, err = file.Seek(0, io.SeekStart)
if err != nil {
return nil, fmt.Errorf("seeking to start of file: %w", err)
}
encoder := yaml.NewEncoder(file)
encoder.SetIndent(2)
// preserve multi-line blocks style
encoder.SetAssumeBlockAsLiteral(true)
err = encoder.Encode(&doc)
if err != nil {
return nil, fmt.Errorf("failed to encode: %w", err)
}
err = file.Close()
if err != nil {
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.",
})
// Use default project values for Infra when not specified in azure.yaml
if prjConfig.Infra.Module == "" {
prjConfig.Infra.Module = project.DefaultModule
}
if prjConfig.Infra.Path == "" {
prjConfig.Infra.Path = project.DefaultPath
}
infraRoot := prjConfig.Infra.Path
if !filepath.IsAbs(infraRoot) {
infraRoot = filepath.Join(prjConfig.Path, infraRoot)
}
var followUpMessage string
addedKeyVault := slices.ContainsFunc(resourcesToAdd, func(resource *project.ResourceConfig) bool {
return strings.EqualFold(resource.Name, "vault")
})
keyVaultFollowUpMessage := fmt.Sprintf(
"\nRun '%s' to add a secret to the key vault.",
output.WithHighLightFormat("azd env set-secret <name>"))
if _, err := pathHasInfraModule(infraRoot, prjConfig.Infra.Module); err == nil {
followUpMessage = fmt.Sprintf(
"Run '%s' to re-synthesize the infrastructure, "+
"then run '%s' to provision these changes anytime later.",
output.WithHighLightFormat("azd infra synth"),
output.WithHighLightFormat("azd provision"))
if addedKeyVault {
followUpMessage += keyVaultFollowUpMessage
}
return &actions.ActionResult{
Message: &actions.ResultMessage{
FollowUp: followUpMessage,
},
}, err
}
verb := "provision"
verbCapitalized := "Provision"
followUpCmd := "provision"
if serviceToAdd != nil {
verb = "provision and deploy"
verbCapitalized = "Provision and deploy"
followUpCmd = "up"
}
a.console.Message(ctx, "")
provisionOption, err := selectProvisionOptions(
ctx,
a.console,
fmt.Sprintf("Do you want to %s these changes?", verb))
if err != nil {
return nil, err
}
if provisionOption == provisionPreview {
err = a.previewProvision(ctx, prjConfig, resourcesToAdd, usedBy)
if err != nil {
return nil, err
}
y, err := a.console.Confirm(ctx, input.ConsoleOptions{
Message: fmt.Sprintf("%s these changes to Azure?", verbCapitalized),
DefaultValue: true,
})
if err != nil {
return nil, err
}
if !y {
provisionOption = provisionSkip
} else {
provisionOption = provision
}
}
if provisionOption == provision {
a.azd.SetArgs([]string{followUpCmd})
err = a.azd.ExecuteContext(ctx)
if err != nil {
return nil, err
}
followUpMessage = "Run '" +
output.WithHighLightFormat("azd show %s", resourceToAdd.Name) +
"' to show details about the newly provisioned resource."
} else {
followUpMessage = fmt.Sprintf(
"Run '%s' to %s these changes anytime later.",
output.WithHighLightFormat("azd %s", followUpCmd),
verb)
}
if addedKeyVault {
followUpMessage += keyVaultFollowUpMessage
}
return &actions.ActionResult{
Message: &actions.ResultMessage{
FollowUp: followUpMessage,
},
}, err
}