Skip to content
Merged
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
61 changes: 46 additions & 15 deletions src/Aspire.Hosting/Dcp/DcpExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -966,31 +966,31 @@ private async Task CreateExecutableAsync(AppResource er, ILogger resourceLogger,
}
var spec = exe.Spec;

// Don't create an args collection unless needed. A null args collection means a project run by the will use args provided by the launch profile.
// https://github.com/dotnet/aspire/blob/main/docs/specs/IDE-execution.md#launch-profile-processing-project-launch-configuration
spec.Args = null;

// An executable can be restarted so args must be reset to an empty state.
// After resetting, first apply any dotnet project related args, e.g. configuration, and then add args from the model resource.
spec.Args = [];
if (er.DcpResource.TryGetAnnotationAsObjectList<string>(CustomResource.ResourceProjectArgsAnnotation, out var projectArgs))
if (er.DcpResource.TryGetAnnotationAsObjectList<string>(CustomResource.ResourceProjectArgsAnnotation, out var projectArgs) && projectArgs.Count > 0)
{
spec.Args ??= [];
spec.Args.AddRange(projectArgs);
}

var launchArgs = new List<(string Value, bool IsSensitive, bool AnnotationOnly)>();
// Get args from app host model resource.
(var appHostArgs, var failedToApplyArgs) = await BuildArgsAsync(resourceLogger, er.ModelResource, cancellationToken).ConfigureAwait(false);

