Skip to content
Open
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
1 change: 0 additions & 1 deletion cli/azd/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ require (
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
github.com/joho/godotenv v1.5.1
github.com/magefile/mage v1.15.0
github.com/mark3labs/mcp-go v0.41.1
github.com/mattn/go-colorable v0.1.14
github.com/mattn/go-isatty v0.0.20
Expand Down
2 changes: 0 additions & 2 deletions cli/azd/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -284,8 +284,6 @@ github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kUL
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mark3labs/mcp-go v0.41.1 h1:w78eWfiQam2i8ICL7AL0WFiq7KHNJQ6UB53ZVtH4KGA=
Expand Down
13 changes: 12 additions & 1 deletion cli/azd/pkg/azapi/standard_deployments.go
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,18 @@ func (ds *StandardDeployments) DeleteSubscriptionDeployment(
})
}

// Deploy empty template to void provision state and keep deployment history instead of deleting previous deployments
// Void the deployment state
return ds.voidSubscriptionDeploymentState(ctx, subscriptionId, deploymentName, options)
}

// voidSubscriptionDeploymentState deploys an empty template to void the provision state
// and keep deployment history instead of deleting previous deployments.
func (ds *StandardDeployments) voidSubscriptionDeploymentState(
ctx context.Context,
subscriptionId string,
deploymentName string,
options map[string]any,
) error {
// Get deployment metadata
deployment, err := ds.GetSubscriptionDeployment(ctx, subscriptionId, deploymentName)
if err != nil {
Expand Down
218 changes: 118 additions & 100 deletions cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -829,98 +829,108 @@ func (p *BicepProvider) Destroy(
return nil, fmt.Errorf("mapping resources to resource groups: %w", err)
}

// If no resources found, we still need to void the deployment state.
// This can happen when resources have been manually deleted before running azd down.
// Voiding the state ensures that subsequent azd provision commands work correctly
// by creating a new empty deployment that becomes the last successful deployment.
if len(groupedResources) == 0 {
return nil, fmt.Errorf("%w, '%s'", infra.ErrDeploymentResourcesNotFound, deploymentToDelete.Name())
}
p.console.StopSpinner(ctx, "", input.StepDone)
// Call deployment.Delete to void the state even though there are no resources to delete
if err := p.destroyDeployment(ctx, deploymentToDelete); err != nil {
return nil, fmt.Errorf("voiding deployment state: %w", err)
}
} else {
keyVaults, err := p.getKeyVaultsToPurge(ctx, groupedResources)
if err != nil {
return nil, fmt.Errorf("getting key vaults to purge: %w", err)
}

keyVaults, err := p.getKeyVaultsToPurge(ctx, groupedResources)
if err != nil {
return nil, fmt.Errorf("getting key vaults to purge: %w", err)
}
managedHSMs, err := p.getManagedHSMsToPurge(ctx, groupedResources)
if err != nil {
return nil, fmt.Errorf("getting managed hsms to purge: %w", err)
}

managedHSMs, err := p.getManagedHSMsToPurge(ctx, groupedResources)
if err != nil {
return nil, fmt.Errorf("getting managed hsms to purge: %w", err)
}
appConfigs, err := p.getAppConfigsToPurge(ctx, groupedResources)
if err != nil {
return nil, fmt.Errorf("getting app configurations to purge: %w", err)
}

appConfigs, err := p.getAppConfigsToPurge(ctx, groupedResources)
if err != nil {
return nil, fmt.Errorf("getting app configurations to purge: %w", err)
}
apiManagements, err := p.getApiManagementsToPurge(ctx, groupedResources)
if err != nil {
return nil, fmt.Errorf("getting API managements to purge: %w", err)
}

apiManagements, err := p.getApiManagementsToPurge(ctx, groupedResources)
if err != nil {
return nil, fmt.Errorf("getting API managements to purge: %w", err)
}
cognitiveAccounts, err := p.getCognitiveAccountsToPurge(ctx, groupedResources)
if err != nil {
return nil, fmt.Errorf("getting cognitive accounts to purge: %w", err)
}

cognitiveAccounts, err := p.getCognitiveAccountsToPurge(ctx, groupedResources)
if err != nil {
return nil, fmt.Errorf("getting cognitive accounts to purge: %w", err)
}
p.console.StopSpinner(ctx, "", input.StepDone)

p.console.StopSpinner(ctx, "", input.StepDone)
if err := p.destroyDeploymentWithConfirmation(
ctx,
options,
deploymentToDelete,
groupedResources,
len(resourcesToDelete),
); err != nil {
return nil, fmt.Errorf("deleting resource groups: %w", err)
}

keyVaultsPurge := itemToPurge{
resourceType: "Key Vault",
count: len(keyVaults),
purge: func(skipPurge bool, self *itemToPurge) error {
return p.purgeKeyVaults(ctx, keyVaults, skipPurge)
},
}
managedHSMsPurge := itemToPurge{
resourceType: "Managed HSM",
count: len(managedHSMs),
purge: func(skipPurge bool, self *itemToPurge) error {
return p.purgeManagedHSMs(ctx, managedHSMs, skipPurge)
},
}
appConfigsPurge := itemToPurge{
resourceType: "App Configuration",
count: len(appConfigs),
purge: func(skipPurge bool, self *itemToPurge) error {
return p.purgeAppConfigs(ctx, appConfigs, skipPurge)
},
}
aPIManagement := itemToPurge{
resourceType: "API Management",
count: len(apiManagements),
purge: func(skipPurge bool, self *itemToPurge) error {
return p.purgeAPIManagement(ctx, apiManagements, skipPurge)
},
}
// Prompt for confirmation before deleting resources
if err := p.promptDeletion(ctx, options, groupedResources, len(resourcesToDelete)); err != nil {
return nil, err
}

var purgeItem []itemToPurge
for _, item := range []itemToPurge{keyVaultsPurge, managedHSMsPurge, appConfigsPurge, aPIManagement} {
if item.count > 0 {
purgeItem = append(purgeItem, item)
p.console.Message(ctx, output.WithGrayFormat("Deleting your resources can take some time.\n"))

if err := p.destroyDeployment(ctx, deploymentToDelete); err != nil {
return nil, fmt.Errorf("deleting resource groups: %w", err)
}
}

// cognitive services are grouped by resource group because the name of the resource group is required to purge
groupByKind := cognitiveAccountsByKind(cognitiveAccounts)
for name, cogAccounts := range groupByKind {
addPurgeItem := itemToPurge{
resourceType: name,
count: len(cogAccounts),
keyVaultsPurge := itemToPurge{
resourceType: "Key Vault",
count: len(keyVaults),
purge: func(skipPurge bool, self *itemToPurge) error {
return p.purgeCognitiveAccounts(ctx, self.cognitiveAccounts, skipPurge)
return p.purgeKeyVaults(ctx, keyVaults, skipPurge)
},
}
managedHSMsPurge := itemToPurge{
resourceType: "Managed HSM",
count: len(managedHSMs),
purge: func(skipPurge bool, self *itemToPurge) error {
return p.purgeManagedHSMs(ctx, managedHSMs, skipPurge)
},
}
appConfigsPurge := itemToPurge{
resourceType: "App Configuration",
count: len(appConfigs),
purge: func(skipPurge bool, self *itemToPurge) error {
return p.purgeAppConfigs(ctx, appConfigs, skipPurge)
},
}
aPIManagement := itemToPurge{
resourceType: "API Management",
count: len(apiManagements),
purge: func(skipPurge bool, self *itemToPurge) error {
return p.purgeAPIManagement(ctx, apiManagements, skipPurge)
},
cognitiveAccounts: groupByKind[name],
}
purgeItem = append(purgeItem, addPurgeItem)
}

if err := p.purgeItems(ctx, purgeItem, options); err != nil {
return nil, fmt.Errorf("purging resources: %w", err)
var purgeItem []itemToPurge
for _, item := range []itemToPurge{keyVaultsPurge, managedHSMsPurge, appConfigsPurge, aPIManagement} {
if item.count > 0 {
purgeItem = append(purgeItem, item)
}
}

// cognitive services are grouped by resource group because the name of the resource group is required to purge
groupByKind := cognitiveAccountsByKind(cognitiveAccounts)
for name, cogAccounts := range groupByKind {
addPurgeItem := itemToPurge{
resourceType: name,
count: len(cogAccounts),
purge: func(skipPurge bool, self *itemToPurge) error {
return p.purgeCognitiveAccounts(ctx, self.cognitiveAccounts, skipPurge)
},
cognitiveAccounts: groupByKind[name],
}
purgeItem = append(purgeItem, addPurgeItem)
}

if err := p.purgeItems(ctx, purgeItem, options); err != nil {
return nil, fmt.Errorf("purging resources: %w", err)
}
}

destroyResult := &provisioning.DestroyResult{
Expand Down Expand Up @@ -1062,38 +1072,46 @@ func (p *BicepProvider) generateResourcesToDelete(groupedResources map[string][]
return append(lines, "\n")
}

// Deletes the azure resources within the deployment
func (p *BicepProvider) destroyDeploymentWithConfirmation(
// promptDeletion prompts the user for confirmation before deleting resources.
// Returns nil if the user confirms, or an error if they deny or an error occurs.
func (p *BicepProvider) promptDeletion(
ctx context.Context,
options provisioning.DestroyOptions,
deployment infra.Deployment,
groupedResources map[string][]*azapi.Resource,
resourceCount int,
) error {
if !options.Force() {
p.console.MessageUxItem(ctx, &ux.MultilineMessage{
Lines: p.generateResourcesToDelete(groupedResources)},
)
confirmDestroy, err := p.console.Confirm(ctx, input.ConsoleOptions{
Message: fmt.Sprintf(
"Total resources to %s: %d, are you sure you want to continue?",
output.WithErrorFormat("delete"),
resourceCount,
),
DefaultValue: false,
})
if options.Force() {
return nil
}

if err != nil {
return fmt.Errorf("prompting for delete confirmation: %w", err)
}
p.console.MessageUxItem(ctx, &ux.MultilineMessage{
Lines: p.generateResourcesToDelete(groupedResources)},
)
confirmDestroy, err := p.console.Confirm(ctx, input.ConsoleOptions{
Message: fmt.Sprintf(
"Total resources to %s: %d, are you sure you want to continue?",
output.WithErrorFormat("delete"),
resourceCount,
),
DefaultValue: false,
})

if !confirmDestroy {
return errors.New("user denied delete confirmation")
}
if err != nil {
return fmt.Errorf("prompting for delete confirmation: %w", err)
}

p.console.Message(ctx, output.WithGrayFormat("Deleting your resources can take some time.\n"))
if !confirmDestroy {
return errors.New("user denied delete confirmation")
}

return nil
}

// destroyDeployment deletes the azure resources within the deployment and voids the deployment state.
func (p *BicepProvider) destroyDeployment(
ctx context.Context,
deployment infra.Deployment,
) error {
err := async.RunWithProgressE(func(progressMessage azapi.DeleteDeploymentProgress) {
switch progressMessage.State {
case azapi.DeleteResourceStateInProgress:
Expand Down
Loading