diff --git a/Aspire.sln b/Aspire.sln
index e6b01fb2dad..50821834b47 100644
--- a/Aspire.sln
+++ b/Aspire.sln
@@ -225,6 +225,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ParameterEndToEnd.AppHost",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ParameterEndToEnd.ApiService", "playground\ParameterEndToEnd\ParameterEndToEnd.ApiService\ParameterEndToEnd.ApiService.csproj", "{FD63D574-8512-421D-B7FC-310AFA974361}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "mysql", "mysql", "{621991F1-854A-4743-835B-10CAF11A0CFF}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MySqlDb.AppHost", "Playground\mysql\MySqlDb.AppHost\MySqlDb.AppHost.csproj", "{7E2AD00B-60E0-46C2-8640-7217D678F312}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MySql.ApiService", "playground\mysql\MySql.ApiService\MySql.ApiService.csproj", "{F699F3AD-2AD9-454B-BA40-82AC3D6250FE}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -599,6 +605,14 @@ Global
{FD63D574-8512-421D-B7FC-310AFA974361}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FD63D574-8512-421D-B7FC-310AFA974361}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FD63D574-8512-421D-B7FC-310AFA974361}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7E2AD00B-60E0-46C2-8640-7217D678F312}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7E2AD00B-60E0-46C2-8640-7217D678F312}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7E2AD00B-60E0-46C2-8640-7217D678F312}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7E2AD00B-60E0-46C2-8640-7217D678F312}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F699F3AD-2AD9-454B-BA40-82AC3D6250FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F699F3AD-2AD9-454B-BA40-82AC3D6250FE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F699F3AD-2AD9-454B-BA40-82AC3D6250FE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F699F3AD-2AD9-454B-BA40-82AC3D6250FE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -705,6 +719,9 @@ Global
{F1387494-34DE-4B31-B587-699B2E9A20CA} = {D173887B-AF42-4576-B9C1-96B9E9B3D9C0}
{54B66163-016D-4122-9364-409AB61BC36B} = {F1387494-34DE-4B31-B587-699B2E9A20CA}
{FD63D574-8512-421D-B7FC-310AFA974361} = {F1387494-34DE-4B31-B587-699B2E9A20CA}
+ {621991F1-854A-4743-835B-10CAF11A0CFF} = {D173887B-AF42-4576-B9C1-96B9E9B3D9C0}
+ {7E2AD00B-60E0-46C2-8640-7217D678F312} = {621991F1-854A-4743-835B-10CAF11A0CFF}
+ {F699F3AD-2AD9-454B-BA40-82AC3D6250FE} = {621991F1-854A-4743-835B-10CAF11A0CFF}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {6DCEDFEC-988E-4CB3-B45B-191EB5086E0C}
diff --git a/playground/mysql/MySql.ApiService/MySql.ApiService.csproj b/playground/mysql/MySql.ApiService/MySql.ApiService.csproj
new file mode 100644
index 00000000000..47cc5db1c56
--- /dev/null
+++ b/playground/mysql/MySql.ApiService/MySql.ApiService.csproj
@@ -0,0 +1,22 @@
+
+
+
+ net8.0
+ enable
+ enable
+ true
+ $(NoWarn);CS8002
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/playground/mysql/MySql.ApiService/MySql.ApiService.http b/playground/mysql/MySql.ApiService/MySql.ApiService.http
new file mode 100644
index 00000000000..77a660c4a6c
--- /dev/null
+++ b/playground/mysql/MySql.ApiService/MySql.ApiService.http
@@ -0,0 +1,26 @@
+@HostAddress = http://localhost:5168
+
+GET {{HostAddress}}/catalog/
+Accept: application/json
+
+###
+
+GET {{HostAddress}}/catalog/1
+Accept: application/json
+
+###
+
+POST {{HostAddress}}/catalog/
+Content-Type: application/json
+
+{
+ "name": "New Catalog Item",
+ "description": "New Catalog Description",
+ "price": 23.55
+}
+
+###
+DELETE {{HostAddress}}/catalog/4
+Accept: application/json
+
+###
diff --git a/playground/mysql/MySql.ApiService/Program.cs b/playground/mysql/MySql.ApiService/Program.cs
new file mode 100644
index 00000000000..498f36a23a6
--- /dev/null
+++ b/playground/mysql/MySql.ApiService/Program.cs
@@ -0,0 +1,76 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Dapper;
+using MySqlConnector;
+
+var builder = WebApplication.CreateBuilder(args);
+
+// Add services to the container.
+// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
+builder.Services.AddEndpointsApiExplorer();
+builder.Services.AddSwaggerGen();
+
+builder.AddServiceDefaults();
+
+builder.Services.AddProblemDetails();
+builder.AddMySqlDataSource("Catalog");
+
+var app = builder.Build();
+
+// Configure the HTTP request pipeline.
+if (app.Environment.IsDevelopment())
+{
+ app.UseSwagger();
+ app.UseSwaggerUI();
+}
+
+app.MapGet("/catalog", async (MySqlConnection db) =>
+{
+ const string sql = """
+ SELECT Id, Name, Description, Price
+ FROM catalog
+ """;
+
+ return await db.QueryAsync(sql);
+});
+
+app.MapGet("/catalog/{id}", async (int id, MySqlConnection db) =>
+{
+ const string sql = """
+ SELECT Id, Name, Description, Price
+ FROM catalog
+ WHERE Id = @id
+ """;
+
+ return await db.QueryFirstOrDefaultAsync(sql, new { id }) is { } item
+ ? Results.Ok(item)
+ : Results.NotFound();
+});
+
+app.MapPost("/catalog", async (CatalogItem item, MySqlConnection db) =>
+{
+ const string sql = """
+ INSERT INTO catalog (Name, Description, Price)
+ VALUES (@Name, @Description, @Price);
+ SELECT LAST_INSERT_ID();
+ """;
+
+ var id = await db.ExecuteScalarAsync(sql, item);
+ return Results.Created($"/catalog/{id}", id);
+});
+
+app.MapDelete("/catalog/{id}", async (int id, MySqlConnection db) =>
+{
+ const string sql = """
+ DELETE FROM catalog
+ WHERE Id = @id
+ """;
+
+ var rows = await db.ExecuteAsync(sql, new { id });
+ return rows > 0 ? Results.NoContent() : Results.NotFound();
+});
+
+app.Run();
+
+public record CatalogItem(int Id, string Name, string Description, decimal Price);
diff --git a/playground/mysql/MySql.ApiService/Properties/launchSettings.json b/playground/mysql/MySql.ApiService/Properties/launchSettings.json
new file mode 100644
index 00000000000..175501a6a2f
--- /dev/null
+++ b/playground/mysql/MySql.ApiService/Properties/launchSettings.json
@@ -0,0 +1,41 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:49646",
+ "sslPort": 44383
+ }
+ },
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "http://localhost:5168",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "https://localhost:7203;http://localhost:5168",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/playground/mysql/MySql.ApiService/appsettings.Development.json b/playground/mysql/MySql.ApiService/appsettings.Development.json
new file mode 100644
index 00000000000..0c208ae9181
--- /dev/null
+++ b/playground/mysql/MySql.ApiService/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/playground/mysql/MySql.ApiService/appsettings.json b/playground/mysql/MySql.ApiService/appsettings.json
new file mode 100644
index 00000000000..10f68b8c8b4
--- /dev/null
+++ b/playground/mysql/MySql.ApiService/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/playground/mysql/MySql.ApiService/data/init.sql b/playground/mysql/MySql.ApiService/data/init.sql
new file mode 100644
index 00000000000..0bfae9f08eb
--- /dev/null
+++ b/playground/mysql/MySql.ApiService/data/init.sql
@@ -0,0 +1,25 @@
+-- MySql init script
+
+-- NOTE: MySql database and table names are case-sensitive on non-Windows platforms!
+-- Column names are always case-insensitive.
+
+-- Create the Catalog table
+CREATE TABLE IF NOT EXISTS `catalog`
+(
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `name` varchar(255) NOT NULL,
+ `description` varchar(255) NOT NULL,
+ `price` DECIMAL(18,2) NOT NULL,
+ PRIMARY KEY (`id`)
+);
+
+-- Insert some sample data into the Catalog table only if the table is empty
+INSERT INTO catalog (name, description, price)
+SELECT *
+FROM (
+ SELECT '.NET Bot Black Hoodie', 'This hoodie will keep you warm while looking cool and representing .NET!', 19.5 UNION ALL
+ SELECT '.NET Black & White Mug', 'The perfect place to keep your favorite beverage while you code.', 8.5 UNION ALL
+ SELECT 'Prism White T-Shirt', "It's a t-shirt, it's white, and it can be yours.", 12
+ ) data
+-- This clause ensures the rows are only inserted if the table is empty
+WHERE NOT EXISTS (SELECT NULL FROM catalog)
diff --git a/playground/mysql/MySqlDb.AppHost/Directory.Build.props b/playground/mysql/MySqlDb.AppHost/Directory.Build.props
new file mode 100644
index 00000000000..b9b39c05e81
--- /dev/null
+++ b/playground/mysql/MySqlDb.AppHost/Directory.Build.props
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/playground/mysql/MySqlDb.AppHost/Directory.Build.targets b/playground/mysql/MySqlDb.AppHost/Directory.Build.targets
new file mode 100644
index 00000000000..b7ba77268f8
--- /dev/null
+++ b/playground/mysql/MySqlDb.AppHost/Directory.Build.targets
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/playground/mysql/MySqlDb.AppHost/MySqlDb.AppHost.csproj b/playground/mysql/MySqlDb.AppHost/MySqlDb.AppHost.csproj
new file mode 100644
index 00000000000..a8f76b4e746
--- /dev/null
+++ b/playground/mysql/MySqlDb.AppHost/MySqlDb.AppHost.csproj
@@ -0,0 +1,22 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/playground/mysql/MySqlDb.AppHost/Program.cs b/playground/mysql/MySqlDb.AppHost/Program.cs
new file mode 100644
index 00000000000..73cb11abfe2
--- /dev/null
+++ b/playground/mysql/MySqlDb.AppHost/Program.cs
@@ -0,0 +1,16 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+var builder = DistributedApplication.CreateBuilder(args);
+
+var catalogDbName = "catalog"; // MySql database & table names are case-sensitive on non-Windows.
+var catalogDb = builder.AddMySqlContainer("mysql")
+ .WithEnvironment("MYSQL_DATABASE", catalogDbName)
+ .WithVolumeMount("../MySql.ApiService/data", "/docker-entrypoint-initdb.d", VolumeMountType.Bind)
+ .WithPhpMyAdmin()
+ .AddDatabase(catalogDbName);
+
+builder.AddProject("apiservice")
+ .WithReference(catalogDb);
+
+builder.Build().Run();
diff --git a/playground/mysql/MySqlDb.AppHost/Properties/launchSettings.json b/playground/mysql/MySqlDb.AppHost/Properties/launchSettings.json
new file mode 100644
index 00000000000..4397aca491e
--- /dev/null
+++ b/playground/mysql/MySqlDb.AppHost/Properties/launchSettings.json
@@ -0,0 +1,16 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:15224",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16160"
+ }
+ }
+ }
+}
diff --git a/playground/mysql/MySqlDb.AppHost/appsettings.Development.json b/playground/mysql/MySqlDb.AppHost/appsettings.Development.json
new file mode 100644
index 00000000000..0c208ae9181
--- /dev/null
+++ b/playground/mysql/MySqlDb.AppHost/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/playground/mysql/MySqlDb.AppHost/appsettings.json b/playground/mysql/MySqlDb.AppHost/appsettings.json
new file mode 100644
index 00000000000..31c092aa450
--- /dev/null
+++ b/playground/mysql/MySqlDb.AppHost/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning",
+ "Aspire.Hosting.Dcp": "Warning"
+ }
+ }
+}
diff --git a/src/Aspire.Hosting/MySql/MySqlBuilderExtensions.cs b/src/Aspire.Hosting/MySql/MySqlBuilderExtensions.cs
index 4dd9b0811ca..f99fa21c805 100644
--- a/src/Aspire.Hosting/MySql/MySqlBuilderExtensions.cs
+++ b/src/Aspire.Hosting/MySql/MySqlBuilderExtensions.cs
@@ -3,6 +3,8 @@
using System.Net.Sockets;
using Aspire.Hosting.ApplicationModel;
+using Aspire.Hosting.Lifecycle;
+using Aspire.Hosting.MySql;
using Aspire.Hosting.Publishing;
namespace Aspire.Hosting;
@@ -73,6 +75,34 @@ public static IResourceBuilder AddDatabase(this IResource
.WithManifestPublishingCallback(context => WriteMySqlDatabaseToManifest(context, mySqlDatabase));
}
+ ///
+ /// Adds a phpMyAdmin administration and development platform for MySql to the application model.
+ ///
+ /// The MySql server resource builder.
+ /// The host port for the application ui.
+ /// The name of the container (Optional).
+ /// A reference to the .
+ public static IResourceBuilder WithPhpMyAdmin(this IResourceBuilder builder, int? hostPort = null, string? containerName = null) where T : IMySqlParentResource
+ {
+ if (builder.ApplicationBuilder.Resources.OfType().Any())
+ {
+ return builder;
+ }
+
+ builder.ApplicationBuilder.Services.TryAddLifecycleHook();
+
+ containerName ??= $"{builder.Resource.Name}-phpmyadmin";
+
+ var phpMyAdminContainer = new PhpMyAdminContainerResource(containerName);
+ builder.ApplicationBuilder.AddResource(phpMyAdminContainer)
+ .WithAnnotation(new ContainerImageAnnotation { Image = "phpmyadmin", Tag = "latest" })
+ .WithHttpEndpoint(containerPort: 80, hostPort: hostPort, name: containerName)
+ .WithVolumeMount(Path.GetTempFileName(), "/etc/phpmyadmin/config.user.inc.php")
+ .ExcludeFromManifest();
+
+ return builder;
+ }
+
private static void WriteMySqlContainerToManifest(ManifestPublishingContext context)
{
context.Writer.WriteString("type", "mysql.server.v0");
diff --git a/src/Aspire.Hosting/MySql/PhpMyAdminConfigWriterHook.cs b/src/Aspire.Hosting/MySql/PhpMyAdminConfigWriterHook.cs
new file mode 100644
index 00000000000..57bf52b2064
--- /dev/null
+++ b/src/Aspire.Hosting/MySql/PhpMyAdminConfigWriterHook.cs
@@ -0,0 +1,85 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Hosting.ApplicationModel;
+using Aspire.Hosting.Lifecycle;
+
+namespace Aspire.Hosting.MySql;
+public class PhpMyAdminConfigWriterHook : IDistributedApplicationLifecycleHook
+{
+ public Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken)
+ {
+ var adminResource = appModel.Resources.OfType().Single();
+ var serverFileMount = adminResource.Annotations.OfType().Single(v => v.Target == "/etc/phpmyadmin/config.user.inc.php");
+ var mySqlInstances = appModel.Resources.OfType();
+
+ if (appModel.Resources.OfType().SingleOrDefault() is not { } myAdminResource)
+ {
+ // No-op if there is no myAdmin resource (removed after hook added).
+ return Task.CompletedTask;
+ }
+
+ if (!mySqlInstances.Any())
+ {
+ // No-op if there are no MySql resources present.
+ return Task.CompletedTask;
+ }
+
+ if (mySqlInstances.Count() == 1)
+ {
+ var singleInstance = mySqlInstances.Single();
+ if (singleInstance.TryGetAllocatedEndPoints(out var allocatedEndPoints))
+ {
+ var endpoint = allocatedEndPoints.Where(ae => ae.Name == "tcp").Single();
+ myAdminResource.Annotations.Add(new EnvironmentCallbackAnnotation((EnvironmentCallbackContext context) =>
+ {
+ context.EnvironmentVariables.Add("PMA_HOST", $"host.docker.internal:{endpoint.Port}");
+ context.EnvironmentVariables.Add("PMA_USER", "root");
+ var password = singleInstance switch
+ {
+ MySqlServerResource psr => psr.Password,
+ MySqlContainerResource pcr => pcr.Password,
+ _ => throw new InvalidOperationException("MySql resource is neither MySqlServerResource or MySqlContainerResource.")
+ };
+ context.EnvironmentVariables.Add("PMA_PASSWORD", password);
+ }));
+ }
+ }
+ else
+ {
+ using var stream = new FileStream(serverFileMount.Source, FileMode.Create);
+ using var writer = new StreamWriter(stream);
+
+ writer.WriteLine(" psr.Password,
+ MySqlContainerResource pcr => pcr.Password,
+ _ => throw new InvalidOperationException("MySql resource is neither MySqlServerResource or MySqlContainerResource.")
+ };
+
+ var endpoint = allocatedEndpoints.Where(ae => ae.Name == "tcp").Single();
+ writer.WriteLine("$i++;");
+ writer.WriteLine($"$cfg['Servers'][$i]['host'] = 'host.docker.internal:{endpoint.Port}';");
+ writer.WriteLine($"$cfg['Servers'][$i]['verbose'] = '{mySqlInstance.Name}';");
+ writer.WriteLine($"$cfg['Servers'][$i]['auth_type'] = 'cookie';");
+ writer.WriteLine($"$cfg['Servers'][$i]['user'] = 'root';");
+ writer.WriteLine($"$cfg['Servers'][$i]['password'] = '{password}';");
+ writer.WriteLine($"$cfg['Servers'][$i]['AllowNoPassword'] = true;");
+ writer.WriteLine();
+ }
+ }
+ writer.WriteLine("$cfg['DefaultServer'] = 1;");
+ writer.WriteLine("?>");
+ }
+
+ return Task.CompletedTask;
+ }
+}
diff --git a/src/Aspire.Hosting/MySql/PhpMyAdminResource.cs b/src/Aspire.Hosting/MySql/PhpMyAdminResource.cs
new file mode 100644
index 00000000000..9387ac08e2d
--- /dev/null
+++ b/src/Aspire.Hosting/MySql/PhpMyAdminResource.cs
@@ -0,0 +1,12 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Hosting.ApplicationModel;
+
+namespace Aspire.Hosting.MySql;
+internal sealed class PhpMyAdminContainerResource : ContainerResource
+{
+ public PhpMyAdminContainerResource(string name) : base(name)
+ {
+ }
+}
diff --git a/tests/Aspire.Hosting.Tests/MySql/MySqlFunctionalTests.cs b/tests/Aspire.Hosting.Tests/MySql/MySqlFunctionalTests.cs
index a61d1777814..051fc47fe18 100644
--- a/tests/Aspire.Hosting.Tests/MySql/MySqlFunctionalTests.cs
+++ b/tests/Aspire.Hosting.Tests/MySql/MySqlFunctionalTests.cs
@@ -1,7 +1,11 @@
// 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 System.Text.RegularExpressions;
+using Aspire.Hosting.MySql;
using Aspire.Hosting.Tests.Helpers;
+using Microsoft.Extensions.DependencyInjection;
using Xunit;
namespace Aspire.Hosting.Tests.MySql;
@@ -32,4 +36,90 @@ public async Task VerifyMySqlWorks()
Assert.True(response.IsSuccessStatusCode, responseContent);
}
+
+ [Fact]
+ public void WithMySqlTwiceEndsUpWithOneAdminContainer()
+ {
+ var builder = DistributedApplication.CreateBuilder();
+ builder.AddMySql("mySql").WithPhpMyAdmin();
+ builder.AddMySqlContainer("mySql2").WithPhpMyAdmin();
+
+ Assert.Single(builder.Resources.OfType());
+ }
+
+ [Fact]
+ public async Task SingleMySqlInstanceProducesCorrectMySqlHostsVariable()
+ {
+ var builder = DistributedApplication.CreateBuilder();
+ var mysql = builder.AddMySql("mySql").WithPhpMyAdmin();
+ var app = builder.Build();
+
+ // Add fake allocated endpoints.
+ mysql.WithAnnotation(new AllocatedEndpointAnnotation("tcp", ProtocolType.Tcp, "host.docker.internal", 5001, "tcp"));
+
+ var model = app.Services.GetRequiredService();
+ var hook = new PhpMyAdminConfigWriterHook();
+ await hook.AfterEndpointsAllocatedAsync(model, CancellationToken.None);
+
+ var myAdmin = builder.Resources.Single(r => r.Name.EndsWith("-phpmyadmin"));
+
+ var envAnnotations = myAdmin.Annotations.OfType();
+
+ var config = new Dictionary();
+ var context = new EnvironmentCallbackContext("dcp", config);
+
+ foreach (var annotation in envAnnotations)
+ {
+ annotation.Callback(context);
+ }
+
+ Assert.Equal("host.docker.internal:5001", context.EnvironmentVariables["PMA_HOST"]);
+ Assert.NotNull(context.EnvironmentVariables["PMA_USER"]);
+ Assert.NotNull(context.EnvironmentVariables["PMA_PASSWORD"]);
+ }
+
+ [Fact]
+ public void WithPhpMyAdminAddsContainer()
+ {
+ var builder = DistributedApplication.CreateBuilder();
+ builder.AddMySql("mySql").WithPhpMyAdmin();
+
+ var container = builder.Resources.Single(r => r.Name == "mySql-phpmyadmin");
+ var volume = container.Annotations.OfType().Single();
+
+ Assert.True(File.Exists(volume.Source)); // File should exist, but will be empty.
+ Assert.Equal("/etc/phpmyadmin/config.user.inc.php", volume.Target);
+ }
+
+ [Fact]
+ public void WithPhpMyAdminProducesValidServerConfigFile()
+ {
+ var builder = DistributedApplication.CreateBuilder();
+ var mysql1 = builder.AddMySql("mysql1").WithPhpMyAdmin(8081);
+ var mysql2 = builder.AddMySql("mysql2").WithPhpMyAdmin(8081);
+
+ // Add fake allocated endpoints.
+ mysql1.WithAnnotation(new AllocatedEndpointAnnotation("tcp", ProtocolType.Tcp, "host.docker.internal", 5001, "tcp"));
+ mysql2.WithAnnotation(new AllocatedEndpointAnnotation("tcp", ProtocolType.Tcp, "host.docker.internal", 5002, "tcp"));
+
+ var myAdmin = builder.Resources.Single(r => r.Name.EndsWith("-phpmyadmin"));
+ var volume = myAdmin.Annotations.OfType().Single();
+
+ var app = builder.Build();
+ var appModel = app.Services.GetRequiredService();
+
+ var hook = new PhpMyAdminConfigWriterHook();
+ hook.AfterEndpointsAllocatedAsync(appModel, CancellationToken.None);
+
+ using var stream = File.OpenRead(volume.Source);
+ var fileContents = new StreamReader(stream).ReadToEnd();
+
+ // check to see that the two hosts are in the file
+ string pattern1 = @"\$cfg\['Servers'\]\[\$i\]\['host'\] = 'host.docker.internal:5001';";
+ string pattern2 = @"\$cfg\['Servers'\]\[\$i\]\['host'\] = 'host.docker.internal:5002';";
+ Match match1 = Regex.Match(fileContents, pattern1);
+ Assert.True(match1.Success);
+ Match match2 = Regex.Match(fileContents, pattern2);
+ Assert.True(match2.Success);
+ }
}