Skip to content
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

feat: add Qdrant module #1149

Open
wants to merge 15 commits into
base: develop
Choose a base branch
from
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
<PackageVersion Include="Neo4j.Driver" Version="5.5.0"/>
<PackageVersion Include="Npgsql" Version="6.0.10"/>
<PackageVersion Include="Oracle.ManagedDataAccess.Core" Version="3.21.90"/>
<PackageVersion Include="Qdrant.Client" Version="1.8.0"/>
<PackageVersion Include="RabbitMQ.Client" Version="6.4.0"/>
<PackageVersion Include="RavenDB.Client" Version="5.4.100"/>
<PackageVersion Include="Selenium.WebDriver" Version="4.8.1"/>
Expand Down
14 changes: 14 additions & 0 deletions Testcontainers.sln
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.PostgreSql",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.PubSub", "src\Testcontainers.PubSub\Testcontainers.PubSub.csproj", "{E6642255-667D-476B-B584-089AA5E6C0B1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Qdrant", "src\Testcontainers.Qdrant\Testcontainers.Qdrant.csproj", "{7C98973D-53D7-49F9-BDFE-E3268F402584}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.RabbitMq", "src\Testcontainers.RabbitMq\Testcontainers.RabbitMq.csproj", "{A6D480BC-FDE8-4B92-A2A6-FF16BEE486AE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.RavenDb", "src\Testcontainers.RavenDb\Testcontainers.RavenDb.csproj", "{F6394475-D6F1-46E2-81BF-4BA78A40B878}"
Expand Down Expand Up @@ -179,6 +181,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.PostgreSql.T
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.PubSub.Tests", "tests\Testcontainers.PubSub.Tests\Testcontainers.PubSub.Tests.csproj", "{0F86BCE8-62E1-4BFC-AA84-63C7514C90AC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Qdrant.Tests", "tests\Testcontainers.Qdrant.Tests\Testcontainers.Qdrant.Tests.csproj", "{9DCE3E7F-B341-4AD0-BAAA-C3B91EB91B0D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.RabbitMq.Tests", "tests\Testcontainers.RabbitMq.Tests\Testcontainers.RabbitMq.Tests.csproj", "{19564567-1736-4626-B406-17E4E02F18B2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.RavenDb.Tests", "tests\Testcontainers.RavenDb.Tests\Testcontainers.RavenDb.Tests.csproj", "{D53726B6-5447-47E6-B881-A44EFF6E5534}"
Expand Down Expand Up @@ -348,6 +352,10 @@ Global
{E6642255-667D-476B-B584-089AA5E6C0B1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E6642255-667D-476B-B584-089AA5E6C0B1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E6642255-667D-476B-B584-089AA5E6C0B1}.Release|Any CPU.Build.0 = Release|Any CPU
{7C98973D-53D7-49F9-BDFE-E3268F402584}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7C98973D-53D7-49F9-BDFE-E3268F402584}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7C98973D-53D7-49F9-BDFE-E3268F402584}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7C98973D-53D7-49F9-BDFE-E3268F402584}.Release|Any CPU.Build.0 = Release|Any CPU
{A6D480BC-FDE8-4B92-A2A6-FF16BEE486AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A6D480BC-FDE8-4B92-A2A6-FF16BEE486AE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A6D480BC-FDE8-4B92-A2A6-FF16BEE486AE}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand Down Expand Up @@ -536,6 +544,10 @@ Global
{0F86BCE8-62E1-4BFC-AA84-63C7514C90AC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0F86BCE8-62E1-4BFC-AA84-63C7514C90AC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0F86BCE8-62E1-4BFC-AA84-63C7514C90AC}.Release|Any CPU.Build.0 = Release|Any CPU
{9DCE3E7F-B341-4AD0-BAAA-C3B91EB91B0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9DCE3E7F-B341-4AD0-BAAA-C3B91EB91B0D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9DCE3E7F-B341-4AD0-BAAA-C3B91EB91B0D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9DCE3E7F-B341-4AD0-BAAA-C3B91EB91B0D}.Release|Any CPU.Build.0 = Release|Any CPU
{19564567-1736-4626-B406-17E4E02F18B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{19564567-1736-4626-B406-17E4E02F18B2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{19564567-1736-4626-B406-17E4E02F18B2}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand Down Expand Up @@ -606,6 +618,7 @@ Global
{464F1120-A0DA-462B-B9E8-45176D883625} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{8AB91636-9055-4900-A72A-7CFFACDFDBF0} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{E6642255-667D-476B-B584-089AA5E6C0B1} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{7C98973D-53D7-49F9-BDFE-E3268F402584} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{A6D480BC-FDE8-4B92-A2A6-FF16BEE486AE} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{F6394475-D6F1-46E2-81BF-4BA78A40B878} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{BFDA179A-40EB-4CEB-B8E9-0DF32C65E2C5} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
Expand Down Expand Up @@ -653,6 +666,7 @@ Global
{3E55CBE8-AFE8-426D-9470-49D63CD1051C} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{56D0DCA5-567F-4B3B-8B79-CB108F8EB8A6} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{0F86BCE8-62E1-4BFC-AA84-63C7514C90AC} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{9DCE3E7F-B341-4AD0-BAAA-C3B91EB91B0D} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{19564567-1736-4626-B406-17E4E02F18B2} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{D53726B6-5447-47E6-B881-A44EFF6E5534} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{31EE94A0-E721-4073-B6F1-DD912D004DEF} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
Expand Down
1 change: 1 addition & 0 deletions src/Testcontainers.Qdrant/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
root = true
94 changes: 94 additions & 0 deletions src/Testcontainers.Qdrant/QdrantBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
namespace Testcontainers.Qdrant;

/// <inheritdoc cref="ContainerBuilder{TBuilderEntity, TContainerEntity, TConfigurationEntity}" />
[PublicAPI]
public sealed class QdrantBuilder : ContainerBuilder<QdrantBuilder, QdrantContainer, QdrantConfiguration>
{
public const string QdrantImage = "qdrant/qdrant:v1.8.3";

public const ushort QdrantHttpPort = 6333;

public const ushort QdrantGrpcPort = 6334;

public const string QdrantTlsCertFilePath = "/qdrant/tls/cert.pem";

public const string QdrantTlsKeyFilePath = "/qdrant/tls/key.pem";

public QdrantBuilder() : this(new QdrantConfiguration()) =>
DockerResourceConfiguration = Init().DockerResourceConfiguration;

private QdrantBuilder(QdrantConfiguration dockerResourceConfiguration) : base(dockerResourceConfiguration) =>
DockerResourceConfiguration = dockerResourceConfiguration;

/// <summary>
/// The API key used to secure the instance. A certificate and private key should also be
/// provided to <see cref="WithCertificate"/> to enable Transport Layer Security (TLS).
/// </summary>
/// <param name="apiKey">The API key</param>
public QdrantBuilder WithApiKey(string apiKey) =>
Merge(DockerResourceConfiguration, new QdrantConfiguration(apiKey: apiKey))
.WithEnvironment("QDRANT__SERVICE__API_KEY", apiKey);

/// <summary>
/// A certificate and private key to enable Transport Layer Security (TLS).
/// </summary>
/// <param name="certificate">A public certificate in PEM format</param>
/// <param name="privateKey">A private key for the certificate in PEM format</param>
public QdrantBuilder WithCertificate(string certificate, string privateKey)
{
return Merge(DockerResourceConfiguration, new QdrantConfiguration(certificate: certificate, privateKey: privateKey))
.WithEnvironment("QDRANT__SERVICE__ENABLE_TLS", "1")
.WithResourceMapping(Encoding.UTF8.GetBytes(certificate), QdrantTlsCertFilePath)
.WithEnvironment("QDRANT__TLS__CERT", QdrantTlsCertFilePath)
.WithResourceMapping(Encoding.UTF8.GetBytes(privateKey), QdrantTlsKeyFilePath)
.WithEnvironment("QDRANT__TLS__KEY", QdrantTlsKeyFilePath);
}

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

var waitStrategy = Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(request =>
{
var httpWaitStrategy = request.ForPort(QdrantHttpPort).ForPath("/readyz");

// allow any certificate defined to pass validation
if (DockerResourceConfiguration.Certificate is not null)
{
httpWaitStrategy.UsingTls()
.UsingHttpMessageHandler(new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (_, _, _, _) => true
});
}

return httpWaitStrategy;
});

var qdrantBuilder = DockerResourceConfiguration.WaitStrategies.Count() > 1 ? this : WithWaitStrategy(waitStrategy);
return new QdrantContainer(qdrantBuilder.DockerResourceConfiguration);
}

/// <inheritdoc />
protected override QdrantBuilder Init() =>
base.Init()
.WithImage(QdrantImage)
.WithPortBinding(QdrantHttpPort, true)
.WithPortBinding(QdrantGrpcPort, true);

/// <inheritdoc />
protected override QdrantBuilder Clone(IResourceConfiguration<CreateContainerParameters> resourceConfiguration) =>
Merge(DockerResourceConfiguration, new QdrantConfiguration(resourceConfiguration));

/// <inheritdoc />
protected override QdrantBuilder Merge(QdrantConfiguration oldValue, QdrantConfiguration newValue) =>
new(new QdrantConfiguration(oldValue, newValue));

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

/// <inheritdoc />
protected override QdrantBuilder Clone(IContainerConfiguration resourceConfiguration) =>
Merge(DockerResourceConfiguration, new QdrantConfiguration(resourceConfiguration));
}
71 changes: 71 additions & 0 deletions src/Testcontainers.Qdrant/QdrantConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
namespace Testcontainers.Qdrant;

