Skip to content

Commit 46a0865

Browse files
feat: Add OpenSearch module (#1395)
Co-authored-by: Andre Hofmeister <[email protected]>
1 parent 5cd0a6f commit 46a0865

File tree

17 files changed

+662
-0
lines changed

17 files changed

+662
-0
lines changed

.github/workflows/cicd.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ jobs:
7474
{ name: "Testcontainers.Nats", runs-on: "ubuntu-22.04" },
7575
{ name: "Testcontainers.Neo4j", runs-on: "ubuntu-22.04" },
7676
{ name: "Testcontainers.Ollama", runs-on: "ubuntu-22.04" },
77+
{ name: "Testcontainers.OpenSearch", runs-on: "ubuntu-22.04" },
7778
{ name: "Testcontainers.Oracle", runs-on: "ubuntu-22.04" },
7879
{ name: "Testcontainers.Oracle11", runs-on: "ubuntu-22.04" },
7980
{ name: "Testcontainers.Oracle18", runs-on: "ubuntu-22.04" },

Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
<PackageVersion Include="Net.IBM.Data.Db2" Version="9.0.0.100"/>
8080
<PackageVersion Include="Npgsql" Version="6.0.11"/>
8181
<PackageVersion Include="OllamaSharp" Version="5.1.13"/>
82+
<PackageVersion Include="OpenSearch.Client" Version="1.8.0"/>
8283
<PackageVersion Include="Oracle.ManagedDataAccess.Core" Version="23.7.0"/>
8384
<PackageVersion Include="Qdrant.Client" Version="1.13.0"/>
8485
<PackageVersion Include="RabbitMQ.Client" Version="6.4.0"/>

Testcontainers.sln

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Neo4j", "src
8989
EndProject
9090
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Ollama", "src\Testcontainers.Ollama\Testcontainers.Ollama.csproj", "{0DB0075D-42EC-4438-93F7-630CF5BCCAF0}"
9191
EndProject
92+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.OpenSearch", "src\Testcontainers.OpenSearch\Testcontainers.OpenSearch.csproj", "{49051DBC-6B80-4412-8505-BC2764A877BD}"
93+
EndProject
9294
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Oracle", "src\Testcontainers.Oracle\Testcontainers.Oracle.csproj", "{596EAFC1-0496-495C-B382-D57415FA456A}"
9395
EndProject
9496
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Papercut", "src\Testcontainers.Papercut\Testcontainers.Papercut.csproj", "{B2608563-8EE4-49AA-A9A0-B1614486AEEF}"
@@ -203,6 +205,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Neo4j.Tests"
203205
EndProject
204206
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Ollama.Tests", "tests\Testcontainers.Ollama.Tests\Testcontainers.Ollama.Tests.csproj", "{D3AD7D72-510C-43A4-A401-DB3C2594508E}"
205207
EndProject
208+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.OpenSearch.Tests", "tests\Testcontainers.OpenSearch.Tests\Testcontainers.OpenSearch.Tests.csproj", "{04A7AF65-2E02-4E20-8056-2AAC0705B0BC}"
209+
EndProject
206210
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Oracle.Tests", "tests\Testcontainers.Oracle.Tests\Testcontainers.Oracle.Tests.csproj", "{4AC1088B-9965-4497-AC8E-570F1AD5631F}"
207211
EndProject
208212
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Oracle11.Tests", "tests\Testcontainers.Oracle11.Tests\Testcontainers.Oracle11.Tests.csproj", "{0A0AC20D-226B-46F9-B267-0D00964A7601}"
@@ -411,6 +415,10 @@ Global
411415
{0DB0075D-42EC-4438-93F7-630CF5BCCAF0}.Debug|Any CPU.Build.0 = Debug|Any CPU
412416
{0DB0075D-42EC-4438-93F7-630CF5BCCAF0}.Release|Any CPU.ActiveCfg = Release|Any CPU
413417
{0DB0075D-42EC-4438-93F7-630CF5BCCAF0}.Release|Any CPU.Build.0 = Release|Any CPU
418+
{49051DBC-6B80-4412-8505-BC2764A877BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
419+
{49051DBC-6B80-4412-8505-BC2764A877BD}.Debug|Any CPU.Build.0 = Debug|Any CPU
420+
{49051DBC-6B80-4412-8505-BC2764A877BD}.Release|Any CPU.ActiveCfg = Release|Any CPU
421+
{49051DBC-6B80-4412-8505-BC2764A877BD}.Release|Any CPU.Build.0 = Release|Any CPU
414422
{596EAFC1-0496-495C-B382-D57415FA456A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
415423
{596EAFC1-0496-495C-B382-D57415FA456A}.Debug|Any CPU.Build.0 = Debug|Any CPU
416424
{596EAFC1-0496-495C-B382-D57415FA456A}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -639,6 +647,10 @@ Global
639647
{D3AD7D72-510C-43A4-A401-DB3C2594508E}.Debug|Any CPU.Build.0 = Debug|Any CPU
640648
{D3AD7D72-510C-43A4-A401-DB3C2594508E}.Release|Any CPU.ActiveCfg = Release|Any CPU
641649
{D3AD7D72-510C-43A4-A401-DB3C2594508E}.Release|Any CPU.Build.0 = Release|Any CPU
650+
{04A7AF65-2E02-4E20-8056-2AAC0705B0BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
651+
{04A7AF65-2E02-4E20-8056-2AAC0705B0BC}.Debug|Any CPU.Build.0 = Debug|Any CPU
652+
{04A7AF65-2E02-4E20-8056-2AAC0705B0BC}.Release|Any CPU.ActiveCfg = Release|Any CPU
653+
{04A7AF65-2E02-4E20-8056-2AAC0705B0BC}.Release|Any CPU.Build.0 = Release|Any CPU
642654
{4AC1088B-9965-4497-AC8E-570F1AD5631F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
643655
{4AC1088B-9965-4497-AC8E-570F1AD5631F}.Debug|Any CPU.Build.0 = Debug|Any CPU
644656
{4AC1088B-9965-4497-AC8E-570F1AD5631F}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -782,6 +794,7 @@ Global
782794
{BF37BEA1-0816-4326-B1E0-E82290F8FCE0} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
783795
{ADC2372B-6FE0-421D-8277-BB628E8EFC22} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
784796
{0DB0075D-42EC-4438-93F7-630CF5BCCAF0} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
797+
{49051DBC-6B80-4412-8505-BC2764A877BD} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
785798
{596EAFC1-0496-495C-B382-D57415FA456A} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
786799
{B2608563-8EE4-49AA-A9A0-B1614486AEEF} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
787800
{8AB91636-9055-4900-A72A-7CFFACDFDBF0} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
@@ -839,6 +852,7 @@ Global
839852
{87A3F137-6DC3-4CE5-91E6-01797D076086} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
840853
{D3F63405-C0FA-4F83-8B79-E30BFF5FF5BF} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
841854
{D3AD7D72-510C-43A4-A401-DB3C2594508E} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
855+
{04A7AF65-2E02-4E20-8056-2AAC0705B0BC} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
842856
{4AC1088B-9965-4497-AC8E-570F1AD5631F} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
843857
{0A0AC20D-226B-46F9-B267-0D00964A7601} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
844858
{E4C887A9-A44A-4641-BB9B-0664CC4C362F} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}

docs/modules/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ await moduleNameContainer.StartAsync();
5757
| MySQL | `mysql:8.0` | [NuGet](https://www.nuget.org/packages/Testcontainers.MySql) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.MySql) |
5858
| NATS | `nats:2.9` | [NuGet](https://www.nuget.org/packages/Testcontainers.Nats) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Nats) |
5959
| Neo4j | `neo4j:5.4` | [NuGet](https://www.nuget.org/packages/Testcontainers.Neo4j) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Neo4j) |
60+
| 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) |
6061
| 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) |
6162
| 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) |
6263
| PostgreSQL | `postgres:15.1` | [NuGet](https://www.nuget.org/packages/Testcontainers.PostgreSql) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.PostgreSql) |

docs/modules/opensearch.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# OpenSearch
2+
3+
[OpenSearch](https://opensearch.org/) is an open-source, enterprise-grade search and observability suite that brings order to unstructured data at scale.
4+
5+
Add the following dependency to your project file:
6+
7+
```shell title="NuGet"
8+
dotnet add package Testcontainers.OpenSearch
9+
```
10+
11+
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:
12+
13+
=== "Start an OpenSearch container"
14+
```csharp
15+
var openSearchContainer = new OpenSearchBuilder().Build();
16+
await openSearchContainer.StartAsync();
17+
```
18+
19+
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.
20+
21+
=== "Base test class"
22+
```csharp
23+
--8<-- "tests/Testcontainers.OpenSearch.Tests/OpenSearchContainerTest.cs:BaseClass"
24+
}
25+
```
26+
=== "Insecure no auth"
27+
```csharp
28+
--8<-- "tests/Testcontainers.OpenSearch.Tests/OpenSearchContainerTest.cs:InsecureNoAuth"
29+
```
30+
=== "SSL default credentials"
31+
```csharp
32+
--8<-- "tests/Testcontainers.OpenSearch.Tests/OpenSearchContainerTest.cs:SslBasicAuthDefaultCredentials"
33+
```
34+
=== "SSL custom credentials"
35+
```csharp
36+
--8<-- "tests/Testcontainers.OpenSearch.Tests/OpenSearchContainerTest.cs:SslBasicAuthCustomCredentials"
37+
```
38+
39+
How to check if the client has established a connection:
40+
41+
=== "Ping example"
42+
```csharp
43+
--8<-- "tests/Testcontainers.OpenSearch.Tests/OpenSearchContainerTest.cs:PingExample"
44+
```
45+
46+
Creating an index and alias:
47+
48+
=== "Create index and alias"
49+
```csharp
50+
--8<-- "tests/Testcontainers.OpenSearch.Tests/OpenSearchContainerTest.cs:CreateIndexAndAlias"
51+
```
52+
=== "Create index implementation"
53+
```csharp
54+
--8<-- "tests/Testcontainers.OpenSearch.Tests/OpenSearchContainerTest.cs:CreateIndexImplementation"
55+
```
56+
57+
Indexing and searching a document:
58+
59+
=== "Indexing document"
60+
```csharp
61+
--8<-- "tests/Testcontainers.OpenSearch.Tests/OpenSearchContainerTest.cs:IndexingDocument"
62+
```
63+
64+
The test example uses the following NuGet dependencies:
65+
66+
=== "Package References"
67+
```xml
68+
--8<-- "tests/Testcontainers.OpenSearch.Tests/Testcontainers.OpenSearch.Tests.csproj:PackageReferences"
69+
```
70+
71+
To execute the tests, use the command `dotnet test` from a terminal.
72+
73+
--8<-- "docs/modules/_call_out_test_projects.txt"

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ nav:
5959
- modules/mongodb.md
6060
- modules/mssql.md
6161
- modules/neo4j.md
62+
- modules/opensearch.md
6263
- modules/postgres.md
6364
- modules/qdrant.md
6465
- modules/rabbitmq.md
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
root = true
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
namespace Testcontainers.OpenSearch;
2+
3+
/// <inheritdoc cref="ContainerBuilder{TBuilderEntity, TContainerEntity, TConfigurationEntity}" />
4+
[PublicAPI]
5+
public sealed class OpenSearchBuilder : ContainerBuilder<OpenSearchBuilder, OpenSearchContainer, OpenSearchConfiguration>
6+
{
7+
public const string OpenSearchImage = "opensearchproject/opensearch:2.12.0";
8+
9+
public const ushort OpenSearchRestApiPort = 9200;
10+
11+
public const ushort OpenSearchTransportPort = 9300;
12+
13+
public const ushort OpenSearchPerformanceAnalyzerPort = 9600;
14+
15+
public const string DefaultUsername = "admin";
16+
17+
public const string DefaultPassword = "yourStrong(!)P@ssw0rd";
18+
19+
/// <summary>
20+
/// Initializes a new instance of the <see cref="OpenSearchBuilder" /> class.
21+
/// </summary>
22+
public OpenSearchBuilder()
23+
: this(new OpenSearchConfiguration())
24+
{
25+
DockerResourceConfiguration = Init().DockerResourceConfiguration;
26+
}
27+
28+
/// <summary>
29+
/// Initializes a new instance of the <see cref="OpenSearchBuilder" /> class.
30+
/// </summary>
31+
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
32+
private OpenSearchBuilder(OpenSearchConfiguration resourceConfiguration)
33+
: base(resourceConfiguration)
34+
{
35+
DockerResourceConfiguration = resourceConfiguration;
36+
}
37+
38+
/// <inheritdoc />
39+
protected override OpenSearchConfiguration DockerResourceConfiguration { get; }
40+
41+
/// <summary>
42+
/// Sets the password for the <c>admin</c> user.
43+
/// </summary>
44+
/// <remarks>
45+
/// The password must meet the following complexity requirements:
46+
/// <list type="bullet">
47+
/// <item><description>Minimum of 8 characters</description></item>
48+
/// <item><description>At least one uppercase letter</description></item>
49+
/// <item><description>At least one lowercase letter</description></item>
50+
/// <item><description>At least one digit</description></item>
51+
/// <item><description>At least one special character</description></item>
52+
/// </list>
53+
/// </remarks>
54+
/// <param name="password">The <c>admin</c> user password.</param>
55+
/// <returns>A configured instance of <see cref="OpenSearchBuilder" />.</returns>
56+
public OpenSearchBuilder WithPassword(string password)
57+
{
58+
return Merge(DockerResourceConfiguration, new OpenSearchConfiguration(password: password))
59+
.WithEnvironment("OPENSEARCH_INITIAL_ADMIN_PASSWORD", password);
60+
}
61+
62+
/// <summary>
63+
/// Enables or disables the built-in security plugin in OpenSearch.
64+
/// </summary>
65+
/// <remarks>
66+
/// When disabled, the <see cref="OpenSearchContainer.GetConnectionString" /> method
67+
/// will use the <c>http</c> protocol instead of <c>https</c>.
68+
/// </remarks>
69+
/// <param name="securityEnabled"><c>true</c> to enable the security plugin; <c>false</c> to disable it.</param>
70+
/// <returns>A configured instance of <see cref="OpenSearchBuilder" />.</returns>
71+
public OpenSearchBuilder WithSecurityEnabled(bool securityEnabled = true)
72+
{
73+
return Merge(DockerResourceConfiguration, new OpenSearchConfiguration(tlsEnabled: securityEnabled))
74+
.WithEnvironment("plugins.security.disabled", (!securityEnabled).ToString().ToLowerInvariant());
75+
}
76+
77+
/// <inheritdoc />
78+
public override OpenSearchContainer Build()
79+
{
80+
Validate();
81+
82+
OpenSearchBuilder openSearchBuilder;
83+
84+
Predicate<System.Version> predicate = v => v.Major == 1 || (v.Major == 2 && v.Minor < 12);
85+
86+
var image = DockerResourceConfiguration.Image;
87+
88+
// Images before version 2.12.0 use a hardcoded default password.
89+
var requiresHardcodedDefaultPassword = image.MatchVersion(predicate);
90+
if (requiresHardcodedDefaultPassword)
91+
{
92+
openSearchBuilder = WithPassword("admin");
93+
}
94+
else
95+
{
96+
openSearchBuilder = this;
97+
}
98+
99+
// 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.
100+
// If the user does not provide a custom waiting strategy, append the default OpenSearch waiting strategy.
101+
openSearchBuilder = DockerResourceConfiguration.WaitStrategies.Count() > 1 ? openSearchBuilder : openSearchBuilder.WithWaitStrategy(Wait.ForUnixContainer().AddCustomWaitStrategy(new WaitUntil(DockerResourceConfiguration)));
102+
return new OpenSearchContainer(openSearchBuilder.DockerResourceConfiguration);
103+
}
104+
105+
/// <inheritdoc />
106+
protected override OpenSearchBuilder Init()
107+
{
108+
return base.Init()
109+
.WithImage(OpenSearchImage)
110+
.WithPortBinding(OpenSearchRestApiPort, true)
111+
.WithPortBinding(OpenSearchTransportPort, true)
112+
.WithPortBinding(OpenSearchPerformanceAnalyzerPort, true)
113+
.WithEnvironment("discovery.type", "single-node")
114+
.WithSecurityEnabled()
115+
.WithUsername(DefaultUsername)
116+
.WithPassword(DefaultPassword);
117+
}
118+
119+
/// <inheritdoc />
120+
protected override void Validate()
121+
{
122+
base.Validate();
123+
124+
_ = Guard.Argument(DockerResourceConfiguration.Password, nameof(DockerResourceConfiguration.Password))
125+
.NotNull()
126+
.NotEmpty();
127+
}
128+
129+
/// <inheritdoc />
130+
protected override OpenSearchBuilder Clone(IResourceConfiguration<CreateContainerParameters> resourceConfiguration)
131+
{
132+
return Merge(DockerResourceConfiguration, new OpenSearchConfiguration(resourceConfiguration));
133+
}
134+
135+
/// <inheritdoc />
136+
protected override OpenSearchBuilder Clone(IContainerConfiguration resourceConfiguration)
137+
{
138+
return Merge(DockerResourceConfiguration, new OpenSearchConfiguration(resourceConfiguration));
139+
}
140+
141+
/// <inheritdoc />
142+
protected override OpenSearchBuilder Merge(OpenSearchConfiguration oldValue, OpenSearchConfiguration newValue)
143+
{
144+
return new OpenSearchBuilder(new OpenSearchConfiguration(oldValue, newValue));
145+
}
146+
147+
/// <summary>
148+
/// Sets the OpenSearch username.
149+
/// </summary>
150+
/// <remarks>
151+
/// The Docker image does not allow to configure the username.
152+
/// </remarks>
153+
/// <param name="username">The OpenSearch username.</param>
154+
/// <returns>A configured instance of <see cref="OpenSearchBuilder" />.</returns>
155+
private OpenSearchBuilder WithUsername(string username)
156+
{
157+
return Merge(DockerResourceConfiguration, new OpenSearchConfiguration(username: username));
158+
}
159+
160+
/// <inheritdoc cref="IWaitUntil" />
161+
private sealed class WaitUntil : IWaitUntil
162+
{
163+
private readonly bool _tlsEnabled;
164+
165+
private readonly string _username;
166+
167+
private readonly string _password;
168+
169+
/// <summary>
170+
/// Initializes a new instance of the <see cref="WaitUntil" /> class.
171+
/// </summary>
172+
/// <param name="configuration">The container configuration.</param>
173+
public WaitUntil(OpenSearchConfiguration configuration)
174+
{
175+
_tlsEnabled = configuration.TlsEnabled.GetValueOrDefault();
176+
_username = configuration.Username;
177+
_password = configuration.Password;
178+
}
179+
180+
/// <inheritdoc />
181+
public async Task<bool> UntilAsync(IContainer container)
182+
{
183+
using var httpMessageHandler = new HttpClientHandler();
184+
httpMessageHandler.ServerCertificateCustomValidationCallback = (_, _, _, _) => true;
185+
186+
var httpWaitStrategy = new HttpWaitStrategy()
187+
.UsingHttpMessageHandler(httpMessageHandler)
188+
.UsingTls(_tlsEnabled)
189+
.WithBasicAuthentication(_username, _password)
190+
.ForPort(OpenSearchRestApiPort);
191+
192+
return await httpWaitStrategy.UntilAsync(container)
193+
.ConfigureAwait(false);
194+
}
195+
}
196+
}

0 commit comments

Comments
 (0)