diff --git a/src/CommunityToolkit.Aspire.Hosting.Redis.Extensions/RedisBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Redis.Extensions/RedisBuilderExtensions.cs index 1e8ee08c0..3ca2800fb 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Redis.Extensions/RedisBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Redis.Extensions/RedisBuilderExtensions.cs @@ -1,5 +1,4 @@ using Aspire.Hosting.ApplicationModel; -using System.Text; #pragma warning disable ASPIREATS001 // AspireExport is experimental @@ -45,6 +44,28 @@ public static IResourceBuilder WithDbGate(this IResourceBuilder ConfigureDbGateContainer(context, builder)) + .WithCertificateTrustConfiguration(context => + { + if (context.Scope == CertificateTrustScope.Append) + { + context.EnvironmentVariables["NODE_EXTRA_CA_CERTS"] = context.CertificateBundlePath; + } + else if (context.EnvironmentVariables.TryGetValue("NODE_OPTIONS", out var existingOptions)) + { + context.EnvironmentVariables["NODE_OPTIONS"] = existingOptions switch + { + string options when !string.IsNullOrEmpty(options) => $"{options} --use-openssl-ca", + ReferenceExpression expression => ReferenceExpression.Create($"{expression} --use-openssl-ca"), + _ => "--use-openssl-ca", + }; + } + else + { + context.EnvironmentVariables["NODE_OPTIONS"] = "--use-openssl-ca"; + } + + return Task.CompletedTask; + }) .WaitFor(builder); configureContainer?.Invoke(dbGateBuilder); @@ -120,13 +141,8 @@ private static void ConfigureDbGateContainer(EnvironmentCallbackContext context, var connectionId = DbGateBuilderExtensions.SanitizeConnectionId(name); var label = $"LABEL_{connectionId}"; - // DbGate assumes Redis is being accessed over a default Aspire container network and hardcodes the resource address - var redisUrl = redisResource.PasswordParameter is not null ? - ReferenceExpression.Create($"rediss://:{redisResource.PasswordParameter}@{name}:{redisResource.PrimaryEndpoint.TargetPort?.ToString()}") : - ReferenceExpression.Create($"rediss://{name}:{redisResource.PrimaryEndpoint.TargetPort?.ToString()}"); - context.EnvironmentVariables.Add(label, name); - context.EnvironmentVariables.Add($"URL_{connectionId}", redisUrl); + context.EnvironmentVariables.Add($"URL_{connectionId}", redisResource.UriExpression); context.EnvironmentVariables.Add($"ENGINE_{connectionId}", "redis@dbgate-plugin-redis"); if (context.EnvironmentVariables.GetValueOrDefault("CONNECTIONS") is string { Length: > 0 } connections) diff --git a/tests/CommunityToolkit.Aspire.Hosting.Redis.Extensions.Tests/AppHostTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Redis.Extensions.Tests/AppHostTests.cs index 9d0e2c2eb..03b477f1f 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Redis.Extensions.Tests/AppHostTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Redis.Extensions.Tests/AppHostTests.cs @@ -1,3 +1,6 @@ +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text.Json; using CommunityToolkit.Aspire.Testing; using Aspire.Components.Common.Tests; @@ -17,4 +20,62 @@ public async Task ResourceStartsAndRespondsOk() Assert.Equal(HttpStatusCode.OK, response.StatusCode); } -} \ No newline at end of file + + [Fact] + public async Task DbGateCanConnectToRedis() + { + using var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken); + cts.CancelAfter(TimeSpan.FromMinutes(5)); + + await fixture.ResourceNotificationService.WaitForResourceHealthyAsync("redis1", cts.Token); + await fixture.ResourceNotificationService.WaitForResourceHealthyAsync("dbgate", cts.Token); + + var httpClient = fixture.CreateHttpClient("dbgate"); + + using var loginResponse = await httpClient.PostAsJsonAsync( + "/auth/login", + new { amoid = "none" }, + cancellationToken: cts.Token); + + Assert.Equal(HttpStatusCode.OK, loginResponse.StatusCode); + + var login = await loginResponse.Content.ReadFromJsonAsync( + cts.Token); + + var accessToken = login.GetProperty("accessToken").GetString(); + + Assert.False(string.IsNullOrWhiteSpace(accessToken)); + + httpClient.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("Bearer", accessToken); + + using var refreshResponse = await httpClient.PostAsJsonAsync( + "/server-connections/refresh", + new + { + conid = "redis1", + keepOpen = true + }, + cancellationToken: cts.Token); + + Assert.Equal(HttpStatusCode.OK, refreshResponse.StatusCode); + + using var pingResponse = await httpClient.PostAsJsonAsync( + "/database-connections/call-method", + new + { + conid = "redis1", + database = "db0", + method = "ping", + args = Array.Empty() + }, + cancellationToken: cts.Token); + + Assert.Equal(HttpStatusCode.OK, pingResponse.StatusCode); + + var pong = await pingResponse.Content.ReadFromJsonAsync( + cts.Token); + + Assert.Equal("PONG", pong); + } +} diff --git a/tests/CommunityToolkit.Aspire.Hosting.Redis.Extensions.Tests/ResourceCreationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Redis.Extensions.Tests/ResourceCreationTests.cs index ed6a56217..c88e11c55 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Redis.Extensions.Tests/ResourceCreationTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Redis.Extensions.Tests/ResourceCreationTests.cs @@ -26,7 +26,7 @@ public async Task WithDbGateAddsAnnotations() Assert.Equal("dbgate", dbGateResource.Name); - var envs = await dbGateResource.GetEnvironmentVariablesAsync(); + var envs = await GetEnvironmentVariablesAsync(builder, dbGateResource); Assert.NotEmpty(envs); @@ -41,18 +41,43 @@ public async Task WithDbGateAddsAnnotations() Assert.Equal("LABEL_redis", item.Key); Assert.Equal(redisResource.Name, item.Value); }, - async item => + item => { - var redisUrl = redisResource.PasswordParameter is not null ? - $"rediss://:{await redisResource.PasswordParameter.GetValueAsync(default)}@{redisResource.Name}:{redisResource.PrimaryEndpoint.TargetPort}" : $"rediss://{redisResource.Name}:{redisResource.PrimaryEndpoint.TargetPort}"; Assert.Equal("URL_redis", item.Key); - Assert.Equal(redisUrl, item.Value); + var redisUrl = Assert.IsType(item.Value); + Assert.Equal(redisResource.UriExpression.ValueExpression, redisUrl.ValueExpression); }, item => { Assert.Equal("ENGINE_redis", item.Key); Assert.Equal("redis@dbgate-plugin-redis", item.Value); }); + + Assert.Single(dbGateResource.Annotations.OfType()); + } + + [Fact] + public async Task WithDbGateUsesRedisUriExpressionWhenTlsIsDisabled() + { + var builder = DistributedApplication.CreateBuilder(); + + var redisResource = builder.AddRedis("redis") + .WithEndpoint("tcp", endpoint => + { + endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", 27017); + endpoint.TlsEnabled = false; + }) + .WithDbGate() + .Resource; + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + var dbGateResource = Assert.Single(appModel.Resources.OfType()); + var envs = await GetEnvironmentVariablesAsync(builder, dbGateResource); + var redisUrl = Assert.IsType(envs["URL_redis"]); + + Assert.Equal(redisResource.UriExpression.ValueExpression, redisUrl.ValueExpression); } [Fact] @@ -134,7 +159,7 @@ public async Task WithDbGateAddsAnnotationsForMultipleRedisResource() Assert.Equal("dbgate", dbGateResource.Name); - var envs = await dbGateResource.GetEnvironmentVariablesAsync(); + var envs = await GetEnvironmentVariablesAsync(builder, dbGateResource); Assert.NotEmpty(envs); @@ -149,13 +174,11 @@ public async Task WithDbGateAddsAnnotationsForMultipleRedisResource() Assert.Equal("LABEL_redis1", item.Key); Assert.Equal(redisResource1.Name, item.Value); }, - async item => + item => { - var redisUrl = redisResource1.PasswordParameter is not null ? - $"rediss://:{await redisResource1.PasswordParameter.GetValueAsync(default)}@{redisResource1.Name}:{redisResource1.PrimaryEndpoint.TargetPort}" : $"rediss://{redisResource1.Name}:{redisResource1.PrimaryEndpoint.TargetPort}"; - Assert.Equal("URL_redis1", item.Key); - Assert.Equal(redisUrl, item.Value); + var redisUrl = Assert.IsType(item.Value); + Assert.Equal(redisResource1.UriExpression.ValueExpression, redisUrl.ValueExpression); }, item => { @@ -167,13 +190,11 @@ public async Task WithDbGateAddsAnnotationsForMultipleRedisResource() Assert.Equal("LABEL_redis2", item.Key); Assert.Equal(redisResource2.Name, item.Value); }, - async item => + item => { - var redisUrl = redisResource2.PasswordParameter is not null ? - $"rediss://:{await redisResource2.PasswordParameter.GetValueAsync(default)}@{redisResource2.Name}:{redisResource2.PrimaryEndpoint.TargetPort}" : $"rediss://{redisResource2.Name}:{redisResource2.PrimaryEndpoint.TargetPort}"; - Assert.Equal("URL_redis2", item.Key); - Assert.Equal(redisUrl, item.Value); + var redisUrl = Assert.IsType(item.Value); + Assert.Equal(redisResource2.UriExpression.ValueExpression, redisUrl.ValueExpression); }, item => { @@ -181,4 +202,21 @@ public async Task WithDbGateAddsAnnotationsForMultipleRedisResource() Assert.Equal("redis@dbgate-plugin-redis", item.Value); }); } + + private static async Task> GetEnvironmentVariablesAsync( + IDistributedApplicationBuilder builder, + IResource resource) + { + Assert.True(resource.TryGetAnnotationsOfType(out var annotations)); + + var environmentVariables = new Dictionary(); + var context = new EnvironmentCallbackContext(builder.ExecutionContext, environmentVariables); + + foreach (var annotation in annotations) + { + await annotation.Callback(context); + } + + return environmentVariables; + } }