From 4fba1858f861a2c5dcab18f515da4ef92fec9f92 Mon Sep 17 00:00:00 2001 From: Alireza Baloochi Date: Sat, 12 Apr 2025 21:06:24 +0330 Subject: [PATCH 1/5] Add adminer integration --- CommunityToolkit.Aspire.sln | 17 +++++ ...lkit.Aspire.Hosting.Adminer.AppHost.csproj | 20 ++++++ .../Program.cs | 8 +++ .../Properties/launchSettings.json | 29 ++++++++ .../appsettings.json | 9 +++ .../AdminerBuilderExtensions.cs | 72 +++++++++++++++++++ .../AdminerContainerImageTags.cs | 11 +++ .../AdminerContainerResource.cs | 17 +++++ .../AdminerLoginServer.cs | 18 +++++ ...unityToolkit.Aspire.Hosting.Adminer.csproj | 19 +++++ .../README.md | 1 + .../login-servers.php | 53 ++++++++++++++ ...spire.Hosting.PostgreSQL.Extensions.csproj | 1 + .../PostgresBuilderExtensions.cs | 65 +++++++++++++++++ 14 files changed, 340 insertions(+) create mode 100644 examples/adminer/CommunityToolkit.Aspire.Hosting.Adminer.AppHost/CommunityToolkit.Aspire.Hosting.Adminer.AppHost.csproj create mode 100644 examples/adminer/CommunityToolkit.Aspire.Hosting.Adminer.AppHost/Program.cs create mode 100644 examples/adminer/CommunityToolkit.Aspire.Hosting.Adminer.AppHost/Properties/launchSettings.json create mode 100644 examples/adminer/CommunityToolkit.Aspire.Hosting.Adminer.AppHost/appsettings.json create mode 100644 src/CommunityToolkit.Aspire.Hosting.Adminer/AdminerBuilderExtensions.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Adminer/AdminerContainerImageTags.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Adminer/AdminerContainerResource.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Adminer/AdminerLoginServer.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Adminer/CommunityToolkit.Aspire.Hosting.Adminer.csproj create mode 100644 src/CommunityToolkit.Aspire.Hosting.Adminer/README.md create mode 100644 src/CommunityToolkit.Aspire.Hosting.Adminer/login-servers.php diff --git a/CommunityToolkit.Aspire.sln b/CommunityToolkit.Aspire.sln index fbdb2aa9d..8a2422e4b 100644 --- a/CommunityToolkit.Aspire.sln +++ b/CommunityToolkit.Aspire.sln @@ -373,6 +373,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hos EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.k6.Tests", "tests\CommunityToolkit.Aspire.Hosting.k6.Tests\CommunityToolkit.Aspire.Hosting.k6.Tests.csproj", "{CCFE3593-49A7-4F03-A329-687490CD0143}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.Adminer", "src\CommunityToolkit.Aspire.Hosting.Adminer\CommunityToolkit.Aspire.Hosting.Adminer.csproj", "{F28330F7-E71A-49C9-8F4B-0BEDCF6A4E5D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "adminer", "adminer", "{A62E017D-5474-4CAC-84CC-974755145B52}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.Adminer.AppHost", "examples\adminer\CommunityToolkit.Aspire.Hosting.Adminer.AppHost\CommunityToolkit.Aspire.Hosting.Adminer.AppHost.csproj", "{85E52133-F4CC-45BC-AEC5-7FE19D3817F2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -979,6 +985,14 @@ Global {CCFE3593-49A7-4F03-A329-687490CD0143}.Debug|Any CPU.Build.0 = Debug|Any CPU {CCFE3593-49A7-4F03-A329-687490CD0143}.Release|Any CPU.ActiveCfg = Release|Any CPU {CCFE3593-49A7-4F03-A329-687490CD0143}.Release|Any CPU.Build.0 = Release|Any CPU + {F28330F7-E71A-49C9-8F4B-0BEDCF6A4E5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F28330F7-E71A-49C9-8F4B-0BEDCF6A4E5D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F28330F7-E71A-49C9-8F4B-0BEDCF6A4E5D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F28330F7-E71A-49C9-8F4B-0BEDCF6A4E5D}.Release|Any CPU.Build.0 = Release|Any CPU + {85E52133-F4CC-45BC-AEC5-7FE19D3817F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {85E52133-F4CC-45BC-AEC5-7FE19D3817F2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {85E52133-F4CC-45BC-AEC5-7FE19D3817F2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {85E52133-F4CC-45BC-AEC5-7FE19D3817F2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1166,6 +1180,9 @@ Global {15BBCE34-82A2-489C-A65B-5BAEA299F07E} = {612ECA40-80B7-4365-9A6A-C35A6BE30FED} {9DB0C7B2-31D3-481C-9C0C-EEAEC9B2AA6A} = {612ECA40-80B7-4365-9A6A-C35A6BE30FED} {CCFE3593-49A7-4F03-A329-687490CD0143} = {899F0713-7FC6-4750-BAFC-AC650B35B453} + {F28330F7-E71A-49C9-8F4B-0BEDCF6A4E5D} = {414151D4-7009-4E78-A5C6-D99EBD1E67D1} + {A62E017D-5474-4CAC-84CC-974755145B52} = {8519CC01-1370-47C8-AD94-B0F326B1563F} + {85E52133-F4CC-45BC-AEC5-7FE19D3817F2} = {A62E017D-5474-4CAC-84CC-974755145B52} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {08B1D4B8-D2C5-4A64-BB8B-E1A2B29525F0} diff --git a/examples/adminer/CommunityToolkit.Aspire.Hosting.Adminer.AppHost/CommunityToolkit.Aspire.Hosting.Adminer.AppHost.csproj b/examples/adminer/CommunityToolkit.Aspire.Hosting.Adminer.AppHost/CommunityToolkit.Aspire.Hosting.Adminer.AppHost.csproj new file mode 100644 index 000000000..625db718c --- /dev/null +++ b/examples/adminer/CommunityToolkit.Aspire.Hosting.Adminer.AppHost/CommunityToolkit.Aspire.Hosting.Adminer.AppHost.csproj @@ -0,0 +1,20 @@ + + + + + Exe + enable + enable + true + + + + + + + + + + + + diff --git a/examples/adminer/CommunityToolkit.Aspire.Hosting.Adminer.AppHost/Program.cs b/examples/adminer/CommunityToolkit.Aspire.Hosting.Adminer.AppHost/Program.cs new file mode 100644 index 000000000..92ed1e127 --- /dev/null +++ b/examples/adminer/CommunityToolkit.Aspire.Hosting.Adminer.AppHost/Program.cs @@ -0,0 +1,8 @@ +var builder = DistributedApplication.CreateBuilder(args); + +var postgres1 = builder.AddPostgres("postgres1") + .WithAdminer(); +postgres1.AddDatabase("db1"); +postgres1.AddDatabase("db2"); + +builder.Build().Run(); \ No newline at end of file diff --git a/examples/adminer/CommunityToolkit.Aspire.Hosting.Adminer.AppHost/Properties/launchSettings.json b/examples/adminer/CommunityToolkit.Aspire.Hosting.Adminer.AppHost/Properties/launchSettings.json new file mode 100644 index 000000000..00e912697 --- /dev/null +++ b/examples/adminer/CommunityToolkit.Aspire.Hosting.Adminer.AppHost/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17202;http://localhost:15211", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21182", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22097" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15211", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19298", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20141" + } + } + } +} diff --git a/examples/adminer/CommunityToolkit.Aspire.Hosting.Adminer.AppHost/appsettings.json b/examples/adminer/CommunityToolkit.Aspire.Hosting.Adminer.AppHost/appsettings.json new file mode 100644 index 000000000..31c092aa4 --- /dev/null +++ b/examples/adminer/CommunityToolkit.Aspire.Hosting.Adminer.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/src/CommunityToolkit.Aspire.Hosting.Adminer/AdminerBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Adminer/AdminerBuilderExtensions.cs new file mode 100644 index 000000000..56264c3c0 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Adminer/AdminerBuilderExtensions.cs @@ -0,0 +1,72 @@ +using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Utils; +using System.Reflection; + +namespace Aspire.Hosting; + +/// +/// Provides extension methods for Adminer resources to an . +/// +public static class AdminerBuilderExtensions +{ + /// + /// Configures the host port that the Adminer resource is exposed on instead of using randomly assigned port. + /// + /// The resource builder for Adminer. + /// The port to bind on the host. If is used random port will be assigned. + /// The resource builder for Adminer. + public static IResourceBuilder WithHostPort(this IResourceBuilder builder, int? port) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithEndpoint(AdminerContainerResource.PrimaryEndpointName, endpoint => + { + endpoint.Port = port; + }); + } + + /// + /// Adds a Adminer container resource to the application. + /// + /// The resource builder. + /// The name of the resource. This name will be used as the connection string name when referenced in a dependency. + /// The host port to bind the underlying container to. + /// + /// Multiple calls will return the same resource builder instance. + /// + /// A reference to the . + public static IResourceBuilder AddAdminer(this IDistributedApplicationBuilder builder, [ResourceName] string name, int? port = null) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(name); + + if (builder.Resources.OfType().SingleOrDefault() is { } existingAdminerResource) + { + var builderForExistingResource = builder.CreateResourceBuilder(existingAdminerResource); + return builderForExistingResource; + } + else + { + var AdminerContainer = new AdminerContainerResource(name); + var AdminerContainerBuilder = builder.AddResource(AdminerContainer) + .WithImage(AdminerContainerImageTags.Image, AdminerContainerImageTags.Tag) + .WithImageRegistry(AdminerContainerImageTags.Registry) + .WithHttpEndpoint(targetPort: 8080, port: port, name: AdminerContainerResource.PrimaryEndpointName) + .ExcludeFromManifest(); + + var assembly = Assembly.GetExecutingAssembly(); + var stream = assembly.GetManifestResourceStream("CommunityToolkit.Aspire.Hosting.Adminer.login-servers.php") ?? throw new InvalidOperationException("Unable to load embedded resource 'login-servers.php'."); + var tempFile = Path.GetTempFileName(); + + using (var fileStream = new FileStream(tempFile, FileMode.Create, FileAccess.Write)) + { + stream.CopyTo(fileStream); + } + + // Refactor this to use WithContainerFiles API when Aspire 9.2 available + AdminerContainerBuilder.WithBindMount(tempFile, "/var/www/html/plugins-enabled/login-servers.php", isReadOnly: true); + + return AdminerContainerBuilder; + } + } +} diff --git a/src/CommunityToolkit.Aspire.Hosting.Adminer/AdminerContainerImageTags.cs b/src/CommunityToolkit.Aspire.Hosting.Adminer/AdminerContainerImageTags.cs new file mode 100644 index 000000000..60e6dc726 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Adminer/AdminerContainerImageTags.cs @@ -0,0 +1,11 @@ + +internal static class AdminerContainerImageTags +{ + /// docker.io + public const string Registry = "docker.io"; + /// docker.io + public const string Image = "library/adminer"; + /// docker.io + public const string Tag = "5.1.0"; +} + diff --git a/src/CommunityToolkit.Aspire.Hosting.Adminer/AdminerContainerResource.cs b/src/CommunityToolkit.Aspire.Hosting.Adminer/AdminerContainerResource.cs new file mode 100644 index 000000000..ea6f0e6bc --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Adminer/AdminerContainerResource.cs @@ -0,0 +1,17 @@ +namespace Aspire.Hosting.ApplicationModel; + +/// +/// Represents a container resource for Adminer. +/// +/// The name of the container resource. +public sealed class AdminerContainerResource(string name) : ContainerResource(name) +{ + internal const string PrimaryEndpointName = "http"; + + private EndpointReference? _primaryEndpoint; + + /// + /// Gets the primary endpoint for the Adminer. + /// + public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, PrimaryEndpointName); +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Hosting.Adminer/AdminerLoginServer.cs b/src/CommunityToolkit.Aspire.Hosting.Adminer/AdminerLoginServer.cs new file mode 100644 index 000000000..76c80b3a8 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Adminer/AdminerLoginServer.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Hosting.Adminer; + +internal class AdminerLoginServer +{ + [JsonPropertyName("server")] + public string? Server { get; set; } + + [JsonPropertyName("username")] + public string? UserName { get; set; } + + [JsonPropertyName("password")] + public string? Password { get; set; } + + [JsonPropertyName("driver")] + public string? Driver { get; set; } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Hosting.Adminer/CommunityToolkit.Aspire.Hosting.Adminer.csproj b/src/CommunityToolkit.Aspire.Hosting.Adminer/CommunityToolkit.Aspire.Hosting.Adminer.csproj new file mode 100644 index 000000000..d0f35b842 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Adminer/CommunityToolkit.Aspire.Hosting.Adminer.csproj @@ -0,0 +1,19 @@ + + + + hosting adminer + A .NET Aspire integration for adminer hosting. + + + + + + + + + + + + + + diff --git a/src/CommunityToolkit.Aspire.Hosting.Adminer/README.md b/src/CommunityToolkit.Aspire.Hosting.Adminer/README.md new file mode 100644 index 000000000..7b5d65ce4 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Adminer/README.md @@ -0,0 +1 @@ +This package is designed to be used internally by the community toolkit and is not intended to be used directly in the application code. diff --git a/src/CommunityToolkit.Aspire.Hosting.Adminer/login-servers.php b/src/CommunityToolkit.Aspire.Hosting.Adminer/login-servers.php new file mode 100644 index 000000000..08ef2535e --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Adminer/login-servers.php @@ -0,0 +1,53 @@ +servers = array(); + if ($_ENV['ADMINER_SERVERS']) { + $this->servers = json_decode($_ENV['ADMINER_SERVERS'], true); + } + + if ($_POST["auth"]["custom_server"]) { + $key = $_POST["auth"]["custom_server"]; + $_POST["auth"]["driver"] = $this->servers[$key]["driver"]; + $_POST["auth"]["server"] = $this->servers[$key]["server"]; + $_POST["auth"]["username"] = $this->servers[$key]["username"]; + $_POST["auth"]["password"] = $this->servers[$key]["password"]; + $_POST["auth"]["db"] = $this->servers[$key]["db"]; + } + } + + function loginFormField($name, $heading, $value) { + if ($name == 'driver') { + return 'Driver' . $value; + } elseif ($name == 'server') { + return 'Host' . $value; + } elseif ($name == 'db' && $_ENV['ADMINER_SERVERS'] != '') { + $out = $heading . $value; + $out .= 'or'; + $out .= 'Server'; + return $out; + } + } +} + +return new AdminerLoginServers(); diff --git a/src/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions.csproj b/src/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions.csproj index dcd84100b..e7cae9718 100644 --- a/src/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions.csproj +++ b/src/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions.csproj @@ -11,6 +11,7 @@ + diff --git a/src/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions/PostgresBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions/PostgresBuilderExtensions.cs index 0c990e177..e90cdbbbf 100644 --- a/src/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions/PostgresBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions/PostgresBuilderExtensions.cs @@ -1,5 +1,8 @@ using Aspire.Hosting.ApplicationModel; +using CommunityToolkit.Aspire.Hosting.Adminer; using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; namespace Aspire.Hosting; @@ -50,6 +53,29 @@ public static IResourceBuilder WithDbGate(this IResource return builder; } + /// + /// + /// + /// + /// + /// + /// + public static IResourceBuilder WithAdminer(this IResourceBuilder builder, Action>? configureContainer = null, string? containerName = null) + { + ArgumentNullException.ThrowIfNull(builder); + + containerName ??= $"{builder.Resource.Name}-adminer"; + var adminerBuilder = AdminerBuilderExtensions.AddAdminer(builder.ApplicationBuilder, containerName); + + adminerBuilder + .WithEnvironment(context => ConfigureAdminerContainer(context, builder.ApplicationBuilder)) + .WaitFor(builder); + + configureContainer?.Invoke(adminerBuilder); + + return builder; + } + private static void ConfigureDbGateContainer(EnvironmentCallbackContext context, IDistributedApplicationBuilder applicationBuilder) { var postgresInstances = applicationBuilder.Resources.OfType(); @@ -96,4 +122,43 @@ private static void ConfigureDbGateContainer(EnvironmentCallbackContext context, } } } + + + internal static void ConfigureAdminerContainer(EnvironmentCallbackContext context, IDistributedApplicationBuilder applicationBuilder) + { + var postgresInstances = applicationBuilder.Resources.OfType(); + + string ADMINER_SERVERS = context.EnvironmentVariables.GetValueOrDefault("ADMINER_SERVERS")?.ToString() ?? string.Empty; + + var new_servers = postgresInstances.ToDictionary( + postgresServer => postgresServer.Name, + postgresServer => + { + var user = postgresServer.UserNameParameter?.Value ?? "postgres"; + return new AdminerLoginServer + { + Server = postgresServer.Name, + UserName = user, + Password = postgresServer.PasswordParameter.Value, + Driver = "pgsql" + }; + }); + + if (string.IsNullOrEmpty(ADMINER_SERVERS)) + { + string servers_json = JsonSerializer.Serialize(new_servers); + context.EnvironmentVariables["ADMINER_SERVERS"] = servers_json; + } + else + { + var servers = JsonSerializer.Deserialize>(ADMINER_SERVERS); + foreach (var server in new_servers) + { + servers!.Add(server.Key, server.Value); + } + string servers_json = JsonSerializer.Serialize(new_servers); + context.EnvironmentVariables["ADMINER_SERVERS"] = servers_json; + } + + } } \ No newline at end of file From 86c0a25231a15c707e78ca40c053e24c7a6ad68f Mon Sep 17 00:00:00 2001 From: Alireza Baloochi Date: Sun, 13 Apr 2025 20:50:19 +0330 Subject: [PATCH 2/5] Add tests --- CommunityToolkit.Aspire.sln | 7 + .../Program.cs | 5 + .../Program.cs | 6 +- ...unityToolkit.Aspire.Hosting.Adminer.csproj | 2 +- ...spire.Hosting.PostgreSQL.Extensions.csproj | 4 + .../PostgresBuilderExtensions.cs | 11 +- .../Adminer}/AdminerLoginServer.cs | 2 - .../AddAdminerTests.cs | 211 ++++++++++++++++++ .../AdminerPublicApiTests.cs | 43 ++++ .../AppHostTests.cs | 20 ++ ...oolkit.Aspire.Hosting.Adminer.Tests.csproj | 12 + ...Hosting.PostgreSQL.Extensions.Tests.csproj | 3 + .../ResourceCreationTests.cs | 156 +++++++++++++ 13 files changed, 472 insertions(+), 10 deletions(-) rename src/{CommunityToolkit.Aspire.Hosting.Adminer => Shared/Adminer}/AdminerLoginServer.cs (88%) create mode 100644 tests/CommunityToolkit.Aspire.Hosting.Adminer.Tests/AddAdminerTests.cs create mode 100644 tests/CommunityToolkit.Aspire.Hosting.Adminer.Tests/AdminerPublicApiTests.cs create mode 100644 tests/CommunityToolkit.Aspire.Hosting.Adminer.Tests/AppHostTests.cs create mode 100644 tests/CommunityToolkit.Aspire.Hosting.Adminer.Tests/CommunityToolkit.Aspire.Hosting.Adminer.Tests.csproj diff --git a/CommunityToolkit.Aspire.sln b/CommunityToolkit.Aspire.sln index 8a2422e4b..5498f6bee 100644 --- a/CommunityToolkit.Aspire.sln +++ b/CommunityToolkit.Aspire.sln @@ -379,6 +379,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "adminer", "adminer", "{A62E EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.Adminer.AppHost", "examples\adminer\CommunityToolkit.Aspire.Hosting.Adminer.AppHost\CommunityToolkit.Aspire.Hosting.Adminer.AppHost.csproj", "{85E52133-F4CC-45BC-AEC5-7FE19D3817F2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.Adminer.Tests", "tests\CommunityToolkit.Aspire.Hosting.Adminer.Tests\CommunityToolkit.Aspire.Hosting.Adminer.Tests.csproj", "{4973B296-C644-4737-BBCC-6666C077CEBA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -993,6 +995,10 @@ Global {85E52133-F4CC-45BC-AEC5-7FE19D3817F2}.Debug|Any CPU.Build.0 = Debug|Any CPU {85E52133-F4CC-45BC-AEC5-7FE19D3817F2}.Release|Any CPU.ActiveCfg = Release|Any CPU {85E52133-F4CC-45BC-AEC5-7FE19D3817F2}.Release|Any CPU.Build.0 = Release|Any CPU + {4973B296-C644-4737-BBCC-6666C077CEBA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4973B296-C644-4737-BBCC-6666C077CEBA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4973B296-C644-4737-BBCC-6666C077CEBA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4973B296-C644-4737-BBCC-6666C077CEBA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1183,6 +1189,7 @@ Global {F28330F7-E71A-49C9-8F4B-0BEDCF6A4E5D} = {414151D4-7009-4E78-A5C6-D99EBD1E67D1} {A62E017D-5474-4CAC-84CC-974755145B52} = {8519CC01-1370-47C8-AD94-B0F326B1563F} {85E52133-F4CC-45BC-AEC5-7FE19D3817F2} = {A62E017D-5474-4CAC-84CC-974755145B52} + {4973B296-C644-4737-BBCC-6666C077CEBA} = {899F0713-7FC6-4750-BAFC-AC650B35B453} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {08B1D4B8-D2C5-4A64-BB8B-E1A2B29525F0} diff --git a/examples/adminer/CommunityToolkit.Aspire.Hosting.Adminer.AppHost/Program.cs b/examples/adminer/CommunityToolkit.Aspire.Hosting.Adminer.AppHost/Program.cs index 92ed1e127..325c7d91c 100644 --- a/examples/adminer/CommunityToolkit.Aspire.Hosting.Adminer.AppHost/Program.cs +++ b/examples/adminer/CommunityToolkit.Aspire.Hosting.Adminer.AppHost/Program.cs @@ -5,4 +5,9 @@ postgres1.AddDatabase("db1"); postgres1.AddDatabase("db2"); +var postgres2 = builder.AddPostgres("postgres2") + .WithAdminer(); +postgres2.AddDatabase("db3"); +postgres2.AddDatabase("db4"); + builder.Build().Run(); \ No newline at end of file diff --git a/examples/postgres-ext/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions.AppHost/Program.cs b/examples/postgres-ext/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions.AppHost/Program.cs index ffe82f812..b52737b26 100644 --- a/examples/postgres-ext/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions.AppHost/Program.cs +++ b/examples/postgres-ext/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions.AppHost/Program.cs @@ -1,12 +1,14 @@ var builder = DistributedApplication.CreateBuilder(args); var postgres1 = builder.AddPostgres("postgres1") - .WithDbGate(c => c.WithHostPort(8068)); + .WithDbGate(c => c.WithHostPort(8068)) + .WithAdminer(c => c.WithHostPort(8069)); postgres1.AddDatabase("db1"); postgres1.AddDatabase("db2"); var postgres2 = builder.AddPostgres("postgres2") - .WithDbGate(c => c.WithHostPort(8068)); + .WithDbGate(c => c.WithHostPort(8068)) + .WithAdminer(c => c.WithHostPort(8069)); postgres2.AddDatabase("db3"); postgres2.AddDatabase("db4"); diff --git a/src/CommunityToolkit.Aspire.Hosting.Adminer/CommunityToolkit.Aspire.Hosting.Adminer.csproj b/src/CommunityToolkit.Aspire.Hosting.Adminer/CommunityToolkit.Aspire.Hosting.Adminer.csproj index d0f35b842..39de4f7f9 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Adminer/CommunityToolkit.Aspire.Hosting.Adminer.csproj +++ b/src/CommunityToolkit.Aspire.Hosting.Adminer/CommunityToolkit.Aspire.Hosting.Adminer.csproj @@ -14,6 +14,6 @@ - + diff --git a/src/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions.csproj b/src/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions.csproj index e7cae9718..6654fe6d9 100644 --- a/src/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions.csproj +++ b/src/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions.csproj @@ -15,4 +15,8 @@ + + + + diff --git a/src/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions/PostgresBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions/PostgresBuilderExtensions.cs index e90cdbbbf..692748f95 100644 --- a/src/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions/PostgresBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions/PostgresBuilderExtensions.cs @@ -1,5 +1,4 @@ using Aspire.Hosting.ApplicationModel; -using CommunityToolkit.Aspire.Hosting.Adminer; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; @@ -68,8 +67,7 @@ public static IResourceBuilder WithAdminer(this IResourc var adminerBuilder = AdminerBuilderExtensions.AddAdminer(builder.ApplicationBuilder, containerName); adminerBuilder - .WithEnvironment(context => ConfigureAdminerContainer(context, builder.ApplicationBuilder)) - .WaitFor(builder); + .WithEnvironment(context => ConfigureAdminerContainer(context, builder.ApplicationBuilder)); configureContainer?.Invoke(adminerBuilder); @@ -151,10 +149,13 @@ internal static void ConfigureAdminerContainer(EnvironmentCallbackContext contex } else { - var servers = JsonSerializer.Deserialize>(ADMINER_SERVERS); + var servers = JsonSerializer.Deserialize>(ADMINER_SERVERS) ?? throw new InvalidOperationException("The servers should not be null. This should never happen."); foreach (var server in new_servers) { - servers!.Add(server.Key, server.Value); + if (!servers.ContainsKey(server.Key)) + { + servers!.Add(server.Key, server.Value); + } } string servers_json = JsonSerializer.Serialize(new_servers); context.EnvironmentVariables["ADMINER_SERVERS"] = servers_json; diff --git a/src/CommunityToolkit.Aspire.Hosting.Adminer/AdminerLoginServer.cs b/src/Shared/Adminer/AdminerLoginServer.cs similarity index 88% rename from src/CommunityToolkit.Aspire.Hosting.Adminer/AdminerLoginServer.cs rename to src/Shared/Adminer/AdminerLoginServer.cs index 76c80b3a8..83d3eda95 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Adminer/AdminerLoginServer.cs +++ b/src/Shared/Adminer/AdminerLoginServer.cs @@ -1,7 +1,5 @@ using System.Text.Json.Serialization; -namespace CommunityToolkit.Aspire.Hosting.Adminer; - internal class AdminerLoginServer { [JsonPropertyName("server")] diff --git a/tests/CommunityToolkit.Aspire.Hosting.Adminer.Tests/AddAdminerTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Adminer.Tests/AddAdminerTests.cs new file mode 100644 index 000000000..80dd3e01f --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Adminer.Tests/AddAdminerTests.cs @@ -0,0 +1,211 @@ +using System.Net.Sockets; +using System.Text.Json; +using Aspire.Hosting; + +namespace CommunityToolkit.Aspire.Hosting.Adminer.Tests; +public class AddAdminerTests +{ + [Fact] + public void AddAdminerContainerWithDefaultsAddsAnnotationMetadata() + { + var appBuilder = DistributedApplication.CreateBuilder(); + + var adminer = appBuilder.AddAdminer("adminer"); + + using var app = appBuilder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var containerResource = Assert.Single(appModel.Resources.OfType()); + Assert.Equal("adminer", containerResource.Name); + + var endpoints = containerResource.Annotations.OfType(); + Assert.Single(endpoints); + + var primaryEndpoint = Assert.Single(endpoints, e => e.Name == "http"); + Assert.Equal(8080, primaryEndpoint.TargetPort); + Assert.False(primaryEndpoint.IsExternal); + Assert.Equal("http", primaryEndpoint.Name); + Assert.Null(primaryEndpoint.Port); + Assert.Equal(ProtocolType.Tcp, primaryEndpoint.Protocol); + Assert.Equal("http", primaryEndpoint.Transport); + Assert.Equal("http", primaryEndpoint.UriScheme); + + var containerAnnotation = Assert.Single(containerResource.Annotations.OfType()); + Assert.Equal(AdminerContainerImageTags.Tag, containerAnnotation.Tag); + Assert.Equal(AdminerContainerImageTags.Image, containerAnnotation.Image); + Assert.Equal(AdminerContainerImageTags.Registry, containerAnnotation.Registry); + + var annotations = adminer.Resource.Annotations; + + Assert.Contains(ManifestPublishingCallbackAnnotation.Ignore, annotations); + } + + [Fact] + public void AddAdminerContainerWithPort() + { + var appBuilder = DistributedApplication.CreateBuilder(); + + var adminer = appBuilder.AddAdminer("adminer", 9090); + + using var app = appBuilder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var containerResource = Assert.Single(appModel.Resources.OfType()); + Assert.Equal("adminer", containerResource.Name); + + var endpoints = containerResource.Annotations.OfType(); + Assert.Single(endpoints); + + var primaryEndpoint = Assert.Single(endpoints, e => e.Name == "http"); + Assert.Equal(8080, primaryEndpoint.TargetPort); + Assert.False(primaryEndpoint.IsExternal); + Assert.Equal("http", primaryEndpoint.Name); + Assert.Equal(9090, primaryEndpoint.Port); + Assert.Equal(ProtocolType.Tcp, primaryEndpoint.Protocol); + Assert.Equal("http", primaryEndpoint.Transport); + Assert.Equal("http", primaryEndpoint.UriScheme); + + var containerAnnotation = Assert.Single(containerResource.Annotations.OfType()); + Assert.Equal(AdminerContainerImageTags.Tag, containerAnnotation.Tag); + Assert.Equal(AdminerContainerImageTags.Image, containerAnnotation.Image); + Assert.Equal(AdminerContainerImageTags.Registry, containerAnnotation.Registry); + + var annotations = adminer.Resource.Annotations; + + Assert.Contains(ManifestPublishingCallbackAnnotation.Ignore, annotations); + } + + [Fact] + public void MultipleAddAdminerCallsShouldAddOneAdminerResource() + { + var appBuilder = DistributedApplication.CreateBuilder(); + + appBuilder.AddAdminer("adminer1"); + appBuilder.AddAdminer("adminer2"); + + using var app = appBuilder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var containerResource = Assert.Single(appModel.Resources.OfType()); + Assert.Equal("adminer1", containerResource.Name); + } + + [Fact] + public void VerifyWithHostPort() + { + var appBuilder = DistributedApplication.CreateBuilder(); + + var adminer = appBuilder.AddAdminer("adminer").WithHostPort(9090); + + using var app = appBuilder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var containerResource = Assert.Single(appModel.Resources.OfType()); + Assert.Equal("adminer", containerResource.Name); + + var endpoints = containerResource.Annotations.OfType(); + Assert.Single(endpoints); + + var primaryEndpoint = Assert.Single(endpoints, e => e.Name == "http"); + Assert.Equal(8080, primaryEndpoint.TargetPort); + Assert.False(primaryEndpoint.IsExternal); + Assert.Equal("http", primaryEndpoint.Name); + Assert.Equal(9090, primaryEndpoint.Port); + Assert.Equal(ProtocolType.Tcp, primaryEndpoint.Protocol); + Assert.Equal("http", primaryEndpoint.Transport); + Assert.Equal("http", primaryEndpoint.UriScheme); + + var containerAnnotation = Assert.Single(containerResource.Annotations.OfType()); + Assert.Equal(AdminerContainerImageTags.Tag, containerAnnotation.Tag); + Assert.Equal(AdminerContainerImageTags.Image, containerAnnotation.Image); + Assert.Equal(AdminerContainerImageTags.Registry, containerAnnotation.Registry); + + var annotations = adminer.Resource.Annotations; + + Assert.Contains(ManifestPublishingCallbackAnnotation.Ignore, annotations); + } + + [Fact] + public async Task WithAdminerShouldAddAnnotationsForMultipleDatabaseTypes() + { + var builder = DistributedApplication.CreateBuilder(); + + var postgresResourceBuilder1 = builder.AddPostgres("postgres1") + .WithAdminer(); + + var postgresResource1 = postgresResourceBuilder1.Resource; + + var postgresResourceBuilder2 = builder.AddPostgres("postgres2") + .WithAdminer(); + + var postgresResource2 = postgresResourceBuilder2.Resource; + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var adminerContainerResource = appModel.Resources.OfType().SingleOrDefault(); + + Assert.NotNull(adminerContainerResource); + + Assert.Equal("postgres1-adminer", adminerContainerResource.Name); + + var envs = await adminerContainerResource.GetEnvironmentVariableValuesAsync(); + + Assert.NotEmpty(envs); + + var servers = new Dictionary + { + { + "postgres1", + new AdminerLoginServer + { + Driver = "pgsql", + Server = postgresResource1.Name, + Password = postgresResource1.PasswordParameter.Value, + UserName = postgresResource1.UserNameParameter?.Value ?? "postgres" + } + }, + { + "postgres2", + new AdminerLoginServer + { + Driver = "pgsql", + Server = postgresResource2.Name, + Password = postgresResource2.PasswordParameter.Value, + UserName = postgresResource2.UserNameParameter?.Value ?? "postgres" + } + } + }; + + var envValue = JsonSerializer.Serialize(servers); + var item = Assert.Single(envs); + Assert.Equal("ADMINER_SERVERS", item.Key); + Assert.Equal(envValue, item.Value); + } + + [Fact] + public void WithAdminerShouldShouldAddOneAdminerResourceForMultipleDatabaseTypes() + { + var builder = DistributedApplication.CreateBuilder(); + + builder.AddPostgres("postgres1") + .WithAdminer(); + + builder.AddPostgres("postgres2") + .WithAdminer(); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var adminerResource = appModel.Resources.OfType().SingleOrDefault(); + + var containerResource = Assert.Single(appModel.Resources.OfType()); + Assert.Equal("postgres1-adminer", containerResource.Name); + } +} diff --git a/tests/CommunityToolkit.Aspire.Hosting.Adminer.Tests/AdminerPublicApiTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Adminer.Tests/AdminerPublicApiTests.cs new file mode 100644 index 000000000..56acbd042 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Adminer.Tests/AdminerPublicApiTests.cs @@ -0,0 +1,43 @@ +using Aspire.Hosting; + +namespace CommunityToolkit.Aspire.Hosting.Adminer.Tests; + +public class AdminerPublicApiTests +{ + [Fact] + public void AddAdminerContainerShouldThrowWhenBuilderIsNull() + { + IDistributedApplicationBuilder builder = null!; + const string name = "adminer"; + + var action = () => builder.AddAdminer(name); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(builder), exception.ParamName); + } + + [Fact] + public void AddAdminerContainerShouldThrowWhenNameIsNull() + { + IDistributedApplicationBuilder builder = new DistributedApplicationBuilder([]); + string name = null!; + + var action = () => builder.AddAdminer(name); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(name), exception.ParamName); + } + + [Fact] + public void WithHostPortShouldThrowWhenBuilderIsNull() + { + IResourceBuilder builder = null!; + + Func>? action = null; + + action = () => builder.WithHostPort(9090); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(builder), exception.ParamName); + } +} diff --git a/tests/CommunityToolkit.Aspire.Hosting.Adminer.Tests/AppHostTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Adminer.Tests/AppHostTests.cs new file mode 100644 index 000000000..556bddaf6 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Adminer.Tests/AppHostTests.cs @@ -0,0 +1,20 @@ +using CommunityToolkit.Aspire.Testing; +using Aspire.Components.Common.Tests; + +namespace CommunityToolkit.Aspire.Hosting.Adminer.Tests; + +[RequiresDocker] +public class AppHostTests(AspireIntegrationTestFixture fixture) : IClassFixture> +{ + [Fact] + public async Task ResourceStartsAndRespondsOk() + { + var resourceName = "postgres1-adminer"; + await fixture.ResourceNotificationService.WaitForResourceHealthyAsync(resourceName).WaitAsync(TimeSpan.FromMinutes(5)); + var httpClient = fixture.CreateHttpClient(resourceName); + + var response = await httpClient.GetAsync("/"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } +} \ No newline at end of file diff --git a/tests/CommunityToolkit.Aspire.Hosting.Adminer.Tests/CommunityToolkit.Aspire.Hosting.Adminer.Tests.csproj b/tests/CommunityToolkit.Aspire.Hosting.Adminer.Tests/CommunityToolkit.Aspire.Hosting.Adminer.Tests.csproj new file mode 100644 index 000000000..e113bc4d3 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Adminer.Tests/CommunityToolkit.Aspire.Hosting.Adminer.Tests.csproj @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/tests/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions.Tests/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions.Tests.csproj b/tests/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions.Tests/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions.Tests.csproj index 326f2c0e4..fcac4a6f0 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions.Tests/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions.Tests.csproj +++ b/tests/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions.Tests/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions.Tests.csproj @@ -4,4 +4,7 @@ + + + diff --git a/tests/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions.Tests/ResourceCreationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions.Tests/ResourceCreationTests.cs index 7492a1a29..6c442c495 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions.Tests/ResourceCreationTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions.Tests/ResourceCreationTests.cs @@ -1,4 +1,5 @@ using Aspire.Hosting; +using System.Text.Json; namespace CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions.Tests; @@ -278,4 +279,159 @@ public async Task WithDbGateAddsAnnotationsForProvidedUsernamePassword() Assert.Equal("postgres1", item.Value); }); } + + [Fact] + public async Task WithAdminerAddsAnnotations() + { + var builder = DistributedApplication.CreateBuilder(); + + var postgresResourceBuilder = builder.AddPostgres("postgres") + .WithAdminer(); + + var postgresResource = postgresResourceBuilder.Resource; + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var adminerResource = appModel.Resources.OfType().SingleOrDefault(); + + Assert.NotNull(adminerResource); + + Assert.Equal("postgres-adminer", adminerResource.Name); + + var envs = await adminerResource.GetEnvironmentVariableValuesAsync(); + + Assert.NotEmpty(envs); + + var servers = new Dictionary + { + { + "postgres", + new AdminerLoginServer + { + Driver = "pgsql", + Server = postgresResource.Name, + Password = postgresResource.PasswordParameter.Value, + UserName = postgresResource.UserNameParameter?.Value ?? "postgres" + } + }, + }; + + var envValue = JsonSerializer.Serialize(servers); + var item = Assert.Single(envs); + Assert.Equal("ADMINER_SERVERS", item.Key); + Assert.Equal(envValue, item.Value); + } + + [Fact] + public void MultipleWithAdminerCallsAddsOneDbGateResource() + { + var builder = DistributedApplication.CreateBuilder(); + builder.AddPostgres("postgres1").WithAdminer(); + builder.AddPostgres("postgres2").WithAdminer(); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var adminerContainer = appModel.Resources.OfType().SingleOrDefault(); + Assert.NotNull(adminerContainer); + + Assert.Equal("postgres1-adminer", adminerContainer.Name); + } + + [Fact] + public void WithAdminerShouldChangeAdminerHostPort() + { + var builder = DistributedApplication.CreateBuilder(); + var postgresResourceBuilder = builder.AddPostgres("postgres") + .WithAdminer(c => c.WithHostPort(8068)); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var adminerContainer = appModel.Resources.OfType().SingleOrDefault(); + Assert.NotNull(adminerContainer); + + var primaryEndpoint = adminerContainer.Annotations.OfType().Single(); + Assert.Equal(8068, primaryEndpoint.Port); + } + + [Fact] + public void WithAdminerShouldChangeAdminerContainerImageTag() + { + var builder = DistributedApplication.CreateBuilder(); + var postgresResourceBuilder = builder.AddPostgres("postgres") + .WithAdminer(c => c.WithImageTag("manualTag")); + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var adminerContainer = appModel.Resources.OfType().SingleOrDefault(); + Assert.NotNull(adminerContainer); + + var containerImageAnnotation = adminerContainer.Annotations.OfType().Single(); + Assert.Equal("manualTag", containerImageAnnotation.Tag); + } + + [Fact] + public async Task WithAdminerAddsAnnotationsForMultiplePostgresResource() + { + var builder = DistributedApplication.CreateBuilder(); + + var postgresResourceBuilder1 = builder.AddPostgres("postgres1") + .WithAdminer(); + + var postgresResource1 = postgresResourceBuilder1.Resource; + + var postgresResourceBuilder2 = builder.AddPostgres("postgres2") + .WithDbGate(); + + var postgresResource2 = postgresResourceBuilder2.Resource; + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var adminerContainer = appModel.Resources.OfType().SingleOrDefault(); + + Assert.NotNull(adminerContainer); + + Assert.Equal("postgres1-adminer", adminerContainer.Name); + + var envs = await adminerContainer.GetEnvironmentVariableValuesAsync(); + + Assert.NotEmpty(envs); + + var servers = new Dictionary + { + { + "postgres1", + new AdminerLoginServer + { + Driver = "pgsql", + Server = postgresResource1.Name, + Password = postgresResource1.PasswordParameter.Value, + UserName = postgresResource1.UserNameParameter?.Value ?? "postgres" + } + }, + { + "postgres2", + new AdminerLoginServer + { + Driver = "pgsql", + Server = postgresResource2.Name, + Password = postgresResource2.PasswordParameter.Value, + UserName = postgresResource2.UserNameParameter?.Value ?? "postgres" + } + } + }; + + var envValue = JsonSerializer.Serialize(servers); + var item = Assert.Single(envs); + Assert.Equal("ADMINER_SERVERS", item.Key); + Assert.Equal(envValue, item.Value); + } } From 0cca8e42d0098f10af6ce02ab893176ead78084d Mon Sep 17 00:00:00 2001 From: Alireza Baloochi Date: Sun, 13 Apr 2025 20:57:43 +0330 Subject: [PATCH 3/5] Fix xml docs --- .../AdminerBuilderExtensions.cs | 1 - .../AdminerContainerImageTags.cs | 4 +-- .../PostgresBuilderExtensions.cs | 28 +++++++++++++++---- .../README.md | 5 ++-- 4 files changed, 28 insertions(+), 10 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Adminer/AdminerBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Adminer/AdminerBuilderExtensions.cs index 56264c3c0..d2e7d1c89 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Adminer/AdminerBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Adminer/AdminerBuilderExtensions.cs @@ -1,5 +1,4 @@ using Aspire.Hosting.ApplicationModel; -using Aspire.Hosting.Utils; using System.Reflection; namespace Aspire.Hosting; diff --git a/src/CommunityToolkit.Aspire.Hosting.Adminer/AdminerContainerImageTags.cs b/src/CommunityToolkit.Aspire.Hosting.Adminer/AdminerContainerImageTags.cs index 60e6dc726..4bd370004 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Adminer/AdminerContainerImageTags.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Adminer/AdminerContainerImageTags.cs @@ -3,9 +3,9 @@ internal static class AdminerContainerImageTags { /// docker.io public const string Registry = "docker.io"; - /// docker.io + /// library/adminer public const string Image = "library/adminer"; - /// docker.io + /// 5.1.0 public const string Tag = "5.1.0"; } diff --git a/src/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions/PostgresBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions/PostgresBuilderExtensions.cs index 692748f95..0848fe8f9 100644 --- a/src/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions/PostgresBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions/PostgresBuilderExtensions.cs @@ -53,12 +53,30 @@ public static IResourceBuilder WithDbGate(this IResource } /// - /// + /// Adds an administration and development platform for PostgreSQL to the application model using Adminer. /// - /// - /// - /// - /// + /// + /// This version of the package defaults to the tag of the container image. + /// The Postgres server resource builder. + /// Configuration callback for Adminer container resource. + /// The name of the container (Optional). + /// + /// Use in application host with a Postgres resource + /// + /// var builder = DistributedApplication.CreateBuilder(args); + /// + /// var postgres = builder.AddPostgres("postgres") + /// .WithAdminer(); + /// var db = postgres.AddDatabase("db"); + /// + /// var api = builder.AddProject<Projects.Api>("api") + /// .WithReference(db); + /// + /// builder.Build().Run(); + /// + /// + /// + /// A reference to the . public static IResourceBuilder WithAdminer(this IResourceBuilder builder, Action>? configureContainer = null, string? containerName = null) { ArgumentNullException.ThrowIfNull(builder); diff --git a/src/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions/README.md b/src/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions/README.md index d8f59eeed..a62613970 100644 --- a/src/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions/README.md +++ b/src/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions/README.md @@ -2,7 +2,7 @@ This integration contains extensions for the [PostgreSQL hosting package](https://nuget.org/packages/Aspire.Hosting.PostgreSQL) for .NET Aspire. -The integration provides support for running [DbGate](https://github.com/dbgate/dbgate) to interact with the PostgreSQL database. +The integration provides support for running [DbGate](https://github.com/dbgate/dbgate) and [Adminer](https://github.com/vrana/adminer) to interact with the PostgreSQL database. ## Getting Started @@ -20,7 +20,8 @@ Then, in the _Program.cs_ file of `AppHost`, define an Postgres resource, then c ```csharp var postgres = builder.AddPostgres("postgres") - .WithDbGate(); + .WithDbGate() + .WithAdminer(); ``` ## Additional Information From 1c5ab6a4a0747707050ec20d0134740192397efa Mon Sep 17 00:00:00 2001 From: Alireza Baloochi Date: Sun, 13 Apr 2025 21:01:18 +0330 Subject: [PATCH 4/5] update tests list --- .github/workflows/tests.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 7396bde35..82b2b269b 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -49,6 +49,7 @@ jobs: Hosting.SqlDatabaseProjects.Tests, Hosting.Sqlite.Tests, Hosting.SqlServer.Extensions.Tests, + Hosting.Adminer.Tests, # Client integration tests EventStore.Tests, From fa138da209d5915f6036862955121bc1712c3e09 Mon Sep 17 00:00:00 2001 From: Alireza Baloochi Date: Sun, 13 Apr 2025 21:12:25 +0330 Subject: [PATCH 5/5] fix --- .../PostgresBuilderExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions/PostgresBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions/PostgresBuilderExtensions.cs index 0848fe8f9..3f965313d 100644 --- a/src/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions/PostgresBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions/PostgresBuilderExtensions.cs @@ -175,7 +175,7 @@ internal static void ConfigureAdminerContainer(EnvironmentCallbackContext contex servers!.Add(server.Key, server.Value); } } - string servers_json = JsonSerializer.Serialize(new_servers); + string servers_json = JsonSerializer.Serialize(servers); context.EnvironmentVariables["ADMINER_SERVERS"] = servers_json; }