From 8ba79b501b315ab47e7d3e78da747b8e6520a416 Mon Sep 17 00:00:00 2001 From: David Jensen Date: Mon, 29 Jan 2024 14:50:17 +0100 Subject: [PATCH 01/13] feat: Add Apache Pulsar module --- Directory.Packages.props | 1 + Testcontainers.sln | 14 ++ docs/modules/index.md | 1 + docs/modules/pulsar.md | 130 ++++++++++++++++++ src/Testcontainers.Pulsar/.editorconfig | 1 + src/Testcontainers.Pulsar/PulsarBuilder.cs | 113 +++++++++++++++ .../PulsarConfiguration.cs | 68 +++++++++ src/Testcontainers.Pulsar/PulsarContainer.cs | 58 ++++++++ .../Testcontainers.Pulsar.csproj | 14 ++ src/Testcontainers.Pulsar/Usings.cs | 8 ++ .../PulsarContainerTest.cs | 80 +++++++++++ ...sarContainerWithTokenAuthenticationTest.cs | 82 +++++++++++ .../Testcontainers.Pulsar.Tests.csproj | 19 +++ tests/Testcontainers.Pulsar.Tests/Usings.cs | 4 + 14 files changed, 593 insertions(+) create mode 100644 docs/modules/pulsar.md create mode 100644 src/Testcontainers.Pulsar/.editorconfig create mode 100644 src/Testcontainers.Pulsar/PulsarBuilder.cs create mode 100644 src/Testcontainers.Pulsar/PulsarConfiguration.cs create mode 100644 src/Testcontainers.Pulsar/PulsarContainer.cs create mode 100644 src/Testcontainers.Pulsar/Testcontainers.Pulsar.csproj create mode 100644 src/Testcontainers.Pulsar/Usings.cs create mode 100644 tests/Testcontainers.Pulsar.Tests/PulsarContainerTest.cs create mode 100644 tests/Testcontainers.Pulsar.Tests/PulsarContainerWithTokenAuthenticationTest.cs create mode 100644 tests/Testcontainers.Pulsar.Tests/Testcontainers.Pulsar.Tests.csproj create mode 100644 tests/Testcontainers.Pulsar.Tests/Usings.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 039256594..acd75c72b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -34,6 +34,7 @@ + diff --git a/Testcontainers.sln b/Testcontainers.sln index 9595905ed..c06394d83 100644 --- a/Testcontainers.sln +++ b/Testcontainers.sln @@ -195,6 +195,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Tests", "tes EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.WebDriver.Tests", "tests\Testcontainers.WebDriver.Tests\Testcontainers.WebDriver.Tests.csproj", "{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Pulsar", "src\Testcontainers.Pulsar\Testcontainers.Pulsar.csproj", "{27D46863-65B9-4934-B3C8-2383B217A477}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Pulsar.Tests", "tests\Testcontainers.Pulsar.Tests\Testcontainers.Pulsar.Tests.csproj", "{D05FCB31-793E-43E0-BD6C-077013AE9113}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -568,6 +572,14 @@ Global {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Debug|Any CPU.Build.0 = Debug|Any CPU {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Release|Any CPU.ActiveCfg = Release|Any CPU {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Release|Any CPU.Build.0 = Release|Any CPU + {27D46863-65B9-4934-B3C8-2383B217A477}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {27D46863-65B9-4934-B3C8-2383B217A477}.Debug|Any CPU.Build.0 = Debug|Any CPU + {27D46863-65B9-4934-B3C8-2383B217A477}.Release|Any CPU.ActiveCfg = Release|Any CPU + {27D46863-65B9-4934-B3C8-2383B217A477}.Release|Any CPU.Build.0 = Release|Any CPU + {D05FCB31-793E-43E0-BD6C-077013AE9113}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D05FCB31-793E-43E0-BD6C-077013AE9113}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D05FCB31-793E-43E0-BD6C-077013AE9113}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D05FCB31-793E-43E0-BD6C-077013AE9113}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {5365F780-0E6C-41F0-B1B9-7DC34368F80C} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} @@ -661,5 +673,7 @@ Global {1A1983E6-5297-435F-B467-E8E1F11277D6} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {27CDB869-A150-4593-958F-6F26E5391E7C} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} + {27D46863-65B9-4934-B3C8-2383B217A477} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} + {D05FCB31-793E-43E0-BD6C-077013AE9113} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} EndGlobalSection EndGlobal diff --git a/docs/modules/index.md b/docs/modules/index.md index e533faecf..dd76e92b5 100644 --- a/docs/modules/index.md +++ b/docs/modules/index.md @@ -56,6 +56,7 @@ await moduleNameContainer.StartAsync(); | Papercut | `jijiechen/papercut:latest` | [NuGet](https://www.nuget.org/packages/Testcontainers.Papercut) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Papercut) | | PostgreSQL | `postgres:15.1` | [NuGet](https://www.nuget.org/packages/Testcontainers.PostgreSql) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.PostgreSql) | | PubSub | `gcr.io/google.com/cloudsdktool/google-cloud-cli:446.0.1-emulators` | [NuGet](https://www.nuget.org/packages/Testcontainers.PubSub) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.PubSub) | +| Pulsar | `apachepulsar/pulsar:3.0.2` | [NuGet](https://www.nuget.org/packages/Testcontainers.Pulsar) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Pulsar) | | RabbitMQ | `rabbitmq:3.11` | [NuGet](https://www.nuget.org/packages/Testcontainers.RabbitMq) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.RabbitMq) | | RavenDB | `ravendb/ravendb:5.4-ubuntu-latest` | [NuGet](https://www.nuget.org/packages/Testcontainers.RavenDb) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.RavenDb) | | Redis | `redis:7.0` | [NuGet](https://www.nuget.org/packages/Testcontainers.Redis) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Redis) | diff --git a/docs/modules/pulsar.md b/docs/modules/pulsar.md new file mode 100644 index 000000000..e42253bb4 --- /dev/null +++ b/docs/modules/pulsar.md @@ -0,0 +1,130 @@ +# Apache Pulsar Module + +Testcontainers can be used to automatically create [Apache Pulsar](https://pulsar.apache.org) containers without external services. + +It's based on the official Apache Pulsar docker image, it is recommended to read the [official guide](https://pulsar.apache.org/docs/next/getting-started-docker/). + +The following example uses the following NuGet packages: + +```console title="Install the NuGet dependencies" +dotnet add package Testcontainers.Pulsar +dotnet add package DotPulsar +dotnet add package xunit +``` +IDEs and editors may also require the following packages to run tests: `xunit.runner.visualstudio` and `Microsoft.NET.Test.Sdk`. + +Copy and paste the following code into a new `.cs` test file within an existing test project. + +```csharp +using System.Collections.Generic; +using System.Threading; +using DotPulsar; +using DotPulsar.Abstractions; +using DotPulsar.Extensions; +using Xunit.Abstractions; + +namespace Testcontainers.Pulsar.Tests; + +public sealed class PulsarContainerTest : IAsyncLifetime +{ + private readonly CancellationTokenSource _cts; + private readonly PulsarContainer _pulsarContainer; + private readonly ITestOutputHelper _testOutputHelper; + + public PulsarContainerTest(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + _pulsarContainer = new PulsarBuilder().Build(); + _cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); + } + + public Task InitializeAsync() + { + return _pulsarContainer.StartAsync(); + } + + public Task DisposeAsync() + { + return _pulsarContainer.DisposeAsync().AsTask(); + } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public async Task PulsarContainer_WhenBrokerIsStarted_ShouldConnect() + { + // Given + await using var client = CreateClient(); + var expected = new List { MessageId.Earliest }; + await using var reader = CreateReader(client, MessageId.Earliest, await CreateTopic(_cts.Token)); + + // When + var actual = await reader.GetLastMessageIds(_cts.Token); + + // Then + Assert.Equal(expected,actual); + } + + private IReader CreateReader(IPulsarClient pulsarClient, MessageId messageId, string topicName) + => pulsarClient.NewReader(Schema.String) + .StartMessageId(messageId) + .Topic(topicName) + .Create(); + + private static string CreateTopicName() => $"persistent://public/default/{Guid.NewGuid():N}"; + + private async Task CreateTopic(string topic, CancellationToken cancellationToken) + { + var arguments = $"bin/pulsar-admin topics create {topic}"; + + var result = await _pulsarContainer.ExecAsync(new[] { "/bin/bash", "-c", arguments }, cancellationToken); + + if (result.ExitCode != 0) + throw new Exception($"Could not create the topic: {result.Stderr}"); + } + + private async Task CreateTopic(CancellationToken cancellationToken) + { + var topic = CreateTopicName(); + await CreateTopic(topic, cancellationToken); + return topic; + } + + private IPulsarClient CreateClient() + => PulsarClient + .Builder() + .ExceptionHandler(context => _testOutputHelper.WriteLine($"PulsarClient got an exception: {context.Exception}")) + .ServiceUrl(new Uri(_pulsarContainer.GetPulsarBrokerUrl())) + .Build(); +} +``` + +To execute the test, use the command `dotnet test` from a terminal. + +## Builder + +### Token authentication +If you need to use token authentication use the follow with method in the builder +```csharp +PulsarBuilder().WithTokenAuthentication().Build(); +``` + +and get the token by using +```csharp +var token = await _pulsarContainer.CreateToken(Timeout.InfiniteTimeSpan); +``` + +#### Pulsar Functions +If you need to use Pulsar Functions use the follow with method in the builder +```csharp +PulsarBuilder().WithFunctions().Build(); +``` +## Access Pulsar +To get the the Pulsar broker url. +```csharp +string pulsarBrokerUrl = _pulsarContainer.GetPulsarBrokerUrl(); +``` + +To get the the Pulsar service url. +```csharp +string pulsarBrokerUrl = _pulsarContainer.GetHttpServiceUrl(); +``` diff --git a/src/Testcontainers.Pulsar/.editorconfig b/src/Testcontainers.Pulsar/.editorconfig new file mode 100644 index 000000000..6f066619d --- /dev/null +++ b/src/Testcontainers.Pulsar/.editorconfig @@ -0,0 +1 @@ +root = true \ No newline at end of file diff --git a/src/Testcontainers.Pulsar/PulsarBuilder.cs b/src/Testcontainers.Pulsar/PulsarBuilder.cs new file mode 100644 index 000000000..3480e3ba7 --- /dev/null +++ b/src/Testcontainers.Pulsar/PulsarBuilder.cs @@ -0,0 +1,113 @@ +using System.Text; +using Testcontainers.Pulsar; + +/// +[PublicAPI] +public sealed class PulsarBuilder : ContainerBuilder +{ + private const string AuthenticationPlugin = "org.apache.pulsar.client.impl.auth.AuthenticationToken"; + private const string SecretKeyPath = "/pulsar/secret.key"; + private const string UserName = "test-user"; + private const string PulsarImage = "apachepulsar/pulsar:3.0.2"; + private const string AdminClustersEndpoint = "/admin/v2/clusters"; + internal const string Enabled = "Enabled"; + + private Dictionary _environmentVariables = new Dictionary + { + { "PULSAR_PREFIX_tokenSecretKey", $"file://{SecretKeyPath}" }, + { "PULSAR_PREFIX_authenticationRefreshCheckSeconds", "5" }, + { "superUserRoles", UserName }, + { "authenticationEnabled", "true" }, + { "authorizationEnabled", "true" }, + { "authenticationProviders", "org.apache.pulsar.broker.authentication.AuthenticationProviderToken" }, + { "authenticateOriginalAuthData", "false" }, + { "brokerClientAuthenticationPlugin", AuthenticationPlugin }, + { "CLIENT_PREFIX_authPlugin", AuthenticationPlugin } + }; + + public const ushort PulsarBrokerPort = 6650; + public const ushort PulsarBrokerHttpPort = 8080; + + /// + /// Initializes a new instance of the class. + /// + public PulsarBuilder() + : this(new PulsarConfiguration()) + { + DockerResourceConfiguration = Init().DockerResourceConfiguration; + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + private PulsarBuilder(PulsarConfiguration resourceConfiguration) + : base(resourceConfiguration) + { + DockerResourceConfiguration = resourceConfiguration; + } + + /// + protected override PulsarConfiguration DockerResourceConfiguration { get; } + + /// + public override PulsarContainer Build() + { + Validate(); + var pulsarStartupCommands = String.Empty; + if (DockerResourceConfiguration.Authentication == Enabled) + { + pulsarStartupCommands = $"bin/pulsar tokens create-secret-key --output {SecretKeyPath} && " + + $"export brokerClientAuthenticationParameters=token:$(bin/pulsar tokens create --secret-key {SecretKeyPath} --subject {UserName}) && " + + $"export CLIENT_PREFIX_authParams=$brokerClientAuthenticationParameters && bin/apply-config-from-env.py conf/standalone.conf && " + + $"bin/apply-config-from-env-with-prefix.py CLIENT_PREFIX_ conf/client.conf && "; + } + pulsarStartupCommands += "bin/pulsar standalone"; + + if (DockerResourceConfiguration.Functions != Enabled) + pulsarStartupCommands += " --no-functions-worker"; + + var pulsarBuilder = WithCommand("/bin/bash", "-c",pulsarStartupCommands); + return new PulsarContainer(pulsarBuilder.DockerResourceConfiguration, TestcontainersSettings.Logger); + } + + /// + protected override PulsarBuilder Init() + { + return base.Init() + .WithImage(PulsarImage) + .WithPortBinding(PulsarBrokerPort, true) + .WithPortBinding(PulsarBrokerHttpPort, true) + .WithWaitStrategy(Wait.ForUnixContainer() + .UntilCommandIsCompleted(["/bin/bash", "-c", "bin/pulsar-admin clusters list"])); + } + + public PulsarBuilder WithTokenAuthentication() + { + return Merge(DockerResourceConfiguration, new PulsarConfiguration(authentication: Enabled)) + .WithEnvironment(_environmentVariables); + } + + public PulsarBuilder WithFunctions() + { + return Merge(DockerResourceConfiguration, new PulsarConfiguration(functions: Enabled)); + } + + /// + protected override PulsarBuilder Clone(IResourceConfiguration resourceConfiguration) + { + return Merge(DockerResourceConfiguration, new PulsarConfiguration(resourceConfiguration)); + } + + /// + protected override PulsarBuilder Clone(IContainerConfiguration resourceConfiguration) + { + return Merge(DockerResourceConfiguration, new PulsarConfiguration(resourceConfiguration)); + } + + /// + protected override PulsarBuilder Merge(PulsarConfiguration oldValue, PulsarConfiguration newValue) + { + return new PulsarBuilder(new PulsarConfiguration(oldValue, newValue)); + } +} \ No newline at end of file diff --git a/src/Testcontainers.Pulsar/PulsarConfiguration.cs b/src/Testcontainers.Pulsar/PulsarConfiguration.cs new file mode 100644 index 000000000..87e591aa1 --- /dev/null +++ b/src/Testcontainers.Pulsar/PulsarConfiguration.cs @@ -0,0 +1,68 @@ +namespace Testcontainers.Pulsar; + +/// +[PublicAPI] +public sealed class PulsarConfiguration : ContainerConfiguration +{ + /// + /// Initializes a new instance of the class. + /// + public PulsarConfiguration(string authentication = null, + string functions = null) + { + Authentication = authentication; + Functions = functions; + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public PulsarConfiguration(IResourceConfiguration resourceConfiguration) + : base(resourceConfiguration) + { + // Passes the configuration upwards to the base implementations to create an updated immutable copy. + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public PulsarConfiguration(IContainerConfiguration resourceConfiguration) + : base(resourceConfiguration) + { + // Passes the configuration upwards to the base implementations to create an updated immutable copy. + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public PulsarConfiguration(PulsarConfiguration resourceConfiguration) + : this(new PulsarConfiguration(), resourceConfiguration) + { + // Passes the configuration upwards to the base implementations to create an updated immutable copy. + } + + /// + /// Initializes a new instance of the class. + /// + /// The old Docker resource configuration. + /// The new Docker resource configuration. + public PulsarConfiguration(PulsarConfiguration oldValue, PulsarConfiguration newValue) + : base(oldValue, newValue) + { + Authentication = BuildConfiguration.Combine(oldValue.Authentication, newValue.Authentication); + Functions = BuildConfiguration.Combine(oldValue.Functions, newValue.Functions); + } + + /// + /// Gets authentication. + /// + public string Authentication { get; } + + /// + /// Gets functions. + /// + public string Functions { get; } +} \ No newline at end of file diff --git a/src/Testcontainers.Pulsar/PulsarContainer.cs b/src/Testcontainers.Pulsar/PulsarContainer.cs new file mode 100644 index 000000000..a91ad7490 --- /dev/null +++ b/src/Testcontainers.Pulsar/PulsarContainer.cs @@ -0,0 +1,58 @@ +namespace Testcontainers.Pulsar; + +/// +[PublicAPI] +public sealed class PulsarContainer : DockerContainer +{ + private readonly PulsarConfiguration _configuration; + + /// + /// Initializes a new instance of the class. + /// + /// The container configuration. + /// The logger. + public PulsarContainer(PulsarConfiguration configuration, ILogger logger) + : base(configuration, logger) + { + _configuration = configuration; + } + + /// + /// Gets the Pulsar broker url. + /// + /// The Pulsar broker url. + public string GetPulsarBrokerUrl() => + new UriBuilder("pulsar://", Hostname, GetMappedPublicPort(PulsarBuilder.PulsarBrokerPort)).ToString(); + + /// + /// Gets the Pulsar service url. + /// + /// The Pulsar service url. + public string GetHttpServiceUrl() => + new UriBuilder("http", Hostname, GetMappedPublicPort(PulsarBuilder.PulsarBrokerHttpPort)).ToString(); + + /// + /// Creates Authentication token + /// + /// Relative expiry time for the token (eg: 1h, 3d, 10y) + /// + /// Authentication token + /// + public async Task CreateToken(TimeSpan expiryTime, CancellationToken cancellationToken = default) + { + if (_configuration.Authentication != PulsarBuilder.Enabled) + throw new Exception($"Could not create the token, because WithAuthentication is not used."); + + var arguments = $"bin/pulsar tokens create --secret-key /pulsar/secret.key --subject test-user"; + + if (expiryTime != Timeout.InfiniteTimeSpan) + arguments += $" --expiry-time {expiryTime.TotalSeconds}s"; + + var result = await ExecAsync(new[] { "/bin/bash", "-c", arguments }, cancellationToken); + + if (result.ExitCode != 0) + throw new Exception($"Could not create the token: {result.Stderr}"); + + return result.Stdout; + } +} \ No newline at end of file diff --git a/src/Testcontainers.Pulsar/Testcontainers.Pulsar.csproj b/src/Testcontainers.Pulsar/Testcontainers.Pulsar.csproj new file mode 100644 index 000000000..22c201030 --- /dev/null +++ b/src/Testcontainers.Pulsar/Testcontainers.Pulsar.csproj @@ -0,0 +1,14 @@ + + + enable + enable + netstandard2.0;netstandard2.1 + latest + + + + + + + + \ No newline at end of file diff --git a/src/Testcontainers.Pulsar/Usings.cs b/src/Testcontainers.Pulsar/Usings.cs new file mode 100644 index 000000000..ca872e8d7 --- /dev/null +++ b/src/Testcontainers.Pulsar/Usings.cs @@ -0,0 +1,8 @@ +global using System; +global using Docker.DotNet.Models; +global using DotNet.Testcontainers; +global using DotNet.Testcontainers.Builders; +global using DotNet.Testcontainers.Configurations; +global using DotNet.Testcontainers.Containers; +global using JetBrains.Annotations; +global using Microsoft.Extensions.Logging; diff --git a/tests/Testcontainers.Pulsar.Tests/PulsarContainerTest.cs b/tests/Testcontainers.Pulsar.Tests/PulsarContainerTest.cs new file mode 100644 index 000000000..931e706a6 --- /dev/null +++ b/tests/Testcontainers.Pulsar.Tests/PulsarContainerTest.cs @@ -0,0 +1,80 @@ +using System.Collections.Generic; +using System.Threading; +using DotPulsar; +using DotPulsar.Abstractions; +using DotPulsar.Extensions; +using Xunit.Abstractions; + +namespace Testcontainers.Pulsar.Tests; + +public sealed class PulsarContainerTest : IAsyncLifetime +{ + private readonly CancellationTokenSource _cts; + private readonly PulsarContainer _pulsarContainer; + private readonly ITestOutputHelper _testOutputHelper; + + public PulsarContainerTest(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + _pulsarContainer = new PulsarBuilder().Build(); + _cts = new CancellationTokenSource(TimeSpan.FromMinutes(2)); + } + + public Task InitializeAsync() + { + return _pulsarContainer.StartAsync(); + } + + public Task DisposeAsync() + { + return _pulsarContainer.DisposeAsync().AsTask(); + } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public async Task PulsarContainer_WhenBrokerIsStarted_ShouldConnect() + { + // Given + await using var client = CreateClient(); + var expected = new List { MessageId.Earliest }; + await using var reader = CreateReader(client, MessageId.Earliest, await CreateTopic(_cts.Token)); + + // When + var actual = await reader.GetLastMessageIds(_cts.Token); + + // Then + Assert.Equal(expected,actual); + } + + private IReader CreateReader(IPulsarClient pulsarClient, MessageId messageId, string topicName) + => pulsarClient.NewReader(Schema.String) + .StartMessageId(messageId) + .Topic(topicName) + .Create(); + + private static string CreateTopicName() => $"persistent://public/default/{Guid.NewGuid():N}"; + + private async Task CreateTopic(string topic, CancellationToken cancellationToken) + { + var arguments = $"bin/pulsar-admin topics create {topic}"; + + var result = await _pulsarContainer.ExecAsync(new[] { "/bin/bash", "-c", arguments }, cancellationToken); + + if (result.ExitCode != 0) + throw new Exception($"Could not create the topic: {result.Stderr}"); + } + + private async Task CreateTopic(CancellationToken cancellationToken) + { + var topic = CreateTopicName(); + await CreateTopic(topic, cancellationToken); + return topic; + } + + private IPulsarClient CreateClient() + => PulsarClient + .Builder() + .ExceptionHandler(context => _testOutputHelper.WriteLine($"PulsarClient got an exception: {context.Exception}")) + .ServiceUrl(new Uri(_pulsarContainer.GetPulsarBrokerUrl())) + .Build(); +} diff --git a/tests/Testcontainers.Pulsar.Tests/PulsarContainerWithTokenAuthenticationTest.cs b/tests/Testcontainers.Pulsar.Tests/PulsarContainerWithTokenAuthenticationTest.cs new file mode 100644 index 000000000..db250d8d1 --- /dev/null +++ b/tests/Testcontainers.Pulsar.Tests/PulsarContainerWithTokenAuthenticationTest.cs @@ -0,0 +1,82 @@ +using System.Collections.Generic; +using System.Threading; +using DotPulsar; +using DotPulsar.Abstractions; +using DotPulsar.Extensions; +using DotPulsar.Internal; +using Xunit.Abstractions; + +namespace Testcontainers.Pulsar.Tests; + +public sealed class PulsarContainerWithTokenAuthenticationTest : IAsyncLifetime +{ + private readonly CancellationTokenSource _cts; + private readonly PulsarContainer _pulsarContainer; + private readonly ITestOutputHelper _testOutputHelper; + + public PulsarContainerWithTokenAuthenticationTest(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + _pulsarContainer = new PulsarBuilder().WithTokenAuthentication().Build(); + _cts = new CancellationTokenSource(TimeSpan.FromMinutes(2)); + } + + public Task InitializeAsync() + { + return _pulsarContainer.StartAsync(); + } + + public Task DisposeAsync() + { + return _pulsarContainer.DisposeAsync().AsTask(); + } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public async Task PulsarContainer_WhenBrokerWithTokenAuthenticationIsStarted_ShouldConnect() + { + // Given + await using var client = CreateClient(); + var expected = new List { MessageId.Earliest }; + await using var reader = CreateReader(client, MessageId.Earliest, await CreateTopic(_cts.Token)); + + // When + var actual = await reader.GetLastMessageIds(_cts.Token); + + // Then + Assert.Equal(expected,actual); + } + + private IReader CreateReader(IPulsarClient pulsarClient, MessageId messageId, string topicName) + => pulsarClient.NewReader(Schema.String) + .StartMessageId(messageId) + .Topic(topicName) + .Create(); + + private static string CreateTopicName() => $"persistent://public/default/{Guid.NewGuid():N}"; + + private async Task CreateTopic(string topic, CancellationToken cancellationToken) + { + var arguments = $"bin/pulsar-admin topics create {topic}"; + + var result = await _pulsarContainer.ExecAsync(new[] { "/bin/bash", "-c", arguments }, cancellationToken); + + if (result.ExitCode != 0) + throw new Exception($"Could not create the topic: {result.Stderr}"); + } + + private async Task CreateTopic(CancellationToken cancellationToken) + { + var topic = CreateTopicName(); + await CreateTopic(topic, cancellationToken); + return topic; + } + + private IPulsarClient CreateClient() + => PulsarClient + .Builder() + .Authentication(new TokenAuthentication(_pulsarContainer.CreateToken(Timeout.InfiniteTimeSpan).Result)) + .ExceptionHandler(context => _testOutputHelper.WriteLine($"PulsarClient got an exception: {context.Exception}")) + .ServiceUrl(new Uri(_pulsarContainer.GetPulsarBrokerUrl())) + .Build(); +} diff --git a/tests/Testcontainers.Pulsar.Tests/Testcontainers.Pulsar.Tests.csproj b/tests/Testcontainers.Pulsar.Tests/Testcontainers.Pulsar.Tests.csproj new file mode 100644 index 000000000..e0f99e2c4 --- /dev/null +++ b/tests/Testcontainers.Pulsar.Tests/Testcontainers.Pulsar.Tests.csproj @@ -0,0 +1,19 @@ + + + net8.0 + false + false + true + + + + + + + + + + + + + diff --git a/tests/Testcontainers.Pulsar.Tests/Usings.cs b/tests/Testcontainers.Pulsar.Tests/Usings.cs new file mode 100644 index 000000000..76e137860 --- /dev/null +++ b/tests/Testcontainers.Pulsar.Tests/Usings.cs @@ -0,0 +1,4 @@ +global using DotNet.Testcontainers.Commons; +global using Xunit; +global using System; +global using System.Threading.Tasks; From 29d590670e2c89fddb57cd966be7be835a47a2fd Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Thu, 22 Feb 2024 20:38:02 +0100 Subject: [PATCH 02/13] chore: Align Pular module with common module config/pattern --- Testcontainers.sln | 28 +++++++-------- src/Testcontainers.Pulsar/PulsarBuilder.cs | 13 ++++--- .../Testcontainers.Pulsar.csproj | 14 ++++---- src/Testcontainers.Pulsar/Usings.cs | 3 +- .../Testcontainers.Pulsar.Tests/.editorconfig | 1 + .../PulsarContainerTest.cs | 9 +---- ...sarContainerWithTokenAuthenticationTest.cs | 10 +----- .../Testcontainers.Pulsar.Tests.csproj | 35 +++++++++---------- tests/Testcontainers.Pulsar.Tests/Usings.cs | 11 ++++-- 9 files changed, 56 insertions(+), 68 deletions(-) create mode 100644 tests/Testcontainers.Pulsar.Tests/.editorconfig diff --git a/Testcontainers.sln b/Testcontainers.sln index c06394d83..e8c10a811 100644 --- a/Testcontainers.sln +++ b/Testcontainers.sln @@ -85,6 +85,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.PostgreSql", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.PubSub", "src\Testcontainers.PubSub\Testcontainers.PubSub.csproj", "{E6642255-667D-476B-B584-089AA5E6C0B1}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Pulsar", "src\Testcontainers.Pulsar\Testcontainers.Pulsar.csproj", "{27D46863-65B9-4934-B3C8-2383B217A477}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.RabbitMq", "src\Testcontainers.RabbitMq\Testcontainers.RabbitMq.csproj", "{A6D480BC-FDE8-4B92-A2A6-FF16BEE486AE}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.RavenDb", "src\Testcontainers.RavenDb\Testcontainers.RavenDb.csproj", "{F6394475-D6F1-46E2-81BF-4BA78A40B878}" @@ -179,6 +181,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.PostgreSql.T EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.PubSub.Tests", "tests\Testcontainers.PubSub.Tests\Testcontainers.PubSub.Tests.csproj", "{0F86BCE8-62E1-4BFC-AA84-63C7514C90AC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Pulsar.Tests", "tests\Testcontainers.Pulsar.Tests\Testcontainers.Pulsar.Tests.csproj", "{D05FCB31-793E-43E0-BD6C-077013AE9113}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.RabbitMq.Tests", "tests\Testcontainers.RabbitMq.Tests\Testcontainers.RabbitMq.Tests.csproj", "{19564567-1736-4626-B406-17E4E02F18B2}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.RavenDb.Tests", "tests\Testcontainers.RavenDb.Tests\Testcontainers.RavenDb.Tests.csproj", "{D53726B6-5447-47E6-B881-A44EFF6E5534}" @@ -195,10 +199,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Tests", "tes EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.WebDriver.Tests", "tests\Testcontainers.WebDriver.Tests\Testcontainers.WebDriver.Tests.csproj", "{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Pulsar", "src\Testcontainers.Pulsar\Testcontainers.Pulsar.csproj", "{27D46863-65B9-4934-B3C8-2383B217A477}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Pulsar.Tests", "tests\Testcontainers.Pulsar.Tests\Testcontainers.Pulsar.Tests.csproj", "{D05FCB31-793E-43E0-BD6C-077013AE9113}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -352,6 +352,10 @@ Global {E6642255-667D-476B-B584-089AA5E6C0B1}.Debug|Any CPU.Build.0 = Debug|Any CPU {E6642255-667D-476B-B584-089AA5E6C0B1}.Release|Any CPU.ActiveCfg = Release|Any CPU {E6642255-667D-476B-B584-089AA5E6C0B1}.Release|Any CPU.Build.0 = Release|Any CPU + {27D46863-65B9-4934-B3C8-2383B217A477}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {27D46863-65B9-4934-B3C8-2383B217A477}.Debug|Any CPU.Build.0 = Debug|Any CPU + {27D46863-65B9-4934-B3C8-2383B217A477}.Release|Any CPU.ActiveCfg = Release|Any CPU + {27D46863-65B9-4934-B3C8-2383B217A477}.Release|Any CPU.Build.0 = Release|Any CPU {A6D480BC-FDE8-4B92-A2A6-FF16BEE486AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A6D480BC-FDE8-4B92-A2A6-FF16BEE486AE}.Debug|Any CPU.Build.0 = Debug|Any CPU {A6D480BC-FDE8-4B92-A2A6-FF16BEE486AE}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -540,6 +544,10 @@ Global {0F86BCE8-62E1-4BFC-AA84-63C7514C90AC}.Debug|Any CPU.Build.0 = Debug|Any CPU {0F86BCE8-62E1-4BFC-AA84-63C7514C90AC}.Release|Any CPU.ActiveCfg = Release|Any CPU {0F86BCE8-62E1-4BFC-AA84-63C7514C90AC}.Release|Any CPU.Build.0 = Release|Any CPU + {D05FCB31-793E-43E0-BD6C-077013AE9113}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D05FCB31-793E-43E0-BD6C-077013AE9113}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D05FCB31-793E-43E0-BD6C-077013AE9113}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D05FCB31-793E-43E0-BD6C-077013AE9113}.Release|Any CPU.Build.0 = Release|Any CPU {19564567-1736-4626-B406-17E4E02F18B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {19564567-1736-4626-B406-17E4E02F18B2}.Debug|Any CPU.Build.0 = Debug|Any CPU {19564567-1736-4626-B406-17E4E02F18B2}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -572,14 +580,6 @@ Global {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Debug|Any CPU.Build.0 = Debug|Any CPU {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Release|Any CPU.ActiveCfg = Release|Any CPU {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Release|Any CPU.Build.0 = Release|Any CPU - {27D46863-65B9-4934-B3C8-2383B217A477}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {27D46863-65B9-4934-B3C8-2383B217A477}.Debug|Any CPU.Build.0 = Debug|Any CPU - {27D46863-65B9-4934-B3C8-2383B217A477}.Release|Any CPU.ActiveCfg = Release|Any CPU - {27D46863-65B9-4934-B3C8-2383B217A477}.Release|Any CPU.Build.0 = Release|Any CPU - {D05FCB31-793E-43E0-BD6C-077013AE9113}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D05FCB31-793E-43E0-BD6C-077013AE9113}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D05FCB31-793E-43E0-BD6C-077013AE9113}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D05FCB31-793E-43E0-BD6C-077013AE9113}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {5365F780-0E6C-41F0-B1B9-7DC34368F80C} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} @@ -618,6 +618,7 @@ Global {464F1120-A0DA-462B-B9E8-45176D883625} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {8AB91636-9055-4900-A72A-7CFFACDFDBF0} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {E6642255-667D-476B-B584-089AA5E6C0B1} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} + {27D46863-65B9-4934-B3C8-2383B217A477} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {A6D480BC-FDE8-4B92-A2A6-FF16BEE486AE} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {F6394475-D6F1-46E2-81BF-4BA78A40B878} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {BFDA179A-40EB-4CEB-B8E9-0DF32C65E2C5} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} @@ -665,6 +666,7 @@ Global {3E55CBE8-AFE8-426D-9470-49D63CD1051C} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {56D0DCA5-567F-4B3B-8B79-CB108F8EB8A6} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {0F86BCE8-62E1-4BFC-AA84-63C7514C90AC} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} + {D05FCB31-793E-43E0-BD6C-077013AE9113} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {19564567-1736-4626-B406-17E4E02F18B2} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {D53726B6-5447-47E6-B881-A44EFF6E5534} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {31EE94A0-E721-4073-B6F1-DD912D004DEF} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} @@ -673,7 +675,5 @@ Global {1A1983E6-5297-435F-B467-E8E1F11277D6} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {27CDB869-A150-4593-958F-6F26E5391E7C} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} - {27D46863-65B9-4934-B3C8-2383B217A477} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} - {D05FCB31-793E-43E0-BD6C-077013AE9113} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} EndGlobalSection EndGlobal diff --git a/src/Testcontainers.Pulsar/PulsarBuilder.cs b/src/Testcontainers.Pulsar/PulsarBuilder.cs index 3480e3ba7..574f55a0f 100644 --- a/src/Testcontainers.Pulsar/PulsarBuilder.cs +++ b/src/Testcontainers.Pulsar/PulsarBuilder.cs @@ -1,5 +1,4 @@ -using System.Text; -using Testcontainers.Pulsar; +namespace Testcontainers.Pulsar; /// [PublicAPI] @@ -57,16 +56,16 @@ public override PulsarContainer Build() var pulsarStartupCommands = String.Empty; if (DockerResourceConfiguration.Authentication == Enabled) { - pulsarStartupCommands = $"bin/pulsar tokens create-secret-key --output {SecretKeyPath} && " + + pulsarStartupCommands = $"bin/pulsar tokens create-secret-key --output {SecretKeyPath} && " + $"export brokerClientAuthenticationParameters=token:$(bin/pulsar tokens create --secret-key {SecretKeyPath} --subject {UserName}) && " + $"export CLIENT_PREFIX_authParams=$brokerClientAuthenticationParameters && bin/apply-config-from-env.py conf/standalone.conf && " + $"bin/apply-config-from-env-with-prefix.py CLIENT_PREFIX_ conf/client.conf && "; } pulsarStartupCommands += "bin/pulsar standalone"; - - if (DockerResourceConfiguration.Functions != Enabled) + + if (DockerResourceConfiguration.Functions != Enabled) pulsarStartupCommands += " --no-functions-worker"; - + var pulsarBuilder = WithCommand("/bin/bash", "-c",pulsarStartupCommands); return new PulsarContainer(pulsarBuilder.DockerResourceConfiguration, TestcontainersSettings.Logger); } @@ -92,7 +91,7 @@ public PulsarBuilder WithFunctions() { return Merge(DockerResourceConfiguration, new PulsarConfiguration(functions: Enabled)); } - + /// protected override PulsarBuilder Clone(IResourceConfiguration resourceConfiguration) { diff --git a/src/Testcontainers.Pulsar/Testcontainers.Pulsar.csproj b/src/Testcontainers.Pulsar/Testcontainers.Pulsar.csproj index 22c201030..a2342ade1 100644 --- a/src/Testcontainers.Pulsar/Testcontainers.Pulsar.csproj +++ b/src/Testcontainers.Pulsar/Testcontainers.Pulsar.csproj @@ -1,14 +1,12 @@  - enable - enable netstandard2.0;netstandard2.1 latest - - - - - - + + + + + + \ No newline at end of file diff --git a/src/Testcontainers.Pulsar/Usings.cs b/src/Testcontainers.Pulsar/Usings.cs index ca872e8d7..ec17f5aba 100644 --- a/src/Testcontainers.Pulsar/Usings.cs +++ b/src/Testcontainers.Pulsar/Usings.cs @@ -1,8 +1,7 @@ global using System; global using Docker.DotNet.Models; -global using DotNet.Testcontainers; global using DotNet.Testcontainers.Builders; global using DotNet.Testcontainers.Configurations; global using DotNet.Testcontainers.Containers; global using JetBrains.Annotations; -global using Microsoft.Extensions.Logging; +global using Microsoft.Extensions.Logging; \ No newline at end of file diff --git a/tests/Testcontainers.Pulsar.Tests/.editorconfig b/tests/Testcontainers.Pulsar.Tests/.editorconfig new file mode 100644 index 000000000..6f066619d --- /dev/null +++ b/tests/Testcontainers.Pulsar.Tests/.editorconfig @@ -0,0 +1 @@ +root = true \ No newline at end of file diff --git a/tests/Testcontainers.Pulsar.Tests/PulsarContainerTest.cs b/tests/Testcontainers.Pulsar.Tests/PulsarContainerTest.cs index 931e706a6..80c8e6adc 100644 --- a/tests/Testcontainers.Pulsar.Tests/PulsarContainerTest.cs +++ b/tests/Testcontainers.Pulsar.Tests/PulsarContainerTest.cs @@ -1,11 +1,4 @@ -using System.Collections.Generic; -using System.Threading; -using DotPulsar; -using DotPulsar.Abstractions; -using DotPulsar.Extensions; -using Xunit.Abstractions; - -namespace Testcontainers.Pulsar.Tests; +namespace Testcontainers.Pulsar; public sealed class PulsarContainerTest : IAsyncLifetime { diff --git a/tests/Testcontainers.Pulsar.Tests/PulsarContainerWithTokenAuthenticationTest.cs b/tests/Testcontainers.Pulsar.Tests/PulsarContainerWithTokenAuthenticationTest.cs index db250d8d1..c9b7127c4 100644 --- a/tests/Testcontainers.Pulsar.Tests/PulsarContainerWithTokenAuthenticationTest.cs +++ b/tests/Testcontainers.Pulsar.Tests/PulsarContainerWithTokenAuthenticationTest.cs @@ -1,12 +1,4 @@ -using System.Collections.Generic; -using System.Threading; -using DotPulsar; -using DotPulsar.Abstractions; -using DotPulsar.Extensions; -using DotPulsar.Internal; -using Xunit.Abstractions; - -namespace Testcontainers.Pulsar.Tests; +namespace Testcontainers.Pulsar; public sealed class PulsarContainerWithTokenAuthenticationTest : IAsyncLifetime { diff --git a/tests/Testcontainers.Pulsar.Tests/Testcontainers.Pulsar.Tests.csproj b/tests/Testcontainers.Pulsar.Tests/Testcontainers.Pulsar.Tests.csproj index e0f99e2c4..7faabcd5d 100644 --- a/tests/Testcontainers.Pulsar.Tests/Testcontainers.Pulsar.Tests.csproj +++ b/tests/Testcontainers.Pulsar.Tests/Testcontainers.Pulsar.Tests.csproj @@ -1,19 +1,18 @@ - - net8.0 - false - false - true - - - - - - - - - - - - - + + net8.0 + false + false + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Testcontainers.Pulsar.Tests/Usings.cs b/tests/Testcontainers.Pulsar.Tests/Usings.cs index 76e137860..a3dffa9da 100644 --- a/tests/Testcontainers.Pulsar.Tests/Usings.cs +++ b/tests/Testcontainers.Pulsar.Tests/Usings.cs @@ -1,4 +1,11 @@ -global using DotNet.Testcontainers.Commons; -global using Xunit; global using System; +global using System.Collections.Generic; +global using System.Threading; global using System.Threading.Tasks; +global using DotNet.Testcontainers.Commons; +global using DotPulsar; +global using DotPulsar.Abstractions; +global using DotPulsar.Extensions; +global using DotPulsar.Internal; +global using Xunit; +global using Xunit.Abstractions; \ No newline at end of file From 95f0237b3dfcadd1697146a6246d4f73e2712341 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Tue, 19 Mar 2024 12:00:45 +0100 Subject: [PATCH 03/13] chore: Move startup command to dedicated startup script --- src/Testcontainers.Pulsar/PulsarBuilder.cs | 108 ++++++------ .../PulsarConfiguration.cs | 25 +-- src/Testcontainers.Pulsar/PulsarContainer.cs | 99 ++++++++--- src/Testcontainers.Pulsar/Usings.cs | 10 +- .../PulsarContainerTest.cs | 155 ++++++++++-------- ...sarContainerWithTokenAuthenticationTest.cs | 74 --------- tests/Testcontainers.Pulsar.Tests/Usings.cs | 2 + 7 files changed, 239 insertions(+), 234 deletions(-) delete mode 100644 tests/Testcontainers.Pulsar.Tests/PulsarContainerWithTokenAuthenticationTest.cs diff --git a/src/Testcontainers.Pulsar/PulsarBuilder.cs b/src/Testcontainers.Pulsar/PulsarBuilder.cs index 574f55a0f..0e3c58ce8 100644 --- a/src/Testcontainers.Pulsar/PulsarBuilder.cs +++ b/src/Testcontainers.Pulsar/PulsarBuilder.cs @@ -4,28 +4,36 @@ [PublicAPI] public sealed class PulsarBuilder : ContainerBuilder { - private const string AuthenticationPlugin = "org.apache.pulsar.client.impl.auth.AuthenticationToken"; - private const string SecretKeyPath = "/pulsar/secret.key"; - private const string UserName = "test-user"; - private const string PulsarImage = "apachepulsar/pulsar:3.0.2"; - private const string AdminClustersEndpoint = "/admin/v2/clusters"; - internal const string Enabled = "Enabled"; - - private Dictionary _environmentVariables = new Dictionary + public const string PulsarImage = "apachepulsar/pulsar:3.0.3"; + + public const ushort PulsarBrokerDataPort = 6650; + + public const ushort PulsarWebServicePort = 8080; + + public const string StartupScriptFilePath = "/testcontainers.sh"; + + public const string SecretKeyFilePath = "/pulsar/secret.key"; + + public const string Username = "test-user"; + + private static readonly IReadOnlyDictionary AuthenticationEnvVars; + + static PulsarBuilder() { - { "PULSAR_PREFIX_tokenSecretKey", $"file://{SecretKeyPath}" }, - { "PULSAR_PREFIX_authenticationRefreshCheckSeconds", "5" }, - { "superUserRoles", UserName }, - { "authenticationEnabled", "true" }, - { "authorizationEnabled", "true" }, - { "authenticationProviders", "org.apache.pulsar.broker.authentication.AuthenticationProviderToken" }, - { "authenticateOriginalAuthData", "false" }, - { "brokerClientAuthenticationPlugin", AuthenticationPlugin }, - { "CLIENT_PREFIX_authPlugin", AuthenticationPlugin } - }; - - public const ushort PulsarBrokerPort = 6650; - public const ushort PulsarBrokerHttpPort = 8080; + const string authenticationPlugin = "org.apache.pulsar.client.impl.auth.AuthenticationToken"; + + var authenticationEnvVars = new Dictionary(); + authenticationEnvVars.Add("authenticateOriginalAuthData", "false"); + authenticationEnvVars.Add("authenticationEnabled", "true"); + authenticationEnvVars.Add("authorizationEnabled", "true"); + authenticationEnvVars.Add("authenticationProviders", "org.apache.pulsar.broker.authentication.AuthenticationProviderToken"); + authenticationEnvVars.Add("brokerClientAuthenticationPlugin", authenticationPlugin); + authenticationEnvVars.Add("CLIENT_PREFIX_authPlugin", authenticationPlugin); + authenticationEnvVars.Add("PULSAR_PREFIX_authenticationRefreshCheckSeconds", "5"); + authenticationEnvVars.Add("PULSAR_PREFIX_tokenSecretKey", "file://" + SecretKeyFilePath); + authenticationEnvVars.Add("superUserRoles", Username); + AuthenticationEnvVars = new ReadOnlyDictionary(authenticationEnvVars); + } /// /// Initializes a new instance of the class. @@ -49,25 +57,31 @@ private PulsarBuilder(PulsarConfiguration resourceConfiguration) /// protected override PulsarConfiguration DockerResourceConfiguration { get; } + /// + /// + /// + /// + public PulsarBuilder WithAuthentication() + { + return Merge(DockerResourceConfiguration, new PulsarConfiguration(authenticationEnabled: true)) + .WithEnvironment(AuthenticationEnvVars); + } + + /// + /// + /// + /// + public PulsarBuilder WithFunctionsWorker(bool functionsWorkerEnabled = true) + { + // TODO: When enabled we need to adjust the wait strategy. + return Merge(DockerResourceConfiguration, new PulsarConfiguration(functionsWorkerEnabled: functionsWorkerEnabled)); + } + /// public override PulsarContainer Build() { Validate(); - var pulsarStartupCommands = String.Empty; - if (DockerResourceConfiguration.Authentication == Enabled) - { - pulsarStartupCommands = $"bin/pulsar tokens create-secret-key --output {SecretKeyPath} && " + - $"export brokerClientAuthenticationParameters=token:$(bin/pulsar tokens create --secret-key {SecretKeyPath} --subject {UserName}) && " + - $"export CLIENT_PREFIX_authParams=$brokerClientAuthenticationParameters && bin/apply-config-from-env.py conf/standalone.conf && " + - $"bin/apply-config-from-env-with-prefix.py CLIENT_PREFIX_ conf/client.conf && "; - } - pulsarStartupCommands += "bin/pulsar standalone"; - - if (DockerResourceConfiguration.Functions != Enabled) - pulsarStartupCommands += " --no-functions-worker"; - - var pulsarBuilder = WithCommand("/bin/bash", "-c",pulsarStartupCommands); - return new PulsarContainer(pulsarBuilder.DockerResourceConfiguration, TestcontainersSettings.Logger); + return new PulsarContainer(DockerResourceConfiguration); } /// @@ -75,21 +89,13 @@ protected override PulsarBuilder Init() { return base.Init() .WithImage(PulsarImage) - .WithPortBinding(PulsarBrokerPort, true) - .WithPortBinding(PulsarBrokerHttpPort, true) - .WithWaitStrategy(Wait.ForUnixContainer() - .UntilCommandIsCompleted(["/bin/bash", "-c", "bin/pulsar-admin clusters list"])); - } - - public PulsarBuilder WithTokenAuthentication() - { - return Merge(DockerResourceConfiguration, new PulsarConfiguration(authentication: Enabled)) - .WithEnvironment(_environmentVariables); - } - - public PulsarBuilder WithFunctions() - { - return Merge(DockerResourceConfiguration, new PulsarConfiguration(functions: Enabled)); + .WithPortBinding(PulsarBrokerDataPort, true) + .WithPortBinding(PulsarWebServicePort, true) + .WithFunctionsWorker(false) + .WithEntrypoint("/bin/sh", "-c") + .WithCommand("while [ ! -f " + StartupScriptFilePath + " ]; do sleep 0.1; done; " + StartupScriptFilePath) + .WithWaitStrategy(Wait.ForUnixContainer().UntilCommandIsCompleted("bin/pulsar-admin", "clusters", "list")) + .WithStartupCallback((container, ct) => container.CopyStartupScriptAsync(ct)); } /// diff --git a/src/Testcontainers.Pulsar/PulsarConfiguration.cs b/src/Testcontainers.Pulsar/PulsarConfiguration.cs index 87e591aa1..369d9ef8f 100644 --- a/src/Testcontainers.Pulsar/PulsarConfiguration.cs +++ b/src/Testcontainers.Pulsar/PulsarConfiguration.cs @@ -7,11 +7,14 @@ public sealed class PulsarConfiguration : ContainerConfiguration /// /// Initializes a new instance of the class. /// - public PulsarConfiguration(string authentication = null, - string functions = null) + /// + /// + public PulsarConfiguration( + bool? authenticationEnabled = null, + bool? functionsWorkerEnabled = null) { - Authentication = authentication; - Functions = functions; + AuthenticationEnabled = authenticationEnabled; + FunctionsWorkerEnabled = functionsWorkerEnabled; } /// @@ -52,17 +55,17 @@ public PulsarConfiguration(PulsarConfiguration resourceConfiguration) public PulsarConfiguration(PulsarConfiguration oldValue, PulsarConfiguration newValue) : base(oldValue, newValue) { - Authentication = BuildConfiguration.Combine(oldValue.Authentication, newValue.Authentication); - Functions = BuildConfiguration.Combine(oldValue.Functions, newValue.Functions); + AuthenticationEnabled = (oldValue.AuthenticationEnabled.HasValue && oldValue.AuthenticationEnabled.Value) || (newValue.AuthenticationEnabled.HasValue && newValue.AuthenticationEnabled.Value); + FunctionsWorkerEnabled = (oldValue.FunctionsWorkerEnabled.HasValue && oldValue.FunctionsWorkerEnabled.Value) || (newValue.FunctionsWorkerEnabled.HasValue && newValue.FunctionsWorkerEnabled.Value); } /// - /// Gets authentication. + /// /// - public string Authentication { get; } - + public bool? AuthenticationEnabled { get; } + /// - /// Gets functions. + /// /// - public string Functions { get; } + public bool? FunctionsWorkerEnabled { get; } } \ No newline at end of file diff --git a/src/Testcontainers.Pulsar/PulsarContainer.cs b/src/Testcontainers.Pulsar/PulsarContainer.cs index a91ad7490..a3ddc7756 100644 --- a/src/Testcontainers.Pulsar/PulsarContainer.cs +++ b/src/Testcontainers.Pulsar/PulsarContainer.cs @@ -10,49 +10,96 @@ public sealed class PulsarContainer : DockerContainer /// Initializes a new instance of the class. /// /// The container configuration. - /// The logger. - public PulsarContainer(PulsarConfiguration configuration, ILogger logger) - : base(configuration, logger) + public PulsarContainer(PulsarConfiguration configuration) + : base(configuration) { _configuration = configuration; } /// - /// Gets the Pulsar broker url. + /// Gets the Pulsar broker address. /// - /// The Pulsar broker url. - public string GetPulsarBrokerUrl() => - new UriBuilder("pulsar://", Hostname, GetMappedPublicPort(PulsarBuilder.PulsarBrokerPort)).ToString(); + /// The Pulsar broker address. + public string GetBrokerAddress() + { + return new UriBuilder("pulsar", Hostname, GetMappedPublicPort(PulsarBuilder.PulsarBrokerDataPort)).ToString(); + } /// - /// Gets the Pulsar service url. + /// Gets the Pulsar web service address. /// - /// The Pulsar service url. - public string GetHttpServiceUrl() => - new UriBuilder("http", Hostname, GetMappedPublicPort(PulsarBuilder.PulsarBrokerHttpPort)).ToString(); + /// The Pulsar web service address. + public string GetServiceAddress() + { + return new UriBuilder(Uri.UriSchemeHttp, Hostname, GetMappedPublicPort(PulsarBuilder.PulsarWebServicePort)).ToString(); + } /// - /// Creates Authentication token + /// Creates an authentication token. /// - /// Relative expiry time for the token (eg: 1h, 3d, 10y) - /// - /// Authentication token - /// - public async Task CreateToken(TimeSpan expiryTime, CancellationToken cancellationToken = default) + /// The time after the authentication token expires. + /// Cancellation token. + /// A task that completes when the authentication token has been created. + /// + public async Task CreateAuthenticationTokenAsync(TimeSpan expire = default, CancellationToken ct = default) { - if (_configuration.Authentication != PulsarBuilder.Enabled) - throw new Exception($"Could not create the token, because WithAuthentication is not used."); + if (_configuration.AuthenticationEnabled.HasValue && !_configuration.AuthenticationEnabled.Value) + { + throw new ArgumentException("Failed to create token. Authentication is not enabled."); + } + + var command = new[] + { + "bin/pulsar", + "tokens", + "create", + "--secret-key", + PulsarBuilder.SecretKeyFilePath, + "--subject", + PulsarBuilder.Username, + "--expiry-time", + $"{expire.TotalSeconds}s", + }; + + var tokensResult = await ExecAsync(command, ct) + .ConfigureAwait(false); - var arguments = $"bin/pulsar tokens create --secret-key /pulsar/secret.key --subject test-user"; + if (tokensResult.ExitCode != 0) + { + throw new ArgumentException($"Failed to create token. Command returned a non-zero exit code: {tokensResult.Stderr}."); + } + + return tokensResult.Stdout; + } + + /// + /// Copies the Pulsar startup script to the container. + /// + /// Cancellation token. + /// A task that completes when the startup script has been copied. + internal Task CopyStartupScriptAsync(CancellationToken ct = default) + { + var startupScript = new StringWriter(); + startupScript.NewLine = "\n"; + startupScript.WriteLine("#!/bin/bash"); - if (expiryTime != Timeout.InfiniteTimeSpan) - arguments += $" --expiry-time {expiryTime.TotalSeconds}s"; + if (_configuration.AuthenticationEnabled.HasValue && _configuration.AuthenticationEnabled.Value) + { + startupScript.WriteLine("bin/pulsar tokens create-secret-key --output " + PulsarBuilder.SecretKeyFilePath); + startupScript.WriteLine("export brokerClientAuthenticationParameters=token:$(bin/pulsar tokens create --secret-key $PULSAR_PREFIX_tokenSecretKey --subject $superUserRoles)"); + startupScript.WriteLine("export CLIENT_PREFIX_authParams=$brokerClientAuthenticationParameters"); + startupScript.WriteLine("bin/apply-config-from-env.py conf/standalone.conf"); + startupScript.WriteLine("bin/apply-config-from-env-with-prefix.py CLIENT_PREFIX_ conf/client.conf"); + } - var result = await ExecAsync(new[] { "/bin/bash", "-c", arguments }, cancellationToken); + startupScript.Write("bin/pulsar standalone"); - if (result.ExitCode != 0) - throw new Exception($"Could not create the token: {result.Stderr}"); + if (_configuration.FunctionsWorkerEnabled.HasValue && !_configuration.FunctionsWorkerEnabled.Value) + { + startupScript.Write(" --no-functions-worker"); + startupScript.Write(" --no-stream-storage"); + } - return result.Stdout; + return CopyAsync(Encoding.Default.GetBytes(startupScript.ToString()), PulsarBuilder.StartupScriptFilePath, Unix.FileMode755, ct); } } \ No newline at end of file diff --git a/src/Testcontainers.Pulsar/Usings.cs b/src/Testcontainers.Pulsar/Usings.cs index ec17f5aba..ef1aedc33 100644 --- a/src/Testcontainers.Pulsar/Usings.cs +++ b/src/Testcontainers.Pulsar/Usings.cs @@ -1,7 +1,13 @@ global using System; +global using System.Collections.Generic; +global using System.Collections.ObjectModel; +global using System.Globalization; +global using System.IO; +global using System.Text; +global using System.Threading; +global using System.Threading.Tasks; global using Docker.DotNet.Models; global using DotNet.Testcontainers.Builders; global using DotNet.Testcontainers.Configurations; global using DotNet.Testcontainers.Containers; -global using JetBrains.Annotations; -global using Microsoft.Extensions.Logging; \ No newline at end of file +global using JetBrains.Annotations; \ No newline at end of file diff --git a/tests/Testcontainers.Pulsar.Tests/PulsarContainerTest.cs b/tests/Testcontainers.Pulsar.Tests/PulsarContainerTest.cs index 80c8e6adc..7a3d3d710 100644 --- a/tests/Testcontainers.Pulsar.Tests/PulsarContainerTest.cs +++ b/tests/Testcontainers.Pulsar.Tests/PulsarContainerTest.cs @@ -1,73 +1,88 @@ namespace Testcontainers.Pulsar; -public sealed class PulsarContainerTest : IAsyncLifetime +public abstract class PulsarContainerTest : IAsyncLifetime { - private readonly CancellationTokenSource _cts; - private readonly PulsarContainer _pulsarContainer; - private readonly ITestOutputHelper _testOutputHelper; - - public PulsarContainerTest(ITestOutputHelper testOutputHelper) - { - _testOutputHelper = testOutputHelper; - _pulsarContainer = new PulsarBuilder().Build(); - _cts = new CancellationTokenSource(TimeSpan.FromMinutes(2)); - } - - public Task InitializeAsync() - { - return _pulsarContainer.StartAsync(); - } - - public Task DisposeAsync() - { - return _pulsarContainer.DisposeAsync().AsTask(); - } - - [Fact] - [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] - public async Task PulsarContainer_WhenBrokerIsStarted_ShouldConnect() - { - // Given - await using var client = CreateClient(); - var expected = new List { MessageId.Earliest }; - await using var reader = CreateReader(client, MessageId.Earliest, await CreateTopic(_cts.Token)); - - // When - var actual = await reader.GetLastMessageIds(_cts.Token); - - // Then - Assert.Equal(expected,actual); - } - - private IReader CreateReader(IPulsarClient pulsarClient, MessageId messageId, string topicName) - => pulsarClient.NewReader(Schema.String) - .StartMessageId(messageId) - .Topic(topicName) - .Create(); - - private static string CreateTopicName() => $"persistent://public/default/{Guid.NewGuid():N}"; - - private async Task CreateTopic(string topic, CancellationToken cancellationToken) - { - var arguments = $"bin/pulsar-admin topics create {topic}"; - - var result = await _pulsarContainer.ExecAsync(new[] { "/bin/bash", "-c", arguments }, cancellationToken); - - if (result.ExitCode != 0) - throw new Exception($"Could not create the topic: {result.Stderr}"); - } - - private async Task CreateTopic(CancellationToken cancellationToken) - { - var topic = CreateTopicName(); - await CreateTopic(topic, cancellationToken); - return topic; - } - - private IPulsarClient CreateClient() - => PulsarClient - .Builder() - .ExceptionHandler(context => _testOutputHelper.WriteLine($"PulsarClient got an exception: {context.Exception}")) - .ServiceUrl(new Uri(_pulsarContainer.GetPulsarBrokerUrl())) - .Build(); -} + private readonly PulsarContainer _pulsarContainer; + + private PulsarContainerTest(PulsarContainer pulsarContainer) + { + _pulsarContainer = pulsarContainer; + } + + protected abstract Task CreateClientAsync(CancellationToken ct = default); + + public Task InitializeAsync() + { + return _pulsarContainer.StartAsync(); + } + + public Task DisposeAsync() + { + return _pulsarContainer.DisposeAsync().AsTask(); + } + + [Fact] + public async Task ConsumerReceivesSendMessage() + { + // Given + const string helloPulsar = "Hello, Pulsar!"; + + var topic = $"persistent://public/default/{Guid.NewGuid():D}"; + + var name = Guid.NewGuid().ToString("D"); + + await using var client = await CreateClientAsync() + .ConfigureAwait(true); + + await using var producer = client.NewProducer(Schema.String) + .Topic(topic) + .Create(); + + await using var consumer = client.NewConsumer(Schema.String) + .Topic(topic) + .SubscriptionName(name) + .InitialPosition(SubscriptionInitialPosition.Earliest) + .Create(); + + // When + _ = await producer.Send(helloPulsar) + .ConfigureAwait(true); + + var message = await consumer.Receive() + .ConfigureAwait(true); + + // Then + Assert.Equal(helloPulsar, Encoding.Default.GetString(message.Data)); + } + + [UsedImplicitly] + public sealed class PulsarDefaultConfiguration : PulsarContainerTest + { + public PulsarDefaultConfiguration() + : base(new PulsarBuilder().Build()) + { + } + + protected override Task CreateClientAsync(CancellationToken ct = default) + { + return Task.FromResult(PulsarClient.Builder().ServiceUrl(new Uri(_pulsarContainer.GetBrokerAddress())).Build()); + } + } + + [UsedImplicitly] + public sealed class PulsarAuthConfiguration : PulsarContainerTest + { + public PulsarAuthConfiguration() + : base(new PulsarBuilder().WithAuthentication().Build()) + { + } + + protected override async Task CreateClientAsync(CancellationToken ct = default) + { + var token = await _pulsarContainer.CreateAuthenticationTokenAsync(TimeSpan.FromHours(1), ct) + .ConfigureAwait(false); + + return PulsarClient.Builder().ServiceUrl(new Uri(_pulsarContainer.GetBrokerAddress())).Authentication(new TokenAuthentication(token)).Build(); + } + } +} \ No newline at end of file diff --git a/tests/Testcontainers.Pulsar.Tests/PulsarContainerWithTokenAuthenticationTest.cs b/tests/Testcontainers.Pulsar.Tests/PulsarContainerWithTokenAuthenticationTest.cs deleted file mode 100644 index c9b7127c4..000000000 --- a/tests/Testcontainers.Pulsar.Tests/PulsarContainerWithTokenAuthenticationTest.cs +++ /dev/null @@ -1,74 +0,0 @@ -namespace Testcontainers.Pulsar; - -public sealed class PulsarContainerWithTokenAuthenticationTest : IAsyncLifetime -{ - private readonly CancellationTokenSource _cts; - private readonly PulsarContainer _pulsarContainer; - private readonly ITestOutputHelper _testOutputHelper; - - public PulsarContainerWithTokenAuthenticationTest(ITestOutputHelper testOutputHelper) - { - _testOutputHelper = testOutputHelper; - _pulsarContainer = new PulsarBuilder().WithTokenAuthentication().Build(); - _cts = new CancellationTokenSource(TimeSpan.FromMinutes(2)); - } - - public Task InitializeAsync() - { - return _pulsarContainer.StartAsync(); - } - - public Task DisposeAsync() - { - return _pulsarContainer.DisposeAsync().AsTask(); - } - - [Fact] - [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] - public async Task PulsarContainer_WhenBrokerWithTokenAuthenticationIsStarted_ShouldConnect() - { - // Given - await using var client = CreateClient(); - var expected = new List { MessageId.Earliest }; - await using var reader = CreateReader(client, MessageId.Earliest, await CreateTopic(_cts.Token)); - - // When - var actual = await reader.GetLastMessageIds(_cts.Token); - - // Then - Assert.Equal(expected,actual); - } - - private IReader CreateReader(IPulsarClient pulsarClient, MessageId messageId, string topicName) - => pulsarClient.NewReader(Schema.String) - .StartMessageId(messageId) - .Topic(topicName) - .Create(); - - private static string CreateTopicName() => $"persistent://public/default/{Guid.NewGuid():N}"; - - private async Task CreateTopic(string topic, CancellationToken cancellationToken) - { - var arguments = $"bin/pulsar-admin topics create {topic}"; - - var result = await _pulsarContainer.ExecAsync(new[] { "/bin/bash", "-c", arguments }, cancellationToken); - - if (result.ExitCode != 0) - throw new Exception($"Could not create the topic: {result.Stderr}"); - } - - private async Task CreateTopic(CancellationToken cancellationToken) - { - var topic = CreateTopicName(); - await CreateTopic(topic, cancellationToken); - return topic; - } - - private IPulsarClient CreateClient() - => PulsarClient - .Builder() - .Authentication(new TokenAuthentication(_pulsarContainer.CreateToken(Timeout.InfiniteTimeSpan).Result)) - .ExceptionHandler(context => _testOutputHelper.WriteLine($"PulsarClient got an exception: {context.Exception}")) - .ServiceUrl(new Uri(_pulsarContainer.GetPulsarBrokerUrl())) - .Build(); -} diff --git a/tests/Testcontainers.Pulsar.Tests/Usings.cs b/tests/Testcontainers.Pulsar.Tests/Usings.cs index a3dffa9da..8d5cfee6f 100644 --- a/tests/Testcontainers.Pulsar.Tests/Usings.cs +++ b/tests/Testcontainers.Pulsar.Tests/Usings.cs @@ -1,5 +1,6 @@ global using System; global using System.Collections.Generic; +global using System.Text; global using System.Threading; global using System.Threading.Tasks; global using DotNet.Testcontainers.Commons; @@ -7,5 +8,6 @@ global using DotPulsar.Abstractions; global using DotPulsar.Extensions; global using DotPulsar.Internal; +global using JetBrains.Annotations; global using Xunit; global using Xunit.Abstractions; \ No newline at end of file From 7b051892baee832484e08afc7d59a0d79a0a1d29 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Tue, 19 Mar 2024 12:26:04 +0100 Subject: [PATCH 04/13] chore: Remove BOM --- docs/modules/pulsar.md | 2 +- src/Testcontainers.Pulsar/PulsarBuilder.cs | 2 +- src/Testcontainers.Pulsar/PulsarConfiguration.cs | 6 +++--- src/Testcontainers.Pulsar/PulsarContainer.cs | 2 +- src/Testcontainers.Pulsar/Testcontainers.Pulsar.csproj | 2 +- src/Testcontainers.Pulsar/Usings.cs | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/modules/pulsar.md b/docs/modules/pulsar.md index e42253bb4..fcc45adf7 100644 --- a/docs/modules/pulsar.md +++ b/docs/modules/pulsar.md @@ -1,4 +1,4 @@ -# Apache Pulsar Module +# Apache Pulsar Module Testcontainers can be used to automatically create [Apache Pulsar](https://pulsar.apache.org) containers without external services. diff --git a/src/Testcontainers.Pulsar/PulsarBuilder.cs b/src/Testcontainers.Pulsar/PulsarBuilder.cs index 0e3c58ce8..a7d902c0b 100644 --- a/src/Testcontainers.Pulsar/PulsarBuilder.cs +++ b/src/Testcontainers.Pulsar/PulsarBuilder.cs @@ -1,4 +1,4 @@ -namespace Testcontainers.Pulsar; +namespace Testcontainers.Pulsar; /// [PublicAPI] diff --git a/src/Testcontainers.Pulsar/PulsarConfiguration.cs b/src/Testcontainers.Pulsar/PulsarConfiguration.cs index 369d9ef8f..41abd0deb 100644 --- a/src/Testcontainers.Pulsar/PulsarConfiguration.cs +++ b/src/Testcontainers.Pulsar/PulsarConfiguration.cs @@ -1,4 +1,4 @@ -namespace Testcontainers.Pulsar; +namespace Testcontainers.Pulsar; /// [PublicAPI] @@ -60,12 +60,12 @@ public PulsarConfiguration(PulsarConfiguration oldValue, PulsarConfiguration new } /// - /// + /// /// public bool? AuthenticationEnabled { get; } /// - /// + /// /// public bool? FunctionsWorkerEnabled { get; } } \ No newline at end of file diff --git a/src/Testcontainers.Pulsar/PulsarContainer.cs b/src/Testcontainers.Pulsar/PulsarContainer.cs index a3ddc7756..90bb83a52 100644 --- a/src/Testcontainers.Pulsar/PulsarContainer.cs +++ b/src/Testcontainers.Pulsar/PulsarContainer.cs @@ -1,4 +1,4 @@ -namespace Testcontainers.Pulsar; +namespace Testcontainers.Pulsar; /// [PublicAPI] diff --git a/src/Testcontainers.Pulsar/Testcontainers.Pulsar.csproj b/src/Testcontainers.Pulsar/Testcontainers.Pulsar.csproj index a2342ade1..a108060b3 100644 --- a/src/Testcontainers.Pulsar/Testcontainers.Pulsar.csproj +++ b/src/Testcontainers.Pulsar/Testcontainers.Pulsar.csproj @@ -1,4 +1,4 @@ - + netstandard2.0;netstandard2.1 latest diff --git a/src/Testcontainers.Pulsar/Usings.cs b/src/Testcontainers.Pulsar/Usings.cs index ef1aedc33..0ad624df5 100644 --- a/src/Testcontainers.Pulsar/Usings.cs +++ b/src/Testcontainers.Pulsar/Usings.cs @@ -1,4 +1,4 @@ -global using System; +global using System; global using System.Collections.Generic; global using System.Collections.ObjectModel; global using System.Globalization; From 2d2c18c7a85db189d5df22a6a0dd7bbe82853041 Mon Sep 17 00:00:00 2001 From: David Jensen Date: Wed, 20 Mar 2024 13:00:06 +0100 Subject: [PATCH 05/13] Started Rework of the waitStrategy so it is more reliable. --- src/Testcontainers.Pulsar/PulsarBuilder.cs | 30 +++++++++++++++++-- .../PulsarConfiguration.cs | 12 ++++++-- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/Testcontainers.Pulsar/PulsarBuilder.cs b/src/Testcontainers.Pulsar/PulsarBuilder.cs index a7d902c0b..4e84a2fb5 100644 --- a/src/Testcontainers.Pulsar/PulsarBuilder.cs +++ b/src/Testcontainers.Pulsar/PulsarBuilder.cs @@ -81,7 +81,26 @@ public PulsarBuilder WithFunctionsWorker(bool functionsWorkerEnabled = true) public override PulsarContainer Build() { Validate(); - return new PulsarContainer(DockerResourceConfiguration); + + var waitStrategy = Wait.ForUnixContainer(); + + waitStrategy = waitStrategy.UntilHttpRequestIsSucceeded(request + => request + .ForPath("/admin/v2/clusters") + .ForPort(PulsarWebServicePort) + .ForResponseMessageMatching(VerifyPulsarStatusAsync)); + + //To Do How do I access PulsarContainer.CreateAuthenticationTokenAsync so i can get the auth token? + // waitStrategy = waitStrategy.UntilHttpRequestIsSucceeded(request + // => request + // .ForPath("/admin/v2/clusters") + // .ForPort(PulsarWebServicePort) + // .ForResponseMessageMatching(VerifyPulsarStatusAsync) + // .WithHeader("Authorization", )); + + var pulsarBuilder = WithWaitStrategy(waitStrategy); + + return new PulsarContainer(pulsarBuilder.DockerResourceConfiguration); } /// @@ -94,10 +113,9 @@ protected override PulsarBuilder Init() .WithFunctionsWorker(false) .WithEntrypoint("/bin/sh", "-c") .WithCommand("while [ ! -f " + StartupScriptFilePath + " ]; do sleep 0.1; done; " + StartupScriptFilePath) - .WithWaitStrategy(Wait.ForUnixContainer().UntilCommandIsCompleted("bin/pulsar-admin", "clusters", "list")) .WithStartupCallback((container, ct) => container.CopyStartupScriptAsync(ct)); } - + /// protected override PulsarBuilder Clone(IResourceConfiguration resourceConfiguration) { @@ -115,4 +133,10 @@ protected override PulsarBuilder Merge(PulsarConfiguration oldValue, PulsarConfi { return new PulsarBuilder(new PulsarConfiguration(oldValue, newValue)); } + + private async Task VerifyPulsarStatusAsync(System.Net.Http.HttpResponseMessage response) + { + var readAsStringAsync = await response.Content.ReadAsStringAsync(); + return readAsStringAsync == "[\"standalone\"]"; + } } \ No newline at end of file diff --git a/src/Testcontainers.Pulsar/PulsarConfiguration.cs b/src/Testcontainers.Pulsar/PulsarConfiguration.cs index 41abd0deb..f5bdb696d 100644 --- a/src/Testcontainers.Pulsar/PulsarConfiguration.cs +++ b/src/Testcontainers.Pulsar/PulsarConfiguration.cs @@ -60,12 +60,20 @@ public PulsarConfiguration(PulsarConfiguration oldValue, PulsarConfiguration new } /// - /// + /// Gets or sets the flag indicating whether authentication is enabled for the Pulsar container. /// + /// + /// By default, authentication is disabled. + /// Setting this property to true enables authentication for the Pulsar container. + /// public bool? AuthenticationEnabled { get; } /// - /// + /// Gets or sets a value indicating whether the functions worker is enabled. /// + /// + /// The functions worker is responsible for executing functions within Pulsar. + /// By default, the functions worker is not enabled. + /// public bool? FunctionsWorkerEnabled { get; } } \ No newline at end of file From ed1ed66004f974fddb69f5fc7113b5eb3752cee0 Mon Sep 17 00:00:00 2001 From: David Jensen Date: Fri, 12 Apr 2024 10:32:01 +0200 Subject: [PATCH 06/13] Update Pulsar image and enhance wait strategy The Pulsar image has been upgraded from apachepulsar/pulsar:3.0.3 to apachepulsar/pulsar:3.0.4. Moreover, changes are made in the wait strategy to improve the way it handles authentication. A new WaitUntil class has been introduced to manage the authentication token. --- src/Testcontainers.Pulsar/PulsarBuilder.cs | 41 ++++++++++++++++------ 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/src/Testcontainers.Pulsar/PulsarBuilder.cs b/src/Testcontainers.Pulsar/PulsarBuilder.cs index 4e84a2fb5..f5445f740 100644 --- a/src/Testcontainers.Pulsar/PulsarBuilder.cs +++ b/src/Testcontainers.Pulsar/PulsarBuilder.cs @@ -4,7 +4,7 @@ namespace Testcontainers.Pulsar; [PublicAPI] public sealed class PulsarBuilder : ContainerBuilder { - public const string PulsarImage = "apachepulsar/pulsar:3.0.3"; + public const string PulsarImage = "apachepulsar/pulsar:3.0.4"; public const ushort PulsarBrokerDataPort = 6650; @@ -84,19 +84,15 @@ public override PulsarContainer Build() var waitStrategy = Wait.ForUnixContainer(); + //TODO We need to switch between the default and custom WaitStrategy depending on if the user used WithAuthentication. + //Would you prefer we handled it in a similar to Couchbase? waitStrategy = waitStrategy.UntilHttpRequestIsSucceeded(request => request .ForPath("/admin/v2/clusters") .ForPort(PulsarWebServicePort) .ForResponseMessageMatching(VerifyPulsarStatusAsync)); - - //To Do How do I access PulsarContainer.CreateAuthenticationTokenAsync so i can get the auth token? - // waitStrategy = waitStrategy.UntilHttpRequestIsSucceeded(request - // => request - // .ForPath("/admin/v2/clusters") - // .ForPort(PulsarWebServicePort) - // .ForResponseMessageMatching(VerifyPulsarStatusAsync) - // .WithHeader("Authorization", )); + + waitStrategy.AddCustomWaitStrategy(new WaitUntil()); var pulsarBuilder = WithWaitStrategy(waitStrategy); @@ -134,9 +130,34 @@ protected override PulsarBuilder Merge(PulsarConfiguration oldValue, PulsarConfi return new PulsarBuilder(new PulsarConfiguration(oldValue, newValue)); } - private async Task VerifyPulsarStatusAsync(System.Net.Http.HttpResponseMessage response) + private static async Task VerifyPulsarStatusAsync(System.Net.Http.HttpResponseMessage response) { var readAsStringAsync = await response.Content.ReadAsStringAsync(); return readAsStringAsync == "[\"standalone\"]"; } + + /// + private sealed class WaitUntil : IWaitUntil + { + /// + public async Task UntilAsync(IContainer container) + { + try + { + var pulsarContainer = container as PulsarContainer; + var authenticationToken = await pulsarContainer.CreateAuthenticationTokenAsync(TimeSpan.FromSeconds(60),CancellationToken.None); + using (var client = new System.Net.Http.HttpClient()) + { + client.BaseAddress = new Uri($"http://{container.Hostname}:{container.GetMappedPublicPort(PulsarWebServicePort)}/"); + client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", authenticationToken.Replace("\n", "")); + System.Net.Http.HttpResponseMessage response = await client.GetAsync("admin/v2/clusters"); + return await VerifyPulsarStatusAsync(response); + } + } + catch (Exception) + { + return false; + } + } + } } \ No newline at end of file From bdcef9d53436046b7cf252696b25a4b7ecd4f9e0 Mon Sep 17 00:00:00 2001 From: David Jensen Date: Fri, 12 Apr 2024 12:46:13 +0200 Subject: [PATCH 07/13] Add PulsarService and functionality to PulsarBuilder A new file, PulsarService.cs, has been added, with a struct named PulsarService that allows the creation of different Pulsar services. Changes have also been made to PulsarBuilder.cs, mainly to incorporate the use of PulsarService. Rather than using a constant value, `WithAuthentication()` and `WithFunctionsWorker(bool functionsWorkerEnabled = true)` now use elements from the PulsarService struct. Wait strategy has been updated to better account for various enabled services. --- src/Testcontainers.Pulsar/PulsarBuilder.cs | 46 +++++++++++++++------- src/Testcontainers.Pulsar/PulsarService.cs | 30 ++++++++++++++ 2 files changed, 61 insertions(+), 15 deletions(-) create mode 100644 src/Testcontainers.Pulsar/PulsarService.cs diff --git a/src/Testcontainers.Pulsar/PulsarBuilder.cs b/src/Testcontainers.Pulsar/PulsarBuilder.cs index f5445f740..4d875fe56 100644 --- a/src/Testcontainers.Pulsar/PulsarBuilder.cs +++ b/src/Testcontainers.Pulsar/PulsarBuilder.cs @@ -1,3 +1,5 @@ +using System.Linq; + namespace Testcontainers.Pulsar; /// @@ -17,6 +19,8 @@ public sealed class PulsarBuilder : ContainerBuilder AuthenticationEnvVars; + + private static readonly ISet EnabledServices = new HashSet(); static PulsarBuilder() { @@ -58,22 +62,27 @@ private PulsarBuilder(PulsarConfiguration resourceConfiguration) protected override PulsarConfiguration DockerResourceConfiguration { get; } /// - /// + /// Enables authentication in the Pulsar container. /// - /// + /// A instance. public PulsarBuilder WithAuthentication() { + EnabledServices.Add(PulsarService.Authentication); + return Merge(DockerResourceConfiguration, new PulsarConfiguration(authenticationEnabled: true)) .WithEnvironment(AuthenticationEnvVars); } /// - /// + /// Initializes a new instance of the class. /// - /// + /// Determines if the functions worker is enabled. + /// A new instance of the class. public PulsarBuilder WithFunctionsWorker(bool functionsWorkerEnabled = true) { - // TODO: When enabled we need to adjust the wait strategy. + if (functionsWorkerEnabled) + EnabledServices.Add(PulsarService.FunctionWorker); + return Merge(DockerResourceConfiguration, new PulsarConfiguration(functionsWorkerEnabled: functionsWorkerEnabled)); } @@ -84,18 +93,25 @@ public override PulsarContainer Build() var waitStrategy = Wait.ForUnixContainer(); - //TODO We need to switch between the default and custom WaitStrategy depending on if the user used WithAuthentication. - //Would you prefer we handled it in a similar to Couchbase? - waitStrategy = waitStrategy.UntilHttpRequestIsSucceeded(request - => request - .ForPath("/admin/v2/clusters") - .ForPort(PulsarWebServicePort) - .ForResponseMessageMatching(VerifyPulsarStatusAsync)); - - waitStrategy.AddCustomWaitStrategy(new WaitUntil()); + if (EnabledServices.Contains(PulsarService.Authentication)) + { + waitStrategy.AddCustomWaitStrategy(new WaitUntil()); + } + else + { + waitStrategy = waitStrategy.UntilHttpRequestIsSucceeded(request + => request + .ForPath("/admin/v2/clusters") + .ForPort(PulsarWebServicePort) + .ForResponseMessageMatching(VerifyPulsarStatusAsync)); + } - var pulsarBuilder = WithWaitStrategy(waitStrategy); + if (EnabledServices.Contains(PulsarService.FunctionWorker)) + { + waitStrategy.UntilMessageIsLogged(".*Function worker service started.*"); + } + var pulsarBuilder = WithWaitStrategy(waitStrategy); return new PulsarContainer(pulsarBuilder.DockerResourceConfiguration); } diff --git a/src/Testcontainers.Pulsar/PulsarService.cs b/src/Testcontainers.Pulsar/PulsarService.cs new file mode 100644 index 000000000..f9dc3c716 --- /dev/null +++ b/src/Testcontainers.Pulsar/PulsarService.cs @@ -0,0 +1,30 @@ +namespace Testcontainers.Pulsar; + +internal readonly struct PulsarService +{ + /// + /// Initializes a new instance of the struct. + /// + /// The identifier. + private PulsarService(string identifier) + { + Identifier = identifier; + } + + /// + /// Gets the Auth. + /// + public static readonly PulsarService Authentication = new PulsarService("auth"); + + /// + /// Gets the Function worker service. + /// + public static readonly PulsarService FunctionWorker = new PulsarService("funk"); + + /// + /// Gets the identifier. + /// + public string Identifier { get; } +} + + From 4f6286c24a6c93a9cc78700d339001b6da33103a Mon Sep 17 00:00:00 2001 From: entvex <1580435+entvex@users.noreply.github.com> Date: Mon, 22 Apr 2024 12:42:58 +0200 Subject: [PATCH 08/13] Update index.md to reflect that we use pulsar 3.0.4 Update index.md to reflect that we use pulsar 3.0.4 --- docs/modules/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/index.md b/docs/modules/index.md index dd76e92b5..0f777c3c0 100644 --- a/docs/modules/index.md +++ b/docs/modules/index.md @@ -56,7 +56,7 @@ await moduleNameContainer.StartAsync(); | Papercut | `jijiechen/papercut:latest` | [NuGet](https://www.nuget.org/packages/Testcontainers.Papercut) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Papercut) | | PostgreSQL | `postgres:15.1` | [NuGet](https://www.nuget.org/packages/Testcontainers.PostgreSql) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.PostgreSql) | | PubSub | `gcr.io/google.com/cloudsdktool/google-cloud-cli:446.0.1-emulators` | [NuGet](https://www.nuget.org/packages/Testcontainers.PubSub) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.PubSub) | -| Pulsar | `apachepulsar/pulsar:3.0.2` | [NuGet](https://www.nuget.org/packages/Testcontainers.Pulsar) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Pulsar) | +| Pulsar | `apachepulsar/pulsar:3.0.4` | [NuGet](https://www.nuget.org/packages/Testcontainers.Pulsar) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Pulsar) | | RabbitMQ | `rabbitmq:3.11` | [NuGet](https://www.nuget.org/packages/Testcontainers.RabbitMq) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.RabbitMq) | | RavenDB | `ravendb/ravendb:5.4-ubuntu-latest` | [NuGet](https://www.nuget.org/packages/Testcontainers.RavenDb) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.RavenDb) | | Redis | `redis:7.0` | [NuGet](https://www.nuget.org/packages/Testcontainers.Redis) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Redis) | From 9ea1eb01a2265c43c0e4fcf3107dbcde92f80e7d Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Mon, 22 Apr 2024 15:33:34 +0200 Subject: [PATCH 09/13] chore: Remove EnabledServices --- src/Testcontainers.Pulsar/PulsarBuilder.cs | 130 +++++++++++------- .../PulsarConfiguration.cs | 16 +-- src/Testcontainers.Pulsar/PulsarService.cs | 30 ---- src/Testcontainers.Pulsar/Usings.cs | 5 +- .../Testcontainers.Databases.Tests/Usings.cs | 1 - .../PulsarContainerTest.cs | 4 +- tests/Testcontainers.Pulsar.Tests/Usings.cs | 5 +- 7 files changed, 90 insertions(+), 101 deletions(-) delete mode 100644 src/Testcontainers.Pulsar/PulsarService.cs diff --git a/src/Testcontainers.Pulsar/PulsarBuilder.cs b/src/Testcontainers.Pulsar/PulsarBuilder.cs index 4d875fe56..7b9b83525 100644 --- a/src/Testcontainers.Pulsar/PulsarBuilder.cs +++ b/src/Testcontainers.Pulsar/PulsarBuilder.cs @@ -1,5 +1,3 @@ -using System.Linq; - namespace Testcontainers.Pulsar; /// @@ -19,13 +17,10 @@ public sealed class PulsarBuilder : ContainerBuilder AuthenticationEnvVars; - - private static readonly ISet EnabledServices = new HashSet(); static PulsarBuilder() { const string authenticationPlugin = "org.apache.pulsar.client.impl.auth.AuthenticationToken"; - var authenticationEnvVars = new Dictionary(); authenticationEnvVars.Add("authenticateOriginalAuthData", "false"); authenticationEnvVars.Add("authenticationEnabled", "true"); @@ -62,27 +57,25 @@ private PulsarBuilder(PulsarConfiguration resourceConfiguration) protected override PulsarConfiguration DockerResourceConfiguration { get; } /// - /// Enables authentication in the Pulsar container. + /// Enables authentication. /// - /// A instance. + /// + /// To create an authentication call . + /// + /// A configured instance of . public PulsarBuilder WithAuthentication() { - EnabledServices.Add(PulsarService.Authentication); - return Merge(DockerResourceConfiguration, new PulsarConfiguration(authenticationEnabled: true)) .WithEnvironment(AuthenticationEnvVars); } /// - /// Initializes a new instance of the class. + /// Enables function workers. /// - /// Determines if the functions worker is enabled. - /// A new instance of the class. + /// Determines whether function workers is enabled or not. + /// A configured instance of . public PulsarBuilder WithFunctionsWorker(bool functionsWorkerEnabled = true) { - if (functionsWorkerEnabled) - EnabledServices.Add(PulsarService.FunctionWorker); - return Merge(DockerResourceConfiguration, new PulsarConfiguration(functionsWorkerEnabled: functionsWorkerEnabled)); } @@ -90,27 +83,14 @@ public PulsarBuilder WithFunctionsWorker(bool functionsWorkerEnabled = true) public override PulsarContainer Build() { Validate(); - - var waitStrategy = Wait.ForUnixContainer(); - if (EnabledServices.Contains(PulsarService.Authentication)) - { - waitStrategy.AddCustomWaitStrategy(new WaitUntil()); - } - else - { - waitStrategy = waitStrategy.UntilHttpRequestIsSucceeded(request - => request - .ForPath("/admin/v2/clusters") - .ForPort(PulsarWebServicePort) - .ForResponseMessageMatching(VerifyPulsarStatusAsync)); - } - - if (EnabledServices.Contains(PulsarService.FunctionWorker)) + var waitStrategy = Wait.ForUnixContainer().AddCustomWaitStrategy(new WaitUntil(DockerResourceConfiguration.AuthenticationEnabled.GetValueOrDefault())); + + if (DockerResourceConfiguration.FunctionsWorkerEnabled.GetValueOrDefault()) { - waitStrategy.UntilMessageIsLogged(".*Function worker service started.*"); + waitStrategy = waitStrategy.UntilMessageIsLogged("Function worker service started"); } - + var pulsarBuilder = WithWaitStrategy(waitStrategy); return new PulsarContainer(pulsarBuilder.DockerResourceConfiguration); } @@ -127,7 +107,7 @@ protected override PulsarBuilder Init() .WithCommand("while [ ! -f " + StartupScriptFilePath + " ]; do sleep 0.1; done; " + StartupScriptFilePath) .WithStartupCallback((container, ct) => container.CopyStartupScriptAsync(ct)); } - + /// protected override PulsarBuilder Clone(IResourceConfiguration resourceConfiguration) { @@ -145,32 +125,80 @@ protected override PulsarBuilder Merge(PulsarConfiguration oldValue, PulsarConfi { return new PulsarBuilder(new PulsarConfiguration(oldValue, newValue)); } - - private static async Task VerifyPulsarStatusAsync(System.Net.Http.HttpResponseMessage response) - { - var readAsStringAsync = await response.Content.ReadAsStringAsync(); - return readAsStringAsync == "[\"standalone\"]"; - } - + /// private sealed class WaitUntil : IWaitUntil { + private readonly HttpWaitStrategy _httpWaitStrategy = new HttpWaitStrategy() + .ForPath("/admin/v2/clusters") + .ForPort(PulsarWebServicePort) + .ForResponseMessageMatching(IsClusterHealthyAsync); + + private readonly bool _authenticationEnabled; + + private string _authToken; + + /// + /// Initializes a new instance of the class. + /// + /// A value indicating whether authentication is enabled or not. + public WaitUntil(bool authenticationEnabled) + { + _authenticationEnabled = authenticationEnabled; + } + /// - public async Task UntilAsync(IContainer container) + public Task UntilAsync(IContainer container) { - try + return UntilAsync(container as PulsarContainer); + } + + /// + private async Task UntilAsync(PulsarContainer container) + { + _ = Guard.Argument(container, nameof(container)) + .NotNull(); + + if (_authenticationEnabled && _authToken == null) { - var pulsarContainer = container as PulsarContainer; - var authenticationToken = await pulsarContainer.CreateAuthenticationTokenAsync(TimeSpan.FromSeconds(60),CancellationToken.None); - using (var client = new System.Net.Http.HttpClient()) + try { - client.BaseAddress = new Uri($"http://{container.Hostname}:{container.GetMappedPublicPort(PulsarWebServicePort)}/"); - client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", authenticationToken.Replace("\n", "")); - System.Net.Http.HttpResponseMessage response = await client.GetAsync("admin/v2/clusters"); - return await VerifyPulsarStatusAsync(response); + _authToken = await container.CreateAuthenticationTokenAsync(TimeSpan.FromHours(1)) + .ConfigureAwait(false); + + _ = _httpWaitStrategy.WithHeader("Authorization", "Bearer " + _authToken.Trim()); + } + catch + { + return false; } } - catch (Exception) + + return await _httpWaitStrategy.UntilAsync(container) + .ConfigureAwait(false); + } + + /// + /// Determines whether the cluster is healthy or not. + /// + /// The HTTP response that contains the cluster information. + /// A value indicating whether the cluster is healthy or not. + private static async Task IsClusterHealthyAsync(HttpResponseMessage response) + { + var jsonString = await response.Content.ReadAsStringAsync() + .ConfigureAwait(false); + + try + { + var status = JsonDocument.Parse(jsonString) + .RootElement + .EnumerateArray() + .ElementAt(0) + .GetString(); + + return "standalone".Equals(status); + } + catch { return false; } diff --git a/src/Testcontainers.Pulsar/PulsarConfiguration.cs b/src/Testcontainers.Pulsar/PulsarConfiguration.cs index f5bdb696d..9a290878b 100644 --- a/src/Testcontainers.Pulsar/PulsarConfiguration.cs +++ b/src/Testcontainers.Pulsar/PulsarConfiguration.cs @@ -7,8 +7,8 @@ public sealed class PulsarConfiguration : ContainerConfiguration /// /// Initializes a new instance of the class. /// - /// - /// + /// A value indicating whether authentication is enabled or not. + /// A value indicating whether function workers is enabled or not. public PulsarConfiguration( bool? authenticationEnabled = null, bool? functionsWorkerEnabled = null) @@ -60,20 +60,12 @@ public PulsarConfiguration(PulsarConfiguration oldValue, PulsarConfiguration new } /// - /// Gets or sets the flag indicating whether authentication is enabled for the Pulsar container. + /// Gets a value indicating whether authentication is enabled or not. /// - /// - /// By default, authentication is disabled. - /// Setting this property to true enables authentication for the Pulsar container. - /// public bool? AuthenticationEnabled { get; } /// - /// Gets or sets a value indicating whether the functions worker is enabled. + /// Gets a value indicating whether function workers is enabled or not. /// - /// - /// The functions worker is responsible for executing functions within Pulsar. - /// By default, the functions worker is not enabled. - /// public bool? FunctionsWorkerEnabled { get; } } \ No newline at end of file diff --git a/src/Testcontainers.Pulsar/PulsarService.cs b/src/Testcontainers.Pulsar/PulsarService.cs deleted file mode 100644 index f9dc3c716..000000000 --- a/src/Testcontainers.Pulsar/PulsarService.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace Testcontainers.Pulsar; - -internal readonly struct PulsarService -{ - /// - /// Initializes a new instance of the struct. - /// - /// The identifier. - private PulsarService(string identifier) - { - Identifier = identifier; - } - - /// - /// Gets the Auth. - /// - public static readonly PulsarService Authentication = new PulsarService("auth"); - - /// - /// Gets the Function worker service. - /// - public static readonly PulsarService FunctionWorker = new PulsarService("funk"); - - /// - /// Gets the identifier. - /// - public string Identifier { get; } -} - - diff --git a/src/Testcontainers.Pulsar/Usings.cs b/src/Testcontainers.Pulsar/Usings.cs index 0ad624df5..35bfa1312 100644 --- a/src/Testcontainers.Pulsar/Usings.cs +++ b/src/Testcontainers.Pulsar/Usings.cs @@ -1,12 +1,15 @@ global using System; global using System.Collections.Generic; global using System.Collections.ObjectModel; -global using System.Globalization; global using System.IO; +global using System.Linq; +global using System.Net.Http; global using System.Text; +global using System.Text.Json; global using System.Threading; global using System.Threading.Tasks; global using Docker.DotNet.Models; +global using DotNet.Testcontainers; global using DotNet.Testcontainers.Builders; global using DotNet.Testcontainers.Configurations; global using DotNet.Testcontainers.Containers; diff --git a/tests/Testcontainers.Databases.Tests/Usings.cs b/tests/Testcontainers.Databases.Tests/Usings.cs index 2ea483b34..5eaa436ef 100644 --- a/tests/Testcontainers.Databases.Tests/Usings.cs +++ b/tests/Testcontainers.Databases.Tests/Usings.cs @@ -1,5 +1,4 @@ global using System; -global using System.Collections.Generic; global using System.Collections.Immutable; global using System.Data.Common; global using System.IO; diff --git a/tests/Testcontainers.Pulsar.Tests/PulsarContainerTest.cs b/tests/Testcontainers.Pulsar.Tests/PulsarContainerTest.cs index 7a3d3d710..27ae620e4 100644 --- a/tests/Testcontainers.Pulsar.Tests/PulsarContainerTest.cs +++ b/tests/Testcontainers.Pulsar.Tests/PulsarContainerTest.cs @@ -79,10 +79,10 @@ public PulsarAuthConfiguration() protected override async Task CreateClientAsync(CancellationToken ct = default) { - var token = await _pulsarContainer.CreateAuthenticationTokenAsync(TimeSpan.FromHours(1), ct) + var authToken = await _pulsarContainer.CreateAuthenticationTokenAsync(TimeSpan.FromHours(1), ct) .ConfigureAwait(false); - return PulsarClient.Builder().ServiceUrl(new Uri(_pulsarContainer.GetBrokerAddress())).Authentication(new TokenAuthentication(token)).Build(); + return PulsarClient.Builder().ServiceUrl(new Uri(_pulsarContainer.GetBrokerAddress())).Authentication(new TokenAuthentication(authToken)).Build(); } } } \ No newline at end of file diff --git a/tests/Testcontainers.Pulsar.Tests/Usings.cs b/tests/Testcontainers.Pulsar.Tests/Usings.cs index 8d5cfee6f..56f7420c0 100644 --- a/tests/Testcontainers.Pulsar.Tests/Usings.cs +++ b/tests/Testcontainers.Pulsar.Tests/Usings.cs @@ -1,13 +1,10 @@ global using System; -global using System.Collections.Generic; global using System.Text; global using System.Threading; global using System.Threading.Tasks; -global using DotNet.Testcontainers.Commons; global using DotPulsar; global using DotPulsar.Abstractions; global using DotPulsar.Extensions; global using DotPulsar.Internal; global using JetBrains.Annotations; -global using Xunit; -global using Xunit.Abstractions; \ No newline at end of file +global using Xunit; \ No newline at end of file From 012c42de6e535fc75f0cd1d56fed42a1a56835cf Mon Sep 17 00:00:00 2001 From: David Jensen Date: Wed, 29 May 2024 14:14:59 +0200 Subject: [PATCH 10/13] Update Pulsar image version and extend auth token validity Pulsar image version in PulsarBuilder has been updated from 3.0.4 to 3.2.3. The authentication token validity period has been extended from 1 hour to 365 days in both PulsarBuilder and PulsarContainerTest to reduce the impact of a token creation bug in Apache Pulsar. This change significantly reduces the impact of a token creation bug that affected versions 3.2.0 to 3.2.3. The bug is related to mishandling time units (seconds as milliseconds). --- src/Testcontainers.Pulsar/PulsarBuilder.cs | 4 ++-- tests/Testcontainers.Pulsar.Tests/PulsarContainerTest.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Testcontainers.Pulsar/PulsarBuilder.cs b/src/Testcontainers.Pulsar/PulsarBuilder.cs index 7b9b83525..4d22c6789 100644 --- a/src/Testcontainers.Pulsar/PulsarBuilder.cs +++ b/src/Testcontainers.Pulsar/PulsarBuilder.cs @@ -4,7 +4,7 @@ namespace Testcontainers.Pulsar; [PublicAPI] public sealed class PulsarBuilder : ContainerBuilder { - public const string PulsarImage = "apachepulsar/pulsar:3.0.4"; + public const string PulsarImage = "apachepulsar/pulsar:3.2.3"; public const ushort PulsarBrokerDataPort = 6650; @@ -163,7 +163,7 @@ private async Task UntilAsync(PulsarContainer container) { try { - _authToken = await container.CreateAuthenticationTokenAsync(TimeSpan.FromHours(1)) + _authToken = await container.CreateAuthenticationTokenAsync(TimeSpan.FromDays(365)) .ConfigureAwait(false); _ = _httpWaitStrategy.WithHeader("Authorization", "Bearer " + _authToken.Trim()); diff --git a/tests/Testcontainers.Pulsar.Tests/PulsarContainerTest.cs b/tests/Testcontainers.Pulsar.Tests/PulsarContainerTest.cs index 27ae620e4..2c51f5a2e 100644 --- a/tests/Testcontainers.Pulsar.Tests/PulsarContainerTest.cs +++ b/tests/Testcontainers.Pulsar.Tests/PulsarContainerTest.cs @@ -79,7 +79,7 @@ public PulsarAuthConfiguration() protected override async Task CreateClientAsync(CancellationToken ct = default) { - var authToken = await _pulsarContainer.CreateAuthenticationTokenAsync(TimeSpan.FromHours(1), ct) + var authToken = await _pulsarContainer.CreateAuthenticationTokenAsync(TimeSpan.FromDays(365), ct) .ConfigureAwait(false); return PulsarClient.Builder().ServiceUrl(new Uri(_pulsarContainer.GetBrokerAddress())).Authentication(new TokenAuthentication(authToken)).Build(); From da4b6e737230457e70db0d3771be65cf6e397270 Mon Sep 17 00:00:00 2001 From: entvex <1580435+entvex@users.noreply.github.com> Date: Wed, 29 May 2024 20:03:42 +0200 Subject: [PATCH 11/13] Handle Pulsar Token bug in affected versions The issue arises because Apache Pulsar incorrectly treats seconds as milliseconds in specific versions. To address this, we multiply the value by 1000 when using affected versions. --- src/Testcontainers.Pulsar/PulsarBuilder.cs | 2 +- src/Testcontainers.Pulsar/PulsarContainer.cs | 7 ++++++- tests/Testcontainers.Pulsar.Tests/PulsarContainerTest.cs | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Testcontainers.Pulsar/PulsarBuilder.cs b/src/Testcontainers.Pulsar/PulsarBuilder.cs index 4d22c6789..e223c5c43 100644 --- a/src/Testcontainers.Pulsar/PulsarBuilder.cs +++ b/src/Testcontainers.Pulsar/PulsarBuilder.cs @@ -163,7 +163,7 @@ private async Task UntilAsync(PulsarContainer container) { try { - _authToken = await container.CreateAuthenticationTokenAsync(TimeSpan.FromDays(365)) + _authToken = await container.CreateAuthenticationTokenAsync(TimeSpan.FromHours(1)) .ConfigureAwait(false); _ = _httpWaitStrategy.WithHeader("Authorization", "Bearer " + _authToken.Trim()); diff --git a/src/Testcontainers.Pulsar/PulsarContainer.cs b/src/Testcontainers.Pulsar/PulsarContainer.cs index 90bb83a52..32225850c 100644 --- a/src/Testcontainers.Pulsar/PulsarContainer.cs +++ b/src/Testcontainers.Pulsar/PulsarContainer.cs @@ -58,9 +58,14 @@ public async Task CreateAuthenticationTokenAsync(TimeSpan expire = defau "--subject", PulsarBuilder.Username, "--expiry-time", - $"{expire.TotalSeconds}s", + "", }; + if (_configuration.Image.FullName is "apachepulsar/pulsar:3.2.0" or "apachepulsar/pulsar:3.2.1" or "apachepulsar/pulsar:3.2.2" or "apachepulsar/pulsar:3.2.3") + command[8] = $"{expire.TotalSeconds * 1000}s"; + else + command[8] = $"{expire.TotalSeconds}s"; + var tokensResult = await ExecAsync(command, ct) .ConfigureAwait(false); diff --git a/tests/Testcontainers.Pulsar.Tests/PulsarContainerTest.cs b/tests/Testcontainers.Pulsar.Tests/PulsarContainerTest.cs index 2c51f5a2e..27ae620e4 100644 --- a/tests/Testcontainers.Pulsar.Tests/PulsarContainerTest.cs +++ b/tests/Testcontainers.Pulsar.Tests/PulsarContainerTest.cs @@ -79,7 +79,7 @@ public PulsarAuthConfiguration() protected override async Task CreateClientAsync(CancellationToken ct = default) { - var authToken = await _pulsarContainer.CreateAuthenticationTokenAsync(TimeSpan.FromDays(365), ct) + var authToken = await _pulsarContainer.CreateAuthenticationTokenAsync(TimeSpan.FromHours(1), ct) .ConfigureAwait(false); return PulsarClient.Builder().ServiceUrl(new Uri(_pulsarContainer.GetBrokerAddress())).Authentication(new TokenAuthentication(authToken)).Build(); From 88241b518028347c21e7b70a2053a813882cacb1 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Sun, 2 Jun 2024 09:29:55 +0200 Subject: [PATCH 12/13] chore: Update Pulsar docs --- docs/modules/elasticsearch.md | 2 - docs/modules/index.md | 2 +- docs/modules/mongodb.md | 2 - docs/modules/neo4j.md | 2 - docs/modules/pulsar.md | 162 ++++++++----------- src/Testcontainers.Pulsar/PulsarContainer.cs | 19 ++- src/Testcontainers.Pulsar/Usings.cs | 3 +- 7 files changed, 86 insertions(+), 106 deletions(-) diff --git a/docs/modules/elasticsearch.md b/docs/modules/elasticsearch.md index 156eb76d2..3d141524d 100644 --- a/docs/modules/elasticsearch.md +++ b/docs/modules/elasticsearch.md @@ -51,5 +51,3 @@ To execute the tests, use the command `dotnet test` from a terminal. ## A Note To Developers The Testcontainers module creates a container that listens to requests over **HTTPS**. To communicate with the Elasticsearch instance, developers must create a `ElasticsearchClientSettings` instance and set the `ServerCertificateValidationCallback` delegate to `CertificateValidations.AllowAll`. Failing to do so will result in a communication failure as the .NET will reject the certificate coming from the container. - -[xunit]: https://xunit.net/ diff --git a/docs/modules/index.md b/docs/modules/index.md index 0f777c3c0..2c2cf5ea2 100644 --- a/docs/modules/index.md +++ b/docs/modules/index.md @@ -56,7 +56,7 @@ await moduleNameContainer.StartAsync(); | Papercut | `jijiechen/papercut:latest` | [NuGet](https://www.nuget.org/packages/Testcontainers.Papercut) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Papercut) | | PostgreSQL | `postgres:15.1` | [NuGet](https://www.nuget.org/packages/Testcontainers.PostgreSql) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.PostgreSql) | | PubSub | `gcr.io/google.com/cloudsdktool/google-cloud-cli:446.0.1-emulators` | [NuGet](https://www.nuget.org/packages/Testcontainers.PubSub) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.PubSub) | -| Pulsar | `apachepulsar/pulsar:3.0.4` | [NuGet](https://www.nuget.org/packages/Testcontainers.Pulsar) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Pulsar) | +| Pulsar | `apachepulsar/pulsar:3.2.3` | [NuGet](https://www.nuget.org/packages/Testcontainers.Pulsar) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Pulsar) | | RabbitMQ | `rabbitmq:3.11` | [NuGet](https://www.nuget.org/packages/Testcontainers.RabbitMq) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.RabbitMq) | | RavenDB | `ravendb/ravendb:5.4-ubuntu-latest` | [NuGet](https://www.nuget.org/packages/Testcontainers.RavenDb) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.RavenDb) | | Redis | `redis:7.0` | [NuGet](https://www.nuget.org/packages/Testcontainers.Redis) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Redis) | diff --git a/docs/modules/mongodb.md b/docs/modules/mongodb.md index 43cb06fcf..478d0d63e 100644 --- a/docs/modules/mongodb.md +++ b/docs/modules/mongodb.md @@ -45,5 +45,3 @@ public sealed class MongoDbContainerTest : IAsyncLifetime ``` To execute the tests, use the command `dotnet test` from a terminal. - -[xunit]: https://xunit.net/ diff --git a/docs/modules/neo4j.md b/docs/modules/neo4j.md index 991bbc7f0..c93e7189a 100644 --- a/docs/modules/neo4j.md +++ b/docs/modules/neo4j.md @@ -47,5 +47,3 @@ public sealed class Neo4jContainerTest : IAsyncLifetime ``` To execute the tests, use the command `dotnet test` from a terminal. - -[xunit]: https://xunit.net/ diff --git a/docs/modules/pulsar.md b/docs/modules/pulsar.md index fcc45adf7..02e2c22da 100644 --- a/docs/modules/pulsar.md +++ b/docs/modules/pulsar.md @@ -1,8 +1,6 @@ -# Apache Pulsar Module +# Apache Pulsar -Testcontainers can be used to automatically create [Apache Pulsar](https://pulsar.apache.org) containers without external services. - -It's based on the official Apache Pulsar docker image, it is recommended to read the [official guide](https://pulsar.apache.org/docs/next/getting-started-docker/). +Testcontainers can be used to automatically create [Apache Pulsar](https://pulsar.apache.org) containers without the need for external services. Based on the official Apache Pulsar Docker image, it is recommended to read the official [getting started](https://pulsar.apache.org/docs/next/getting-started-docker/) guide. The following example uses the following NuGet packages: @@ -11,120 +9,100 @@ dotnet add package Testcontainers.Pulsar dotnet add package DotPulsar dotnet add package xunit ``` + IDEs and editors may also require the following packages to run tests: `xunit.runner.visualstudio` and `Microsoft.NET.Test.Sdk`. Copy and paste the following code into a new `.cs` test file within an existing test project. ```csharp -using System.Collections.Generic; -using System.Threading; +using System; +using System.Text; +using System.Threading.Tasks; using DotPulsar; -using DotPulsar.Abstractions; using DotPulsar.Extensions; -using Xunit.Abstractions; +using Xunit; -namespace Testcontainers.Pulsar.Tests; +namespace Testcontainers.Pulsar; public sealed class PulsarContainerTest : IAsyncLifetime { - private readonly CancellationTokenSource _cts; - private readonly PulsarContainer _pulsarContainer; - private readonly ITestOutputHelper _testOutputHelper; - - public PulsarContainerTest(ITestOutputHelper testOutputHelper) - { - _testOutputHelper = testOutputHelper; - _pulsarContainer = new PulsarBuilder().Build(); - _cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); - } - - public Task InitializeAsync() - { - return _pulsarContainer.StartAsync(); - } - - public Task DisposeAsync() - { - return _pulsarContainer.DisposeAsync().AsTask(); - } - - [Fact] - [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] - public async Task PulsarContainer_WhenBrokerIsStarted_ShouldConnect() - { - // Given - await using var client = CreateClient(); - var expected = new List { MessageId.Earliest }; - await using var reader = CreateReader(client, MessageId.Earliest, await CreateTopic(_cts.Token)); - - // When - var actual = await reader.GetLastMessageIds(_cts.Token); - - // Then - Assert.Equal(expected,actual); - } - - private IReader CreateReader(IPulsarClient pulsarClient, MessageId messageId, string topicName) - => pulsarClient.NewReader(Schema.String) - .StartMessageId(messageId) - .Topic(topicName) - .Create(); - - private static string CreateTopicName() => $"persistent://public/default/{Guid.NewGuid():N}"; - - private async Task CreateTopic(string topic, CancellationToken cancellationToken) - { - var arguments = $"bin/pulsar-admin topics create {topic}"; - - var result = await _pulsarContainer.ExecAsync(new[] { "/bin/bash", "-c", arguments }, cancellationToken); - - if (result.ExitCode != 0) - throw new Exception($"Could not create the topic: {result.Stderr}"); - } - - private async Task CreateTopic(CancellationToken cancellationToken) - { - var topic = CreateTopicName(); - await CreateTopic(topic, cancellationToken); - return topic; - } - - private IPulsarClient CreateClient() - => PulsarClient - .Builder() - .ExceptionHandler(context => _testOutputHelper.WriteLine($"PulsarClient got an exception: {context.Exception}")) - .ServiceUrl(new Uri(_pulsarContainer.GetPulsarBrokerUrl())) - .Build(); + private readonly PulsarContainer _pulsarContainer = + new PulsarBuilder().Build(); + + [Fact] + public async Task ConsumerReceivesSendMessage() + { + const string helloPulsar = "Hello, Pulsar!"; + + var topic = $"persistent://public/default/{Guid.NewGuid():D}"; + + var name = Guid.NewGuid().ToString("D"); + + await using var client = PulsarClient.Builder() + .ServiceUrl(new Uri(_pulsarContainer.GetBrokerAddress())) + .Build(); + + await using var producer = client.NewProducer(Schema.String) + .Topic(topic) + .Create(); + + await using var consumer = client.NewConsumer(Schema.String) + .Topic(topic) + .SubscriptionName(name) + .InitialPosition(SubscriptionInitialPosition.Earliest) + .Create(); + + _ = await producer.Send(helloPulsar) + .ConfigureAwait(true); + + var message = await consumer.Receive() + .ConfigureAwait(true); + + Assert.Equal(helloPulsar, Encoding.Default.GetString(message.Data)); + } + + public Task InitializeAsync() + => _pulsarContainer.StartAsync(); + + public Task DisposeAsync() + => _pulsarContainer.DisposeAsync().AsTask(); } ``` -To execute the test, use the command `dotnet test` from a terminal. +To execute the tests, use the command `dotnet test` from a terminal. + +## Access Pulsar -## Builder +To get the Pulsar broker URL use: -### Token authentication -If you need to use token authentication use the follow with method in the builder ```csharp -PulsarBuilder().WithTokenAuthentication().Build(); +string pulsarBrokerUrl = _pulsarContainer.GetPulsarBrokerUrl(); ``` -and get the token by using +To get the Pulsar service URL use: ```csharp -var token = await _pulsarContainer.CreateToken(Timeout.InfiniteTimeSpan); +string pulsarServiceUrl = _pulsarContainer.GetHttpServiceUrl(); ``` -#### Pulsar Functions -If you need to use Pulsar Functions use the follow with method in the builder +### Enable token authentication + +If you need to use token authentication, use the following builder configuration to enable authentication: + ```csharp -PulsarBuilder().WithFunctions().Build(); +PulsarContainer _pulsarContainer = PulsarBuilder().WithTokenAuthentication().Build(); ``` -## Access Pulsar -To get the the Pulsar broker url. + +Start the container and get the token from the running instance by using: + ```csharp -string pulsarBrokerUrl = _pulsarContainer.GetPulsarBrokerUrl(); +var authToken = await container.CreateAuthenticationTokenAsync(TimeSpan.FromHours(1)) + .ConfigureAwait(false); ``` -To get the the Pulsar service url. +#### Enable Pulsar Functions + +If you need to use Pulsar Functions, use the following builder configuration to enable it: + ```csharp -string pulsarBrokerUrl = _pulsarContainer.GetHttpServiceUrl(); +PulsarContainer _pulsarContainer = PulsarBuilder().WithFunctions().Build(); ``` diff --git a/src/Testcontainers.Pulsar/PulsarContainer.cs b/src/Testcontainers.Pulsar/PulsarContainer.cs index 32225850c..7d42e7cac 100644 --- a/src/Testcontainers.Pulsar/PulsarContainer.cs +++ b/src/Testcontainers.Pulsar/PulsarContainer.cs @@ -43,11 +43,23 @@ public string GetServiceAddress() /// public async Task CreateAuthenticationTokenAsync(TimeSpan expire = default, CancellationToken ct = default) { + int secondsToMilliseconds; + if (_configuration.AuthenticationEnabled.HasValue && !_configuration.AuthenticationEnabled.Value) { throw new ArgumentException("Failed to create token. Authentication is not enabled."); } + if (_configuration.Image.Tag.StartsWith("3.2") || _configuration.Image.Tag.StartsWith("latest")) + { + Logger.LogWarning("The 'apachepulsar/pulsar:3.2.?' image contains a regression. The expiry time is converted to the wrong unit of time: https://github.com/apache/pulsar/issues/22811."); + secondsToMilliseconds = 1000; + } + else + { + secondsToMilliseconds = 1; + } + var command = new[] { "bin/pulsar", @@ -58,14 +70,9 @@ public async Task CreateAuthenticationTokenAsync(TimeSpan expire = defau "--subject", PulsarBuilder.Username, "--expiry-time", - "", + $"{secondsToMilliseconds * expire.TotalSeconds}s", }; - if (_configuration.Image.FullName is "apachepulsar/pulsar:3.2.0" or "apachepulsar/pulsar:3.2.1" or "apachepulsar/pulsar:3.2.2" or "apachepulsar/pulsar:3.2.3") - command[8] = $"{expire.TotalSeconds * 1000}s"; - else - command[8] = $"{expire.TotalSeconds}s"; - var tokensResult = await ExecAsync(command, ct) .ConfigureAwait(false); diff --git a/src/Testcontainers.Pulsar/Usings.cs b/src/Testcontainers.Pulsar/Usings.cs index 35bfa1312..59c490a37 100644 --- a/src/Testcontainers.Pulsar/Usings.cs +++ b/src/Testcontainers.Pulsar/Usings.cs @@ -13,4 +13,5 @@ global using DotNet.Testcontainers.Builders; global using DotNet.Testcontainers.Configurations; global using DotNet.Testcontainers.Containers; -global using JetBrains.Annotations; \ No newline at end of file +global using JetBrains.Annotations; +global using Microsoft.Extensions.Logging; \ No newline at end of file From f4d5be96de5e451c39b5cc2efd92b5db30714a54 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Sun, 2 Jun 2024 09:48:07 +0200 Subject: [PATCH 13/13] fix: Use correct Pulsar docs section, add Pulsar doc --- docs/modules/pulsar.md | 4 ++-- mkdocs.yml | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/modules/pulsar.md b/docs/modules/pulsar.md index 02e2c22da..3f91b83fa 100644 --- a/docs/modules/pulsar.md +++ b/docs/modules/pulsar.md @@ -84,7 +84,7 @@ To get the Pulsar service URL use: string pulsarServiceUrl = _pulsarContainer.GetHttpServiceUrl(); ``` -### Enable token authentication +## Enable token authentication If you need to use token authentication, use the following builder configuration to enable authentication: @@ -99,7 +99,7 @@ var authToken = await container.CreateAuthenticationTokenAsync(TimeSpan.FromHour .ConfigureAwait(false); ``` -#### Enable Pulsar Functions +## Enable Pulsar Functions If you need to use Pulsar Functions, use the following builder configuration to enable it: diff --git a/mkdocs.yml b/mkdocs.yml index 97c52d79b..8c43c04de 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -45,4 +45,5 @@ nav: - modules/mssql.md - modules/neo4j.md - modules/postgres.md + - modules/pulsar.md - modules/rabbitmq.md