From 35cce2904d1ad572c700bc6eaf5929c444f13e21 Mon Sep 17 00:00:00 2001 From: Marko Urh Date: Thu, 9 May 2024 22:31:17 +0200 Subject: [PATCH 1/5] feat: apache pulsar (standalone) and pulsar manager hosting support, including playground --- Aspire.sln | 24 ++ Directory.Packages.props | 1 + NuGet.config | 3 +- .../ApachePulsar.Api/ApachePulsar.Api.csproj | 20 ++ .../ApachePulsar.Api/PlayerEndpoints.cs | 19 ++ .../PlayerRegistrationExtensions.cs | 65 +++++ .../apache-pulsar/ApachePulsar.Api/Players.cs | 89 ++++++ .../apache-pulsar/ApachePulsar.Api/Program.cs | 31 ++ .../Properties/launchSettings.json | 14 + .../appsettings.Development.json | 8 + .../ApachePulsar.Api/appsettings.json | 9 + .../ApachePulsar.Api/requests.http | 4 + .../ApachePulsar.AppHost.csproj | 33 +++ .../Directory.Build.props | 8 + .../Directory.Build.targets | 9 + .../ApachePulsar.AppHost/Program.cs | 40 +++ .../Properties/launchSettings.json | 44 +++ .../application.properties | 184 ++++++++++++ .../appsettings.Development.json | 8 + .../ApachePulsar.AppHost/appsettings.json | 13 + .../ApachePulsar.AppHost/aspire-manifest.json | 104 +++++++ .../ApachePulsar.AppHost/bkvm.conf | 32 ++ .../Aspire.Hosting.Apache.Pulsar.csproj | 33 +++ .../PublicAPI.Shipped.txt | 1 + .../PublicAPI.Unshipped.txt | 23 ++ .../PulsarBuilderExtensions.cs | 108 +++++++ .../PulsarContainerImageTags.cs | 11 + .../PulsarManagerBuilderExtensions.cs | 170 +++++++++++ .../PulsarManagerContainerImageTags.cs | 50 ++++ .../PulsarManagerResource.cs | 33 +++ .../PulsarResource.cs | 34 +++ ...andalonePulsarCommandLineArgsAnnotation.cs | 16 + src/Shared/apache-pulsar-icon.png | Bin 0 -> 7810 bytes .../Apache/Pulsar/AddPulsarManagerTests.cs | 276 ++++++++++++++++++ .../Apache/Pulsar/AddPulsarTests.cs | 242 +++++++++++++++ .../Aspire.Hosting.Tests.csproj | 1 + 36 files changed, 1759 insertions(+), 1 deletion(-) create mode 100644 playground/apache-pulsar/ApachePulsar.Api/ApachePulsar.Api.csproj create mode 100644 playground/apache-pulsar/ApachePulsar.Api/PlayerEndpoints.cs create mode 100644 playground/apache-pulsar/ApachePulsar.Api/PlayerRegistrationExtensions.cs create mode 100644 playground/apache-pulsar/ApachePulsar.Api/Players.cs create mode 100644 playground/apache-pulsar/ApachePulsar.Api/Program.cs create mode 100644 playground/apache-pulsar/ApachePulsar.Api/Properties/launchSettings.json create mode 100644 playground/apache-pulsar/ApachePulsar.Api/appsettings.Development.json create mode 100644 playground/apache-pulsar/ApachePulsar.Api/appsettings.json create mode 100644 playground/apache-pulsar/ApachePulsar.Api/requests.http create mode 100644 playground/apache-pulsar/ApachePulsar.AppHost/ApachePulsar.AppHost.csproj create mode 100644 playground/apache-pulsar/ApachePulsar.AppHost/Directory.Build.props create mode 100644 playground/apache-pulsar/ApachePulsar.AppHost/Directory.Build.targets create mode 100644 playground/apache-pulsar/ApachePulsar.AppHost/Program.cs create mode 100644 playground/apache-pulsar/ApachePulsar.AppHost/Properties/launchSettings.json create mode 100644 playground/apache-pulsar/ApachePulsar.AppHost/application.properties create mode 100644 playground/apache-pulsar/ApachePulsar.AppHost/appsettings.Development.json create mode 100644 playground/apache-pulsar/ApachePulsar.AppHost/appsettings.json create mode 100644 playground/apache-pulsar/ApachePulsar.AppHost/aspire-manifest.json create mode 100644 playground/apache-pulsar/ApachePulsar.AppHost/bkvm.conf create mode 100644 src/Aspire.Hosting.Apache.Pulsar/Aspire.Hosting.Apache.Pulsar.csproj create mode 100644 src/Aspire.Hosting.Apache.Pulsar/PublicAPI.Shipped.txt create mode 100644 src/Aspire.Hosting.Apache.Pulsar/PublicAPI.Unshipped.txt create mode 100644 src/Aspire.Hosting.Apache.Pulsar/PulsarBuilderExtensions.cs create mode 100644 src/Aspire.Hosting.Apache.Pulsar/PulsarContainerImageTags.cs create mode 100644 src/Aspire.Hosting.Apache.Pulsar/PulsarManagerBuilderExtensions.cs create mode 100644 src/Aspire.Hosting.Apache.Pulsar/PulsarManagerContainerImageTags.cs create mode 100644 src/Aspire.Hosting.Apache.Pulsar/PulsarManagerResource.cs create mode 100644 src/Aspire.Hosting.Apache.Pulsar/PulsarResource.cs create mode 100644 src/Aspire.Hosting.Apache.Pulsar/StandalonePulsarCommandLineArgsAnnotation.cs create mode 100644 src/Shared/apache-pulsar-icon.png create mode 100644 tests/Aspire.Hosting.Tests/Apache/Pulsar/AddPulsarManagerTests.cs create mode 100644 tests/Aspire.Hosting.Tests/Apache/Pulsar/AddPulsarTests.cs diff --git a/Aspire.sln b/Aspire.sln index 75a4063471..19b4b1527a 100644 --- a/Aspire.sln +++ b/Aspire.sln @@ -461,6 +461,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Consumer", "playground\kafk EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Producer", "playground\kafka\Producer\Producer.csproj", "{FEE2F9B0-F32D-41B3-8917-0C13DE4F5953}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Hosting.Apache.Pulsar", "src\Aspire.Hosting.Apache.Pulsar\Aspire.Hosting.Apache.Pulsar.csproj", "{2BE6B31D-25E0-4641-BE98-BF40C1A43204}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "apache-pulsar", "apache-pulsar", "{3CF517C3-3F47-40F6-9330-10E0174A8800}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApachePulsar.AppHost", "playground\apache-pulsar\ApachePulsar.AppHost\ApachePulsar.AppHost.csproj", "{BC17A942-37AB-4E5A-8C7B-70846AE8934C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApachePulsar.Api", "playground\apache-pulsar\ApachePulsar.Api\ApachePulsar.Api.csproj", "{484C6267-F5D1-45B4-B458-B12F0EC90884}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1211,6 +1219,18 @@ Global {FEE2F9B0-F32D-41B3-8917-0C13DE4F5953}.Debug|Any CPU.Build.0 = Debug|Any CPU {FEE2F9B0-F32D-41B3-8917-0C13DE4F5953}.Release|Any CPU.ActiveCfg = Release|Any CPU {FEE2F9B0-F32D-41B3-8917-0C13DE4F5953}.Release|Any CPU.Build.0 = Release|Any CPU + {2BE6B31D-25E0-4641-BE98-BF40C1A43204}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2BE6B31D-25E0-4641-BE98-BF40C1A43204}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2BE6B31D-25E0-4641-BE98-BF40C1A43204}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2BE6B31D-25E0-4641-BE98-BF40C1A43204}.Release|Any CPU.Build.0 = Release|Any CPU + {BC17A942-37AB-4E5A-8C7B-70846AE8934C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BC17A942-37AB-4E5A-8C7B-70846AE8934C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BC17A942-37AB-4E5A-8C7B-70846AE8934C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BC17A942-37AB-4E5A-8C7B-70846AE8934C}.Release|Any CPU.Build.0 = Release|Any CPU + {484C6267-F5D1-45B4-B458-B12F0EC90884}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {484C6267-F5D1-45B4-B458-B12F0EC90884}.Debug|Any CPU.Build.0 = Debug|Any CPU + {484C6267-F5D1-45B4-B458-B12F0EC90884}.Release|Any CPU.ActiveCfg = Release|Any CPU + {484C6267-F5D1-45B4-B458-B12F0EC90884}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1431,6 +1451,10 @@ Global {A39389A0-E780-4B97-808B-DC95CF59B35C} = {920BB263-E68F-4FA2-93FC-2E385EEA405B} {7AA4C56C-3BB2-4FF0-BB03-F3F0D6A4FDAB} = {920BB263-E68F-4FA2-93FC-2E385EEA405B} {FEE2F9B0-F32D-41B3-8917-0C13DE4F5953} = {920BB263-E68F-4FA2-93FC-2E385EEA405B} + {2BE6B31D-25E0-4641-BE98-BF40C1A43204} = {B80354C7-BE58-43F6-8928-9F3A74AB7F47} + {3CF517C3-3F47-40F6-9330-10E0174A8800} = {D173887B-AF42-4576-B9C1-96B9E9B3D9C0} + {BC17A942-37AB-4E5A-8C7B-70846AE8934C} = {3CF517C3-3F47-40F6-9330-10E0174A8800} + {484C6267-F5D1-45B4-B458-B12F0EC90884} = {3CF517C3-3F47-40F6-9330-10E0174A8800} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6DCEDFEC-988E-4CB3-B45B-191EB5086E0C} diff --git a/Directory.Packages.props b/Directory.Packages.props index bf4616da84..1700b68267 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -150,6 +150,7 @@ + diff --git a/NuGet.config b/NuGet.config index 6d66afe4db..712ea3d06b 100644 --- a/NuGet.config +++ b/NuGet.config @@ -1,4 +1,4 @@ - + @@ -19,6 +19,7 @@ + diff --git a/playground/apache-pulsar/ApachePulsar.Api/ApachePulsar.Api.csproj b/playground/apache-pulsar/ApachePulsar.Api/ApachePulsar.Api.csproj new file mode 100644 index 0000000000..061846c0a6 --- /dev/null +++ b/playground/apache-pulsar/ApachePulsar.Api/ApachePulsar.Api.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + CS8002 + + + + + + + + + + + + + diff --git a/playground/apache-pulsar/ApachePulsar.Api/PlayerEndpoints.cs b/playground/apache-pulsar/ApachePulsar.Api/PlayerEndpoints.cs new file mode 100644 index 0000000000..6aaf19e9b9 --- /dev/null +++ b/playground/apache-pulsar/ApachePulsar.Api/PlayerEndpoints.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Mvc; + +public static class PlayerEndpoints +{ + public static void Map(WebApplication app) + { + app.MapPost("/start-match", async ([FromServices] PingPlayer startPlayer, CancellationToken cancellation) => + { + await startPlayer.SmackTheBall(cancellation); + return Results.Ok(); + }).WithOpenApi(); + + app.MapGet("/ping-player/received", ([FromServices] PingPlayer player) => Results.Ok((object?)player.ReceivedBalls)).WithOpenApi(); + app.MapGet("/pong-player/received", ([FromServices] PongPlayer player) => Results.Ok((object?)player.ReceivedBalls)).WithOpenApi(); + } +} diff --git a/playground/apache-pulsar/ApachePulsar.Api/PlayerRegistrationExtensions.cs b/playground/apache-pulsar/ApachePulsar.Api/PlayerRegistrationExtensions.cs new file mode 100644 index 0000000000..ff5f269d14 --- /dev/null +++ b/playground/apache-pulsar/ApachePulsar.Api/PlayerRegistrationExtensions.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using DotPulsar; +using DotPulsar.Abstractions; +using DotPulsar.Extensions; + +public static class PlayerTopicFactory +{ + public const string TopicPrefix = "persistent://public/default"; + + public static string GetTopic(string player) => $"{TopicPrefix}/{player}"; + + public static string GetOpponentTopic(string player) => player switch + { + nameof(PingPlayer) => GetTopic(nameof(PongPlayer)), + nameof(PongPlayer) => GetTopic(nameof(PingPlayer)), + _ => throw new ArgumentOutOfRangeException(nameof(player), player, null) + }; +} + +public static class PlayerRegistrationExtensions +{ + public static Type PlayerKey() where T : Player => typeof(T); + + public static string PlayerName() where T : Player => PlayerKey().Name; + + public static void Register(this IServiceCollection services, IPulsarClient client) + where T : Player + { + var player = PlayerName(); + var opponentField = PlayerTopicFactory.GetOpponentTopic(player); + var playerField = PlayerTopicFactory.GetTopic(player); + + // Produce player move (message) into opponent field (topic) + services.AddKeyedSingleton(player, (_, _) => client + .NewProducer(Schema.String) + .ProducerName(player) + .Topic(opponentField) + ); + + // Listen your field (topic) for player move (message) so you can respond back + services.AddKeyedSingleton(player, (_, _) => client + .NewConsumer(Schema.String) + .ConsumerName(player) + .Topic(playerField) + .SubscriptionName(player) + ); + + services.AddSingleton(Create); + services.AddHostedService(sp => sp.GetRequiredService()); + } + + public static T Create(this IServiceProvider sp) + where T : Player + { + var player = PlayerName(); + return (T)Activator.CreateInstance( + typeof(T), + sp.GetRequiredKeyedService>(player), + sp.GetRequiredKeyedService>(player), + sp.GetRequiredService().CreateLogger(player) + )!; + } +} diff --git a/playground/apache-pulsar/ApachePulsar.Api/Players.cs b/playground/apache-pulsar/ApachePulsar.Api/Players.cs new file mode 100644 index 0000000000..86f954c01f --- /dev/null +++ b/playground/apache-pulsar/ApachePulsar.Api/Players.cs @@ -0,0 +1,89 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using DotPulsar; +using DotPulsar.Abstractions; +using DotPulsar.Exceptions; +using DotPulsar.Extensions; + +/// +/// Ping player produces pings +/// He receives pongs from Pong player +/// +public sealed class PingPlayer(IConsumerBuilder consumerBuilder, IProducerBuilder producerBuilder, ILogger logger) + : Player(consumerBuilder, producerBuilder, logger) +{ + protected override string Move => "ping"; +} + +/// +/// Pong player produces pongs +/// He receives pings from Ping player +/// +public sealed class PongPlayer(IConsumerBuilder consumerBuilder, IProducerBuilder producerBuilder, ILogger logger) + : Player(consumerBuilder, producerBuilder, logger) +{ + protected override string Move => "pong"; +} + +public abstract class Player( + IConsumerBuilder consumerBuilder, + IProducerBuilder producerBuilder, + ILogger logger +) : BackgroundService +{ + protected abstract string Move { get; } + + private uint _receivedBalls; + public uint ReceivedBalls => _receivedBalls; + + private readonly Lazy> _producer = new(producerBuilder.Create); + + /// + /// Kick the ball (message) into opponent field (topic) + /// + public async Task SmackTheBall(CancellationToken cancellation) + { + logger.LogInformation("Responding: {message}", Move); + + await Task.Delay(700, cancellation); // add some sim... + + await _producer.Value.Send(new MessageMetadata(), Move, cancellation); + } + + /// + /// Observes your own field (topic) for players responses (messages) so you can respond back + /// + private async Task ReceiveBall(CancellationToken cancellation) + { + var consumer = consumerBuilder.Create(); + await foreach (var message in consumer.Messages(cancellation)) + { + logger.LogInformation("Received: {message}", message); + + Interlocked.Increment(ref _receivedBalls); + + await SmackTheBall(cancellation); + } + } + + // Listener + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + var attempt = 0; + while (true) + { + try + { + await ReceiveBall(stoppingToken); + } + catch (DotPulsarException e) + { + logger.LogWarning("Pulsar is still warming up, retry connection attempt {attempt}.", ++attempt); + logger.LogDebug(e, "Pulsar faulted"); + + await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken); + } + } + } +} diff --git a/playground/apache-pulsar/ApachePulsar.Api/Program.cs b/playground/apache-pulsar/ApachePulsar.Api/Program.cs new file mode 100644 index 0000000000..3453d3de15 --- /dev/null +++ b/playground/apache-pulsar/ApachePulsar.Api/Program.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using DotPulsar; + +var builder = WebApplication.CreateBuilder(args); + +var services = builder + .AddServiceDefaults() + .Services + .AddEndpointsApiExplorer() + .AddSwaggerGen(); + +var pulsarConnection = new Uri(builder.Configuration.GetConnectionString("Pulsar")!); +Console.WriteLine($"Pulsar connection string {pulsarConnection}"); + +var client = PulsarClient + .Builder() + .ServiceUrl(pulsarConnection) + .Build(); + +services.Register(client); +services.Register(client); + +var app = builder.Build(); + +app.UseSwagger().UseSwaggerUI(); + +PlayerEndpoints.Map(app); + +app.Run(); diff --git a/playground/apache-pulsar/ApachePulsar.Api/Properties/launchSettings.json b/playground/apache-pulsar/ApachePulsar.Api/Properties/launchSettings.json new file mode 100644 index 0000000000..fac3392471 --- /dev/null +++ b/playground/apache-pulsar/ApachePulsar.Api/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5226", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/playground/apache-pulsar/ApachePulsar.Api/appsettings.Development.json b/playground/apache-pulsar/ApachePulsar.Api/appsettings.Development.json new file mode 100644 index 0000000000..0c208ae918 --- /dev/null +++ b/playground/apache-pulsar/ApachePulsar.Api/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/playground/apache-pulsar/ApachePulsar.Api/appsettings.json b/playground/apache-pulsar/ApachePulsar.Api/appsettings.json new file mode 100644 index 0000000000..10f68b8c8b --- /dev/null +++ b/playground/apache-pulsar/ApachePulsar.Api/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/playground/apache-pulsar/ApachePulsar.Api/requests.http b/playground/apache-pulsar/ApachePulsar.Api/requests.http new file mode 100644 index 0000000000..4ddd7b5512 --- /dev/null +++ b/playground/apache-pulsar/ApachePulsar.Api/requests.http @@ -0,0 +1,4 @@ +@Url=http://localhost:5226 + +POST {{Url}}/start-match +Content-Type: application/json diff --git a/playground/apache-pulsar/ApachePulsar.AppHost/ApachePulsar.AppHost.csproj b/playground/apache-pulsar/ApachePulsar.AppHost/ApachePulsar.AppHost.csproj new file mode 100644 index 0000000000..0fac257e9e --- /dev/null +++ b/playground/apache-pulsar/ApachePulsar.AppHost/ApachePulsar.AppHost.csproj @@ -0,0 +1,33 @@ + + + + Exe + net8.0 + enable + enable + true + + + + + + + + + + + + + + + + + + Always + + + Always + + + + diff --git a/playground/apache-pulsar/ApachePulsar.AppHost/Directory.Build.props b/playground/apache-pulsar/ApachePulsar.AppHost/Directory.Build.props new file mode 100644 index 0000000000..d9b2c324ac --- /dev/null +++ b/playground/apache-pulsar/ApachePulsar.AppHost/Directory.Build.props @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/playground/apache-pulsar/ApachePulsar.AppHost/Directory.Build.targets b/playground/apache-pulsar/ApachePulsar.AppHost/Directory.Build.targets new file mode 100644 index 0000000000..466c472e8d --- /dev/null +++ b/playground/apache-pulsar/ApachePulsar.AppHost/Directory.Build.targets @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/playground/apache-pulsar/ApachePulsar.AppHost/Program.cs b/playground/apache-pulsar/ApachePulsar.AppHost/Program.cs new file mode 100644 index 0000000000..9f1fbe91d4 --- /dev/null +++ b/playground/apache-pulsar/ApachePulsar.AppHost/Program.cs @@ -0,0 +1,40 @@ +// 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 pulsarSuperUserUserName = builder.AddParameter("pulsar-manager-superuser-username"); +var pulsarSuperUserPassword = builder.AddParameter("pulsar-manager-superuser-password"); + +var pulsar = builder + .AddPulsar( + name: "pulsar", + servicePort: 8080, + brokerPort: 6650 + ) + .WithPulsarManager( + name: "pulsar-manager", + frontendPort: 9527, + backendPort: 7750, + configureContainer: c => c + .WithApplicationProperties() + .WithDefaultEnvironment("pulsar-playground") + // TODO: Uncomment after new pulsar manager release (>0.4.0) + //.WithDefaultSuperUser( + // userName: pulsarSuperUserUserName, + // password: pulsarSuperUserPassword + //) + ); + +builder.AddProject("api") + .WithExternalHttpEndpoints() + .WithReference(pulsar); + +// This project is only added in playground projects to support development/debugging +// of the dashboard. It is not required in end developer code. Comment out this code +// to test end developer dashboard launch experience. Refer to Directory.Build.props +// for the path to the dashboard binary (defaults to the Aspire.Dashboard bin output +// in the artifacts dir). +builder.AddProject(KnownResourceNames.AspireDashboard); + +builder.Build().Run(); diff --git a/playground/apache-pulsar/ApachePulsar.AppHost/Properties/launchSettings.json b/playground/apache-pulsar/ApachePulsar.AppHost/Properties/launchSettings.json new file mode 100644 index 0000000000..e20fa9f175 --- /dev/null +++ b/playground/apache-pulsar/ApachePulsar.AppHost/Properties/launchSettings.json @@ -0,0 +1,44 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:17023;http://localhost:15261", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21013", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22062", + "DOTNET_ASPIRE_SHOW_DASHBOARD_RESOURCES": "true" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:15261", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19174", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20135", + "DOTNET_ASPIRE_SHOW_DASHBOARD_RESOURCES": "true", + "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true" + } + }, + "generate-manifest": { + "commandName": "Project", + "launchBrowser": false, + "dotnetRunMessages": true, + "commandLineArgs": "--publisher manifest --output-path aspire-manifest.json", + "applicationUrl": "http://localhost:15888", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19174" + } + } + } +} diff --git a/playground/apache-pulsar/ApachePulsar.AppHost/application.properties b/playground/apache-pulsar/ApachePulsar.AppHost/application.properties new file mode 100644 index 0000000000..50e404c5dc --- /dev/null +++ b/playground/apache-pulsar/ApachePulsar.AppHost/application.properties @@ -0,0 +1,184 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +spring.cloud.refresh.refreshable=none +server.port=7750 + +# configuration log +logging.path= +logging.file=pulsar-manager.log + +# DEBUG print execute sql +logging.level.org.apache=DEBUG + +mybatis.type-aliases-package=org.apache.pulsar.manager + +# database connection + +# SQLLite +#spring.datasource.driver-class-name=org.sqlite.JDBC +#spring.datasource.url=jdbc:sqlite:pulsar_manager.db +#spring.datasource.initialization-mode=always +#spring.datasource.schema=classpath:/META-INF/sql/sqlite-schema.sql +#spring.datasource.username= +#spring.datasource.password= + +#HerdDB JDBC Driver +spring.datasource.driver-class-name=herddb.jdbc.Driver +# HerdDB - local in memory-only +#spring.datasource.url=jdbc:herddb:local +# HerdDB - start embedded server, data persisted on local disk (directory 'dbdata'), listening on localhost:7000 +spring.datasource.url=jdbc:herddb:server:localhost:7000?server.start=true&server.base.dir=dbdata +# HerdDB - start embedded server 'diskless-cluster' mode, WAL and Data persisted on Bookies, Metadata on ZooKeeper in '/herd', listening on localhost:7000 +#spring.datasource.url=jdbc:herddb:zookeeper:localhost:2181?server.start=true&server.base.dir=dbdata&server.mode=diskless-cluster&server.node.id=localhost +# HerdDB - connect to standalone server at localhost:7000 +#spring.datasource.url=jdbc:herddb:server:localhost:7000 +# HerdDB - connect to cluster, uses ZooKeeper for service discovery +#spring.datasource.url=jdbc:herddb:zookeeper:localhost:2181/herd + + +spring.datasource.schema=classpath:/META-INF/sql/herddb-schema.sql +spring.datasource.username=sa +spring.datasource.password=hdb +spring.datasource.initialization-mode=always + +# postgresql configuration +#spring.datasource.driver-class-name=org.postgresql.Driver +#spring.datasource.url=jdbc:postgresql://127.0.0.1:5432/pulsar_manager +#spring.datasource.username=postgres +#spring.datasource.password=postgres + +# zuul config +# https://cloud.spring.io/spring-cloud-static/Dalston.SR5/multi/multi__router_and_filter_zuul.html +# By Default Zuul adds Authorization to be dropped headers list. Below we are manually setting it +zuul.sensitive-headers=Cookie,Set-Cookie +zuul.routes.admin.path=/admin/** +zuul.routes.admin.url=http://localhost:8080/admin/ +zuul.routes.lookup.path=/lookup/** +zuul.routes.lookup.url=http://localhost:8080/lookup/ + +# pagehelper plugin +#pagehelper.helperDialect=sqlite +# force 'mysql' for HerdDB, comment out for postgresql +pagehelper.helperDialect=mysql + +backend.directRequestBroker=true +backend.directRequestHost=http://localhost:8080 +backend.jwt.token= +backend.broker.pulsarAdmin.authPlugin= +backend.broker.pulsarAdmin.authParams= +backend.broker.pulsarAdmin.tlsAllowInsecureConnection=false +backend.broker.pulsarAdmin.tlsTrustCertsFilePath= +backend.broker.pulsarAdmin.tlsEnableHostnameVerification=false + +jwt.secret=dab1c8ba-b01b-11e9-b384-186590e06885 +jwt.sessionTime=2592000 +# If user.management.enable is true, the following account and password will no longer be valid. +pulsar-manager.account=pulsar +pulsar-manager.password=pulsar +# If true, the database is used for user management +user.management.enable=false + +# Optional -> SECRET, PRIVATE, default -> PRIVATE, empty -> disable auth +# SECRET mode -> bin/pulsar tokens create --secret-key file:///path/to/my-secret.key --subject test-user +# PRIVATE mode -> bin/pulsar tokens create --private-key file:///path/to/my-private.key --subject test-user +# Detail information: http://pulsar.apache.org/docs/en/security-token-admin/ +jwt.broker.token.mode= +jwt.broker.secret.key=file:///path/broker-secret.key +jwt.broker.public.key=file:///path/pulsar/broker-public.key +jwt.broker.private.key=file:///path/broker-private.key + +# bookie +bookie.host=http://localhost:8050 +bookie.enable=false + +redirect.scheme=http +redirect.host=localhost +redirect.port=9527 + +# Stats interval +# millisecond +insert.stats.interval=30000 +# millisecond +clear.stats.interval=300000 +init.delay.interval=0 + +# cluster data reload +cluster.cache.reload.interval.ms=60000 + +user.access.token.expire=604800 + +# thymeleaf configuration for third login. +spring.thymeleaf.cache=false +spring.thymeleaf.prefix=classpath:/templates/ +spring.thymeleaf.check-template-location=true +spring.thymeleaf.suffix=.html +spring.thymeleaf.encoding=UTF-8 +spring.thymeleaf.servlet.content-type=text/html +spring.thymeleaf.mode=HTML5 + +# default environment configuration +default.environment.name= +default.environment.service_url= +default.environment.bookie_url= +# enable tls encryption +# keytool -import -alias test-keystore -keystore ca-certs -file certs/ca.cert.pem +tls.enabled=false +tls.keystore=keystore-file-path +tls.keystore.password=keystore-password +tls.hostname.verifier=false +tls.pulsar.admin.ca-certs=ca-client-path + +# support peek message, default false +pulsar.peek.message=false + +# swagger configuration +swagger.enabled=true + +# casdoor configuration +casdoor.endpoint = http://localhost:8000 +casdoor.clientId = 6ba06c1e1a30929fdda7 +casdoor.clientSecret = df92bbf913225ebbae9af7ba8d41fe19507eb079 +casdoor.certificate=\ +-----BEGIN CERTIFICATE-----\n\ +MIIE+TCCAuGgAwIBAgIDAeJAMA0GCSqGSIb3DQEBCwUAMDYxHTAbBgNVBAoTFENh\n\ +c2Rvb3IgT3JnYW5pemF0aW9uMRUwEwYDVQQDEwxDYXNkb29yIENlcnQwHhcNMjEx\n\ +MDE1MDgxMTUyWhcNNDExMDE1MDgxMTUyWjA2MR0wGwYDVQQKExRDYXNkb29yIE9y\n\ +Z2FuaXphdGlvbjEVMBMGA1UEAxMMQ2FzZG9vciBDZXJ0MIICIjANBgkqhkiG9w0B\n\ +AQEFAAOCAg8AMIICCgKCAgEAsInpb5E1/ym0f1RfSDSSE8IR7y+lw+RJjI74e5ej\n\ +rq4b8zMYk7HeHCyZr/hmNEwEVXnhXu1P0mBeQ5ypp/QGo8vgEmjAETNmzkI1NjOQ\n\ +CjCYwUrasO/f/MnI1C0j13vx6mV1kHZjSrKsMhYY1vaxTEP3+VB8Hjg3MHFWrb07\n\ +uvFMCJe5W8+0rKErZCKTR8+9VB3janeBz//zQePFVh79bFZate/hLirPK0Go9P1g\n\ +OvwIoC1A3sarHTP4Qm/LQRt0rHqZFybdySpyWAQvhNaDFE7mTstRSBb/wUjNCUBD\n\ +PTSLVjC04WllSf6Nkfx0Z7KvmbPstSj+btvcqsvRAGtvdsB9h62Kptjs1Yn7GAuo\n\ +I3qt/4zoKbiURYxkQJXIvwCQsEftUuk5ew5zuPSlDRLoLByQTLbx0JqLAFNfW3g/\n\ +pzSDjgd/60d6HTmvbZni4SmjdyFhXCDb1Kn7N+xTojnfaNkwep2REV+RMc0fx4Gu\n\ +hRsnLsmkmUDeyIZ9aBL9oj11YEQfM2JZEq+RVtUx+wB4y8K/tD1bcY+IfnG5rBpw\n\ +IDpS262boq4SRSvb3Z7bB0w4ZxvOfJ/1VLoRftjPbLIf0bhfr/AeZMHpIKOXvfz4\n\ +yE+hqzi68wdF0VR9xYc/RbSAf7323OsjYnjjEgInUtRohnRgCpjIk/Mt2Kt84Kb0\n\ +wn8CAwEAAaMQMA4wDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAgEAn2lf\n\ +DKkLX+F1vKRO/5gJ+Plr8P5NKuQkmwH97b8CS2gS1phDyNgIc4/LSdzuf4Awe6ve\n\ +C06lVdWSIis8UPUPdjmT2uMPSNjwLxG3QsrimMURNwFlLTfRem/heJe0Zgur9J1M\n\ +8haawdSdJjH2RgmFoDeE2r8NVRfhbR8KnCO1ddTJKuS1N0/irHz21W4jt4rxzCvl\n\ +2nR42Fybap3O/g2JXMhNNROwZmNjgpsF7XVENCSuFO1jTywLaqjuXCg54IL7XVLG\n\ +omKNNNcc8h1FCeKj/nnbGMhodnFWKDTsJcbNmcOPNHo6ixzqMy/Hqc+mWYv7maAG\n\ +Jtevs3qgMZ8F9Qzr3HpUc6R3ZYYWDY/xxPisuKftOPZgtH979XC4mdf0WPnOBLqL\n\ +2DJ1zaBmjiGJolvb7XNVKcUfDXYw85ZTZQ5b9clI4e+6bmyWqQItlwt+Ati/uFEV\n\ +XzCj70B4lALX6xau1kLEpV9O1GERizYRz5P9NJNA7KoO5AVMp9w0DQTkt+LbXnZE\n\ +HHnWKy8xHQKZF9sR7YBPGLs/Ac6tviv5Ua15OgJ/8dLRZ/veyFfGo2yZsI+hKVU5\n\ +nCCJHBcAyFnm1hdvdwEdH33jDBjNB6ciotJZrf/3VYaIWSalADosHAgMWfXuWP+h\n\ +8XKXmzlxuHbTMQYtZPDgspS5aK+S4Q9wb8RRAYo=\n\ +-----END CERTIFICATE-----\n\ +casdoor.organizationName = pulsar +casdoor.applicationName = app-pulsar diff --git a/playground/apache-pulsar/ApachePulsar.AppHost/appsettings.Development.json b/playground/apache-pulsar/ApachePulsar.AppHost/appsettings.Development.json new file mode 100644 index 0000000000..0c208ae918 --- /dev/null +++ b/playground/apache-pulsar/ApachePulsar.AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/playground/apache-pulsar/ApachePulsar.AppHost/appsettings.json b/playground/apache-pulsar/ApachePulsar.AppHost/appsettings.json new file mode 100644 index 0000000000..bc4c6c364d --- /dev/null +++ b/playground/apache-pulsar/ApachePulsar.AppHost/appsettings.json @@ -0,0 +1,13 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + }, + "Parameters": { + "pulsar-manager-superuser-username": "admin", + "pulsar-manager-superuser-password": "apachepulsar" + } +} diff --git a/playground/apache-pulsar/ApachePulsar.AppHost/aspire-manifest.json b/playground/apache-pulsar/ApachePulsar.AppHost/aspire-manifest.json new file mode 100644 index 0000000000..da57ee9c8b --- /dev/null +++ b/playground/apache-pulsar/ApachePulsar.AppHost/aspire-manifest.json @@ -0,0 +1,104 @@ +{ + "resources": { + "pulsar-manager-superuser-username": { + "type": "parameter.v0", + "value": "{pulsar-manager-superuser-username.inputs.value}", + "inputs": { + "value": { + "type": "string" + } + } + }, + "pulsar-manager-superuser-password": { + "type": "parameter.v0", + "value": "{pulsar-manager-superuser-password.inputs.value}", + "inputs": { + "value": { + "type": "string" + } + } + }, + "pulsar": { + "type": "container.v0", + "connectionString": "{pulsar.bindings.broker.url}", + "image": "docker.io/apachepulsar/pulsar:3.2.0", + "entrypoint": "/bin/bash", + "args": [ + "-c", + "bin/apply-config-from-env.py conf/standalone.conf \u0026\u0026 bin/pulsar standalone" + ], + "bindings": { + "service": { + "scheme": "http", + "protocol": "tcp", + "transport": "http", + "targetPort": 8080 + }, + "broker": { + "scheme": "pulsar", + "protocol": "tcp", + "transport": "tcp", + "targetPort": 6650 + } + } + }, + "pulsar-manager": { + "type": "container.v0", + "image": "docker.io/apachepulsar/pulsar-manager:v0.4.0", + "bindMounts": [ + { + "source": "application.properties", + "target": "/pulsar-manager/pulsar-manager/application.properties", + "readOnly": false + } + ], + "env": { + "services__pulsar__service__0": "{pulsar.bindings.service.url}", + "services__pulsar__broker__0": "{pulsar.bindings.broker.url}", + "SPRING_CONFIGURATION_FILE": "/pulsar-manager/pulsar-manager/application.properties", + "DEFAULT_ENVIRONMENT_NAME": "pulsar-playground", + "DEFAULT_ENVIRONMENT_BOOKIE_URL": "{pulsar.bindings.broker.url}", + "DEFAULT_ENVIRONMENT_SERVICE_URL": "{pulsar.bindings.service.url}" + }, + "bindings": { + "frontend": { + "scheme": "http", + "protocol": "tcp", + "transport": "http", + "targetPort": 9527 + }, + "backend": { + "scheme": "http", + "protocol": "tcp", + "transport": "http", + "targetPort": 7750 + } + } + }, + "api": { + "type": "project.v0", + "path": "../ApachePulsar.Api/ApachePulsar.Api.csproj", + "env": { + "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true", + "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true", + "OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory", + "ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true", + "ConnectionStrings__pulsar": "{pulsar.connectionString}" + }, + "bindings": { + "http": { + "scheme": "http", + "protocol": "tcp", + "transport": "http", + "external": true + }, + "https": { + "scheme": "https", + "protocol": "tcp", + "transport": "http", + "external": true + } + } + } + } +} \ No newline at end of file diff --git a/playground/apache-pulsar/ApachePulsar.AppHost/bkvm.conf b/playground/apache-pulsar/ApachePulsar.AppHost/bkvm.conf new file mode 100644 index 0000000000..a5d834fb80 --- /dev/null +++ b/playground/apache-pulsar/ApachePulsar.AppHost/bkvm.conf @@ -0,0 +1,32 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Change this to true in order to start BKVM +bkvm.enabled=true + +# BookKeeper Connection +# Default value zk+null://127.0.0.1:2181/ledgers works for Pulsar Standalone +metadataServiceUri=zk+null://127.0.0.1:2181/ledgers + +# Refresh BK metadata at boot. +# BK metadata are not scanned automatically in BKVM, you have to request it from the UI +metdata.refreshAtBoot=true + +# HerdDB database connection, not to be changed if you are running embedded HerdDB in Pulsar Manager +# If you are using PostGRE SQL you will have to change this configuration +# We want to use the HerdDB database started by PulsarManager itself, by default BKVM wants to start its one database +jdbc.url=jdbc:herddb:localhost:7000?server.mode=standalone&server.start=false +jdbc.startDatabase=false +server.mode=standalone +server.start=false diff --git a/src/Aspire.Hosting.Apache.Pulsar/Aspire.Hosting.Apache.Pulsar.csproj b/src/Aspire.Hosting.Apache.Pulsar/Aspire.Hosting.Apache.Pulsar.csproj new file mode 100644 index 0000000000..eb7a9ee610 --- /dev/null +++ b/src/Aspire.Hosting.Apache.Pulsar/Aspire.Hosting.Apache.Pulsar.csproj @@ -0,0 +1,33 @@ + + + + $(NetCurrent) + true + aspire hosting apache pulsar + Apache Pulsar® support for .NET Aspire. + $(SharedDir)apache-pulsar-icon.png + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Aspire.Hosting.Apache.Pulsar/PublicAPI.Shipped.txt b/src/Aspire.Hosting.Apache.Pulsar/PublicAPI.Shipped.txt new file mode 100644 index 0000000000..7dc5c58110 --- /dev/null +++ b/src/Aspire.Hosting.Apache.Pulsar/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/Aspire.Hosting.Apache.Pulsar/PublicAPI.Unshipped.txt b/src/Aspire.Hosting.Apache.Pulsar/PublicAPI.Unshipped.txt new file mode 100644 index 0000000000..2452ca40fb --- /dev/null +++ b/src/Aspire.Hosting.Apache.Pulsar/PublicAPI.Unshipped.txt @@ -0,0 +1,23 @@ +#nullable enable +Aspire.Hosting.ApplicationModel.PulsarManagerResource +Aspire.Hosting.ApplicationModel.PulsarManagerResource.BackendEndpoint.get -> Aspire.Hosting.ApplicationModel.EndpointReference! +Aspire.Hosting.ApplicationModel.PulsarManagerResource.FrontendEndpoint.get -> Aspire.Hosting.ApplicationModel.EndpointReference! +Aspire.Hosting.ApplicationModel.PulsarManagerResource.PulsarManagerResource(string! name) -> void +Aspire.Hosting.ApplicationModel.PulsarResource +Aspire.Hosting.ApplicationModel.PulsarResource.BrokerEndpoint.get -> Aspire.Hosting.ApplicationModel.EndpointReference! +Aspire.Hosting.ApplicationModel.PulsarResource.ConnectionStringExpression.get -> Aspire.Hosting.ApplicationModel.ReferenceExpression! +Aspire.Hosting.ApplicationModel.PulsarResource.PulsarResource(string! name) -> void +Aspire.Hosting.ApplicationModel.PulsarResource.ServiceEndpoint.get -> Aspire.Hosting.ApplicationModel.EndpointReference! +Aspire.Hosting.PulsarBuilderExtensions +Aspire.Hosting.PulsarManagerBuilderExtensions +static Aspire.Hosting.PulsarBuilderExtensions.AddPulsar(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, int? servicePort = null, int? brokerPort = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static Aspire.Hosting.PulsarBuilderExtensions.AsStandalone(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static Aspire.Hosting.PulsarBuilderExtensions.WithConfigBindMount(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! source, bool isReadOnly = false) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static Aspire.Hosting.PulsarBuilderExtensions.WithConfigVolume(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string? name = null, bool isReadOnly = false) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static Aspire.Hosting.PulsarBuilderExtensions.WithDataBindMount(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! source, bool isReadOnly = false) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static Aspire.Hosting.PulsarBuilderExtensions.WithDataVolume(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string? name = null, bool isReadOnly = false) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static Aspire.Hosting.PulsarManagerBuilderExtensions.WithApplicationProperties(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! source = "application.properties", bool isReadOnly = false) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static Aspire.Hosting.PulsarManagerBuilderExtensions.WithBookKeeperVisualManager(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! source = "bkvm.conf", bool isReadOnly = false) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static Aspire.Hosting.PulsarManagerBuilderExtensions.WithDefaultEnvironment(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string? name = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static Aspire.Hosting.PulsarManagerBuilderExtensions.WithDefaultSuperUser(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, Aspire.Hosting.ApplicationModel.IResourceBuilder? userName = null, Aspire.Hosting.ApplicationModel.IResourceBuilder? email = null, Aspire.Hosting.ApplicationModel.IResourceBuilder? password = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static Aspire.Hosting.PulsarManagerBuilderExtensions.WithPulsarManager(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string? name = null, int? frontendPort = null, int? backendPort = null, System.Action!>? configureContainer = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! diff --git a/src/Aspire.Hosting.Apache.Pulsar/PulsarBuilderExtensions.cs b/src/Aspire.Hosting.Apache.Pulsar/PulsarBuilderExtensions.cs new file mode 100644 index 0000000000..7e9494f9aa --- /dev/null +++ b/src/Aspire.Hosting.Apache.Pulsar/PulsarBuilderExtensions.cs @@ -0,0 +1,108 @@ +// 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.Apache.Pulsar; +using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Utils; + +namespace Aspire.Hosting; + +/// +/// Provides extension methods for adding Pulsar resource to a . +/// +public static class PulsarBuilderExtensions +{ + /// + /// Adds Pulsar container to the application module + /// Runs in standalone mode by default. + /// + /// + /// The default image and tag are "apachepulsar/pulsar" and "3.2.0". + /// + /// The . + /// The name of the resource. This name will be used as the endpoint name when referenced in dependency. + /// The service port that the underlying container is bound to when running locally. + /// The broker port that the underlying container is bound to when running locally. + /// A reference to the . + public static IResourceBuilder AddPulsar( + this IDistributedApplicationBuilder builder, + string name, + int? servicePort = null, + int? brokerPort = null + ) + { + var pulsar = new PulsarResource(name); + return builder.AddResource(pulsar) + .WithImage(PulsarContainerImageTags.Image, PulsarContainerImageTags.Tag) + .WithImageRegistry(PulsarContainerImageTags.Registry) + .WithEndpoint(port: servicePort, targetPort: 8080, name: PulsarResource.ServiceEndpointName, scheme: "http") + .WithEndpoint(port: brokerPort, targetPort: 6650, name: PulsarResource.BrokerEndpointName, scheme: "pulsar") + .WithEntrypoint("/bin/bash") + .AsStandalone(); + } + + /// + /// Configures Pulsar to run in standalone mode + /// + /// The . + /// A reference to the . + public static IResourceBuilder AsStandalone( + this IResourceBuilder builder + ) => builder + .WithAnnotation(new StandalonePulsarCommandLineArgsAnnotation(), ResourceAnnotationMutationBehavior.Replace); + + /// + /// Adds a named volume for the data folder to Pulsar container resource. + /// + /// The resource builder. + /// The name of the volume. Defaults to an auto-generated name based on the application and resource names. + /// A flag that indicates if this is a read-only volume. + /// The . + public static IResourceBuilder WithDataVolume( + this IResourceBuilder builder, + string? name = null, + bool isReadOnly = false + ) => builder + .WithVolume(name ?? VolumeNameGenerator.CreateVolumeName(builder, "pulsardata"), "/pulsar/data", isReadOnly); + + /// + /// Adds a named volume for the config folder to Pulsar container resource. + /// + /// The resource builder. + /// The name of the volume. Defaults to an auto-generated name based on the application and resource names. + /// A flag that indicates if this is a read-only volume. + /// The . + public static IResourceBuilder WithConfigVolume( + this IResourceBuilder builder, + string? name = null, + bool isReadOnly = false + ) => builder + .WithVolume(name ?? VolumeNameGenerator.CreateVolumeName(builder, "pulsarconf"), "/pulsar/conf", isReadOnly); + + /// + /// Adds a bind mount for the data folder to Pulsar container resource. + /// + /// The resource builder. + /// The source directory on the host to mount into the container. + /// A flag that indicates if this is a read-only mount. + /// The . + public static IResourceBuilder WithDataBindMount( + this IResourceBuilder builder, + string source, + bool isReadOnly = false + ) => builder + .WithBindMount(source, "/pulsar/data", isReadOnly); + + /// + /// Adds a bind mount for the config folder to Pulsar container resource. + /// + /// The resource builder. + /// The source directory on the host to mount into the container. + /// A flag that indicates if this is a read-only mount. + /// The . + public static IResourceBuilder WithConfigBindMount( + this IResourceBuilder builder, + string source, + bool isReadOnly = false + ) => builder.WithBindMount(source, "/pulsar/conf", isReadOnly); +} diff --git a/src/Aspire.Hosting.Apache.Pulsar/PulsarContainerImageTags.cs b/src/Aspire.Hosting.Apache.Pulsar/PulsarContainerImageTags.cs new file mode 100644 index 0000000000..d375aa1854 --- /dev/null +++ b/src/Aspire.Hosting.Apache.Pulsar/PulsarContainerImageTags.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Hosting.Apache.Pulsar; + +internal static class PulsarContainerImageTags +{ + public const string Registry = "docker.io"; + public const string Image = "apachepulsar/pulsar"; + public const string Tag = "3.2.0"; +} diff --git a/src/Aspire.Hosting.Apache.Pulsar/PulsarManagerBuilderExtensions.cs b/src/Aspire.Hosting.Apache.Pulsar/PulsarManagerBuilderExtensions.cs new file mode 100644 index 0000000000..3f6ad5ce45 --- /dev/null +++ b/src/Aspire.Hosting.Apache.Pulsar/PulsarManagerBuilderExtensions.cs @@ -0,0 +1,170 @@ +// 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.Apache.Pulsar; +using Aspire.Hosting.ApplicationModel; + +namespace Aspire.Hosting; + +/// +/// Provides extension methods for adding Pulsar Manager resource to a . +/// +public static class PulsarManagerBuilderExtensions +{ + /// + /// Configures a container resource for Pulsar Manager which is pre-configured to + /// connect to the that this method is used on. + /// + /// + /// The default image and tag are "apachepulsar/pulsar-manager" and "0.4.0". + /// + /// The for the . + /// The name of the resource. This name will be used as the endpoint name when referenced in dependency. + /// The manager frontend port that the underlying container is bound to when running locally. + /// The manager backend port that the underlying container is bound to when running locally. + /// Configuration callback for Pulsar Manager container resource. + /// A reference to the for the . + public static IResourceBuilder WithPulsarManager( + this IResourceBuilder builder, + string? name = null, + int? frontendPort = null, + int? backendPort = null, + Action>? configureContainer = null + ) + { + var applicationBuilder = builder.ApplicationBuilder; + + if (applicationBuilder.Resources.OfType().SingleOrDefault() is not null) + { + return builder; + } + + name ??= $"{builder.Resource.Name}-manager"; + + var pulsarManager = new PulsarManagerResource(name); + + var pulsarManagerBuilder = builder.ApplicationBuilder.AddResource(pulsarManager) + .WithImage(PulsarManagerContainerImageTags.Image, PulsarManagerContainerImageTags.Tag) + .WithImageRegistry(PulsarManagerContainerImageTags.Registry) + .WithEndpoint(port: frontendPort, targetPort: 9527, name: PulsarManagerResource.FrontendEndpointName, scheme: "http") + .WithEndpoint(port: backendPort, targetPort: 7750, name: PulsarManagerResource.BackendEndpointName, scheme: "http"); + + pulsarManagerBuilder + .WithReference(builder.GetEndpoint(PulsarResource.ServiceEndpointName)) + .WithReference(builder.GetEndpoint(PulsarResource.BrokerEndpointName)); + + pulsarManagerBuilder + .WithEnvironment("SPRING_CONFIGURATION_FILE", "/pulsar-manager/pulsar-manager/application.properties"); + + configureContainer?.Invoke(pulsarManagerBuilder); + + return builder; + } + + /// + /// Adds a bind mount for the application properties file to a Pulsar Manager. + /// + /// The for the . + /// The source directory on the host to mount into the container. + /// A flag that indicates if this is a read-only mount. + /// A reference to the for the . + public static IResourceBuilder WithApplicationProperties( + this IResourceBuilder builder, + string source = "application.properties", + bool isReadOnly = false + ) => builder + .WithBindMount(source, "/pulsar-manager/pulsar-manager/application.properties", isReadOnly); + + /// + /// Adds a bind mount for the bookkeeper visual manager configuration file to a Pulsar Manager. + /// + /// The for the . + /// The source directory on the host to mount into the container. + /// A flag that indicates if this is a read-only mount. + /// A reference to the for the . + public static IResourceBuilder WithBookKeeperVisualManager( + this IResourceBuilder builder, + string source = "bkvm.conf", + bool isReadOnly = false + ) => builder + .WithBindMount(source, "/pulsar-manager/pulsar-manager/bkvm.conf", isReadOnly); + + /// + /// Seeds default super-user to a Pulsar Manager. + /// + /// + /// This method only supports the Pulsar Manager container image and tags including and above v0.4.1
+ /// Calling this method on a resource configured with an unrecognized image registry, name, or tag will result in a being thrown. + ///
+ /// The for the . + /// The parameter used to provide the username for the Pulsar Manager default superuser. If a default value will be used. + /// The parameter used to provide the email for the Pulsar Manager default superuser. If a default value will be used. + /// The parameter used to provide the password for the Pulsar Manager default superuser. If a random password will be generated. + /// A reference to the for the . + public static IResourceBuilder WithDefaultSuperUser( + this IResourceBuilder builder, + IResourceBuilder? userName = null, + IResourceBuilder? email = null, + IResourceBuilder? password = null + ) + { + var appBuilder = builder.ApplicationBuilder; + var resourceName = builder.Resource.Name; + + var emailResource = email is not null + ? ReferenceExpression.Create($"{email.Resource}") + : ReferenceExpression.Create($"pulsar@manager.com"); + + var userNameResource = userName is not null + ? ReferenceExpression.Create($"{userName.Resource}") + : ReferenceExpression.Create($"pulsar"); + + var passwordResource = password is not null + ? password.Resource + : ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter( + appBuilder, + $"{resourceName}-password", + special: false + ); + + var imageAnnotation = builder.Resource.Annotations.OfType().Last(); + + var supportsSuperuserEnv = PulsarManagerContainerImageTags.SupportsDefaultSuperUserEnvVars(imageAnnotation); + if (!supportsSuperuserEnv) + { + throw new DistributedApplicationException( + $"Cannot configure the Pulsar Manager resource '{builder.Resource.Name}' to " + + $"enable the management plugin as it uses an unrecognized container " + + $"image registry, name, or tag." + ); + } + + builder.WithEnvironment(context => + { + context.EnvironmentVariables["DEFAULT_SUPERUSER_ENABLED"] = "true"; + context.EnvironmentVariables["DEFAULT_SUPERUSER_EMAIL"] = emailResource; + context.EnvironmentVariables["DEFAULT_SUPERUSER_NAME"] = userNameResource; + context.EnvironmentVariables["DEFAULT_SUPERUSER_PASSWORD"] = passwordResource; + }); + + return builder; + } + + /// + /// Adds default environment to Pulsar Manager configured with pre-configured Pulsar service and bookie (broker) endpoints. + /// + /// The for the . + /// Default environment name. + /// A reference to the for the . + public static IResourceBuilder WithDefaultEnvironment( + this IResourceBuilder builder, + string? name = null + ) => builder + .WithEnvironment(context => + { + var pulsar = builder.ApplicationBuilder.Resources.OfType().Single(); + context.EnvironmentVariables["DEFAULT_ENVIRONMENT_NAME"] = name ?? "default"; + context.EnvironmentVariables["DEFAULT_ENVIRONMENT_BOOKIE_URL"] = pulsar.BrokerEndpoint; + context.EnvironmentVariables["DEFAULT_ENVIRONMENT_SERVICE_URL"] = pulsar.ServiceEndpoint; + }); +} diff --git a/src/Aspire.Hosting.Apache.Pulsar/PulsarManagerContainerImageTags.cs b/src/Aspire.Hosting.Apache.Pulsar/PulsarManagerContainerImageTags.cs new file mode 100644 index 0000000000..7f25008232 --- /dev/null +++ b/src/Aspire.Hosting.Apache.Pulsar/PulsarManagerContainerImageTags.cs @@ -0,0 +1,50 @@ +// 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.Apache.Pulsar; + +internal static class PulsarManagerContainerImageTags +{ + public const string Registry = "docker.io"; + public const string Image = "apachepulsar/pulsar-manager"; + public const string Tag = "v0.4.0"; + + // TODO: Bump after release of Pulsar Manager + // Below PRs add value: + // https://github.com/apache/pulsar-manager/pull/565 - adds super-user via env vars + // https://github.com/apache/pulsar-manager/pull/564 - fixes duplicate super-user seed + + private static readonly Version s_versionThresholdNotSupportingDefaultSuperUserViaEnvVars = new(0, 4, 0); + + /// + /// Calculates if provided image supports seeding default super-user into Pulsar Manager + /// + /// + /// Support for seeding default super-user in Pulsar Manager has been added in > v0.4.0 + /// + /// The container image annotation. + /// True if supported, false if not + internal static bool SupportsDefaultSuperUserEnvVars(ContainerImageAnnotation annotation) + { + if (annotation.Image != Image) + { + return false; + } + if (string.IsNullOrWhiteSpace(annotation.Tag)) + { + return false; + } + + var versionParts = annotation.Tag + .TrimStart('v') + .ToCharArray() + .Where(c => c != '.') + .ToArray(); + + Version version = new(versionParts[0], versionParts[1], versionParts[2]); + + return version > s_versionThresholdNotSupportingDefaultSuperUserViaEnvVars; + } +} diff --git a/src/Aspire.Hosting.Apache.Pulsar/PulsarManagerResource.cs b/src/Aspire.Hosting.Apache.Pulsar/PulsarManagerResource.cs new file mode 100644 index 0000000000..24373cddb8 --- /dev/null +++ b/src/Aspire.Hosting.Apache.Pulsar/PulsarManagerResource.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Hosting.ApplicationModel; + +/// +/// A resource that represents Pulsar Manager resource +/// +public class PulsarManagerResource : ContainerResource +{ + internal const string FrontendEndpointName = "frontend"; + internal const string BackendEndpointName = "backend"; + + /// + /// Initializes a new instance of the class. + /// + /// The name of the resource. + public PulsarManagerResource(string name) : base(name) + { + BackendEndpoint = new(this, FrontendEndpointName); + FrontendEndpoint = new(this, FrontendEndpointName); + } + + /// + /// Gets the frontend endpoint of Pulsar Manager + /// + public EndpointReference FrontendEndpoint { get; } + + /// + /// Gets the backend endpoint of Pulsar Manager + /// + public EndpointReference BackendEndpoint { get; } +} diff --git a/src/Aspire.Hosting.Apache.Pulsar/PulsarResource.cs b/src/Aspire.Hosting.Apache.Pulsar/PulsarResource.cs new file mode 100644 index 0000000000..51dd0af30e --- /dev/null +++ b/src/Aspire.Hosting.Apache.Pulsar/PulsarResource.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Hosting.ApplicationModel; + +/// +/// A resource that represents a Pulsar resource. +/// +public class PulsarResource(string name) + : ContainerResource(name), + IResourceWithConnectionString +{ + internal const string ServiceEndpointName = "service"; + private EndpointReference? _serviceEndpoint; + + internal const string BrokerEndpointName = "broker"; + private EndpointReference? _brokerEndpoint; + + /// + /// Gets the service endpoint for Pulsar server. + /// + public EndpointReference ServiceEndpoint => _serviceEndpoint ??= new(this, ServiceEndpointName); + + /// + /// Gets the broker endpoint for Pulsar server. + /// + public EndpointReference BrokerEndpoint => _brokerEndpoint ??= new(this, BrokerEndpointName); + + /// + /// Gets the connection string expression for Pulsar + /// + public ReferenceExpression ConnectionStringExpression + => ReferenceExpression.Create($"{BrokerEndpoint}"); +} diff --git a/src/Aspire.Hosting.Apache.Pulsar/StandalonePulsarCommandLineArgsAnnotation.cs b/src/Aspire.Hosting.Apache.Pulsar/StandalonePulsarCommandLineArgsAnnotation.cs new file mode 100644 index 0000000000..7e4ae1de13 --- /dev/null +++ b/src/Aspire.Hosting.Apache.Pulsar/StandalonePulsarCommandLineArgsAnnotation.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. + +using Aspire.Hosting.ApplicationModel; + +namespace Aspire.Hosting.Apache.Pulsar; + +internal sealed class StandalonePulsarCommandLineArgsAnnotation() + : CommandLineArgsCallbackAnnotation(context => + { + context.Args.Add("-c"); + context.Args.Add("bin/apply-config-from-env.py conf/standalone.conf && " + + "bin/pulsar standalone"); + + return Task.CompletedTask; + }); diff --git a/src/Shared/apache-pulsar-icon.png b/src/Shared/apache-pulsar-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..218ac2c3a2683b83086538db658c8eb7695e082b GIT binary patch literal 7810 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 literal 0 HcmV?d00001 diff --git a/tests/Aspire.Hosting.Tests/Apache/Pulsar/AddPulsarManagerTests.cs b/tests/Aspire.Hosting.Tests/Apache/Pulsar/AddPulsarManagerTests.cs new file mode 100644 index 0000000000..2410db2cb8 --- /dev/null +++ b/tests/Aspire.Hosting.Tests/Apache/Pulsar/AddPulsarManagerTests.cs @@ -0,0 +1,276 @@ +// 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.Apache.Pulsar; +using Aspire.Hosting.Tests.Utils; +using Aspire.Hosting.Utils; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Aspire.Hosting.Tests.Apache.Pulsar; + +public sealed class AddPulsarManagerTests +{ + [Fact] + public void AddPulsarManagerContainerWithDefaultsAddsAnnotationMetadata() + { + var appBuilder = DistributedApplication.CreateBuilder(); + appBuilder.AddPulsar("pulsar").WithPulsarManager("pulsar-manager"); + + using var app = appBuilder.Build(); + var appModel = app.Services.GetRequiredService(); + + var containerResource = Assert.Single(appModel.Resources.OfType()); + Assert.Equal("pulsar-manager", containerResource.Name); + + var endpoints = containerResource.Annotations.OfType().ToArray(); + Assert.Equal(2, endpoints.Length); + + var backendEndpoint = endpoints.Single(x => x.Name == "backend"); + Assert.Equal(7750, backendEndpoint.TargetPort); + Assert.False(backendEndpoint.IsExternal); + Assert.Null(backendEndpoint.Port); + Assert.Equal(ProtocolType.Tcp, backendEndpoint.Protocol); + Assert.Equal("http", backendEndpoint.Transport); + Assert.Equal("http", backendEndpoint.UriScheme); + + var frontendEndpoint = endpoints.Single(x => x.Name == "frontend"); + Assert.Equal(9527, frontendEndpoint.TargetPort); + Assert.False(frontendEndpoint.IsExternal); + Assert.Null(frontendEndpoint.Port); + Assert.Equal(ProtocolType.Tcp, frontendEndpoint.Protocol); + Assert.Equal("http", frontendEndpoint.Transport); + Assert.Equal("http", frontendEndpoint.UriScheme); + + var containerAnnotation = Assert.Single(containerResource.Annotations.OfType()); + Assert.Equal(PulsarManagerContainerImageTags.Tag, containerAnnotation.Tag); + Assert.Equal(PulsarManagerContainerImageTags.Image, containerAnnotation.Image); + Assert.Equal(PulsarManagerContainerImageTags.Registry, containerAnnotation.Registry); + } + + [Fact] + public async Task PulsarManagerAddsStandardEnvironmentVariables() + { + var builder = DistributedApplication.CreateBuilder(); + + var envVars = await GetEnvironmentVariables(builder); + + Assert.Equal("/pulsar-manager/pulsar-manager/application.properties", envVars["SPRING_CONFIGURATION_FILE"]); + Assert.Equal("{pulsar.bindings.service.url}", envVars["services__pulsar__service__0"]); + Assert.Equal("{pulsar.bindings.broker.url}", envVars["services__pulsar__broker__0"]); + } + + [Fact] + public void PulsarManagerDuplicateInvocationDoesNotCreateNewContainerResource() + { + var builder = DistributedApplication.CreateBuilder(); + builder.AddPulsar("pulsar") + .WithPulsarManager() + .WithPulsarManager(); + + Assert.Single(builder.Resources.OfType()); + } + + [Theory] + [InlineData(null)] + [InlineData(true)] + [InlineData(false)] + public void WithBookKeeperVisualManagerAddsMountAnnotation(bool? isReadOnly) + { + using var builder = TestDistributedApplicationBuilder.Create(); + builder.AddPulsar("pulsar").WithPulsarManager(null, null, null, c => + { + if (isReadOnly.HasValue) + { + c.WithBookKeeperVisualManager("mydata", isReadOnly: isReadOnly.Value); + } + else + { + c.WithBookKeeperVisualManager("mydata"); + } + } + ); + + var pulsarManager = Assert.Single(builder.Resources.OfType()); + + var volumeAnnotation = pulsarManager.Annotations.OfType().Single(); + + Assert.Equal(Path.Combine(builder.AppHostDirectory, "mydata"), volumeAnnotation.Source); + Assert.Equal("/pulsar-manager/pulsar-manager/bkvm.conf", volumeAnnotation.Target); + Assert.Equal(ContainerMountType.BindMount, volumeAnnotation.Type); + Assert.Equal(isReadOnly ?? false, volumeAnnotation.IsReadOnly); + } + + [Theory] + [InlineData(null)] + [InlineData(true)] + [InlineData(false)] + public void WithApplicationPropertiesAddsMountAnnotation(bool? isReadOnly) + { + using var builder = TestDistributedApplicationBuilder.Create(); + builder.AddPulsar("pulsar").WithPulsarManager(null, null, null, c => + { + if (isReadOnly.HasValue) + { + c.WithApplicationProperties("mydata", isReadOnly: isReadOnly.Value); + } + else + { + c.WithApplicationProperties("mydata"); + } + } + ); + + var pulsarManager = Assert.Single(builder.Resources.OfType()); + + var volumeAnnotation = pulsarManager.Annotations.OfType().Single(); + + Assert.Equal(Path.Combine(builder.AppHostDirectory, "mydata"), volumeAnnotation.Source); + Assert.Equal("/pulsar-manager/pulsar-manager/application.properties", volumeAnnotation.Target); + Assert.Equal(ContainerMountType.BindMount, volumeAnnotation.Type); + Assert.Equal(isReadOnly ?? false, volumeAnnotation.IsReadOnly); + } + + [Fact] + public async Task PulsarManagerWithDefinedDefaultEnvironmentAddsDefaultEnvironmentEnvironmentVariables() + { + var builder = DistributedApplication.CreateBuilder(); + + var envVars = await GetEnvironmentVariables(builder, + x => x.WithDefaultEnvironment("test-default-environment") + ); + + Assert.Equal("test-default-environment", envVars["DEFAULT_ENVIRONMENT_NAME"]); + Assert.Equal("{pulsar.bindings.broker.url}", envVars["DEFAULT_ENVIRONMENT_BOOKIE_URL"]); + Assert.Equal("{pulsar.bindings.service.url}", envVars["DEFAULT_ENVIRONMENT_SERVICE_URL"]); + } + + [Fact] + public async Task PulsarManagerWithTagSupportingDefinedDefaultSuperUserAddsEnvironmentVariables() + { + var builder = DistributedApplication.CreateBuilder(); + + var userNameParameter = builder.AddParameter("user"); + var emailParameter = builder.AddParameter("email"); + var passwordParameter = builder.AddParameter("password"); + + var envVars = await GetEnvironmentVariables(builder, + x => x + // [>v0.4.0] supports default superuser via env + .WithImageTag("v0.4.1") + .WithDefaultSuperUser( + userNameParameter, + emailParameter, + passwordParameter + ) + ); + + Assert.Equal("true", envVars["DEFAULT_SUPERUSER_ENABLED"]); + Assert.Equal("{user.value}", envVars["DEFAULT_SUPERUSER_NAME"]); + Assert.Equal("{email.value}", envVars["DEFAULT_SUPERUSER_EMAIL"]); + Assert.Equal("{password.value}", envVars["DEFAULT_SUPERUSER_PASSWORD"]); + } + + [Theory] + [InlineData(PulsarManagerContainerImageTags.Image, "v0.4.0")] + [InlineData(PulsarManagerContainerImageTags.Image, "")] + [InlineData(PulsarManagerContainerImageTags.Image, null)] + [InlineData("someimage", PulsarManagerContainerImageTags.Tag)] + public void PulsarManagerThrowsForUnsupportedImageForDefaultSuperUser(string? image, string? tag) + { + var builder = DistributedApplication.CreateBuilder(); + var pulsar = builder.AddPulsar("pulsar"); + + Assert.Throws(() => pulsar + .WithPulsarManager(null, null, null, + c => c + .WithImage(image!) + .WithImageTag(tag!) + .WithDefaultSuperUser() + ) + ); + } + + private static async ValueTask> GetEnvironmentVariables( + IDistributedApplicationBuilder builder, + Action>? containerConfiguration = null + ) + { + builder.AddPulsar("pulsar").WithPulsarManager(null, null, null, containerConfiguration); + + await using var app = builder.Build(); + var appModel = app.Services.GetRequiredService(); + + var pulsarManager = Assert.Single(appModel.Resources.OfType()); + + return await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync( + pulsarManager, + DistributedApplicationOperation.Publish + ); + } + + [Theory] + [InlineData(null, null)] + [InlineData(6000, null)] + [InlineData(null, 6000)] + [InlineData(6000, 7000)] + public async Task VerifyPulsarManagerManifest(int? frontendPort, int? backendPort) + { + using var appBuilder = TestDistributedApplicationBuilder.Create(); + appBuilder + .AddPulsar("pulsar") + .WithPulsarManager( + "pulsar-manager", + frontendPort: frontendPort, + backendPort: backendPort + ); + + var pulsarManager = appBuilder.Resources.OfType().Single(); + + var manifest = (await ManifestUtils.GetManifest( + pulsarManager + )).ToString(); + + var expectedManifest = $$$""" + { + "type": "container.v0", + "image": "{{{PulsarManagerContainerImageTags.Registry}}}/{{{PulsarManagerContainerImageTags.Image}}}:{{{PulsarManagerContainerImageTags.Tag}}}", + "env": { + "services__pulsar__service__0": "{pulsar.bindings.service.url}", + "services__pulsar__broker__0": "{pulsar.bindings.broker.url}", + "SPRING_CONFIGURATION_FILE": "/pulsar-manager/pulsar-manager/application.properties" + }, + "bindings": { + "frontend": { + "scheme": "http", + "protocol": "tcp", + "transport": "http", + {{{PortManifestPart(frontendPort)}}} + "targetPort": 9527 + }, + "backend": { + "scheme": "http", + "protocol": "tcp", + "transport": "http", + {{{PortManifestPart(backendPort)}}} + "targetPort": 7750 + } + } + } + """; + + // removes blank lines + expectedManifest = Regex.Replace( + expectedManifest, + @"^\s*$\n", + string.Empty, + RegexOptions.Multiline + ); + + Assert.Equal(expectedManifest, manifest); + } + + private static string PortManifestPart(int? port) => port is null ? string.Empty : $"\"port\": {port},"; +} diff --git a/tests/Aspire.Hosting.Tests/Apache/Pulsar/AddPulsarTests.cs b/tests/Aspire.Hosting.Tests/Apache/Pulsar/AddPulsarTests.cs new file mode 100644 index 0000000000..77e7e13649 --- /dev/null +++ b/tests/Aspire.Hosting.Tests/Apache/Pulsar/AddPulsarTests.cs @@ -0,0 +1,242 @@ +// 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.Apache.Pulsar; +using Aspire.Hosting.Utils; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Aspire.Hosting.Tests.Apache.Pulsar; + +public class AddPulsarTests +{ + [Fact] + public void AddPulsarContainerWithDefaultsAddsAnnotationMetadata() + { + var appBuilder = DistributedApplication.CreateBuilder(); + appBuilder.AddPulsar("pulsar"); + + using var app = appBuilder.Build(); + var appModel = app.Services.GetRequiredService(); + + var containerResource = Assert.Single(appModel.Resources.OfType()); + Assert.Equal("pulsar", containerResource.Name); + + var endpoints = containerResource.Annotations.OfType().ToArray(); + Assert.Equal(2, endpoints.Length); + + var brokerEndpoint = endpoints.Single(x => x.Name == "broker"); + Assert.Equal(6650, brokerEndpoint.TargetPort); + Assert.False(brokerEndpoint.IsExternal); + Assert.Null(brokerEndpoint.Port); + Assert.Equal(ProtocolType.Tcp, brokerEndpoint.Protocol); + Assert.Equal("tcp", brokerEndpoint.Transport); + Assert.Equal("pulsar", brokerEndpoint.UriScheme); + + var serviceEndpoint = endpoints.Single(x => x.Name == "service"); + Assert.Equal(8080, serviceEndpoint.TargetPort); + Assert.False(serviceEndpoint.IsExternal); + Assert.Null(serviceEndpoint.Port); + Assert.Equal(ProtocolType.Tcp, serviceEndpoint.Protocol); + Assert.Equal("http", serviceEndpoint.Transport); + Assert.Equal("http", serviceEndpoint.UriScheme); + + var containerAnnotation = Assert.Single(containerResource.Annotations.OfType()); + Assert.Equal(PulsarContainerImageTags.Tag, containerAnnotation.Tag); + Assert.Equal(PulsarContainerImageTags.Image, containerAnnotation.Image); + Assert.Equal(PulsarContainerImageTags.Registry, containerAnnotation.Registry); + } + + [Fact] + public async Task PulsarCreatesConnectionString() + { + var appBuilder = DistributedApplication.CreateBuilder(); + appBuilder + .AddPulsar("pulsar") + .WithEndpoint("broker", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 27017)); + + await using var app = appBuilder.Build(); + var appModel = app.Services.GetRequiredService(); + + var connectionStringResource = Assert.Single(appModel.Resources.OfType()) as IResourceWithConnectionString; + var connectionString = await connectionStringResource.GetConnectionStringAsync(); + + Assert.Equal("pulsar://localhost:27017", connectionString); + Assert.Equal("{pulsar.bindings.broker.url}", connectionStringResource.ConnectionStringExpression.ValueExpression); + } + + [Fact] + public void PulsarAddsSingleStandaloneCommandLineArgs() + { + using var builder = TestDistributedApplicationBuilder.Create(); + var pulsar = builder.AddPulsar("pulsar") + .AsStandalone() + .AsStandalone(); + + Assert.True(pulsar.Resource.TryGetAnnotationsOfType(out var argsAnnotations)); + Assert.NotNull(argsAnnotations.Single()); + } + + [Theory] + [InlineData(null)] + [InlineData(true)] + [InlineData(false)] + public void WithDataVolumeAddsVolumeAnnotation(bool? isReadOnly) + { + using var builder = TestDistributedApplicationBuilder.Create(); + var pulsar = builder.AddPulsar("pulsar"); + if (isReadOnly.HasValue) + { + pulsar.WithDataVolume(isReadOnly: isReadOnly.Value); + } + else + { + pulsar.WithDataVolume(); + } + + var volumeAnnotation = pulsar.Resource.Annotations.OfType().Single(); + + var appName = builder.Environment.ApplicationName; + Assert.Equal($"{appName}-pulsar-pulsardata", volumeAnnotation.Source); + Assert.Equal("/pulsar/data", volumeAnnotation.Target); + Assert.Equal(ContainerMountType.Volume, volumeAnnotation.Type); + Assert.Equal(isReadOnly ?? false, volumeAnnotation.IsReadOnly); + } + + [Theory] + [InlineData(null)] + [InlineData(true)] + [InlineData(false)] + public void WithDataBindMountAddsMountAnnotation(bool? isReadOnly) + { + using var builder = TestDistributedApplicationBuilder.Create(); + var pulsar = builder.AddPulsar("pulsar"); + if (isReadOnly.HasValue) + { + pulsar.WithDataBindMount("mydata", isReadOnly: isReadOnly.Value); + } + else + { + pulsar.WithDataBindMount("mydata"); + } + + var volumeAnnotation = pulsar.Resource.Annotations.OfType().Single(); + + Assert.Equal(Path.Combine(builder.AppHostDirectory, "mydata"), volumeAnnotation.Source); + Assert.Equal("/pulsar/data", volumeAnnotation.Target); + Assert.Equal(ContainerMountType.BindMount, volumeAnnotation.Type); + Assert.Equal(isReadOnly ?? false, volumeAnnotation.IsReadOnly); + } + + [Theory] + [InlineData(null)] + [InlineData(true)] + [InlineData(false)] + public void WithConfigVolumeAddsVolumeAnnotation(bool? isReadOnly) + { + using var builder = TestDistributedApplicationBuilder.Create(); + var pulsar = builder.AddPulsar("pulsar"); + if (isReadOnly.HasValue) + { + pulsar.WithConfigVolume(isReadOnly: isReadOnly.Value); + } + else + { + pulsar.WithConfigVolume(); + } + + var volumeAnnotation = pulsar.Resource.Annotations.OfType().Single(); + + var appName = builder.Environment.ApplicationName; + Assert.Equal($"{appName}-pulsar-pulsarconf", volumeAnnotation.Source); + Assert.Equal("/pulsar/conf", volumeAnnotation.Target); + Assert.Equal(ContainerMountType.Volume, volumeAnnotation.Type); + Assert.Equal(isReadOnly ?? false, volumeAnnotation.IsReadOnly); + } + + [Theory] + [InlineData(null)] + [InlineData(true)] + [InlineData(false)] + public void WithConfigBindMountAddsMountAnnotation(bool? isReadOnly) + { + using var builder = TestDistributedApplicationBuilder.Create(); + var pulsar = builder.AddPulsar("pulsar"); + if (isReadOnly.HasValue) + { + pulsar.WithConfigBindMount("mydata", isReadOnly: isReadOnly.Value); + } + else + { + pulsar.WithConfigBindMount("mydata"); + } + + var volumeAnnotation = pulsar.Resource.Annotations.OfType().Single(); + + Assert.Equal(Path.Combine(builder.AppHostDirectory, "mydata"), volumeAnnotation.Source); + Assert.Equal("/pulsar/conf", volumeAnnotation.Target); + Assert.Equal(ContainerMountType.BindMount, volumeAnnotation.Type); + Assert.Equal(isReadOnly ?? false, volumeAnnotation.IsReadOnly); + } + + [Theory] + [InlineData(null, null)] + [InlineData(6000, null)] + [InlineData(null, 6000)] + [InlineData(6000, 7000)] + public async Task VerifyPulsarManifest(int? brokerPort, int? servicePort) + { + using var appBuilder = TestDistributedApplicationBuilder.Create(); + + var manifest = (await ManifestUtils.GetManifest( + appBuilder.AddPulsar( + name: "pulsar", + servicePort: servicePort, + brokerPort: brokerPort + ).Resource + )).ToString(); + + var expectedManifest = $$""" + { + "type": "container.v0", + "connectionString": "{pulsar.bindings.broker.url}", + "image": "{{PulsarContainerImageTags.Registry}}/{{PulsarContainerImageTags.Image}}:{{PulsarContainerImageTags.Tag}}", + "entrypoint": "/bin/bash", + "args": [ + "-c", + "bin/apply-config-from-env.py conf/standalone.conf \u0026\u0026 bin/pulsar standalone" + ], + "bindings": { + "service": { + "scheme": "http", + "protocol": "tcp", + "transport": "http", + {{PortManifestPart(servicePort)}} + "targetPort": 8080 + }, + "broker": { + "scheme": "pulsar", + "protocol": "tcp", + "transport": "tcp", + {{PortManifestPart(brokerPort)}} + "targetPort": 6650 + } + } + } + """; + + // removes blank lines + expectedManifest = Regex.Replace( + expectedManifest, + @"^\s*$\n", + string.Empty, + RegexOptions.Multiline + ); + + Assert.Equal(expectedManifest, manifest); + } + + private static string PortManifestPart(int? port) => port is null ? string.Empty : $"\"port\": {port},"; +} diff --git a/tests/Aspire.Hosting.Tests/Aspire.Hosting.Tests.csproj b/tests/Aspire.Hosting.Tests/Aspire.Hosting.Tests.csproj index 474a505991..397feffa3b 100644 --- a/tests/Aspire.Hosting.Tests/Aspire.Hosting.Tests.csproj +++ b/tests/Aspire.Hosting.Tests/Aspire.Hosting.Tests.csproj @@ -10,6 +10,7 @@ + From 4355b6567bb457990395c86d4161e7fde28f2911 Mon Sep 17 00:00:00 2001 From: Marko Urh Date: Thu, 9 May 2024 22:40:44 +0200 Subject: [PATCH 2/5] refactor: cleanup todo's --- playground/apache-pulsar/ApachePulsar.AppHost/Program.cs | 8 -------- .../PulsarBuilderExtensions.cs | 3 ++- .../PulsarManagerBuilderExtensions.cs | 6 ++++-- .../PulsarManagerContainerImageTags.cs | 5 ----- 4 files changed, 6 insertions(+), 16 deletions(-) diff --git a/playground/apache-pulsar/ApachePulsar.AppHost/Program.cs b/playground/apache-pulsar/ApachePulsar.AppHost/Program.cs index 9f1fbe91d4..0269ab3426 100644 --- a/playground/apache-pulsar/ApachePulsar.AppHost/Program.cs +++ b/playground/apache-pulsar/ApachePulsar.AppHost/Program.cs @@ -3,9 +3,6 @@ var builder = DistributedApplication.CreateBuilder(args); -var pulsarSuperUserUserName = builder.AddParameter("pulsar-manager-superuser-username"); -var pulsarSuperUserPassword = builder.AddParameter("pulsar-manager-superuser-password"); - var pulsar = builder .AddPulsar( name: "pulsar", @@ -19,11 +16,6 @@ configureContainer: c => c .WithApplicationProperties() .WithDefaultEnvironment("pulsar-playground") - // TODO: Uncomment after new pulsar manager release (>0.4.0) - //.WithDefaultSuperUser( - // userName: pulsarSuperUserUserName, - // password: pulsarSuperUserPassword - //) ); builder.AddProject("api") diff --git a/src/Aspire.Hosting.Apache.Pulsar/PulsarBuilderExtensions.cs b/src/Aspire.Hosting.Apache.Pulsar/PulsarBuilderExtensions.cs index 7e9494f9aa..1d7668573a 100644 --- a/src/Aspire.Hosting.Apache.Pulsar/PulsarBuilderExtensions.cs +++ b/src/Aspire.Hosting.Apache.Pulsar/PulsarBuilderExtensions.cs @@ -104,5 +104,6 @@ public static IResourceBuilder WithConfigBindMount( this IResourceBuilder builder, string source, bool isReadOnly = false - ) => builder.WithBindMount(source, "/pulsar/conf", isReadOnly); + ) => builder + .WithBindMount(source, "/pulsar/conf", isReadOnly); } diff --git a/src/Aspire.Hosting.Apache.Pulsar/PulsarManagerBuilderExtensions.cs b/src/Aspire.Hosting.Apache.Pulsar/PulsarManagerBuilderExtensions.cs index 3f6ad5ce45..cb27f0457d 100644 --- a/src/Aspire.Hosting.Apache.Pulsar/PulsarManagerBuilderExtensions.cs +++ b/src/Aspire.Hosting.Apache.Pulsar/PulsarManagerBuilderExtensions.cs @@ -93,8 +93,10 @@ public static IResourceBuilder WithBookKeeperVisualManage /// Seeds default super-user to a Pulsar Manager. /// /// - /// This method only supports the Pulsar Manager container image and tags including and above v0.4.1
- /// Calling this method on a resource configured with an unrecognized image registry, name, or tag will result in a being thrown. + /// This method only supports the Pulsar Manager container image and tags above v0.4.0
+ /// Calling this method on a resource configured with an unrecognized image registry, name, or tag will result in a being thrown.