/// <inheritdoc cref="ContainerConfiguration" />
[PublicAPI]
public sealed class QdrantConfiguration : ContainerConfiguration
{
/// <summary>
/// Initializes a new instance of the <see cref="QdrantConfiguration" /> class.
/// </summary>
public QdrantConfiguration(string apiKey = null, string certificate = null, string privateKey = null)
{
ApiKey = apiKey;
Certificate = certificate;
PrivateKey = privateKey;
}

/// <summary>
/// Initializes a new instance of the <see cref="QdrantConfiguration" /> class.
/// </summary>
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
public QdrantConfiguration(IResourceConfiguration<CreateContainerParameters> resourceConfiguration)
: base(resourceConfiguration)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="QdrantConfiguration" /> class.
/// </summary>
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
public QdrantConfiguration(IContainerConfiguration resourceConfiguration)
: base(resourceConfiguration)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="QdrantConfiguration" /> class.
/// </summary>
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
public QdrantConfiguration(QdrantConfiguration resourceConfiguration)
: this(new QdrantConfiguration(), resourceConfiguration)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="QdrantConfiguration" /> class.
/// </summary>
/// <param name="oldValue">The old Docker resource configuration.</param>
/// <param name="newValue">The new Docker resource configuration.</param>
public QdrantConfiguration(QdrantConfiguration oldValue, QdrantConfiguration newValue)
: base(oldValue, newValue)
{
ApiKey = BuildConfiguration.Combine(oldValue.ApiKey, newValue.ApiKey);
Certificate = BuildConfiguration.Combine(oldValue.Certificate, newValue.Certificate);
PrivateKey = BuildConfiguration.Combine(oldValue.PrivateKey, newValue.PrivateKey);
}

/// <summary>
/// Gets the API key used to secure Qdrant.
/// </summary>
public string ApiKey { get; }

/// <summary>
/// Gets the certificate used to configure Transport Layer Security. Certificate must be in PEM format.
/// </summary>
public string Certificate { get; }

/// <summary>
/// Gets the private key used to configure Transport Layer Security. Private key must be in PEM format.
/// </summary>
public string PrivateKey { get; }
}
33 changes: 33 additions & 0 deletions src/Testcontainers.Qdrant/QdrantContainer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
namespace Testcontainers.Qdrant;

