Skip to content

SPIKE: Spike parameter prompting from CLI #9349

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
19 changes: 17 additions & 2 deletions src/Aspire.Cli/Backchannel/AppHostBackchannel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ internal interface IAppHostBackchannel
Task ConnectAsync(string socketPath, CancellationToken cancellationToken);
IAsyncEnumerable<(string Id, string StatusText, bool IsComplete, bool IsError)> GetPublishingActivitiesAsync(CancellationToken cancellationToken);
Task<string[]> GetCapabilitiesAsync(CancellationToken cancellationToken);
Task RequestParameterPromptsAsync(CancellationToken cancellationToken);
}

internal sealed class AppHostBackchannel(ILogger<AppHostBackchannel> logger, CliRpcTarget target) : IAppHostBackchannel
Expand Down Expand Up @@ -77,7 +78,7 @@ await rpc.InvokeWithCancellationAsync(
return url;
}

public async IAsyncEnumerable<(string Resource, string Type, string State, string[] Endpoints)> GetResourceStatesAsync([EnumeratorCancellation]CancellationToken cancellationToken)
public async IAsyncEnumerable<(string Resource, string Type, string State, string[] Endpoints)> GetResourceStatesAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
using var activity = _activitySource.StartActivity();

Expand Down Expand Up @@ -143,7 +144,7 @@ public async Task ConnectAsync(string socketPath, CancellationToken cancellation
}
}

public async IAsyncEnumerable<(string Id, string StatusText, bool IsComplete, bool IsError)> GetPublishingActivitiesAsync([EnumeratorCancellation]CancellationToken cancellationToken)
public async IAsyncEnumerable<(string Id, string StatusText, bool IsComplete, bool IsError)> GetPublishingActivitiesAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
using var activity = _activitySource.StartActivity();

Expand Down Expand Up @@ -179,4 +180,18 @@ public async Task<string[]> GetCapabilitiesAsync(CancellationToken cancellationT

return capabilities;
}

public async Task RequestParameterPromptsAsync(CancellationToken cancellationToken)
{
using var activity = _activitySource.StartActivity();

var rpc = await _rpcTaskCompletionSource.Task.ConfigureAwait(false);

logger.LogDebug("Requesting parameter prompts");

await rpc.InvokeWithCancellationAsync(
"RequestParameterPromptsAsync",
Array.Empty<object>(),
cancellationToken).ConfigureAwait(false);
}
}
13 changes: 13 additions & 0 deletions src/Aspire.Cli/Backchannel/CliRpcTarget.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,17 @@ namespace Aspire.Cli.Backchannel;

internal class CliRpcTarget
{
#pragma warning disable CA1822 // Mark members as static
public async Task<string> GetParameterValue(string parameterName, CancellationToken cancellationToken)
#pragma warning restore CA1822 // Mark members as static
{
if (GetParameterValueCallback is null)
{
throw new InvalidOperationException("GetParameterValueCallback is not set.");
}

return await GetParameterValueCallback(parameterName).WaitAsync(cancellationToken).ConfigureAwait(false);
}

public Func<string, Task<string>>? GetParameterValueCallback { get; set; } = null!;
}
3 changes: 3 additions & 0 deletions src/Aspire.Cli/Commands/PublishCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,9 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
}

var backchannel = await backchannelCompletionSource.Task.ConfigureAwait(false);

await backchannel.RequestParameterPromptsAsync(cancellationToken);

var publishingActivities = backchannel.GetPublishingActivitiesAsync(cancellationToken);

var debugMode = parseResult.GetValue<bool?>("--debug") ?? false;
Expand Down
20 changes: 19 additions & 1 deletion src/Aspire.Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ private static IHost BuildApplication(string[] args)
builder.Services.AddSingleton<ICertificateService, CertificateService>();
builder.Services.AddTransient<IDotNetCliRunner, DotNetCliRunner>();
builder.Services.AddTransient<IAppHostBackchannel, AppHostBackchannel>();
builder.Services.AddSingleton<CliRpcTarget>();
builder.Services.AddSingleton<CliRpcTarget>(BuildCliRpcTarget);
builder.Services.AddTransient<INuGetPackageCache, NuGetPackageCache>();

// Commands.
Expand All @@ -136,6 +136,24 @@ private static IHost BuildApplication(string[] args)
return app;
}

