Skip to content

Add --no-wait option to azd down command #5217

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
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
Binary file added azd
Binary file not shown.
23 changes: 21 additions & 2 deletions cli/azd/cmd/down.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
type downFlags struct {
forceDelete bool
purgeDelete bool
noWait bool
global *internal.GlobalCommandOptions
internal.EnvFlag
}
Expand All @@ -38,6 +39,12 @@ func (i *downFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandOpt
//nolint:lll
"Does not require confirmation before it permanently deletes resources that are soft-deleted by default (for example, key vaults).",
)
local.BoolVar(
&i.noWait,
"no-wait",
false,
"Does not wait for the destroy operation to complete before returning. Note: When used with --purge, the command will still wait for purge operations to complete.",
)
i.EnvFlag.Bind(local, global)
i.global = global
}
Expand Down Expand Up @@ -109,14 +116,25 @@ func (a *downAction) Run(ctx context.Context) (*actions.ActionResult, error) {
a.console.WarnForFeature(ctx, azapi.FeatureDeploymentStacks)
}

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

var header string
if a.flags.noWait {
if a.flags.purgeDelete {
header = "Azure resources are being deleted and purged. Waiting for purge operations to complete..."
} else {
header = "Azure resource deletion has started and is continuing in the background."
}
} else {
header = fmt.Sprintf("Your application was removed from Azure in %s.", ux.DurationAsText(since(startTime)))
}

return &actions.ActionResult{
Message: &actions.ResultMessage{
Header: fmt.Sprintf("Your application was removed from Azure in %s.", ux.DurationAsText(since(startTime))),
Header: header,
},
}, nil
}
Expand All @@ -134,5 +152,6 @@ func getCmdDownHelpFooter(*cobra.Command) string {
"Forcibly delete all applications resources without confirmation.": output.WithHighLightFormat("azd down --force"),
"Permanently delete resources that are soft-deleted by default," +
" without confirmation.": output.WithHighLightFormat("azd down --purge"),
"Start the delete process but do not wait for it to complete.": output.WithHighLightFormat("azd down --no-wait"),
})
}
2 changes: 1 addition & 1 deletion cli/azd/internal/vsrpc/environment_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ func (s *environmentService) DeleteEnvironmentAsync(
}

