Skip to content

Commit 2c691d2

Browse files
feat: Add Qdrant module (#1149)
Co-authored-by: Andre Hofmeister <[email protected]>
1 parent 615f5cc commit 2c691d2

18 files changed

+607
-0
lines changed

.github/workflows/cicd.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ jobs:
8181
{ name: "Testcontainers.PostgreSql", runs-on: "ubuntu-22.04" },
8282
{ name: "Testcontainers.PubSub", runs-on: "ubuntu-22.04" },
8383
{ name: "Testcontainers.Pulsar", runs-on: "ubuntu-22.04" },
84+
{ name: "Testcontainers.Qdrant", runs-on: "ubuntu-22.04" },
8485
{ name: "Testcontainers.RabbitMq", runs-on: "ubuntu-22.04" },
8586
{ name: "Testcontainers.RavenDb", runs-on: "ubuntu-22.04" },
8687
{ name: "Testcontainers.Redis", runs-on: "ubuntu-22.04" },

Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
<PackageVersion Include="Neo4j.Driver" Version="5.5.0"/>
7171
<PackageVersion Include="Npgsql" Version="6.0.11"/>
7272
<PackageVersion Include="Oracle.ManagedDataAccess.Core" Version="23.7.0"/>
73+
<PackageVersion Include="Qdrant.Client" Version="1.13.0"/>
7374
<PackageVersion Include="RabbitMQ.Client" Version="6.4.0"/>
7475
<PackageVersion Include="RavenDB.Client" Version="5.4.100"/>
7576
<PackageVersion Include="Selenium.WebDriver" Version="4.8.1"/>

Testcontainers.sln

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.PubSub", "sr
9393
EndProject
9494
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Pulsar", "src\Testcontainers.Pulsar\Testcontainers.Pulsar.csproj", "{27D46863-65B9-4934-B3C8-2383B217A477}"
9595
EndProject
96+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Qdrant", "src\Testcontainers.Qdrant\Testcontainers.Qdrant.csproj", "{7C98973D-53D7-49F9-BDFE-E3268F402584}"
97+
EndProject
9698
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.RabbitMq", "src\Testcontainers.RabbitMq\Testcontainers.RabbitMq.csproj", "{A6D480BC-FDE8-4B92-A2A6-FF16BEE486AE}"
9799
EndProject
98100
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.RavenDb", "src\Testcontainers.RavenDb\Testcontainers.RavenDb.csproj", "{F6394475-D6F1-46E2-81BF-4BA78A40B878}"
@@ -211,6 +213,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.PubSub.Tests
211213
EndProject
212214
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Pulsar.Tests", "tests\Testcontainers.Pulsar.Tests\Testcontainers.Pulsar.Tests.csproj", "{D05FCB31-793E-43E0-BD6C-077013AE9113}"
213215
EndProject
216+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Qdrant.Tests", "tests\Testcontainers.Qdrant.Tests\Testcontainers.Qdrant.Tests.csproj", "{9DCE3E7F-B341-4AD0-BAAA-C3B91EB91B0D}"
217+
EndProject
214218
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.RabbitMq.Tests", "tests\Testcontainers.RabbitMq.Tests\Testcontainers.RabbitMq.Tests.csproj", "{19564567-1736-4626-B406-17E4E02F18B2}"
215219
EndProject
216220
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.RavenDb.Tests", "tests\Testcontainers.RavenDb.Tests\Testcontainers.RavenDb.Tests.csproj", "{D53726B6-5447-47E6-B881-A44EFF6E5534}"
@@ -402,6 +406,10 @@ Global
402406
{27D46863-65B9-4934-B3C8-2383B217A477}.Debug|Any CPU.Build.0 = Debug|Any CPU
403407
{27D46863-65B9-4934-B3C8-2383B217A477}.Release|Any CPU.ActiveCfg = Release|Any CPU
404408
{27D46863-65B9-4934-B3C8-2383B217A477}.Release|Any CPU.Build.0 = Release|Any CPU
409+
{7C98973D-53D7-49F9-BDFE-E3268F402584}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
410+
{7C98973D-53D7-49F9-BDFE-E3268F402584}.Debug|Any CPU.Build.0 = Debug|Any CPU
411+
{7C98973D-53D7-49F9-BDFE-E3268F402584}.Release|Any CPU.ActiveCfg = Release|Any CPU
412+
{7C98973D-53D7-49F9-BDFE-E3268F402584}.Release|Any CPU.Build.0 = Release|Any CPU
405413
{A6D480BC-FDE8-4B92-A2A6-FF16BEE486AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
406414
{A6D480BC-FDE8-4B92-A2A6-FF16BEE486AE}.Debug|Any CPU.Build.0 = Debug|Any CPU
407415
{A6D480BC-FDE8-4B92-A2A6-FF16BEE486AE}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -638,6 +646,10 @@ Global
638646
{D05FCB31-793E-43E0-BD6C-077013AE9113}.Debug|Any CPU.Build.0 = Debug|Any CPU
639647
{D05FCB31-793E-43E0-BD6C-077013AE9113}.Release|Any CPU.ActiveCfg = Release|Any CPU
640648
{D05FCB31-793E-43E0-BD6C-077013AE9113}.Release|Any CPU.Build.0 = Release|Any CPU
649+
{9DCE3E7F-B341-4AD0-BAAA-C3B91EB91B0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
650+
{9DCE3E7F-B341-4AD0-BAAA-C3B91EB91B0D}.Debug|Any CPU.Build.0 = Debug|Any CPU
651+
{9DCE3E7F-B341-4AD0-BAAA-C3B91EB91B0D}.Release|Any CPU.ActiveCfg = Release|Any CPU
652+
{9DCE3E7F-B341-4AD0-BAAA-C3B91EB91B0D}.Release|Any CPU.Build.0 = Release|Any CPU
641653
{19564567-1736-4626-B406-17E4E02F18B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
642654
{19564567-1736-4626-B406-17E4E02F18B2}.Debug|Any CPU.Build.0 = Debug|Any CPU
643655
{19564567-1736-4626-B406-17E4E02F18B2}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -724,6 +736,7 @@ Global
724736
{8AB91636-9055-4900-A72A-7CFFACDFDBF0} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
725737
{E6642255-667D-476B-B584-089AA5E6C0B1} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
726738
{27D46863-65B9-4934-B3C8-2383B217A477} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
739+
{7C98973D-53D7-49F9-BDFE-E3268F402584} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
727740
{A6D480BC-FDE8-4B92-A2A6-FF16BEE486AE} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
728741
{F6394475-D6F1-46E2-81BF-4BA78A40B878} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
729742
{BFDA179A-40EB-4CEB-B8E9-0DF32C65E2C5} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
@@ -783,6 +796,7 @@ Global
783796
{56D0DCA5-567F-4B3B-8B79-CB108F8EB8A6} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
784797
{0F86BCE8-62E1-4BFC-AA84-63C7514C90AC} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
785798
{D05FCB31-793E-43E0-BD6C-077013AE9113} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
799+
{9DCE3E7F-B341-4AD0-BAAA-C3B91EB91B0D} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
786800
{19564567-1736-4626-B406-17E4E02F18B2} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
787801
{D53726B6-5447-47E6-B881-A44EFF6E5534} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
788802
{31EE94A0-E721-4073-B6F1-DD912D004DEF} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}

docs/modules/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ await moduleNameContainer.StartAsync();
6161
| PostgreSQL | `postgres:15.1` | [NuGet](https://www.nuget.org/packages/Testcontainers.PostgreSql) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.PostgreSql) |
6262
| 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) |
6363
| Pulsar | `apachepulsar/pulsar:3.0.6` | [NuGet](https://www.nuget.org/packages/Testcontainers.Pulsar) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Pulsar) |
64+
| Qdrant | `qdrant/qdrant:v1.13.4` | [NuGet](https://www.nuget.org/packages/Testcontainers.Qdrant) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Qdrant) |
6465
| RabbitMQ | `rabbitmq:3.11` | [NuGet](https://www.nuget.org/packages/Testcontainers.RabbitMq) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.RabbitMq) |
6566
| 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) |
6667
| Redis | `redis:7.0` | [NuGet](https://www.nuget.org/packages/Testcontainers.Redis) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Redis) |

docs/modules/qdrant.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Qdrant
2+
3+
[Qdrant](https://qdrant.tech/) is an open source vector database designed for scalable and efficient similarity search and nearest neighbor retrieval. It provides both RESTful and gRPC APIs, making it easy to integrate with various applications, including search, recommendation, AI, and machine learning systems.
4+
5+
Add the following dependency to your project file:
6+
7+
```shell title="NuGet"
8+
dotnet add package Testcontainers.Qdrant
9+
```
10+
11+
You can start an Qdrant container instance from any .NET application. 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.
12+
13+
=== "Usage Example"
14+
```csharp
15+
--8<-- "tests/Testcontainers.Qdrant.Tests/QdrantDefaultContainerTest.cs:UseQdrantContainer"
16+
```
17+
18+
The test example uses the following NuGet dependencies:
19+
20+
=== "Package References"
21+
```xml
22+
--8<-- "tests/Testcontainers.Qdrant.Tests/Testcontainers.Qdrant.Tests.csproj:PackageReferences"
23+
```
24+
25+
To execute the tests, use the command `dotnet test` from a terminal.
26+
27+
--8<-- "docs/modules/_call_out_test_projects.txt"
28+
29+
## Configure API key
30+
31+
To set and configure an API key, use the following container builder method:
32+
33+
=== "Configure the API key"
34+
```csharp
35+
--8<-- "tests/Testcontainers.Qdrant.Tests/QdrantSecureContainerTest.cs:ConfigureQdrantContainerApiKey"
36+
```
37+
38+
Make sure the underlying Qdrant HTTP or gRPC client adds the API key to the HTTP header or gRPC metadata:
39+
40+
=== "Configure the Qdrant client"
41+
```csharp
42+
--8<-- "tests/Testcontainers.Qdrant.Tests/QdrantSecureContainerTest.cs:ConfigureQdrantClientApiKey"
43+
```
44+
45+
## Configure TLS
46+
47+
The following example generates a self-signed certificate and configures the module to use TLS with the certificate and private key:
48+
49+
!!! note
50+
51+
Please ensure that both the certificate and private key are provided in PEM format.
52+
53+
=== "Configure the TLS certificate"
54+
```csharp
55+
--8<-- "tests/Testcontainers.Qdrant.Tests/QdrantSecureContainerTest.cs:ConfigureQdrantContainerCertificate"
56+
```
57+
58+
The Qdrant client is configured to validate the TLS certificate using its thumbprint:
59+
60+
=== "Configure the Qdrant client"
61+
```csharp
62+
--8<-- "tests/Testcontainers.Qdrant.Tests/QdrantSecureContainerTest.cs:ConfigureQdrantClientCertificate-1"
63+
64+
--8<-- "tests/Testcontainers.Qdrant.Tests/QdrantSecureContainerTest.cs:ConfigureQdrantClientCertificate-2"
65+
```
66+
67+
## A Note To Developers
68+
69+
The module creates a container that listens to requests over **HTTP**. The official Qdrant client uses the gRPC APIs to communicate with Qdrant. **.NET Core** and **.NET** support the above example with no additional configuration. However, **.NET Framework** has limited supported for gRPC over HTTP/2, but it can be enabled by:
70+
71+
1. Configuring the module to use TLS.
72+
1. Configuring server certificate validation.
73+
1. Reference [`System.Net.Http.WinHttpHandler`](https://www.nuget.org/packages/System.Net.Http.WinHttpHandler) version `6.0.1` or later, and configure `WinHttpHandler` as the handler for `GrpcChannelOptions` in the Qdrant client.
74+
75+
Refer to the official [Qdrant .NET SDK](https://github.com/qdrant/qdrant-dotnet) for more information.

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ nav:
5757
- modules/mssql.md
5858
- modules/neo4j.md
5959
- modules/postgres.md
60+
- modules/qdrant.md
6061
- modules/rabbitmq.md
6162
- contributing.md
6263
- contributing_docs.md
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
root = true
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
namespace Testcontainers.Qdrant;
2+
3+
/// <inheritdoc cref="ContainerBuilder{TBuilderEntity, TContainerEntity, TConfigurationEntity}" />
4+
[PublicAPI]
5+
public sealed class QdrantBuilder : ContainerBuilder<QdrantBuilder, QdrantContainer, QdrantConfiguration>
6+
{
7+
public const string QdrantImage = "qdrant/qdrant:v1.13.4";
8+
9+
public const ushort QdrantHttpPort = 6333;
10+
11+
public const ushort QdrantGrpcPort = 6334;
12+
13+
public const string CertificateFilePath = "/qdrant/tls/cert.pem";
14+
15+
public const string CertificateKeyFilePath = "/qdrant/tls/key.pem";
16+
17+
/// <summary>
18+
/// Initializes a new instance of the <see cref="QdrantBuilder" /> class.
19+
/// </summary>
20+
public QdrantBuilder()
21+
: this(new QdrantConfiguration())
22+
{
23+
DockerResourceConfiguration = Init().DockerResourceConfiguration;
24+
}
25+
26+
/// <summary>
27+
/// Initializes a new instance of the <see cref="QdrantBuilder" /> class.
28+
/// </summary>
29+
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
30+
private QdrantBuilder(QdrantConfiguration resourceConfiguration)
31+
: base(resourceConfiguration)
32+
{
33+
DockerResourceConfiguration = resourceConfiguration;
34+
}
35+
36+
/// <inheritdoc />
37+
protected override QdrantConfiguration DockerResourceConfiguration { get; }
38+
39+
/// <summary>
40+
/// Sets the API key to secure the instance.
41+
/// </summary>
42+
/// <param name="apiKey">The API key.</param>
43+
/// <returns>A configured instance of <see cref="QdrantBuilder" />.</returns>
44+
public QdrantBuilder WithApiKey(string apiKey)
45+
{
46+
return Merge(DockerResourceConfiguration, new QdrantConfiguration(apiKey: apiKey))
47+
.WithEnvironment("QDRANT__SERVICE__API_KEY", apiKey);
48+
}
49+
50+
/// <summary>
51+
/// Sets the public certificate and private key to enable TLS.
52+
/// </summary>
53+
/// <param name="certificate">The public certificate in PEM format.</param>
54+
/// <param name="certificateKey">The private key associated with the certificate in PEM format.</param>
55+
/// <returns>A configured instance of <see cref="QdrantBuilder" />.</returns>
56+
public QdrantBuilder WithCertificate(string certificate, string certificateKey)
57+
{
58+
return Merge(DockerResourceConfiguration, new QdrantConfiguration(certificate: certificate, certificateKey: certificateKey))
59+
.WithEnvironment("QDRANT__SERVICE__ENABLE_TLS", "1")
60+
.WithEnvironment("QDRANT__TLS__CERT", CertificateFilePath)
61+
.WithEnvironment("QDRANT__TLS__KEY", CertificateKeyFilePath)
62+
.WithResourceMapping(Encoding.UTF8.GetBytes(certificate), CertificateFilePath)
63+
.WithResourceMapping(Encoding.UTF8.GetBytes(certificateKey), CertificateKeyFilePath);
64+
}
65+
66+
/// <inheritdoc />
67+
public override QdrantContainer Build()
68+
{
69+
Validate();
70+
71+
// By default, the base builder waits until the container is running. However, for Qdrant, a more advanced waiting strategy is necessary that requires access to the configured certificate.
72+
// If the user does not provide a custom waiting strategy, append the default Qdrant waiting strategy.
73+
var qdrantBuilder = DockerResourceConfiguration.WaitStrategies.Count() > 1 ? this : WithWaitStrategy(Wait.ForUnixContainer().AddCustomWaitStrategy(new WaitUntil(DockerResourceConfiguration)));
74+
return new QdrantContainer(qdrantBuilder.DockerResourceConfiguration);
75+
}
76+
77+
/// <inheritdoc />
78+
protected override QdrantBuilder Init()
79+
{
80+
return base.Init()
81+
.WithImage(QdrantImage)
82+
.WithPortBinding(QdrantHttpPort, true)
83+
.WithPortBinding(QdrantGrpcPort, true);
84+
}
85+
86+
/// <inheritdoc />
87+
protected override QdrantBuilder Clone(IResourceConfiguration<CreateContainerParameters> resourceConfiguration)
88+
{
89+
return Merge(DockerResourceConfiguration, new QdrantConfiguration(resourceConfiguration));
90+
}
91+
92+
/// <inheritdoc />
93+
protected override QdrantBuilder Clone(IContainerConfiguration resourceConfiguration)
94+
{
95+
return Merge(DockerResourceConfiguration, new QdrantConfiguration(resourceConfiguration));
96+
}
97+
98+
/// <inheritdoc />
99+
protected override QdrantBuilder Merge(QdrantConfiguration oldValue, QdrantConfiguration newValue)
100+
{
101+
return new QdrantBuilder(new QdrantConfiguration(oldValue, newValue));
102+
}
103+
104+
/// <inheritdoc cref="IWaitUntil" />
105+
private sealed class WaitUntil : IWaitUntil
106+
{
107+
private readonly bool _tlsEnabled;
108+
109+
/// <summary>
110+
/// Initializes a new instance of the <see cref="WaitUntil" /> class.
111+
/// </summary>
112+
/// <param name="configuration">The container configuration.</param>
113+
public WaitUntil(QdrantConfiguration configuration)
114+
{
115+
_tlsEnabled = configuration.TlsEnabled;
116+
}
117+
118+
/// <inheritdoc />
119+
public async Task<bool> UntilAsync(IContainer container)
120+
{
121+
using var httpMessageHandler = new HttpClientHandler();
122+
httpMessageHandler.ServerCertificateCustomValidationCallback = (_, _, _, _) => true;
123+
124+
var httpWaitStrategy = new HttpWaitStrategy()
125+
.UsingHttpMessageHandler(httpMessageHandler)
126+
.UsingTls(_tlsEnabled)
127+
.ForPort(QdrantHttpPort)
128+
.ForPath("/readyz");
129+
130+
return await httpWaitStrategy.UntilAsync(container)
131+
.ConfigureAwait(false);
132+
}
133+
}
134+
}

0 commit comments

Comments
 (0)