// If the executable is a project then include any command line args from the launch profile.
if (er.ModelResource is ProjectResource project)
{
// When the .NET project is launched from an IDE the launch profile args are automatically added.
// We still want to display the args in the dashboard so only add them to the custom arg annotations.
var annotationOnly = spec.ExecutionType == ExecutionType.IDE;
var launchArgs = BuildLaunchArgs(er, spec, appHostArgs);

var launchProfileArgs = GetLaunchProfileArgs(project.GetEffectiveLaunchProfile()?.LaunchProfile);
launchArgs.AddRange(launchProfileArgs.Select(a => (a, isSensitive: false, annotationOnly)));
var executableArgs = launchArgs.Where(a => !a.AnnotationOnly).Select(a => a.Value).ToList();
if (executableArgs.Count > 0)
{
spec.Args ??= [];
spec.Args.AddRange(executableArgs);
}

(var args, var failedToApplyArgs) = await BuildArgsAsync(resourceLogger, er.ModelResource, cancellationToken).ConfigureAwait(false);
launchArgs.AddRange(args.Select(a => (a.Value, a.IsSensitive, annotationOnly: false)));

spec.Args.AddRange(launchArgs.Where(a => !a.AnnotationOnly).Select(a => a.Value));
// Arg annotations are what is displayed in the dashboard.
er.DcpResource.SetAnnotationAsObjectList(CustomResource.ResourceAppArgsAnnotation, launchArgs.Select(a => new AppLaunchArgumentAnnotation(a.Value, isSensitive: a.IsSensitive)));

(spec.Env, var failedToApplyConfiguration) = await BuildEnvVarsAsync(resourceLogger, er.ModelResource, cancellationToken).ConfigureAwait(false);
Expand All @@ -1003,6 +1003,37 @@ private async Task CreateExecutableAsync(AppResource er, ILogger resourceLogger,
await _kubernetesService.CreateAsync(exe, cancellationToken).ConfigureAwait(false);
}

private static List<(string Value, bool IsSensitive, bool AnnotationOnly)> BuildLaunchArgs(AppResource er, ExecutableSpec spec, List<(string Value, bool IsSensitive)> appHostArgs)
{
// Launch args is the final list of args that are displayed in the UI and possibly added to the executable spec.
// They're built from app host resource model args and any args in the effective launch profile.
// Follows behavior in the IDE execution spec when in IDE execution mode:
// https://github.com/dotnet/aspire/blob/main/docs/specs/IDE-execution.md#launch-profile-processing-project-launch-configuration
var launchArgs = new List<(string Value, bool IsSensitive, bool AnnotationOnly)>();

// If the executable is a project then include any command line args from the launch profile.
if (er.ModelResource is ProjectResource project)
{
// Args in the launch profile is used when:
// 1. The project is run as an executable. Launch profile args are combined with app host supplied args.
// 2. The project is run by the IDE and no app host args are specified.
if (spec.ExecutionType == ExecutionType.Process || (spec.ExecutionType == ExecutionType.IDE && appHostArgs.Count == 0))
{
// When the .NET project is launched from an IDE the launch profile args are automatically added.
// We still want to display the args in the dashboard so only add them to the custom arg annotations.
var annotationOnly = spec.ExecutionType == ExecutionType.IDE;

var launchProfileArgs = GetLaunchProfileArgs(project.GetEffectiveLaunchProfile()?.LaunchProfile);
launchArgs.AddRange(launchProfileArgs.Select(a => (a, isSensitive: false, annotationOnly)));
}
}

// In the situation where args are combined (process execution) the app host args are added after the launch profile args.
launchArgs.AddRange(appHostArgs.Select(a => (a.Value, a.IsSensitive, annotationOnly: false)));

return launchArgs;
}

private static List<string> GetLaunchProfileArgs(LaunchProfile? launchProfile)
{
var args = new List<string>();
Expand Down
50 changes: 19 additions & 31 deletions tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,17 +106,19 @@ public async Task ResourceStarted_ProjectHasReplicas_EventRaisedOnce()
}

[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task CreateExecutable_LaunchProfileHasCommandLineArgs_AnnotationsAdded(bool isIDE)
[InlineData(ExecutionType.IDE, false, null, new string[] { "--", "--test1", "--test2" })]
[InlineData(ExecutionType.IDE, true, new string[] { "--withargs-test" }, new string[] { "--withargs-test" })]
[InlineData(ExecutionType.Process, false, new string[] { "--", "--test1", "--test2" }, new string[] { "--", "--test1", "--test2" })]
[InlineData(ExecutionType.Process, true, new string[] { "--", "--test1", "--test2", "--withargs-test" }, new string[] { "--", "--test1", "--test2", "--withargs-test" })]
public async Task CreateExecutable_LaunchProfileHasCommandLineArgs_AnnotationsAdded(string executionType, bool addAppHostArgs, string[]? expectedArgs, string[]? expectedAnnotations)
{
var builder = DistributedApplication.CreateBuilder(new DistributedApplicationOptions
{
AssemblyName = typeof(DistributedApplicationTests).Assembly.FullName
});

IConfiguration? configuration = null;
if (isIDE)
if (executionType == ExecutionType.IDE)
{
var configurationBuilder = new ConfigurationBuilder();
configurationBuilder.AddInMemoryCollection(new Dictionary<string, string?>
Expand All @@ -127,11 +129,15 @@ public async Task CreateExecutable_LaunchProfileHasCommandLineArgs_AnnotationsAd
configuration = configurationBuilder.Build();
}

var resource = builder.AddProject<Projects.ServiceA>("ServiceA")
.WithArgs(c =>
{
c.Args.Add("--withargs-test");
}).Resource;
var resourceBuilder = builder.AddProject<Projects.ServiceA>("ServiceA");
if (addAppHostArgs)
{
resourceBuilder
.WithArgs(c =>
{
c.Args.Add("--withargs-test");
});
}

var kubernetesService = new TestKubernetesService();
using var app = builder.Build();
Expand All @@ -148,30 +154,12 @@ public async Task CreateExecutable_LaunchProfileHasCommandLineArgs_AnnotationsAd

var exe = Assert.Single(executables);

if (isIDE)
{
var callArg = Assert.Single(exe.Spec.Args!);
Assert.Equal("--withargs-test", callArg);
}
else
{
// ignore dotnet specific args for .NET project
var callArgs = exe.Spec.Args![^4..];

Assert.Collection(callArgs,
a => Assert.Equal("--", a),
a => Assert.Equal("--test1", a),
a => Assert.Equal("--test2", a),
a => Assert.Equal("--withargs-test", a));
}
// Ignore dotnet specific args for .NET project in process execution.
var callArgs = executionType == ExecutionType.IDE ? exe.Spec.Args : exe.Spec.Args![^(expectedArgs?.Length ?? 0)..];
Assert.Equal(expectedArgs, callArgs);

Assert.True(exe.TryGetAnnotationAsObjectList<AppLaunchArgumentAnnotation>(CustomResource.ResourceAppArgsAnnotation, out var argAnnotations));

Assert.Collection(argAnnotations,
a => Assert.Equal("--", a.Argument),
a => Assert.Equal("--test1", a.Argument),
a => Assert.Equal("--test2", a.Argument),
a => Assert.Equal("--withargs-test", a.Argument));
Assert.Equal(expectedAnnotations, argAnnotations.Select(a => a.Argument));
}

[Fact]
Expand Down