Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using Aspire.Hosting.ApplicationModel;
using System.Text;

#pragma warning disable ASPIREATS001 // AspireExport is experimental

Expand Down Expand Up @@ -45,6 +44,28 @@ public static IResourceBuilder<RedisResource> WithDbGate(this IResourceBuilder<R

dbGateBuilder
.WithEnvironment(context => 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);
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -17,4 +20,62 @@ public async Task ResourceStartsAndRespondsOk()

Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
}

[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<JsonElement>(
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<object>()
},
cancellationToken: cts.Token);

Assert.Equal(HttpStatusCode.OK, pingResponse.StatusCode);

var pong = await pingResponse.Content.ReadFromJsonAsync<string>(
cts.Token);

Assert.Equal("PONG", pong);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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<ReferenceExpression>(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<CertificateTrustConfigurationCallbackAnnotation>());
}

[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<DistributedApplicationModel>();
var dbGateResource = Assert.Single(appModel.Resources.OfType<DbGateContainerResource>());
var envs = await GetEnvironmentVariablesAsync(builder, dbGateResource);
var redisUrl = Assert.IsType<ReferenceExpression>(envs["URL_redis"]);

Assert.Equal(redisResource.UriExpression.ValueExpression, redisUrl.ValueExpression);
}

[Fact]
Expand Down Expand Up @@ -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);

Expand All @@ -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<ReferenceExpression>(item.Value);
Assert.Equal(redisResource1.UriExpression.ValueExpression, redisUrl.ValueExpression);
},
item =>
{
Expand All @@ -167,18 +190,33 @@ 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<ReferenceExpression>(item.Value);
Assert.Equal(redisResource2.UriExpression.ValueExpression, redisUrl.ValueExpression);
},
item =>
{
Assert.Equal("ENGINE_redis2", item.Key);
Assert.Equal("redis@dbgate-plugin-redis", item.Value);
});
}

private static async Task<Dictionary<string, object>> GetEnvironmentVariablesAsync(
IDistributedApplicationBuilder builder,
IResource resource)
{
Assert.True(resource.TryGetAnnotationsOfType<EnvironmentCallbackAnnotation>(out var annotations));

var environmentVariables = new Dictionary<string, object>();
var context = new EnvironmentCallbackContext(builder.ExecutionContext, environmentVariables);

foreach (var annotation in annotations)
{
await annotation.Callback(context);
}

return environmentVariables;
}
Comment on lines +206 to +221

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the need for this over the original approach to getting environment variables?

@0mar-rivero 0mar-rivero Aug 28, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original approach was deadlocking when resolving the env variables that referenced the endpoint. Probably because the endpoint was not allocated yet.

May be related to microsoft/aspire#14954

}