1#![allow(clippy::doc_markdown)]
7#![allow(clippy::must_use_candidate)]
8#![allow(clippy::return_self_not_must_use)]
9#![allow(clippy::needless_borrows_for_generic_args)]
10#![allow(clippy::redundant_closure_for_method_calls)]
11#![allow(clippy::inefficient_to_string)]
12
13use crate::{DockerCommand, RunCommand};
14use async_trait::async_trait;
15use std::collections::HashMap;
16
17#[cfg(any(feature = "template-redis", feature = "template-redis-cluster"))]
19pub mod redis;
20
21#[cfg(any(
23 feature = "template-postgres",
24 feature = "template-mysql",
25 feature = "template-mongodb"
26))]
27pub mod database;
28
29#[cfg(feature = "template-nginx")]
31pub mod web;
32
33pub type Result<T> = std::result::Result<T, TemplateError>;
35
36#[derive(Debug, thiserror::Error)]
38pub enum TemplateError {
39 #[error("Docker command failed: {0}")]
41 DockerError(#[from] crate::Error),
42
43 #[error("Invalid configuration: {0}")]
45 InvalidConfig(String),
46
47 #[error("Template already running: {0}")]
49 AlreadyRunning(String),
50
51 #[error("Template not running: {0}")]
53 NotRunning(String),
54}
55
56#[derive(Debug, Clone)]
58pub struct TemplateConfig {
59 pub name: String,
61
62 pub image: String,
64
65 pub tag: String,
67
68 pub ports: Vec<(u16, u16)>,
70
71 pub env: HashMap<String, String>,
73
74 pub volumes: Vec<VolumeMount>,
76
77 pub network: Option<String>,
79
80 pub health_check: Option<HealthCheck>,
82
83 pub auto_remove: bool,
85
86 pub memory_limit: Option<String>,
88
89 pub cpu_limit: Option<String>,
91
92 pub platform: Option<String>,
94}
95
96#[derive(Debug, Clone)]
98pub struct VolumeMount {
99 pub source: String,
101
102 pub target: String,
104
105 pub read_only: bool,
107}
108
109#[derive(Debug, Clone)]
111pub struct HealthCheck {
112 pub test: Vec<String>,
114
115 pub interval: String,
117
118 pub timeout: String,
120
121 pub retries: i32,
123
124 pub start_period: String,
126}
127
128#[async_trait]
130pub trait Template: Send + Sync {
131 fn name(&self) -> &str;
133
134 fn config(&self) -> &TemplateConfig;
136
137 fn config_mut(&mut self) -> &mut TemplateConfig;
139
140 fn build_command(&self) -> RunCommand {
142 let config = self.config();
143 let mut cmd = RunCommand::new(format!("{}:{}", config.image, config.tag))
144 .name(&config.name)
145 .detach();
146
147 for (host, container) in &config.ports {
149 cmd = cmd.port(*host, *container);
150 }
151
152 for (key, value) in &config.env {
154 cmd = cmd.env(key, value);
155 }
156
157 for mount in &config.volumes {
159 if mount.read_only {
160 cmd = cmd.volume_ro(&mount.source, &mount.target);
161 } else {
162 cmd = cmd.volume(&mount.source, &mount.target);
163 }
164 }
165
166 if let Some(network) = &config.network {
168 cmd = cmd.network(network);
169 }
170
171 if let Some(health) = &config.health_check {
173 cmd = cmd
174 .health_cmd(&health.test.join(" "))
175 .health_interval(&health.interval)
176 .health_timeout(&health.timeout)
177 .health_retries(health.retries)
178 .health_start_period(&health.start_period);
179 }
180
181 if let Some(memory) = &config.memory_limit {
183 cmd = cmd.memory(memory);
184 }
185
186 if let Some(cpu) = &config.cpu_limit {
187 cmd = cmd.cpus(cpu);
188 }
189
190 if let Some(platform) = &config.platform {
192 cmd = cmd.platform(platform);
193 }
194
195 if config.auto_remove {
197 cmd = cmd.remove();
198 }
199
200 cmd
201 }
202
203 async fn start(&self) -> Result<String> {
205 let output = self.build_command().execute().await?;
206 Ok(output.0)
207 }
208
209 async fn start_and_wait(&self) -> Result<String> {
211 let container_id = self.start().await?;
212 self.wait_for_ready().await?;
213 Ok(container_id)
214 }
215
216 async fn stop(&self) -> Result<()> {
218 use crate::StopCommand;
219
220 StopCommand::new(self.config().name.as_str())
221 .execute()
222 .await?;
223
224 Ok(())
225 }
226
227 async fn remove(&self) -> Result<()> {
229 use crate::RmCommand;
230
231 RmCommand::new(self.config().name.as_str())
232 .force()
233 .volumes()
234 .execute()
235 .await?;
236
237 Ok(())
238 }
239
240 async fn is_running(&self) -> Result<bool> {
242 use crate::PsCommand;
243
244 let output = PsCommand::new()
245 .filter(format!("name={}", &self.config().name))
246 .quiet()
247 .execute()
248 .await?;
249
250 Ok(!output.stdout.trim().is_empty())
252 }
253
254 async fn logs(&self, follow: bool, tail: Option<&str>) -> Result<crate::CommandOutput> {
256 use crate::LogsCommand;
257
258 let mut cmd = LogsCommand::new(&self.config().name);
259
260 if follow {
261 cmd = cmd.follow();
262 }
263
264 if let Some(lines) = tail {
265 cmd = cmd.tail(lines);
266 }
267
268 cmd.execute().await.map_err(Into::into)
269 }
270
271 async fn exec(&self, command: Vec<&str>) -> Result<crate::ExecOutput> {
273 use crate::ExecCommand;
274
275 let cmd_vec: Vec<String> = command.iter().map(|s| s.to_string()).collect();
276 let cmd = ExecCommand::new(&self.config().name, cmd_vec);
277
278 cmd.execute().await.map_err(Into::into)
279 }
280
281 async fn wait_for_ready(&self) -> Result<()> {
289 use std::time::Duration;
290 use tokio::time::{sleep, timeout};
291
292 let wait_timeout = Duration::from_secs(30);
294 let check_interval = Duration::from_millis(500);
295
296 timeout(wait_timeout, async {
297 loop {
298 if !self.is_running().await? {
300 return Err(TemplateError::NotRunning(self.config().name.clone()));
301 }
302
303 if self.config().health_check.is_some() {
305 use crate::InspectCommand;
306
307 let inspect = InspectCommand::new(&self.config().name).execute().await?;
308
309 if let Ok(containers) =
311 serde_json::from_str::<serde_json::Value>(&inspect.stdout)
312 {
313 if let Some(first) = containers.as_array().and_then(|arr| arr.first()) {
314 if let Some(state) = first.get("State") {
315 if let Some(health) = state.get("Health") {
316 if let Some(status) =
317 health.get("Status").and_then(|s| s.as_str())
318 {
319 if status == "healthy" {
320 return Ok(());
321 }
322 }
323 } else if let Some(running) =
324 state.get("Running").and_then(|r| r.as_bool())
325 {
326 if running {
328 return Ok(());
329 }
330 }
331 }
332 }
333 }
334 } else {
335 return Ok(());
337 }
338
339 sleep(check_interval).await;
340 }
341 })
342 .await
343 .map_err(|_| {
344 TemplateError::InvalidConfig(format!(
345 "Container {} failed to become ready within timeout",
346 self.config().name
347 ))
348 })?
349 }
350}
351
352pub struct TemplateBuilder {
354 config: TemplateConfig,
355}
356
357impl TemplateBuilder {
358 pub fn new(name: impl Into<String>, image: impl Into<String>) -> Self {
360 Self {
361 config: TemplateConfig {
362 name: name.into(),
363 image: image.into(),
364 tag: "latest".to_string(),
365 ports: Vec::new(),
366 env: HashMap::new(),
367 volumes: Vec::new(),
368 network: None,
369 health_check: None,
370 auto_remove: false,
371 memory_limit: None,
372 cpu_limit: None,
373 platform: None,
374 },
375 }
376 }
377
378 pub fn tag(mut self, tag: impl Into<String>) -> Self {
380 self.config.tag = tag.into();
381 self
382 }
383
384 pub fn port(mut self, host: u16, container: u16) -> Self {
386 self.config.ports.push((host, container));
387 self
388 }
389
390 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
392 self.config.env.insert(key.into(), value.into());
393 self
394 }
395
396 pub fn volume(mut self, source: impl Into<String>, target: impl Into<String>) -> Self {
398 self.config.volumes.push(VolumeMount {
399 source: source.into(),
400 target: target.into(),
401 read_only: false,
402 });
403 self
404 }
405
406 pub fn volume_ro(mut self, source: impl Into<String>, target: impl Into<String>) -> Self {
408 self.config.volumes.push(VolumeMount {
409 source: source.into(),
410 target: target.into(),
411 read_only: true,
412 });
413 self
414 }
415
416 pub fn network(mut self, network: impl Into<String>) -> Self {
418 self.config.network = Some(network.into());
419 self
420 }
421
422 pub fn auto_remove(mut self) -> Self {
424 self.config.auto_remove = true;
425 self
426 }
427
428 pub fn memory_limit(mut self, limit: impl Into<String>) -> Self {
430 self.config.memory_limit = Some(limit.into());
431 self
432 }
433
434 pub fn cpu_limit(mut self, limit: impl Into<String>) -> Self {
436 self.config.cpu_limit = Some(limit.into());
437 self
438 }
439
440 pub fn build(self) -> CustomTemplate {
442 CustomTemplate {
443 config: self.config,
444 }
445 }
446}
447
448pub struct CustomTemplate {
450 config: TemplateConfig,
451}
452
453#[async_trait]
454impl Template for CustomTemplate {
455 fn name(&self) -> &str {
456 &self.config.name
457 }
458
459 fn config(&self) -> &TemplateConfig {
460 &self.config
461 }
462
463 fn config_mut(&mut self) -> &mut TemplateConfig {
464 &mut self.config
465 }
466}
467
468#[cfg(feature = "template-redis")]
471pub use redis::RedisTemplate;
472
473#[cfg(feature = "template-redis-cluster")]
474pub use redis::{ClusterInfo, NodeInfo, NodeRole, RedisClusterConnection, RedisClusterTemplate};
475
476#[cfg(feature = "template-postgres")]
477pub use database::postgres::{PostgresConnectionString, PostgresTemplate};
478
479#[cfg(feature = "template-mysql")]
480pub use database::mysql::{MysqlConnectionString, MysqlTemplate};
481
482#[cfg(feature = "template-mongodb")]
483pub use database::mongodb::{MongodbConnectionString, MongodbTemplate};
484
485#[cfg(feature = "template-nginx")]
486pub use web::nginx::NginxTemplate;