Add aspire destroy command for tearing down deployed environments#16097
Add aspire destroy command for tearing down deployed environments#16097
Conversation
Implements #13013 - adds a top-level 'aspire destroy' command that tears down previously deployed Aspire environments. The command follows the same pipeline architecture as 'aspire deploy' and 'aspire publish'. Changes: - Add WellKnownPipelineSteps.Destroy and DestroyPrereq aggregation steps - Add DestroyCommand CLI command with --yes flag to skip confirmation - Add destroy-prereq step with interactive confirmation prompt - Wire Docker Compose's existing docker-compose-down step to destroy - Wire Kubernetes Helm's existing helm-uninstall step to destroy - Add Azure resource group deletion via ARM SDK for ACA/App Service - Add IResourceGroupResource.DeleteAsync to provisioning abstractions - Add PipelineOptions.Yes for forwarding --yes flag to AppHost - Update pipeline step count test and accept diagnostics snapshots Validated with pipeline tests (65), Docker Compose tests (85), Kubernetes tests (88), and Azure deployer tests (28) all passing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep the prereq generic — each environment step already surfaces target-specific details (resource group, Helm release, compose project). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Query the resource group via ARM to list all resources before deleting,
so users can see exactly what will be destroyed. The discovery phase
logs each resource type and name, then reports the total count.
Pipeline output:
Discovering resources in myapp-rg
ContainerApps/containerApps: api
KeyVault/vaults: kv-myapp
ContainerRegistry/registries: acrmyapp
Found 3 resource(s) in myapp-rg
Deleting resource group myapp-rg (3 resource(s))
If enumeration fails (e.g. permissions), deletion still proceeds.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- DestroyCommandTests: help, invalid project, --step destroy argument, --yes flag forwarding, --output-path inclusion (5 tests) - K8s: HelmUninstallStep_RequiredByDestroy verifies helm-uninstall depends on destroy-prereq - Register DestroyCommand in test DI (CliTestHelper.cs) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Each environment step now owns its own confirmation with full context:
- Azure: discovers resources in RG, then asks to confirm deletion
- Docker Compose: asks to confirm compose down
- Kubernetes: asks to confirm helm uninstall with release name + namespace
Pipeline layering: destroy → destroy-{env} (prompt) → action step (no prompt)
This means 'aspire do docker-compose-down' skips the prompt (explicit action),
while 'aspire destroy' chains through the confirmation layer.
The generic destroy-prereq is now a plain no-op placeholder step.
--yes skips all confirmation prompts.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace manual cleanup commands with 'aspire destroy --yes': - Azure (14 files): add aspire destroy step before exit, keep CleanupResourceGroupAsync as safety net in finally block - Docker (2 tests): replace 'docker compose down' with aspire destroy - Podman (1 test): replace 'podman compose down' with aspire destroy - Kubernetes (1 test): replace 'helm uninstall' with aspire destroy, keep KinD cluster deletion separate Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fail fast when --yes is not set and interactivity is unavailable, instead of silently proceeding with destruction - Consolidate destroy steps: each environment's destroy step does confirm + action in one step, keeping standalone action steps (docker-compose-down, helm-uninstall) clean for aspire do usage - destroy-prereq is now a plain no-op placeholder Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The destroy aggregation step now deletes the deployment state file after all environment destroy steps succeed, acting as an implicit cache clear. This ensures the next deploy starts fresh. Removed per-section Azure state cleanup since the whole file is now deleted at the end. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Move non-interactive/--yes check before ARM calls in Azure destroy so it fails fast without doing expensive Azure work - Document that state file deletion is intentional (includes saved parameters — expected for full environment teardown) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Tell the user to pass the same --output-path they used during deploy, instead of just saying the file doesn't exist. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Include the actual error output (e.g. 'Cannot connect to Docker daemon') instead of generic 'ensure runtime is installed' guidance. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Save minimal deployment state during deploy so destroy can verify
what was actually deployed:
Docker Compose: saves OutputPath, ProjectName, ComposeFilePath
to DockerCompose:{name} state section during compose-up
Helm: saves ReleaseName, Namespace
to Helm:{name} state section during helm-deploy
Destroy steps now check for deployment state first and report
'Nothing to destroy' instead of failing with confusing errors
when no deployment exists.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1. Merge Azure destroy step into existing PipelineStepAnnotation 2. Flatten await using blocks to reduce nesting 3. Remove global::Azure prefix (using Azure; works fine) 4. Add IDeploymentStateManager.ClearAllStateAsync for centralized cleanup 5. Per-environment destroy steps now clean up their own state sections 6. Extract shared AspireDestroyAsync helper for E2E test cleanup, removing duplication across 17 test files Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Full destroy is a full reset — clears parameters, Azure config, and per-environment state. Per-section cleanup in environment steps handles partial/scoped operations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Each environment now adds summary entries showing what was destroyed: - Azure: resource group name + subscription - Docker Compose: environment name - Helm: release name + namespace Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Consolidate all state file mutations through IDeploymentStateManager so in-memory state is also reset correctly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Three new tests covering the destroy pipeline for Azure: - WithAzureState: verifies RG discovery and deletion runs - WithNoAzureState: verifies 'Nothing to destroy' message - NonInteractiveWithoutYes: verifies fail-fast with --yes guidance Added InMemoryDeploymentStateManager for stateful test scenarios. Added deploymentStateManager parameter to ConfigureTestServices. Updated all IDeploymentStateManager mocks with ClearAllStateAsync. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reliability fixes from code review: - Docker destroy now uses saved ComposeFilePath/ProjectName from deployment state instead of recomputing from current model - Helm destroy uses saved ReleaseName/Namespace for both the confirmation prompt and the actual uninstall call - Docker destroy only clears state after successful compose down, preserves state when compose file is missing Test improvements: - Extract InMemoryDeploymentStateManager to shared test code - Add FakeContainerRuntime.WasComposeDownCalled tracking - Add 2 Docker Compose destroy pipeline tests: - WithState: verifies compose down is called via FakeContainerRuntime - WithNoState: verifies 'Nothing to destroy' without calling compose down Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extract IHelmRunner interface from HelmDeploymentEngine to enable testability of Helm operations without requiring a real helm binary. - IHelmRunner: abstraction for running helm CLI commands - DefaultHelmRunner: production implementation using ProcessUtil - FakeHelmRunner: test double that tracks calls and returns exit code 0 - Refactor HelmDeployAsync and HelmUninstallAsync to use IHelmRunner - Register DefaultHelmRunner in DI via AddKubernetesInfrastructureCore New tests: - DestroyHelm_WithState: verifies helm uninstall is called with saved release name and namespace from deployment state - DestroyHelm_WithNoState: verifies 'Nothing to destroy' without calling helm Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Make TestResourceGroupResource observable with WasDeleteCalled and WasGetResourcesCalled tracking, threaded through ARM client/subscription - Azure destroy test now asserts ARM DeleteAsync and GetResourcesAsync were actually called, not just that the step was created - Add deploy→destroy roundtrip test for Docker Compose that verifies state persisted during deploy is correctly consumed by destroy Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Clearer intent — 'Yes' was ambiguous, 'SkipConfirmation' describes exactly what the option does. The CLI flag remains --yes/-y. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 16097Or
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 16097" |
Suppress CP0006 for ClearAllStateAsync added to IDeploymentStateManager and Compose methods added to IContainerRuntime. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Removes redundant Microsoft.Extensions.DependencyInjection using (only Extensions variant needed for TryAddSingleton). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
/deployment-tests |
Link to the resource group in the portal so users can monitor the async deletion operation or diagnose failures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Consolidate portal URL construction into a single shared class: - GetResourceGroupUrl: used by deploy summary and destroy summary - GetDeploymentUrl(string, string, string): used by BicepProvisioner - GetDeploymentUrl(ResourceIdentifier): used by BicepProvisioner Removes duplicate URL construction logic from AzureEnvironmentResource and BicepProvisioner. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
/deployment-tests |
| dockerComposeUpStep.RequiredBy(WellKnownPipelineSteps.Deploy); | ||
| steps.Add(dockerComposeUpStep); | ||
|
|
||
| var dockerComposeDestroyStep = new PipelineStep |
There was a problem hiding this comment.
Do you plan on keeping the existing down step as a less destructive step?
There was a problem hiding this comment.
It's just as destructive, this just prompts before doing the downing
There was a problem hiding this comment.
Hmmm, as far as a I remember the existing down command didn't clear out deployment state or the existing project files. It was just a redirect to docker compose down.
There was a problem hiding this comment.
Ah yes, that is still there as a separate step.
JamesNK
left a comment
There was a problem hiding this comment.
Reviewed the aspire destroy implementation across CLI, Azure, Docker Compose, and Helm environments. Found 3 issues:\n\n- 1 bug (Helm): Deployment state deleted even when helm uninstall fails, preventing retry\n- 1 bug (Hosting): ClearAllStateAsync modifies shared fields without holding _stateLock\n- 1 bug (Docker Compose): Missing user-visible feedback when compose file no longer exists on disk
src/Aspire.Hosting/Pipelines/Internal/DeploymentStateManagerBase.cs
Outdated
Show resolved
Hide resolved
- Improve --yes option description to be clearer (JamesNK) - Change confirmation button text from 'Yes, destroy' to 'Destroy' (JamesNK) - Fix Helm: throw on non-zero exit so state isn't cleared on failure (JamesNK) - Fix ClearAllStateAsync: acquire _stateLock before mutating in-memory state to maintain locking discipline (JamesNK) - Fix missing ReportingStep.CompleteAsync when compose file no longer exists during destroy (JamesNK) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Change 'DESTROY COMPLETED'/'DESTROY FAILED' to 'Destroy completed'/ 'Destroy failed' per review feedback (JamesNK). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Verifies that when helm uninstall exits non-zero, deployment state is preserved so the user can retry aspire destroy. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| }; | ||
|
|
||
| var deployTask = await ctx.ReportingStep.CreateTaskAsync( | ||
| new MarkdownString($"Running compose down for **{Name}** using **{runtime.Name}**"), |
There was a problem hiding this comment.
Could the names in any of these markdownstrings contain markdown content? Do they need to be escaped?
There was a problem hiding this comment.
This is a good rule, I'm not sure we're consistent though, this is new. It's a resource name.
There was a problem hiding this comment.
Resource names are validated to ASCII letters/digits/hyphens by ModelName, so they can't contain markdown special characters. RG names from Azure state could theoretically have issues but this is a pre-existing pattern across all pipeline steps. Filed as a follow-up to audit all MarkdownString usage.
src/Aspire.Hosting.Kubernetes/Deployment/HelmDeploymentEngine.cs
Outdated
Show resolved
Hide resolved
| // TODO: Rename this to something related to deployment state | ||
| { "--clear-cache", "Pipeline:ClearCache" }, | ||
|
|
||
| { "--yes", "Pipeline:SkipConfirmation" }, |
There was a problem hiding this comment.
I don't like --yes as a name. It's so generic and could mean anything to the person who reads it.
There was a problem hiding this comment.
Good point. --force isn't great either — it implies overriding safety checks, not just skipping a prompt.
Options:
| Flag | Precedent | Pros | Cons |
|---|---|---|---|
--yes / -y |
apt-get -y, npm init -y, az group delete --yes |
Well-understood "answer yes to prompts" | Generic, as you said |
--force / -f |
azd down --force, docker system prune -f |
Common | Implies behavior change, not just prompt skip |
--no-confirm |
nuget push --no-confirm |
Precise meaning | Verbose |
--skip-confirmation |
— | Very clear | Too long |
I'd lean toward --yes since it's the most widely understood for this exact use case. WDYT?
- Lowercase non-proper-noun words in prompt titles (JamesNK) - Revert --force back to --yes pending naming discussion - Fix zero-width character introduced by sed in test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
🎬 CLI E2E Test Recordings — 68 recordings uploaded (commit View recordings
📹 Recordings uploaded automatically from CI run #24325113651 |
Description
Add
aspire destroy— a new top-level CLI command that tears down previously deployed Aspire environments. Completes the deployment lifecycle:aspire publish → aspire deploy → aspire destroy.What it does
Each compute environment implements its own destroy step with contextual confirmation:
docker compose downusing persisted deployment statehelm uninstallusing persisted release name and namespaceKey design decisions
--yes/-yskips prompts.destroy-{env}step (confirm + action) and a standalone action step (docker-compose-down,helm-uninstall) callable viaaspire dowithout confirmation.--yesin non-interactive mode throws immediately instead of silently proceeding.IDeploymentStateManager.ClearAllStateAsync.New abstractions
WellKnownPipelineSteps.Destroy/DestroyPrereq— pipeline aggregation stepsPipelineOptions.SkipConfirmation— forwarded from CLI--yesflagIDeploymentStateManager.ClearAllStateAsync()— centralized state cleanupIResourceGroupResource.DeleteAsync()/GetResourcesAsync()— ARM abstractions for destroyIHelmRunner— testable abstraction for Helm CLI operationsValidation
--yes), Docker Compose deploy→destroy (interactive +--yes)aspire destroy --yesvia sharedAspireDestroyAsynchelperFixes #13013
Checklist
<remarks />and<code />elements on your triple slash comments?aspire.devissue: