Skip to content

feat: Add OpenSearch module #1395

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Jun 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/cicd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ jobs:
{ name: "Testcontainers.Nats", runs-on: "ubuntu-22.04" },
{ name: "Testcontainers.Neo4j", runs-on: "ubuntu-22.04" },
{ name: "Testcontainers.Ollama", runs-on: "ubuntu-22.04" },
{ name: "Testcontainers.OpenSearch", runs-on: "ubuntu-22.04" },
{ name: "Testcontainers.Oracle", runs-on: "ubuntu-22.04" },
{ name: "Testcontainers.Oracle11", runs-on: "ubuntu-22.04" },
{ name: "Testcontainers.Oracle18", runs-on: "ubuntu-22.04" },
Expand Down
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
<PackageVersion Include="Net.IBM.Data.Db2" Version="9.0.0.100"/>
<PackageVersion Include="Npgsql" Version="6.0.11"/>
<PackageVersion Include="OllamaSharp" Version="5.1.13"/>
<PackageVersion Include="OpenSearch.Client" Version="1.8.0"/>
<PackageVersion Include="Oracle.ManagedDataAccess.Core" Version="23.7.0"/>
<PackageVersion Include="Qdrant.Client" Version="1.13.0"/>
<PackageVersion Include="RabbitMQ.Client" Version="6.4.0"/>
Expand Down
14 changes: 14 additions & 0 deletions Testcontainers.sln
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Neo4j", "src
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Ollama", "src\Testcontainers.Ollama\Testcontainers.Ollama.csproj", "{0DB0075D-42EC-4438-93F7-630CF5BCCAF0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.OpenSearch", "src\Testcontainers.OpenSearch\Testcontainers.OpenSearch.csproj", "{49051DBC-6B80-4412-8505-BC2764A877BD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Oracle", "src\Testcontainers.Oracle\Testcontainers.Oracle.csproj", "{596EAFC1-0496-495C-B382-D57415FA456A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Papercut", "src\Testcontainers.Papercut\Testcontainers.Papercut.csproj", "{B2608563-8EE4-49AA-A9A0-B1614486AEEF}"
Expand Down Expand Up @@ -203,6 +205,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Neo4j.Tests"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Ollama.Tests", "tests\Testcontainers.Ollama.Tests\Testcontainers.Ollama.Tests.csproj", "{D3AD7D72-510C-43A4-A401-DB3C2594508E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.OpenSearch.Tests", "tests\Testcontainers.OpenSearch.Tests\Testcontainers.OpenSearch.Tests.csproj", "{04A7AF65-2E02-4E20-8056-2AAC0705B0BC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Oracle.Tests", "tests\Testcontainers.Oracle.Tests\Testcontainers.Oracle.Tests.csproj", "{4AC1088B-9965-4497-AC8E-570F1AD5631F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Oracle11.Tests", "tests\Testcontainers.Oracle11.Tests\Testcontainers.Oracle11.Tests.csproj", "{0A0AC20D-226B-46F9-B267-0D00964A7601}"
Expand Down Expand Up @@ -411,6 +415,10 @@ Global
{0DB0075D-42EC-4438-93F7-630CF5BCCAF0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0DB0075D-42EC-4438-93F7-630CF5BCCAF0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0DB0075D-42EC-4438-93F7-630CF5BCCAF0}.Release|Any CPU.Build.0 = Release|Any CPU
{49051DBC-6B80-4412-8505-BC2764A877BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{49051DBC-6B80-4412-8505-BC2764A877BD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{49051DBC-6B80-4412-8505-BC2764A877BD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{49051DBC-6B80-4412-8505-BC2764A877BD}.Release|Any CPU.Build.0 = Release|Any CPU
{596EAFC1-0496-495C-B382-D57415FA456A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{596EAFC1-0496-495C-B382-D57415FA456A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{596EAFC1-0496-495C-B382-D57415FA456A}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand Down Expand Up @@ -639,6 +647,10 @@ Global
{D3AD7D72-510C-43A4-A401-DB3C2594508E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D3AD7D72-510C-43A4-A401-DB3C2594508E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D3AD7D72-510C-43A4-A401-DB3C2594508E}.Release|Any CPU.Build.0 = Release|Any CPU
{04A7AF65-2E02-4E20-8056-2AAC0705B0BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{04A7AF65-2E02-4E20-8056-2AAC0705B0BC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{04A7AF65-2E02-4E20-8056-2AAC0705B0BC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{04A7AF65-2E02-4E20-8056-2AAC0705B0BC}.Release|Any CPU.Build.0 = Release|Any CPU
{4AC1088B-9965-4497-AC8E-570F1AD5631F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4AC1088B-9965-4497-AC8E-570F1AD5631F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4AC1088B-9965-4497-AC8E-570F1AD5631F}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand Down Expand Up @@ -782,6 +794,7 @@ Global
{BF37BEA1-0816-4326-B1E0-E82290F8FCE0} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{ADC2372B-6FE0-421D-8277-BB628E8EFC22} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{0DB0075D-42EC-4438-93F7-630CF5BCCAF0} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{49051DBC-6B80-4412-8505-BC2764A877BD} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{596EAFC1-0496-495C-B382-D57415FA456A} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{B2608563-8EE4-49AA-A9A0-B1614486AEEF} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{8AB91636-9055-4900-A72A-7CFFACDFDBF0} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
Expand Down Expand Up @@ -839,6 +852,7 @@ Global
{87A3F137-6DC3-4CE5-91E6-01797D076086} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{D3F63405-C0FA-4F83-8B79-E30BFF5FF5BF} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{D3AD7D72-510C-43A4-A401-DB3C2594508E} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{04A7AF65-2E02-4E20-8056-2AAC0705B0BC} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{4AC1088B-9965-4497-AC8E-570F1AD5631F} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{0A0AC20D-226B-46F9-B267-0D00964A7601} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{E4C887A9-A44A-4641-BB9B-0664CC4C362F} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
Expand Down
1 change: 1 addition & 0 deletions docs/modules/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ await moduleNameContainer.StartAsync();
| MySQL | `mysql:8.0` | [NuGet](https://www.nuget.org/packages/Testcontainers.MySql) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.MySql) |
| NATS | `nats:2.9` | [NuGet](https://www.nuget.org/packages/Testcontainers.Nats) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Nats) |
| Neo4j | `neo4j:5.4` | [NuGet](https://www.nuget.org/packages/Testcontainers.Neo4j) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Neo4j) |
| OpenSearch | `opensearchproject/opensearch:2.12.0` | [NuGet](https://www.nuget.org/packages/Testcontainers.OpenSearch) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.OpenSearch) |
| Oracle | `gvenzl/oracle-xe:21.3.0-slim-faststart` | [NuGet](https://www.nuget.org/packages/Testcontainers.Oracle) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Oracle) |
| Papercut | `changemakerstudiosus/papercut-smtp: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) |
Expand Down
73 changes: 73 additions & 0 deletions docs/modules/opensearch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# OpenSearch

[OpenSearch](https://opensearch.org/) is an open-source, enterprise-grade search and observability suite that brings order to unstructured data at scale.

Add the following dependency to your project file:

```shell title="NuGet"
dotnet add package Testcontainers.OpenSearch
```

You can start an OpenSearch container instance from any .NET application. To create and start a container instance with the default configuration, use the module-specific builder as shown below:

=== "Start an OpenSearch container"
```csharp
var openSearchContainer = new OpenSearchBuilder().Build();
await openSearchContainer.StartAsync();
```

This example uses xUnit.net's `IAsyncLifetime` interface to manage the lifecycle of the container. The container is started in the `InitializeAsync` method before the test method runs, ensuring that the environment is ready for testing. After the test completes, the container is removed in the `DisposeAsync` method.

=== "Base test class"
```csharp
--8<-- "tests/Testcontainers.OpenSearch.Tests/OpenSearchContainerTest.cs:BaseClass"
}
```
=== "Insecure no auth"
```csharp
--8<-- "tests/Testcontainers.OpenSearch.Tests/OpenSearchContainerTest.cs:InsecureNoAuth"
```
=== "SSL default credentials"
```csharp
--8<-- "tests/Testcontainers.OpenSearch.Tests/OpenSearchContainerTest.cs:SslBasicAuthDefaultCredentials"
```
=== "SSL custom credentials"
```csharp
--8<-- "tests/Testcontainers.OpenSearch.Tests/OpenSearchContainerTest.cs:SslBasicAuthCustomCredentials"
```

How to check if the client has established a connection:

=== "Ping example"
```csharp
--8<-- "tests/Testcontainers.OpenSearch.Tests/OpenSearchContainerTest.cs:PingExample"
```

Creating an index and alias:

=== "Create index and alias"
```csharp
--8<-- "tests/Testcontainers.OpenSearch.Tests/OpenSearchContainerTest.cs:CreateIndexAndAlias"
```
=== "Create index implementation"
```csharp
--8<-- "tests/Testcontainers.OpenSearch.Tests/OpenSearchContainerTest.cs:CreateIndexImplementation"
```

Indexing and searching a document:

=== "Indexing document"
```csharp
--8<-- "tests/Testcontainers.OpenSearch.Tests/OpenSearchContainerTest.cs:IndexingDocument"
```

The test example uses the following NuGet dependencies:

=== "Package References"
```xml
--8<-- "tests/Testcontainers.OpenSearch.Tests/Testcontainers.OpenSearch.Tests.csproj:PackageReferences"
```

To execute the tests, use the command `dotnet test` from a terminal.

--8<-- "docs/modules/_call_out_test_projects.txt"
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ nav:
- modules/mongodb.md
- modules/mssql.md
- modules/neo4j.md
- modules/opensearch.md
- modules/postgres.md
- modules/qdrant.md
- modules/rabbitmq.md
Expand Down
1 change: 1 addition & 0 deletions src/Testcontainers.OpenSearch/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
root = true
196 changes: 196 additions & 0 deletions src/Testcontainers.OpenSearch/OpenSearchBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
namespace Testcontainers.OpenSearch;

/// <inheritdoc cref="ContainerBuilder{TBuilderEntity, TContainerEntity, TConfigurationEntity}" />
[PublicAPI]
public sealed class OpenSearchBuilder : ContainerBuilder<OpenSearchBuilder, OpenSearchContainer, OpenSearchConfiguration>
{
public const string OpenSearchImage = "opensearchproject/opensearch:2.12.0";

public const ushort OpenSearchRestApiPort = 9200;

public const ushort OpenSearchTransportPort = 9300;

public const ushort OpenSearchPerformanceAnalyzerPort = 9600;

public const string DefaultUsername = "admin";

public const string DefaultPassword = "yourStrong(!)P@ssw0rd";

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

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

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

/// <summary>
/// Sets the password for the <c>admin</c> user.
/// </summary>
/// <remarks>
/// The password must meet the following complexity requirements:
/// <list type="bullet">
/// <item><description>Minimum of 8 characters</description></item>
/// <item><description>At least one uppercase letter</description></item>
/// <item><description>At least one lowercase letter</description></item>
/// <item><description>At least one digit</description></item>
/// <item><description>At least one special character</description></item>
/// </list>
/// </remarks>
/// <param name="password">The <c>admin</c> user password.</param>
/// <returns>A configured instance of <see cref="OpenSearchBuilder" />.</returns>
public OpenSearchBuilder WithPassword(string password)
{
return Merge(DockerResourceConfiguration, new OpenSearchConfiguration(password: password))
.WithEnvironment("OPENSEARCH_INITIAL_ADMIN_PASSWORD", password);
}

/// <summary>
/// Enables or disables the built-in security plugin in OpenSearch.
/// </summary>
/// <remarks>
/// When disabled, the <see cref="OpenSearchContainer.GetConnectionString" /> method
/// will use the <c>http</c> protocol instead of <c>https</c>.
/// </remarks>
/// <param name="securityEnabled"><c>true</c> to enable the security plugin; <c>false</c> to disable it.</param>
/// <returns>A configured instance of <see cref="OpenSearchBuilder" />.</returns>
public OpenSearchBuilder WithSecurityEnabled(bool securityEnabled = true)
{
return Merge(DockerResourceConfiguration, new OpenSearchConfiguration(tlsEnabled: securityEnabled))
.WithEnvironment("plugins.security.disabled", (!securityEnabled).ToString().ToLowerInvariant());
}

/// <inheritdoc />
public override OpenSearchContainer Build()
{
Validate();

OpenSearchBuilder openSearchBuilder;

Predicate<System.Version> predicate = v => v.Major == 1 || (v.Major == 2 && v.Minor < 12);

var image = DockerResourceConfiguration.Image;

// Images before version 2.12.0 use a hardcoded default password.
var requiresHardcodedDefaultPassword = image.MatchVersion(predicate);
if (requiresHardcodedDefaultPassword)
{
openSearchBuilder = WithPassword("admin");
}
else
{
openSearchBuilder = this;
}

// By default, the base builder waits until the container is running. However, for OpenSearch, a more advanced waiting strategy is necessary that requires access to the password.
// If the user does not provide a custom waiting strategy, append the default OpenSearch waiting strategy.
openSearchBuilder = DockerResourceConfiguration.WaitStrategies.Count() > 1 ? openSearchBuilder : openSearchBuilder.WithWaitStrategy(Wait.ForUnixContainer().AddCustomWaitStrategy(new WaitUntil(DockerResourceConfiguration)));
return new OpenSearchContainer(openSearchBuilder.DockerResourceConfiguration);
}

/// <inheritdoc />
protected override OpenSearchBuilder Init()
{
return base.Init()
.WithImage(OpenSearchImage)
.WithPortBinding(OpenSearchRestApiPort, true)
.WithPortBinding(OpenSearchTransportPort, true)
.WithPortBinding(OpenSearchPerformanceAnalyzerPort, true)
.WithEnvironment("discovery.type", "single-node")
.WithSecurityEnabled()
.WithUsername(DefaultUsername)
.WithPassword(DefaultPassword);
}

/// <inheritdoc />
protected override void Validate()
{
base.Validate();

_ = Guard.Argument(DockerResourceConfiguration.Password, nameof(DockerResourceConfiguration.Password))
.NotNull()
.NotEmpty();
}

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

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

/// <inheritdoc />
protected override OpenSearchBuilder Merge(OpenSearchConfiguration oldValue, OpenSearchConfiguration newValue)
{
return new OpenSearchBuilder(new OpenSearchConfiguration(oldValue, newValue));
}

/// <summary>
/// Sets the OpenSearch username.
/// </summary>
/// <remarks>
/// The Docker image does not allow to configure the username.
/// </remarks>
/// <param name="username">The OpenSearch username.</param>
/// <returns>A configured instance of <see cref="OpenSearchBuilder" />.</returns>
private OpenSearchBuilder WithUsername(string username)
{
return Merge(DockerResourceConfiguration, new OpenSearchConfiguration(username: username));
}

/// <inheritdoc cref="IWaitUntil" />
private sealed class WaitUntil : IWaitUntil
{
private readonly bool _tlsEnabled;

private readonly string _username;

private readonly string _password;

/// <summary>
/// Initializes a new instance of the <see cref="WaitUntil" /> class.
/// </summary>
/// <param name="configuration">The container configuration.</param>
public WaitUntil(OpenSearchConfiguration configuration)
{
_tlsEnabled = configuration.TlsEnabled.GetValueOrDefault();
_username = configuration.Username;
_password = configuration.Password;
}

/// <inheritdoc />
public async Task<bool> UntilAsync(IContainer container)
{
using var httpMessageHandler = new HttpClientHandler();
httpMessageHandler.ServerCertificateCustomValidationCallback = (_, _, _, _) => true;

var httpWaitStrategy = new HttpWaitStrategy()
.UsingHttpMessageHandler(httpMessageHandler)
.UsingTls(_tlsEnabled)
.WithBasicAuthentication(_username, _password)
.ForPort(OpenSearchRestApiPort);

return await httpWaitStrategy.UntilAsync(container)
.ConfigureAwait(false);
}
}
}
Loading
Loading