+ /// To support seeding super-user on v0.4.0 or lower, please refer to following documentation:
+ /// ///
/// The for the . /// The parameter used to provide the username for the Pulsar Manager default superuser. If a default value will be used. diff --git a/src/Aspire.Hosting.Apache.Pulsar/PulsarManagerContainerImageTags.cs b/src/Aspire.Hosting.Apache.Pulsar/PulsarManagerContainerImageTags.cs index 7f25008232..901b081cc1 100644 --- a/src/Aspire.Hosting.Apache.Pulsar/PulsarManagerContainerImageTags.cs +++ b/src/Aspire.Hosting.Apache.Pulsar/PulsarManagerContainerImageTags.cs @@ -11,11 +11,6 @@ internal static class PulsarManagerContainerImageTags public const string Image = "apachepulsar/pulsar-manager"; public const string Tag = "v0.4.0"; - // TODO: Bump after release of Pulsar Manager - // Below PRs add value: - // https://github.com/apache/pulsar-manager/pull/565 - adds super-user via env vars - // https://github.com/apache/pulsar-manager/pull/564 - fixes duplicate super-user seed - private static readonly Version s_versionThresholdNotSupportingDefaultSuperUserViaEnvVars = new(0, 4, 0); /// From b98aac30c1a4f533a8cfd251c58fe87910b903fb Mon Sep 17 00:00:00 2001 From: Marko Urh Date: Fri, 10 May 2024 16:34:13 +0200 Subject: [PATCH 3/5] fix: latest application.properties --- .../application.properties | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/playground/apache-pulsar/ApachePulsar.AppHost/application.properties b/playground/apache-pulsar/ApachePulsar.AppHost/application.properties index 50e404c5dc..d2c90488c9 100644 --- a/playground/apache-pulsar/ApachePulsar.AppHost/application.properties +++ b/playground/apache-pulsar/ApachePulsar.AppHost/application.properties @@ -20,7 +20,7 @@ logging.path= logging.file=pulsar-manager.log # DEBUG print execute sql -logging.level.org.apache=DEBUG +logging.level.org.apache=INFO mybatis.type-aliases-package=org.apache.pulsar.manager @@ -59,6 +59,11 @@ spring.datasource.initialization-mode=always #spring.datasource.username=postgres #spring.datasource.password=postgres +# hikari configuration +spring.datasource.hikari.connectionTimeout=10000 +spring.datasource.hikari.idleTimeout=60000 +spring.datasource.hikari.maxLifetime=300000 + # zuul config # https://cloud.spring.io/spring-cloud-static/Dalston.SR5/multi/multi__router_and_filter_zuul.html # By Default Zuul adds Authorization to be dropped headers list. Below we are manually setting it @@ -84,11 +89,6 @@ backend.broker.pulsarAdmin.tlsEnableHostnameVerification=false jwt.secret=dab1c8ba-b01b-11e9-b384-186590e06885 jwt.sessionTime=2592000 -# If user.management.enable is true, the following account and password will no longer be valid. -pulsar-manager.account=pulsar -pulsar-manager.password=pulsar -# If true, the database is used for user management -user.management.enable=false # Optional -> SECRET, PRIVATE, default -> PRIVATE, empty -> disable auth # SECRET mode -> bin/pulsar tokens create --secret-key file:///path/to/my-secret.key --subject test-user @@ -132,6 +132,13 @@ spring.thymeleaf.mode=HTML5 default.environment.name= default.environment.service_url= default.environment.bookie_url= + +# default superuser configuration +default.superuser.enabled=false +default.superuser.name= +default.superuser.password= +default.superuser.email= + # enable tls encryption # keytool -import -alias test-keystore -keystore ca-certs -file certs/ca.cert.pem tls.enabled=false From d2f45d085630e44d73363454225dd956eb1c1e0d Mon Sep 17 00:00:00 2001 From: Marko Urh Date: Fri, 10 May 2024 23:28:09 +0200 Subject: [PATCH 4/5] cleanup: simpler playground app, remove service refs to pulsar from manager --- .../ApachePulsar.Api/PlayerEndpoints.cs | 19 ----- .../PlayerRegistrationExtensions.cs | 65 --------------- .../apache-pulsar/ApachePulsar.Api/Players.cs | 81 ++++++++++++------- .../apache-pulsar/ApachePulsar.Api/Program.cs | 49 ++++++++--- .../ApachePulsar.Api/requests.http | 10 ++- .../PulsarManagerBuilderExtensions.cs | 4 - 6 files changed, 96 insertions(+), 132 deletions(-) delete mode 100644 playground/apache-pulsar/ApachePulsar.Api/PlayerEndpoints.cs delete mode 100644 playground/apache-pulsar/ApachePulsar.Api/PlayerRegistrationExtensions.cs diff --git a/playground/apache-pulsar/ApachePulsar.Api/PlayerEndpoints.cs b/playground/apache-pulsar/ApachePulsar.Api/PlayerEndpoints.cs deleted file mode 100644 index 6aaf19e9b9..0000000000 --- a/playground/apache-pulsar/ApachePulsar.Api/PlayerEndpoints.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.AspNetCore.Mvc; - -public static class PlayerEndpoints -{ - public static void Map(WebApplication app) - { - app.MapPost("/start-match", async ([FromServices] PingPlayer startPlayer, CancellationToken cancellation) => - { - await startPlayer.SmackTheBall(cancellation); - return Results.Ok(); - }).WithOpenApi(); - - app.MapGet("/ping-player/received", ([FromServices] PingPlayer player) => Results.Ok((object?)player.ReceivedBalls)).WithOpenApi(); - app.MapGet("/pong-player/received", ([FromServices] PongPlayer player) => Results.Ok((object?)player.ReceivedBalls)).WithOpenApi(); - } -} diff --git a/playground/apache-pulsar/ApachePulsar.Api/PlayerRegistrationExtensions.cs b/playground/apache-pulsar/ApachePulsar.Api/PlayerRegistrationExtensions.cs deleted file mode 100644 index ff5f269d14..0000000000 --- a/playground/apache-pulsar/ApachePulsar.Api/PlayerRegistrationExtensions.cs +++ /dev/null @@ -1,65 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using DotPulsar; -using DotPulsar.Abstractions; -using DotPulsar.Extensions; - -public static class PlayerTopicFactory -{ - public const string TopicPrefix = "persistent://public/default"; - - public static string GetTopic(string player) => $"{TopicPrefix}/{player}"; - - public static string GetOpponentTopic(string player) => player switch - { - nameof(PingPlayer) => GetTopic(nameof(PongPlayer)), - nameof(PongPlayer) => GetTopic(nameof(PingPlayer)), - _ => throw new ArgumentOutOfRangeException(nameof(player), player, null) - }; -} - -public static class PlayerRegistrationExtensions -{ - public static Type PlayerKey() where T : Player => typeof(T); - - public static string PlayerName() where T : Player => PlayerKey().Name; - - public static void Register(this IServiceCollection services, IPulsarClient client) - where T : Player - { - var player = PlayerName(); - var opponentField = PlayerTopicFactory.GetOpponentTopic(player); - var playerField = PlayerTopicFactory.GetTopic(player); - - // Produce player move (message) into opponent field (topic) - services.AddKeyedSingleton(player, (_, _) => client - .NewProducer(Schema.String) - .ProducerName(player) - .Topic(opponentField) - ); - - // Listen your field (topic) for player move (message) so you can respond back - services.AddKeyedSingleton(player, (_, _) => client - .NewConsumer(Schema.String) - .ConsumerName(player) - .Topic(playerField) - .SubscriptionName(player) - ); - - services.AddSingleton(Create); - services.AddHostedService(sp => sp.GetRequiredService()); - } - - public static T Create(this IServiceProvider sp) - where T : Player - { - var player = PlayerName(); - return (T)Activator.CreateInstance( - typeof(T), - sp.GetRequiredKeyedService>(player), - sp.GetRequiredKeyedService>(player), - sp.GetRequiredService().CreateLogger(player) - )!; - } -} diff --git a/playground/apache-pulsar/ApachePulsar.Api/Players.cs b/playground/apache-pulsar/ApachePulsar.Api/Players.cs index 86f954c01f..27b16cd032 100644 --- a/playground/apache-pulsar/ApachePulsar.Api/Players.cs +++ b/playground/apache-pulsar/ApachePulsar.Api/Players.cs @@ -6,49 +6,36 @@ using DotPulsar.Exceptions; using DotPulsar.Extensions; -/// -/// Ping player produces pings -/// He receives pongs from Pong player -/// -public sealed class PingPlayer(IConsumerBuilder consumerBuilder, IProducerBuilder producerBuilder, ILogger logger) - : Player(consumerBuilder, producerBuilder, logger) -{ - protected override string Move => "ping"; -} - -/// -/// Pong player produces pongs -/// He receives pings from Ping player -/// -public sealed class PongPlayer(IConsumerBuilder consumerBuilder, IProducerBuilder producerBuilder, ILogger logger) - : Player(consumerBuilder, producerBuilder, logger) -{ - protected override string Move => "pong"; -} - public abstract class Player( - IConsumerBuilder consumerBuilder, - IProducerBuilder producerBuilder, + IConsumerBuilder consumerB, + IProducerBuilder producerB, + MatchCoordinator coordinator, ILogger logger ) : BackgroundService { - protected abstract string Move { get; } - private uint _receivedBalls; public uint ReceivedBalls => _receivedBalls; - private readonly Lazy> _producer = new(producerBuilder.Create); + protected abstract string Move { get; } /// /// Kick the ball (message) into opponent field (topic) /// - public async Task SmackTheBall(CancellationToken cancellation) + public async Task SmackTheBall(CancellationToken cancellationToken = default) { - logger.LogInformation("Responding: {message}", Move); + if (coordinator.MatchHalt) + { + logger.LogWarning("Match halted"); + return; + } + + var producer = producerB.Create(); + + logger.LogInformation("Sending: {message}", Move); - await Task.Delay(700, cancellation); // add some sim... + await Task.Delay(700, cancellationToken); // add some sim... - await _producer.Value.Send(new MessageMetadata(), Move, cancellation); + await producer.Send(new MessageMetadata(), Move, cancellationToken); } /// @@ -56,7 +43,7 @@ public async Task SmackTheBall(CancellationToken cancellation) /// private async Task ReceiveBall(CancellationToken cancellation) { - var consumer = consumerBuilder.Create(); + var consumer = consumerB.Create(); await foreach (var message in consumer.Messages(cancellation)) { logger.LogInformation("Received: {message}", message); @@ -87,3 +74,37 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) } } } + +public sealed class PingPlayer( + [FromKeyedServices(typeof(PingPlayer))] IConsumerBuilder consumerB, + [FromKeyedServices(typeof(PingPlayer))] IProducerBuilder producerB, + MatchCoordinator coordinator, + ILogger logger +) : Player(consumerB, producerB, coordinator, logger) +{ + protected override string Move => "ping"; +} + +public sealed class PongPlayer( + [FromKeyedServices(typeof(PongPlayer))] IConsumerBuilder consumerB, + [FromKeyedServices(typeof(PongPlayer))] IProducerBuilder producerB, + MatchCoordinator coordinator, + ILogger logger +) : Player(consumerB, producerB, coordinator, logger) +{ + protected override string Move => "pong"; +} + +public sealed class MatchCoordinator(ILogger logger) +{ + public bool MatchHalt { get; private set; } + + public async Task HaltMatch() + { + MatchHalt = true; + logger.LogWarning("Match halted, match will be able to resume after 3 seconds timeout"); + + await Task.Delay(TimeSpan.FromSeconds(3)); + MatchHalt = false; + } +} diff --git a/playground/apache-pulsar/ApachePulsar.Api/Program.cs b/playground/apache-pulsar/ApachePulsar.Api/Program.cs index 3453d3de15..e9f31578fe 100644 --- a/playground/apache-pulsar/ApachePulsar.Api/Program.cs +++ b/playground/apache-pulsar/ApachePulsar.Api/Program.cs @@ -2,30 +2,55 @@ // The .NET Foundation licenses this file to you under the MIT license. using DotPulsar; +using DotPulsar.Extensions; +using Microsoft.AspNetCore.Mvc; var builder = WebApplication.CreateBuilder(args); - -var services = builder - .AddServiceDefaults() - .Services - .AddEndpointsApiExplorer() - .AddSwaggerGen(); +var services = builder.AddServiceDefaults().Services.AddEndpointsApiExplorer().AddSwaggerGen(); var pulsarConnection = new Uri(builder.Configuration.GetConnectionString("Pulsar")!); -Console.WriteLine($"Pulsar connection string {pulsarConnection}"); - -var client = PulsarClient +var pulsarClient = PulsarClient .Builder() .ServiceUrl(pulsarConnection) .Build(); -services.Register(client); -services.Register(client); +Console.WriteLine($"Pulsar connection string {pulsarConnection}"); + +var pulsarNamespace = "persistent://public/default"; +var pingTopic = $"{pulsarNamespace}/ping-field"; +var pongTopic = $"{pulsarNamespace}/pong-field"; + +// Each player plays (produces) a move (message) into opponents field (topic) +// Each player then responds to opponent moves (messages) being played into their field (topic) + +var pingProducerB = pulsarClient.NewProducer(Schema.String).Topic(pongTopic); +var pingConsumerB = pulsarClient.NewConsumer(Schema.String).Topic(pingTopic).SubscriptionName("ping-player"); + +var pongProducerB = pulsarClient.NewProducer(Schema.String).Topic(pingTopic); +var pongConsumerB = pulsarClient.NewConsumer(Schema.String).Topic(pongTopic).SubscriptionName("pong-player"); + +services.AddSingleton(); + +services + .AddSingleton() + .AddHostedService(sp => sp.GetRequiredService()) + .AddKeyedSingleton(typeof(PingPlayer), (_, _) => pingProducerB) + .AddKeyedSingleton(typeof(PingPlayer), (_, _) => pingConsumerB); + +services + .AddSingleton() + .AddHostedService(sp => sp.GetRequiredService()) + .AddKeyedSingleton(typeof(PongPlayer), (_, _) => pongProducerB) + .AddKeyedSingleton(typeof(PongPlayer), (_, _) => pongConsumerB); var app = builder.Build(); app.UseSwagger().UseSwaggerUI(); -PlayerEndpoints.Map(app); +app.MapPost("/match/start", async ([FromServices] PingPlayer p) => await p.SmackTheBall()).WithOpenApi(); +app.MapPost("/match/stop", async ([FromServices] MatchCoordinator mc) => await mc.HaltMatch()).WithOpenApi(); + +app.MapGet("/ping-player/received", ([FromServices] PingPlayer p) => Results.Ok(p.ReceivedBalls)).WithOpenApi(); +app.MapGet("/pong-player/received", ([FromServices] PongPlayer p) => Results.Ok(p.ReceivedBalls)).WithOpenApi(); app.Run(); diff --git a/playground/apache-pulsar/ApachePulsar.Api/requests.http b/playground/apache-pulsar/ApachePulsar.Api/requests.http index 4ddd7b5512..e0b310b80d 100644 --- a/playground/apache-pulsar/ApachePulsar.Api/requests.http +++ b/playground/apache-pulsar/ApachePulsar.Api/requests.http @@ -1,4 +1,10 @@ @Url=http://localhost:5226 -POST {{Url}}/start-match -Content-Type: application/json +POST {{Url}}/match/start +### +POST {{Url}}/match/stop +### +GET {{Url}}/ping-player/received +### +GET {{Url}}/pong-player/received +### diff --git a/src/Aspire.Hosting.Apache.Pulsar/PulsarManagerBuilderExtensions.cs b/src/Aspire.Hosting.Apache.Pulsar/PulsarManagerBuilderExtensions.cs index cb27f0457d..6ee85c161e 100644 --- a/src/Aspire.Hosting.Apache.Pulsar/PulsarManagerBuilderExtensions.cs +++ b/src/Aspire.Hosting.Apache.Pulsar/PulsarManagerBuilderExtensions.cs @@ -49,10 +49,6 @@ public static IResourceBuilder WithPulsarManager( .WithEndpoint(port: frontendPort, targetPort: 9527, name: PulsarManagerResource.FrontendEndpointName, scheme: "http") .WithEndpoint(port: backendPort, targetPort: 7750, name: PulsarManagerResource.BackendEndpointName, scheme: "http"); - pulsarManagerBuilder - .WithReference(builder.GetEndpoint(PulsarResource.ServiceEndpointName)) - .WithReference(builder.GetEndpoint(PulsarResource.BrokerEndpointName)); - pulsarManagerBuilder .WithEnvironment("SPRING_CONFIGURATION_FILE", "/pulsar-manager/pulsar-manager/application.properties"); From 7702adb11dfbfa09fe43b779f212fbf91b8d0893 Mon Sep 17 00:00:00 2001 From: Marko Urh Date: Fri, 7 Jun 2024 22:27:54 +0200 Subject: [PATCH 5/5] allow port change for pulsar service and pulsar manager fe update tests --- Aspire.sln | 5 ++-- .../ApachePulsar.AppHost/Program.cs | 4 +-- .../application.properties | 17 ++++-------- .../PublicAPI.Unshipped.txt | 4 +-- .../PulsarBuilderExtensions.cs | 6 ++--- .../PulsarManagerBuilderExtensions.cs | 10 +++---- .../PulsarManagerContainerImageTags.cs | 5 ++++ .../PulsarManagerResource.cs | 5 +++- .../PulsarResource.cs | 2 ++ .../Apache/Pulsar/AddPulsarManagerTests.cs | 26 +++++++------------ .../Apache/Pulsar/AddPulsarTests.cs | 14 ++++------ 11 files changed, 42 insertions(+), 56 deletions(-) diff --git a/Aspire.sln b/Aspire.sln index a3ed65e3f5..df790e2d5f 100644 --- a/Aspire.sln +++ b/Aspire.sln @@ -463,13 +463,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Consumer", "playground\kafk EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Producer", "playground\kafka\Producer\Producer.csproj", "{FEE2F9B0-F32D-41B3-8917-0C13DE4F5953}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Hosting.Apache.Pulsar", "src\Aspire.Hosting.Apache.Pulsar\Aspire.Hosting.Apache.Pulsar.csproj", "{2BE6B31D-25E0-4641-BE98-BF40C1A43204}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Hosting.Apache.Pulsar", "src\Aspire.Hosting.Apache.Pulsar\Aspire.Hosting.Apache.Pulsar.csproj", "{2BE6B31D-25E0-4641-BE98-BF40C1A43204}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "apache-pulsar", "apache-pulsar", "{3CF517C3-3F47-40F6-9330-10E0174A8800}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApachePulsar.AppHost", "playground\apache-pulsar\ApachePulsar.AppHost\ApachePulsar.AppHost.csproj", "{BC17A942-37AB-4E5A-8C7B-70846AE8934C}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApachePulsar.Api", "playground\apache-pulsar\ApachePulsar.Api\ApachePulsar.Api.csproj", "{484C6267-F5D1-45B4-B458-B12F0EC90884}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "milvus", "milvus", "{BD2CD8FB-18EC-4930-8228-C49D89622022}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MilvusPlayground.AppHost", "playground\milvus\MilvusPlayground.AppHost\MilvusPlayground.AppHost.csproj", "{CE3B7E15-2319-45CD-9CED-0017E306DE9A}" @@ -482,7 +483,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Milvus.Client", "src EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Milvus.Client.Tests", "tests\Aspire.Milvus.Client.Tests\Aspire.Milvus.Client.Tests.csproj", "{9FAE1602-2C69-4D24-8655-A164489441E8}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Hosting.Valkey", "src\Aspire.Hosting.Valkey\Aspire.Hosting.Valkey.csproj", "{5CB63205-24F4-4388-A41B-BAF3BEA59866}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Hosting.Valkey", "src\Aspire.Hosting.Valkey\Aspire.Hosting.Valkey.csproj", "{5CB63205-24F4-4388-A41B-BAF3BEA59866}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/playground/apache-pulsar/ApachePulsar.AppHost/Program.cs b/playground/apache-pulsar/ApachePulsar.AppHost/Program.cs index 93cb5f9f5b..2cab6f86ee 100644 --- a/playground/apache-pulsar/ApachePulsar.AppHost/Program.cs +++ b/playground/apache-pulsar/ApachePulsar.AppHost/Program.cs @@ -6,13 +6,11 @@ var pulsar = builder .AddPulsar( name: "pulsar", - targetPort: 8080, port: 6650 ) .WithPulsarManager( name: "pulsar-manager", - frontendPort: 9527, - backendPort: 7750, + port: 9527, configureContainer: c => c .WithApplicationProperties() .WithDefaultEnvironment("pulsar-playground") diff --git a/playground/apache-pulsar/ApachePulsar.AppHost/application.properties b/playground/apache-pulsar/ApachePulsar.AppHost/application.properties index d2c90488c9..b038a25b96 100644 --- a/playground/apache-pulsar/ApachePulsar.AppHost/application.properties +++ b/playground/apache-pulsar/ApachePulsar.AppHost/application.properties @@ -59,11 +59,6 @@ spring.datasource.initialization-mode=always #spring.datasource.username=postgres #spring.datasource.password=postgres -# hikari configuration -spring.datasource.hikari.connectionTimeout=10000 -spring.datasource.hikari.idleTimeout=60000 -spring.datasource.hikari.maxLifetime=300000 - # zuul config # https://cloud.spring.io/spring-cloud-static/Dalston.SR5/multi/multi__router_and_filter_zuul.html # By Default Zuul adds Authorization to be dropped headers list. Below we are manually setting it @@ -89,6 +84,11 @@ backend.broker.pulsarAdmin.tlsEnableHostnameVerification=false jwt.secret=dab1c8ba-b01b-11e9-b384-186590e06885 jwt.sessionTime=2592000 +# If user.management.enable is true, the following account and password will no longer be valid. +pulsar-manager.account=pulsar +pulsar-manager.password=pulsar +# If true, the database is used for user management +user.management.enable=true # Optional -> SECRET, PRIVATE, default -> PRIVATE, empty -> disable auth # SECRET mode -> bin/pulsar tokens create --secret-key file:///path/to/my-secret.key --subject test-user @@ -132,13 +132,6 @@ spring.thymeleaf.mode=HTML5 default.environment.name= default.environment.service_url= default.environment.bookie_url= - -# default superuser configuration -default.superuser.enabled=false -default.superuser.name= -default.superuser.password= -default.superuser.email= - # enable tls encryption # keytool -import -alias test-keystore -keystore ca-certs -file certs/ca.cert.pem tls.enabled=false diff --git a/src/Aspire.Hosting.Apache.Pulsar/PublicAPI.Unshipped.txt b/src/Aspire.Hosting.Apache.Pulsar/PublicAPI.Unshipped.txt index 2452ca40fb..84c3ddb964 100644 --- a/src/Aspire.Hosting.Apache.Pulsar/PublicAPI.Unshipped.txt +++ b/src/Aspire.Hosting.Apache.Pulsar/PublicAPI.Unshipped.txt @@ -10,7 +10,7 @@ Aspire.Hosting.ApplicationModel.PulsarResource.PulsarResource(string! name) -> v Aspire.Hosting.ApplicationModel.PulsarResource.ServiceEndpoint.get -> Aspire.Hosting.ApplicationModel.EndpointReference! Aspire.Hosting.PulsarBuilderExtensions Aspire.Hosting.PulsarManagerBuilderExtensions -static Aspire.Hosting.PulsarBuilderExtensions.AddPulsar(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, int? servicePort = null, int? brokerPort = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static Aspire.Hosting.PulsarBuilderExtensions.AddPulsar(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, int? port = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.PulsarBuilderExtensions.AsStandalone(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.PulsarBuilderExtensions.WithConfigBindMount(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! source, bool isReadOnly = false) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.PulsarBuilderExtensions.WithConfigVolume(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string? name = null, bool isReadOnly = false) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! @@ -20,4 +20,4 @@ static Aspire.Hosting.PulsarManagerBuilderExtensions.WithApplicationProperties(t static Aspire.Hosting.PulsarManagerBuilderExtensions.WithBookKeeperVisualManager(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! source = "bkvm.conf", bool isReadOnly = false) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.PulsarManagerBuilderExtensions.WithDefaultEnvironment(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string? name = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.PulsarManagerBuilderExtensions.WithDefaultSuperUser(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, Aspire.Hosting.ApplicationModel.IResourceBuilder? userName = null, Aspire.Hosting.ApplicationModel.IResourceBuilder? email = null, Aspire.Hosting.ApplicationModel.IResourceBuilder? password = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! -static Aspire.Hosting.PulsarManagerBuilderExtensions.WithPulsarManager(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string? name = null, int? frontendPort = null, int? backendPort = null, System.Action!>? configureContainer = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static Aspire.Hosting.PulsarManagerBuilderExtensions.WithPulsarManager(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string? name = null, int? port = null, System.Action!>? configureContainer = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! diff --git a/src/Aspire.Hosting.Apache.Pulsar/PulsarBuilderExtensions.cs b/src/Aspire.Hosting.Apache.Pulsar/PulsarBuilderExtensions.cs index 67096614fc..43f2e29913 100644 --- a/src/Aspire.Hosting.Apache.Pulsar/PulsarBuilderExtensions.cs +++ b/src/Aspire.Hosting.Apache.Pulsar/PulsarBuilderExtensions.cs @@ -21,13 +21,11 @@ public static class PulsarBuilderExtensions /// /// The . /// The name of the resource. This name will be used as the endpoint name when referenced in dependency. - /// The host port of the Pulsar service that the underlying container is bound to when running locally. /// The host port of the Pulsar broker that the underlying container is bound to when running locally. /// A reference to the . public static IResourceBuilder AddPulsar( this IDistributedApplicationBuilder builder, string name, - int? targetPort = null, int? port = null ) { @@ -35,8 +33,8 @@ public static IResourceBuilder AddPulsar( return builder.AddResource(pulsar) .WithImage(PulsarContainerImageTags.Image, PulsarContainerImageTags.Tag) .WithImageRegistry(PulsarContainerImageTags.Registry) - .WithEndpoint(port: targetPort, targetPort: 8080, name: PulsarResource.ServiceEndpointName, scheme: "http") - .WithEndpoint(port: port, targetPort: 6650, name: PulsarResource.BrokerEndpointName, scheme: "pulsar") + .WithEndpoint(targetPort: PulsarResource.ServiceInternalPort, name: PulsarResource.ServiceEndpointName, scheme: "http") + .WithEndpoint(port: port, targetPort: PulsarResource.BrokerInternalPort, name: PulsarResource.BrokerEndpointName, scheme: "pulsar") .WithEntrypoint("/bin/bash") .AsStandalone(); } diff --git a/src/Aspire.Hosting.Apache.Pulsar/PulsarManagerBuilderExtensions.cs b/src/Aspire.Hosting.Apache.Pulsar/PulsarManagerBuilderExtensions.cs index 6ee85c161e..5fc89cbab1 100644 --- a/src/Aspire.Hosting.Apache.Pulsar/PulsarManagerBuilderExtensions.cs +++ b/src/Aspire.Hosting.Apache.Pulsar/PulsarManagerBuilderExtensions.cs @@ -20,15 +20,13 @@ public static class PulsarManagerBuilderExtensions /// /// The for the . /// The name of the resource. This name will be used as the endpoint name when referenced in dependency. - /// The manager frontend port that the underlying container is bound to when running locally. - /// The manager backend port that the underlying container is bound to when running locally. + /// The manager frontend port that the underlying container is bound to when running locally. /// Configuration callback for Pulsar Manager container resource. /// A reference to the for the . public static IResourceBuilder WithPulsarManager( this IResourceBuilder builder, string? name = null, - int? frontendPort = null, - int? backendPort = null, + int? port = null, Action>? configureContainer = null ) { @@ -46,8 +44,8 @@ public static IResourceBuilder WithPulsarManager( var pulsarManagerBuilder = builder.ApplicationBuilder.AddResource(pulsarManager) .WithImage(PulsarManagerContainerImageTags.Image, PulsarManagerContainerImageTags.Tag) .WithImageRegistry(PulsarManagerContainerImageTags.Registry) - .WithEndpoint(port: frontendPort, targetPort: 9527, name: PulsarManagerResource.FrontendEndpointName, scheme: "http") - .WithEndpoint(port: backendPort, targetPort: 7750, name: PulsarManagerResource.BackendEndpointName, scheme: "http"); + .WithEndpoint(port: port, targetPort: PulsarManagerResource.FrontendInternalPort, name: PulsarManagerResource.FrontendEndpointName, scheme: "http") + .WithEndpoint(targetPort: PulsarManagerResource.BackendInternalPort, name: PulsarManagerResource.BackendEndpointName, scheme: "http"); pulsarManagerBuilder .WithEnvironment("SPRING_CONFIGURATION_FILE", "/pulsar-manager/pulsar-manager/application.properties"); diff --git a/src/Aspire.Hosting.Apache.Pulsar/PulsarManagerContainerImageTags.cs b/src/Aspire.Hosting.Apache.Pulsar/PulsarManagerContainerImageTags.cs index 901b081cc1..838baf7eba 100644 --- a/src/Aspire.Hosting.Apache.Pulsar/PulsarManagerContainerImageTags.cs +++ b/src/Aspire.Hosting.Apache.Pulsar/PulsarManagerContainerImageTags.cs @@ -10,6 +10,10 @@ internal static class PulsarManagerContainerImageTags public const string Registry = "docker.io"; public const string Image = "apachepulsar/pulsar-manager"; public const string Tag = "v0.4.0"; + // TODO: Wait new release for user seed via envvars + // Updates: + // - bump tag + // - update application.properties in playground private static readonly Version s_versionThresholdNotSupportingDefaultSuperUserViaEnvVars = new(0, 4, 0); @@ -36,6 +40,7 @@ internal static bool SupportsDefaultSuperUserEnvVars(ContainerImageAnnotation an .TrimStart('v') .ToCharArray() .Where(c => c != '.') + .Select(c => (int)char.GetNumericValue(c)) .ToArray(); Version version = new(versionParts[0], versionParts[1], versionParts[2]); diff --git a/src/Aspire.Hosting.Apache.Pulsar/PulsarManagerResource.cs b/src/Aspire.Hosting.Apache.Pulsar/PulsarManagerResource.cs index 24373cddb8..8d76bf4c3f 100644 --- a/src/Aspire.Hosting.Apache.Pulsar/PulsarManagerResource.cs +++ b/src/Aspire.Hosting.Apache.Pulsar/PulsarManagerResource.cs @@ -8,6 +8,9 @@ namespace Aspire.Hosting.ApplicationModel; /// public class PulsarManagerResource : ContainerResource { + internal const int FrontendInternalPort = 9527; + internal const int BackendInternalPort = 7750; + internal const string FrontendEndpointName = "frontend"; internal const string BackendEndpointName = "backend"; @@ -17,8 +20,8 @@ public class PulsarManagerResource : ContainerResource /// The name of the resource. public PulsarManagerResource(string name) : base(name) { - BackendEndpoint = new(this, FrontendEndpointName); FrontendEndpoint = new(this, FrontendEndpointName); + BackendEndpoint = new(this, BackendEndpointName); } /// diff --git a/src/Aspire.Hosting.Apache.Pulsar/PulsarResource.cs b/src/Aspire.Hosting.Apache.Pulsar/PulsarResource.cs index 51dd0af30e..2a4dea097f 100644 --- a/src/Aspire.Hosting.Apache.Pulsar/PulsarResource.cs +++ b/src/Aspire.Hosting.Apache.Pulsar/PulsarResource.cs @@ -10,9 +10,11 @@ public class PulsarResource(string name) : ContainerResource(name), IResourceWithConnectionString { + internal const int ServiceInternalPort = 8080; internal const string ServiceEndpointName = "service"; private EndpointReference? _serviceEndpoint; + internal const int BrokerInternalPort = 6650; internal const string BrokerEndpointName = "broker"; private EndpointReference? _brokerEndpoint; diff --git a/tests/Aspire.Hosting.Tests/Apache/Pulsar/AddPulsarManagerTests.cs b/tests/Aspire.Hosting.Tests/Apache/Pulsar/AddPulsarManagerTests.cs index 2410db2cb8..158a0381d1 100644 --- a/tests/Aspire.Hosting.Tests/Apache/Pulsar/AddPulsarManagerTests.cs +++ b/tests/Aspire.Hosting.Tests/Apache/Pulsar/AddPulsarManagerTests.cs @@ -58,8 +58,6 @@ public async Task PulsarManagerAddsStandardEnvironmentVariables() var envVars = await GetEnvironmentVariables(builder); Assert.Equal("/pulsar-manager/pulsar-manager/application.properties", envVars["SPRING_CONFIGURATION_FILE"]); - Assert.Equal("{pulsar.bindings.service.url}", envVars["services__pulsar__service__0"]); - Assert.Equal("{pulsar.bindings.broker.url}", envVars["services__pulsar__broker__0"]); } [Fact] @@ -80,7 +78,7 @@ public void PulsarManagerDuplicateInvocationDoesNotCreateNewContainerResource() public void WithBookKeeperVisualManagerAddsMountAnnotation(bool? isReadOnly) { using var builder = TestDistributedApplicationBuilder.Create(); - builder.AddPulsar("pulsar").WithPulsarManager(null, null, null, c => + builder.AddPulsar("pulsar").WithPulsarManager(null, null, c => { if (isReadOnly.HasValue) { @@ -110,7 +108,7 @@ public void WithBookKeeperVisualManagerAddsMountAnnotation(bool? isReadOnly) public void WithApplicationPropertiesAddsMountAnnotation(bool? isReadOnly) { using var builder = TestDistributedApplicationBuilder.Create(); - builder.AddPulsar("pulsar").WithPulsarManager(null, null, null, c => + builder.AddPulsar("pulsar").WithPulsarManager(null, null, c => { if (isReadOnly.HasValue) { @@ -184,7 +182,7 @@ public void PulsarManagerThrowsForUnsupportedImageForDefaultSuperUser(string? im var pulsar = builder.AddPulsar("pulsar"); Assert.Throws(() => pulsar - .WithPulsarManager(null, null, null, + .WithPulsarManager(null, null, c => c .WithImage(image!) .WithImageTag(tag!) @@ -198,7 +196,7 @@ private static async ValueTask> GetEnvironmentVariabl Action>? containerConfiguration = null ) { - builder.AddPulsar("pulsar").WithPulsarManager(null, null, null, containerConfiguration); + builder.AddPulsar("pulsar").WithPulsarManager(null, null, containerConfiguration); await using var app = builder.Build(); var appModel = app.Services.GetRequiredService(); @@ -212,19 +210,16 @@ private static async ValueTask> GetEnvironmentVariabl } [Theory] - [InlineData(null, null)] - [InlineData(6000, null)] - [InlineData(null, 6000)] - [InlineData(6000, 7000)] - public async Task VerifyPulsarManagerManifest(int? frontendPort, int? backendPort) + [InlineData(6000)] + [InlineData(null)] + public async Task VerifyPulsarManagerManifest(int? port) { using var appBuilder = TestDistributedApplicationBuilder.Create(); appBuilder .AddPulsar("pulsar") .WithPulsarManager( "pulsar-manager", - frontendPort: frontendPort, - backendPort: backendPort + port ); var pulsarManager = appBuilder.Resources.OfType().Single(); @@ -238,8 +233,6 @@ public async Task VerifyPulsarManagerManifest(int? frontendPort, int? backendPor "type": "container.v0", "image": "{{{PulsarManagerContainerImageTags.Registry}}}/{{{PulsarManagerContainerImageTags.Image}}}:{{{PulsarManagerContainerImageTags.Tag}}}", "env": { - "services__pulsar__service__0": "{pulsar.bindings.service.url}", - "services__pulsar__broker__0": "{pulsar.bindings.broker.url}", "SPRING_CONFIGURATION_FILE": "/pulsar-manager/pulsar-manager/application.properties" }, "bindings": { @@ -247,14 +240,13 @@ public async Task VerifyPulsarManagerManifest(int? frontendPort, int? backendPor "scheme": "http", "protocol": "tcp", "transport": "http", - {{{PortManifestPart(frontendPort)}}} + {{{PortManifestPart(port)}}} "targetPort": 9527 }, "backend": { "scheme": "http", "protocol": "tcp", "transport": "http", - {{{PortManifestPart(backendPort)}}} "targetPort": 7750 } } diff --git a/tests/Aspire.Hosting.Tests/Apache/Pulsar/AddPulsarTests.cs b/tests/Aspire.Hosting.Tests/Apache/Pulsar/AddPulsarTests.cs index 287e73b5b7..cbd111d2c8 100644 --- a/tests/Aspire.Hosting.Tests/Apache/Pulsar/AddPulsarTests.cs +++ b/tests/Aspire.Hosting.Tests/Apache/Pulsar/AddPulsarTests.cs @@ -182,19 +182,16 @@ public void WithConfigBindMountAddsMountAnnotation(bool? isReadOnly) } [Theory] - [InlineData(null, null)] - [InlineData(6000, null)] - [InlineData(null, 6000)] - [InlineData(6000, 7000)] - public async Task VerifyPulsarManifest(int? brokerPort, int? servicePort) + [InlineData(null)] + [InlineData(6000)] + public async Task VerifyPulsarManifest(int? port) { using var appBuilder = TestDistributedApplicationBuilder.Create(); var manifest = (await ManifestUtils.GetManifest( appBuilder.AddPulsar( name: "pulsar", - targetPort: servicePort, - brokerPort: brokerPort + port ).Resource )).ToString(); @@ -213,14 +210,13 @@ public async Task VerifyPulsarManifest(int? brokerPort, int? servicePort) "scheme": "http", "protocol": "tcp", "transport": "http", - {{PortManifestPart(servicePort)}} "targetPort": 8080 }, "broker": { "scheme": "pulsar", "protocol": "tcp", "transport": "tcp", - {{PortManifestPart(brokerPort)}} + {{PortManifestPart(port)}} "targetPort": 6650 } }