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 stop(&self) -> Result<()> {
211 use crate::StopCommand;
212
213 StopCommand::new(self.config().name.as_str())
214 .execute()
215 .await?;
216
217 Ok(())
218 }
219
220 async fn remove(&self) -> Result<()> {
222 use crate::RmCommand;
223
224 RmCommand::new(self.config().name.as_str())
225 .force()
226 .volumes()
227 .execute()
228 .await?;
229
230 Ok(())
231 }
232
233 async fn is_running(&self) -> Result<bool> {
235 use crate::PsCommand;
236
237 let output = PsCommand::new()
238 .filter(format!("name={}", &self.config().name))
239 .quiet()
240 .execute()
241 .await?;
242
243 Ok(!output.containers.is_empty())
244 }
245
246 async fn logs(&self, follow: bool, tail: Option<&str>) -> Result<crate::CommandOutput> {
248 use crate::LogsCommand;
249
250 let mut cmd = LogsCommand::new(&self.config().name);
251
252 if follow {
253 cmd = cmd.follow();
254 }
255
256 if let Some(lines) = tail {
257 cmd = cmd.tail(lines);
258 }
259
260 cmd.execute().await.map_err(Into::into)
261 }
262
263 async fn exec(&self, command: Vec<&str>) -> Result<crate::ExecOutput> {
265 use crate::ExecCommand;
266
267 let cmd_vec: Vec<String> = command.iter().map(|s| s.to_string()).collect();
268 let cmd = ExecCommand::new(&self.config().name, cmd_vec);
269
270 cmd.execute().await.map_err(Into::into)
271 }
272}
273
274pub struct TemplateBuilder {
276 config: TemplateConfig,
277}
278
279impl TemplateBuilder {
280 pub fn new(name: impl Into<String>, image: impl Into<String>) -> Self {
282 Self {
283 config: TemplateConfig {
284 name: name.into(),
285 image: image.into(),
286 tag: "latest".to_string(),
287 ports: Vec::new(),
288 env: HashMap::new(),
289 volumes: Vec::new(),
290 network: None,
291 health_check: None,
292 auto_remove: false,
293 memory_limit: None,
294 cpu_limit: None,
295 platform: None,
296 },
297 }
298 }
299
300 pub fn tag(mut self, tag: impl Into<String>) -> Self {
302 self.config.tag = tag.into();
303 self
304 }
305
306 pub fn port(mut self, host: u16, container: u16) -> Self {
308 self.config.ports.push((host, container));
309 self
310 }
311
312 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
314 self.config.env.insert(key.into(), value.into());
315 self
316 }
317
318 pub fn volume(mut self, source: impl Into<String>, target: impl Into<String>) -> Self {
320 self.config.volumes.push(VolumeMount {
321 source: source.into(),
322 target: target.into(),
323 read_only: false,
324 });
325 self
326 }
327
328 pub fn volume_ro(mut self, source: impl Into<String>, target: impl Into<String>) -> Self {
330 self.config.volumes.push(VolumeMount {
331 source: source.into(),
332 target: target.into(),
333 read_only: true,
334 });
335 self
336 }
337
338 pub fn network(mut self, network: impl Into<String>) -> Self {
340 self.config.network = Some(network.into());
341 self
342 }
343
344 pub fn auto_remove(mut self) -> Self {
346 self.config.auto_remove = true;
347 self
348 }
349
350 pub fn memory_limit(mut self, limit: impl Into<String>) -> Self {
352 self.config.memory_limit = Some(limit.into());
353 self
354 }
355
356 pub fn cpu_limit(mut self, limit: impl Into<String>) -> Self {
358 self.config.cpu_limit = Some(limit.into());
359 self
360 }
361
362 pub fn build(self) -> CustomTemplate {
364 CustomTemplate {
365 config: self.config,
366 }
367 }
368}
369
370pub struct CustomTemplate {
372 config: TemplateConfig,
373}
374
375#[async_trait]
376impl Template for CustomTemplate {
377 fn name(&self) -> &str {
378 &self.config.name
379 }
380
381 fn config(&self) -> &TemplateConfig {
382 &self.config
383 }
384
385 fn config_mut(&mut self) -> &mut TemplateConfig {
386 &mut self.config
387 }
388}
389
390#[cfg(feature = "template-redis")]
393pub use redis::RedisTemplate;
394
395#[cfg(feature = "template-redis-cluster")]
396pub use redis::{ClusterInfo, NodeInfo, NodeRole, RedisClusterConnection, RedisClusterTemplate};
397
398#[cfg(feature = "template-postgres")]
399pub use database::postgres::{PostgresConnectionString, PostgresTemplate};
400
401#[cfg(feature = "template-mysql")]
402pub use database::mysql::{MysqlConnectionString, MysqlTemplate};
403
404#[cfg(feature = "template-mongodb")]
405pub use database::mongodb::{MongodbConnectionString, MongodbTemplate};
406
407#[cfg(feature = "template-nginx")]
408pub use web::nginx::NginxTemplate;