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
}