Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 1 addition & 12 deletions cli/azd/cmd/down.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,21 +114,10 @@ func (a *downAction) Run(ctx context.Context) (*actions.ActionResult, error) {
startTime := time.Now()

destroyOptions := provisioning.NewDestroyOptions(a.flags.forceDelete, a.flags.purgeDelete)
destroyResult, err := infraManager.Destroy(ctx, destroyOptions)
if err != nil {
if _, err = infraManager.Destroy(ctx, destroyOptions); err != nil {
return nil, fmt.Errorf("deleting infrastructure: %w", err)
}

// Remove any outputs from the template from the environment since destroying the infrastructure
// invalidated them all.
for outputName := range destroyResult.Outputs {
a.env.DotenvDelete(outputName)
}

if err := a.env.Save(); err != nil {
return nil, fmt.Errorf("saving environment: %w", err)
}

return &actions.ActionResult{
Message: &actions.ResultMessage{
Header: fmt.Sprintf("Your application was removed from Azure in %s.", ux.DurationAsText(time.Since(startTime))),
Expand Down
34 changes: 25 additions & 9 deletions cli/azd/pkg/environment/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ type Environment struct {
// dotenv is a map of keys to values, persisted to the `.env` file stored in this environment's [Root].
dotenv map[string]string

// deletedKeys keeps track of deleted keys from the `.env` to be reapplied before a merge operation
// happens in Save
deletedKeys map[string]struct{}

// Config is environment specific config
Config config.Config

Expand Down Expand Up @@ -123,17 +127,19 @@ func GetEnvironment(azdContext *azdcontext.AzdContext, name string) (*Environmen
// to a given directory when saved.
func EmptyWithRoot(root string) *Environment {
return &Environment{
Root: root,
dotenv: make(map[string]string),
Config: config.NewEmptyConfig(),
Root: root,
dotenv: make(map[string]string),
deletedKeys: make(map[string]struct{}),
Config: config.NewEmptyConfig(),
}
}

// Ephemeral returns returns an empty ephemeral environment (i.e. not backed by a file) with a set
func Ephemeral() *Environment {
return &Environment{
dotenv: make(map[string]string),
Config: config.NewEmptyConfig(),
dotenv: make(map[string]string),
deletedKeys: make(map[string]struct{}),
Config: config.NewEmptyConfig(),
}
}

Expand Down Expand Up @@ -177,6 +183,7 @@ func (e *Environment) LookupEnv(key string) (string, bool) {
// does not exist. [Save] should be called to ensure this change is persisted.
func (e *Environment) DotenvDelete(key string) {
delete(e.dotenv, key)
e.deletedKeys[key] = struct{}{}
}

// Dotenv returns a copy of the key value pairs from the .env file in the environment.
Expand All @@ -188,6 +195,7 @@ func (e *Environment) Dotenv() map[string]string {
// called to ensure this change is persisted.
func (e *Environment) DotenvSet(key string, value string) {
e.dotenv[key] = value
delete(e.deletedKeys, key)
}

// Reloads environment variables and configuration
Expand All @@ -196,10 +204,12 @@ func (e *Environment) Reload() error {
envPath := filepath.Join(e.Root, azdcontext.DotEnvFileName)
if envMap, err := godotenv.Read(envPath); errors.Is(err, os.ErrNotExist) {
e.dotenv = make(map[string]string)
e.deletedKeys = make(map[string]struct{})
} else if err != nil {
return fmt.Errorf("loading .env: %w", err)
} else {
e.dotenv = envMap
e.deletedKeys = make(map[string]struct{})
}

// Reload env config
Expand Down Expand Up @@ -239,6 +249,7 @@ func (e *Environment) Save() error {

// Cache current values & reload to get any new env vars
currentValues := e.dotenv
deletedValues := e.deletedKeys
if err := e.Reload(); err != nil {
return fmt.Errorf("failed reloading env vars, %w", err)
}
Expand All @@ -248,6 +259,11 @@ func (e *Environment) Save() error {
e.dotenv[key] = value
}

// Replay deletion
for key := range deletedValues {
delete(e.dotenv, key)
}

err := os.MkdirAll(e.Root, osutil.PermissionDirectory)
if err != nil {
return fmt.Errorf("failed to create a directory: %w", err)
Expand All @@ -269,7 +285,7 @@ func (e *Environment) GetEnvName() string {

// SetEnvName is shorthand for DotenvSet(EnvNameEnvVarName, envname)
func (e *Environment) SetEnvName(envname string) {
e.dotenv[EnvNameEnvVarName] = envname
e.DotenvSet(EnvNameEnvVarName, envname)
}

// GetSubscriptionId is shorthand for Getenv(SubscriptionIdEnvVarName)
Expand All @@ -284,7 +300,7 @@ func (e *Environment) GetTenantId() string {

// SetLocation is shorthand for DotenvSet(SubscriptionIdEnvVarName, location)
func (e *Environment) SetSubscriptionId(id string) {
e.dotenv[SubscriptionIdEnvVarName] = id
e.DotenvSet(SubscriptionIdEnvVarName, id)
}

// GetLocation is shorthand for Getenv(LocationEnvVarName)
Expand All @@ -294,7 +310,7 @@ func (e *Environment) GetLocation() string {

// SetLocation is shorthand for DotenvSet(LocationEnvVarName, location)
func (e *Environment) SetLocation(location string) {
e.dotenv[LocationEnvVarName] = location
e.DotenvSet(LocationEnvVarName, location)
}

func normalize(key string) string {
Expand All @@ -308,7 +324,7 @@ func (e *Environment) GetServiceProperty(serviceName string, propertyName string

// Sets the value of a service-namespaced property in the environment.
func (e *Environment) SetServiceProperty(serviceName string, propertyName string, value string) {
e.dotenv[fmt.Sprintf("SERVICE_%s_%s", normalize(serviceName), propertyName)] = value
e.DotenvSet(fmt.Sprintf("SERVICE_%s_%s", normalize(serviceName), propertyName), value)
}

// Creates a slice of key value pairs, based on the entries in the `.env` file like `KEY=VALUE` that
Expand Down
22 changes: 22 additions & 0 deletions cli/azd/pkg/environment/environment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,28 @@ func Test_SaveAndReload(t *testing.T) {
require.Equal(t, env.dotenv["SERVICE_API_ENDPOINT_URL"], "http://api.example.com")
require.Equal(t, "SUBSCRIPTION_ID", env.GetSubscriptionId())
require.Equal(t, "eastus2", env.GetLocation())

// Delete the newly added property
env.DotenvDelete("SERVICE_WEB_ENDPOINT_URL")
err = env.Save()
require.NoError(t, err)

// Verify the property is deleted
_, ok := env.LookupEnv("SERVICE_WEB_ENDPOINT_URL")
require.False(t, ok)

// Delete an existing key, then add it with a different value and save the environment, to ensure we
// don't drop the existing key even though it was deleted in an earlier operation.
env.DotenvDelete("SERVICE_API_ENDPOINT_URL")
env.DotenvSet("SERVICE_API_ENDPOINT_URL", "http://api.example.com/updated")

err = env.Save()
require.NoError(t, err)

// Verify the property still exists, and has the updated value.
value, ok := env.LookupEnv("SERVICE_API_ENDPOINT_URL")
require.True(t, ok)
require.Equal(t, "http://api.example.com/updated", value)
}

func TestCleanName(t *testing.T) {
Expand Down
8 changes: 8 additions & 0 deletions cli/azd/test/functional/up_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,14 @@ func Test_CLI_Up_Down_ContainerApp(t *testing.T) {

_, err = cli.RunCommand(ctx, "infra", "delete", "--force", "--purge")
require.NoError(t, err)

// As part of deleting the infrastructure, outputs of the infrastructure such as "WEBSITE_URL" should
// have been removed from the environment.
env, err = godotenv.Read(filepath.Join(dir, azdcontext.EnvironmentDirectoryName, envName, ".env"))
require.NoError(t, err)

_, has = env["WEBSITE_URL"]
require.False(t, has, "WEBSITE_URL should have been removed from the environment as part of infrastructure removal")
}

func Test_CLI_Up_ResourceGroupScope(t *testing.T) {
Expand Down