-
-
Notifications
You must be signed in to change notification settings - Fork 340
Add PostgreSQL SSL configuration example and tests #1529
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
base: develop
Are you sure you want to change the base?
Changes from all commits
e18d777
3e427c9
f038ada
a874c75
99f7596
63733f6
955edb3
f6400da
f4ab1cd
13420f5
67e9694
b288bc1
26c4892
5666f22
dab0069
897ba81
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 |
|---|---|---|
|
|
@@ -2,7 +2,8 @@ namespace Testcontainers.PostgreSql; | |
|
|
||
| /// <inheritdoc cref="ContainerBuilder{TBuilderEntity, TContainerEntity, TConfigurationEntity}" /> | ||
| [PublicAPI] | ||
| public sealed class PostgreSqlBuilder : ContainerBuilder<PostgreSqlBuilder, PostgreSqlContainer, PostgreSqlConfiguration> | ||
| public sealed class | ||
| PostgreSqlBuilder : ContainerBuilder<PostgreSqlBuilder, PostgreSqlContainer, PostgreSqlConfiguration> | ||
| { | ||
| public const string PostgreSqlImage = "postgres:15.1"; | ||
|
|
||
|
|
@@ -14,6 +15,8 @@ public sealed class PostgreSqlBuilder : ContainerBuilder<PostgreSqlBuilder, Post | |
|
|
||
| public const string DefaultPassword = "postgres"; | ||
|
|
||
| private const string DefaultCertificatesDirectory = "/var/lib/postgresql/certs"; | ||
|
|
||
| /// <summary> | ||
| /// Initializes a new instance of the <see cref="PostgreSqlBuilder" /> class. | ||
| /// </summary> | ||
|
|
@@ -69,14 +72,122 @@ public PostgreSqlBuilder WithPassword(string password) | |
| .WithEnvironment("POSTGRES_PASSWORD", password); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Sets the PostgreSql SSL mode. | ||
| /// </summary> | ||
| /// <param name="sslMode">The PostgreSql SSL mode.</param> | ||
| /// <returns>A configured instance of <see cref="PostgreSqlBuilder" />.</returns> | ||
| public PostgreSqlBuilder WithSslMode(SslMode sslMode) | ||
| { | ||
| return Merge(DockerResourceConfiguration, new PostgreSqlConfiguration(sslMode: sslMode)) | ||
| .WithEnvironment("PGSSLMODE", sslMode.ToString().ToLowerInvariant()); | ||
| } | ||
|
Comment on lines
+75
to
+84
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. 🧩 Analysis chainVerify SslMode enum values match PostgreSQL expectations. The implementation converts the 🏁 Script executed: #!/bin/bash
# Verify SslMode enum definition matches PostgreSQL SSL mode values
ast-grep --pattern 'enum SslMode {
$$$
}'Length of output: 0 Let me search more broadly for the Length of output: 62 🏁 Script executed: #!/bin/bash
# Also search for where SslMode is used/imported in the PostgreSqlBuilder file
rg -n "SslMode" src/Testcontainers.PostgreSql/PostgreSqlBuilder.csLength of output: 60 🏁 Script executed: #!/bin/bash
# Find all C# files that might contain SslMode definition
fd -e cs --exec rg -l "SslMode" {} \;Length of output: 282 🏁 Script executed: #!/bin/bash
# Read the SslMode enum definition
cat src/Testcontainers.PostgreSql/SslMode.csLength of output: 610 🌐 Web query: 💡 Result: Valid PGSSLMODE (sslmode) values:
Fix enum value conversion for The current implementation has a critical issue: enum values Solution: Either (1) rename the enum values to 🤖 Prompt for AI Agents |
||
|
|
||
| /// <summary> | ||
| /// Sets the PostgreSql root certificate file. | ||
| /// </summary> | ||
| /// <param name="rootCertFile">The path to the root certificate file.</param> | ||
| /// <returns>A configured instance of <see cref="PostgreSqlBuilder" />.</returns> | ||
| public PostgreSqlBuilder WithRootCertificate(string rootCertFile) | ||
| { | ||
| return Merge(DockerResourceConfiguration, new PostgreSqlConfiguration(rootCertFile: rootCertFile)) | ||
| .WithBindMount(rootCertFile, Path.Combine(DefaultCertificatesDirectory, "root.crt"), AccessMode.ReadOnly) | ||
| .WithEnvironment("PGSSLROOTCERT", Path.Combine(DefaultCertificatesDirectory, "root.crt")); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Sets the PostgreSql client certificate and key files. | ||
| /// </summary> | ||
| /// <param name="clientCertFile">The path to the client certificate file.</param> | ||
| /// <param name="clientKeyFile">The path to the client key file.</param> | ||
| /// <returns>A configured instance of <see cref="PostgreSqlBuilder" />.</returns> | ||
| public PostgreSqlBuilder WithClientCertificate(string clientCertFile, string clientKeyFile) | ||
| { | ||
| return Merge(DockerResourceConfiguration, | ||
| new PostgreSqlConfiguration(clientCertFile: clientCertFile, clientKeyFile: clientKeyFile)) | ||
| .WithBindMount(clientCertFile, Path.Combine(DefaultCertificatesDirectory, "postgresql.crt"), | ||
| AccessMode.ReadOnly) | ||
| .WithBindMount(clientKeyFile, Path.Combine(DefaultCertificatesDirectory, "postgresql.key"), | ||
| AccessMode.ReadOnly) | ||
| .WithEnvironment("PGSSLCERT", Path.Combine(DefaultCertificatesDirectory, "postgresql.crt")) | ||
| .WithEnvironment("PGSSLKEY", Path.Combine(DefaultCertificatesDirectory, "postgresql.key")); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Configures the PostgreSQL server to run with SSL using the provided CA certificate, server certificate and private key. | ||
| /// This enables server-side SSL configuration with client certificate authentication. | ||
| /// </summary> | ||
| /// <param name="caCertFile">The path to the CA certificate file.</param> | ||
| /// <param name="serverCertFile">The path to the server certificate file.</param> | ||
| /// <param name="serverKeyFile">The path to the server private key file.</param> | ||
| /// <returns>A configured instance of <see cref="PostgreSqlBuilder" />.</returns> | ||
| /// <remarks> | ||
| /// This method configures PostgreSQL for server-side SSL with client certificate authentication. | ||
| /// It requires a custom PostgreSQL configuration file that enables SSL and sets the appropriate | ||
| /// certificate paths. The certificates are mounted into the container and PostgreSQL is configured | ||
| /// to use them for SSL connections. | ||
| /// </remarks> | ||
| public PostgreSqlBuilder WithSSLSettings(string caCertFile, string serverCertFile, string serverKeyFile) | ||
| { | ||
| if (string.IsNullOrWhiteSpace(caCertFile)) | ||
| { | ||
| throw new ArgumentException("CA certificate file path cannot be null or empty.", nameof(caCertFile)); | ||
| } | ||
|
|
||
| if (string.IsNullOrWhiteSpace(serverCertFile)) | ||
| { | ||
| throw new ArgumentException("Server certificate file path cannot be null or empty.", | ||
| nameof(serverCertFile)); | ||
| } | ||
|
|
||
| if (string.IsNullOrWhiteSpace(serverKeyFile)) | ||
| { | ||
| throw new ArgumentException("Server key file path cannot be null or empty.", nameof(serverKeyFile)); | ||
| } | ||
|
|
||
| const string sslConfigDir = "/tmp/testcontainers-dotnet/postgres"; | ||
|
|
||
| var wrapperEntrypoint = @"#!/bin/sh | ||
| set -e | ||
| SSL_DIR=/tmp/testcontainers-dotnet/postgres | ||
| # Fix ownership and permissions for SSL key/cert before Postgres init runs | ||
| if [ -f ""$SSL_DIR/server.key"" ]; then | ||
| chown postgres:postgres ""$SSL_DIR/server.key"" || true | ||
| chmod 600 ""$SSL_DIR/server.key"" || true | ||
| fi | ||
| if [ -f ""$SSL_DIR/server.crt"" ]; then | ||
| chown postgres:postgres ""$SSL_DIR/server.crt"" || true | ||
| fi | ||
| if [ -f ""$SSL_DIR/ca_cert.pem"" ]; then | ||
| chown postgres:postgres ""$SSL_DIR/ca_cert.pem"" || true | ||
| fi | ||
| exec /usr/local/bin/docker-entrypoint.sh ""$@"" | ||
| "; | ||
|
Comment on lines
+150
to
+165
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. Consider the implications of suppressing permission errors. The shell script uses Consider either:
|
||
|
|
||
| return Merge(DockerResourceConfiguration, new PostgreSqlConfiguration( | ||
| serverCertFile: serverCertFile, | ||
| serverKeyFile: serverKeyFile, | ||
| caCertFile: caCertFile)) | ||
| .WithResourceMapping(File.ReadAllBytes(caCertFile), $"{sslConfigDir}/ca_cert.pem", fileMode: Unix.FileMode644) | ||
| .WithResourceMapping(File.ReadAllBytes(serverCertFile), $"{sslConfigDir}/server.crt", fileMode: Unix.FileMode644) | ||
| .WithResourceMapping(File.ReadAllBytes(serverKeyFile), $"{sslConfigDir}/server.key", fileMode: Unix.FileMode700) | ||
| .WithResourceMapping(Encoding.UTF8.GetBytes(wrapperEntrypoint), "/usr/local/bin/docker-entrypoint-ssl.sh", fileMode: Unix.FileMode755) | ||
| .WithEntrypoint("/usr/local/bin/docker-entrypoint-ssl.sh") | ||
| .WithCommand("-c", "ssl=on") | ||
| .WithCommand("-c", $"ssl_ca_file={sslConfigDir}/ca_cert.pem") | ||
| .WithCommand("-c", $"ssl_cert_file={sslConfigDir}/server.crt") | ||
| .WithCommand("-c", $"ssl_key_file={sslConfigDir}/server.key"); | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public override PostgreSqlContainer Build() | ||
| { | ||
| Validate(); | ||
|
|
||
| // By default, the base builder waits until the container is running. However, for PostgreSql, a more advanced waiting strategy is necessary that requires access to the configured database and username. | ||
| // If the user does not provide a custom waiting strategy, append the default PostgreSql waiting strategy. | ||
| var postgreSqlBuilder = DockerResourceConfiguration.WaitStrategies.Count() > 1 ? this : WithWaitStrategy(Wait.ForUnixContainer().AddCustomWaitStrategy(new WaitUntil(DockerResourceConfiguration))); | ||
| // Ensure PostgreSQL is actually ready to accept connections over TCP, not just that the container is running. | ||
| // Always append the pg_isready-based wait strategy by default so tests using the default fixture are stable. | ||
| var postgreSqlBuilder = | ||
| WithWaitStrategy(Wait.ForUnixContainer().AddCustomWaitStrategy(new WaitUntil(DockerResourceConfiguration))); | ||
| return new PostgreSqlContainer(postgreSqlBuilder.DockerResourceConfiguration); | ||
| } | ||
|
Comment on lines
+187
to
192
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. 🧩 Analysis chainClarify wait strategy behavior and verify non-breaking change. The comment states "Always append the pg_isready-based wait strategy" but the code calls 🏁 Script executed: #!/bin/bash
# Check how WithWaitStrategy works - does it append or replace?
ast-grep --pattern 'WithWaitStrategy($$$) {
$$$
}'Length of output: 0 I'll search for the Length of output: 115359 Add check to preserve user-provided wait strategies in PostgreSqlBuilder.Build(). The code unconditionally calls Additionally, the comment states "Always append" but the code replaces rather than appends. The PostgreSQL builder should:
Apply the same pattern used in var postgreSqlBuilder = DockerResourceConfiguration.WaitStrategies.Count() > 1
? this
: WithWaitStrategy(Wait.ForUnixContainer().AddCustomWaitStrategy(new WaitUntil(DockerResourceConfiguration)));🤖 Prompt for AI Agents |
||
|
|
||
|
|
@@ -135,7 +246,11 @@ private sealed class WaitUntil : IWaitUntil | |
| public WaitUntil(PostgreSqlConfiguration configuration) | ||
| { | ||
| // Explicitly specify the host to ensure readiness only after the initdb scripts have executed, and the server is listening on TCP/IP. | ||
| _command = new List<string> { "pg_isready", "--host", "localhost", "--dbname", configuration.Database, "--username", configuration.Username }; | ||
| _command = new List<string> | ||
| { | ||
| "pg_isready", "--host", "localhost", "--dbname", configuration.Database, "--username", | ||
| configuration.Username | ||
| }; | ||
| } | ||
|
|
||
| /// <summary> | ||
|
|
@@ -154,7 +269,8 @@ public async Task<bool> UntilAsync(IContainer container) | |
|
|
||
| if (execResult.Stderr.Contains("pg_isready was not found")) | ||
| { | ||
| throw new NotSupportedException($"The '{container.Image.FullName}' image does not contain: pg_isready. Please use 'postgres:9.3' onwards."); | ||
| throw new NotSupportedException( | ||
| $"The '{container.Image.FullName}' image does not contain: pg_isready. Please use 'postgres:9.3' onwards."); | ||
| } | ||
|
|
||
| return 0L.Equals(execResult.ExitCode); | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -44,7 +44,7 @@ public async Task<ExecResult> ExecScriptAsync(string scriptContent, Cancellation | |||||||||||
| await CopyAsync(Encoding.Default.GetBytes(scriptContent), scriptFilePath, fileMode: Unix.FileMode644, ct: ct) | ||||||||||||
| .ConfigureAwait(false); | ||||||||||||
|
|
||||||||||||
| return await ExecAsync(new[] { "psql", "--username", _configuration.Username, "--dbname", _configuration.Database, "--file", scriptFilePath }, ct) | ||||||||||||
| return await ExecAsync(new[] { "psql", "--host", "localhost", "--username", _configuration.Username, "--dbname", _configuration.Database, "--file", scriptFilePath }, ct) | ||||||||||||
| .ConfigureAwait(false); | ||||||||||||
|
Comment on lines
+47
to
48
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. Don’t force TCP localhost without providing credentials — risk of password prompt/failure. With Apply this diff: - return await ExecAsync(new[] { "psql", "--host", "localhost", "--username", _configuration.Username, "--dbname", _configuration.Database, "--file", scriptFilePath }, ct)
+ var dsn = $"postgresql://{Uri.EscapeDataString(_configuration.Username)}:{Uri.EscapeDataString(_configuration.Password)}@localhost:{PostgreSqlBuilder.PostgreSqlPort}/{_configuration.Database}";
+ return await ExecAsync(new[] { "psql", dsn, "--file", scriptFilePath }, ct)
.ConfigureAwait(false);Alternative: only add 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||
| } | ||||||||||||
| } | ||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| namespace Testcontainers.PostgreSql; | ||
|
|
||
| /// <summary> | ||
| /// Represents the SSL mode for PostgreSQL connections. | ||
| /// </summary> | ||
| public enum SslMode | ||
| { | ||
| /// <summary> | ||
| /// SSL is disabled. | ||
| /// </summary> | ||
| Disable, | ||
|
|
||
| /// <summary> | ||
| /// SSL is required. | ||
| /// </summary> | ||
| Require, | ||
|
|
||
| /// <summary> | ||
| /// SSL is required, and the server certificate is verified against the root certificate. | ||
| /// </summary> | ||
| VerifyCa, | ||
|
|
||
| /// <summary> | ||
| /// SSL is required, and the server certificate is verified against the root certificate and the common name. | ||
| /// </summary> | ||
| VerifyFull | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.