Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
e6edfa6
Add aspire destroy command for tearing down deployed environments
davidfowl Apr 12, 2026
a2e0559
Simplify destroy-prereq confirmation message
davidfowl Apr 12, 2026
3a7c1b3
Enumerate Azure resources before destroying resource group
davidfowl Apr 12, 2026
619ef46
Add DestroyCommand unit tests and pipeline wiring tests
davidfowl Apr 12, 2026
0943758
Move confirmation prompts to environment-specific destroy steps
davidfowl Apr 12, 2026
6cbd33a
Update deployment E2E tests to use aspire destroy for cleanup
davidfowl Apr 12, 2026
a6202c2
Fix review findings: non-interactive guard and step wiring
davidfowl Apr 12, 2026
364bb12
Clear deployment state after successful destroy
davidfowl Apr 12, 2026
5840dfc
Address review: fail-fast non-interactive guard and state cleanup docs
davidfowl Apr 12, 2026
03dea07
Improve error message when compose file not found during destroy
davidfowl Apr 12, 2026
02134e9
Surface stderr in compose down error messages
davidfowl Apr 12, 2026
9f8e99c
Persist deployment state for Docker Compose and Helm
davidfowl Apr 12, 2026
c469926
Address manual CR feedback
davidfowl Apr 12, 2026
d8c1892
Clear all deployment state on full destroy via ClearAllStateAsync
davidfowl Apr 12, 2026
3adc71e
Add deployment summary to destroy output
davidfowl Apr 12, 2026
9ece783
Use ClearAllStateAsync for --clear-cache instead of raw File.Delete
davidfowl Apr 12, 2026
23cd951
Add Azure destroy unit tests with mockable state manager
davidfowl Apr 12, 2026
5c93ddd
Use persisted state for destroy operations and add Compose destroy tests
davidfowl Apr 12, 2026
762d6b7
Introduce IHelmRunner abstraction and add Helm destroy tests
davidfowl Apr 12, 2026
b0d4cc4
Improve test quality: observable ARM mocks and deploy→destroy roundtrip
davidfowl Apr 12, 2026
3f531d5
Rename PipelineOptions.Yes to SkipConfirmation
davidfowl Apr 12, 2026
55aa8c9
Add API compat suppressions for new interface members
davidfowl Apr 12, 2026
3c5d18e
Fix unused using in KubernetesEnvironmentExtensions
davidfowl Apr 12, 2026
d70f56f
Add Azure portal link to destroy summary
davidfowl Apr 12, 2026
728e244
Extract AzurePortalUrls helper for shared portal URL generation
davidfowl Apr 12, 2026
c0c3b04
Address PR review feedback
davidfowl Apr 13, 2026
99b8d87
Use sentence case for destroy status messages
davidfowl Apr 13, 2026
a583f50
Add regression test for Helm uninstall failure preserving state
davidfowl Apr 13, 2026
38e5450
Address remaining review feedback
davidfowl Apr 13, 2026
c6a7600
Fix compose destroy failing on stale build contexts
davidfowl Apr 13, 2026
052bb72
Wait for Azure resource group deletion to complete
davidfowl Apr 13, 2026
2759774
Fix Helm double task completion and improve Azure delete messaging
davidfowl Apr 13, 2026
9125cd1
Add deletion status to destroy summary
davidfowl Apr 13, 2026
72fe4d7
Show portal URL inline in destroy status summary
davidfowl Apr 13, 2026
84ce8cf
Address review feedback from eerhardt
davidfowl Apr 13, 2026
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
82 changes: 82 additions & 0 deletions src/Aspire.Cli/Commands/DestroyCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.CommandLine;
using Aspire.Cli.Configuration;
using Aspire.Cli.DotNet;
using Aspire.Cli.Interaction;
using Aspire.Cli.Projects;
using Aspire.Cli.Resources;
using Aspire.Cli.Telemetry;
using Aspire.Cli.Utils;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Spectre.Console;

namespace Aspire.Cli.Commands;

