diff --git a/cli/azd/go.mod b/cli/azd/go.mod index e546991bbfa..f51c6a9552f 100644 --- a/cli/azd/go.mod +++ b/cli/azd/go.mod @@ -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 diff --git a/cli/azd/go.sum b/cli/azd/go.sum index 9a9a668bb40..b245f675535 100644 --- a/cli/azd/go.sum +++ b/cli/azd/go.sum @@ -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= diff --git a/cli/azd/pkg/azapi/standard_deployments.go b/cli/azd/pkg/azapi/standard_deployments.go index 789d983b1e3..4dac2019d4d 100644 --- a/cli/azd/pkg/azapi/standard_deployments.go +++ b/cli/azd/pkg/azapi/standard_deployments.go @@ -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 { diff --git a/cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go b/cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go index 5d9c01ee0d1..dc7d7687d6a 100644 --- a/cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go +++ b/cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go @@ -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{ @@ -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: