func()

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
}