Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
203 changes: 122 additions & 81 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,106 @@ 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())
}

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)
}

appConfigs, err := p.getAppConfigsToPurge(ctx, groupedResources)
if err != nil {
return nil, fmt.Errorf("getting app configurations to purge: %w", err)
}
p.console.StopSpinner(ctx, "", input.StepDone)
// Call deployment.Delete to void the state even though there are no resources to delete
if err := p.destroyDeploymentWithoutConfirmation(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)
}

apiManagements, err := p.getApiManagementsToPurge(ctx, groupedResources)
if err != nil {
return nil, fmt.Errorf("getting API managements to purge: %w", err)
}
managedHSMs, err := p.getManagedHSMsToPurge(ctx, groupedResources)
if err != nil {
return nil, fmt.Errorf("getting managed hsms to purge: %w", err)
}

cognitiveAccounts, err := p.getCognitiveAccountsToPurge(ctx, groupedResources)
if err != nil {
return nil, fmt.Errorf("getting cognitive accounts to purge: %w", err)
}
appConfigs, err := p.getAppConfigsToPurge(ctx, groupedResources)
if err != nil {
return nil, fmt.Errorf("getting app configurations to purge: %w", err)
}

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)
},
}
apiManagements, err := p.getApiManagementsToPurge(ctx, groupedResources)
if err != nil {
return nil, fmt.Errorf("getting API managements to purge: %w", err)
}

var purgeItem []itemToPurge
for _, item := range []itemToPurge{keyVaultsPurge, managedHSMsPurge, appConfigsPurge, aPIManagement} {
if item.count > 0 {
purgeItem = append(purgeItem, item)
cognitiveAccounts, err := p.getCognitiveAccountsToPurge(ctx, groupedResources)
if err != nil {
return nil, fmt.Errorf("getting cognitive accounts to purge: %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),
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.purgeCognitiveAccounts(ctx, self.cognitiveAccounts, skipPurge)
return p.purgeKeyVaults(ctx, keyVaults, skipPurge)
},
cognitiveAccounts: groupByKind[name],
}
purgeItem = append(purgeItem, addPurgeItem)
}
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)
},
}

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)
if err := p.purgeItems(ctx, purgeItem, options); err != nil {
return nil, fmt.Errorf("purging resources: %w", err)
}
}

destroyResult := &provisioning.DestroyResult{
Expand Down Expand Up @@ -1121,6 +1129,39 @@ func (p *BicepProvider) destroyDeploymentWithConfirmation(
return nil
}

// destroyDeploymentWithoutConfirmation deletes the deployment without prompting for confirmation.
// This is used when there are no resources to delete but we still need to void the deployment state.
func (p *BicepProvider) destroyDeploymentWithoutConfirmation(
ctx context.Context,
deployment infra.Deployment,
) error {
err := async.RunWithProgressE(func(progressMessage azapi.DeleteDeploymentProgress) {
switch progressMessage.State {
case azapi.DeleteResourceStateInProgress:
p.console.ShowSpinner(ctx, progressMessage.Message, input.Step)
case azapi.DeleteResourceStateSucceeded:
p.console.StopSpinner(ctx, progressMessage.Message, input.StepDone)
case azapi.DeleteResourceStateFailed:
p.console.StopSpinner(ctx, progressMessage.Message, input.StepFailed)
}
}, func(progress *async.Progress[azapi.DeleteDeploymentProgress]) error {
optionsMap, err := convert.ToMap(p.options)
if err != nil {
return err
}

return deployment.Delete(ctx, optionsMap, progress)
})

if err != nil {
return err
}

p.console.Message(ctx, "")

return nil
}

func itemsCountAsText(items []itemToPurge) string {
count := len(items)
if count < 1 {
Expand Down
Loading