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 ;
7
2
8
- namespace Aspire ;
3
+ namespace Aspire . Hosting ;
9
4
10
- internal static class DevCertHostingExtensions
5
+ /// <summary>
6
+ /// Extensions for adding Dev Certs to aspire resources.
7
+ /// </summary>
8
+ public static class DevCertHostingExtensions
11
9
{
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
+
12
25
/// <summary>
13
26
/// Injects the ASP.NET Core HTTPS developer certificate into the resource via the specified environment variables when
14
27
/// <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 .
16
29
/// </summary>
17
30
/// <remarks>
18
31
/// This method <strong>does not</strong> configure an HTTPS endpoint on the resource.
19
32
/// Use <see cref="ResourceBuilderExtensions.WithHttpsEndpoint{TResource}"/> to configure an HTTPS endpoint.
20
33
/// </remarks>
21
34
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
24
37
{
25
- if ( builder . ApplicationBuilder . ExecutionContext . IsRunMode && builder . ApplicationBuilder . Environment . IsDevelopment ( ) )
38
+ if ( ! builder . ApplicationBuilder . ExecutionContext . IsRunMode )
26
39
{
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 ;
78
41
}
79
42
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 ) ) )
90
45
{
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 ." ) ;
92
47
}
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" ) ;
97
53
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";
104
56
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 ;
110
60
111
- if ( File . Exists ( certKeyExportPath ) )
61
+ if ( existingResource == null )
112
62
{
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
+ } ) ;
115
77
}
116
-
117
- if ( ! Directory . Exists ( tempDir ) )
78
+ else
118
79
{
119
- logger . LogTrace ( "Creating directory to export dev cert to '{ExportDir}'" , tempDir ) ;
120
- Directory . CreateDirectory ( tempDir ) ;
80
+ exportExecutable = builder . ApplicationBuilder . CreateResourceBuilder ( ( ExecutableResource ) existingResource ) ;
121
81
}
122
82
123
- string [ ] args = [ "dev-certs" , "https" , "--export-path" , $ "\" { certExportPath } \" ", "--format" , "Pem" , "--no-password" ] ;
124
- var argsString = string . Join ( ' ' , args ) ;
83
+ builder . WaitForCompletion ( exportExecutable ) ;
125
84
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 )
128
87
{
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
137
89
138
- var exportProcess = new Process { StartInfo = exportStartInfo } ;
139
90
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 } " ;
142
93
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 ) =>
148
96
{
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
+ }
158
108
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
+ }
161
117
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
+ } ) ;
167
120
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 ) )
173
122
{
174
- exportProcess . Kill ( true ) ;
175
- logger . LogError ( "HTTPS dev certificate export timed out after {TimeoutSeconds} seconds" , timeout . TotalSeconds ) ;
123
+ builder . WithEnvironment ( certFileEnv , certFileDest ) ;
176
124
}
177
- else
125
+ if ( ! string . IsNullOrEmpty ( certKeyFileEnv ) )
178
126
{
179
- logger . LogError ( "HTTPS dev certificate export failed for an unknown reason" ) ;
127
+ builder . WithEnvironment ( certKeyFileEnv , certKeyFileDest ) ;
180
128
}
181
- return default ;
182
129
}
183
- finally
130
+ else
184
131
{
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 ;
192
132
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 ) ;
197
137
}
138
+
139
+ return builder ;
198
140
}
199
141
}
0 commit comments