docker_wrapper/template/database/
postgres.rs1#![allow(clippy::doc_markdown)]
4#![allow(clippy::must_use_candidate)]
5#![allow(clippy::return_self_not_must_use)]
6#![allow(clippy::map_unwrap_or)]
7#![allow(clippy::format_push_string)]
8#![allow(clippy::uninlined_format_args)]
9
10use crate::template::{HealthCheck, Template, TemplateConfig, VolumeMount};
11use async_trait::async_trait;
12use std::collections::HashMap;
13
14pub struct PostgresTemplate {
16 config: TemplateConfig,
17}
18
19impl PostgresTemplate {
20 pub fn new(name: impl Into<String>) -> Self {
22 let name = name.into();
23 let mut env = HashMap::new();
24
25 env.insert("POSTGRES_PASSWORD".to_string(), "postgres".to_string());
27 env.insert("POSTGRES_USER".to_string(), "postgres".to_string());
28 env.insert("POSTGRES_DB".to_string(), "postgres".to_string());
29
30 let config = TemplateConfig {
31 name: name.clone(),
32 image: "postgres".to_string(),
33 tag: "15-alpine".to_string(),
34 ports: vec![(5432, 5432)],
35 env,
36 volumes: Vec::new(),
37 network: None,
38 health_check: Some(HealthCheck {
39 test: vec![
40 "pg_isready".to_string(),
41 "-U".to_string(),
42 "postgres".to_string(),
43 ],
44 interval: "10s".to_string(),
45 timeout: "5s".to_string(),
46 retries: 5,
47 start_period: "10s".to_string(),
48 }),
49 auto_remove: false,
50 memory_limit: None,
51 cpu_limit: None,
52 platform: None,
53 };
54
55 Self { config }
56 }
57
58 pub fn port(mut self, port: u16) -> Self {
60 self.config.ports = vec![(port, 5432)];
61 self
62 }
63
64 pub fn database(mut self, db: impl Into<String>) -> Self {
66 self.config.env.insert("POSTGRES_DB".to_string(), db.into());
67 self
68 }
69
70 pub fn user(mut self, user: impl Into<String>) -> Self {
72 let user = user.into();
73 self.config
74 .env
75 .insert("POSTGRES_USER".to_string(), user.clone());
76
77 if let Some(health) = &mut self.config.health_check {
79 health.test = vec!["pg_isready".to_string(), "-U".to_string(), user];
80 }
81 self
82 }
83
84 pub fn password(mut self, password: impl Into<String>) -> Self {
86 self.config
87 .env
88 .insert("POSTGRES_PASSWORD".to_string(), password.into());
89 self
90 }
91
92 pub fn with_persistence(mut self, volume_name: impl Into<String>) -> Self {
94 self.config.volumes.push(VolumeMount {
95 source: volume_name.into(),
96 target: "/var/lib/postgresql/data".to_string(),
97 read_only: false,
98 });
99 self
100 }
101
102 pub fn init_scripts(mut self, scripts_path: impl Into<String>) -> Self {
104 self.config.volumes.push(VolumeMount {
105 source: scripts_path.into(),
106 target: "/docker-entrypoint-initdb.d".to_string(),
107 read_only: true,
108 });
109 self
110 }
111
112 pub fn memory_limit(mut self, limit: impl Into<String>) -> Self {
114 self.config.memory_limit = Some(limit.into());
115 self
116 }
117
118 pub fn shared_memory(mut self, size: impl Into<String>) -> Self {
120 self.config
121 .env
122 .insert("POSTGRES_SHARED_MEMORY".to_string(), size.into());
123 self
124 }
125
126 pub fn with_extension(mut self, extension: impl Into<String>) -> Self {
128 let ext = extension.into();
129 let current = self
130 .config
131 .env
132 .get("POSTGRES_EXTENSIONS")
133 .map(|s| format!("{},{}", s, ext))
134 .unwrap_or(ext);
135 self.config
136 .env
137 .insert("POSTGRES_EXTENSIONS".to_string(), current);
138 self
139 }
140
141 pub fn version(mut self, version: impl Into<String>) -> Self {
143 self.config.tag = format!("{}-alpine", version.into());
144 self
145 }
146
147 pub fn network(mut self, network: impl Into<String>) -> Self {
149 self.config.network = Some(network.into());
150 self
151 }
152
153 pub fn auto_remove(mut self) -> Self {
155 self.config.auto_remove = true;
156 self
157 }
158
159 pub fn postgres_args(mut self, args: impl Into<String>) -> Self {
161 self.config
162 .env
163 .insert("POSTGRES_INITDB_ARGS".to_string(), args.into());
164 self
165 }
166
167 pub fn with_ssl(mut self) -> Self {
169 self.config
170 .env
171 .insert("POSTGRES_SSL_MODE".to_string(), "require".to_string());
172 self
173 }
174
175 pub fn locale(mut self, locale: impl Into<String>) -> Self {
177 let locale = locale.into();
178 self.config.env.insert(
179 "POSTGRES_INITDB_ARGS".to_string(),
180 format!("--locale={}", locale),
181 );
182 self
183 }
184
185 pub fn custom_image(mut self, image: impl Into<String>, tag: impl Into<String>) -> Self {
187 self.config.image = image.into();
188 self.config.tag = tag.into();
189 self
190 }
191
192 pub fn platform(mut self, platform: impl Into<String>) -> Self {
194 self.config.platform = Some(platform.into());
195 self
196 }
197}
198
199#[async_trait]
200impl Template for PostgresTemplate {
201 fn name(&self) -> &str {
202 &self.config.name
203 }
204
205 fn config(&self) -> &TemplateConfig {
206 &self.config
207 }
208
209 fn config_mut(&mut self) -> &mut TemplateConfig {
210 &mut self.config
211 }
212}
213
214pub struct PostgresConnectionString {
216 host: String,
217 port: u16,
218 database: String,
219 user: String,
220 password: String,
221}
222
223impl PostgresConnectionString {
224 pub fn from_template(template: &PostgresTemplate) -> Self {
226 let config = template.config();
227 let port = config.ports.first().map(|(h, _)| *h).unwrap_or(5432);
228
229 Self {
230 host: "localhost".to_string(),
231 port,
232 database: config
233 .env
234 .get("POSTGRES_DB")
235 .cloned()
236 .unwrap_or_else(|| "postgres".to_string()),
237 user: config
238 .env
239 .get("POSTGRES_USER")
240 .cloned()
241 .unwrap_or_else(|| "postgres".to_string()),
242 password: config
243 .env
244 .get("POSTGRES_PASSWORD")
245 .cloned()
246 .unwrap_or_else(|| "postgres".to_string()),
247 }
248 }
249
250 pub fn url(&self) -> String {
252 format!(
253 "postgresql://{}:{}@{}:{}/{}",
254 self.user, self.password, self.host, self.port, self.database
255 )
256 }
257
258 pub fn key_value(&self) -> String {
260 format!(
261 "host={} port={} dbname={} user={} password={}",
262 self.host, self.port, self.database, self.user, self.password
263 )
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270 use crate::DockerCommand;
271
272 #[test]
273 fn test_postgres_template_basic() {
274 let template = PostgresTemplate::new("test-postgres");
275 assert_eq!(template.name(), "test-postgres");
276 assert_eq!(template.config().image, "postgres");
277 assert_eq!(template.config().tag, "15-alpine");
278 assert_eq!(template.config().ports, vec![(5432, 5432)]);
279 }
280
281 #[test]
282 fn test_postgres_template_custom_config() {
283 let template = PostgresTemplate::new("test-postgres")
284 .database("mydb")
285 .user("myuser")
286 .password("secret123")
287 .port(15432);
288
289 assert_eq!(
290 template.config().env.get("POSTGRES_DB"),
291 Some(&"mydb".to_string())
292 );
293 assert_eq!(
294 template.config().env.get("POSTGRES_USER"),
295 Some(&"myuser".to_string())
296 );
297 assert_eq!(
298 template.config().env.get("POSTGRES_PASSWORD"),
299 Some(&"secret123".to_string())
300 );
301 assert_eq!(template.config().ports, vec![(15432, 5432)]);
302 }
303
304 #[test]
305 fn test_postgres_template_with_persistence() {
306 let template = PostgresTemplate::new("test-postgres").with_persistence("postgres-data");
307
308 assert_eq!(template.config().volumes.len(), 1);
309 assert_eq!(template.config().volumes[0].source, "postgres-data");
310 assert_eq!(
311 template.config().volumes[0].target,
312 "/var/lib/postgresql/data"
313 );
314 }
315
316 #[test]
317 fn test_postgres_template_with_init_scripts() {
318 let template = PostgresTemplate::new("test-postgres").init_scripts("./init-scripts");
319
320 assert_eq!(template.config().volumes.len(), 1);
321 assert_eq!(template.config().volumes[0].source, "./init-scripts");
322 assert_eq!(
323 template.config().volumes[0].target,
324 "/docker-entrypoint-initdb.d"
325 );
326 assert!(template.config().volumes[0].read_only);
327 }
328
329 #[test]
330 fn test_postgres_connection_string() {
331 let template = PostgresTemplate::new("test-postgres")
332 .database("testdb")
333 .user("testuser")
334 .password("testpass")
335 .port(15432);
336
337 let conn = PostgresConnectionString::from_template(&template);
338
339 assert_eq!(
340 conn.url(),
341 "postgresql://testuser:testpass@localhost:15432/testdb"
342 );
343
344 assert_eq!(
345 conn.key_value(),
346 "host=localhost port=15432 dbname=testdb user=testuser password=testpass"
347 );
348 }
349
350 #[test]
351 fn test_postgres_build_command() {
352 let template = PostgresTemplate::new("test-postgres")
353 .database("mydb")
354 .port(15432);
355
356 let cmd = template.build_command();
357 let args = cmd.build_command_args();
358
359 assert!(args.contains(&"run".to_string()));
361 assert!(args.contains(&"--name".to_string()));
362 assert!(args.contains(&"test-postgres".to_string()));
363 assert!(args.contains(&"--publish".to_string()));
364 assert!(args.contains(&"15432:5432".to_string()));
365 assert!(args.contains(&"--env".to_string()));
366 }
367}