private static CliRpcTarget BuildCliRpcTarget(IServiceProvider serviceProvider)
{
var ansiConsole = serviceProvider.GetRequiredService<IAnsiConsole>();

return new CliRpcTarget()
{
GetParameterValueCallback = async (parameterName) =>
{
var textPrompt = new TextPrompt<string>($"Enter value for {parameterName}:")
.AllowEmpty()
.ShowDefaultValue()
.PromptStyle("green");

return await ansiConsole.PromptAsync(textPrompt);
}
};
}

private static IAnsiConsole BuildAnsiConsole(IServiceProvider serviceProvider)
{
AnsiConsoleSettings settings = new AnsiConsoleSettings()
Expand Down
32 changes: 26 additions & 6 deletions src/Aspire.Hosting/Backchannel/AppHostRpcTarget.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StreamJsonRpc;

namespace Aspire.Hosting.Backchannel;

Expand All @@ -21,9 +22,9 @@ internal class AppHostRpcTarget(
PublishingActivityProgressReporter activityReporter,
IHostApplicationLifetime lifetime,
DistributedApplicationOptions options
)
)
{
public async IAsyncEnumerable<(string Id, string StatusText, bool IsComplete, bool IsError)> GetPublishingActivitiesAsync([EnumeratorCancellation]CancellationToken cancellationToken)
public async IAsyncEnumerable<(string Id, string StatusText, bool IsComplete, bool IsError)> GetPublishingActivitiesAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
while (cancellationToken.IsCancellationRequested == false)
{
Expand All @@ -43,7 +44,7 @@ DistributedApplicationOptions options
publishingActivityStatus.IsError
);

if ( publishingActivityStatus.Activity.IsPrimary &&(publishingActivityStatus.IsComplete || publishingActivityStatus.IsError))
if (publishingActivityStatus.Activity.IsPrimary && (publishingActivityStatus.IsComplete || publishingActivityStatus.IsError))
{
// If the activity is complete or an error and it is the primary activity,
// we can stop listening for updates.
Expand All @@ -52,7 +53,7 @@ DistributedApplicationOptions options
}
}

public async IAsyncEnumerable<(string Resource, string Type, string State, string[] Endpoints)> GetResourceStatesAsync([EnumeratorCancellation]CancellationToken cancellationToken)
public async IAsyncEnumerable<(string Resource, string Type, string State, string[] Endpoints)> GetResourceStatesAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
var resourceEvents = resourceNotificationService.WatchAsync(cancellationToken);

Expand All @@ -69,7 +70,7 @@ DistributedApplicationOptions options
logger.LogTrace("Resource {Resource} does not have endpoints.", resourceEvent.Resource.Name);
endpoints = Enumerable.Empty<EndpointAnnotation>();
}

var endpointUris = endpoints
.Where(e => e.AllocatedEndpoint != null)
.Select(e => e.AllocatedEndpoint!.UriString)
Expand Down Expand Up @@ -129,7 +130,7 @@ await resourceNotificationService.WaitForResourceHealthyAsync(
if (!StringUtils.TryGetUriFromDelimitedString(dashboardOptions.Value.DashboardUrl, ";", out var dashboardUri))
{
logger.LogWarning("Dashboard URL could not be parsed from dashboard options.");
throw new InvalidOperationException("Dashboard URL could not be parsed from dashboard options.");
throw new InvalidOperationException("Dashboard URL could not be parsed from dashboard options.");
}

var codespacesUrlRewriter = serviceProvider.GetService<CodespacesUrlRewriter>();
Expand Down Expand Up @@ -173,4 +174,23 @@ public Task<string[]> GetCapabilitiesAsync(CancellationToken cancellationToken)
});
}
#pragma warning restore CA1822

public async Task RequestParameterPromptsAsync(CancellationToken cancellationToken)
{
if (ClientRpc == null)
{
throw new DistributedApplicationException("ClientRpc is not set.");
}

var model = serviceProvider.GetRequiredService<DistributedApplicationModel>();
var parameters = model.Resources.OfType<ParameterResource>().ToList();

foreach (var parameter in parameters)
{
var value = await ClientRpc.InvokeWithCancellationAsync<string>("GetParameterValue", [parameter.Name], cancellationToken).ConfigureAwait(false);
_ = value;
}
}

public JsonRpc? ClientRpc { get; set; }
}
1 change: 1 addition & 0 deletions src/Aspire.Hosting/Backchannel/BackchannelService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ await eventing.PublishAsync(
var stream = new NetworkStream(clientSocket, true);
var rpc = JsonRpc.Attach(stream, appHostRpcTarget);
_rpc = rpc;
appHostRpcTarget.ClientRpc = rpc;

// NOTE: The DistributedApplicationRunner will await this TCS
// when a backchannel is expected, and will not stop
Expand Down
Loading