Skip to content

Conversation

@DeagleGross
Copy link
Member

@DeagleGross DeagleGross commented Jun 26, 2025

is the result of experiments #9842 and #9912

Description

This pull request introduces a new exec command to the Aspire CLI, allowing users to execute commands on a target resource through the app host. It also includes supporting changes to the backchannel, interaction services, and overall CLI infrastructure to enable this functionality.

Implementation

As discussed here feature is built fully on top of existing capabilities - aspire exec forces apphost on the startup to add a new ExecutableResource to the DCP resources, and then via streams the logs of the resource back to cli backchannel via ResourceLoggerService and manages the lifetime of the resource via ResourceNotificationService.

Moreover, new resource inherits Environment, ResourceRelationship and WaitFor annotations of the target resource in order to be launched exactly in the same configuration as the target resource.

There are 2 types of events I am waiting for from ResourceNotificationService:

  1. when executable resource reaches KnownResourceStates.Running - i am changing the type of the log from "waiting" to "running". This is useful to have more interaction from the UI side and show that command is not yet being executed, but is waiting for other resources to start up (yellow text in the video)
  2. when executable resource reaches KnownResourceStates.TerminalStates - we know there will be no logs coming, and we can complete the logger then. It will stop the async-foreach of watching the logs and cli will also understand that it can continue with the flow.

Extra configuration

  1. Instead of specifying --resource one can write --start-resource forcing the target resource to start before the command execution.
  2. --apphost-keepalive is a flag which will not force apphost shutdown from the cli, but simply exists the cli after command is executed.

Demo

Video shows execution of such aspire exec:

aspire exec --project .../TestingAppHost1.AppHost.csproj --resource mywebapp1 --add-postgres -- dotnet ef database update

WebApp does not have the connection string defined anywhere explicitly, it only has the DbContext registration like:

builder.Services.AddDbContextPool<MyAppDbContext>(options =>
{
    var connectionString = builder.Configuration.GetConnectionString("postgresDb");
    options.UseNpgsql(connectionString);
});

And AppHost only defines the pgSql resource and the webapp referencing and waiting for db:

if (args.Contains("--add-postgres"))
{
    pgsqlDb = builder
        .AddPostgres("postgres1", 6000, builder.AddParameter("pgsqluser"), builder.AddParameter("pgsqlpass", secret: true))
        .AddDatabase("postgresDb");
}

...

if (args.Contains("--add-postgres") && pgsqlDb is not null)
{
    webApp.WithReference(pgsqlDb).WaitFor(pgsqlDb);
}

That shows that connection string is coming from the apphost's runtime knowledge about how postgresDb resource is being built and launched.

aspire.exec.local.tool.migrations.add.mp4
aspire.exec.local.tool.database.update.mp4

Fixes #9859

@DeagleGross DeagleGross self-assigned this Jun 26, 2025
@DeagleGross
Copy link
Member Author

DeagleGross commented Jun 26, 2025

TODO:

  • localize cli
  • write tests to run different types of aspire execs and expect output logs

@DeagleGross DeagleGross marked this pull request as ready for review June 26, 2025 12:25
{
using var activity = _telemetry.ActivitySource.StartActivity(this.Name);

var passedAppHostProjectFile = parseResult.GetValue<FileInfo?>("--project");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: Should this be apphost instead? I could see confusion for people thinking project means a project resource.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It makes sense, but I am following the convention other commands have. For example RunCommand also refers to apphost path as --project, see

var passedAppHostProjectFile = parseResult.GetValue<FileInfo?>("--project");

I think it will only bring confusion to users if in exec apphost flag would change from --project to --apphost.


// a bit hacky, but in order to pass a full command with possible quotes and etc properly without losing the signature
// we can wrap it in a string and pass it as a single argument
"--command", $"\"{string.Join(" ", commandTokens)}\"",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this work? Should there be a test with an argument with a quoted string?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added test with quoted string - it is running dotnet build "MyRandom.csproj" and it works! see Exec_EchoWithQuotedString_ShouldProduceLogs

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually if you have other idea on how to parse / send the command to apphost - let me know. Aspire apphost does not have commandLineParser installed on its side, so it can only take the args passed from cli. I decided to pack the command in such a way, and I know it is not pretty, but it works at least.

return ExitCodeConstants.Success;
}
}
catch (OperationCanceledException ex) when (ex.CancellationToken == cancellationToken)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit gnarly, looks like it was copied from other commands. Wonder if we can turn it into a helper method that handles all these different exception types.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you mind doing it in the follow-up PR? I can refactor all other commands as well

/// Additional info about type of the message.
/// Should be used for controlling the display style.
/// </summary>
public string? Type { get; init; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like it should be an enum

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is hard to achieve - apphost will have its own type of enum, and i have to somehow tell both cli and apphost to serialize/deserialize the enum in the same manner.

Nowdays apphost does not have the serialization context, and there is an effort to provide better experience, but not yet merged + requires serialization context for apphost:
#10058

lets do in the follow-up PR if possible.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

filed #10201

@DeagleGross DeagleGross enabled auto-merge (squash) July 2, 2025 19:32
@DeagleGross DeagleGross merged commit 2c143e3 into main Jul 2, 2025
496 of 498 checks passed
@DeagleGross DeagleGross deleted the dmkorolev/aspire-exec-resources branch July 2, 2025 20:15
@DeagleGross DeagleGross restored the dmkorolev/aspire-exec-resources branch July 2, 2025 20:20
@BrennanConroy BrennanConroy deleted the dmkorolev/aspire-exec-resources branch July 2, 2025 20:28
BrennanConroy added a commit that referenced this pull request Jul 2, 2025
BrennanConroy added a commit that referenced this pull request Jul 3, 2025
DeagleGross added a commit that referenced this pull request Jul 3, 2025
@github-actions github-actions bot locked and limited conversation to collaborators Aug 2, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: custom command execution against AppHost resources

5 participants