diff --git a/src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs b/src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs index fc4b463bd24..8c1d873a11b 100644 --- a/src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs +++ b/src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs @@ -424,6 +424,51 @@ public static IResourceBuilder WithCreationScript(this return builder; } + /// + /// Configures the password that the PostgreSQL resource is used. + /// + /// The resource builder. + /// The parameter used to provide the password for the PostgreSQL resource. + /// The . + public static IResourceBuilder WithPassword(this IResourceBuilder builder, IResourceBuilder password) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(password); + + builder.Resource.PasswordParameter = password.Resource; + return builder; + } + + /// + /// Configures the user name that the PostgreSQL resource is used. + /// + /// The resource builder. + /// The parameter used to provide the user name for the PostgreSQL resource. + /// The . + public static IResourceBuilder WithUserName(this IResourceBuilder builder, IResourceBuilder userName) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(userName); + + builder.Resource.UserNameParameter = userName.Resource; + return builder; + } + + /// + /// Configures the host port that the PostgreSQL resource is exposed on instead of using randomly assigned port. + /// + /// The resource builder. + /// The port to bind on the host. If is used random port will be assigned. + /// The . + public static IResourceBuilder WithHostPort(this IResourceBuilder builder, int? port) + { + ArgumentNullException.ThrowIfNull(builder); + return builder.WithEndpoint(PostgresServerResource.PrimaryEndpointName, endpoint => + { + endpoint.Port = port; + }); + } + private static IEnumerable WritePgWebBookmarks(IEnumerable postgresInstances) { var bookmarkFiles = new List(); diff --git a/src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs b/src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs index 35b3a167956..a07f544f4e8 100644 --- a/src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs +++ b/src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs @@ -599,4 +599,35 @@ public static IResourceBuilder WithDataBindMount(this IRes return builder.WithBindMount(source, "/data"); } + + /// + /// Configures the password that the Redis resource is used. + /// + /// The resource builder. + /// The parameter used to provide the password for the Redis resource. + /// The . + public static IResourceBuilder WithPassword(this IResourceBuilder builder, IResourceBuilder password) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(password); + + builder.Resource.SetPassword(password.Resource); + return builder; + } + + /// + /// Configures the host port that the Redis resource is exposed on instead of using randomly assigned port. + /// + /// The resource builder. + /// The port to bind on the host. If is used random port will be assigned. + /// The . + public static IResourceBuilder WithHostPort(this IResourceBuilder builder, int port) + { + ArgumentNullException.ThrowIfNull(builder); + return builder.WithEndpoint(RedisResource.PrimaryEndpointName, endpoint => + { + endpoint.Port = port; + }); + + } } diff --git a/src/Aspire.Hosting.Redis/RedisResource.cs b/src/Aspire.Hosting.Redis/RedisResource.cs index fc9ea4b2843..2d3769de7ba 100644 --- a/src/Aspire.Hosting.Redis/RedisResource.cs +++ b/src/Aspire.Hosting.Redis/RedisResource.cs @@ -31,7 +31,7 @@ public RedisResource(string name, ParameterResource password) : this(name) /// /// Gets the parameter that contains the Redis server password. /// - public ParameterResource? PasswordParameter { get; } + public ParameterResource? PasswordParameter { get; private set; } private ReferenceExpression BuildConnectionString() { @@ -76,4 +76,11 @@ public ReferenceExpression ConnectionStringExpression return BuildConnectionString().GetValueAsync(cancellationToken); } + + internal void SetPassword(ParameterResource password) + { + ArgumentNullException.ThrowIfNull(password); + + PasswordParameter = password; + } } diff --git a/src/Aspire.Hosting.SqlServer/SqlServerBuilderExtensions.cs b/src/Aspire.Hosting.SqlServer/SqlServerBuilderExtensions.cs index 977ea4c7cd3..eeb28a02c88 100644 --- a/src/Aspire.Hosting.SqlServer/SqlServerBuilderExtensions.cs +++ b/src/Aspire.Hosting.SqlServer/SqlServerBuilderExtensions.cs @@ -200,6 +200,36 @@ public static IResourceBuilder WithCreationScript(thi return builder; } + /// + /// Configures the password that the SqlServer resource is used. + /// + /// The resource builder. + /// The parameter used to provide the password for the SqlServer resource. + /// The . + public static IResourceBuilder WithPassword(this IResourceBuilder builder, IResourceBuilder password) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(password); + + builder.Resource.SetPassword(password.Resource); + return builder; + } + + /// + /// Configures the host port that the SqlServer resource is exposed on instead of using randomly assigned port. + /// + /// The resource builder. + /// The port to bind on the host. If is used random port will be assigned. + /// The . + public static IResourceBuilder WithHostPort(this IResourceBuilder builder, int port) + { + ArgumentNullException.ThrowIfNull(builder); + return builder.WithEndpoint(SqlServerServerResource.PrimaryEndpointName, endpoint => + { + endpoint.Port = port; + }); + } + private static async Task CreateDatabaseAsync(SqlConnection sqlConnection, SqlServerDatabaseResource sqlDatabase, IServiceProvider serviceProvider, CancellationToken ct) { try diff --git a/src/Aspire.Hosting.SqlServer/SqlServerServerResource.cs b/src/Aspire.Hosting.SqlServer/SqlServerServerResource.cs index addfa0b027a..8cf381b2cbc 100644 --- a/src/Aspire.Hosting.SqlServer/SqlServerServerResource.cs +++ b/src/Aspire.Hosting.SqlServer/SqlServerServerResource.cs @@ -31,7 +31,7 @@ public SqlServerServerResource(string name, ParameterResource password) : base(n /// /// Gets the parameter that contains the SQL Server password. /// - public ParameterResource PasswordParameter { get; } + public ParameterResource PasswordParameter { get; private set; } private ReferenceExpression ConnectionString => ReferenceExpression.Create( @@ -76,6 +76,13 @@ public ReferenceExpression ConnectionStringExpression /// public IReadOnlyDictionary Databases => _databases; + internal void SetPassword(ParameterResource password) + { + ArgumentNullException.ThrowIfNull(password); + + PasswordParameter = password; + } + internal void AddDatabase(SqlServerDatabaseResource database) { _databases.TryAdd(database.Name, database.DatabaseName); diff --git a/tests/Aspire.Hosting.Azure.Tests/AzurePostgresExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzurePostgresExtensionsTests.cs index cc011572937..1263d8b6fa8 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzurePostgresExtensionsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzurePostgresExtensionsTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Net.Sockets; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Utils; using Microsoft.Extensions.DependencyInjection; @@ -341,6 +342,62 @@ public async Task AddAzurePostgresFlexibleServerRunAsContainerProducesCorrectCon Assert.EndsWith("Database=db2Name", db2ConnectionString); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task AddAzurePostgresFlexibleServerRunAsContainerProducesCorrectUserNameAndPasswordAndHost(bool addDbBeforeRunAsContainer) + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var postgres = builder.AddAzurePostgresFlexibleServer("postgres-data"); + var pass = builder.AddParameter("pass", "p@ssw0rd1"); + var user = builder.AddParameter("user", "user1"); + + IResourceBuilder db1 = null!; + IResourceBuilder db2 = null!; + if (addDbBeforeRunAsContainer) + { + db1 = postgres.AddDatabase("db1"); + db2 = postgres.AddDatabase("db2", "db2Name"); + } + + IResourceBuilder? innerPostgres = null; + postgres.RunAsContainer(configureContainer: c => + { + c.WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 12455)) + .WithHostPort(12455) + .WithPassword(pass) + .WithUserName(user); + innerPostgres = c; + }); + + if (!addDbBeforeRunAsContainer) + { + db1 = postgres.AddDatabase("db1"); + db2 = postgres.AddDatabase("db2", "db2Name"); + } + + Assert.NotNull(innerPostgres); + + var endpoint = Assert.Single(innerPostgres.Resource.Annotations.OfType()); + Assert.Equal(5432, endpoint.TargetPort); + Assert.False(endpoint.IsExternal); + Assert.Equal("tcp", endpoint.Name); + Assert.Equal(12455, endpoint.Port); + Assert.Equal(ProtocolType.Tcp, endpoint.Protocol); + Assert.Equal("tcp", endpoint.Transport); + Assert.Equal("tcp", endpoint.UriScheme); + + Assert.True(postgres.Resource.IsContainer(), "The resource should now be a container resource."); + Assert.Equal("Host=localhost;Port=12455;Username=user1;Password=p@ssw0rd1", await postgres.Resource.ConnectionStringExpression.GetValueAsync(CancellationToken.None)); + + var db1ConnectionString = await db1.Resource.ConnectionStringExpression.GetValueAsync(CancellationToken.None); + Assert.Equal("Host=localhost;Port=12455;Username=user1;Password=p@ssw0rd1;Database=db1", db1ConnectionString); + + var db2ConnectionString = await db2.Resource.ConnectionStringExpression.GetValueAsync(CancellationToken.None); + Assert.Equal("Host=localhost;Port=12455;Username=user1;Password=p@ssw0rd1;Database=db2Name", db2ConnectionString); + } + [Theory] [InlineData(true)] [InlineData(false)] diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureRedisExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureRedisExtensionsTests.cs index bc3d6481fda..283d72fe056 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureRedisExtensionsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureRedisExtensionsTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Net.Sockets; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Utils; using Microsoft.Extensions.DependencyInjection; @@ -210,6 +211,40 @@ public async Task AddAzureRedisRunAsContainerProducesCorrectConnectionString() Assert.Equal($"localhost:12455,password={redisResource.PasswordParameter.Value}", await redis.Resource.ConnectionStringExpression.GetValueAsync(CancellationToken.None)); } + [Fact] + public async Task AddAzureRedisRunAsContainerProducesCorrectHostAndPassword() + { + using var builder = TestDistributedApplicationBuilder.Create(); + var pass = builder.AddParameter("pass", "p@ssw0rd1"); + + RedisResource? redisResource = null; + var redis = builder.AddAzureRedis("cache") + .RunAsContainer(c => + { + redisResource = c.Resource; + + c.WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 12455)) + .WithHostPort(12455) + .WithPassword(pass); + }); + + Assert.NotNull(redisResource); + + var endpoint = Assert.Single(redisResource.Annotations.OfType()); + Assert.Equal(6379, endpoint.TargetPort); + Assert.False(endpoint.IsExternal); + Assert.Equal("tcp", endpoint.Name); + Assert.Equal(12455, endpoint.Port); + Assert.Equal(ProtocolType.Tcp, endpoint.Protocol); + Assert.Equal("tcp", endpoint.Transport); + Assert.Equal("tcp", endpoint.UriScheme); + + Assert.True(redis.Resource.IsContainer(), "The resource should now be a container resource."); + + Assert.NotNull(redisResource?.PasswordParameter); + Assert.Equal($"localhost:12455,password=p@ssw0rd1", await redis.Resource.ConnectionStringExpression.GetValueAsync(CancellationToken.None)); + } + [Theory] [InlineData(true)] [InlineData(false)] diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureSqlExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureSqlExtensionsTests.cs index 0b00ea47faf..fcaf88ede4b 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureSqlExtensionsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureSqlExtensionsTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Net.Sockets; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Utils; using Xunit; @@ -189,6 +190,60 @@ public async Task AddAzureSqlServerRunAsContainerProducesCorrectConnectionString Assert.EndsWith(";TrustServerCertificate=true;Database=db2Name", db2ConnectionString); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task AddAzureSqlServerRunAsContainerProducesCorrectPasswordAndPort(bool addDbBeforeRunAsContainer) + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var sql = builder.AddAzureSqlServer("sql"); + var pass = builder.AddParameter("pass", "p@ssw0rd1"); + IResourceBuilder db1 = null!; + IResourceBuilder db2 = null!; + if (addDbBeforeRunAsContainer) + { + db1 = sql.AddDatabase("db1"); + db2 = sql.AddDatabase("db2", "db2Name"); + } + + IResourceBuilder? innerSql = null; + sql.RunAsContainer(configureContainer: c => + { + c.WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 12455)) + .WithHostPort(12455) + .WithPassword(pass); + innerSql = c; + }); + + Assert.NotNull(innerSql); + + if (!addDbBeforeRunAsContainer) + { + db1 = sql.AddDatabase("db1"); + db2 = sql.AddDatabase("db2", "db2Name"); + } + + var endpoint = Assert.Single(innerSql.Resource.Annotations.OfType()); + Assert.Equal(1433, endpoint.TargetPort); + Assert.False(endpoint.IsExternal); + Assert.Equal("tcp", endpoint.Name); + Assert.Equal(12455, endpoint.Port); + Assert.Equal(ProtocolType.Tcp, endpoint.Protocol); + Assert.Equal("tcp", endpoint.Transport); + Assert.Equal("tcp", endpoint.UriScheme); + + Assert.True(sql.Resource.IsContainer(), "The resource should now be a container resource."); + var serverConnectionString = await sql.Resource.ConnectionStringExpression.GetValueAsync(CancellationToken.None); + Assert.Equal("Server=127.0.0.1,12455;User ID=sa;Password=p@ssw0rd1;TrustServerCertificate=true", serverConnectionString); + + var db1ConnectionString = await db1.Resource.ConnectionStringExpression.GetValueAsync(CancellationToken.None); + Assert.StartsWith("Server=127.0.0.1,12455;User ID=sa;Password=p@ssw0rd1;TrustServerCertificate=true;Database=db1", db1ConnectionString); + + var db2ConnectionString = await db2.Resource.ConnectionStringExpression.GetValueAsync(CancellationToken.None); + Assert.StartsWith("Server=127.0.0.1,12455;User ID=sa;Password=p@ssw0rd1;TrustServerCertificate=true;Database=db2Name", db2ConnectionString); + } + [Theory] [InlineData(true, true)] [InlineData(true, false)] diff --git a/tests/Aspire.Hosting.PostgreSQL.Tests/AddPostgresTests.cs b/tests/Aspire.Hosting.PostgreSQL.Tests/AddPostgresTests.cs index 116cc46a792..b728585dac0 100644 --- a/tests/Aspire.Hosting.PostgreSQL.Tests/AddPostgresTests.cs +++ b/tests/Aspire.Hosting.PostgreSQL.Tests/AddPostgresTests.cs @@ -660,4 +660,44 @@ private static string CreatePgWebBookmarkfileContent(PostgresDatabaseResource po return fileContent; } + + [Fact] + public void VerifyPostgresServerResourceWithHostPort() + { + var builder = DistributedApplication.CreateBuilder(); + builder.AddPostgres("postgres") + .WithHostPort(1000); + + var resource = Assert.Single(builder.Resources.OfType()); + var endpoint = Assert.Single(resource.Annotations.OfType()); + Assert.Equal(1000, endpoint.Port); + } + + [Fact] + public async Task VerifyPostgresServerResourceWithPassword() + { + var builder = DistributedApplication.CreateBuilder(); + var password = "p@ssw0rd1"; + var pass = builder.AddParameter("pass", password); + var postgres = builder.AddPostgres("postgres") + .WithPassword(pass) + .WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 2000)); + + var connectionString = await postgres.Resource.GetConnectionStringAsync(); + Assert.Equal("Host=localhost;Port=2000;Username=postgres;Password=p@ssw0rd1", connectionString); + } + + [Fact] + public async Task VerifyPostgresServerResourceWithUserName() + { + var builder = DistributedApplication.CreateBuilder(); + var user = "user1"; + var pass = builder.AddParameter("user", user); + var postgres = builder.AddPostgres("postgres") + .WithUserName(pass) + .WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 2000)); + + var connectionString = await postgres.Resource.GetConnectionStringAsync(); + Assert.Equal($"Host=localhost;Port=2000;Username=user1;Password={postgres.Resource.PasswordParameter.Value}", connectionString); + } } diff --git a/tests/Aspire.Hosting.Redis.Tests/AddRedisTests.cs b/tests/Aspire.Hosting.Redis.Tests/AddRedisTests.cs index 37f4a0a3d85..15fc463e91e 100644 --- a/tests/Aspire.Hosting.Redis.Tests/AddRedisTests.cs +++ b/tests/Aspire.Hosting.Redis.Tests/AddRedisTests.cs @@ -318,6 +318,40 @@ public void WithRedisInsightSupportsChangingHostPort() Assert.Equal(1000, endpoint.Port); } + [Fact] + public void VerifyRedisResourceWithHostPort() + { + var builder = DistributedApplication.CreateBuilder(); + builder.AddRedis("myredis") + .WithHostPort(1000); + + var resource = Assert.Single(builder.Resources.OfType()); + var endpoint = Assert.Single(resource.Annotations.OfType()); + Assert.Equal(1000, endpoint.Port); + } + + [Fact] + public async Task VerifyRedisResourceWithPassword() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var password = "p@ssw0rd1"; + var pass = builder.AddParameter("pass", password); + var redis = builder + .AddRedis("myRedis") + .WithPassword(pass) + .WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 5001)); + + using var app = builder.Build(); + var appModel = app.Services.GetRequiredService(); + var containerResource = Assert.Single(appModel.Resources.OfType()); + + var connectionStringResource = Assert.Single(appModel.Resources.OfType()); + var connectionString = await connectionStringResource.GetConnectionStringAsync(default); + Assert.Equal("{myRedis.bindings.tcp.host}:{myRedis.bindings.tcp.port},password={pass.value}", connectionStringResource.ConnectionStringExpression.ValueExpression); + Assert.StartsWith($"localhost:5001,password={password}", connectionString); + } + [Fact] public async Task SingleRedisInstanceWithoutPasswordProducesCorrectRedisHostsVariable() { @@ -373,7 +407,7 @@ public async Task MultipleRedisInstanceProducesCorrectRedisHostsVariable() redis1.WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 5001)); redis2.WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 5002, "host2")); - await builder.Eventing.PublishAsync(new (app.Services, app.Services.GetRequiredService())); + await builder.Eventing.PublishAsync(new(app.Services, app.Services.GetRequiredService())); var commander = builder.Resources.Single(r => r.Name.EndsWith("-commander")); diff --git a/tests/Aspire.Hosting.SqlServer.Tests/AddSqlServerTests.cs b/tests/Aspire.Hosting.SqlServer.Tests/AddSqlServerTests.cs index 2a130baa493..673cc2d2638 100644 --- a/tests/Aspire.Hosting.SqlServer.Tests/AddSqlServerTests.cs +++ b/tests/Aspire.Hosting.SqlServer.Tests/AddSqlServerTests.cs @@ -249,4 +249,36 @@ public void CanAddDatabasesWithTheSameNameOnMultipleServers() Assert.Equal("{sqlserver1.connectionString};Initial Catalog=imports", db1.Resource.ConnectionStringExpression.ValueExpression); Assert.Equal("{sqlserver2.connectionString};Initial Catalog=imports", db2.Resource.ConnectionStringExpression.ValueExpression); } + + [Fact] + public void VerifySqlServerServerResourceWithHostPort() + { + var builder = DistributedApplication.CreateBuilder(); + builder.AddSqlServer("sqlserver1") + .WithHostPort(1000); + + var resource = Assert.Single(builder.Resources.OfType()); + var endpoint = Assert.Single(resource.Annotations.OfType()); + Assert.Equal(1000, endpoint.Port); + } + + [Fact] + public async Task VerifySqlServerServerResourceWithPassword() + { + var appBuilder = DistributedApplication.CreateBuilder(); + + var pass = appBuilder.AddParameter("pass", "p@ssw0rd1"); + appBuilder + .AddSqlServer("sqlserver") + .WithPassword(pass) + .WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 1433)); + + using var app = appBuilder.Build(); + var appModel = app.Services.GetRequiredService(); + + var connectionStringResource = Assert.Single(appModel.Resources.OfType()); + var connectionString = await connectionStringResource.GetConnectionStringAsync(default); + Assert.Equal("Server=127.0.0.1,1433;User ID=sa;Password=p@ssw0rd1;TrustServerCertificate=true", connectionString); + Assert.Equal("Server={sqlserver.bindings.tcp.host},{sqlserver.bindings.tcp.port};User ID=sa;Password={pass.value};TrustServerCertificate=true", connectionStringResource.ConnectionStringExpression.ValueExpression); + } }