#![allow(clippy::doc_markdown)]
#![allow(clippy::must_use_candidate)]
#![allow(clippy::return_self_not_must_use)]
#![allow(clippy::map_unwrap_or)]
#![allow(clippy::format_push_string)]
#![allow(clippy::uninlined_format_args)]
use crate::template::{HealthCheck, Template, TemplateConfig, VolumeMount};
use async_trait::async_trait;
use std::collections::HashMap;
pub struct PostgresTemplate {
config: TemplateConfig,
}
impl PostgresTemplate {
pub fn new(name: impl Into<String>) -> Self {
let name = name.into();
let mut env = HashMap::new();
env.insert("POSTGRES_PASSWORD".to_string(), "postgres".to_string());
env.insert("POSTGRES_USER".to_string(), "postgres".to_string());
env.insert("POSTGRES_DB".to_string(), "postgres".to_string());
let config = TemplateConfig {
name: name.clone(),
image: "postgres".to_string(),
tag: "15-alpine".to_string(),
ports: vec![(5432, 5432)],
env,
volumes: Vec::new(),
network: None,
health_check: Some(HealthCheck {
test: vec![
"pg_isready".to_string(),
"-U".to_string(),
"postgres".to_string(),
],
interval: "10s".to_string(),
timeout: "5s".to_string(),
retries: 5,
start_period: "10s".to_string(),
}),
auto_remove: false,
memory_limit: None,
cpu_limit: None,
platform: None,
};
Self { config }
}
pub fn port(mut self, port: u16) -> Self {
self.config.ports = vec![(port, 5432)];
self
}
pub fn database(mut self, db: impl Into<String>) -> Self {
self.config.env.insert("POSTGRES_DB".to_string(), db.into());
self
}
pub fn user(mut self, user: impl Into<String>) -> Self {
let user = user.into();
self.config
.env
.insert("POSTGRES_USER".to_string(), user.clone());
if let Some(health) = &mut self.config.health_check {
health.test = vec!["pg_isready".to_string(), "-U".to_string(), user];
}
self
}
pub fn password(mut self, password: impl Into<String>) -> Self {
self.config
.env
.insert("POSTGRES_PASSWORD".to_string(), password.into());
self
}
pub fn with_persistence(mut self, volume_name: impl Into<String>) -> Self {
self.config.volumes.push(VolumeMount {
source: volume_name.into(),
target: "/var/lib/postgresql/data".to_string(),
read_only: false,
});
self
}
pub fn init_scripts(mut self, scripts_path: impl Into<String>) -> Self {
self.config.volumes.push(VolumeMount {
source: scripts_path.into(),
target: "/docker-entrypoint-initdb.d".to_string(),
read_only: true,
});
self
}
pub fn memory_limit(mut self, limit: impl Into<String>) -> Self {
self.config.memory_limit = Some(limit.into());
self
}
pub fn shared_memory(mut self, size: impl Into<String>) -> Self {
self.config
.env
.insert("POSTGRES_SHARED_MEMORY".to_string(), size.into());
self
}
pub fn with_extension(mut self, extension: impl Into<String>) -> Self {
let ext = extension.into();
let current = self
.config
.env
.get("POSTGRES_EXTENSIONS")
.map(|s| format!("{},{}", s, ext))
.unwrap_or(ext);
self.config
.env
.insert("POSTGRES_EXTENSIONS".to_string(), current);
self
}
pub fn version(mut self, version: impl Into<String>) -> Self {
self.config.tag = format!("{}-alpine", version.into());
self
}
pub fn network(mut self, network: impl Into<String>) -> Self {
self.config.network = Some(network.into());
self
}
pub fn auto_remove(mut self) -> Self {
self.config.auto_remove = true;
self
}
pub fn postgres_args(mut self, args: impl Into<String>) -> Self {
self.config
.env
.insert("POSTGRES_INITDB_ARGS".to_string(), args.into());
self
}
pub fn with_ssl(mut self) -> Self {
self.config
.env
.insert("POSTGRES_SSL_MODE".to_string(), "require".to_string());
self
}
pub fn locale(mut self, locale: impl Into<String>) -> Self {
let locale = locale.into();
self.config.env.insert(
"POSTGRES_INITDB_ARGS".to_string(),
format!("--locale={}", locale),
);
self
}
pub fn custom_image(mut self, image: impl Into<String>, tag: impl Into<String>) -> Self {
self.config.image = image.into();
self.config.tag = tag.into();
self
}
pub fn platform(mut self, platform: impl Into<String>) -> Self {
self.config.platform = Some(platform.into());
self
}
}
#[async_trait]
impl Template for PostgresTemplate {
fn name(&self) -> &str {
&self.config.name
}
fn config(&self) -> &TemplateConfig {
&self.config
}
fn config_mut(&mut self) -> &mut TemplateConfig {
&mut self.config
}
async fn wait_for_ready(&self) -> crate::template::Result<()> {
use std::time::Duration;
use tokio::time::{sleep, timeout};
let wait_timeout = Duration::from_secs(60);
let check_interval = Duration::from_millis(500);
timeout(wait_timeout, async {
loop {
if !self.is_running().await.unwrap_or(false) {
sleep(check_interval).await;
continue;
}
let user = self
.config
.env
.get("POSTGRES_USER")
.map(|s| s.as_str())
.unwrap_or("postgres");
let db = self
.config
.env
.get("POSTGRES_DB")
.map(|s| s.as_str())
.unwrap_or("postgres");
let check_cmd = vec!["pg_isready", "-h", "localhost", "-U", user, "-d", db];
if let Ok(result) = self.exec(check_cmd).await {
if result.stdout.contains("accepting connections") {
return Ok(());
}
}
sleep(check_interval).await;
}
})
.await
.map_err(|_| {
crate::template::TemplateError::InvalidConfig(format!(
"PostgreSQL container {} failed to become ready within timeout",
self.config().name
))
})?
}
}
pub struct PostgresConnectionString {
host: String,
port: u16,
database: String,
user: String,
password: String,
}
impl PostgresConnectionString {
pub fn from_template(template: &PostgresTemplate) -> Self {
let config = template.config();
let port = config.ports.first().map(|(h, _)| *h).unwrap_or(5432);
Self {
host: "localhost".to_string(),
port,
database: config
.env
.get("POSTGRES_DB")
.cloned()
.unwrap_or_else(|| "postgres".to_string()),
user: config
.env
.get("POSTGRES_USER")
.cloned()
.unwrap_or_else(|| "postgres".to_string()),
password: config
.env
.get("POSTGRES_PASSWORD")
.cloned()
.unwrap_or_else(|| "postgres".to_string()),
}
}
pub fn url(&self) -> String {
format!(
"postgresql://{}:{}@{}:{}/{}",
self.user, self.password, self.host, self.port, self.database
)
}
pub fn key_value(&self) -> String {
format!(
"host={} port={} dbname={} user={} password={}",
self.host, self.port, self.database, self.user, self.password
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::DockerCommand;
#[test]
fn test_postgres_template_basic() {
let template = PostgresTemplate::new("test-postgres");
assert_eq!(template.name(), "test-postgres");
assert_eq!(template.config().image, "postgres");
assert_eq!(template.config().tag, "15-alpine");
assert_eq!(template.config().ports, vec![(5432, 5432)]);
}
#[test]
fn test_postgres_template_custom_config() {
let template = PostgresTemplate::new("test-postgres")
.database("mydb")
.user("myuser")
.password("secret123")
.port(15432);
assert_eq!(
template.config().env.get("POSTGRES_DB"),
Some(&"mydb".to_string())
);
assert_eq!(
template.config().env.get("POSTGRES_USER"),
Some(&"myuser".to_string())
);
assert_eq!(
template.config().env.get("POSTGRES_PASSWORD"),
Some(&"secret123".to_string())
);
assert_eq!(template.config().ports, vec![(15432, 5432)]);
}
#[test]
fn test_postgres_template_with_persistence() {
let template = PostgresTemplate::new("test-postgres").with_persistence("postgres-data");
assert_eq!(template.config().volumes.len(), 1);
assert_eq!(template.config().volumes[0].source, "postgres-data");
assert_eq!(
template.config().volumes[0].target,
"/var/lib/postgresql/data"
);
}
#[test]
fn test_postgres_template_with_init_scripts() {
let template = PostgresTemplate::new("test-postgres").init_scripts("./init-scripts");
assert_eq!(template.config().volumes.len(), 1);
assert_eq!(template.config().volumes[0].source, "./init-scripts");
assert_eq!(
template.config().volumes[0].target,
"/docker-entrypoint-initdb.d"
);
assert!(template.config().volumes[0].read_only);
}
#[test]
fn test_postgres_connection_string() {
let template = PostgresTemplate::new("test-postgres")
.database("testdb")
.user("testuser")
.password("testpass")
.port(15432);
let conn = PostgresConnectionString::from_template(&template);
assert_eq!(
conn.url(),
"postgresql://testuser:testpass@localhost:15432/testdb"
);
assert_eq!(
conn.key_value(),
"host=localhost port=15432 dbname=testdb user=testuser password=testpass"
);
}
#[test]
fn test_postgres_build_command() {
let template = PostgresTemplate::new("test-postgres")
.database("mydb")
.port(15432);
let cmd = template.build_command();
let args = cmd.build_command_args();
assert!(args.contains(&"run".to_string()));
assert!(args.contains(&"--name".to_string()));
assert!(args.contains(&"test-postgres".to_string()));
assert!(args.contains(&"--publish".to_string()));
assert!(args.contains(&"15432:5432".to_string()));
assert!(args.contains(&"--env".to_string()));
}
}