Skip to content

Commit

Permalink
feat: Add Apache Pulsar module
Browse files Browse the repository at this point in the history
  • Loading branch information
David Jensen committed Feb 9, 2024
1 parent 1c80fe8 commit b31b4c1
Show file tree
Hide file tree
Showing 14 changed files with 594 additions and 1 deletion.
3 changes: 2 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
<PackageVersion Include="Confluent.Kafka" Version="2.0.2"/>
<PackageVersion Include="Consul" Version="1.6.10.9"/>
<PackageVersion Include="CouchbaseNetClient" Version="3.4.3"/>
<PackageVersion Include="DotPulsar" Version="3.1.2"/>
<PackageVersion Include="Elastic.Clients.Elasticsearch" Version="8.0.5"/>
<PackageVersion Include="EventStore.Client.Grpc.Streams" Version="22.0.0"/>
<PackageVersion Include="FirebirdSql.Data.FirebirdClient" Version="10.0.0"/>
Expand Down Expand Up @@ -59,4 +60,4 @@
<PackageVersion Include="Selenium.WebDriver" Version="4.8.1"/>
<PackageVersion Include="StackExchange.Redis" Version="2.6.90"/>
</ItemGroup>
</Project>
</Project>
14 changes: 14 additions & 0 deletions Testcontainers.sln
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,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
Expand Down Expand Up @@ -556,6 +560,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}
Expand Down Expand Up @@ -647,5 +659,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
1 change: 1 addition & 0 deletions docs/modules/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down
130 changes: 130 additions & 0 deletions docs/modules/pulsar.md
Original file line number Diff line number Diff line change
@@ -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> { 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<string> 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<string> 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();
```
1 change: 1 addition & 0 deletions src/Testcontainers.Pulsar/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
root = true
113 changes: 113 additions & 0 deletions src/Testcontainers.Pulsar/PulsarBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
using System.Text;
using Testcontainers.Pulsar;

/// <inheritdoc cref="ContainerBuilder{TBuilderEntity, TContainerEntity, TConfigurationEntity}" />
[PublicAPI]
public sealed class PulsarBuilder : ContainerBuilder<PulsarBuilder, PulsarContainer, PulsarConfiguration>
{
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<string, string> _environmentVariables = new Dictionary<string, string>
{
{ "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;

/// <summary>
/// Initializes a new instance of the <see cref="PulsarBuilder" /> class.
/// </summary>
public PulsarBuilder()
: this(new PulsarConfiguration())
{
DockerResourceConfiguration = Init().DockerResourceConfiguration;
}

/// <summary>
/// Initializes a new instance of the <see cref="PulsarBuilder" /> class.
/// </summary>
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
private PulsarBuilder(PulsarConfiguration resourceConfiguration)
: base(resourceConfiguration)
{
DockerResourceConfiguration = resourceConfiguration;
}

/// <inheritdoc />
protected override PulsarConfiguration DockerResourceConfiguration { get; }

/// <inheritdoc />
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);
}

/// <inheritdoc />
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));
}

/// <inheritdoc />
protected override PulsarBuilder Clone(IResourceConfiguration<CreateContainerParameters> resourceConfiguration)
{
return Merge(DockerResourceConfiguration, new PulsarConfiguration(resourceConfiguration));
}

/// <inheritdoc />
protected override PulsarBuilder Clone(IContainerConfiguration resourceConfiguration)
{
return Merge(DockerResourceConfiguration, new PulsarConfiguration(resourceConfiguration));
}

/// <inheritdoc />
protected override PulsarBuilder Merge(PulsarConfiguration oldValue, PulsarConfiguration newValue)
{
return new PulsarBuilder(new PulsarConfiguration(oldValue, newValue));
}
}
68 changes: 68 additions & 0 deletions src/Testcontainers.Pulsar/PulsarConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
namespace Testcontainers.Pulsar;

/// <inheritdoc cref="ContainerConfiguration" />
[PublicAPI]
public sealed class PulsarConfiguration : ContainerConfiguration
{
/// <summary>
/// Initializes a new instance of the <see cref="PulsarConfiguration" /> class.
/// </summary>
public PulsarConfiguration(string authentication = null,

Check warning on line 10 in src/Testcontainers.Pulsar/PulsarConfiguration.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04)

Cannot convert null literal to non-nullable reference type.

Check warning on line 10 in src/Testcontainers.Pulsar/PulsarConfiguration.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04)

Cannot convert null literal to non-nullable reference type.

Check warning on line 10 in src/Testcontainers.Pulsar/PulsarConfiguration.cs

View workflow job for this annotation

GitHub Actions / build (windows-2022)

Cannot convert null literal to non-nullable reference type.

Check warning on line 10 in src/Testcontainers.Pulsar/PulsarConfiguration.cs

View workflow job for this annotation

GitHub Actions / build (windows-2022)

Cannot convert null literal to non-nullable reference type.
string functions = null)

Check warning on line 11 in src/Testcontainers.Pulsar/PulsarConfiguration.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04)

Cannot convert null literal to non-nullable reference type.

Check warning on line 11 in src/Testcontainers.Pulsar/PulsarConfiguration.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04)

Cannot convert null literal to non-nullable reference type.

Check warning on line 11 in src/Testcontainers.Pulsar/PulsarConfiguration.cs

View workflow job for this annotation

GitHub Actions / build (windows-2022)

Cannot convert null literal to non-nullable reference type.

Check warning on line 11 in src/Testcontainers.Pulsar/PulsarConfiguration.cs

View workflow job for this annotation

GitHub Actions / build (windows-2022)

Cannot convert null literal to non-nullable reference type.
{
Authentication = authentication;
Functions = functions;
}

/// <summary>
/// Initializes a new instance of the <see cref="PulsarConfiguration" /> class.
/// </summary>
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
public PulsarConfiguration(IResourceConfiguration<CreateContainerParameters> resourceConfiguration)

Check warning on line 21 in src/Testcontainers.Pulsar/PulsarConfiguration.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04)

Non-nullable property 'Authentication' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 21 in src/Testcontainers.Pulsar/PulsarConfiguration.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04)

Non-nullable property 'Functions' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 21 in src/Testcontainers.Pulsar/PulsarConfiguration.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04)

Non-nullable property 'Authentication' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 21 in src/Testcontainers.Pulsar/PulsarConfiguration.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04)

Non-nullable property 'Functions' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 21 in src/Testcontainers.Pulsar/PulsarConfiguration.cs

View workflow job for this annotation

GitHub Actions / build (windows-2022)

Non-nullable property 'Authentication' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 21 in src/Testcontainers.Pulsar/PulsarConfiguration.cs

View workflow job for this annotation

GitHub Actions / build (windows-2022)

Non-nullable property 'Functions' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 21 in src/Testcontainers.Pulsar/PulsarConfiguration.cs

View workflow job for this annotation

GitHub Actions / build (windows-2022)

Non-nullable property 'Authentication' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 21 in src/Testcontainers.Pulsar/PulsarConfiguration.cs

View workflow job for this annotation

GitHub Actions / build (windows-2022)

Non-nullable property 'Functions' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
: base(resourceConfiguration)
{
// Passes the configuration upwards to the base implementations to create an updated immutable copy.
}

/// <summary>
/// Initializes a new instance of the <see cref="PulsarConfiguration" /> class.
/// </summary>
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
public PulsarConfiguration(IContainerConfiguration resourceConfiguration)

Check warning on line 31 in src/Testcontainers.Pulsar/PulsarConfiguration.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04)

Non-nullable property 'Authentication' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 31 in src/Testcontainers.Pulsar/PulsarConfiguration.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04)

Non-nullable property 'Functions' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 31 in src/Testcontainers.Pulsar/PulsarConfiguration.cs

View workflow job for this annotation

GitHub Actions / build (windows-2022)

Non-nullable property 'Authentication' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 31 in src/Testcontainers.Pulsar/PulsarConfiguration.cs

View workflow job for this annotation

GitHub Actions / build (windows-2022)

Non-nullable property 'Functions' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
: base(resourceConfiguration)
{
// Passes the configuration upwards to the base implementations to create an updated immutable copy.
}

/// <summary>
/// Initializes a new instance of the <see cref="PulsarConfiguration" /> class.
/// </summary>
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
public PulsarConfiguration(PulsarConfiguration resourceConfiguration)
: this(new PulsarConfiguration(), resourceConfiguration)
{
// Passes the configuration upwards to the base implementations to create an updated immutable copy.
}

/// <summary>
/// Initializes a new instance of the <see cref="PulsarConfiguration" /> class.
/// </summary>
/// <param name="oldValue">The old Docker resource configuration.</param>
/// <param name="newValue">The new Docker resource configuration.</param>
public PulsarConfiguration(PulsarConfiguration oldValue, PulsarConfiguration newValue)
: base(oldValue, newValue)
{
Authentication = BuildConfiguration.Combine(oldValue.Authentication, newValue.Authentication);
Functions = BuildConfiguration.Combine(oldValue.Functions, newValue.Functions);
}

/// <summary>
/// Gets authentication.
/// </summary>
public string Authentication { get; }

/// <summary>
/// Gets functions.
/// </summary>
public string Functions { get; }
}
Loading

0 comments on commit b31b4c1

Please sign in to comment.