-
-
Notifications
You must be signed in to change notification settings - Fork 290
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
Add Qdrant container #994
Add Qdrant container #994
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
root = true |
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)) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we do not require the |
||||||
.WithBindMount(configurationFilePath, QdrantLocalConfigurationFilePath); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
/// <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) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we not expect the developer to provide valid |
||||||
{ | ||||||
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)); | ||||||
} |
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; } | ||
} |
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(); | ||
} | ||
} |
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> |
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; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
root = true |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
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()), | ||
DefaultRequestHeaders = { Host = "Testcontainers" }, | ||
}; | ||
|
||
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()), | ||
DefaultRequestHeaders = { Host = "Testcontainers" }, | ||
}; | ||
|
||
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()), | ||
DefaultRequestHeaders = { Host = "Testcontainers" }, | ||
}; | ||
|
||
client.DefaultRequestHeaders.Add("api-key", ApiKey); | ||
|
||
// The SSL connection could not be established | ||
await Assert.ThrowsAsync<HttpRequestException>(() => client.GetAsync("/collections")); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please move the usings (other classes too) into the global
Usings.cs
file.