forked from testcontainers/testcontainers-dotnet
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This commit adds a qdrant container to the list of supported Testcontainers. The qdrant container allows configuration of: - an API key to authenticate to Qdrant - an x509 certificate used to secure communication to Qdrant with Transport Layer Security - a custom configuration file. See https://qdrant.tech/documentation/guides/configuration/ Closes testcontainers#992
- Loading branch information
Showing
14 changed files
with
520 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
root = true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
using System.IO; | ||
using System.Security.Cryptography.X509Certificates; | ||
using Org.BouncyCastle.OpenSsl; | ||
using Org.BouncyCastle.Security; | ||
|
||
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.5.0"; | ||
|
||
public const ushort QdrantHttpPort = 6333; | ||
|
||
public const ushort QdrantGrpcPort = 6334; | ||
|
||
public const string QdrantLocalConfigurationFilePath = "/qdrant/config/local.yaml"; | ||
|
||
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> | ||
/// A path to a configuration file with which configure the instance. | ||
/// </summary> | ||
/// <param name="configurationFilePath">The path to the configuration file</param> | ||
public QdrantBuilder WithConfigFile(string configurationFilePath) => | ||
Merge(DockerResourceConfiguration, new QdrantConfiguration(configurationFilePath: configurationFilePath)) | ||
.WithBindMount(configurationFilePath, QdrantLocalConfigurationFilePath); | ||
|
||
/// <summary> | ||
/// The API key used to secure the instance. A certificate should also be provided to <see cref="WithCertificate"/> | ||
/// to enable 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 to use to enable Transport Layer Security (TLS). The certificate must contain the | ||
/// private key. | ||
/// </summary> | ||
/// <param name="certificate">A certificate containing a private key</param> | ||
public QdrantBuilder WithCertificate(X509Certificate2 certificate) | ||
{ | ||
if (!certificate.HasPrivateKey) | ||
{ | ||
throw new ArgumentException("certificate must contain a private key", nameof(certificate)); | ||
} | ||
|
||
var builder = new StringBuilder(); | ||
builder.AppendLine("-----BEGIN CERTIFICATE-----"); | ||
builder.AppendLine(Convert.ToBase64String(certificate.RawData, Base64FormattingOptions.InsertLineBreaks)); | ||
builder.AppendLine("-----END CERTIFICATE-----"); | ||
var cert = builder.ToString(); | ||
builder.Clear(); | ||
|
||
var keyPair = DotNetUtilities.GetKeyPair(certificate.PrivateKey); | ||
var pemWriter = new PemWriter(new StringWriter(builder)); | ||
pemWriter.WriteObject(keyPair.Private); | ||
var key = builder.ToString(); | ||
|
||
return Merge(DockerResourceConfiguration, new QdrantConfiguration(certificate: certificate)) | ||
.WithEnvironment("QDRANT__SERVICE__ENABLE_TLS", "1") | ||
.WithResourceMapping(Encoding.UTF8.GetBytes(cert), QdrantTlsCertFilePath) | ||
.WithEnvironment("QDRANT__TLS__CERT", QdrantTlsCertFilePath) | ||
.WithResourceMapping(Encoding.UTF8.GetBytes(key), QdrantTlsKeyFilePath) | ||
.WithEnvironment("QDRANT__TLS__KEY", QdrantTlsKeyFilePath); | ||
} | ||
|
||
/// <inheritdoc /> | ||
public override QdrantContainer Build() | ||
{ | ||
Validate(); | ||
return new QdrantContainer(DockerResourceConfiguration, TestcontainersSettings.Logger); | ||
} | ||
|
||
/// <inheritdoc /> | ||
protected override QdrantBuilder Init() => | ||
base.Init() | ||
.WithImage(QdrantImage) | ||
.WithPortBinding(QdrantHttpPort, true) | ||
.WithPortBinding(QdrantGrpcPort, true) | ||
.WithWaitStrategy(Wait.ForUnixContainer() | ||
.UntilMessageIsLogged(".*Actix runtime found; starting in Actix runtime.*")); | ||
|
||
/// <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)); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
using System.Security.Cryptography.X509Certificates; | ||
|
||
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, X509Certificate2 certificate = null, string configurationFilePath = null) | ||
{ | ||
ApiKey = apiKey; | ||
Certificate = certificate; | ||
ConfigurationFilePath = configurationFilePath; | ||
} | ||
|
||
/// <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); | ||
ConfigurationFilePath = BuildConfiguration.Combine(oldValue.ConfigurationFilePath, newValue.ConfigurationFilePath); | ||
} | ||
|
||
/// <summary> | ||
/// Gets the API key used to secure Qdrant | ||
/// </summary> | ||
public string ApiKey { get; } | ||
|
||
/// <summary> | ||
/// Gets the certificate used to configure Transport Layer Security | ||
/// </summary> | ||
public X509Certificate2 Certificate { get; } | ||
|
||
/// <summary> | ||
/// Gets the path to the configuration file used to configure Qdrant | ||
/// </summary> | ||
public string ConfigurationFilePath { get; } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
namespace Testcontainers.Qdrant; | ||
|
||
/// <inheritdoc cref="DockerContainer" /> | ||
[PublicAPI] | ||
public class QdrantContainer : DockerContainer | ||
{ | ||
private readonly QdrantConfiguration _configuration; | ||
|
||
public QdrantContainer(QdrantConfiguration configuration, ILogger logger) : base(configuration, logger) | ||
{ | ||
_configuration = configuration; | ||
} | ||
|
||
public string GetHttpConnectionString() | ||
{ | ||
var scheme = _configuration.Certificate != null ? Uri.UriSchemeHttps : Uri.UriSchemeHttp; | ||
var endpoint = new UriBuilder(scheme, Hostname, GetMappedPublicPort(QdrantBuilder.QdrantHttpPort)); | ||
return endpoint.ToString(); | ||
} | ||
|
||
public string GetGrpcConnectionString() | ||
{ | ||
var scheme = _configuration.Certificate != null ? Uri.UriSchemeHttps : Uri.UriSchemeHttp; | ||
var endpoint = new UriBuilder(scheme, Hostname, GetMappedPublicPort(QdrantBuilder.QdrantGrpcPort)); | ||
return endpoint.ToString(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
<PropertyGroup> | ||
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks> | ||
<LangVersion>latest</LangVersion> | ||
</PropertyGroup> | ||
<ItemGroup> | ||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All"/> | ||
<PackageReference Include="JetBrains.Annotations" Version="2022.3.1" PrivateAssets="All"/> | ||
</ItemGroup> | ||
<ItemGroup> | ||
<ProjectReference Include="$(SolutionDir)src/Testcontainers/Testcontainers.csproj"/> | ||
</ItemGroup> | ||
</Project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
global using System; | ||
global using System.Collections.Generic; | ||
global using System.Linq; | ||
global using System.Text; | ||
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; | ||
global using Microsoft.Extensions.Logging; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
root = true |
82 changes: 82 additions & 0 deletions
82
tests/Testcontainers.Qdrant.Tests/QdrantContainerApiKeyCertificateTest.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
using System.Net.Http; | ||
using System.Security.Cryptography.X509Certificates; | ||
|
||
namespace Testcontainers.Qdrant; | ||
|
||
public sealed class QdrantContainerApiKeyCertificateTest : IAsyncLifetime | ||
{ | ||
private static readonly X509Certificate2 Cert = X509CertificateGenerator.GenerateCert("CN=Testcontainers"); | ||
private const string ApiKey = "password!"; | ||
|
||
private readonly QdrantContainer _qdrantContainer = new QdrantBuilder() | ||
.WithApiKey(ApiKey) | ||
.WithCertificate(Cert) | ||
.Build(); | ||
|
||
public Task InitializeAsync() | ||
{ | ||
return _qdrantContainer.StartAsync(); | ||
} | ||
|
||
public Task DisposeAsync() | ||
{ | ||
return _qdrantContainer.DisposeAsync().AsTask(); | ||
} | ||
|
||
[Fact] | ||
[Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] | ||
public async Task PingReturnsValidResponse() | ||
{ | ||
var httpMessageHandler = new HttpClientHandler | ||
{ | ||
ServerCertificateCustomValidationCallback = (_, certificate, _, _) => | ||
certificate.Thumbprint == Cert.Thumbprint, | ||
}; | ||
|
||
var client = new HttpClient(httpMessageHandler) | ||
{ | ||
BaseAddress = new Uri(_qdrantContainer.GetHttpConnectionString()), | ||
}; | ||
|
||
client.DefaultRequestHeaders.Add("api-key", ApiKey); | ||
|
||
var response = await client.GetAsync("/collections"); | ||
|
||
Assert.True(response.IsSuccessStatusCode); | ||
} | ||
|
||
[Fact] | ||
[Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] | ||
public async Task PingWithoutApiKeyReturnsInvalidResponse() | ||
{ | ||
var httpMessageHandler = new HttpClientHandler | ||
{ | ||
ServerCertificateCustomValidationCallback = (_, _, _, _) => true, | ||
}; | ||
|
||
var client = new HttpClient(httpMessageHandler) | ||
{ | ||
BaseAddress = new Uri(_qdrantContainer.GetHttpConnectionString()), | ||
}; | ||
|
||
var response = await client.GetAsync("/collections"); | ||
|
||
Assert.False(response.IsSuccessStatusCode); | ||
Assert.Equal("Invalid api-key", await response.Content.ReadAsStringAsync()); | ||
} | ||
|
||
[Fact] | ||
[Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] | ||
public async Task PingWithoutCertificateValidationReturnsInvalidResponse() | ||
{ | ||
var client = new HttpClient | ||
{ | ||
BaseAddress = new Uri(_qdrantContainer.GetHttpConnectionString()), | ||
}; | ||
|
||
client.DefaultRequestHeaders.Add("api-key", ApiKey); | ||
|
||
// The SSL connection could not be established | ||
await Assert.ThrowsAsync<HttpRequestException>(() => client.GetAsync("/collections")); | ||
} | ||
} |
Oops, something went wrong.