docker_wrapper/template/database/
mongodb.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 MongodbTemplate {
16 config: TemplateConfig,
17}
18
19impl MongodbTemplate {
20 pub fn new(name: impl Into<String>) -> Self {
22 let name = name.into();
23 let env = HashMap::new();
24
25 let config = TemplateConfig {
26 name: name.clone(),
27 image: "mongo".to_string(),
28 tag: "7.0".to_string(),
29 ports: vec![(27017, 27017)],
30 env,
31 volumes: Vec::new(),
32 network: None,
33 health_check: Some(HealthCheck {
34 test: vec![
35 "mongosh".to_string(),
36 "--eval".to_string(),
37 "db.adminCommand('ping')".to_string(),
38 ],
39 interval: "10s".to_string(),
40 timeout: "5s".to_string(),
41 retries: 5,
42 start_period: "20s".to_string(),
43 }),
44 auto_remove: false,
45 memory_limit: None,
46 cpu_limit: None,
47 platform: None,
48 };
49
50 Self { config }
51 }
52
53 pub fn port(mut self, port: u16) -> Self {
55 self.config.ports = vec![(port, 27017)];
56 self
57 }
58
59 pub fn root_username(mut self, username: impl Into<String>) -> Self {
61 self.config
62 .env
63 .insert("MONGO_INITDB_ROOT_USERNAME".to_string(), username.into());
64 self
65 }
66
67 pub fn root_password(mut self, password: impl Into<String>) -> Self {
69 self.config
70 .env
71 .insert("MONGO_INITDB_ROOT_PASSWORD".to_string(), password.into());
72 self
73 }
74
75 pub fn database(mut self, db: impl Into<String>) -> Self {
77 self.config
78 .env
79 .insert("MONGO_INITDB_DATABASE".to_string(), db.into());
80 self
81 }
82
83 pub fn with_persistence(mut self, volume_name: impl Into<String>) -> Self {
85 self.config.volumes.push(VolumeMount {
86 source: volume_name.into(),
87 target: "/data/db".to_string(),
88 read_only: false,
89 });
90 self
91 }
92
93 pub fn init_scripts(mut self, scripts_path: impl Into<String>) -> Self {
95 self.config.volumes.push(VolumeMount {
96 source: scripts_path.into(),
97 target: "/docker-entrypoint-initdb.d".to_string(),
98 read_only: true,
99 });
100 self
101 }
102
103 pub fn config_file(mut self, config_path: impl Into<String>) -> Self {
105 self.config.volumes.push(VolumeMount {
106 source: config_path.into(),
107 target: "/etc/mongo/mongod.conf".to_string(),
108 read_only: true,
109 });
110 self
111 }
112
113 pub fn memory_limit(mut self, limit: impl Into<String>) -> Self {
115 self.config.memory_limit = Some(limit.into());
116 self
117 }
118
119 pub fn cache_size(mut self, size: impl Into<String>) -> Self {
121 self.config
122 .env
123 .insert("MONGO_WIREDTIGER_CACHE_SIZE_GB".to_string(), size.into());
124 self
125 }
126
127 pub fn replica_set(mut self, name: impl Into<String>) -> Self {
129 self.config
130 .env
131 .insert("MONGO_REPLICA_SET".to_string(), name.into());
132 self
133 }
134
135 pub fn with_auth(mut self) -> Self {
137 self.config
138 .env
139 .insert("MONGO_AUTH".to_string(), "yes".to_string());
140 self
141 }
142
143 pub fn version(mut self, version: impl Into<String>) -> Self {
145 self.config.tag = version.into();
146 self
147 }
148
149 pub fn network(mut self, network: impl Into<String>) -> Self {
151 self.config.network = Some(network.into());
152 self
153 }
154
155 pub fn auto_remove(mut self) -> Self {
157 self.config.auto_remove = true;
158 self
159 }
160
161 pub fn journal_commit_interval(mut self, ms: u32) -> Self {
163 self.config
164 .env
165 .insert("MONGO_JOURNAL_COMMIT_INTERVAL".to_string(), ms.to_string());
166 self
167 }
168
169 pub fn quiet(mut self) -> Self {
171 self.config
172 .env
173 .insert("MONGO_QUIET".to_string(), "yes".to_string());
174 self
175 }
176
177 pub fn custom_image(mut self, image: impl Into<String>, tag: impl Into<String>) -> Self {
179 self.config.image = image.into();
180 self.config.tag = tag.into();
181 self
182 }
183
184 pub fn platform(mut self, platform: impl Into<String>) -> Self {
186 self.config.platform = Some(platform.into());
187 self
188 }
189}
190
191#[async_trait]
192impl Template for MongodbTemplate {
193 fn name(&self) -> &str {
194 &self.config.name
195 }
196
197 fn config(&self) -> &TemplateConfig {
198 &self.config
199 }
200
201 fn config_mut(&mut self) -> &mut TemplateConfig {
202 &mut self.config
203 }
204
205 fn build_command(&self) -> crate::RunCommand {
206 let config = self.config();
207 let image_tag = format!("{}:{}", config.image, config.tag);
208
209 let mut cmd = crate::RunCommand::new(image_tag)
210 .name(&config.name)
211 .detach();
212
213 for (host, container) in &config.ports {
215 cmd = cmd.port(*host, *container);
216 }
217
218 for mount in &config.volumes {
220 if mount.read_only {
221 cmd = cmd.volume_ro(&mount.source, &mount.target);
222 } else {
223 cmd = cmd.volume(&mount.source, &mount.target);
224 }
225 }
226
227 if let Some(network) = &config.network {
229 cmd = cmd.network(network);
230 }
231
232 for (key, value) in &config.env {
234 if key != "MONGO_REPLICA_SET" {
235 cmd = cmd.env(key, value);
236 }
237 }
238
239 if let Some(health) = &config.health_check {
241 cmd = cmd
242 .health_cmd(&health.test.join(" "))
243 .health_interval(&health.interval)
244 .health_timeout(&health.timeout)
245 .health_retries(health.retries)
246 .health_start_period(&health.start_period);
247 }
248
249 if let Some(memory) = &config.memory_limit {
251 cmd = cmd.memory(memory);
252 }
253
254 if let Some(cpu) = &config.cpu_limit {
255 cmd = cmd.cpus(cpu);
256 }
257
258 if config.auto_remove {
260 cmd = cmd.remove();
261 }
262
263 if let Some(platform) = &config.platform {
265 cmd = cmd.platform(platform);
266 }
267
268 if let Some(replica_set) = config.env.get("MONGO_REPLICA_SET") {
270 cmd = cmd.cmd(vec![
272 "mongod".to_string(),
273 "--replSet".to_string(),
274 replica_set.clone(),
275 "--bind_ip_all".to_string(),
276 ]);
277 }
278
279 cmd
280 }
281
282 async fn wait_for_ready(&self) -> crate::template::Result<()> {
283 use std::time::Duration;
284 use tokio::time::{sleep, timeout};
285
286 let wait_timeout = Duration::from_secs(30);
288 let check_interval = Duration::from_millis(500);
289
290 timeout(wait_timeout, async {
291 loop {
292 if !self.is_running().await? {
294 return Err(crate::template::TemplateError::NotRunning(
295 self.config().name.clone(),
296 ));
297 }
298
299 let check_cmd = if self.config.tag.starts_with("4.") {
301 vec![
303 "mongo",
304 "--host",
305 "localhost",
306 "--eval",
307 "db.runCommand({ ping: 1 })",
308 "--quiet",
309 ]
310 } else {
311 vec![
313 "mongosh",
314 "--host",
315 "localhost",
316 "--eval",
317 "db.runCommand({ ping: 1 })",
318 "--quiet",
319 ]
320 };
321
322 if let Ok(result) = self.exec(check_cmd).await {
324 if result.stdout.contains("ok") && result.stdout.contains('1') {
326 return Ok(());
327 }
328 }
329
330 sleep(check_interval).await;
331 }
332 })
333 .await
334 .map_err(|_| {
335 crate::template::TemplateError::InvalidConfig(format!(
336 "MongoDB container {} failed to become ready within timeout",
337 self.config().name
338 ))
339 })?
340 }
341}
342
343pub struct MongodbConnectionString {
345 host: String,
346 port: u16,
347 database: Option<String>,
348 username: Option<String>,
349 password: Option<String>,
350 replica_set: Option<String>,
351}
352
353impl MongodbConnectionString {
354 pub fn from_template(template: &MongodbTemplate) -> Self {
356 let config = template.config();
357 let port = config.ports.first().map(|(h, _)| *h).unwrap_or(27017);
358
359 Self {
360 host: "localhost".to_string(),
361 port,
362 database: config.env.get("MONGO_INITDB_DATABASE").cloned(),
363 username: config.env.get("MONGO_INITDB_ROOT_USERNAME").cloned(),
364 password: config.env.get("MONGO_INITDB_ROOT_PASSWORD").cloned(),
365 replica_set: config.env.get("MONGO_REPLICA_SET").cloned(),
366 }
367 }
368
369 pub fn url(&self) -> String {
371 let mut url = String::from("mongodb://");
372
373 if let (Some(user), Some(pass)) = (&self.username, &self.password) {
375 url.push_str(&format!("{}:{}@", user, pass));
376 }
377
378 url.push_str(&format!("{}:{}", self.host, self.port));
380
381 if let Some(db) = &self.database {
383 url.push_str(&format!("/{}", db));
384 }
385
386 if let Some(rs) = &self.replica_set {
388 if self.database.is_none() {
389 url.push('/');
390 }
391 url.push_str(&format!("?replicaSet={}", rs));
392 }
393
394 url
395 }
396
397 pub fn srv_url(&self) -> String {
399 let mut url = String::from("mongodb+srv://");
400
401 if let (Some(user), Some(pass)) = (&self.username, &self.password) {
403 url.push_str(&format!("{}:{}@", user, pass));
404 }
405
406 url.push_str(&self.host);
408
409 if let Some(db) = &self.database {
411 url.push_str(&format!("/{}", db));
412 }
413
414 url
415 }
416}
417
418#[cfg(test)]
419mod tests {
420 use super::*;
421
422 #[test]
423 fn test_mongodb_template_basic() {
424 let template = MongodbTemplate::new("test-mongo");
425 assert_eq!(template.name(), "test-mongo");
426 assert_eq!(template.config().image, "mongo");
427 assert_eq!(template.config().tag, "7.0");
428 assert_eq!(template.config().ports, vec![(27017, 27017)]);
429 }
430
431 #[test]
432 fn test_mongodb_template_with_auth() {
433 let template = MongodbTemplate::new("test-mongo")
434 .root_username("admin")
435 .root_password("secret123")
436 .database("mydb")
437 .with_auth();
438
439 assert_eq!(
440 template.config().env.get("MONGO_INITDB_ROOT_USERNAME"),
441 Some(&"admin".to_string())
442 );
443 assert_eq!(
444 template.config().env.get("MONGO_INITDB_ROOT_PASSWORD"),
445 Some(&"secret123".to_string())
446 );
447 assert_eq!(
448 template.config().env.get("MONGO_INITDB_DATABASE"),
449 Some(&"mydb".to_string())
450 );
451 assert_eq!(
452 template.config().env.get("MONGO_AUTH"),
453 Some(&"yes".to_string())
454 );
455 }
456
457 #[test]
458 fn test_mongodb_template_with_persistence() {
459 let template = MongodbTemplate::new("test-mongo").with_persistence("mongo-data");
460
461 assert_eq!(template.config().volumes.len(), 1);
462 assert_eq!(template.config().volumes[0].source, "mongo-data");
463 assert_eq!(template.config().volumes[0].target, "/data/db");
464 }
465
466 #[test]
467 fn test_mongodb_connection_string() {
468 let template = MongodbTemplate::new("test-mongo")
469 .root_username("admin")
470 .root_password("pass")
471 .database("testdb")
472 .port(27018);
473
474 let conn = MongodbConnectionString::from_template(&template);
475
476 assert_eq!(conn.url(), "mongodb://admin:pass@localhost:27018/testdb");
477 }
478
479 #[test]
480 fn test_mongodb_connection_string_no_auth() {
481 let template = MongodbTemplate::new("test-mongo");
482 let conn = MongodbConnectionString::from_template(&template);
483
484 assert_eq!(conn.url(), "mongodb://localhost:27017");
485 }
486
487 #[test]
488 fn test_mongodb_connection_string_replica_set() {
489 let template = MongodbTemplate::new("test-mongo").replica_set("rs0");
490
491 let conn = MongodbConnectionString::from_template(&template);
492
493 assert_eq!(conn.url(), "mongodb://localhost:27017/?replicaSet=rs0");
494 }
495}