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
109 changes: 81 additions & 28 deletions src/Aspire.Cli/Projects/GuestAppHostProject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -383,8 +383,9 @@ public async Task<int> RunAsync(AppHostProjectContext context, CancellationToken
// Signal that build/preparation is complete
context.BuildCompletionSource?.TrySetResult(true);

// Read launch settings and set shared environment variables
var launchSettingsEnvVars = GetServerEnvironmentVariables(directory);
// Read launch settings once and reuse them for both the temporary server and guest AppHost.
var launchProfileEnvironmentVariables = ReadLaunchSettingsEnvironmentVariables(directory);
var launchSettingsEnvVars = GetServerEnvironmentVariables(launchProfileEnvironmentVariables);

// Apply certificate environment variables (e.g., SSL_CERT_DIR on Linux)
foreach (var kvp in certEnvVars)
Expand Down Expand Up @@ -470,14 +471,13 @@ await GenerateCodeViaRpcAsync(

// Step 8: Execute the guest apphost

// Pass the socket path, project directory, and apphost file path to the guest process
var environmentVariables = new Dictionary<string, string>(context.EnvironmentVariables)
{
["REMOTE_APP_HOST_SOCKET_PATH"] = socketPath,
["ASPIRE_PROJECT_DIRECTORY"] = directory.FullName,
["ASPIRE_APPHOST_FILEPATH"] = appHostFile.FullName,
[KnownConfigNames.RemoteAppHostToken] = authenticationToken
};
// Pass the launch profile and certificate environment variables through to the guest AppHost
// so it sees the same dashboard and resource service endpoints as the temporary .NET server.
var environmentVariables = CreateGuestEnvironmentVariables(context.EnvironmentVariables, launchProfileEnvironmentVariables, certEnvVars);
environmentVariables["REMOTE_APP_HOST_SOCKET_PATH"] = socketPath;
environmentVariables["ASPIRE_PROJECT_DIRECTORY"] = directory.FullName;
environmentVariables["ASPIRE_APPHOST_FILEPATH"] = appHostFile.FullName;
environmentVariables[KnownConfigNames.RemoteAppHostToken] = authenticationToken;
Comment thread
sebastienros marked this conversation as resolved.

// Check if the extension should launch the guest app host (for VS Code debugging).
// This mirrors the pattern in DotNetCliRunner.ExecuteAsync for .NET app hosts.
Expand Down Expand Up @@ -581,17 +581,70 @@ await GenerateCodeViaRpcAsync(

internal Dictionary<string, string> GetServerEnvironmentVariables(DirectoryInfo directory)
{
var envVars = ReadLaunchSettingsEnvironmentVariables(directory) ?? new Dictionary<string, string>();
return GetServerEnvironmentVariables(ReadLaunchSettingsEnvironmentVariables(directory));
}

private static Dictionary<string, string> GetServerEnvironmentVariables(IDictionary<string, string>? launchProfileEnvironmentVariables)
{
var envVars = new Dictionary<string, string>();
MergeLaunchProfileEnvironmentVariables(launchProfileEnvironmentVariables, envVars, defaultEnvironment: "Development");
return envVars;
}

internal Dictionary<string, string> CreateGuestEnvironmentVariables(
DirectoryInfo directory,
IDictionary<string, string> contextEnvironmentVariables,
IDictionary<string, string>? additionalEnvironmentVariables = null)
{
return CreateGuestEnvironmentVariables(
contextEnvironmentVariables,
ReadLaunchSettingsEnvironmentVariables(directory),
additionalEnvironmentVariables);
}

// Support ASPIRE_ENVIRONMENT from the launch profile to set both DOTNET_ENVIRONMENT and ASPNETCORE_ENVIRONMENT
envVars.TryGetValue("ASPIRE_ENVIRONMENT", out var environment);
environment ??= "Development";
internal static Dictionary<string, string> CreateGuestEnvironmentVariables(
IDictionary<string, string> contextEnvironmentVariables,
IDictionary<string, string>? launchProfileEnvironmentVariables,
IDictionary<string, string>? additionalEnvironmentVariables = null)
{
var environmentVariables = new Dictionary<string, string>(contextEnvironmentVariables);

// Set the environment for the AppHost server process
envVars["DOTNET_ENVIRONMENT"] = environment;
envVars["ASPNETCORE_ENVIRONMENT"] = environment;
MergeLaunchProfileEnvironmentVariables(launchProfileEnvironmentVariables, environmentVariables);

return envVars;
if (additionalEnvironmentVariables is not null)
{
foreach (var (key, value) in additionalEnvironmentVariables)
{
environmentVariables[key] = value;
}
}

return environmentVariables;
}

private static void MergeLaunchProfileEnvironmentVariables(
IDictionary<string, string>? launchProfileEnvironmentVariables,
IDictionary<string, string> environmentVariables,
string? defaultEnvironment = null)
{
if (launchProfileEnvironmentVariables is not null)
{
foreach (var (key, value) in launchProfileEnvironmentVariables)
{
environmentVariables[key] = value;
}
}

if (launchProfileEnvironmentVariables?.TryGetValue("ASPIRE_ENVIRONMENT", out var environment) == true)
{
environmentVariables["DOTNET_ENVIRONMENT"] = environment;
environmentVariables["ASPNETCORE_ENVIRONMENT"] = environment;
}
else if (defaultEnvironment is not null)
{
environmentVariables["DOTNET_ENVIRONMENT"] = defaultEnvironment;
environmentVariables["ASPNETCORE_ENVIRONMENT"] = defaultEnvironment;
}
}

private Dictionary<string, string>? ReadLaunchSettingsEnvironmentVariables(DirectoryInfo directory)
Expand Down Expand Up @@ -765,8 +818,9 @@ public async Task<int> PublishAsync(PublishContext context, CancellationToken ca
// Store output collector in context for exception handling
context.OutputCollector = prepareOutput;

// Read launch settings and set shared environment variables
var launchSettingsEnvVars = GetServerEnvironmentVariables(directory);
// Read launch settings once and reuse them for both the temporary server and guest AppHost.
var launchProfileEnvironmentVariables = ReadLaunchSettingsEnvironmentVariables(directory);
var launchSettingsEnvVars = GetServerEnvironmentVariables(launchProfileEnvironmentVariables);

// Generate a backchannel socket path for CLI to connect to AppHost server
var backchannelSocketPath = GetBackchannelSocketPath();
Expand Down Expand Up @@ -840,14 +894,13 @@ await GenerateCodeViaRpcAsync(
cancellationToken);
}

// Pass the socket path, project directory, and apphost file path to the guest process
var environmentVariables = new Dictionary<string, string>(context.EnvironmentVariables)
{
["REMOTE_APP_HOST_SOCKET_PATH"] = jsonRpcSocketPath,
["ASPIRE_PROJECT_DIRECTORY"] = directory.FullName,
["ASPIRE_APPHOST_FILEPATH"] = appHostFile.FullName,
[KnownConfigNames.RemoteAppHostToken] = authenticationToken
};
// Pass the launch profile environment variables through to the guest AppHost so publish mode
// uses the same dashboard and resource service endpoints as the temporary .NET server.
var environmentVariables = CreateGuestEnvironmentVariables(context.EnvironmentVariables, launchProfileEnvironmentVariables);
environmentVariables["REMOTE_APP_HOST_SOCKET_PATH"] = jsonRpcSocketPath;
environmentVariables["ASPIRE_PROJECT_DIRECTORY"] = directory.FullName;
environmentVariables["ASPIRE_APPHOST_FILEPATH"] = appHostFile.FullName;
environmentVariables[KnownConfigNames.RemoteAppHostToken] = authenticationToken;
Comment thread
sebastienros marked this conversation as resolved.

// Step 6: Execute the guest apphost for publishing
// Pass the publish arguments (e.g., --operation publish --step deploy)
Expand Down
43 changes: 43 additions & 0 deletions tests/Aspire.Cli.Tests/Projects/GuestAppHostProjectTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,49 @@ public void GetServerEnvironmentVariables_ParsesLaunchSettingsWithComments()
Assert.False(envVars.ContainsKey("ASPIRE_DASHBOARD_OTLP_HTTP_ENDPOINT_URL"));
}

[Fact]
public void CreateGuestEnvironmentVariables_MergesLaunchProfileContextAndAdditionalEnvironmentVariables()
{
var project = CreateGuestAppHostProject();

var aspireConfigPath = Path.Combine(_workspace.WorkspaceRoot.FullName, AspireConfigFile.FileName);
File.WriteAllText(aspireConfigPath, """
{
"profiles": {
"https": {
"applicationUrl": "https://localhost:16319;http://localhost:16320",
"environmentVariables": {
"ASPIRE_ENVIRONMENT": "Staging",
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:17269",
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:18269"
}
}
}
}
""");

var envVars = project.CreateGuestEnvironmentVariables(
_workspace.WorkspaceRoot,
new Dictionary<string, string>
{
["CUSTOM_CONTEXT_VARIABLE"] = "context",
["ASPNETCORE_URLS"] = "http://context"
},
new Dictionary<string, string>
{
["SSL_CERT_DIR"] = "/tmp/certs"
});

Assert.Equal("context", envVars["CUSTOM_CONTEXT_VARIABLE"]);
Assert.Equal("https://localhost:16319;http://localhost:16320", envVars["ASPNETCORE_URLS"]);
Assert.Equal("Staging", envVars["ASPIRE_ENVIRONMENT"]);
Assert.Equal("Staging", envVars["DOTNET_ENVIRONMENT"]);
Assert.Equal("Staging", envVars["ASPNETCORE_ENVIRONMENT"]);
Assert.Equal("https://localhost:17269", envVars["ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL"]);
Assert.Equal("https://localhost:18269", envVars["ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL"]);
Assert.Equal("/tmp/certs", envVars["SSL_CERT_DIR"]);
}

private static GuestAppHostProject CreateGuestAppHostProject()
{
var language = new LanguageInfo(
Expand Down
Loading