Skip to content
New issue

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

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

Already on GitHub? # to your account

Improvements to RunWithHttpsDevCert extensions #667

Draft
wants to merge 33 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
772bf41
Use Directory.CreateTempSubDirectory instead of Path.GetTempDir
DamianEdwards Jan 28, 2025
61e8e77
Update DevCertHostingExtensions.cs
DamianEdwards Jan 28, 2025
88d1363
Put dev-cert in /obj (or user temp if not available)
DamianEdwards Jan 29, 2025
21a3172
Update DevCertHostingExtensions.cs
DamianEdwards Jan 29, 2025
b31c3e0
Fix typo
DamianEdwards Jan 29, 2025
f6f0c1b
WIP
DamianEdwards Jan 30, 2025
898321e
More WIP
DamianEdwards Jan 30, 2025
f7d0f93
WIPpy
DamianEdwards Jan 30, 2025
0e30ab5
Working now
DamianEdwards Jan 30, 2025
bc59278
Update Metrics sample
DamianEdwards Jan 30, 2025
78eacd0
Fix
DamianEdwards Jan 30, 2025
fa88fca
Run Redis with the dev cert in the Node sample
DamianEdwards Jan 31, 2025
6f2bda3
Update RedisHostingExtensions.cs
DamianEdwards Jan 31, 2025
c692ad4
Update Directory.Build.targets
DamianEdwards Jan 31, 2025
c14cacb
Don't use cert with Redis for now
DamianEdwards Feb 1, 2025
25feb3c
Insert resource logger at 0
DamianEdwards Feb 1, 2025
e23cec6
Enable DCP logs
DamianEdwards Feb 1, 2025
cbd2813
Update ci.yml
DamianEdwards Feb 1, 2025
fc132fe
Update ci.yml
DamianEdwards Feb 1, 2025
29a126d
Update ci.yml
DamianEdwards Feb 1, 2025
ef10a2c
pls give me logs!
DamianEdwards Feb 1, 2025
90a1322
Fix double slashes
DamianEdwards Feb 1, 2025
b53e2ec
Update ci.yml
DamianEdwards Feb 1, 2025
b47d9c8
moar logs!
DamianEdwards Feb 1, 2025
995a77e
grrrrr
DamianEdwards Feb 1, 2025
bf36d18
quote container args
DamianEdwards Feb 1, 2025
e328cff
Revert "quote container args"
DamianEdwards Feb 1, 2025
c35823c
Update all to latest 9.1 SDK
DamianEdwards Feb 1, 2025
33fa711
Revert "Update all to latest 9.1 SDK"
DamianEdwards Feb 2, 2025
dfc257b
Don't bind mount the dev-cert folder to see if container starts
DamianEdwards Feb 3, 2025
073f519
Add bind mount back but don't pass Redis TLS args
DamianEdwards Feb 3, 2025
974bc2d
Use SetUnixFileMode
DamianEdwards Feb 4, 2025
15376bd
Update DevCertHostingExtensions.cs
DamianEdwards Feb 4, 2025
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
11 changes: 10 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ jobs:
DOTNET_INSTALL_DIR: ${{ matrix.os == 'ubuntu-latest' && '' || 'dotnet' }}
ASPIRE_ALLOW_UNSECURED_TRANSPORT: true
SuppressNETCoreSdkPreviewMessage: true
DCP_DIAGNOSTICS_LOG_LEVEL: debug
DCP_PRESERVE_EXECUTABLE_LOGS: 1
steps:
- uses: actions/checkout@v4

Expand Down Expand Up @@ -93,7 +95,14 @@ jobs:
RunConfiguration.CollectSourceInformation=true
env:
DAPR_CLI_PATH: ${{ steps.install-dapr.outputs.dapr-path }}


- name: Copy dcp logs
if: (success() || steps.test.conclusion == 'failure') && matrix.os == 'ubuntu-latest'
run: |
mkdir -p ./TestResults/dcplogs
cp -r /tmp/dcp/logs ./TestResults/dcplogs
cp -r /tmp/aspire.* ./TestResults/dcplogs

