Skip to content

Commit 81eaa6f

Browse files
committed
Updated Devcerts code
1 parent fe14d9d commit 81eaa6f

File tree

2 files changed

+110
-168
lines changed

2 files changed

+110
-168
lines changed

src/CommunityToolkit.Aspire.Hosting.OpenTelemetryCollector/OpenTelemetryCollectorExtensions.cs

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -51,21 +51,21 @@ public static IResourceBuilder<OpenTelemetryCollectorResource> AddOpenTelemetryC
5151

5252
if (!settings.ForceNonSecureReceiver && isHttpsEnabled && builder.ExecutionContext.IsRunMode && builder.Environment.IsDevelopment())
5353
{
54-
DevCertHostingExtensions.RunWithHttpsDevCertificate(resourceBuilder, "HTTPS_CERT_FILE", "HTTPS_CERT_KEY_FILE", (certFilePath, certKeyPath) =>
54+
resourceBuilder.RunWithHttpsDevCertificate();
55+
var certFilePath = Path.Combine(DevCertHostingExtensions.DEV_CERT_BIND_MOUNT_DEST_DIR, DevCertHostingExtensions.CERT_FILE_NAME);
56+
var certKeyPath = Path.Combine(DevCertHostingExtensions.DEV_CERT_BIND_MOUNT_DEST_DIR, DevCertHostingExtensions.CERT_KEY_FILE_NAME);
57+
if (settings.EnableHttpEndpoint)
5558
{
56-
if (settings.EnableHttpEndpoint)
57-
{
58-
resourceBuilder.WithArgs(
59-
$@"--config=yaml:receivers::otlp::protocols::http::tls::cert_file: ""{certFilePath}""",
60-
$@"--config=yaml:receivers::otlp::protocols::http::tls::key_file: ""{certKeyPath}""");
61-
}
62-
if (settings.EnableGrpcEndpoint)
63-
{
64-
resourceBuilder.WithArgs(
65-
$@"--config=yaml:receivers::otlp::protocols::grpc::tls::cert_file: ""{certFilePath}""",
66-
$@"--config=yaml:receivers::otlp::protocols::grpc::tls::key_file: ""{certKeyPath}""");
67-
}
68-
});
59+
resourceBuilder.WithArgs(
60+
$@"--config=yaml:receivers::otlp::protocols::http::tls::cert_file: ""{certFilePath}""",
61+
$@"--config=yaml:receivers::otlp::protocols::http::tls::key_file: ""{certKeyPath}""");
62+
}
63+
if (settings.EnableGrpcEndpoint)
64+
{
65+
resourceBuilder.WithArgs(
66+
$@"--config=yaml:receivers::otlp::protocols::grpc::tls::cert_file: ""{certFilePath}""",
67+
$@"--config=yaml:receivers::otlp::protocols::grpc::tls::key_file: ""{certKeyPath}""");
68+
}
6969
}
7070
return resourceBuilder;
7171
}
Lines changed: 96 additions & 154 deletions
Original file line numberDiff line numberDiff line change
@@ -1,199 +1,141 @@
1-
using Aspire.Hosting;
2-
using Aspire.Hosting.ApplicationModel;
3-
using Microsoft.Extensions.DependencyInjection;
4-
using Microsoft.Extensions.Hosting;
5-
using Microsoft.Extensions.Logging;
6-
using System.Diagnostics;
1+
using Aspire.Hosting.ApplicationModel;
72

8-
namespace Aspire;
3+
namespace Aspire.Hosting;
94