internal sealed class DestroyCommand : PipelineCommandBase
{
internal override HelpGroup HelpGroup => HelpGroup.Deployment;

private readonly Option<bool> _yesOption;

public DestroyCommand(IDotNetCliRunner runner, IInteractionService interactionService, IProjectLocator projectLocator, AspireCliTelemetry telemetry, IFeatures features, ICliUpdateNotifier updateNotifier, CliExecutionContext executionContext, ICliHostEnvironment hostEnvironment, IAppHostProjectFactory projectFactory, IConfiguration configuration, ILogger<DestroyCommand> logger, IAnsiConsole ansiConsole)
: base("destroy", DestroyCommandStrings.Description, runner, interactionService, projectLocator, telemetry, features, updateNotifier, executionContext, hostEnvironment, projectFactory, configuration, logger, ansiConsole)
{
_yesOption = new Option<bool>("--yes", "-y")
{
Description = DestroyCommandStrings.YesOptionDescription
};
Options.Add(_yesOption);
}

protected override string OperationCompletedPrefix => DestroyCommandStrings.OperationCompletedPrefix;
protected override string OperationFailedPrefix => DestroyCommandStrings.OperationFailedPrefix;
protected override string GetOutputPathDescription() => DestroyCommandStrings.OutputPathArgumentDescription;

protected override Task<string[]> GetRunArgumentsAsync(string? fullyQualifiedOutputPath, string[] unmatchedTokens, ParseResult parseResult, CancellationToken cancellationToken)
{
var baseArgs = new List<string> { "--operation", "publish", "--step", "destroy" };

if (fullyQualifiedOutputPath != null)
{
baseArgs.AddRange(["--output-path", fullyQualifiedOutputPath]);
}

var yes = parseResult.GetValue(_yesOption);
if (yes)
{
baseArgs.AddRange(["--yes", "true"]);
}

var logLevel = parseResult.GetValue(s_logLevelOption);
if (!string.IsNullOrEmpty(logLevel))
{
baseArgs.AddRange(["--log-level", logLevel!]);
}

var includeExceptionDetails = parseResult.GetValue(s_includeExceptionDetailsOption);
if (includeExceptionDetails)
{
baseArgs.AddRange(["--include-exception-details", "true"]);
}

var environment = parseResult.GetValue(s_environmentOption);
if (!string.IsNullOrEmpty(environment))
{
baseArgs.AddRange(["--environment", environment!]);
}

baseArgs.AddRange(unmatchedTokens);

return Task.FromResult<string[]>([.. baseArgs]);
}

protected override string GetCanceledMessage() => DestroyCommandStrings.DestroyCanceled;

protected override string GetProgressMessage(ParseResult parseResult)
{
return "Executing step destroy";
}
}
2 changes: 2 additions & 0 deletions src/Aspire.Cli/Commands/RootCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ public RootCommand(
AddCommand addCommand,
PublishCommand publishCommand,
DeployCommand deployCommand,
DestroyCommand destroyCommand,
DoCommand doCommand,
ConfigCommand configCommand,
CacheCommand cacheCommand,
Expand Down Expand Up @@ -215,6 +216,7 @@ public RootCommand(
Subcommands.Add(certificatesCommand);
Subcommands.Add(doctorCommand);
Subcommands.Add(deployCommand);
Subcommands.Add(destroyCommand);
Subcommands.Add(doCommand);
Subcommands.Add(updateCommand);
Subcommands.Add(extensionInternalCommand);
Expand Down
1 change: 1 addition & 0 deletions src/Aspire.Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,7 @@ internal static async Task<IHost> BuildApplicationAsync(string[] args, CliStartu
builder.Services.AddTransient<DashboardRunCommand>();
builder.Services.AddTransient<UpdateCommand>();
builder.Services.AddTransient<DeployCommand>();
builder.Services.AddTransient<DestroyCommand>();
builder.Services.AddTransient<DoCommand>();
builder.Services.AddTransient<ExecCommand>();
builder.Services.AddTransient<McpCommand>();
Expand Down
117 changes: 117 additions & 0 deletions src/Aspire.Cli/Resources/DestroyCommandStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

138 changes: 138 additions & 0 deletions src/Aspire.Cli/Resources/DestroyCommandStrings.resx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema

Version 2.0

The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.

Example:

... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>

There are any number of "resheader" rows that contain simple
name/value pairs.

Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.

The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:

Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.

mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.

mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.

mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Description" xml:space="preserve">
<value>Destroy a previously deployed AppHost environment (Preview)</value>
</data>
<data name="OutputPathArgumentDescription" xml:space="preserve">
<value>The output path containing the deployment artifacts to destroy</value>
</data>
<data name="DestroyCanceled" xml:space="preserve">
<value>The destroy operation was canceled.</value>
</data>
<data name="OperationCompletedPrefix" xml:space="preserve">
<value>DESTROY COMPLETED</value>
</data>
<data name="OperationFailedPrefix" xml:space="preserve">
<value>DESTROY FAILED</value>
</data>
<data name="YesOptionDescription" xml:space="preserve">
<value>Skip the confirmation prompt and proceed with the destroy operation</value>
</data>
</root>
Loading
Loading