diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml
index a71afadfe..162c738d9 100644
--- a/.github/workflows/cicd.yml
+++ b/.github/workflows/cicd.yml
@@ -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" },
diff --git a/Directory.Packages.props b/Directory.Packages.props
index be2ddadfd..2ea3a74be 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -79,6 +79,7 @@
+
diff --git a/Testcontainers.sln b/Testcontainers.sln
index 6227bcc1d..e09e2b093 100644
--- a/Testcontainers.sln
+++ b/Testcontainers.sln
@@ -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}"
@@ -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}"
@@ -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
@@ -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
@@ -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}
@@ -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}
diff --git a/docs/modules/index.md b/docs/modules/index.md
index 8f1962beb..254a2d9fe 100644
--- a/docs/modules/index.md
+++ b/docs/modules/index.md
@@ -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) |
diff --git a/docs/modules/opensearch.md b/docs/modules/opensearch.md
new file mode 100644
index 000000000..df050268f
--- /dev/null
+++ b/docs/modules/opensearch.md
@@ -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"
\ No newline at end of file
diff --git a/mkdocs.yml b/mkdocs.yml
index f5510ebf3..af21de188 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -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
diff --git a/src/Testcontainers.OpenSearch/.editorconfig b/src/Testcontainers.OpenSearch/.editorconfig
new file mode 100644
index 000000000..6f066619d
--- /dev/null
+++ b/src/Testcontainers.OpenSearch/.editorconfig
@@ -0,0 +1 @@
+root = true
\ No newline at end of file
diff --git a/src/Testcontainers.OpenSearch/OpenSearchBuilder.cs b/src/Testcontainers.OpenSearch/OpenSearchBuilder.cs
new file mode 100644
index 000000000..50f1e4e5a
--- /dev/null
+++ b/src/Testcontainers.OpenSearch/OpenSearchBuilder.cs
@@ -0,0 +1,196 @@
+namespace Testcontainers.OpenSearch;
+
+///
+[PublicAPI]
+public sealed class OpenSearchBuilder : ContainerBuilder
+{
+ 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";
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public OpenSearchBuilder()
+ : this(new OpenSearchConfiguration())
+ {
+ DockerResourceConfiguration = Init().DockerResourceConfiguration;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Docker resource configuration.
+ private OpenSearchBuilder(OpenSearchConfiguration resourceConfiguration)
+ : base(resourceConfiguration)
+ {
+ DockerResourceConfiguration = resourceConfiguration;
+ }
+
+ ///
+ protected override OpenSearchConfiguration DockerResourceConfiguration { get; }
+
+ ///
+ /// Sets the password for the admin user.
+ ///
+ ///
+ /// The password must meet the following complexity requirements:
+ ///
+ /// - Minimum of 8 characters
+ /// - At least one uppercase letter
+ /// - At least one lowercase letter
+ /// - At least one digit
+ /// - At least one special character
+ ///
+ ///
+ /// The admin user password.
+ /// A configured instance of .
+ public OpenSearchBuilder WithPassword(string password)
+ {
+ return Merge(DockerResourceConfiguration, new OpenSearchConfiguration(password: password))
+ .WithEnvironment("OPENSEARCH_INITIAL_ADMIN_PASSWORD", password);
+ }
+
+ ///
+ /// Enables or disables the built-in security plugin in OpenSearch.
+ ///
+ ///
+ /// When disabled, the method
+ /// will use the http protocol instead of https.
+ ///
+ /// true to enable the security plugin; false to disable it.
+ /// A configured instance of .
+ public OpenSearchBuilder WithSecurityEnabled(bool securityEnabled = true)
+ {
+ return Merge(DockerResourceConfiguration, new OpenSearchConfiguration(tlsEnabled: securityEnabled))
+ .WithEnvironment("plugins.security.disabled", (!securityEnabled).ToString().ToLowerInvariant());
+ }
+
+ ///
+ public override OpenSearchContainer Build()
+ {
+ Validate();
+
+ OpenSearchBuilder openSearchBuilder;
+
+ Predicate 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);
+ }
+
+ ///
+ 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);
+ }
+
+ ///
+ protected override void Validate()
+ {
+ base.Validate();
+
+ _ = Guard.Argument(DockerResourceConfiguration.Password, nameof(DockerResourceConfiguration.Password))
+ .NotNull()
+ .NotEmpty();
+ }
+
+ ///
+ protected override OpenSearchBuilder Clone(IResourceConfiguration resourceConfiguration)
+ {
+ return Merge(DockerResourceConfiguration, new OpenSearchConfiguration(resourceConfiguration));
+ }
+
+ ///
+ protected override OpenSearchBuilder Clone(IContainerConfiguration resourceConfiguration)
+ {
+ return Merge(DockerResourceConfiguration, new OpenSearchConfiguration(resourceConfiguration));
+ }
+
+ ///
+ protected override OpenSearchBuilder Merge(OpenSearchConfiguration oldValue, OpenSearchConfiguration newValue)
+ {
+ return new OpenSearchBuilder(new OpenSearchConfiguration(oldValue, newValue));
+ }
+
+ ///
+ /// Sets the OpenSearch username.
+ ///
+ ///
+ /// The Docker image does not allow to configure the username.
+ ///
+ /// The OpenSearch username.
+ /// A configured instance of .
+ private OpenSearchBuilder WithUsername(string username)
+ {
+ return Merge(DockerResourceConfiguration, new OpenSearchConfiguration(username: username));
+ }
+
+ ///
+ private sealed class WaitUntil : IWaitUntil
+ {
+ private readonly bool _tlsEnabled;
+
+ private readonly string _username;
+
+ private readonly string _password;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The container configuration.
+ public WaitUntil(OpenSearchConfiguration configuration)
+ {
+ _tlsEnabled = configuration.TlsEnabled.GetValueOrDefault();
+ _username = configuration.Username;
+ _password = configuration.Password;
+ }
+
+ ///
+ public async Task 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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Testcontainers.OpenSearch/OpenSearchConfiguration.cs b/src/Testcontainers.OpenSearch/OpenSearchConfiguration.cs
new file mode 100644
index 000000000..613b75cb1
--- /dev/null
+++ b/src/Testcontainers.OpenSearch/OpenSearchConfiguration.cs
@@ -0,0 +1,77 @@
+namespace Testcontainers.OpenSearch;
+
+///
+[PublicAPI]
+public sealed class OpenSearchConfiguration : ContainerConfiguration
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// A boolean value indicating whether TLS is enabled.
+ /// The OpenSearch username.
+ /// The OpenSearch password.
+ public OpenSearchConfiguration(
+ bool? tlsEnabled = null,
+ string username = null,
+ string password = null)
+ {
+ TlsEnabled = tlsEnabled;
+ Username = username;
+ Password = password;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Docker resource configuration.
+ public OpenSearchConfiguration(IResourceConfiguration resourceConfiguration)
+ : base(resourceConfiguration)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Docker resource configuration.
+ public OpenSearchConfiguration(IContainerConfiguration resourceConfiguration)
+ : base(resourceConfiguration)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Docker resource configuration.
+ public OpenSearchConfiguration(OpenSearchConfiguration resourceConfiguration)
+ : this(new OpenSearchConfiguration(), resourceConfiguration)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The old Docker resource configuration.
+ /// The new Docker resource configuration.
+ public OpenSearchConfiguration(OpenSearchConfiguration oldValue, OpenSearchConfiguration newValue)
+ : base(oldValue, newValue)
+ {
+ TlsEnabled = BuildConfiguration.Combine(oldValue.TlsEnabled, newValue.TlsEnabled);
+ Username = BuildConfiguration.Combine(oldValue.Username, newValue.Username);
+ Password = BuildConfiguration.Combine(oldValue.Password, newValue.Password);
+ }
+
+ ///
+ /// Gets a value indicating whether TLS is enabled or not.
+ ///
+ public bool? TlsEnabled { get; }
+
+ ///
+ /// Gets the OpenSearch username.
+ ///
+ public string Username { get; }
+
+ ///
+ /// Gets the OpenSearch password.
+ ///
+ public string Password { get; }
+}
\ No newline at end of file
diff --git a/src/Testcontainers.OpenSearch/OpenSearchContainer.cs b/src/Testcontainers.OpenSearch/OpenSearchContainer.cs
new file mode 100644
index 000000000..49213a424
--- /dev/null
+++ b/src/Testcontainers.OpenSearch/OpenSearchContainer.cs
@@ -0,0 +1,37 @@
+namespace Testcontainers.OpenSearch;
+
+///
+[PublicAPI]
+public sealed class OpenSearchContainer : DockerContainer
+{
+ private readonly OpenSearchConfiguration _configuration;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The container configuration.
+ public OpenSearchContainer(OpenSearchConfiguration configuration)
+ : base(configuration)
+ {
+ _configuration = configuration;
+ }
+
+ ///
+ /// Gets the OpenSearch credentials.
+ ///
+ /// The OpenSearch credentials.
+ public NetworkCredential GetCredentials()
+ {
+ return new NetworkCredential(_configuration.Username, _configuration.Password);
+ }
+
+ ///
+ /// Gets the OpenSearch connection string.
+ ///
+ /// The OpenSearch connection string.
+ public string GetConnectionString()
+ {
+ var schema = _configuration.TlsEnabled.GetValueOrDefault() ? Uri.UriSchemeHttps : Uri.UriSchemeHttp;
+ return new UriBuilder(schema, Hostname, GetMappedPublicPort(OpenSearchBuilder.OpenSearchRestApiPort)).ToString();
+ }
+}
\ No newline at end of file
diff --git a/src/Testcontainers.OpenSearch/Testcontainers.OpenSearch.csproj b/src/Testcontainers.OpenSearch/Testcontainers.OpenSearch.csproj
new file mode 100644
index 000000000..9a25b9c4d
--- /dev/null
+++ b/src/Testcontainers.OpenSearch/Testcontainers.OpenSearch.csproj
@@ -0,0 +1,12 @@
+
+
+ net8.0;net9.0;netstandard2.0;netstandard2.1
+ latest
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Testcontainers.OpenSearch/Usings.cs b/src/Testcontainers.OpenSearch/Usings.cs
new file mode 100644
index 000000000..ca1247a6e
--- /dev/null
+++ b/src/Testcontainers.OpenSearch/Usings.cs
@@ -0,0 +1,11 @@
+global using System;
+global using System.Linq;
+global using System.Net;
+global using System.Net.Http;
+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;
+global using JetBrains.Annotations;
\ No newline at end of file
diff --git a/tests/Testcontainers.OpenSearch.Tests/.editorconfig b/tests/Testcontainers.OpenSearch.Tests/.editorconfig
new file mode 100644
index 000000000..6f066619d
--- /dev/null
+++ b/tests/Testcontainers.OpenSearch.Tests/.editorconfig
@@ -0,0 +1 @@
+root = true
\ No newline at end of file
diff --git a/tests/Testcontainers.OpenSearch.Tests/OpenSearchBuilderTest.cs b/tests/Testcontainers.OpenSearch.Tests/OpenSearchBuilderTest.cs
new file mode 100644
index 000000000..9b3d53d78
--- /dev/null
+++ b/tests/Testcontainers.OpenSearch.Tests/OpenSearchBuilderTest.cs
@@ -0,0 +1,22 @@
+namespace Testcontainers.OpenSearch;
+
+public sealed class OpenSearchBuilderTest
+{
+ [Theory]
+ [InlineData("opensearchproject/opensearch:1.0.0")]
+ [InlineData("opensearchproject/opensearch:1.1.0")]
+ [InlineData("opensearchproject/opensearch:2.11.0")]
+ [InlineData("opensearchproject/opensearch:2.11.1")]
+ [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
+ public void ShouldUseHardcodedDefaultPassword(string image)
+ {
+ // Given
+ var opensearchContainer = new OpenSearchBuilder().WithImage(image).Build();
+
+ // When
+ var credentials = opensearchContainer.GetCredentials();
+
+ // Then
+ Assert.Equal("admin", credentials.Password);
+ }
+}
\ No newline at end of file
diff --git a/tests/Testcontainers.OpenSearch.Tests/OpenSearchContainerTest.cs b/tests/Testcontainers.OpenSearch.Tests/OpenSearchContainerTest.cs
new file mode 100644
index 000000000..d7c41b432
--- /dev/null
+++ b/tests/Testcontainers.OpenSearch.Tests/OpenSearchContainerTest.cs
@@ -0,0 +1,185 @@
+namespace Testcontainers.OpenSearch;
+
+//
+public abstract class OpenSearchContainerTest : IAsyncLifetime
+{
+ private const string IndexName = "testcontainers";
+
+ private readonly OpenSearchContainer _openSearchContainer;
+
+ private OpenSearchContainerTest(OpenSearchContainer openSearchContainer)
+ {
+ _openSearchContainer = openSearchContainer;
+ }
+
+ public async ValueTask InitializeAsync()
+ {
+ await _openSearchContainer.StartAsync()
+ .ConfigureAwait(false);
+ }
+
+ public async ValueTask DisposeAsync()
+ {
+ await DisposeAsyncCore()
+ .ConfigureAwait(false);
+
+ GC.SuppressFinalize(this);
+ }
+ //
+
+ //
+ [Fact]
+ [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
+ public async Task PingReturnsValidResponse()
+ {
+ // Given
+ var client = CreateClient();
+
+ // When
+ var response = await client.PingAsync(ct: TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+
+ // Then
+ Assert.True(response.IsValid);
+ }
+ //
+
+ //
+ [Fact]
+ [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
+ public async Task ShouldCreateIndexAndAlias()
+ {
+ // Given
+ var client = CreateClient();
+
+ var index = Indices.Index(IndexName);
+
+ var alias = new Name(IndexName + "-alias");
+
+ // When
+ var createIndexResponse = await CreateIndexAsync(client)
+ .ConfigureAwait(true);
+
+ var createAliasResponse = await client.Indices.PutAliasAsync(index, alias, ct: TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+
+ // Then
+ Assert.True(createIndexResponse.IsValid);
+ Assert.True(createAliasResponse.IsValid);
+ }
+ //
+
+ //
+ [Fact]
+ [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
+ public async Task ShouldIndexAndSearchForDocument()
+ {
+ // Given
+ var client = CreateClient();
+
+ var document = new Document(Guid.NewGuid().ToString(), Guid.NewGuid().ToString());
+
+ // When
+ Func, IIndexRequest> indexRequest = i =>
+ i.Index(IndexName).Id(document.Id).Refresh(Refresh.True);
+
+ Func, ISearchRequest> searchRequest = s =>
+ s.Index(IndexName).Query(q => q.Match(m => m.Field("title").Query(document.Title)));
+
+ var createIndexResponse = await CreateIndexAsync(client)
+ .ConfigureAwait(true);
+
+ var indexResponse = await client.IndexAsync(document, indexRequest, TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+
+ var searchResponse = await client.SearchAsync(searchRequest, TestContext.Current.CancellationToken)
+ .ConfigureAwait(true);
+
+ // Then
+ Assert.True(createIndexResponse.IsValid);
+
+ Assert.True(indexResponse.IsValid);
+ Assert.Equal(document.Id, indexResponse.Id);
+
+ Assert.True(searchResponse.IsValid);
+ Assert.Single(searchResponse.Documents, item => document.Id.Equals(item.Id));
+ }
+ //
+
+ //
+ private static Task CreateIndexAsync(OpenSearchClient client)
+ {
+ Func createIndexRequest = c =>
+ c.Settings(s => s.NumberOfReplicas(0).NumberOfShards(1)).Map(m => m.AutoMap());
+
+ return client.Indices.CreateAsync(Indices.Index(IndexName), createIndexRequest, TestContext.Current.CancellationToken);
+ }
+ //
+
+ protected virtual ValueTask DisposeAsyncCore()
+ {
+ return _openSearchContainer.DisposeAsync();
+ }
+
+ protected virtual OpenSearchClient CreateClient()
+ {
+ var credentials = _openSearchContainer.GetCredentials();
+
+ var connectionString = new Uri(_openSearchContainer.GetConnectionString());
+ Assert.Equal(Uri.UriSchemeHttps, connectionString.Scheme);
+
+ var connectionSettings = new ConnectionSettings(connectionString)
+ .BasicAuthentication(credentials.UserName, credentials.Password)
+ .ServerCertificateValidationCallback((_, _, _, _) => true);
+
+ return new OpenSearchClient(connectionSettings);
+ }
+
+ //
+ [UsedImplicitly]
+ public sealed class InsecureNoAuthConfiguration : OpenSearchContainerTest
+ {
+ public InsecureNoAuthConfiguration()
+ : base(new OpenSearchBuilder()
+ .WithSecurityEnabled(false)
+ .Build())
+ {
+ }
+
+ protected override OpenSearchClient CreateClient()
+ {
+ var connectionString = new Uri(_openSearchContainer.GetConnectionString());
+ Assert.Equal(Uri.UriSchemeHttp, connectionString.Scheme);
+ return new OpenSearchClient(connectionString);
+ }
+ }
+ //
+
+ //
+ [UsedImplicitly]
+ public sealed class SslBasicAuthDefaultCredentialsConfiguration : OpenSearchContainerTest
+ {
+ public SslBasicAuthDefaultCredentialsConfiguration()
+ : base(new OpenSearchBuilder()
+ .Build())
+ {
+ }
+ }
+ //
+
+ //
+ [UsedImplicitly]
+ public sealed class SslBasicAuthCustomCredentialsConfiguration : OpenSearchContainerTest
+ {
+ public SslBasicAuthCustomCredentialsConfiguration()
+ : base(new OpenSearchBuilder()
+ .WithPassword(new string(OpenSearchBuilder.DefaultPassword.Reverse().ToArray()))
+ .Build())
+ {
+ }
+ }
+ //
+
+ [UsedImplicitly]
+ private record Document(string Id, string Title);
+}
\ No newline at end of file
diff --git a/tests/Testcontainers.OpenSearch.Tests/Testcontainers.OpenSearch.Tests.csproj b/tests/Testcontainers.OpenSearch.Tests/Testcontainers.OpenSearch.Tests.csproj
new file mode 100644
index 000000000..6f7d93a01
--- /dev/null
+++ b/tests/Testcontainers.OpenSearch.Tests/Testcontainers.OpenSearch.Tests.csproj
@@ -0,0 +1,21 @@
+
+
+ net9.0
+ false
+ false
+ Exe
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/Testcontainers.OpenSearch.Tests/Usings.cs b/tests/Testcontainers.OpenSearch.Tests/Usings.cs
new file mode 100644
index 000000000..e99d10a3d
--- /dev/null
+++ b/tests/Testcontainers.OpenSearch.Tests/Usings.cs
@@ -0,0 +1,8 @@
+global using System;
+global using System.Linq;
+global using System.Threading.Tasks;
+global using DotNet.Testcontainers.Commons;
+global using JetBrains.Annotations;
+global using OpenSearch.Client;
+global using OpenSearch.Net;
+global using Xunit;
\ No newline at end of file