cli/azd/cmd/env.go (1,003 lines of code) (raw):
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package cmd
import (
"context"
"errors"
"fmt"
"io"
"slices"
"strings"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
"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/alpha"
"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"
)
var featureCompose = alpha.MustFeatureKey("compose")
func envActions(root *actions.ActionDescriptor) *actions.ActionDescriptor {
group := root.Add("env", &actions.ActionDescriptorOptions{
Command: &cobra.Command{
Use: "env",
Short: "Manage environments.",
},
HelpOptions: actions.ActionHelpOptions{
Description: getCmdEnvHelpDescription,
},
GroupingOptions: actions.CommandGroupOptions{
RootLevelHelp: actions.CmdGroupManage,
},
})
group.Add("set", &actions.ActionDescriptorOptions{
Command: newEnvSetCmd(),
FlagsResolver: newEnvSetFlags,
ActionResolver: newEnvSetAction,
})
group.Add("set-secret", &actions.ActionDescriptorOptions{
Command: &cobra.Command{
Use: "set-secret <name>",
Short: "Set a <name> as a reference to a Key Vault secret in the environment.",
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,
})
group.Add("new", &actions.ActionDescriptorOptions{
Command: newEnvNewCmd(),
FlagsResolver: newEnvNewFlags,
ActionResolver: newEnvNewAction,
})
group.Add("list", &actions.ActionDescriptorOptions{
Command: newEnvListCmd(),
ActionResolver: newEnvListAction,
OutputFormats: []output.Format{output.JsonFormat, output.TableFormat},
DefaultFormat: output.TableFormat,
})
group.Add("refresh", &actions.ActionDescriptorOptions{
Command: newEnvRefreshCmd(),
FlagsResolver: newEnvRefreshFlags,
ActionResolver: newEnvRefreshAction,
OutputFormats: []output.Format{output.JsonFormat, output.NoneFormat},
DefaultFormat: output.NoneFormat,
})
group.Add("get-values", &actions.ActionDescriptorOptions{
Command: newEnvGetValuesCmd(),
FlagsResolver: newEnvGetValuesFlags,
ActionResolver: newEnvGetValuesAction,
OutputFormats: []output.Format{output.JsonFormat, output.EnvVarsFormat},
DefaultFormat: output.EnvVarsFormat,
})
group.Add("get-value", &actions.ActionDescriptorOptions{
Command: newEnvGetValueCmd(),
FlagsResolver: newEnvGetValueFlags,
ActionResolver: newEnvGetValueAction,
})
return group
}
func newEnvSetFlags(cmd *cobra.Command, global *internal.GlobalCommandOptions) *envSetFlags {
flags := &envSetFlags{}
flags.Bind(cmd.Flags(), global)
return flags
}
func newEnvSetCmd() *cobra.Command {
return &cobra.Command{
Use: "set <key> <value>",
Short: "Manage your environment settings.",
Args: cobra.ExactArgs(2),
}
}
type envSetFlags struct {
internal.EnvFlag
global *internal.GlobalCommandOptions
}
func (f *envSetFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandOptions) {
f.EnvFlag.Bind(local, global)
f.global = global
}
type envSetAction struct {
console input.Console
azdCtx *azdcontext.AzdContext
env *environment.Environment
envManager environment.Manager
flags *envSetFlags
args []string
}
func newEnvSetAction(
azdCtx *azdcontext.AzdContext,
env *environment.Environment,
envManager environment.Manager,
console input.Console,
flags *envSetFlags,
args []string,
) actions.Action {
return &envSetAction{
console: console,
azdCtx: azdCtx,
env: env,
envManager: envManager,
flags: flags,
args: args,
}
}
func (e *envSetAction) Run(ctx context.Context) (*actions.ActionResult, error) {
key := e.args[0]
value := e.args[1]
dotEnv := e.env.Dotenv()
warnKeyCaseConflicts(ctx, e.console, dotEnv, key)
e.env.DotenvSet(key, value)
if err := e.envManager.Save(ctx, e.env); err != nil {
return nil, fmt.Errorf("saving environment: %w", err)
}
return nil, nil
}
// Prints a warning message if there are any case-insensitive conflicts with the provided key
func warnKeyCaseConflicts(
ctx context.Context,
console input.Console,
dotEnv map[string]string,
key string) {
var conflicts []string
for k := range dotEnv {
if strings.EqualFold(k, key) && k != key {
conflicts = append(conflicts, "'"+k+"'")
}
}
if len(conflicts) == 1 {
console.MessageUxItem(ctx,
&ux.WarningMessage{
Description: fmt.Sprintf(
"'%s' already exists as %s. Did you mean to set %s instead?",
key,
conflicts[0],
conflicts[0]),
})
} else if len(conflicts) > 1 {
slices.Sort(conflicts)
console.MessageUxItem(ctx,
&ux.WarningMessage{
Description: fmt.Sprintf(
"'%s' already exists as %s",
key,
ux.ListAsText(conflicts)),
})
}
}
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
alphaFeatureManager *alpha.FeatureManager
projectConfig *project.ProjectConfig
}
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
}
// createNewKeyVaultSecret creates a new secret in an Azure Key Vault and returns the name of the created secret.
func (e *envSetSecretAction) createNewKeyVaultSecret(ctx context.Context, secretName, subId, kvName string) (string, error) {
var kvSecretName string
var err error
for {
kvSecretName, err = e.console.Prompt(ctx, input.ConsoleOptions{
Message: "Enter a name for the Key Vault secret",
DefaultValue: strings.ReplaceAll(secretName, "_", "-") + "-kv-secret",
})
if err != nil {
return "", 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 "", 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, kvName, kvSecretName, kvSecretValue)
if err != nil {
return retry.RetryableError(fmt.Errorf("creating Key Vault secret: %w", err))
}
return nil
},
)
if err != nil {
return "", fmt.Errorf("setting Key Vault secret: %w", err)
}
return kvSecretName, nil
}
// selectKeyVaultSecret presents a selection list of secrets from the specified Key Vault and
// returns the selected secret name.
func (e *envSetSecretAction) selectKeyVaultSecret(ctx context.Context, subId string, kvName string) (string, error) {
listWithoutNumbers := !e.console.IsSpinnerInteractive()
secretsInKv, err := e.kvService.ListKeyVaultSecrets(ctx, subId, kvName)
if err != nil {
return "", fmt.Errorf("listing Key Vault secrets: %w", err)
}
if len(secretsInKv) == 0 {
return "", 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 "", fmt.Errorf("selecting Key Vault secret: %w", err)
}
return secretsInKv[secretSelectionIndex], 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,
alphaFeatureManager *alpha.FeatureManager,
projectConfig *project.ProjectConfig,
) 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,
alphaFeatureManager: alphaFeatureManager,
projectConfig: projectConfig,
}
}
func newEnvSelectCmd() *cobra.Command {
return &cobra.Command{
Use: "select <environment>",
Short: "Set the default environment.",
Args: cobra.ExactArgs(1),
}
}
type envSelectAction struct {
azdCtx *azdcontext.AzdContext
envManager environment.Manager
args []string
}
func newEnvSelectAction(azdCtx *azdcontext.AzdContext, envManager environment.Manager, args []string) actions.Action {
return &envSelectAction{
azdCtx: azdCtx,
envManager: envManager,
args: args,
}
}
func (e *envSelectAction) Run(ctx context.Context) (*actions.ActionResult, error) {
_, err := e.envManager.Get(ctx, e.args[0])
if errors.Is(err, environment.ErrNotFound) {
return nil, fmt.Errorf(
`environment '%s' does not exist. You can create it with "azd env new %s"`,
e.args[0],
e.args[0],
)
} else if err != nil {
return nil, fmt.Errorf("ensuring environment exists: %w", err)
}
if err := e.azdCtx.SetProjectState(azdcontext.ProjectState{DefaultEnvironment: e.args[0]}); err != nil {
return nil, fmt.Errorf("setting default environment: %w", err)
}
return nil, nil
}
func newEnvListCmd() *cobra.Command {
return &cobra.Command{
Use: "list",
Short: "List environments.",
Aliases: []string{"ls"},
}
}
type envListAction struct {
envManager environment.Manager
azdCtx *azdcontext.AzdContext
formatter output.Formatter
writer io.Writer
}
func newEnvListAction(
envManager environment.Manager,
azdCtx *azdcontext.AzdContext,
formatter output.Formatter,
writer io.Writer,
) actions.Action {
return &envListAction{
envManager: envManager,
azdCtx: azdCtx,
formatter: formatter,
writer: writer,
}
}
func (e *envListAction) Run(ctx context.Context) (*actions.ActionResult, error) {
envs, err := e.envManager.List(ctx)
if err != nil {
return nil, fmt.Errorf("listing environments: %w", err)
}
if e.formatter.Kind() == output.TableFormat {
columns := []output.Column{
{
Heading: "NAME",
ValueTemplate: "{{.Name}}",
},
{
Heading: "DEFAULT",
ValueTemplate: "{{.IsDefault}}",
},
{
Heading: "LOCAL",
ValueTemplate: "{{.HasLocal}}",
},
{
Heading: "REMOTE",
ValueTemplate: "{{.HasRemote}}",
},
}
err = e.formatter.Format(envs, e.writer, output.TableFormatterOptions{
Columns: columns,
})
} else {
err = e.formatter.Format(envs, e.writer, nil)
}
if err != nil {
return nil, err
}
return nil, nil
}
type envNewFlags struct {
subscription string
location string
global *internal.GlobalCommandOptions
}
func (f *envNewFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandOptions) {
local.StringVar(
&f.subscription,
"subscription",
"",
"Name or ID of an Azure subscription to use for the new environment",
)
local.StringVarP(&f.location, "location", "l", "", "Azure location for the new environment")
f.global = global
}
func newEnvNewFlags(cmd *cobra.Command, global *internal.GlobalCommandOptions) *envNewFlags {
flags := &envNewFlags{}
flags.Bind(cmd.Flags(), global)
return flags
}
func newEnvNewCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "new <environment>",
Short: "Create a new environment and set it as the default.",
}
cmd.Args = cobra.MaximumNArgs(1)
return cmd
}
type envNewAction struct {
azdCtx *azdcontext.AzdContext
envManager environment.Manager
flags *envNewFlags
args []string
console input.Console
}
func newEnvNewAction(
azdCtx *azdcontext.AzdContext,
envManager environment.Manager,
flags *envNewFlags,
args []string,
console input.Console,
) actions.Action {
return &envNewAction{
azdCtx: azdCtx,
envManager: envManager,
flags: flags,
args: args,
console: console,
}
}
func (en *envNewAction) Run(ctx context.Context) (*actions.ActionResult, error) {
environmentName := ""
if len(en.args) >= 1 {
environmentName = en.args[0]
}
envSpec := environment.Spec{
Name: environmentName,
Subscription: en.flags.subscription,
Location: en.flags.location,
}
env, err := en.envManager.Create(ctx, envSpec)
if err != nil {
return nil, fmt.Errorf("creating new environment: %w", err)
}
if err := en.azdCtx.SetProjectState(azdcontext.ProjectState{DefaultEnvironment: env.Name()}); err != nil {
return nil, fmt.Errorf("saving default environment: %w", err)
}
return nil, nil
}
type envRefreshFlags struct {
hint string
global *internal.GlobalCommandOptions
internal.EnvFlag
}
func (er *envRefreshFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandOptions) {
local.StringVarP(&er.hint, "hint", "", "", "Hint to help identify the environment to refresh")
er.EnvFlag.Bind(local, global)
er.global = global
}
func newEnvRefreshFlags(cmd *cobra.Command, global *internal.GlobalCommandOptions) *envRefreshFlags {
flags := &envRefreshFlags{}
flags.Bind(cmd.Flags(), global)
return flags
}
func newEnvRefreshCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "refresh <environment>",
Short: "Refresh environment settings by using information from a previous infrastructure provision.",
// We want to support the usual -e / --environment arguments as all our commands which take environments do, but for
// ergonomics, we'd also like you to be able to run `azd env refresh some-environment-name` to behave the same way as
// `azd env refresh -e some-environment-name` would have.
Args: func(cmd *cobra.Command, args []string) error {
if err := cobra.MaximumNArgs(1)(cmd, args); err != nil {
return err
}
if len(args) == 0 {
return nil
}
if flagValue, err := cmd.Flags().GetString(internal.EnvironmentNameFlagName); err == nil {
if flagValue != "" && args[0] != flagValue {
return errors.New(
"the --environment flag and an explicit environment name as an argument may not be used together")
}
}
return cmd.Flags().Set(internal.EnvironmentNameFlagName, args[0])
},
Annotations: map[string]string{},
}
// This is like the Use property above, but does not include the hint to show an environment name is supported. This
// is used by some tests which need to construct a valid command line to run `azd` and here using `<environment>` would
// be invalid, since it is an invalid name.
cmd.Annotations["azdtest.use"] = "refresh"
return cmd
}
type envRefreshAction struct {
provisionManager *provisioning.Manager
projectConfig *project.ProjectConfig
projectManager project.ProjectManager
env *environment.Environment
envManager environment.Manager
prompters prompt.Prompter
flags *envRefreshFlags
console input.Console
formatter output.Formatter
writer io.Writer
importManager *project.ImportManager
}
func newEnvRefreshAction(
provisionManager *provisioning.Manager,
projectConfig *project.ProjectConfig,
projectManager project.ProjectManager,
env *environment.Environment,
envManager environment.Manager,
prompters prompt.Prompter,
flags *envRefreshFlags,
console input.Console,
formatter output.Formatter,
writer io.Writer,
importManager *project.ImportManager,
) actions.Action {
return &envRefreshAction{
provisionManager: provisionManager,
projectManager: projectManager,
env: env,
envManager: envManager,
prompters: prompters,
console: console,
flags: flags,
formatter: formatter,
projectConfig: projectConfig,
writer: writer,
importManager: importManager,
}
}
func (ef *envRefreshAction) Run(ctx context.Context) (*actions.ActionResult, error) {
// Command title
ef.console.MessageUxItem(ctx, &ux.MessageTitle{
Title: fmt.Sprintf("Refreshing environment %s (azd env refresh)", ef.env.Name()),
})
if err := ef.projectManager.Initialize(ctx, ef.projectConfig); err != nil {
return nil, err
}
if err := ef.projectManager.EnsureAllTools(ctx, ef.projectConfig, nil); err != nil {
return nil, err
}
infra, err := ef.importManager.ProjectInfrastructure(ctx, ef.projectConfig)
if err != nil {
return nil, err
}
defer func() { _ = infra.Cleanup() }()
// env refresh supports "BYOI" infrastructure where bicep isn't available
err = ef.provisionManager.Initialize(ctx, ef.projectConfig.Path, infra.Options)
if errors.Is(err, bicep.ErrEnsureEnvPreReqBicepCompileFailed) {
// If bicep is not available, we continue to prompt for subscription and location unfiltered
err = provisioning.EnsureSubscriptionAndLocation(ctx, ef.envManager, ef.env, ef.prompters,
provisioning.EnsureSubscriptionAndLocationOptions{})
if err != nil {
return nil, err
}
} else if err != nil {
return nil, fmt.Errorf("initializing provisioning manager: %w", err)
}
// If resource group is defined within the project but not in the environment then
// add it to the environment to support BYOI lookup scenarios like ADE
// Infra providers do not currently have access to project configuration
projectResourceGroup, _ := ef.projectConfig.ResourceGroupName.Envsubst(ef.env.Getenv)
if _, has := ef.env.LookupEnv(environment.ResourceGroupEnvVarName); !has && projectResourceGroup != "" {
ef.env.DotenvSet(environment.ResourceGroupEnvVarName, projectResourceGroup)
}
stateOptions := provisioning.NewStateOptions(ef.flags.hint)
getStateResult, err := ef.provisionManager.State(ctx, stateOptions)
if err != nil {
return nil, fmt.Errorf("getting deployment: %w", err)
}
if err := ef.provisionManager.UpdateEnvironment(ctx, getStateResult.State.Outputs); err != nil {
return nil, err
}
if ef.formatter.Kind() == output.JsonFormat {
err = ef.formatter.Format(provisioning.NewEnvRefreshResultFromState(getStateResult.State), ef.writer, nil)
if err != nil {
return nil, fmt.Errorf("writing deployment result in JSON format: %w", err)
}
}
servicesStable, err := ef.importManager.ServiceStable(ctx, ef.projectConfig)
if err != nil {
return nil, err
}
for _, svc := range servicesStable {
eventArgs := project.ServiceLifecycleEventArgs{
Project: ef.projectConfig,
Service: svc,
Args: map[string]any{
"bicepOutput": getStateResult.State.Outputs,
},
}
if err := svc.RaiseEvent(ctx, project.ServiceEventEnvUpdated, eventArgs); err != nil {
return nil, err
}
}
localEnvPath := ef.envManager.EnvPath(ef.env)
return &actions.ActionResult{
Message: &actions.ResultMessage{
Header: "Environment refresh completed",
FollowUp: fmt.Sprintf("View environment variables at %s", output.WithHyperlink(localEnvPath, localEnvPath)),
},
}, nil
}
func newEnvGetValuesFlags(cmd *cobra.Command, global *internal.GlobalCommandOptions) *envGetValuesFlags {
flags := &envGetValuesFlags{}
flags.Bind(cmd.Flags(), global)
return flags
}
func newEnvGetValuesCmd() *cobra.Command {
return &cobra.Command{
Use: "get-values",
Short: "Get all environment values.",
}
}
type envGetValuesFlags struct {
internal.EnvFlag
global *internal.GlobalCommandOptions
}
func (eg *envGetValuesFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandOptions) {
eg.EnvFlag.Bind(local, global)
eg.global = global
}
type envGetValuesAction struct {
azdCtx *azdcontext.AzdContext
console input.Console
envManager environment.Manager
formatter output.Formatter
writer io.Writer
flags *envGetValuesFlags
}
func newEnvGetValuesAction(
azdCtx *azdcontext.AzdContext,
envManager environment.Manager,
console input.Console,
formatter output.Formatter,
writer io.Writer,
flags *envGetValuesFlags,
) actions.Action {
return &envGetValuesAction{
azdCtx: azdCtx,
console: console,
envManager: envManager,
formatter: formatter,
writer: writer,
flags: flags,
}
}
func (eg *envGetValuesAction) Run(ctx context.Context) (*actions.ActionResult, error) {
name, err := eg.azdCtx.GetDefaultEnvironmentName()
if err != nil {
return nil, err
}
// Note: if there is not an environment yet, GetDefaultEnvironmentName() returns empty string (not error)
// and later, when envManager.Get() is called with the empty string, azd returns an error.
// But if there is already an environment (default to be selected), azd must honor the --environment flag
// over the default environment.
if eg.flags.EnvironmentName != "" {
name = eg.flags.EnvironmentName
}
env, err := eg.envManager.Get(ctx, name)
if errors.Is(err, environment.ErrNotFound) {
return nil, fmt.Errorf(
`"environment does not exist. You can create it with "azd env new"`,
)
} else if err != nil {
return nil, fmt.Errorf("ensuring environment exists: %w", err)
}
return nil, eg.formatter.Format(env.Dotenv(), eg.writer, nil)
}
func newEnvGetValueFlags(cmd *cobra.Command, global *internal.GlobalCommandOptions) *envGetValueFlags {
flags := &envGetValueFlags{}
flags.Bind(cmd.Flags(), global)
return flags
}
func newEnvGetValueCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "get-value <keyName>",
Short: "Get specific environment value.",
}
cmd.Args = cobra.MaximumNArgs(1)
return cmd
}
type envGetValueFlags struct {
internal.EnvFlag
global *internal.GlobalCommandOptions
}
func (eg *envGetValueFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandOptions) {
eg.EnvFlag.Bind(local, global)
eg.global = global
}
type envGetValueAction struct {
azdCtx *azdcontext.AzdContext
console input.Console
envManager environment.Manager
writer io.Writer
flags *envGetValueFlags
args []string
}
func newEnvGetValueAction(
azdCtx *azdcontext.AzdContext,
envManager environment.Manager,
console input.Console,
writer io.Writer,
flags *envGetValueFlags,
args []string,
) actions.Action {
return &envGetValueAction{
azdCtx: azdCtx,
console: console,
envManager: envManager,
writer: writer,
flags: flags,
args: args,
}
}
func (eg *envGetValueAction) Run(ctx context.Context) (*actions.ActionResult, error) {
if len(eg.args) < 1 {
return nil, fmt.Errorf("no key name provided")
}
keyName := eg.args[0]
name, err := eg.azdCtx.GetDefaultEnvironmentName()
if err != nil {
return nil, err
}
// Note: if there is not an environment yet, GetDefaultEnvironmentName() returns empty string (not error)
// and later, when envManager.Get() is called with the empty string, azd returns an error.
// But if there is already an environment (default to be selected), azd must honor the --environment flag
// over the default environment.
if eg.flags.EnvironmentName != "" {
name = eg.flags.EnvironmentName
}
env, err := eg.envManager.Get(ctx, name)
if errors.Is(err, environment.ErrNotFound) {
return nil, fmt.Errorf(
`environment '%s' does not exist. You can create it with "azd env new %s"`,
name,
name,
)
} else if err != nil {
return nil, fmt.Errorf("ensuring environment exists: %w", err)
}
values := env.Dotenv()
keyValue, exists := values[keyName]
if !exists {
return nil, fmt.Errorf("key '%s' not found in the environment values", keyName)
}
// Directly write the key value to the writer
if _, err := fmt.Fprintln(eg.writer, keyValue); err != nil {
return nil, fmt.Errorf("writing key value: %w", err)
}
return nil, nil
}
func getCmdEnvHelpDescription(*cobra.Command) string {
return generateCmdHelpDescription(
"Manage your application environments. With this command group, you can create a new environment or get, set,"+
" and list your application environments.",
[]string{
formatHelpNote("An Application can have multiple environments (ex: dev, test, prod)."),
formatHelpNote("Each environment may have a different configuration (that is, connectivity information)" +
" for accessing Azure resources."),
formatHelpNote(fmt.Sprintf("You can find all environment configuration under the %s folder.",
output.WithLinkFormat(".azure/<environment-name>"))),
formatHelpNote(fmt.Sprintf("The environment name is stored as the %s environment variable in the %s file.",
output.WithHighLightFormat("AZURE_ENV_NAME"),
output.WithLinkFormat(".azure/<environment-name>/.env"))),
})
}