10-
internal static class DevCertHostingExtensions
5+
/// <summary>
6+
/// Extensions for adding Dev Certs to aspire resources.
7+
/// </summary>
8+
public static class DevCertHostingExtensions
119
{
10+
/// <summary>
11+
/// The destination directory for the certificate files in a container.
12+
/// </summary>
13+
public const string DEV_CERT_BIND_MOUNT_DEST_DIR = "/dev-certs";
14+
15+
/// <summary>
16+
/// The file name of the certificate file.
17+
/// </summary>
18+
public const string CERT_FILE_NAME = "dev-cert.pem";
19+
20+
/// <summary>
21+
/// The file name of the certificate key file.
22+
/// </summary>
23+
public const string CERT_KEY_FILE_NAME = "dev-cert.key";
24+
1225
/// <summary>
1326
/// Injects the ASP.NET Core HTTPS developer certificate into the resource via the specified environment variables when
1427
/// <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/>
15-
/// If the resource is a <see cref="ContainerResource"/>, the certificate files will be bind mounted into the container.
28+
/// If the resource is a <see cref="ContainerResource"/>, the certificate files will be provided via WithContainerFiles.
1629
/// </summary>
1730
/// <remarks>
1831
/// This method <strong>does not</strong> configure an HTTPS endpoint on the resource.
1932
/// Use <see cref="ResourceBuilderExtensions.WithHttpsEndpoint{TResource}"/> to configure an HTTPS endpoint.
2033
/// </remarks>
2134
public static IResourceBuilder<TResource> RunWithHttpsDevCertificate<TResource>(
22-
this IResourceBuilder<TResource> builder, string certFileEnv, string certKeyFileEnv, Action<string, string>? onSuccessfulExport = null)
23-
where TResource : IResourceWithEnvironment
35+
this IResourceBuilder<TResource> builder, string certFileEnv = "", string certKeyFileEnv = "")
36+
where TResource : IResourceWithEnvironment, IResourceWithWaitSupport
2437
{
25-
if (builder.ApplicationBuilder.ExecutionContext.IsRunMode && builder.ApplicationBuilder.Environment.IsDevelopment())
38+
if (!builder.ApplicationBuilder.ExecutionContext.IsRunMode)
2639
{
27-
builder.OnBeforeResourceStarted(async (resource, readyEvent, cancellationToken) =>
28-
{
29-
var logger = readyEvent.Services.GetRequiredService<ResourceLoggerService>().GetLogger(builder.Resource);
30-
31-
// Export the ASP.NET Core HTTPS development certificate & private key to files and configure the resource to use them via
32-
// the specified environment variables.
33-
var (exported, certPath, certKeyPath) = await TryExportDevCertificateAsync(builder.ApplicationBuilder, logger);
34-
35-
if (!exported)
36-
{
37-
// The export failed for some reason, don't configure the resource to use the certificate.
38-
return;
39-
}
40-
41-
var certKeyFileDest = "";
42-
var certFileDest = "";
43-
44-
if (builder.Resource is ContainerResource containerResource)
45-
{
46-
// Bind-mount the certificate files into the container.
47-
const string DEV_CERT_BIND_MOUNT_DEST_DIR = "/dev-certs";
48-
49-
var certFileName = Path.GetFileName(certPath);
50-
var certKeyFileName = Path.GetFileName(certKeyPath);
51-
52-
var bindSource = Path.GetDirectoryName(certPath) ?? throw new UnreachableException();
53-
54-
certFileDest = $"{DEV_CERT_BIND_MOUNT_DEST_DIR}/{certFileName}";
55-
certKeyFileDest = $"{DEV_CERT_BIND_MOUNT_DEST_DIR}/{certKeyFileName}";
56-
57-
containerResource.TryGetContainerMounts(out var mounts);
58-
if (mounts is null || !mounts.Any(m => m.Source == bindSource))
59-
{
60-
builder.ApplicationBuilder.CreateResourceBuilder(containerResource)
61-
.WithBindMount(bindSource, DEV_CERT_BIND_MOUNT_DEST_DIR, isReadOnly: false)
62-
.WithEnvironment(certFileEnv, certFileDest)
63-
.WithEnvironment(certKeyFileEnv, certKeyFileDest);
64-
}
65-
}
66-
else
67-
{
68-
builder
69-
.WithEnvironment(certFileEnv, certPath)
70-
.WithEnvironment(certKeyFileEnv, certKeyPath);
71-
}
72-
73-
if (onSuccessfulExport is not null)
74-
{
75-
onSuccessfulExport(certFileDest, certKeyFileDest);
76-
}
77-
});
40+
return builder;
7841
}
7942

80-
return builder;
81-
}
82-
83-
private static async Task<(bool, string CertFilePath, string CertKeyFilPath)> TryExportDevCertificateAsync(IDistributedApplicationBuilder builder, ILogger logger)
84-
{
85-
// Exports the ASP.NET Core HTTPS development certificate & private key to PEM files using 'dotnet dev-certs https' to a temporary
86-
// directory and returns the path.
87-
// TODO: Check if we're running on a platform that already has the cert and key exported to a file (e.g. macOS) and just use those instead.
88-
var appHostSha256 = builder.Configuration["AppHost:Sha256"];
89-
if (appHostSha256 is null)
43+
if (builder.Resource is not ContainerResource &&
44+
(!string.IsNullOrEmpty(certFileEnv) || !string.IsNullOrEmpty(certKeyFileEnv)))
9045
{
91-
throw new InvalidOperationException("Configuration value 'AppHost:Sha256' is missing. Cannot export development certificate.");
46+
throw new InvalidOperationException("RunWithHttpsDevCertificate needs environment variables only for Resources that aren't Containers.");
9247
}
93-
var appNameHash = appHostSha256[..10];
94-
var tempDir = Path.Combine(Path.GetTempPath(), $"aspire.{appNameHash}");
95-
var certExportPath = Path.Combine(tempDir, "dev-cert.pem");
96-
var certKeyExportPath = Path.Combine(tempDir, "dev-cert.key");
48+
49+
// Create temp directory for certificate export
50+
var tempDir = Directory.CreateTempSubdirectory("aspire-dev-certs");
51+
var certExportPath = Path.Combine(tempDir.FullName, "dev-cert.pem");
52+
var certKeyExportPath = Path.Combine(tempDir.FullName, "dev-cert.key");
9753

98-
if (File.Exists(certExportPath) && File.Exists(certKeyExportPath))
99-
{
100-
// Certificate already exported, return the path.
101-
logger.LogDebug("Using previously exported dev cert files '{CertPath}' and '{CertKeyPath}'", certExportPath, certKeyExportPath);
102-
return (true, certExportPath, certKeyExportPath);
103-
}
54+
// Create a unique resource name for the certificate export
55+
var exportResourceName = $"dev-cert-export";
10456

105-
if (File.Exists(certExportPath))
106-
{
107-
logger.LogTrace("Deleting previously exported dev cert file '{CertPath}'", certExportPath);
108-
File.Delete(certExportPath);
109-
}
57+
// Check if we already have a certificate export resource
58+
var existingResource = builder.ApplicationBuilder.Resources.FirstOrDefault(r => r.Name == exportResourceName);
59+
IResourceBuilder<ExecutableResource> exportExecutable;
11060

111-
if (File.Exists(certKeyExportPath))
61+
if (existingResource == null)
11262
{
113-
logger.LogTrace("Deleting previously exported dev cert key file '{CertKeyPath}'", certKeyExportPath);
114-
File.Delete(certKeyExportPath);
63+
// Create the executable resource to export the certificate
64+
exportExecutable = builder.ApplicationBuilder
65+
.AddExecutable(exportResourceName, "dotnet", tempDir.FullName)
66+
.WithEnvironment("DOTNET_CLI_UI_LANGUAGE", "en") // Ensure consistent output language
67+
.WithArgs(context =>
68+
{
69+
context.Args.Add("dev-certs");
70+
context.Args.Add("https");
71+
context.Args.Add("--export-path");
72+
context.Args.Add(certExportPath);
73+
context.Args.Add("--format");
74+
context.Args.Add("Pem");
75+
context.Args.Add("--no-password");
76+
});
11577
}
116-
117-
if (!Directory.Exists(tempDir))
78+
else
11879
{
119-
logger.LogTrace("Creating directory to export dev cert to '{ExportDir}'", tempDir);
120-
Directory.CreateDirectory(tempDir);
80+
exportExecutable = builder.ApplicationBuilder.CreateResourceBuilder((ExecutableResource)existingResource);
12181
}
12282

123-
string[] args = ["dev-certs", "https", "--export-path", $"\"{certExportPath}\"", "--format", "Pem", "--no-password"];
124-
var argsString = string.Join(' ', args);
83+
builder.WaitForCompletion(exportExecutable);
12584

126-
logger.LogTrace("Running command to export dev cert: {ExportCmd}", $"dotnet {argsString}");
127-
var exportStartInfo = new ProcessStartInfo
85+
// Configure the current resource with the certificate paths
86+
if (builder.Resource is ContainerResource containerResource)
12887
{
129-
FileName = "dotnet",
130-
Arguments = argsString,
131-
RedirectStandardOutput = true,
132-
RedirectStandardError = true,
133-
UseShellExecute = false,
134-
CreateNoWindow = true,
135-
WindowStyle = ProcessWindowStyle.Hidden,
136-
};
88+
// Use WithContainerFiles to provide the certificate files to the container
13789

138-
var exportProcess = new Process { StartInfo = exportStartInfo };
13990

140-
Task? stdOutTask = null;
141-
Task? stdErrTask = null;
91+
var certFileDest = $"{DEV_CERT_BIND_MOUNT_DEST_DIR}/{CERT_FILE_NAME}";
92+
var certKeyFileDest = $"{DEV_CERT_BIND_MOUNT_DEST_DIR}/{CERT_KEY_FILE_NAME}";
14293

143-
try
144-
{
145-
try
146-
{
147-
if (exportProcess.Start())
94+
builder.ApplicationBuilder.CreateResourceBuilder(containerResource)
95+
.WithContainerFiles(DEV_CERT_BIND_MOUNT_DEST_DIR, (context, cancellationToken) =>
14896
{
149-
stdOutTask = ConsumeOutput(exportProcess.StandardOutput, msg => logger.LogInformation("> {StandardOutput}", msg));
150-
stdErrTask = ConsumeOutput(exportProcess.StandardError, msg => logger.LogError("! {ErrorOutput}", msg));
151-
}
152-
}
153-
catch (Exception ex)
154-
{
155-
logger.LogError(ex, "Failed to start HTTPS dev certificate export process");
156-
return default;
157-
}
97+
var files = new List<ContainerFile>();
98+
99+
// Check if certificate files exist before adding them
100+
if (File.Exists(certExportPath))
101+
{
102+
files.Add(new ContainerFile
103+
{
104+
Name = CERT_FILE_NAME,
105+
SourcePath = certExportPath
106+
});
107+
}
158108

159-
var timeout = TimeSpan.FromSeconds(5);
160-
var exited = exportProcess.WaitForExit(timeout);
109+
if (File.Exists(certKeyExportPath))
110+
{
111+
files.Add(new ContainerFile
112+
{
113+
Name = CERT_KEY_FILE_NAME,
114+
SourcePath = certKeyExportPath
115+
});
116+
}
161117

162-
if (exited && File.Exists(certExportPath) && File.Exists(certKeyExportPath))
163-
{
164-
logger.LogDebug("Dev cert exported to '{CertPath}' and '{CertKeyPath}'", certExportPath, certKeyExportPath);
165-
return (true, certExportPath, certKeyExportPath);
166-
}
118+
return Task.FromResult(files.AsEnumerable<ContainerFileSystemItem>());
119+
});
167120

168-
if (exportProcess.HasExited && exportProcess.ExitCode != 0)
169-
{
170-
logger.LogError("HTTPS dev certificate export failed with exit code {ExitCode}", exportProcess.ExitCode);
171-
}
172-
else if (!exportProcess.HasExited)
121+
if (!string.IsNullOrEmpty(certFileEnv))
173122
{
174-
exportProcess.Kill(true);
175-
logger.LogError("HTTPS dev certificate export timed out after {TimeoutSeconds} seconds", timeout.TotalSeconds);
123+
builder.WithEnvironment(certFileEnv, certFileDest);
176124
}
177-
else
125+
if (!string.IsNullOrEmpty(certKeyFileEnv))
178126
{
179-
logger.LogError("HTTPS dev certificate export failed for an unknown reason");
127+
builder.WithEnvironment(certKeyFileEnv, certKeyFileDest);
180128
}
181-
return default;
182129
}
183-
finally
130+
else
184131
{
185-
await Task.WhenAll(stdOutTask ?? Task.CompletedTask, stdErrTask ?? Task.CompletedTask);
186-
}
187-
188-
static async Task ConsumeOutput(TextReader reader, Action<string> callback)
189-
{
190-
char[] buffer = new char[256];
191-
int charsRead;
192132

193-
while ((charsRead = await reader.ReadAsync(buffer, 0, buffer.Length)) > 0)
194-
{
195-
callback(new string(buffer, 0, charsRead));
196-
}
133+
// For non-container resources, set the file paths directly
134+
builder
135+
.WithEnvironment(certFileEnv, certExportPath)
136+
.WithEnvironment(certKeyFileEnv, certKeyExportPath);
197137
}
138+
139+
return builder;
198140
}
199141
}

0 commit comments

Comments
 (0)