- name: Publish Test Results
if: (success() || steps.test.conclusion == 'failure') && matrix.os == 'ubuntu-latest'
uses: actions/upload-artifact@v4
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsAspireHost>true</IsAspireHost>
<UserSecretsId>f428d36b-3fad-4237-a29d-6dd8c8605ca5</UserSecretsId>
</PropertyGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace Aspire.Hosting;

Expand All @@ -8,24 +9,25 @@ internal static class NodeHostingExtensions
/// Injects the ASP.NET Core HTTPS developer certificate into the resource via the specified environment variables when
/// <paramref name="builder"/>.<see cref="IResourceBuilder{T}.ApplicationBuilder">ApplicationBuilder</see>.<see cref="IDistributedApplicationBuilder.ExecutionContext">ExecutionContext</see>.<see cref="DistributedApplicationExecutionContext.IsRunMode">IsRunMode</see><c> == true</c>.<br/>
/// </summary>
public static IResourceBuilder<NodeAppResource> RunWithHttpsDevCertificate(this IResourceBuilder<NodeAppResource> builder, string certFileEnv, string certKeyFileEnv)
public static IResourceBuilder<NodeAppResource> RunWithHttpsDevCertificate(this IResourceBuilder<NodeAppResource> builder, CertificateFileFormat certificateFileFormat, string certFileEnv, string? certPasswordOrKeyEnv)
{
if (builder.ApplicationBuilder.ExecutionContext.IsRunMode && builder.ApplicationBuilder.Environment.IsDevelopment())
{
DevCertHostingExtensions.RunWithHttpsDevCertificate(builder, certFileEnv, certKeyFileEnv, (certFilePath, certKeyPath) =>
DevCertHostingExtensions.RunWithHttpsDevCertificate(builder, certificateFileFormat, certFileEnv, certPasswordOrKeyEnv, (services, certFilePath, certPasswordOrKeyPath) =>
{
builder.WithHttpsEndpoint(env: "HTTPS_PORT");
var httpsEndpoint = builder.GetEndpoint("https");

builder.WithEnvironment(context =>

// Configure Node to trust the ASP.NET Core HTTPS development certificate as a root CA.
builder.WithEnvironment(async context =>
{
// Configure Node to trust the ASP.NET Core HTTPS development certificate as a root CA.
if (context.EnvironmentVariables.TryGetValue(certFileEnv, out var certPath))
{
context.EnvironmentVariables["NODE_EXTRA_CA_CERTS"] = certPath;
context.EnvironmentVariables["HTTPS_REDIRECT_PORT"] = ReferenceExpression.Create($"{httpsEndpoint.Property(EndpointProperty.Port)}");
}
var logger = services.GetRequiredService<ResourceLoggerService>().GetLogger(builder.Resource);
var (succeded, pemFilePath, _) = await DevCertHostingExtensions.TryExportDevCertificateAsync(CertificateFileFormat.Pem, builder.ApplicationBuilder, logger);
context.EnvironmentVariables["NODE_EXTRA_CA_CERTS"] = pemFilePath;
context.EnvironmentVariables["HTTPS_REDIRECT_PORT"] = ReferenceExpression.Create($"{httpsEndpoint.Property(EndpointProperty.Port)}");
});

return Task.CompletedTask;
});
}

Expand Down
5 changes: 3 additions & 2 deletions samples/AspireWithNode/AspireWithNode.AppHost/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

var builder = DistributedApplication.CreateBuilder(args);

var cache = builder.AddRedis("cache");
var cache = builder.AddRedis("cache")
.RunWithHttpsDevCertificate();

var weatherapi = builder.AddProject<Projects.AspireWithNode_AspNetCoreApi>("weatherapi");

Expand All @@ -20,7 +21,7 @@

if (builder.Environment.IsDevelopment() && launchProfile == "https")
{
frontend.RunWithHttpsDevCertificate("HTTPS_CERT_FILE", "HTTPS_CERT_KEY_FILE");
frontend.RunWithHttpsDevCertificate(CertificateFileFormat.PfxWithPassword, "HTTPS_CERT_PFX_FILE", "HTTPS_CERT_PASSWORD");
}

builder.Build().Run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using Microsoft.Extensions.Hosting;

namespace Aspire.Hosting;

internal static class RedisHostingExtensions
{
/// <summary>
/// Configures the Redis resource to use the ASP.NET Core HTTPS developer certificate when
/// <paramref name="builder"/>.<see cref="IResourceBuilder{T}.ApplicationBuilder">ApplicationBuilder</see>.<see cref="IDistributedApplicationBuilder.ExecutionContext">ExecutionContext</see>.<see cref="DistributedApplicationExecutionContext.IsRunMode">IsRunMode</see><c> == true</c>.<br/>
/// </summary>
public static IResourceBuilder<RedisResource> RunWithHttpsDevCertificate(this IResourceBuilder<RedisResource> builder)
{
if (builder.ApplicationBuilder.ExecutionContext.IsRunMode && builder.ApplicationBuilder.Environment.IsDevelopment())
{
DevCertHostingExtensions.RunWithHttpsDevCertificate(builder, CertificateFileFormat.Pem, "TLS_CERT_FILE", "TLS_KEY_FILE", (services, certFilePath, certPasswordOrKeyPath) =>
{
// This callback is invoked during the BeforeStartEvent phase if the certificate is successfully exported.
builder.WithConnectionStringRedirection(new RedisTlsConnectionString(builder.Resource));

// Configure Redis to use the ASP.NET Core HTTPS development certificate.
builder.WithArgs(context =>
{
context.Args.Add("--port");
context.Args.Add("0");
context.Args.Add("--tls-port");
context.Args.Add(builder.Resource.PrimaryEndpoint.TargetPort.ToString()!);
context.Args.Add("--tls-cert-file");
context.Args.Add($"{DevCertHostingExtensions.DEV_CERT_BIND_MOUNT_DEST_DIR}/{DevCertHostingExtensions.DEV_CERT_FILE_NAME_PEM}");
context.Args.Add("--tls-key-file");
context.Args.Add($"{DevCertHostingExtensions.DEV_CERT_BIND_MOUNT_DEST_DIR}/{DevCertHostingExtensions.DEV_CERT_FILE_NAME_KEY}");
context.Args.Add("--tls-ca-cert-file");
context.Args.Add($"{DevCertHostingExtensions.DEV_CERT_BIND_MOUNT_DEST_DIR}/{DevCertHostingExtensions.DEV_CERT_FILE_NAME_PEM}");
context.Args.Add("--tls-auth-clients");
context.Args.Add("no");
});

return Task.CompletedTask;
});
}

return builder;
}

private class RedisTlsConnectionString(RedisResource resource) : IResourceWithConnectionString
{
public string Name { get; } = $"{resource.Name}-tls";

public ReferenceExpression ConnectionStringExpression => ReferenceExpression.Create(
$"{resource.PrimaryEndpoint.Property(EndpointProperty.Host)}:{resource.PrimaryEndpoint.Property(EndpointProperty.Port)},Ssl=true");

public ResourceAnnotationCollection Annotations { get; } = [];
}
}
40 changes: 26 additions & 14 deletions samples/AspireWithNode/NodeFrontend/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ const config = {
httpPort: process.env['PORT'] ?? 8080,
httpsPort: process.env['HTTPS_PORT'] ?? 8443,
httpsRedirectPort: process.env['HTTPS_REDIRECT_PORT'] ?? (process.env['HTTPS_PORT'] ?? 8443),
certPfxFile: process.env['HTTPS_CERT_PFX_FILE'] ?? '',
certPassword: process.env['HTTPS_CERT_PASSWORD'] ?? '',
certFile: process.env['HTTPS_CERT_FILE'] ?? '',
certKeyFile: process.env['HTTPS_CERT_KEY_FILE'] ?? '',
cacheAddress: process.env['ConnectionStrings__cache'] ?? '',
Expand All @@ -20,28 +22,38 @@ const config = {
console.log(`config: ${JSON.stringify(config)}`);

// Setup HTTPS options
const httpsOptions = fs.existsSync(config.certFile) && fs.existsSync(config.certKeyFile)
const httpsOptions = fs.existsSync(config.certPfxFile)
? {
cert: fs.readFileSync(config.certFile),
key: fs.readFileSync(config.certKeyFile),
pfx: fs.readFileSync(config.certPfxFile),
passphrase: config.certPassword,
enabled: true
}
: { enabled: false };
: fs.existsSync(config.certFile) && fs.existsSync(config.certKeyFile)
? {
cert: fs.readFileSync(config.certFile),
key: fs.readFileSync(config.certKeyFile),
enabled: true
}
: { enabled: false };

// Setup connection to Redis cache
const passwordPrefix = ',password=';
let cacheOptions = {};
config.cacheAddress.split(',').forEach(setting => {
const parts = setting.split('=');
if (parts.length === 2) {
const key = parts[0].toLowerCase();
const value = parts[1];
cacheOptions[key] = value;
} else if (parts.length === 1) {
cacheOptions['url'] = setting;
}
});

let cacheConfig = {
url: `redis://${config.cacheAddress}`
url: `${ cacheOptions.ssl ? "rediss" : "redis" }://${cacheOptions.url}`,
password: cacheOptions.password
};

const cachePasswordIndex = config.cacheAddress.indexOf(passwordPrefix);
if (cachePasswordIndex > 0) {
cacheConfig = {
url: `redis://${config.cacheAddress.substring(0, cachePasswordIndex)}`,
password: config.cacheAddress.substring(cachePasswordIndex + passwordPrefix.length)
}
}

const cache = createClient(cacheConfig);
cache.on('error', err => console.error('Redis Client Error', err));
await cache.connect();
Expand Down
11 changes: 11 additions & 0 deletions samples/Directory.Build.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Project>
Copy link
Member

Choose a reason for hiding this comment

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

It wouldn't hurt to stick a comment in here in case someone stumbles in and asks what this is for.

<!-- Embed the path to /obj so that hosting integrations can write files to re-use between runs there -->
<Target Name="EmbedAppHostIntermediateOutputPath" BeforeTargets="GetAssemblyAttributes">
<ItemGroup>
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
<_Parameter1>apphostprojectbaseintermediateoutputpath</_Parameter1>
<_Parameter2>$(BaseIntermediateOutputPath)</_Parameter2>
</AssemblyAttribute>
</ItemGroup>
</Target>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,16 @@ public static IResourceBuilder<OpenTelemetryCollectorResource> AddOpenTelemetryC

if (isHttpsEnabled && builder.ExecutionContext.IsRunMode && builder.Environment.IsDevelopment())
{
DevCertHostingExtensions.RunWithHttpsDevCertificate(resourceBuilder, "HTTPS_CERT_FILE", "HTTPS_CERT_KEY_FILE", (certFilePath, certKeyPath) =>
DevCertHostingExtensions.RunWithHttpsDevCertificate(resourceBuilder, CertificateFileFormat.Pem, "HTTPS_CERT_FILE", "HTTPS_CERT_KEY_FILE", (_, certFilePath, certKeyPath) =>
{
// Set TLS details using YAML path via the command line. This allows the values to be added to the existing config file.
// Setting the values in the config file doesn't work because adding the "tls" section always enables TLS, even if there is no cert provided.
resourceBuilder.WithArgs(
@"--config=yaml:receivers::otlp::protocols::grpc::tls::cert_file: ""dev-certs/dev-cert.pem""",
@"--config=yaml:receivers::otlp::protocols::grpc::tls::key_file: ""dev-certs/dev-cert.key""",
@"--config=/etc/otelcol-contrib/config.yaml");

return Task.CompletedTask;
});
}

Expand Down
Loading