// Enable force and purge options
destroyOptions := provisioning.NewDestroyOptions(true, true)
destroyOptions := provisioning.NewDestroyOptions(true, true, false)
_, err = c.provisionManager.Destroy(ctx, destroyOptions)
if errors.Is(err, infra.ErrDeploymentsNotFound) || errors.Is(err, infra.ErrDeploymentResourcesNotFound) {
_ = observer.OnNext(ctx, newInfoProgressMessage("No Azure resources were found"))
Expand Down
4 changes: 2 additions & 2 deletions cli/azd/pkg/devcenter/provision_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ func Test_ProvisionProvider_Destroy(t *testing.T) {
Return(outputParams, nil)

provider := newProvisionProviderForTest(t, mockContext, config, env, manager)
destroyOptions := provisioning.NewDestroyOptions(true, true)
destroyOptions := provisioning.NewDestroyOptions(true, true, false)
result, err := provider.Destroy(*mockContext.Context, destroyOptions)
require.NoError(t, err)
require.NotNil(t, result)
Expand All @@ -419,7 +419,7 @@ func Test_ProvisionProvider_Destroy(t *testing.T) {
mockdevcentersdk.MockDeleteEnvironment(mockContext, config.Project, config.User, env.Name(), nil)

provider := newProvisionProviderForTest(t, mockContext, config, env, nil)
destroyOptions := provisioning.NewDestroyOptions(true, true)
destroyOptions := provisioning.NewDestroyOptions(true, true, false)
result, err := provider.Destroy(*mockContext.Context, destroyOptions)
require.Error(t, err)
require.Nil(t, result)
Expand Down
4 changes: 2 additions & 2 deletions cli/azd/pkg/infra/provisioning/bicep/bicep_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ func TestBicepDestroy(t *testing.T) {

infraProvider := createBicepProvider(t, mockContext)

destroyOptions := provisioning.NewDestroyOptions(false, false)
destroyOptions := provisioning.NewDestroyOptions(false, false, false)
destroyResult, err := infraProvider.Destroy(*mockContext.Context, destroyOptions)

require.Nil(t, err)
Expand All @@ -212,7 +212,7 @@ func TestBicepDestroy(t *testing.T) {

infraProvider := createBicepProvider(t, mockContext)

destroyOptions := provisioning.NewDestroyOptions(true, true)
destroyOptions := provisioning.NewDestroyOptions(true, true, false)
destroyResult, err := infraProvider.Destroy(*mockContext.Context, destroyOptions)

require.Nil(t, err)
Expand Down
29 changes: 29 additions & 0 deletions cli/azd/pkg/infra/provisioning/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,35 @@ func (m *Manager) Preview(ctx context.Context) (*DeployPreviewResult, error) {

// Destroys the Azure infrastructure for the specified project
func (m *Manager) Destroy(ctx context.Context, options DestroyOptions) (*DestroyResult, error) {
// If noWait is true but purge is also true, we need to perform the operation synchronously
// to ensure purge operations complete, which prevents issues when running azd up later
if options.NoWait() && !options.Purge() {
// Create a new context that won't be cancelled when the parent context is done
newCtx := context.Background()
// Run the destroy operation in a goroutine and don't wait for it to complete
go func() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot In a Go, when the main function reaches the end, the program exits immediately—even if there are running goroutines in the background. Go does not automatically wait for them to finish.

The Azd down command not only triggers and handles deleting resources, it also purge any soft-deleted resources like KeyVault. AZD can only call the delete requests without waiting, but that would sacrifice the purge operation, leaving soft-deleted resources, which would block running azd up again, as some resources' name would be in deleted mode, blocking a new resource to use the same name

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing this out. I've fixed the implementation in 6366171 to address the issue with goroutines exiting prematurely. Now when --purge is true, the command will not use the async behavior even if --no-wait is specified, ensuring critical purge operations complete. I've also updated the help text and output messages to explain this behavior to users.

destroyResult, err := m.provider.Destroy(newCtx, options)
if err != nil {
// Just log the error since we can't return it
return
}

// Remove any outputs from the template from the environment since destroying the infrastructure
// invalidated them all.
for _, key := range destroyResult.InvalidatedEnvKeys {
m.env.DotenvDelete(key)
}

// Update environment files to remove invalid infrastructure parameters
_ = m.envManager.Save(newCtx, m.env)
}()

// Return immediately with an empty result
return &DestroyResult{
InvalidatedEnvKeys: []string{},
}, nil
}

destroyResult, err := m.provider.Destroy(ctx, options)
if err != nil {
return nil, fmt.Errorf("error deleting Azure resources: %w", err)
Expand Down
4 changes: 2 additions & 2 deletions cli/azd/pkg/infra/provisioning/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ func TestManagerDestroyWithPositiveConfirmation(t *testing.T) {
err := mgr.Initialize(*mockContext.Context, "", provisioning.Options{Provider: "test"})
require.NoError(t, err)

destroyOptions := provisioning.NewDestroyOptions(false, false)
destroyOptions := provisioning.NewDestroyOptions(false, false, false)
destroyResult, err := mgr.Destroy(*mockContext.Context, destroyOptions)

require.NotNil(t, destroyResult)
Expand Down Expand Up @@ -214,7 +214,7 @@ func TestManagerDestroyWithNegativeConfirmation(t *testing.T) {
err := mgr.Initialize(*mockContext.Context, "", provisioning.Options{Provider: "test"})
require.NoError(t, err)

destroyOptions := provisioning.NewDestroyOptions(false, false)
destroyOptions := provisioning.NewDestroyOptions(false, false, false)
destroyResult, err := mgr.Destroy(*mockContext.Context, destroyOptions)

require.Nil(t, destroyResult)
Expand Down
13 changes: 10 additions & 3 deletions cli/azd/pkg/infra/provisioning/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ type DestroyOptions struct {
force bool
// Whether or not to purge any key vaults associated with the deployment
purge bool
// Whether or not to start the deletion and return immediately without waiting for completion
noWait bool
}

type StateOptions struct {
Expand All @@ -55,14 +57,19 @@ func (o *DestroyOptions) Purge() bool {
return o.purge
}

func (o *DestroyOptions) NoWait() bool {
return o.noWait
}

func (o *DestroyOptions) Force() bool {
return o.force
}

func NewDestroyOptions(force bool, purge bool) DestroyOptions {
func NewDestroyOptions(force bool, purge bool, noWait bool) DestroyOptions {
return DestroyOptions{
force: force,
purge: purge,
force: force,
purge: purge,
noWait: noWait,
}
}

Expand Down
24 changes: 24 additions & 0 deletions cli/azd/pkg/infra/provisioning/options_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package provisioning

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestNewDestroyOptions(t *testing.T) {
options := NewDestroyOptions(true, true, true)

require.True(t, options.Force())
require.True(t, options.Purge())
require.True(t, options.NoWait())

options = NewDestroyOptions(false, false, false)

require.False(t, options.Force())
require.False(t, options.Purge())
require.False(t, options.NoWait())
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ func TestTerraformDestroy(t *testing.T) {
prepareDestroyMocks(mockContext.CommandRunner)

infraProvider := createTerraformProvider(t, mockContext)
destroyOptions := provisioning.NewDestroyOptions(false, false)
destroyOptions := provisioning.NewDestroyOptions(false, false, false)
destroyResult, err := infraProvider.Destroy(*mockContext.Context, destroyOptions)

require.Nil(t, err)
Expand Down