diff --git a/playground/SqlServerEndToEnd/SqlServerEndToEnd.AppHost/Program.cs b/playground/SqlServerEndToEnd/SqlServerEndToEnd.AppHost/AppHost.cs similarity index 59% rename from playground/SqlServerEndToEnd/SqlServerEndToEnd.AppHost/Program.cs rename to playground/SqlServerEndToEnd/SqlServerEndToEnd.AppHost/AppHost.cs index c1fb9fb9f72..2e1b1ade11a 100644 --- a/playground/SqlServerEndToEnd/SqlServerEndToEnd.AppHost/Program.cs +++ b/playground/SqlServerEndToEnd/SqlServerEndToEnd.AppHost/AppHost.cs @@ -1,24 +1,29 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#pragma warning disable ASPIRECERTIFICATES001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + var builder = DistributedApplication.CreateBuilder(args); -var sql1 = builder.AddAzureSqlServer("sql1") +var sqlLocal = builder.AddAzureSqlServer("sqlLocal") .RunAsContainer(); -var db1 = sql1.AddDatabase("db1"); +var dbLocal = sqlLocal.AddDatabase("dbLocal"); + +var sqlAzure = builder.AddAzureSqlServer("sqlAzure"); +var dbAzure = sqlAzure.AddDatabase("dbAzure"); -var sql2 = builder.AddAzureSqlServer("sql2"); -var db2 = sql2.AddDatabase("db2"); +var sqlNoTls = builder.AddSqlServer("sqlNoTls") + .WithoutHttpsCertificate(); var dbsetup = builder.AddProject("dbsetup") - .WithReference(db1).WaitFor(sql1) - .WithReference(db2).WaitFor(sql2); + .WithReference(dbLocal).WaitFor(sqlLocal) + .WithReference(dbAzure).WaitFor(sqlAzure); builder.AddProject("api") .WithExternalHttpEndpoints() - .WithReference(db1).WaitFor(db1) - .WithReference(db2).WaitFor(db2) + .WithReference(dbLocal).WaitFor(dbLocal) + .WithReference(dbAzure).WaitFor(dbAzure) .WaitForCompletion(dbsetup); #if !SKIP_DASHBOARD_REFERENCE diff --git a/src/Aspire.Hosting.SqlServer/Aspire.Hosting.SqlServer.csproj b/src/Aspire.Hosting.SqlServer/Aspire.Hosting.SqlServer.csproj index 21a02116c81..75bc8616ec0 100644 --- a/src/Aspire.Hosting.SqlServer/Aspire.Hosting.SqlServer.csproj +++ b/src/Aspire.Hosting.SqlServer/Aspire.Hosting.SqlServer.csproj @@ -11,6 +11,7 @@ + diff --git a/src/Aspire.Hosting.SqlServer/SqlServerBuilderExtensions.cs b/src/Aspire.Hosting.SqlServer/SqlServerBuilderExtensions.cs index a90962ab290..deca86ca782 100644 --- a/src/Aspire.Hosting.SqlServer/SqlServerBuilderExtensions.cs +++ b/src/Aspire.Hosting.SqlServer/SqlServerBuilderExtensions.cs @@ -9,9 +9,12 @@ using Microsoft.Data.SqlClient; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Aspire.Hosting.Utils; namespace Aspire.Hosting; +#pragma warning disable ASPIRECERTIFICATES001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + /// /// Provides extension methods for adding SQL Server resources to the application model. /// @@ -49,7 +52,7 @@ public static IResourceBuilder AddSqlServer(this IDistr var healthCheckKey = $"{name}_check"; builder.Services.AddHealthChecks().AddSqlServer(sp => connectionString ?? throw new InvalidOperationException("Connection string is unavailable"), name: healthCheckKey); - return builder.AddResource(sqlServer) + var sqlBuilder = builder.AddResource(sqlServer) .WithEndpoint(port: port, targetPort: 1433, name: SqlServerServerResource.PrimaryEndpointName) .WithImage(SqlServerContainerImageTags.Image, SqlServerContainerImageTags.Tag) .WithImageRegistry(SqlServerContainerImageTags.Registry) @@ -89,6 +92,55 @@ public static IResourceBuilder AddSqlServer(this IDistr await CreateDatabaseAsync(sqlConnection, sqlDatabase, @event.Services, ct).ConfigureAwait(false); } }); + + return sqlBuilder + .SubscribeHttpsEndpointsUpdate(ctx => + { + var executionContext = ctx.Services.GetRequiredService(); + + // Dev cert versions prior to 6 don't include "127.0.0.1" in the SAN, and so won't be trusted + // So only enable TLS if we have a custom cert, or a dev cert with version 6 or higher. + if (executionContext.IsRunMode) + { + ctx.Resource.TryGetLastAnnotation(out var certAnnotation); + + if (certAnnotation == null || certAnnotation.UseDeveloperCertificate == true) + { + var devCertService = ctx.Services.GetRequiredService(); + var cert = devCertService.Certificates.First(); + if (cert.GetCertificateVersion() < 6) + { + return; + } + } + } + + sqlBuilder.WithEndpoint(SqlServerServerResource.PrimaryEndpointName, x => x.TlsEnabled = true); + }) + .WithContainerFiles("/var/opt/mssql/", async (ctx, ct) => + { + var certContext = ctx.HttpsCertificateContext; + + if (certContext is null) + { + return []; + } + + var config = $""" + [network] + tlscert = {await certContext.CertificatePath.GetValueAsync(ct).ConfigureAwait(false)} + tlskey = {await certContext.KeyPath.GetValueAsync(ct).ConfigureAwait(false)} + forceencryption = 1 + """; + + return [ + new ContainerFile + { + Name = "mssql.conf", + Contents = config + } + ]; + }); } /// diff --git a/src/Aspire.Hosting.SqlServer/SqlServerServerResource.cs b/src/Aspire.Hosting.SqlServer/SqlServerServerResource.cs index a597bcf4f6a..a638f3876cb 100644 --- a/src/Aspire.Hosting.SqlServer/SqlServerServerResource.cs +++ b/src/Aspire.Hosting.SqlServer/SqlServerServerResource.cs @@ -45,9 +45,19 @@ public SqlServerServerResource(string name, ParameterResource password) : base(n /// public ParameterResource PasswordParameter { get; private set; } - private ReferenceExpression ConnectionString => - ReferenceExpression.Create( - $"Server={PrimaryEndpoint.Property(EndpointProperty.IPV4Host)},{PrimaryEndpoint.Property(EndpointProperty.Port)};User ID={DefaultUserName};Password={PasswordParameter};TrustServerCertificate=true"); + private ReferenceExpression BuildConnectionString() + { + var builder = new ReferenceExpressionBuilder(); + + builder.Append($"Server={PrimaryEndpoint.Property(EndpointProperty.IPV4Host)},{PrimaryEndpoint.Property(EndpointProperty.Port)};"); + builder.Append($";User ID={UserNameReference}"); + builder.Append($";Password={PasswordParameter}"); + builder.Append($"{PrimaryEndpoint.GetTlsValue( + enabledValue: ReferenceExpression.Empty, + disabledValue: ReferenceExpression.Create($";TrustServerCertificate=true"))}"); + + return builder.Build(); + } /// /// Gets a reference to the user name for the SQL Server. @@ -69,19 +79,17 @@ public SqlServerServerResource(string name, ParameterResource password) : base(n internal ReferenceExpression BuildJdbcConnectionString(string? databaseName = null) { var builder = new ReferenceExpressionBuilder(); - builder.AppendLiteral("jdbc:sqlserver://"); - builder.Append($"{Host}"); - builder.AppendLiteral(":"); - builder.Append($"{Port}"); + + builder.Append($"jdbc:sqlserver://{Host}:{Port}"); if (!string.IsNullOrEmpty(databaseName)) { - var databaseNameReference = ReferenceExpression.Create($"{databaseName:uri}"); - builder.AppendLiteral(";databaseName="); - builder.Append($"{databaseNameReference}"); + builder.Append($";databaseName={databaseName:uri}"); } - builder.AppendLiteral(";trustServerCertificate=true"); + builder.Append($"{PrimaryEndpoint.GetTlsValue( + enabledValue: ReferenceExpression.Empty, + disabledValue: ReferenceExpression.Create($";trustServerCertificate=true"))}"); return builder.Build(); } @@ -90,9 +98,9 @@ internal ReferenceExpression BuildJdbcConnectionString(string? databaseName = nu /// Gets the JDBC connection string for the SQL Server. /// /// - /// Format: jdbc:sqlserver://{host}:{port};trustServerCertificate=true. - /// User and password credentials are not included in the JDBC connection string. Use the Username and Password connection properties to access credentials. - /// + /// Format: jdbc:sqlserver://{Host}:{Port}[;databaseName={Database}][;trustServerCertificate=true]. + /// User and password credentials are not included in the JDBC connection string. + /// Use the Username and Password connection properties to access credentials. /// public ReferenceExpression JdbcConnectionString => BuildJdbcConnectionString(); /// @@ -107,7 +115,7 @@ public ReferenceExpression ConnectionStringExpression return connectionStringAnnotation.Resource.ConnectionStringExpression; } - return ConnectionString; + return BuildConnectionString(); } } @@ -115,15 +123,10 @@ public ReferenceExpression ConnectionStringExpression /// Gets the connection string for the SQL Server. /// /// A to observe while waiting for the task to complete. - /// A connection string for the SQL Server in the form "Server=host,port;User ID=sa;Password=password;TrustServerCertificate=true". + /// A connection string for the SQL Server in the form "Server=host,port;User ID=sa;Password=password", with "TrustServerCertificate=true" appended when a certificate is not configured. public ValueTask GetConnectionStringAsync(CancellationToken cancellationToken = default) { - if (this.TryGetLastAnnotation(out var connectionStringAnnotation)) - { - return connectionStringAnnotation.Resource.GetConnectionStringAsync(cancellationToken); - } - - return ConnectionString.GetValueAsync(cancellationToken); + return ConnectionStringExpression.GetValueAsync(cancellationToken); } private readonly Dictionary _databases = new(StringComparers.ResourceName);