Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
ea2dd7d
WIP
vhvb1989 Sep 23, 2024
f2cce1c
Merge branch 'main' of github.com:Azure/azure-dev into azd-env-add-se…
vhvb1989 Nov 2, 2024
68016bf
create vault and assign role
vhvb1989 Nov 5, 2024
dabb1d0
Merge branch 'main' of github.com:Azure/azure-dev into azd-env-add-se…
vhvb1989 Nov 5, 2024
10ae74c
linter
vhvb1989 Nov 5, 2024
fb49321
create or select secret
vhvb1989 Nov 5, 2024
c3b90e0
provision - bicep - get secrets
vhvb1989 Nov 5, 2024
b7fb14a
Merge branch 'main' of github.com:Azure/azure-dev into azd-env-add-se…
vhvb1989 Nov 6, 2024
daeed99
resolve in hooks
vhvb1989 Nov 6, 2024
2b35c9f
snap usage
vhvb1989 Nov 6, 2024
17562e7
Merge branch 'main' of github.com:Azure/azure-dev into azd-env-add-se…
vhvb1989 Nov 6, 2024
cde24fa
Merge branch 'main' of github.com:Azure/azure-dev into azd-env-add-se…
vhvb1989 Nov 6, 2024
7905a94
Merge branch 'main' of github.com:Azure/azure-dev into azd-env-add-se…
vhvb1989 Jan 9, 2025
3b781dc
update to latest
vhvb1989 Jan 9, 2025
882303d
Merge branch 'main' of github.com:Azure/azure-dev into azd-env-add-se…
vhvb1989 Jan 14, 2025
90a87c3
update msges
vhvb1989 Jan 15, 2025
a4c7970
Merge branch 'main' of github.com:Azure/azure-dev into azd-env-add-se…
vhvb1989 Jan 16, 2025
a2b0f12
wording changes
vhvb1989 Jan 16, 2025
11a95ab
update snap
vhvb1989 Jan 16, 2025
318b943
Merge branch 'main' of github.com:Azure/azure-dev into azd-env-add-se…
vhvb1989 Jan 17, 2025
efb68c5
recordings
vhvb1989 Jan 17, 2025
c303c85
add subId as part of akvs
vhvb1989 Jan 20, 2025
726729e
Merge branch 'main' of github.com:Azure/azure-dev into azd-env-add-se…
vhvb1989 Jan 20, 2025
941eeb0
Merge branch 'main' of github.com:Azure/azure-dev into azd-env-add-se…
vhvb1989 Jan 22, 2025
6c3dfc0
validate secret name on client side
vhvb1989 Jan 22, 2025
f7bac70
Adding functional tests and recording
vhvb1989 Jan 23, 2025
9ea9d69
better recording handling
vhvb1989 Jan 23, 2025
e64ebb4
Merge branch 'main' of github.com:Azure/azure-dev into azd-env-add-se…
vhvb1989 Jan 23, 2025
5ea98e9
Merge branch 'main' of github.com:Azure/azure-dev into azd-env-add-se…
vhvb1989 Jan 25, 2025
fda205c
rename key vault account to just key vault
vhvb1989 Jan 25, 2025
5c89019
recording
vhvb1989 Jan 27, 2025
4238976
add subcription variable
vhvb1989 Jan 27, 2025
5f0e60d
Merge branch 'main' of github.com:Azure/azure-dev into azd-env-add-se…
vhvb1989 Jan 28, 2025
9d80cbd
Apply suggestions from code review
vhvb1989 Jan 28, 2025
4a8b377
remove env var for avoid numbers and use the console isInteractive
vhvb1989 Jan 28, 2025
2d17c75
Merge branch 'azd-env-add-secret' of github.com:vhvb1989/azure-dev in…
vhvb1989 Jan 28, 2025
da7ef83
skip in playback
vhvb1989 Jan 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions cli/azd/.vscode/cspell-azd-dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ ABRT
ACCESSTOKEN
aiomysql
aiopg
akvs
alphafeatures
apimanagement
apims
Expand Down Expand Up @@ -59,9 +60,9 @@ Backticks
bicepparam
blockblob
BOOLSLICE
buildargs
BUILDID
BUILDNUMBER
buildargs
buildpacks
byoi
cflags
Expand All @@ -80,12 +81,12 @@ csharpapptest
cupaloy
custommaps
deletedservices
deviceid
devcenter
devcenters
devcentersdk
devdeviceid
devel
deviceid
diffmatchpatch
discarder
docf
Expand Down Expand Up @@ -238,4 +239,4 @@ wireinject
yacspin
yamlnode
ymlt
zerr
zerr
322 changes: 322 additions & 0 deletions cli/azd/cmd/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,26 @@ import (
"errors"
"fmt"
"io"
"strings"
"time"

"github.com/azure/azure-dev/cli/azd/cmd/actions"
"github.com/azure/azure-dev/cli/azd/internal"
"github.com/azure/azure-dev/cli/azd/pkg/account"
"github.com/azure/azure-dev/cli/azd/pkg/azapi"
"github.com/azure/azure-dev/cli/azd/pkg/azureutil"
"github.com/azure/azure-dev/cli/azd/pkg/entraid"
"github.com/azure/azure-dev/cli/azd/pkg/environment"
"github.com/azure/azure-dev/cli/azd/pkg/environment/azdcontext"
"github.com/azure/azure-dev/cli/azd/pkg/infra/provisioning"
"github.com/azure/azure-dev/cli/azd/pkg/infra/provisioning/bicep"
"github.com/azure/azure-dev/cli/azd/pkg/input"
"github.com/azure/azure-dev/cli/azd/pkg/keyvault"
"github.com/azure/azure-dev/cli/azd/pkg/output"
"github.com/azure/azure-dev/cli/azd/pkg/output/ux"
"github.com/azure/azure-dev/cli/azd/pkg/project"
"github.com/azure/azure-dev/cli/azd/pkg/prompt"
"github.com/sethvargo/go-retry"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
Expand All @@ -45,6 +52,17 @@ func envActions(root *actions.ActionDescriptor) *actions.ActionDescriptor {
ActionResolver: newEnvSetAction,
})

