func()

in cli/azd/cmd/env.go [250:523]


func (e *envSetSecretAction) Run(ctx context.Context) (*actions.ActionResult, error) {
	if len(e.args) < 1 {
		return nil, fmt.Errorf(
			"no <name> provided. Please provide a name as argument like: 'azd env set-secret <name>'")
	}
	secretName := e.args[0]

	// When no interactive is supported in the terminal azd will not add numbers to the list when
	// asking to select options. For example, instead of showing "1. Option 1", it will show "Option 1". This is useful
	// when the user wants to prefill the selection in stdin before calling azd env set-secret (e.g. in a script).
	listWithoutNumbers := !e.console.IsSpinnerInteractive()

	createNewStrategy := "Create a new Key Vault secret"
	selectExistingStrategy := "Select an existing Key Vault secret"
	setSecretStrategies := []string{createNewStrategy, selectExistingStrategy}
	selectedStrategyIndex, err := e.console.Select(
		ctx,
		input.ConsoleOptions{
			Message:      "Select how you want to set " + secretName,
			Options:      setSecretStrategies,
			DefaultValue: createNewStrategy,
			Help: "When creating a new Key Vault secret, you can either create a new Key Vault or" +
				" pick an existing one. A Key Vault secret belongs to a Key Vault.",
		})
	if err != nil {
		return nil, fmt.Errorf("selecting secret setting strategy: %w", err)
	}

	willCreateNewSecret := setSecretStrategies[selectedStrategyIndex] == createNewStrategy

	createSuccessResult := func(secretName, kvSecretName, kvName string) *actions.ActionResult {
		return &actions.ActionResult{
			Message: &actions.ResultMessage{
				Header: fmt.Sprintf("The key %s was saved in the environment as a reference to the"+
					" Key Vault secret %s from the Key Vault %s",
					output.WithBackticks(secretName),
					output.WithBackticks(kvSecretName),
					output.WithBackticks(kvName)),
				FollowUp: fmt.Sprintf("Learn how to use Key Vault secrets with azd and more: %s",
					output.WithLinkFormat("https://aka.ms/azd-env-set-secret")),
			},
		}
	}

	// Provide shortcuts for using the Key Vault created by composability (azd add)
	if e.alphaFeatureManager.IsEnabled(featureCompose) {
		if kvId, hasComposeKv := e.env.LookupEnv("AZURE_RESOURCE_VAULT_ID"); hasComposeKv { // KV is provisioned
			resId, err := arm.ParseResourceID(kvId)
			if err != nil {
				return nil, fmt.Errorf("parsing key vault resource id: %w", err)
			}
			kvName := resId.Name
			kvSubId := resId.SubscriptionID
			subscriptionOptions := []string{"Yes", "No, use different key vault"}
			useProjectKvPrompt, err := e.console.Select(
				ctx,
				input.ConsoleOptions{
					Message:      "Key vault detected in this project. Use this key vault?",
					Options:      subscriptionOptions,
					DefaultValue: subscriptionOptions[0],
				})

			if err != nil {
				return nil, fmt.Errorf("selecting key vault option: %w", err)
			}

			if useProjectKvPrompt == 0 { // Use project Key Vault
				kvAccount := keyvault.Vault{
					Name: kvName,
					Id:   kvId,
				}

				var kvSecretName string
				if willCreateNewSecret {
					kvSecretName, err = e.createNewKeyVaultSecret(ctx, secretName, kvSubId, kvAccount.Name)

				} else {
					kvSecretName, err = e.selectKeyVaultSecret(ctx, kvSubId, kvAccount.Name)
				}
				if err != nil {
					return nil, err
				}

				envValue := keyvault.NewAzureKeyVaultSecret(kvSubId, kvAccount.Name, kvSecretName)
				e.env.DotenvSet(secretName, envValue)
				if err := e.envManager.Save(ctx, e.env); err != nil {
					return nil, fmt.Errorf("saving environment: %w", err)
				}

				return createSuccessResult(secretName, kvSecretName, kvAccount.Name), nil
			}
		} else if _, hasProjectKv := e.projectConfig.Resources["vault"]; hasProjectKv { // KV defined but not provisioned yet
			e.console.Message(ctx,
				output.WithWarningFormat("\nAn existing project key vault is defined but is not provisioned yet. ")+
					fmt.Sprintf("Run '%s' first to use it.\n", output.WithHighLightFormat("azd provision")))
			options := []string{"Use a different key vault", "Cancel"}
			useProjectKvPrompt, err := e.console.Select(
				ctx,
				input.ConsoleOptions{
					Message:      "How do you want to proceed?",
					Options:      options,
					DefaultValue: options[0],
				})

			if err != nil {
				return nil, fmt.Errorf("selecting key vault option: %w", err)
			}
			if useProjectKvPrompt == 1 { // Cancel
				return nil, fmt.Errorf("operation cancelled. Run 'azd provision' to provision the project Key Vault first")
			}
		}
	}

	subscriptionNote := "\nYou can set the Key Vault secret from any Azure subscription where you have access to."
	e.console.Message(ctx, subscriptionNote)

	// default messages based on willCreateNewSecret == true
	pickSubscription := "Select the subscription where you want to create the Key Vault secret"
	pickKvAccount := "Select the Key Vault where you want to create the Key Vault secret"

	if !willCreateNewSecret {
		// reassign messages for selecting existing secret
		pickSubscription = "Select the subscription where the Key Vault secret is"
		pickKvAccount = "Select the Key Vault where the Key Vault secret is"
	}

	subId, err := e.prompter.PromptSubscription(ctx, pickSubscription)
	if err != nil {
		return nil, fmt.Errorf("prompting for subscription: %w", err)
	}
	tenantId, err := e.subResolver.LookupTenant(ctx, subId)
	if err != nil {
		return nil, fmt.Errorf("looking up tenant for subscription: %w", err)
	}

	e.console.ShowSpinner(ctx, "Finding Key Vaults from the selected subscription", input.Step)
	vaultsList, err := e.kvService.ListSubscriptionVaults(ctx, subId)
	if err != nil {
		return nil, fmt.Errorf("getting the list of Key Vaults: %w", err)
	}
	// prompt for vault selection
	e.console.StopSpinner(ctx, "", input.Step)

	atLeastOneKvAccountExists := len(vaultsList) > 0
	if !atLeastOneKvAccountExists && !willCreateNewSecret {
		e.console.MessageUxItem(ctx, &ux.WarningMessage{
			Description: "No Azure Key Vaults were found in the selected subscription",
		})
		// update the flow to offer creating a new Key Vault
		willCreateNewSecret = true
	}

	createNewKvAccountOption := "Create a new Key Vault"
	selectKvAccountOptions := []string{}

	// Create a combined list with "Create a new Key Vault" as the first option
	if willCreateNewSecret {
		if listWithoutNumbers {
			selectKvAccountOptions = append(selectKvAccountOptions, createNewKvAccountOption)
		} else {
			selectKvAccountOptions = append(selectKvAccountOptions, fmt.Sprintf("%2d. %s", 1, createNewKvAccountOption))
		}
	}

	// Add the existing vaults with adjusted numbering
	for index, vault := range vaultsList {
		if listWithoutNumbers {
			selectKvAccountOptions = append(selectKvAccountOptions, vault.Name)
		} else {
			offset := 1
			// Existing KVs start at #2 since #1 will be "Create a new Key Vault"
			if willCreateNewSecret {
				offset = 2
			}
			selectKvAccountOptions = append(selectKvAccountOptions, fmt.Sprintf("%2d. %s", index+offset, vault.Name))
		}
	}

	kvAccountSelectionIndex, err := e.console.Select(ctx, input.ConsoleOptions{
		Message:      pickKvAccount,
		Options:      selectKvAccountOptions,
		DefaultValue: selectKvAccountOptions[0],
	})
	if err != nil {
		return nil, fmt.Errorf("selecting Key Vault: %w", err)
	}

	willCreateNewKvAccount := false
	if willCreateNewSecret {
		willCreateNewKvAccount = kvAccountSelectionIndex == 0
		if !willCreateNewKvAccount {
			// when willCreateNewSecret is true, we added a new option at the beginning of the list
			// to recover the original kv account name
			kvAccountSelectionIndex--
		}
	}

	var kvAccount keyvault.Vault
	if atLeastOneKvAccountExists {
		kvAccount = vaultsList[kvAccountSelectionIndex]
	}

	if willCreateNewKvAccount {
		location, err := e.prompter.PromptLocation(
			ctx, subId, "Select the location to create the Key Vault", nil, nil)
		if err != nil {
			return nil, fmt.Errorf("prompting for Key Vault location: %w", err)
		}
		rg, err := e.prompter.PromptResourceGroupFrom(ctx, subId, location, prompt.PromptResourceGroupFromOptions{
			DefaultName:          "rg-for-my-key-vault",
			NewResourceGroupHelp: "The name of the new resource group where the Key Vault will be created.",
		})
		if err != nil {
			return nil, fmt.Errorf("prompting for resource group: %w", err)
		}

		kvAccountName := ""
		for {
			kvAccountNameInput, err := e.console.Prompt(ctx, input.ConsoleOptions{
				Message: "Enter a name for the Key Vault",
				Help:    "The name must be unique within the subscription and must be between 3 and 24 characters long",
			})
			if err != nil {
				return nil, fmt.Errorf("prompting for Key Vault name: %w", err)
			}
			if kvAccountNameInput == "" {
				e.console.Message(ctx, "Key Vault name cannot be empty")
				continue
			}
			kvAccountName = kvAccountNameInput
			break
		}

		e.console.ShowSpinner(ctx, "Creating Key Vault", input.Step)
		vault, err := e.kvService.CreateVault(ctx, tenantId, subId, rg, location, kvAccountName)
		e.console.StopSpinner(ctx, "", input.Step)
		if err != nil {
			return nil, fmt.Errorf("error creating Key Vault: %w", err)
		}
		kvAccount = vault

		// RBAC role assignment
		e.console.ShowSpinner(ctx, "Adding Administrator Role", input.Step)
		principalId, err := azureutil.GetCurrentPrincipalId(ctx, e.userProfileService, tenantId)
		if err != nil {
			return nil, fmt.Errorf("getting current principal ID: %w", err)
		}
		err = e.entraIdService.CreateRbac(
			ctx, subId, kvAccount.Id, keyvault.RoleIdKeyVaultAdministrator, principalId)
		if err != nil {
			return nil, fmt.Errorf("adding Administrator Role: %w", err)
		}
		e.console.StopSpinner(ctx, "", input.Step)
	}

	var kvSecretName string
	if willCreateNewSecret {
		kvSecretName, err = e.createNewKeyVaultSecret(ctx, secretName, subId, kvAccount.Name)
	} else {
		kvSecretName, err = e.selectKeyVaultSecret(ctx, subId, kvAccount.Name)
	}
	if err != nil {
		return nil, err
	}

	// akvs -> Azure Key Vault Secret (akvs://<subId>/<keyvault-name>/<secret-name>)
	envValue := keyvault.NewAzureKeyVaultSecret(subId, kvAccount.Name, kvSecretName)
	e.env.DotenvSet(secretName, envValue)
	if err := e.envManager.Save(ctx, e.env); err != nil {
		return nil, fmt.Errorf("saving environment: %w", err)
	}

	return createSuccessResult(secretName, kvSecretName, kvAccount.Name), nil
}