/// <inheritdoc cref="DockerContainer" />
[PublicAPI]
public sealed class QdrantContainer : DockerContainer
{
private readonly QdrantConfiguration _configuration;

public QdrantContainer(QdrantConfiguration configuration) : base(configuration)
{
_configuration = configuration;
}

/// <summary>
/// Gets the connection string for connecting to Qdrant REST APIs
/// </summary>
public string GetHttpConnectionString()
{
var scheme = _configuration.Certificate != null ? Uri.UriSchemeHttps : Uri.UriSchemeHttp;
var endpoint = new UriBuilder(scheme, Hostname, GetMappedPublicPort(QdrantBuilder.QdrantHttpPort));
return endpoint.ToString();
}

/// <summary>
/// Gets the connection string for connecting to Qdrant gRPC APIs
/// </summary>
public string GetGrpcConnectionString()
{
var scheme = _configuration.Certificate != null ? Uri.UriSchemeHttps : Uri.UriSchemeHttp;
var endpoint = new UriBuilder(scheme, Hostname, GetMappedPublicPort(QdrantBuilder.QdrantGrpcPort));
return endpoint.ToString();
}
}
12 changes: 12 additions & 0 deletions src/Testcontainers.Qdrant/Testcontainers.Qdrant.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0;net8.0;netstandard2.0;netstandard2.1;net462</TargetFrameworks>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" VersionOverride="2023.3.0" PrivateAssets="All"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../Testcontainers/Testcontainers.csproj"/>
</ItemGroup>
</Project>
9 changes: 9 additions & 0 deletions src/Testcontainers.Qdrant/Usings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
global using System;
global using System.Linq;
global using System.Net.Http;
global using System.Text;
global using Docker.DotNet.Models;
global using DotNet.Testcontainers.Builders;
global using DotNet.Testcontainers.Configurations;
global using DotNet.Testcontainers.Containers;
global using JetBrains.Annotations;
1 change: 1 addition & 0 deletions tests/Testcontainers.Qdrant.Tests/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
root = true
24 changes: 24 additions & 0 deletions tests/Testcontainers.Qdrant.Tests/PemCertificate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace Testcontainers.Qdrant;

public record PemCertificate(string Certificate, string PrivateKey, string Thumbprint)
{
public static PemCertificate Create(string commonName)
{
using var key = RSA.Create(2048);
var utcNow = DateTimeOffset.UtcNow;
var request = new CertificateRequest(
$"CN={commonName}",
key,
HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1)
{
CertificateExtensions = { new X509BasicConstraintsExtension(false, false, 0, true) },
};

var certificate = request.CreateSelfSigned(utcNow, utcNow.AddYears(1));
return new PemCertificate(
certificate.ExportCertificatePem(),
certificate.GetRSAPrivateKey().ExportPkcs8PrivateKeyPem(),
certificate.GetCertHashString(HashAlgorithmName.SHA256));
}
}