From d8472ee38c9cfe7e9f0a9f96e98785395e3cf1e4 Mon Sep 17 00:00:00 2001 From: Alireza Baloochi Date: Tue, 20 May 2025 22:12:54 +0330 Subject: [PATCH] Add DbGate support for MySql --- ...olkit.Aspire.Hosting.DbGate.AppHost.csproj | 1 + .../Program.cs | 10 + .../Program.cs | 3 +- .../DbGateContainerImageTags.cs | 6 +- ...kit.Aspire.Hosting.MySql.Extensions.csproj | 1 + .../MySqlBuilderExtensions.cs | 85 +++++++ .../README.md | 3 +- .../AddDbGateTests.cs | 78 ++++++- .../ResourceCreationTests.cs | 209 ++++++++++++++++++ 9 files changed, 390 insertions(+), 6 deletions(-) diff --git a/examples/dbgate/CommunityToolkit.Aspire.Hosting.DbGate.AppHost/CommunityToolkit.Aspire.Hosting.DbGate.AppHost.csproj b/examples/dbgate/CommunityToolkit.Aspire.Hosting.DbGate.AppHost/CommunityToolkit.Aspire.Hosting.DbGate.AppHost.csproj index a6b99132..e32acfbb 100644 --- a/examples/dbgate/CommunityToolkit.Aspire.Hosting.DbGate.AppHost/CommunityToolkit.Aspire.Hosting.DbGate.AppHost.csproj +++ b/examples/dbgate/CommunityToolkit.Aspire.Hosting.DbGate.AppHost/CommunityToolkit.Aspire.Hosting.DbGate.AppHost.csproj @@ -18,6 +18,7 @@ + diff --git a/examples/dbgate/CommunityToolkit.Aspire.Hosting.DbGate.AppHost/Program.cs b/examples/dbgate/CommunityToolkit.Aspire.Hosting.DbGate.AppHost/Program.cs index 5ce0b109..cb536582 100644 --- a/examples/dbgate/CommunityToolkit.Aspire.Hosting.DbGate.AppHost/Program.cs +++ b/examples/dbgate/CommunityToolkit.Aspire.Hosting.DbGate.AppHost/Program.cs @@ -32,4 +32,14 @@ sqlserver2.AddDatabase("db12"); +var mysql1 = builder.AddMySql("mysql1") + .WithDbGate(c => c.WithHostPort(8068)); +mysql1.AddDatabase("db13"); +mysql1.AddDatabase("db14"); + +var mysql2 = builder.AddMySql("mysql2") + .WithDbGate(); +mysql2.AddDatabase("db15"); +mysql2.AddDatabase("db16"); + builder.Build().Run(); \ No newline at end of file diff --git a/examples/mysql-ext/CommunityToolkit.Aspire.Hosting.MySql.Extensions.AppHost/Program.cs b/examples/mysql-ext/CommunityToolkit.Aspire.Hosting.MySql.Extensions.AppHost/Program.cs index adec63c4..34bbcb38 100644 --- a/examples/mysql-ext/CommunityToolkit.Aspire.Hosting.MySql.Extensions.AppHost/Program.cs +++ b/examples/mysql-ext/CommunityToolkit.Aspire.Hosting.MySql.Extensions.AppHost/Program.cs @@ -1,7 +1,8 @@ var builder = DistributedApplication.CreateBuilder(args); var mysql1 = builder.AddMySql("mysql1") - .WithAdminer(c => c.WithHostPort(8989)); + .WithAdminer(c => c.WithHostPort(8989)) + .WithDbGate(c => c.WithHostPort(9999)); mysql1.AddDatabase("db1"); mysql1.AddDatabase("db2"); diff --git a/src/CommunityToolkit.Aspire.Hosting.DbGate/DbGateContainerImageTags.cs b/src/CommunityToolkit.Aspire.Hosting.DbGate/DbGateContainerImageTags.cs index e8d1e960..ab338331 100644 --- a/src/CommunityToolkit.Aspire.Hosting.DbGate/DbGateContainerImageTags.cs +++ b/src/CommunityToolkit.Aspire.Hosting.DbGate/DbGateContainerImageTags.cs @@ -1,11 +1,11 @@ internal static class DbGateContainerImageTags { - /// docker.io + /// docker.io public const string Registry = "docker.io"; - /// dbgate/dbgate + /// dbgate/dbgate public const string Image = "dbgate/dbgate"; - /// 6.1.4 + /// 6.1.4 public const string Tag = "6.1.4"; } diff --git a/src/CommunityToolkit.Aspire.Hosting.MySql.Extensions/CommunityToolkit.Aspire.Hosting.MySql.Extensions.csproj b/src/CommunityToolkit.Aspire.Hosting.MySql.Extensions/CommunityToolkit.Aspire.Hosting.MySql.Extensions.csproj index 0fcf0071..8eebc9bc 100644 --- a/src/CommunityToolkit.Aspire.Hosting.MySql.Extensions/CommunityToolkit.Aspire.Hosting.MySql.Extensions.csproj +++ b/src/CommunityToolkit.Aspire.Hosting.MySql.Extensions/CommunityToolkit.Aspire.Hosting.MySql.Extensions.csproj @@ -12,6 +12,7 @@ + diff --git a/src/CommunityToolkit.Aspire.Hosting.MySql.Extensions/MySqlBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.MySql.Extensions/MySqlBuilderExtensions.cs index e8d3e7c9..f8c052cd 100644 --- a/src/CommunityToolkit.Aspire.Hosting.MySql.Extensions/MySqlBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.MySql.Extensions/MySqlBuilderExtensions.cs @@ -50,6 +50,91 @@ public static IResourceBuilder WithAdminer(this IResourceBu return builder; } + /// + /// Adds an administration and development platform for MySql to the application model using DbGate. + /// + /// + /// This version of the package defaults to the tag of the container image. + /// The MySql server resource builder. + /// Configuration callback for DbGate container resource. + /// The name of the container (Optional). + /// + /// Use in application host with a MySql resource + /// + /// var builder = DistributedApplication.CreateBuilder(args); + /// + /// var mysql = builder.AddMySql("mysql") + /// .WithDbGate(); + /// var db = mysql.AddDatabase("db"); + /// + /// var api = builder.AddProject<Projects.Api>("api") + /// .WithReference(db); + /// + /// builder.Build().Run(); + /// + /// + /// + /// A reference to the . + public static IResourceBuilder WithDbGate(this IResourceBuilder builder, Action>? configureContainer = null, string? containerName = null) + { + ArgumentNullException.ThrowIfNull(builder); + + containerName ??= $"{builder.Resource.Name}-dbgate"; + + var dbGateBuilder = DbGateBuilderExtensions.AddDbGate(builder.ApplicationBuilder, containerName); + + dbGateBuilder + .WithEnvironment(context => ConfigureDbGateContainer(context, builder.ApplicationBuilder)); + + configureContainer?.Invoke(dbGateBuilder); + + return builder; + } + + private static void ConfigureDbGateContainer(EnvironmentCallbackContext context, IDistributedApplicationBuilder applicationBuilder) + { + var mysqlInstances = applicationBuilder.Resources.OfType(); + + var counter = 1; + + // Multiple WithDbGate calls will be ignored + if (context.EnvironmentVariables.ContainsKey($"LABEL_mysql{counter}")) + { + return; + } + + foreach (var mySqlServerResource in mysqlInstances) + { + // DbGate assumes MySql is being accessed over a default Aspire container network and hardcodes the resource address + context.EnvironmentVariables.Add($"LABEL_mysql{counter}", mySqlServerResource.Name); + context.EnvironmentVariables.Add($"SERVER_mysql{counter}", mySqlServerResource.Name); + context.EnvironmentVariables.Add($"USER_mysql{counter}", "root"); + context.EnvironmentVariables.Add($"PASSWORD_mysql{counter}", mySqlServerResource.PasswordParameter.Value); + context.EnvironmentVariables.Add($"PORT_mysql{counter}", mySqlServerResource.PrimaryEndpoint.TargetPort!.ToString()!); + context.EnvironmentVariables.Add($"ENGINE_mysql{counter}", "mysql@dbgate-plugin-mysql"); + + counter++; + } + + var instancesCount = mysqlInstances.Count(); + if (instancesCount > 0) + { + var strBuilder = new StringBuilder(); + strBuilder.AppendJoin(',', Enumerable.Range(1, instancesCount).Select(i => $"mysql{i}")); + var connections = strBuilder.ToString(); + + string CONNECTIONS = context.EnvironmentVariables.GetValueOrDefault("CONNECTIONS")?.ToString() ?? string.Empty; + if (string.IsNullOrEmpty(CONNECTIONS)) + { + context.EnvironmentVariables["CONNECTIONS"] = connections; + } + else + { + context.EnvironmentVariables["CONNECTIONS"] += $",{connections}"; + } + } + } + internal static void ConfigureAdminerContainer(EnvironmentCallbackContext context, IDistributedApplicationBuilder applicationBuilder) { var mysqlInstances = applicationBuilder.Resources.OfType(); diff --git a/src/CommunityToolkit.Aspire.Hosting.MySql.Extensions/README.md b/src/CommunityToolkit.Aspire.Hosting.MySql.Extensions/README.md index af823e87..ca7386eb 100644 --- a/src/CommunityToolkit.Aspire.Hosting.MySql.Extensions/README.md +++ b/src/CommunityToolkit.Aspire.Hosting.MySql.Extensions/README.md @@ -2,7 +2,7 @@ This integration contains extensions for the [MySql hosting package](https://nuget.org/packages/Aspire.Hosting.MySql) for .NET Aspire. -The integration provides support for running [Adminer](https://github.com/vrana/adminer) to interact with the MySql database. +The integration provides support for running [Adminer](https://github.com/vrana/adminer) and [DbGate](https://github.com/dbgate/dbgate) to interact with the MySql database. ## Getting Started @@ -20,6 +20,7 @@ Then, in the _Program.cs_ file of `AppHost`, define an MySql resource, then call ```csharp var mysql = builder.AddMySql("mysql") + .WithDbGate() .WithAdminer(); ``` diff --git a/tests/CommunityToolkit.Aspire.Hosting.DbGate.Tests/AddDbGateTests.cs b/tests/CommunityToolkit.Aspire.Hosting.DbGate.Tests/AddDbGateTests.cs index 3059789f..ea6c7490 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.DbGate.Tests/AddDbGateTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.DbGate.Tests/AddDbGateTests.cs @@ -221,6 +221,16 @@ public async Task WithDbGateShouldAddAnnotationsForMultipleDatabaseTypes() var sqlserverResource2 = sqlserverResourceBuilder2.Resource; + var mysqlResourceBuilder1 = builder.AddMySql("mysql1") + .WithDbGate(); + + var mysqlResource1 = mysqlResourceBuilder1.Resource; + + var mysqlResourceBuilder2 =builder.AddMySql("mysql2") + .WithDbGate(); + + var mysqlResource2 = mysqlResourceBuilder2.Resource; + using var app = builder.Build(); var appModel = app.Services.GetRequiredService(); @@ -268,7 +278,7 @@ public async Task WithDbGateShouldAddAnnotationsForMultipleDatabaseTypes() item => { Assert.Equal("CONNECTIONS", item.Key); - Assert.Equal("mongodb1,mongodb2,postgres1,postgres2,redis1,redis2,sqlserver1,sqlserver2", item.Value); + Assert.Equal("mongodb1,mongodb2,postgres1,postgres2,redis1,redis2,sqlserver1,sqlserver2,mysql1,mysql2", item.Value); }, item => { @@ -423,6 +433,66 @@ public async Task WithDbGateShouldAddAnnotationsForMultipleDatabaseTypes() { Assert.Equal("ENGINE_sqlserver2", item.Key); Assert.Equal("mssql@dbgate-plugin-mssql", item.Value); + }, + item => + { + Assert.Equal("LABEL_mysql1", item.Key); + Assert.Equal(mysqlResource1.Name, item.Value); + }, + item => + { + Assert.Equal("SERVER_mysql1", item.Key); + Assert.Equal(mysqlResource1.Name, item.Value); + }, + item => + { + Assert.Equal("USER_mysql1", item.Key); + Assert.Equal("root", item.Value); + }, + item => + { + Assert.Equal("PASSWORD_mysql1", item.Key); + Assert.Equal(mysqlResource1.PasswordParameter.Value, item.Value); + }, + item => + { + Assert.Equal("PORT_mysql1", item.Key); + Assert.Equal(mysqlResource1.PrimaryEndpoint.TargetPort.ToString(), item.Value); + }, + item => + { + Assert.Equal("ENGINE_mysql1", item.Key); + Assert.Equal("mysql@dbgate-plugin-mysql", item.Value); + }, + item => + { + Assert.Equal("LABEL_mysql2", item.Key); + Assert.Equal(mysqlResource2.Name, item.Value); + }, + item => + { + Assert.Equal("SERVER_mysql2", item.Key); + Assert.Equal(mysqlResource2.Name, item.Value); + }, + item => + { + Assert.Equal("USER_mysql2", item.Key); + Assert.Equal("root", item.Value); + }, + item => + { + Assert.Equal("PASSWORD_mysql2", item.Key); + Assert.Equal(mysqlResource2.PasswordParameter.Value, item.Value); + }, + item => + { + Assert.Equal("PORT_mysql2", item.Key); + Assert.Equal(mysqlResource2.PrimaryEndpoint.TargetPort.ToString(), item.Value); + }, + item => + { + Assert.Equal("ENGINE_mysql2", item.Key); + Assert.Equal("mysql@dbgate-plugin-mysql", item.Value); }); } @@ -455,6 +525,12 @@ public void WithDbGateShouldShouldAddOneDbGateResourceForMultipleDatabaseTypes() builder.AddSqlServer("sqlserver2") .WithDbGate(); + builder.AddMySql("mysql1") + .WithDbGate(); + + builder.AddMySql("mysql2") + .WithDbGate(); + using var app = builder.Build(); var appModel = app.Services.GetRequiredService(); diff --git a/tests/CommunityToolkit.Aspire.Hosting.MySql.Extensions.Tests/ResourceCreationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.MySql.Extensions.Tests/ResourceCreationTests.cs index 997a6374..de7b08fd 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.MySql.Extensions.Tests/ResourceCreationTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.MySql.Extensions.Tests/ResourceCreationTests.cs @@ -159,4 +159,213 @@ public async Task WithAdminerAddsAnnotationsForMultipleMySqlResource() Assert.Equal("ADMINER_SERVERS", item.Key); Assert.Equal(envValue, item.Value); } + + [Fact] + public async Task WithDbGateAddsAnnotations() + { + var builder = DistributedApplication.CreateBuilder(); + + var mysqlResourceBuilder = builder.AddMySql("mysql") + .WithDbGate(); + + var mysqlResource = mysqlResourceBuilder.Resource; + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var dbGateResource = appModel.Resources.OfType().SingleOrDefault(); + + Assert.NotNull(dbGateResource); + + Assert.Equal("mysql-dbgate", dbGateResource.Name); + + var envs = await dbGateResource.GetEnvironmentVariableValuesAsync(); + + Assert.NotEmpty(envs); + Assert.Collection(envs, + item => + { + Assert.Equal("LABEL_mysql1", item.Key); + Assert.Equal(mysqlResource.Name, item.Value); + }, + item => + { + Assert.Equal("SERVER_mysql1", item.Key); + Assert.Equal(mysqlResource.Name, item.Value); + }, + item => + { + Assert.Equal("USER_mysql1", item.Key); + Assert.Equal("root", item.Value); + }, + item => + { + Assert.Equal("PASSWORD_mysql1", item.Key); + Assert.Equal(mysqlResource.PasswordParameter.Value, item.Value); + }, + item => + { + Assert.Equal("PORT_mysql1", item.Key); + Assert.Equal(mysqlResource.PrimaryEndpoint.TargetPort.ToString(), item.Value); + }, + item => + { + Assert.Equal("ENGINE_mysql1", item.Key); + Assert.Equal("mysql@dbgate-plugin-mysql", item.Value); + }, + item => + { + Assert.Equal("CONNECTIONS", item.Key); + Assert.Equal("mysql1", item.Value); + }); + } + + [Fact] + public void MultipleWithDbGateCallsAddsOneDbGateResource() + { + var builder = DistributedApplication.CreateBuilder(); + builder.AddMySql("mysql1").WithDbGate(); + builder.AddMySql("mysql2").WithDbGate(); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var dbGateResource = appModel.Resources.OfType().SingleOrDefault(); + Assert.NotNull(dbGateResource); + + Assert.Equal("mysql1-dbgate", dbGateResource.Name); + } + + [Fact] + public void WithDbGateShouldChangeDbGateHostPort() + { + var builder = DistributedApplication.CreateBuilder(); + var mysqlResourceBuilder = builder.AddMySql("mysql") + .WithDbGate(c => c.WithHostPort(8068)); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var dbGateResource = appModel.Resources.OfType().SingleOrDefault(); + Assert.NotNull(dbGateResource); + + var primaryEndpoint = dbGateResource.Annotations.OfType().Single(); + Assert.Equal(8068, primaryEndpoint.Port); + } + + [Fact] + public void WithDbGateShouldChangeDbGateContainerImageTag() + { + var builder = DistributedApplication.CreateBuilder(); + var mysqlResourceBuilder = builder.AddMySql("mysql") + .WithDbGate(c => c.WithImageTag("manualTag")); + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var dbGateResource = appModel.Resources.OfType().SingleOrDefault(); + Assert.NotNull(dbGateResource); + + var containerImageAnnotation = dbGateResource.Annotations.OfType().Single(); + Assert.Equal("manualTag", containerImageAnnotation.Tag); + } + + [Fact] + public async Task WithDbGateAddsAnnotationsForMultipleMySqlResource() + { + var builder = DistributedApplication.CreateBuilder(); + + var mysqlResourceBuilder1 = builder.AddMySql("mysql1") + .WithDbGate(); + + var mysqlResource1 = mysqlResourceBuilder1.Resource; + + var mysqlResourceBuilder2 = builder.AddMySql("mysql2") + .WithDbGate(); + + var mysqlResource2 = mysqlResourceBuilder2.Resource; + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var dbGateResource = appModel.Resources.OfType().SingleOrDefault(); + + Assert.NotNull(dbGateResource); + + Assert.Equal("mysql1-dbgate", dbGateResource.Name); + + var envs = await dbGateResource.GetEnvironmentVariableValuesAsync(); + + Assert.NotEmpty(envs); + Assert.Collection(envs, + item => + { + Assert.Equal("LABEL_mysql1", item.Key); + Assert.Equal(mysqlResource1.Name, item.Value); + }, + item => + { + Assert.Equal("SERVER_mysql1", item.Key); + Assert.Equal(mysqlResource1.Name, item.Value); + }, + item => + { + Assert.Equal("USER_mysql1", item.Key); + Assert.Equal("root", item.Value); + }, + item => + { + Assert.Equal("PASSWORD_mysql1", item.Key); + Assert.Equal(mysqlResource1.PasswordParameter.Value, item.Value); + }, + item => + { + Assert.Equal("PORT_mysql1", item.Key); + Assert.Equal(mysqlResource1.PrimaryEndpoint.TargetPort.ToString(), item.Value); + }, + item => + { + Assert.Equal("ENGINE_mysql1", item.Key); + Assert.Equal("mysql@dbgate-plugin-mysql", item.Value); + }, + item => + { + Assert.Equal("LABEL_mysql2", item.Key); + Assert.Equal(mysqlResource2.Name, item.Value); + }, + item => + { + Assert.Equal("SERVER_mysql2", item.Key); + Assert.Equal(mysqlResource2.Name, item.Value); + }, + item => + { + Assert.Equal("USER_mysql2", item.Key); + Assert.Equal("root", item.Value); + }, + item => + { + Assert.Equal("PASSWORD_mysql2", item.Key); + Assert.Equal(mysqlResource2.PasswordParameter.Value, item.Value); + }, + item => + { + Assert.Equal("PORT_mysql2", item.Key); + Assert.Equal(mysqlResource2.PrimaryEndpoint.TargetPort.ToString(), item.Value); + }, + item => + { + Assert.Equal("ENGINE_mysql2", item.Key); + Assert.Equal("mysql@dbgate-plugin-mysql", item.Value); + }, + item => + { + Assert.Equal("CONNECTIONS", item.Key); + Assert.Equal("mysql1,mysql2", item.Value); + }); + } }