group.Add("set-secret", &actions.ActionDescriptorOptions{
Comment thread
vhvb1989 marked this conversation as resolved.
Command: &cobra.Command{
Use: "set-secret <name>",
Short: "Set a <name> as a reference to a Key Vault secret in the environment.",
Comment thread
vhvb1989 marked this conversation as resolved.
Long: "You can either create a new Key Vault secret or select an existing one.\n" +
"The provided name is the key for the .env file which holds the secret reference to the Key Vault secret.",
},
FlagsResolver: newEnvSetSecretFlags,
ActionResolver: newEnvSetSecretAction,
})

group.Add("select", &actions.ActionDescriptorOptions{
Command: newEnvSelectCmd(),
ActionResolver: newEnvSelectAction,
Expand Down Expand Up @@ -150,6 +168,310 @@ func (e *envSetAction) Run(ctx context.Context) (*actions.ActionResult, error) {
return nil, nil
}

func newEnvSetSecretFlags(cmd *cobra.Command, global *internal.GlobalCommandOptions) *envSetSecretFlags {
flags := &envSetSecretFlags{}
flags.Bind(cmd.Flags(), global)

return flags
}

type envSetSecretFlags struct {
internal.EnvFlag
global *internal.GlobalCommandOptions
}

func (f *envSetSecretFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandOptions) {
f.EnvFlag.Bind(local, global)
f.global = global
}

type envSetSecretAction struct {
console input.Console
azdCtx *azdcontext.AzdContext
env *environment.Environment
envManager environment.Manager
flags *envSetFlags
args []string
prompter prompt.Prompter
kvService keyvault.KeyVaultService
entraIdService entraid.EntraIdService
subResolver account.SubscriptionTenantResolver
userProfileService *azapi.UserProfileService
}

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

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{}
// indexOffset makes the ids to start from 1 instead of 0 when displaying the options
indexOffset := 1
if willCreateNewSecret {
selectKvAccountOptions = append(selectKvAccountOptions, createNewKvAccountOption)
}

for index, vault := range vaultsList {
if listWithoutNumbers {
selectKvAccountOptions = append(selectKvAccountOptions, vault.Name)
} else {
selectKvAccountOptions = append(selectKvAccountOptions, fmt.Sprintf("%2d. %s", index+indexOffset, 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 := selectKvAccountOptions[kvAccountSelectionIndex] == createNewKvAccountOption
if willCreateNewSecret && !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)
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
Comment thread
vhvb1989 marked this conversation as resolved.
}
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 {
for {
kvSecretName, err = e.console.Prompt(ctx, input.ConsoleOptions{
Message: "Enter a name for the Key Vault secret",
DefaultValue: strings.ReplaceAll(secretName, "_", "-") + "-kv-secret",
Comment thread
vhvb1989 marked this conversation as resolved.
})
if err != nil {
return nil, fmt.Errorf("prompting for Key Vault secret name: %w", err)
}
if keyvault.IsValidSecretName(kvSecretName) {
break
}
e.console.Message(ctx, "Invalid Key Vault secret name. The name must be between 1 and 127 characters"+
" long and can contain only alphanumeric characters and dashes.")
}

kvSecretValue, err := e.console.Prompt(ctx, input.ConsoleOptions{
Message: "Enter the value for the Key Vault secret",
IsPassword: true,
})
if err != nil {
return nil, fmt.Errorf("prompting for secret value: %w", err)
}
// Creating a secret in a new account too soon can fail due to rbac role assignment not being ready
err = retry.Do(
ctx,
retry.WithMaxRetries(3, retry.NewConstant(5*time.Second)),
func(ctx context.Context) error {
err = e.kvService.CreateKeyVaultSecret(ctx, subId, kvAccount.Name, kvSecretName, kvSecretValue)
if err != nil {
return retry.RetryableError(fmt.Errorf("creating Key Vault secret: %w", err))
}
return nil
},
)
if err != nil {
return nil, fmt.Errorf("setting Key Vault secret: %w", err)
}
} else {
secretsInKv, err := e.kvService.ListKeyVaultSecrets(ctx, subId, kvAccount.Name)
if err != nil {
return nil, fmt.Errorf("listing Key Vault secrets: %w", err)
}
if len(secretsInKv) == 0 {
return nil, fmt.Errorf("no Key Vault secrets were found in the selected Key Vault")
}
options := make([]string, len(secretsInKv))
for i, secret := range secretsInKv {
if listWithoutNumbers {
options[i] = secret
} else {
options[i] = fmt.Sprintf("%2d. %s", i+1, secret)
}
}
secretSelectionIndex, err := e.console.Select(ctx, input.ConsoleOptions{
Message: "Select the Key Vault secret",
Options: options,
DefaultValue: options[0],
})
if err != nil {
return nil, fmt.Errorf("selecting Key Vault secret: %w", err)
}
kvSecretName = secretsInKv[secretSelectionIndex]
}

// akvs -> Azure Key Vault Secret (akvs://<subId>/<keyvault-name>/<secret-name>)
envValue := keyvault.NewAkvs(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 &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(kvAccount.Name)),
FollowUp: fmt.Sprintf("Learn how to use Key Vault secrets with azd and more: %s",
output.WithLinkFormat("https://aka.ms/azd-env-set-secret")),
},
}, nil
}

func newEnvSetSecretAction(
azdCtx *azdcontext.AzdContext,
env *environment.Environment,
envManager environment.Manager,
console input.Console,
flags *envSetFlags,
args []string,
prompter prompt.Prompter,
kvService keyvault.KeyVaultService,
entraIdService entraid.EntraIdService,
subResolver account.SubscriptionTenantResolver,
userProfileService *azapi.UserProfileService,
) actions.Action {
return &envSetSecretAction{
console: console,
azdCtx: azdCtx,
env: env,
envManager: envManager,
flags: flags,
args: args,
prompter: prompter,
kvService: kvService,
entraIdService: entraIdService,
subResolver: subResolver,
userProfileService: userProfileService,
}
}

func newEnvSelectCmd() *cobra.Command {
return &cobra.Command{
Use: "select <environment>",
Expand Down
Loading