From 6b6d3fc8993fbf9074c960a6cc1e5676ed800572 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Sun, 10 Sep 2023 13:57:11 +1000 Subject: [PATCH 01/14] Add Qdrant container 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 #992 --- Testcontainers.sln | 18 +++ src/Testcontainers.Qdrant/.editorconfig | 1 + src/Testcontainers.Qdrant/QdrantBuilder.cs | 109 ++++++++++++++++++ .../QdrantConfiguration.cs | 73 ++++++++++++ src/Testcontainers.Qdrant/QdrantContainer.cs | 27 +++++ .../Testcontainers.Qdrant.csproj | 13 +++ src/Testcontainers.Qdrant/Usings.cs | 12 ++ .../Testcontainers.Qdrant.Tests/.editorconfig | 1 + .../QdrantContainerApiKeyCertificateTest.cs | 82 +++++++++++++ .../QdrantContainerConfigurationFileTest.cs | 64 ++++++++++ .../QdrantContainerTest.cs | 31 +++++ .../Testcontainers.Qdrant.Tests.csproj | 17 +++ tests/Testcontainers.Qdrant.Tests/Usings.cs | 4 + .../X509CertificateGenerator.cs | 68 +++++++++++ 14 files changed, 520 insertions(+) create mode 100644 src/Testcontainers.Qdrant/.editorconfig create mode 100644 src/Testcontainers.Qdrant/QdrantBuilder.cs create mode 100644 src/Testcontainers.Qdrant/QdrantConfiguration.cs create mode 100644 src/Testcontainers.Qdrant/QdrantContainer.cs create mode 100644 src/Testcontainers.Qdrant/Testcontainers.Qdrant.csproj create mode 100644 src/Testcontainers.Qdrant/Usings.cs create mode 100644 tests/Testcontainers.Qdrant.Tests/.editorconfig create mode 100644 tests/Testcontainers.Qdrant.Tests/QdrantContainerApiKeyCertificateTest.cs create mode 100644 tests/Testcontainers.Qdrant.Tests/QdrantContainerConfigurationFileTest.cs create mode 100644 tests/Testcontainers.Qdrant.Tests/QdrantContainerTest.cs create mode 100644 tests/Testcontainers.Qdrant.Tests/Testcontainers.Qdrant.Tests.csproj create mode 100644 tests/Testcontainers.Qdrant.Tests/Usings.cs create mode 100644 tests/Testcontainers.Qdrant.Tests/X509CertificateGenerator.cs diff --git a/Testcontainers.sln b/Testcontainers.sln index 9595905ed..c87995416 100644 --- a/Testcontainers.sln +++ b/Testcontainers.sln @@ -195,6 +195,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Tests", "tes EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.WebDriver.Tests", "tests\Testcontainers.WebDriver.Tests\Testcontainers.WebDriver.Tests.csproj", "{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}" 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.Qdrant.Tests", "tests\Testcontainers.Qdrant.Tests\Testcontainers.Qdrant.Tests.csproj", "{9DCE3E7F-B341-4AD0-BAAA-C3B91EB91B0D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -568,6 +572,18 @@ Global {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Debug|Any CPU.Build.0 = Debug|Any CPU {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Release|Any CPU.ActiveCfg = Release|Any CPU {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Release|Any CPU.Build.0 = Release|Any CPU + {23D898F8-36BE-4393-BFE2-41A862C0F951}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {23D898F8-36BE-4393-BFE2-41A862C0F951}.Debug|Any CPU.Build.0 = Debug|Any CPU + {23D898F8-36BE-4393-BFE2-41A862C0F951}.Release|Any CPU.ActiveCfg = Release|Any CPU + {23D898F8-36BE-4393-BFE2-41A862C0F951}.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 + {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 EndGlobalSection GlobalSection(NestedProjects) = preSolution {5365F780-0E6C-41F0-B1B9-7DC34368F80C} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} @@ -661,5 +677,7 @@ Global {1A1983E6-5297-435F-B467-E8E1F11277D6} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {27CDB869-A150-4593-958F-6F26E5391E7C} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} + {7C98973D-53D7-49F9-BDFE-E3268F402584} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} + {9DCE3E7F-B341-4AD0-BAAA-C3B91EB91B0D} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} EndGlobalSection EndGlobal diff --git a/src/Testcontainers.Qdrant/.editorconfig b/src/Testcontainers.Qdrant/.editorconfig new file mode 100644 index 000000000..6f066619d --- /dev/null +++ b/src/Testcontainers.Qdrant/.editorconfig @@ -0,0 +1 @@ +root = true \ No newline at end of file diff --git a/src/Testcontainers.Qdrant/QdrantBuilder.cs b/src/Testcontainers.Qdrant/QdrantBuilder.cs new file mode 100644 index 000000000..ce3a1d1b8 --- /dev/null +++ b/src/Testcontainers.Qdrant/QdrantBuilder.cs @@ -0,0 +1,109 @@ +using System.IO; +using System.Security.Cryptography.X509Certificates; +using Org.BouncyCastle.OpenSsl; +using Org.BouncyCastle.Security; + +namespace Testcontainers.Qdrant; + +/// +[PublicAPI] +public sealed class QdrantBuilder : ContainerBuilder +{ + 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; + + /// + /// A path to a configuration file with which configure the instance. + /// + /// The path to the configuration file + public QdrantBuilder WithConfigFile(string configurationFilePath) => + Merge(DockerResourceConfiguration, new QdrantConfiguration(configurationFilePath: configurationFilePath)) + .WithBindMount(configurationFilePath, QdrantLocalConfigurationFilePath); + + /// + /// The API key used to secure the instance. A certificate should also be provided to + /// to enable TLS + /// + /// The API key + public QdrantBuilder WithApiKey(string apiKey) => + Merge(DockerResourceConfiguration, new QdrantConfiguration(apiKey: apiKey)) + .WithEnvironment("QDRANT__SERVICE__API_KEY", apiKey); + + /// + /// A certificate to use to enable Transport Layer Security (TLS). The certificate must contain the + /// private key. + /// + /// A certificate containing a private key + 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); + } + + /// + public override QdrantContainer Build() + { + Validate(); + return new QdrantContainer(DockerResourceConfiguration, TestcontainersSettings.Logger); + } + + /// + 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.*")); + + /// + protected override QdrantBuilder Clone(IResourceConfiguration resourceConfiguration) => + Merge(DockerResourceConfiguration, new QdrantConfiguration(resourceConfiguration)); + + /// + protected override QdrantBuilder Merge(QdrantConfiguration oldValue, QdrantConfiguration newValue) => + new(new QdrantConfiguration(oldValue, newValue)); + + /// + protected override QdrantConfiguration DockerResourceConfiguration { get; } + + /// + protected override QdrantBuilder Clone(IContainerConfiguration resourceConfiguration) => + Merge(DockerResourceConfiguration, new QdrantConfiguration(resourceConfiguration)); +} diff --git a/src/Testcontainers.Qdrant/QdrantConfiguration.cs b/src/Testcontainers.Qdrant/QdrantConfiguration.cs new file mode 100644 index 000000000..714303f38 --- /dev/null +++ b/src/Testcontainers.Qdrant/QdrantConfiguration.cs @@ -0,0 +1,73 @@ +using System.Security.Cryptography.X509Certificates; + +namespace Testcontainers.Qdrant; + +/// +[PublicAPI] +public sealed class QdrantConfiguration : ContainerConfiguration +{ + /// + /// Initializes a new instance of the class. + /// + public QdrantConfiguration(string apiKey = null, X509Certificate2 certificate = null, string configurationFilePath = null) + { + ApiKey = apiKey; + Certificate = certificate; + ConfigurationFilePath = configurationFilePath; + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public QdrantConfiguration(IResourceConfiguration resourceConfiguration) + : base(resourceConfiguration) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public QdrantConfiguration(IContainerConfiguration resourceConfiguration) + : base(resourceConfiguration) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public QdrantConfiguration(QdrantConfiguration resourceConfiguration) + : this(new QdrantConfiguration(), resourceConfiguration) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The old Docker resource configuration. + /// The new Docker resource configuration. + 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); + } + + /// + /// Gets the API key used to secure Qdrant + /// + public string ApiKey { get; } + + /// + /// Gets the certificate used to configure Transport Layer Security + /// + public X509Certificate2 Certificate { get; } + + /// + /// Gets the path to the configuration file used to configure Qdrant + /// + public string ConfigurationFilePath { get; } +} diff --git a/src/Testcontainers.Qdrant/QdrantContainer.cs b/src/Testcontainers.Qdrant/QdrantContainer.cs new file mode 100644 index 000000000..1067e088e --- /dev/null +++ b/src/Testcontainers.Qdrant/QdrantContainer.cs @@ -0,0 +1,27 @@ +namespace Testcontainers.Qdrant; + +/// +[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(); + } +} diff --git a/src/Testcontainers.Qdrant/Testcontainers.Qdrant.csproj b/src/Testcontainers.Qdrant/Testcontainers.Qdrant.csproj new file mode 100644 index 000000000..4c05d521f --- /dev/null +++ b/src/Testcontainers.Qdrant/Testcontainers.Qdrant.csproj @@ -0,0 +1,13 @@ + + + netstandard2.0;netstandard2.1 + latest + + + + + + + + + \ No newline at end of file diff --git a/src/Testcontainers.Qdrant/Usings.cs b/src/Testcontainers.Qdrant/Usings.cs new file mode 100644 index 000000000..5696bb0bf --- /dev/null +++ b/src/Testcontainers.Qdrant/Usings.cs @@ -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; \ No newline at end of file diff --git a/tests/Testcontainers.Qdrant.Tests/.editorconfig b/tests/Testcontainers.Qdrant.Tests/.editorconfig new file mode 100644 index 000000000..6f066619d --- /dev/null +++ b/tests/Testcontainers.Qdrant.Tests/.editorconfig @@ -0,0 +1 @@ +root = true \ No newline at end of file diff --git a/tests/Testcontainers.Qdrant.Tests/QdrantContainerApiKeyCertificateTest.cs b/tests/Testcontainers.Qdrant.Tests/QdrantContainerApiKeyCertificateTest.cs new file mode 100644 index 000000000..d55105e4c --- /dev/null +++ b/tests/Testcontainers.Qdrant.Tests/QdrantContainerApiKeyCertificateTest.cs @@ -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(() => client.GetAsync("/collections")); + } +} \ No newline at end of file diff --git a/tests/Testcontainers.Qdrant.Tests/QdrantContainerConfigurationFileTest.cs b/tests/Testcontainers.Qdrant.Tests/QdrantContainerConfigurationFileTest.cs new file mode 100644 index 000000000..716b92a3a --- /dev/null +++ b/tests/Testcontainers.Qdrant.Tests/QdrantContainerConfigurationFileTest.cs @@ -0,0 +1,64 @@ +using System.IO; +using System.Net.Http; + +namespace Testcontainers.Qdrant; + +public sealed class QdrantContainerConfigurationFileTest : IAsyncLifetime +{ + private const string ApiKey = "password!"; + + private readonly QdrantContainer _qdrantContainer = new QdrantBuilder() + .WithConfigFile(CreateConfigFile()) + .Build(); + + private static string CreateConfigFile() + { + var tempFile = Path.GetTempFileName(); + File.WriteAllLines(tempFile, new[] + { + "service:", + $" api_key: {ApiKey}", + }); + return tempFile; + } + + 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 client = new HttpClient + { + 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 client = new HttpClient + { + BaseAddress = new Uri(_qdrantContainer.GetHttpConnectionString()), + }; + + var response = await client.GetAsync("/collections"); + + Assert.False(response.IsSuccessStatusCode); + Assert.Equal("Invalid api-key", await response.Content.ReadAsStringAsync()); + } +} \ No newline at end of file diff --git a/tests/Testcontainers.Qdrant.Tests/QdrantContainerTest.cs b/tests/Testcontainers.Qdrant.Tests/QdrantContainerTest.cs new file mode 100644 index 000000000..5cdb730fc --- /dev/null +++ b/tests/Testcontainers.Qdrant.Tests/QdrantContainerTest.cs @@ -0,0 +1,31 @@ +using System.Net.Http; + +namespace Testcontainers.Qdrant; + +public sealed class QdrantContainerTest : IAsyncLifetime +{ + private readonly QdrantContainer _qdrantContainer = new QdrantBuilder().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 client = new HttpClient + { + BaseAddress = new Uri(_qdrantContainer.GetHttpConnectionString()), + }; + + var response = await client.GetAsync("/"); + Assert.True(response.IsSuccessStatusCode); + } +} \ No newline at end of file diff --git a/tests/Testcontainers.Qdrant.Tests/Testcontainers.Qdrant.Tests.csproj b/tests/Testcontainers.Qdrant.Tests/Testcontainers.Qdrant.Tests.csproj new file mode 100644 index 000000000..c243a4546 --- /dev/null +++ b/tests/Testcontainers.Qdrant.Tests/Testcontainers.Qdrant.Tests.csproj @@ -0,0 +1,17 @@ + + + net6.0 + false + false + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Testcontainers.Qdrant.Tests/Usings.cs b/tests/Testcontainers.Qdrant.Tests/Usings.cs new file mode 100644 index 000000000..58083ebd6 --- /dev/null +++ b/tests/Testcontainers.Qdrant.Tests/Usings.cs @@ -0,0 +1,4 @@ +global using System; +global using System.Threading.Tasks; +global using DotNet.Testcontainers.Commons; +global using Xunit; \ No newline at end of file diff --git a/tests/Testcontainers.Qdrant.Tests/X509CertificateGenerator.cs b/tests/Testcontainers.Qdrant.Tests/X509CertificateGenerator.cs new file mode 100644 index 000000000..ef5f2f6fc --- /dev/null +++ b/tests/Testcontainers.Qdrant.Tests/X509CertificateGenerator.cs @@ -0,0 +1,68 @@ +using System.IO; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using Org.BouncyCastle.Asn1.X509; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Generators; +using Org.BouncyCastle.Crypto.Operators; +using Org.BouncyCastle.Crypto.Prng; +using Org.BouncyCastle.Math; +using Org.BouncyCastle.OpenSsl; +using Org.BouncyCastle.Pkcs; +using Org.BouncyCastle.Security; +using Org.BouncyCastle.Utilities; +using Org.BouncyCastle.X509; + +namespace Testcontainers.Qdrant; + +public static class X509CertificateGenerator +{ + public static X509Certificate2 GenerateCert(string subjectName) + { + var randomGenerator = new CryptoApiRandomGenerator(); + var random = new SecureRandom(randomGenerator); + var serialNumber = BigIntegers.CreateRandomInRange( + BigInteger.One, + BigInteger.ValueOf(long.MaxValue), random); + var subjectDistinguishedName = new X509Name(subjectName); + var issuerDistinguishedName = subjectDistinguishedName; + var notBefore = DateTime.UtcNow.Date; + var notAfter = notBefore.AddYears(1); + var keyGenerationParameters = new KeyGenerationParameters(random, 2048); + var keyPairGenerator = new RsaKeyPairGenerator(); + keyPairGenerator.Init(keyGenerationParameters); + var subjectKeyPair = keyPairGenerator.GenerateKeyPair(); + var issuerPrivateKey = subjectKeyPair.Private; + + var certificateGenerator = new X509V3CertificateGenerator(); + certificateGenerator.SetSerialNumber(serialNumber); + certificateGenerator.AddExtension( + X509Extensions.ExtendedKeyUsage, + true, + new ExtendedKeyUsage(KeyPurposeID.id_kp_serverAuth)); + certificateGenerator.SetIssuerDN(issuerDistinguishedName); + certificateGenerator.SetSubjectDN(subjectDistinguishedName); + certificateGenerator.SetNotBefore(notBefore); + certificateGenerator.SetNotAfter(notAfter); + certificateGenerator.SetPublicKey(subjectKeyPair.Public); + + var signatureFactory = new Asn1SignatureFactory("SHA512WITHRSA", issuerPrivateKey, random); + var certificate = certificateGenerator.Generate(signatureFactory); + var privateKeyInfo = PrivateKeyInfoFactory.CreatePrivateKeyInfo(subjectKeyPair.Private); + + var builder = new StringBuilder(); + using var writer = new StringWriter(builder); + using var pemWriter = new PemWriter(writer); + + pemWriter.WriteObject(certificate); + var pemCert = builder.ToString(); + builder.Clear(); + + pemWriter.WriteObject(privateKeyInfo); + var pemKey = builder.ToString(); + + return X509Certificate2.CreateFromPem(pemCert, pemKey); + } +} + + From 1cd8f77c11b0c7f789421f30647db891139c6ce3 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Sun, 10 Sep 2023 22:47:29 +1000 Subject: [PATCH 02/14] Fix TLS tests on Linux Qdrant uses rustls for TLS, which does not accept IPv4 or IPv6 addresses as the hostname in SNI, adhering to RFC 6066: https://www.rfc-editor.org/rfc/rfc6066#page-7 Sending an IPv4 or IPv6 address as the hostname in SNI causes the handshake to be rejected and qdrant to log WARN rustls::msgs::handshake: Illegal SNI hostname received "127.0.0.1" By default, .NET on linux sends the DNS name in the URI in Server Name Indication (SNI), irrespective of whether it's an IP address or not. In order to resolve this, a custom Host request header is added, per https://github.com/dotnet/runtime/issues/20876#issuecomment-379585707 --- src/Testcontainers.Qdrant/QdrantContainer.cs | 2 +- .../QdrantContainerApiKeyCertificateTest.cs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Testcontainers.Qdrant/QdrantContainer.cs b/src/Testcontainers.Qdrant/QdrantContainer.cs index 1067e088e..c836aae38 100644 --- a/src/Testcontainers.Qdrant/QdrantContainer.cs +++ b/src/Testcontainers.Qdrant/QdrantContainer.cs @@ -10,7 +10,7 @@ public QdrantContainer(QdrantConfiguration configuration, ILogger logger) : base { _configuration = configuration; } - + public string GetHttpConnectionString() { var scheme = _configuration.Certificate != null ? Uri.UriSchemeHttps : Uri.UriSchemeHttp; diff --git a/tests/Testcontainers.Qdrant.Tests/QdrantContainerApiKeyCertificateTest.cs b/tests/Testcontainers.Qdrant.Tests/QdrantContainerApiKeyCertificateTest.cs index d55105e4c..fc9558ba4 100644 --- a/tests/Testcontainers.Qdrant.Tests/QdrantContainerApiKeyCertificateTest.cs +++ b/tests/Testcontainers.Qdrant.Tests/QdrantContainerApiKeyCertificateTest.cs @@ -36,6 +36,7 @@ public async Task PingReturnsValidResponse() var client = new HttpClient(httpMessageHandler) { BaseAddress = new Uri(_qdrantContainer.GetHttpConnectionString()), + DefaultRequestHeaders = { Host = "Testcontainers" }, }; client.DefaultRequestHeaders.Add("api-key", ApiKey); @@ -57,6 +58,7 @@ public async Task PingWithoutApiKeyReturnsInvalidResponse() var client = new HttpClient(httpMessageHandler) { BaseAddress = new Uri(_qdrantContainer.GetHttpConnectionString()), + DefaultRequestHeaders = { Host = "Testcontainers" }, }; var response = await client.GetAsync("/collections"); @@ -72,6 +74,7 @@ public async Task PingWithoutCertificateValidationReturnsInvalidResponse() var client = new HttpClient { BaseAddress = new Uri(_qdrantContainer.GetHttpConnectionString()), + DefaultRequestHeaders = { Host = "Testcontainers" }, }; client.DefaultRequestHeaders.Add("api-key", ApiKey); From 0181b8e5275f08796e5235b02c0ef9e7b937ed8a Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Sun, 10 Sep 2023 23:24:36 +1000 Subject: [PATCH 03/14] Remove superfluous solution items --- Testcontainers.sln | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Testcontainers.sln b/Testcontainers.sln index c87995416..7110f52bf 100644 --- a/Testcontainers.sln +++ b/Testcontainers.sln @@ -572,10 +572,6 @@ Global {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Debug|Any CPU.Build.0 = Debug|Any CPU {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Release|Any CPU.ActiveCfg = Release|Any CPU {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Release|Any CPU.Build.0 = Release|Any CPU - {23D898F8-36BE-4393-BFE2-41A862C0F951}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {23D898F8-36BE-4393-BFE2-41A862C0F951}.Debug|Any CPU.Build.0 = Debug|Any CPU - {23D898F8-36BE-4393-BFE2-41A862C0F951}.Release|Any CPU.ActiveCfg = Release|Any CPU - {23D898F8-36BE-4393-BFE2-41A862C0F951}.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 From dc5f0cb55a755f4a1254da6031a05770b2c7ee98 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Sat, 30 Mar 2024 09:04:12 +1000 Subject: [PATCH 04/14] PR feedback --- src/Testcontainers.Qdrant/QdrantBuilder.cs | 58 +++---------- .../QdrantConfiguration.cs | 18 ++-- src/Testcontainers.Qdrant/QdrantContainer.cs | 2 +- .../Testcontainers.Qdrant.csproj | 4 +- src/Testcontainers.Qdrant/Usings.cs | 4 - .../QdrantContainerApiKeyCertificateTest.cs | 83 +++++++++++++------ .../QdrantContainerConfigurationFileTest.cs | 64 -------------- .../QdrantContainerTest.cs | 12 +++ .../Testcontainers.Qdrant.Tests.csproj | 12 +-- .../X509CertificateGenerator.cs | 11 +-- 10 files changed, 106 insertions(+), 162 deletions(-) delete mode 100644 tests/Testcontainers.Qdrant.Tests/QdrantContainerConfigurationFileTest.cs diff --git a/src/Testcontainers.Qdrant/QdrantBuilder.cs b/src/Testcontainers.Qdrant/QdrantBuilder.cs index ce3a1d1b8..a4b8e6d75 100644 --- a/src/Testcontainers.Qdrant/QdrantBuilder.cs +++ b/src/Testcontainers.Qdrant/QdrantBuilder.cs @@ -1,21 +1,14 @@ -using System.IO; -using System.Security.Cryptography.X509Certificates; -using Org.BouncyCastle.OpenSsl; -using Org.BouncyCastle.Security; - namespace Testcontainers.Qdrant; /// [PublicAPI] public sealed class QdrantBuilder : ContainerBuilder { - public const string QdrantImage = "qdrant/qdrant:v1.5.0"; + public const string QdrantImage = "qdrant/qdrant:v1.8.3"; 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"; @@ -26,53 +19,28 @@ public QdrantBuilder() : this(new QdrantConfiguration()) => private QdrantBuilder(QdrantConfiguration dockerResourceConfiguration) : base(dockerResourceConfiguration) => DockerResourceConfiguration = dockerResourceConfiguration; - - /// - /// A path to a configuration file with which configure the instance. - /// - /// The path to the configuration file - public QdrantBuilder WithConfigFile(string configurationFilePath) => - Merge(DockerResourceConfiguration, new QdrantConfiguration(configurationFilePath: configurationFilePath)) - .WithBindMount(configurationFilePath, QdrantLocalConfigurationFilePath); - + /// - /// The API key used to secure the instance. A certificate should also be provided to - /// to enable TLS + /// The API key used to secure the instance. A certificate and private key should also be + /// provided to to enable Transport Layer Security (TLS). /// /// The API key public QdrantBuilder WithApiKey(string apiKey) => Merge(DockerResourceConfiguration, new QdrantConfiguration(apiKey: apiKey)) .WithEnvironment("QDRANT__SERVICE__API_KEY", apiKey); - + /// - /// A certificate to use to enable Transport Layer Security (TLS). The certificate must contain the - /// private key. + /// A certificate and private key to enable Transport Layer Security (TLS). /// - /// A certificate containing a private key - public QdrantBuilder WithCertificate(X509Certificate2 certificate) + /// A public certificate in PEM format + /// A private key for the certificate in PEM format + public QdrantBuilder WithCertificate(string certificate, string privateKey) { - 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)) + return Merge(DockerResourceConfiguration, new QdrantConfiguration(certificate: certificate, privateKey: privateKey)) .WithEnvironment("QDRANT__SERVICE__ENABLE_TLS", "1") - .WithResourceMapping(Encoding.UTF8.GetBytes(cert), QdrantTlsCertFilePath) + .WithResourceMapping(Encoding.UTF8.GetBytes(certificate), QdrantTlsCertFilePath) .WithEnvironment("QDRANT__TLS__CERT", QdrantTlsCertFilePath) - .WithResourceMapping(Encoding.UTF8.GetBytes(key), QdrantTlsKeyFilePath) + .WithResourceMapping(Encoding.UTF8.GetBytes(privateKey), QdrantTlsKeyFilePath) .WithEnvironment("QDRANT__TLS__KEY", QdrantTlsKeyFilePath); } @@ -80,7 +48,7 @@ public QdrantBuilder WithCertificate(X509Certificate2 certificate) public override QdrantContainer Build() { Validate(); - return new QdrantContainer(DockerResourceConfiguration, TestcontainersSettings.Logger); + return new QdrantContainer(DockerResourceConfiguration); } /// diff --git a/src/Testcontainers.Qdrant/QdrantConfiguration.cs b/src/Testcontainers.Qdrant/QdrantConfiguration.cs index 714303f38..e161db4c2 100644 --- a/src/Testcontainers.Qdrant/QdrantConfiguration.cs +++ b/src/Testcontainers.Qdrant/QdrantConfiguration.cs @@ -1,4 +1,4 @@ -using System.Security.Cryptography.X509Certificates; +using static DotNet.Testcontainers.Builders.BuildConfiguration; namespace Testcontainers.Qdrant; @@ -9,11 +9,11 @@ public sealed class QdrantConfiguration : ContainerConfiguration /// /// Initializes a new instance of the class. /// - public QdrantConfiguration(string apiKey = null, X509Certificate2 certificate = null, string configurationFilePath = null) + public QdrantConfiguration(string apiKey = null, string certificate = null, string privateKey = null) { ApiKey = apiKey; Certificate = certificate; - ConfigurationFilePath = configurationFilePath; + PrivateKey = privateKey; } /// @@ -51,9 +51,9 @@ public QdrantConfiguration(QdrantConfiguration resourceConfiguration) 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); + ApiKey = Combine(oldValue.ApiKey, newValue.ApiKey); + Certificate = Combine(oldValue.Certificate, newValue.Certificate); + PrivateKey = Combine(oldValue.PrivateKey, newValue.PrivateKey); } /// @@ -64,10 +64,10 @@ public QdrantConfiguration(QdrantConfiguration oldValue, QdrantConfiguration new /// /// Gets the certificate used to configure Transport Layer Security /// - public X509Certificate2 Certificate { get; } + public string Certificate { get; } /// - /// Gets the path to the configuration file used to configure Qdrant + /// Gets the private key used to configure Transport Layer Security /// - public string ConfigurationFilePath { get; } + public string PrivateKey { get; } } diff --git a/src/Testcontainers.Qdrant/QdrantContainer.cs b/src/Testcontainers.Qdrant/QdrantContainer.cs index c836aae38..6eb065088 100644 --- a/src/Testcontainers.Qdrant/QdrantContainer.cs +++ b/src/Testcontainers.Qdrant/QdrantContainer.cs @@ -6,7 +6,7 @@ public class QdrantContainer : DockerContainer { private readonly QdrantConfiguration _configuration; - public QdrantContainer(QdrantConfiguration configuration, ILogger logger) : base(configuration, logger) + public QdrantContainer(QdrantConfiguration configuration) : base(configuration) { _configuration = configuration; } diff --git a/src/Testcontainers.Qdrant/Testcontainers.Qdrant.csproj b/src/Testcontainers.Qdrant/Testcontainers.Qdrant.csproj index 4c05d521f..7f562f0df 100644 --- a/src/Testcontainers.Qdrant/Testcontainers.Qdrant.csproj +++ b/src/Testcontainers.Qdrant/Testcontainers.Qdrant.csproj @@ -1,11 +1,11 @@ - netstandard2.0;netstandard2.1 + net6.0;net8.0;netstandard2.0;netstandard2.1 latest - + diff --git a/src/Testcontainers.Qdrant/Usings.cs b/src/Testcontainers.Qdrant/Usings.cs index 5696bb0bf..fd93092b4 100644 --- a/src/Testcontainers.Qdrant/Usings.cs +++ b/src/Testcontainers.Qdrant/Usings.cs @@ -1,10 +1,6 @@ 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; diff --git a/tests/Testcontainers.Qdrant.Tests/QdrantContainerApiKeyCertificateTest.cs b/tests/Testcontainers.Qdrant.Tests/QdrantContainerApiKeyCertificateTest.cs index fc9558ba4..5ca86430d 100644 --- a/tests/Testcontainers.Qdrant.Tests/QdrantContainerApiKeyCertificateTest.cs +++ b/tests/Testcontainers.Qdrant.Tests/QdrantContainerApiKeyCertificateTest.cs @@ -1,16 +1,27 @@ using System.Net.Http; +using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; +using Grpc.Core; +using Grpc.Core.Interceptors; +using Grpc.Net.Client; +using Qdrant.Client; +using Qdrant.Client.Grpc; +using static Testcontainers.Qdrant.X509CertificateGenerator; +using Uri = System.Uri; namespace Testcontainers.Qdrant; public sealed class QdrantContainerApiKeyCertificateTest : IAsyncLifetime { - private static readonly X509Certificate2 Cert = X509CertificateGenerator.GenerateCert("CN=Testcontainers"); + private static readonly PemCertificate Cert = Generate("CN=Testcontainers"); + private static readonly string Thumbprint = + X509Certificate2.CreateFromPem(Cert.Certificate, Cert.PrivateKey) + .GetCertHashString(HashAlgorithmName.SHA256); private const string ApiKey = "password!"; private readonly QdrantContainer _qdrantContainer = new QdrantBuilder() .WithApiKey(ApiKey) - .WithCertificate(Cert) + .WithCertificate(Cert.Certificate, Cert.PrivateKey) .Build(); public Task InitializeAsync() @@ -25,51 +36,68 @@ public Task DisposeAsync() [Fact] [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] - public async Task PingReturnsValidResponse() + public async Task ListCollectionsReturnsValidResponse() { var httpMessageHandler = new HttpClientHandler { - ServerCertificateCustomValidationCallback = (_, certificate, _, _) => - certificate.Thumbprint == Cert.Thumbprint, + ServerCertificateCustomValidationCallback = + CertificateValidation.Thumbprint(Thumbprint), }; - - var client = new HttpClient(httpMessageHandler) + + var channel = GrpcChannel.ForAddress( + _qdrantContainer.GetGrpcConnectionString(), + new GrpcChannelOptions + { + HttpClient = new HttpClient(httpMessageHandler) + { + DefaultRequestHeaders = { Host = "Testcontainers" }, + }, + }); + var callInvoker = channel.Intercept(metadata => { - BaseAddress = new Uri(_qdrantContainer.GetHttpConnectionString()), - DefaultRequestHeaders = { Host = "Testcontainers" }, - }; - - client.DefaultRequestHeaders.Add("api-key", ApiKey); + metadata.Add("api-key", ApiKey); + return metadata; + }); - var response = await client.GetAsync("/collections"); + var grpcClient = new QdrantGrpcClient(callInvoker); + var client = new QdrantClient(grpcClient); + var response = await client.ListCollectionsAsync(); - Assert.True(response.IsSuccessStatusCode); + Assert.Empty(response); } [Fact] [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] - public async Task PingWithoutApiKeyReturnsInvalidResponse() + public async Task ListCollectionsWithoutApiKeyReturnsInvalidResponse() { var httpMessageHandler = new HttpClientHandler { - ServerCertificateCustomValidationCallback = (_, _, _, _) => true, - }; - - var client = new HttpClient(httpMessageHandler) - { - BaseAddress = new Uri(_qdrantContainer.GetHttpConnectionString()), - DefaultRequestHeaders = { Host = "Testcontainers" }, + ServerCertificateCustomValidationCallback = + CertificateValidation.Thumbprint(Thumbprint) }; + + var channel = GrpcChannel.ForAddress( + _qdrantContainer.GetGrpcConnectionString(), + new GrpcChannelOptions + { + HttpClient = new HttpClient(httpMessageHandler) + { + DefaultRequestHeaders = { Host = "Testcontainers" }, + }, + }); + + var grpcClient = new QdrantGrpcClient(channel); + var client = new QdrantClient(grpcClient); - var response = await client.GetAsync("/collections"); + var exception = await Assert.ThrowsAsync(async () => + await client.ListCollectionsAsync()); - Assert.False(response.IsSuccessStatusCode); - Assert.Equal("Invalid api-key", await response.Content.ReadAsStringAsync()); + Assert.Equal(StatusCode.PermissionDenied, exception.Status.StatusCode); } [Fact] [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] - public async Task PingWithoutCertificateValidationReturnsInvalidResponse() + public async Task ListCollectionsWithoutCertificateValidationReturnsInvalidResponse() { var client = new HttpClient { @@ -80,6 +108,7 @@ public async Task PingWithoutCertificateValidationReturnsInvalidResponse() client.DefaultRequestHeaders.Add("api-key", ApiKey); // The SSL connection could not be established - await Assert.ThrowsAsync(() => client.GetAsync("/collections")); + await Assert.ThrowsAsync(() => + client.GetAsync("/collections")); } } \ No newline at end of file diff --git a/tests/Testcontainers.Qdrant.Tests/QdrantContainerConfigurationFileTest.cs b/tests/Testcontainers.Qdrant.Tests/QdrantContainerConfigurationFileTest.cs deleted file mode 100644 index 716b92a3a..000000000 --- a/tests/Testcontainers.Qdrant.Tests/QdrantContainerConfigurationFileTest.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System.IO; -using System.Net.Http; - -namespace Testcontainers.Qdrant; - -public sealed class QdrantContainerConfigurationFileTest : IAsyncLifetime -{ - private const string ApiKey = "password!"; - - private readonly QdrantContainer _qdrantContainer = new QdrantBuilder() - .WithConfigFile(CreateConfigFile()) - .Build(); - - private static string CreateConfigFile() - { - var tempFile = Path.GetTempFileName(); - File.WriteAllLines(tempFile, new[] - { - "service:", - $" api_key: {ApiKey}", - }); - return tempFile; - } - - 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 client = new HttpClient - { - 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 client = new HttpClient - { - BaseAddress = new Uri(_qdrantContainer.GetHttpConnectionString()), - }; - - var response = await client.GetAsync("/collections"); - - Assert.False(response.IsSuccessStatusCode); - Assert.Equal("Invalid api-key", await response.Content.ReadAsStringAsync()); - } -} \ No newline at end of file diff --git a/tests/Testcontainers.Qdrant.Tests/QdrantContainerTest.cs b/tests/Testcontainers.Qdrant.Tests/QdrantContainerTest.cs index 5cdb730fc..25e8e0a23 100644 --- a/tests/Testcontainers.Qdrant.Tests/QdrantContainerTest.cs +++ b/tests/Testcontainers.Qdrant.Tests/QdrantContainerTest.cs @@ -1,4 +1,5 @@ using System.Net.Http; +using Qdrant.Client; namespace Testcontainers.Qdrant; @@ -28,4 +29,15 @@ public async Task PingReturnsValidResponse() var response = await client.GetAsync("/"); Assert.True(response.IsSuccessStatusCode); } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public async Task ListCollectionsReturnsValidResponse() + { + var uri = new Uri(_qdrantContainer.GetGrpcConnectionString()); + var client = new QdrantClient(uri.Host, uri.Port); + + var response = await client.ListCollectionsAsync(); + Assert.Empty(response); + } } \ No newline at end of file diff --git a/tests/Testcontainers.Qdrant.Tests/Testcontainers.Qdrant.Tests.csproj b/tests/Testcontainers.Qdrant.Tests/Testcontainers.Qdrant.Tests.csproj index c243a4546..a21c30ba4 100644 --- a/tests/Testcontainers.Qdrant.Tests/Testcontainers.Qdrant.Tests.csproj +++ b/tests/Testcontainers.Qdrant.Tests/Testcontainers.Qdrant.Tests.csproj @@ -1,14 +1,16 @@ - net6.0 + net8.0 false false - - - - + + + + + + diff --git a/tests/Testcontainers.Qdrant.Tests/X509CertificateGenerator.cs b/tests/Testcontainers.Qdrant.Tests/X509CertificateGenerator.cs index ef5f2f6fc..7a131ef96 100644 --- a/tests/Testcontainers.Qdrant.Tests/X509CertificateGenerator.cs +++ b/tests/Testcontainers.Qdrant.Tests/X509CertificateGenerator.cs @@ -17,7 +17,9 @@ namespace Testcontainers.Qdrant; public static class X509CertificateGenerator { - public static X509Certificate2 GenerateCert(string subjectName) + public record PemCertificate(string Certificate, string PrivateKey); + + public static PemCertificate Generate(string subjectName) { var randomGenerator = new CryptoApiRandomGenerator(); var random = new SecureRandom(randomGenerator); @@ -55,13 +57,12 @@ public static X509Certificate2 GenerateCert(string subjectName) using var pemWriter = new PemWriter(writer); pemWriter.WriteObject(certificate); - var pemCert = builder.ToString(); + var cert = builder.ToString(); builder.Clear(); pemWriter.WriteObject(privateKeyInfo); - var pemKey = builder.ToString(); - - return X509Certificate2.CreateFromPem(pemCert, pemKey); + var privateKey = builder.ToString(); + return new PemCertificate(cert, privateKey); } } From 93494d7f555e44c588379e49fa8bdbdf7c87e66e Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Sat, 30 Mar 2024 10:17:49 +1000 Subject: [PATCH 05/14] remove superfluous whitespace --- .../Testcontainers.Qdrant.Tests/X509CertificateGenerator.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/Testcontainers.Qdrant.Tests/X509CertificateGenerator.cs b/tests/Testcontainers.Qdrant.Tests/X509CertificateGenerator.cs index 7a131ef96..72863d7d4 100644 --- a/tests/Testcontainers.Qdrant.Tests/X509CertificateGenerator.cs +++ b/tests/Testcontainers.Qdrant.Tests/X509CertificateGenerator.cs @@ -1,5 +1,4 @@ using System.IO; -using System.Security.Cryptography.X509Certificates; using System.Text; using Org.BouncyCastle.Asn1.X509; using Org.BouncyCastle.Crypto; @@ -64,6 +63,4 @@ public static PemCertificate Generate(string subjectName) var privateKey = builder.ToString(); return new PemCertificate(cert, privateKey); } -} - - +} \ No newline at end of file From cb55a2f3336e3ca4adb6a375033d76ab986726b3 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Thu, 4 Apr 2024 13:26:15 +1000 Subject: [PATCH 06/14] address PR checks --- Directory.Packages.props | 1 + .../QdrantConfiguration.cs | 6 ++-- src/Testcontainers.Qdrant/QdrantContainer.cs | 6 ++++ .../Testcontainers.Qdrant.csproj | 1 - .../QdrantContainerApiKeyCertificateTest.cs | 29 +++++++------------ .../QdrantContainerTest.cs | 3 -- .../Testcontainers.Qdrant.Tests.csproj | 12 ++++---- tests/Testcontainers.Qdrant.Tests/Usings.cs | 21 ++++++++++++++ .../X509CertificateGenerator.cs | 16 +--------- 9 files changed, 48 insertions(+), 47 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 039256594..34c2c98ea 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -57,6 +57,7 @@ + diff --git a/src/Testcontainers.Qdrant/QdrantConfiguration.cs b/src/Testcontainers.Qdrant/QdrantConfiguration.cs index e161db4c2..1dd24ffaf 100644 --- a/src/Testcontainers.Qdrant/QdrantConfiguration.cs +++ b/src/Testcontainers.Qdrant/QdrantConfiguration.cs @@ -57,17 +57,17 @@ public QdrantConfiguration(QdrantConfiguration oldValue, QdrantConfiguration new } /// - /// Gets the API key used to secure Qdrant + /// Gets the API key used to secure Qdrant. /// public string ApiKey { get; } /// - /// Gets the certificate used to configure Transport Layer Security + /// Gets the certificate used to configure Transport Layer Security. Certificate must be in PEM format. /// public string Certificate { get; } /// - /// Gets the private key used to configure Transport Layer Security + /// Gets the private key used to configure Transport Layer Security. Private key must be in PEM format. /// public string PrivateKey { get; } } diff --git a/src/Testcontainers.Qdrant/QdrantContainer.cs b/src/Testcontainers.Qdrant/QdrantContainer.cs index 6eb065088..5531b8ad7 100644 --- a/src/Testcontainers.Qdrant/QdrantContainer.cs +++ b/src/Testcontainers.Qdrant/QdrantContainer.cs @@ -11,6 +11,9 @@ public QdrantContainer(QdrantConfiguration configuration) : base(configuration) _configuration = configuration; } + /// + /// Gets the connection string for connecting to Qdrant REST APIs + /// public string GetHttpConnectionString() { var scheme = _configuration.Certificate != null ? Uri.UriSchemeHttps : Uri.UriSchemeHttp; @@ -18,6 +21,9 @@ public string GetHttpConnectionString() return endpoint.ToString(); } + /// + /// Gets the connection string for connecting to Qdrant gRPC APIs + /// public string GetGrpcConnectionString() { var scheme = _configuration.Certificate != null ? Uri.UriSchemeHttps : Uri.UriSchemeHttp; diff --git a/src/Testcontainers.Qdrant/Testcontainers.Qdrant.csproj b/src/Testcontainers.Qdrant/Testcontainers.Qdrant.csproj index 7f562f0df..83255ca49 100644 --- a/src/Testcontainers.Qdrant/Testcontainers.Qdrant.csproj +++ b/src/Testcontainers.Qdrant/Testcontainers.Qdrant.csproj @@ -4,7 +4,6 @@ latest - diff --git a/tests/Testcontainers.Qdrant.Tests/QdrantContainerApiKeyCertificateTest.cs b/tests/Testcontainers.Qdrant.Tests/QdrantContainerApiKeyCertificateTest.cs index 5ca86430d..e864df58a 100644 --- a/tests/Testcontainers.Qdrant.Tests/QdrantContainerApiKeyCertificateTest.cs +++ b/tests/Testcontainers.Qdrant.Tests/QdrantContainerApiKeyCertificateTest.cs @@ -1,24 +1,16 @@ -using System.Net.Http; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; -using Grpc.Core; -using Grpc.Core.Interceptors; -using Grpc.Net.Client; -using Qdrant.Client; -using Qdrant.Client.Grpc; -using static Testcontainers.Qdrant.X509CertificateGenerator; -using Uri = System.Uri; - namespace Testcontainers.Qdrant; public sealed class QdrantContainerApiKeyCertificateTest : IAsyncLifetime { - private static readonly PemCertificate Cert = Generate("CN=Testcontainers"); + private const string Host = "Testcontainers"; + private const string ApiKey = "password!"; + + private static readonly X509CertificateGenerator.PemCertificate Cert = + X509CertificateGenerator.Generate($"CN={Host}"); private static readonly string Thumbprint = X509Certificate2.CreateFromPem(Cert.Certificate, Cert.PrivateKey) .GetCertHashString(HashAlgorithmName.SHA256); - private const string ApiKey = "password!"; - + private readonly QdrantContainer _qdrantContainer = new QdrantBuilder() .WithApiKey(ApiKey) .WithCertificate(Cert.Certificate, Cert.PrivateKey) @@ -50,7 +42,7 @@ public async Task ListCollectionsReturnsValidResponse() { HttpClient = new HttpClient(httpMessageHandler) { - DefaultRequestHeaders = { Host = "Testcontainers" }, + DefaultRequestHeaders = { Host = Host }, }, }); var callInvoker = channel.Intercept(metadata => @@ -82,7 +74,7 @@ public async Task ListCollectionsWithoutApiKeyReturnsInvalidResponse() { HttpClient = new HttpClient(httpMessageHandler) { - DefaultRequestHeaders = { Host = "Testcontainers" }, + DefaultRequestHeaders = { Host = Host }, }, }); @@ -102,13 +94,12 @@ public async Task ListCollectionsWithoutCertificateValidationReturnsInvalidRespo var client = new HttpClient { BaseAddress = new Uri(_qdrantContainer.GetHttpConnectionString()), - DefaultRequestHeaders = { Host = "Testcontainers" }, + DefaultRequestHeaders = { Host = Host }, }; client.DefaultRequestHeaders.Add("api-key", ApiKey); // The SSL connection could not be established - await Assert.ThrowsAsync(() => - client.GetAsync("/collections")); + await Assert.ThrowsAsync(() => client.GetAsync("/collections")); } } \ No newline at end of file diff --git a/tests/Testcontainers.Qdrant.Tests/QdrantContainerTest.cs b/tests/Testcontainers.Qdrant.Tests/QdrantContainerTest.cs index 25e8e0a23..87e3815e3 100644 --- a/tests/Testcontainers.Qdrant.Tests/QdrantContainerTest.cs +++ b/tests/Testcontainers.Qdrant.Tests/QdrantContainerTest.cs @@ -1,6 +1,3 @@ -using System.Net.Http; -using Qdrant.Client; - namespace Testcontainers.Qdrant; public sealed class QdrantContainerTest : IAsyncLifetime diff --git a/tests/Testcontainers.Qdrant.Tests/Testcontainers.Qdrant.Tests.csproj b/tests/Testcontainers.Qdrant.Tests/Testcontainers.Qdrant.Tests.csproj index a21c30ba4..7b945b9ed 100644 --- a/tests/Testcontainers.Qdrant.Tests/Testcontainers.Qdrant.Tests.csproj +++ b/tests/Testcontainers.Qdrant.Tests/Testcontainers.Qdrant.Tests.csproj @@ -1,16 +1,16 @@ - net8.0 + net8.0 false false - - - - + + + + - + diff --git a/tests/Testcontainers.Qdrant.Tests/Usings.cs b/tests/Testcontainers.Qdrant.Tests/Usings.cs index 58083ebd6..4bf54b735 100644 --- a/tests/Testcontainers.Qdrant.Tests/Usings.cs +++ b/tests/Testcontainers.Qdrant.Tests/Usings.cs @@ -1,4 +1,25 @@ global using System; +global using System.IO; +global using System.Net.Http; +global using System.Security.Cryptography; +global using System.Security.Cryptography.X509Certificates; +global using System.Text; global using System.Threading.Tasks; global using DotNet.Testcontainers.Commons; +global using Grpc.Core; +global using Grpc.Core.Interceptors; +global using Grpc.Net.Client; +global using Org.BouncyCastle.Asn1.X509; +global using Org.BouncyCastle.Crypto; +global using Org.BouncyCastle.Crypto.Generators; +global using Org.BouncyCastle.Crypto.Operators; +global using Org.BouncyCastle.Crypto.Prng; +global using Org.BouncyCastle.Math; +global using Org.BouncyCastle.OpenSsl; +global using Org.BouncyCastle.Pkcs; +global using Org.BouncyCastle.Security; +global using Org.BouncyCastle.Utilities; +global using Org.BouncyCastle.X509; +global using Qdrant.Client; +global using Qdrant.Client.Grpc; global using Xunit; \ No newline at end of file diff --git a/tests/Testcontainers.Qdrant.Tests/X509CertificateGenerator.cs b/tests/Testcontainers.Qdrant.Tests/X509CertificateGenerator.cs index 72863d7d4..9b10027f0 100644 --- a/tests/Testcontainers.Qdrant.Tests/X509CertificateGenerator.cs +++ b/tests/Testcontainers.Qdrant.Tests/X509CertificateGenerator.cs @@ -1,18 +1,4 @@ -using System.IO; -using System.Text; -using Org.BouncyCastle.Asn1.X509; -using Org.BouncyCastle.Crypto; -using Org.BouncyCastle.Crypto.Generators; -using Org.BouncyCastle.Crypto.Operators; -using Org.BouncyCastle.Crypto.Prng; -using Org.BouncyCastle.Math; -using Org.BouncyCastle.OpenSsl; -using Org.BouncyCastle.Pkcs; -using Org.BouncyCastle.Security; -using Org.BouncyCastle.Utilities; -using Org.BouncyCastle.X509; - -namespace Testcontainers.Qdrant; +namespace Testcontainers.Qdrant; public static class X509CertificateGenerator { From ca0b39b9c82555ddd00b4c210e2fc70b4acb876b Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Sat, 6 Apr 2024 16:28:36 +1000 Subject: [PATCH 07/14] Use readyz endpoint This commit moves the wait for strategy to use the readyz endpoint. If a certificate has been specified, perform the check with TLS and allow any certificate to pass validation. --- src/Testcontainers.Qdrant/QdrantBuilder.cs | 28 ++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/Testcontainers.Qdrant/QdrantBuilder.cs b/src/Testcontainers.Qdrant/QdrantBuilder.cs index a4b8e6d75..10e96e6f7 100644 --- a/src/Testcontainers.Qdrant/QdrantBuilder.cs +++ b/src/Testcontainers.Qdrant/QdrantBuilder.cs @@ -1,3 +1,6 @@ +using System.Linq; +using System.Net.Http; + namespace Testcontainers.Qdrant; /// @@ -48,7 +51,26 @@ public QdrantBuilder WithCertificate(string certificate, string privateKey) public override QdrantContainer Build() { Validate(); - return new QdrantContainer(DockerResourceConfiguration); + + 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); } /// @@ -56,9 +78,7 @@ 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.*")); + .WithPortBinding(QdrantGrpcPort, true); /// protected override QdrantBuilder Clone(IResourceConfiguration resourceConfiguration) => From da10ea1b653806b4475d6dd00bea3792e3ae8426 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Sat, 6 Apr 2024 16:31:25 +1000 Subject: [PATCH 08/14] Also target net462 The qdrant gRPC client uses .NET Framework build of Grpc.Net.Client, which uses WinHttpHandler. The wiring up is easier on net462 if net462 is targeted specifically. Same reason why the Milvus container also targets net462 (https://github.com/testcontainers/testcontainers-dotnet/pull/1131#discussion_r1507712752). --- src/Testcontainers.Qdrant/Testcontainers.Qdrant.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Testcontainers.Qdrant/Testcontainers.Qdrant.csproj b/src/Testcontainers.Qdrant/Testcontainers.Qdrant.csproj index 83255ca49..85b539f53 100644 --- a/src/Testcontainers.Qdrant/Testcontainers.Qdrant.csproj +++ b/src/Testcontainers.Qdrant/Testcontainers.Qdrant.csproj @@ -1,6 +1,6 @@ - net6.0;net8.0;netstandard2.0;netstandard2.1 + net6.0;net8.0;netstandard2.0;netstandard2.1;net462 latest From bb42811380a02acadabe728160189bd6fa5956ff Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Sun, 7 Apr 2024 09:36:07 +0200 Subject: [PATCH 09/14] chore: Order csproj in sln --- Testcontainers.sln | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Testcontainers.sln b/Testcontainers.sln index 7110f52bf..46bb2474b 100644 --- a/Testcontainers.sln +++ b/Testcontainers.sln @@ -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}" @@ -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}" @@ -195,10 +199,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Tests", "tes EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.WebDriver.Tests", "tests\Testcontainers.WebDriver.Tests\Testcontainers.WebDriver.Tests.csproj", "{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}" 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.Qdrant.Tests", "tests\Testcontainers.Qdrant.Tests\Testcontainers.Qdrant.Tests.csproj", "{9DCE3E7F-B341-4AD0-BAAA-C3B91EB91B0D}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -352,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 @@ -540,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 @@ -572,14 +580,6 @@ Global {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Debug|Any CPU.Build.0 = Debug|Any CPU {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Release|Any CPU.ActiveCfg = Release|Any CPU {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.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 - {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 EndGlobalSection GlobalSection(NestedProjects) = preSolution {5365F780-0E6C-41F0-B1B9-7DC34368F80C} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} @@ -618,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} @@ -665,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} @@ -673,7 +675,5 @@ Global {1A1983E6-5297-435F-B467-E8E1F11277D6} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {27CDB869-A150-4593-958F-6F26E5391E7C} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} - {7C98973D-53D7-49F9-BDFE-E3268F402584} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} - {9DCE3E7F-B341-4AD0-BAAA-C3B91EB91B0D} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} EndGlobalSection EndGlobal From 21a3439a47030b8573be8391d0b90ac5880d569d Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Sun, 7 Apr 2024 09:36:29 +0200 Subject: [PATCH 10/14] chore: Replace SolutionDir with relative path --- src/Testcontainers.Qdrant/Testcontainers.Qdrant.csproj | 2 +- .../Testcontainers.Qdrant.Tests.csproj | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Testcontainers.Qdrant/Testcontainers.Qdrant.csproj b/src/Testcontainers.Qdrant/Testcontainers.Qdrant.csproj index 85b539f53..51735310a 100644 --- a/src/Testcontainers.Qdrant/Testcontainers.Qdrant.csproj +++ b/src/Testcontainers.Qdrant/Testcontainers.Qdrant.csproj @@ -7,6 +7,6 @@ - + \ No newline at end of file diff --git a/tests/Testcontainers.Qdrant.Tests/Testcontainers.Qdrant.Tests.csproj b/tests/Testcontainers.Qdrant.Tests/Testcontainers.Qdrant.Tests.csproj index 7b945b9ed..631b9c4b7 100644 --- a/tests/Testcontainers.Qdrant.Tests/Testcontainers.Qdrant.Tests.csproj +++ b/tests/Testcontainers.Qdrant.Tests/Testcontainers.Qdrant.Tests.csproj @@ -13,7 +13,7 @@ - - + + \ No newline at end of file From f662ec65af724aa3b58fccc57a1a060165218c80 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Sun, 7 Apr 2024 09:36:42 +0200 Subject: [PATCH 11/14] chore: Remove BOM --- .../X509CertificateGenerator.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/Testcontainers.Qdrant.Tests/X509CertificateGenerator.cs b/tests/Testcontainers.Qdrant.Tests/X509CertificateGenerator.cs index 9b10027f0..b78ff0c02 100644 --- a/tests/Testcontainers.Qdrant.Tests/X509CertificateGenerator.cs +++ b/tests/Testcontainers.Qdrant.Tests/X509CertificateGenerator.cs @@ -1,15 +1,15 @@ -namespace Testcontainers.Qdrant; +namespace Testcontainers.Qdrant; public static class X509CertificateGenerator { public record PemCertificate(string Certificate, string PrivateKey); - + public static PemCertificate Generate(string subjectName) { var randomGenerator = new CryptoApiRandomGenerator(); var random = new SecureRandom(randomGenerator); var serialNumber = BigIntegers.CreateRandomInRange( - BigInteger.One, + BigInteger.One, BigInteger.ValueOf(long.MaxValue), random); var subjectDistinguishedName = new X509Name(subjectName); var issuerDistinguishedName = subjectDistinguishedName; @@ -20,31 +20,31 @@ public static PemCertificate Generate(string subjectName) keyPairGenerator.Init(keyGenerationParameters); var subjectKeyPair = keyPairGenerator.GenerateKeyPair(); var issuerPrivateKey = subjectKeyPair.Private; - + var certificateGenerator = new X509V3CertificateGenerator(); certificateGenerator.SetSerialNumber(serialNumber); certificateGenerator.AddExtension( - X509Extensions.ExtendedKeyUsage, - true, + X509Extensions.ExtendedKeyUsage, + true, new ExtendedKeyUsage(KeyPurposeID.id_kp_serverAuth)); certificateGenerator.SetIssuerDN(issuerDistinguishedName); certificateGenerator.SetSubjectDN(subjectDistinguishedName); certificateGenerator.SetNotBefore(notBefore); certificateGenerator.SetNotAfter(notAfter); certificateGenerator.SetPublicKey(subjectKeyPair.Public); - + var signatureFactory = new Asn1SignatureFactory("SHA512WITHRSA", issuerPrivateKey, random); var certificate = certificateGenerator.Generate(signatureFactory); var privateKeyInfo = PrivateKeyInfoFactory.CreatePrivateKeyInfo(subjectKeyPair.Private); - + var builder = new StringBuilder(); using var writer = new StringWriter(builder); using var pemWriter = new PemWriter(writer); - + pemWriter.WriteObject(certificate); var cert = builder.ToString(); builder.Clear(); - + pemWriter.WriteObject(privateKeyInfo); var privateKey = builder.ToString(); return new PemCertificate(cert, privateKey); From 331abc561085ae13790f94af3b46453e1dc7cb17 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Sun, 7 Apr 2024 09:38:18 +0200 Subject: [PATCH 12/14] chore: Fix minor repo inconsistencies --- src/Testcontainers.Qdrant/QdrantBuilder.cs | 179 +++++++++--------- .../QdrantConfiguration.cs | 122 ++++++------ src/Testcontainers.Qdrant/QdrantContainer.cs | 54 +++--- src/Testcontainers.Qdrant/Usings.cs | 5 +- 4 files changed, 178 insertions(+), 182 deletions(-) diff --git a/src/Testcontainers.Qdrant/QdrantBuilder.cs b/src/Testcontainers.Qdrant/QdrantBuilder.cs index 10e96e6f7..07e388c88 100644 --- a/src/Testcontainers.Qdrant/QdrantBuilder.cs +++ b/src/Testcontainers.Qdrant/QdrantBuilder.cs @@ -1,97 +1,94 @@ -using System.Linq; -using System.Net.Http; - namespace Testcontainers.Qdrant; /// [PublicAPI] public sealed class QdrantBuilder : ContainerBuilder { - 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; - - /// - /// The API key used to secure the instance. A certificate and private key should also be - /// provided to to enable Transport Layer Security (TLS). - /// - /// The API key - public QdrantBuilder WithApiKey(string apiKey) => - Merge(DockerResourceConfiguration, new QdrantConfiguration(apiKey: apiKey)) - .WithEnvironment("QDRANT__SERVICE__API_KEY", apiKey); - - /// - /// A certificate and private key to enable Transport Layer Security (TLS). - /// - /// A public certificate in PEM format - /// A private key for the certificate in PEM format - 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); - } - - /// - 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); - } - - /// - protected override QdrantBuilder Init() => - base.Init() - .WithImage(QdrantImage) - .WithPortBinding(QdrantHttpPort, true) - .WithPortBinding(QdrantGrpcPort, true); - - /// - protected override QdrantBuilder Clone(IResourceConfiguration resourceConfiguration) => - Merge(DockerResourceConfiguration, new QdrantConfiguration(resourceConfiguration)); - - /// - protected override QdrantBuilder Merge(QdrantConfiguration oldValue, QdrantConfiguration newValue) => - new(new QdrantConfiguration(oldValue, newValue)); - - /// - protected override QdrantConfiguration DockerResourceConfiguration { get; } - - /// - protected override QdrantBuilder Clone(IContainerConfiguration resourceConfiguration) => - Merge(DockerResourceConfiguration, new QdrantConfiguration(resourceConfiguration)); -} + 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; + + /// + /// The API key used to secure the instance. A certificate and private key should also be + /// provided to to enable Transport Layer Security (TLS). + /// + /// The API key + public QdrantBuilder WithApiKey(string apiKey) => + Merge(DockerResourceConfiguration, new QdrantConfiguration(apiKey: apiKey)) + .WithEnvironment("QDRANT__SERVICE__API_KEY", apiKey); + + /// + /// A certificate and private key to enable Transport Layer Security (TLS). + /// + /// A public certificate in PEM format + /// A private key for the certificate in PEM format + 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); + } + + /// + 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); + } + + /// + protected override QdrantBuilder Init() => + base.Init() + .WithImage(QdrantImage) + .WithPortBinding(QdrantHttpPort, true) + .WithPortBinding(QdrantGrpcPort, true); + + /// + protected override QdrantBuilder Clone(IResourceConfiguration resourceConfiguration) => + Merge(DockerResourceConfiguration, new QdrantConfiguration(resourceConfiguration)); + + /// + protected override QdrantBuilder Merge(QdrantConfiguration oldValue, QdrantConfiguration newValue) => + new(new QdrantConfiguration(oldValue, newValue)); + + /// + protected override QdrantConfiguration DockerResourceConfiguration { get; } + + /// + protected override QdrantBuilder Clone(IContainerConfiguration resourceConfiguration) => + Merge(DockerResourceConfiguration, new QdrantConfiguration(resourceConfiguration)); +} \ No newline at end of file diff --git a/src/Testcontainers.Qdrant/QdrantConfiguration.cs b/src/Testcontainers.Qdrant/QdrantConfiguration.cs index 1dd24ffaf..1f80db5f4 100644 --- a/src/Testcontainers.Qdrant/QdrantConfiguration.cs +++ b/src/Testcontainers.Qdrant/QdrantConfiguration.cs @@ -1,73 +1,71 @@ -using static DotNet.Testcontainers.Builders.BuildConfiguration; - namespace Testcontainers.Qdrant; /// [PublicAPI] public sealed class QdrantConfiguration : ContainerConfiguration { - /// - /// Initializes a new instance of the class. - /// - public QdrantConfiguration(string apiKey = null, string certificate = null, string privateKey = null) - { - ApiKey = apiKey; - Certificate = certificate; - PrivateKey = privateKey; - } + /// + /// Initializes a new instance of the class. + /// + public QdrantConfiguration(string apiKey = null, string certificate = null, string privateKey = null) + { + ApiKey = apiKey; + Certificate = certificate; + PrivateKey = privateKey; + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public QdrantConfiguration(IResourceConfiguration resourceConfiguration) + : base(resourceConfiguration) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public QdrantConfiguration(IContainerConfiguration resourceConfiguration) + : base(resourceConfiguration) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The Docker resource configuration. - public QdrantConfiguration(IResourceConfiguration resourceConfiguration) - : base(resourceConfiguration) - { - } + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public QdrantConfiguration(QdrantConfiguration resourceConfiguration) + : this(new QdrantConfiguration(), resourceConfiguration) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The Docker resource configuration. - public QdrantConfiguration(IContainerConfiguration resourceConfiguration) - : base(resourceConfiguration) - { - } + /// + /// Initializes a new instance of the class. + /// + /// The old Docker resource configuration. + /// The new Docker resource configuration. + 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); + } - /// - /// Initializes a new instance of the class. - /// - /// The Docker resource configuration. - public QdrantConfiguration(QdrantConfiguration resourceConfiguration) - : this(new QdrantConfiguration(), resourceConfiguration) - { - } + /// + /// Gets the API key used to secure Qdrant. + /// + public string ApiKey { get; } - /// - /// Initializes a new instance of the class. - /// - /// The old Docker resource configuration. - /// The new Docker resource configuration. - public QdrantConfiguration(QdrantConfiguration oldValue, QdrantConfiguration newValue) - : base(oldValue, newValue) - { - ApiKey = Combine(oldValue.ApiKey, newValue.ApiKey); - Certificate = Combine(oldValue.Certificate, newValue.Certificate); - PrivateKey = Combine(oldValue.PrivateKey, newValue.PrivateKey); - } - - /// - /// Gets the API key used to secure Qdrant. - /// - public string ApiKey { get; } - - /// - /// Gets the certificate used to configure Transport Layer Security. Certificate must be in PEM format. - /// - public string Certificate { get; } + /// + /// Gets the certificate used to configure Transport Layer Security. Certificate must be in PEM format. + /// + public string Certificate { get; } - /// - /// Gets the private key used to configure Transport Layer Security. Private key must be in PEM format. - /// - public string PrivateKey { get; } -} + /// + /// Gets the private key used to configure Transport Layer Security. Private key must be in PEM format. + /// + public string PrivateKey { get; } +} \ No newline at end of file diff --git a/src/Testcontainers.Qdrant/QdrantContainer.cs b/src/Testcontainers.Qdrant/QdrantContainer.cs index 5531b8ad7..0851188c5 100644 --- a/src/Testcontainers.Qdrant/QdrantContainer.cs +++ b/src/Testcontainers.Qdrant/QdrantContainer.cs @@ -2,32 +2,32 @@ namespace Testcontainers.Qdrant; /// [PublicAPI] -public class QdrantContainer : DockerContainer +public sealed class QdrantContainer : DockerContainer { - private readonly QdrantConfiguration _configuration; + private readonly QdrantConfiguration _configuration; - public QdrantContainer(QdrantConfiguration configuration) : base(configuration) - { - _configuration = configuration; - } - - /// - /// Gets the connection string for connecting to Qdrant REST APIs - /// - public string GetHttpConnectionString() - { - var scheme = _configuration.Certificate != null ? Uri.UriSchemeHttps : Uri.UriSchemeHttp; - var endpoint = new UriBuilder(scheme, Hostname, GetMappedPublicPort(QdrantBuilder.QdrantHttpPort)); - return endpoint.ToString(); - } - - /// - /// Gets the connection string for connecting to Qdrant gRPC APIs - /// - public string GetGrpcConnectionString() - { - var scheme = _configuration.Certificate != null ? Uri.UriSchemeHttps : Uri.UriSchemeHttp; - var endpoint = new UriBuilder(scheme, Hostname, GetMappedPublicPort(QdrantBuilder.QdrantGrpcPort)); - return endpoint.ToString(); - } -} + public QdrantContainer(QdrantConfiguration configuration) : base(configuration) + { + _configuration = configuration; + } + + /// + /// Gets the connection string for connecting to Qdrant REST APIs + /// + public string GetHttpConnectionString() + { + var scheme = _configuration.Certificate != null ? Uri.UriSchemeHttps : Uri.UriSchemeHttp; + var endpoint = new UriBuilder(scheme, Hostname, GetMappedPublicPort(QdrantBuilder.QdrantHttpPort)); + return endpoint.ToString(); + } + + /// + /// Gets the connection string for connecting to Qdrant gRPC APIs + /// + public string GetGrpcConnectionString() + { + var scheme = _configuration.Certificate != null ? Uri.UriSchemeHttps : Uri.UriSchemeHttp; + var endpoint = new UriBuilder(scheme, Hostname, GetMappedPublicPort(QdrantBuilder.QdrantGrpcPort)); + return endpoint.ToString(); + } +} \ No newline at end of file diff --git a/src/Testcontainers.Qdrant/Usings.cs b/src/Testcontainers.Qdrant/Usings.cs index fd93092b4..b2b9653ef 100644 --- a/src/Testcontainers.Qdrant/Usings.cs +++ b/src/Testcontainers.Qdrant/Usings.cs @@ -1,8 +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; -global using Microsoft.Extensions.Logging; \ No newline at end of file +global using JetBrains.Annotations; \ No newline at end of file From 96be0c74539326707b7972bf165059ec8a739e4c Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Mon, 8 Apr 2024 21:14:43 +1000 Subject: [PATCH 13/14] Use CertificateRequest to generate PEM certificate --- .../PemCertificate.cs | 24 +++++++++ .../QdrantContainerApiKeyCertificateTest.cs | 13 ++--- .../Testcontainers.Qdrant.Tests.csproj | 1 - tests/Testcontainers.Qdrant.Tests/Usings.cs | 13 ----- .../X509CertificateGenerator.cs | 52 ------------------- 5 files changed, 28 insertions(+), 75 deletions(-) create mode 100644 tests/Testcontainers.Qdrant.Tests/PemCertificate.cs delete mode 100644 tests/Testcontainers.Qdrant.Tests/X509CertificateGenerator.cs diff --git a/tests/Testcontainers.Qdrant.Tests/PemCertificate.cs b/tests/Testcontainers.Qdrant.Tests/PemCertificate.cs new file mode 100644 index 000000000..5b5471dd6 --- /dev/null +++ b/tests/Testcontainers.Qdrant.Tests/PemCertificate.cs @@ -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)); + } +} \ No newline at end of file diff --git a/tests/Testcontainers.Qdrant.Tests/QdrantContainerApiKeyCertificateTest.cs b/tests/Testcontainers.Qdrant.Tests/QdrantContainerApiKeyCertificateTest.cs index e864df58a..f6b62e1a7 100644 --- a/tests/Testcontainers.Qdrant.Tests/QdrantContainerApiKeyCertificateTest.cs +++ b/tests/Testcontainers.Qdrant.Tests/QdrantContainerApiKeyCertificateTest.cs @@ -4,13 +4,8 @@ public sealed class QdrantContainerApiKeyCertificateTest : IAsyncLifetime { private const string Host = "Testcontainers"; private const string ApiKey = "password!"; - - private static readonly X509CertificateGenerator.PemCertificate Cert = - X509CertificateGenerator.Generate($"CN={Host}"); - private static readonly string Thumbprint = - X509Certificate2.CreateFromPem(Cert.Certificate, Cert.PrivateKey) - .GetCertHashString(HashAlgorithmName.SHA256); - + private static readonly PemCertificate Cert = PemCertificate.Create(Host); + private readonly QdrantContainer _qdrantContainer = new QdrantBuilder() .WithApiKey(ApiKey) .WithCertificate(Cert.Certificate, Cert.PrivateKey) @@ -33,7 +28,7 @@ public async Task ListCollectionsReturnsValidResponse() var httpMessageHandler = new HttpClientHandler { ServerCertificateCustomValidationCallback = - CertificateValidation.Thumbprint(Thumbprint), + CertificateValidation.Thumbprint(Cert.Thumbprint), }; var channel = GrpcChannel.ForAddress( @@ -65,7 +60,7 @@ public async Task ListCollectionsWithoutApiKeyReturnsInvalidResponse() var httpMessageHandler = new HttpClientHandler { ServerCertificateCustomValidationCallback = - CertificateValidation.Thumbprint(Thumbprint) + CertificateValidation.Thumbprint(Cert.Thumbprint) }; var channel = GrpcChannel.ForAddress( diff --git a/tests/Testcontainers.Qdrant.Tests/Testcontainers.Qdrant.Tests.csproj b/tests/Testcontainers.Qdrant.Tests/Testcontainers.Qdrant.Tests.csproj index 7b945b9ed..c9916359a 100644 --- a/tests/Testcontainers.Qdrant.Tests/Testcontainers.Qdrant.Tests.csproj +++ b/tests/Testcontainers.Qdrant.Tests/Testcontainers.Qdrant.Tests.csproj @@ -9,7 +9,6 @@ - diff --git a/tests/Testcontainers.Qdrant.Tests/Usings.cs b/tests/Testcontainers.Qdrant.Tests/Usings.cs index 4bf54b735..abf2356ac 100644 --- a/tests/Testcontainers.Qdrant.Tests/Usings.cs +++ b/tests/Testcontainers.Qdrant.Tests/Usings.cs @@ -1,25 +1,12 @@ global using System; -global using System.IO; global using System.Net.Http; global using System.Security.Cryptography; global using System.Security.Cryptography.X509Certificates; -global using System.Text; global using System.Threading.Tasks; global using DotNet.Testcontainers.Commons; global using Grpc.Core; global using Grpc.Core.Interceptors; global using Grpc.Net.Client; -global using Org.BouncyCastle.Asn1.X509; -global using Org.BouncyCastle.Crypto; -global using Org.BouncyCastle.Crypto.Generators; -global using Org.BouncyCastle.Crypto.Operators; -global using Org.BouncyCastle.Crypto.Prng; -global using Org.BouncyCastle.Math; -global using Org.BouncyCastle.OpenSsl; -global using Org.BouncyCastle.Pkcs; -global using Org.BouncyCastle.Security; -global using Org.BouncyCastle.Utilities; -global using Org.BouncyCastle.X509; global using Qdrant.Client; global using Qdrant.Client.Grpc; global using Xunit; \ No newline at end of file diff --git a/tests/Testcontainers.Qdrant.Tests/X509CertificateGenerator.cs b/tests/Testcontainers.Qdrant.Tests/X509CertificateGenerator.cs deleted file mode 100644 index 9b10027f0..000000000 --- a/tests/Testcontainers.Qdrant.Tests/X509CertificateGenerator.cs +++ /dev/null @@ -1,52 +0,0 @@ -namespace Testcontainers.Qdrant; - -public static class X509CertificateGenerator -{ - public record PemCertificate(string Certificate, string PrivateKey); - - public static PemCertificate Generate(string subjectName) - { - var randomGenerator = new CryptoApiRandomGenerator(); - var random = new SecureRandom(randomGenerator); - var serialNumber = BigIntegers.CreateRandomInRange( - BigInteger.One, - BigInteger.ValueOf(long.MaxValue), random); - var subjectDistinguishedName = new X509Name(subjectName); - var issuerDistinguishedName = subjectDistinguishedName; - var notBefore = DateTime.UtcNow.Date; - var notAfter = notBefore.AddYears(1); - var keyGenerationParameters = new KeyGenerationParameters(random, 2048); - var keyPairGenerator = new RsaKeyPairGenerator(); - keyPairGenerator.Init(keyGenerationParameters); - var subjectKeyPair = keyPairGenerator.GenerateKeyPair(); - var issuerPrivateKey = subjectKeyPair.Private; - - var certificateGenerator = new X509V3CertificateGenerator(); - certificateGenerator.SetSerialNumber(serialNumber); - certificateGenerator.AddExtension( - X509Extensions.ExtendedKeyUsage, - true, - new ExtendedKeyUsage(KeyPurposeID.id_kp_serverAuth)); - certificateGenerator.SetIssuerDN(issuerDistinguishedName); - certificateGenerator.SetSubjectDN(subjectDistinguishedName); - certificateGenerator.SetNotBefore(notBefore); - certificateGenerator.SetNotAfter(notAfter); - certificateGenerator.SetPublicKey(subjectKeyPair.Public); - - var signatureFactory = new Asn1SignatureFactory("SHA512WITHRSA", issuerPrivateKey, random); - var certificate = certificateGenerator.Generate(signatureFactory); - var privateKeyInfo = PrivateKeyInfoFactory.CreatePrivateKeyInfo(subjectKeyPair.Private); - - var builder = new StringBuilder(); - using var writer = new StringWriter(builder); - using var pemWriter = new PemWriter(writer); - - pemWriter.WriteObject(certificate); - var cert = builder.ToString(); - builder.Clear(); - - pemWriter.WriteObject(privateKeyInfo); - var privateKey = builder.ToString(); - return new PemCertificate(cert, privateKey); - } -} \ No newline at end of file From f31250ef9c3932ccc050a9ec3b22aea1f5ab9ed5 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Mon, 8 Apr 2024 21:18:29 +1000 Subject: [PATCH 14/14] remove BOM --- tests/Testcontainers.Qdrant.Tests/PemCertificate.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Testcontainers.Qdrant.Tests/PemCertificate.cs b/tests/Testcontainers.Qdrant.Tests/PemCertificate.cs index 5b5471dd6..661052e05 100644 --- a/tests/Testcontainers.Qdrant.Tests/PemCertificate.cs +++ b/tests/Testcontainers.Qdrant.Tests/PemCertificate.cs @@ -1,4 +1,4 @@ -namespace Testcontainers.Qdrant; +namespace Testcontainers.Qdrant; public record PemCertificate(string Certificate, string PrivateKey, string Thumbprint) {