Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
75 changes: 75 additions & 0 deletions docs/modules/opensearch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# 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"
}
```
=== "SslBasicAuth"
```csharp
--8<-- "tests/Testcontainers.OpenSearch.Tests/OpenSearchContainerTest.cs:SslBasicAuth"
```
=== "InsecureNoAuth"
```csharp
--8<-- "tests/Testcontainers.OpenSearch.Tests/OpenSearchContainerTest.cs:InsecureNoAuth"
```

How to check that client established connection:
=== "PingExample"
```csharp
--8<-- "tests/Testcontainers.OpenSearch.Tests/OpenSearchContainerTest.cs:PingExample"
```

Creating index and index alias:
=== "IndexAndAliasCreation"
```csharp
--8<-- "tests/Testcontainers.OpenSearch.Tests/OpenSearchContainerTest.cs:IndexAndAliasCreation"
```
=== "CreateTestIndexImpl"
```csharp
--8<-- "tests/Testcontainers.OpenSearch.Tests/OpenSearchContainerTest.cs:CreateTestIndexImpl"
```

Indexing and searching for document:
=== "IndexingDocument"
```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"

## Using legacy docker images

Before version `2.12.0` OpenSearch docker images have a hardcoded password for default 'admin' user. If you are using such an image, set password explicitly like this:

=== "LegacyImageAdminPassword"
```xml
--8<-- "tests/Testcontainers.OpenSearch.Tests/OpenSearchContainerDifferentImagesTest.cs:LegacyImageAdminPassword"
```
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,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
181 changes: 181 additions & 0 deletions src/Testcontainers.OpenSearch/OpenSearchBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
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 int OpenSearchRestApiPort = 9200;

public const int OpenSearchTransportPort = 9300;

public const int OpenSearchPerformanceAnalyzerPort = 9600;

public const string DefaultUsername = "admin";

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

public const string DefaultOldInsecurePassword = "admin";

/// <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();

// 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.
var openSearchBuilder = DockerResourceConfiguration.WaitStrategies.Count() > 1 ? this : 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