docker_wrapper/template/
mod.rs

1//! Docker template system for common container configurations
2//!
3//! This module provides pre-configured templates for common Docker setups,
4//! making it easy to spin up development environments with best practices.
5
6#![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// Redis templates
18#[cfg(any(feature = "template-redis", feature = "template-redis-cluster"))]
19pub mod redis;
20
21// Database templates
22#[cfg(any(
23    feature = "template-postgres",
24    feature = "template-mysql",
25    feature = "template-mongodb"
26))]
27pub mod database;
28
29// Web server templates
30#[cfg(feature = "template-nginx")]
31pub mod web;
32
33/// Result type for template operations
34pub type Result<T> = std::result::Result<T, TemplateError>;
35
36/// Template-specific errors
37#[derive(Debug, thiserror::Error)]
38pub enum TemplateError {
39    /// Docker command execution failed
40    #[error("Docker command failed: {0}")]
41    DockerError(#[from] crate::Error),
42
43    /// Invalid template configuration provided
44    #[error("Invalid configuration: {0}")]
45    InvalidConfig(String),
46
47    /// Attempted to start a template that is already running
48    #[error("Template already running: {0}")]
49    AlreadyRunning(String),
50
51    /// Attempted to operate on a template that is not running
52    #[error("Template not running: {0}")]
53    NotRunning(String),
54}
55
56/// Configuration for a Docker template
57#[derive(Debug, Clone)]
58pub struct TemplateConfig {
59    /// Container name
60    pub name: String,
61
62    /// Image to use
63    pub image: String,
64
65    /// Image tag
66    pub tag: String,
67
68    /// Port mappings (host -> container)
69    pub ports: Vec<(u16, u16)>,
70
71    /// Environment variables
72    pub env: HashMap<String, String>,
73
74    /// Volume mounts
75    pub volumes: Vec<VolumeMount>,
76
77    /// Network to connect to
78    pub network: Option<String>,
79
80    /// Health check configuration
81    pub health_check: Option<HealthCheck>,
82
83    /// Whether to remove container on stop
84    pub auto_remove: bool,
85
86    /// Memory limit
87    pub memory_limit: Option<String>,
88
89    /// CPU limit
90    pub cpu_limit: Option<String>,
91
92    /// Platform specification (e.g., "linux/amd64", "linux/arm64")
93    pub platform: Option<String>,
94}
95
96/// Volume mount configuration
97#[derive(Debug, Clone)]
98pub struct VolumeMount {
99    /// Source (host path or volume name)
100    pub source: String,
101
102    /// Target (container path)
103    pub target: String,
104
105    /// Read-only mount
106    pub read_only: bool,
107}
108
109/// Health check configuration
110#[derive(Debug, Clone)]
111pub struct HealthCheck {
112    /// Command to run for health check
113    pub test: Vec<String>,
114
115    /// Time between checks
116    pub interval: String,
117
118    /// Maximum time to wait for check
119    pub timeout: String,
120
121    /// Number of retries before marking unhealthy
122    pub retries: i32,
123
124    /// Start period for container initialization
125    pub start_period: String,
126}
127
128/// Trait for Docker container templates
129#[async_trait]
130pub trait Template: Send + Sync {
131    /// Get the template name
132    fn name(&self) -> &str;
133
134    /// Get the template configuration
135    fn config(&self) -> &TemplateConfig;
136
137    /// Get a mutable reference to the configuration
138    fn config_mut(&mut self) -> &mut TemplateConfig;
139
140    /// Build the RunCommand for this template
141    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        // Add port mappings
148        for (host, container) in &config.ports {
149            cmd = cmd.port(*host, *container);
150        }
151
152        // Add environment variables
153        for (key, value) in &config.env {
154            cmd = cmd.env(key, value);
155        }
156
157        // Add volume mounts
158        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        // Add network
167        if let Some(network) = &config.network {
168            cmd = cmd.network(network);
169        }
170
171        // Add health check
172        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        // Add resource limits
182        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        // Add platform if specified
191        if let Some(platform) = &config.platform {
192            cmd = cmd.platform(platform);
193        }
194
195        // Auto-remove
196        if config.auto_remove {
197            cmd = cmd.remove();
198        }
199
200        cmd
201    }
202
203    /// Start the container with this template
204    async fn start(&self) -> Result<String> {
205        let output = self.build_command().execute().await?;
206        Ok(output.0)
207    }
208
209    /// Stop the container
210    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    /// Remove the container
221    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    /// Check if the container is running
234    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    /// Get container logs
247    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    /// Execute a command in the running container
264    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
274/// Builder for creating custom templates
275pub struct TemplateBuilder {
276    config: TemplateConfig,
277}
278
279impl TemplateBuilder {
280    /// Create a new template builder
281    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    /// Set the image tag
301    pub fn tag(mut self, tag: impl Into<String>) -> Self {
302        self.config.tag = tag.into();
303        self
304    }
305
306    /// Add a port mapping
307    pub fn port(mut self, host: u16, container: u16) -> Self {
308        self.config.ports.push((host, container));
309        self
310    }
311
312    /// Add an environment variable
313    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    /// Add a volume mount
319    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    /// Add a read-only volume mount
329    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    /// Set the network
339    pub fn network(mut self, network: impl Into<String>) -> Self {
340        self.config.network = Some(network.into());
341        self
342    }
343
344    /// Enable auto-remove
345    pub fn auto_remove(mut self) -> Self {
346        self.config.auto_remove = true;
347        self
348    }
349
350    /// Set memory limit
351    pub fn memory_limit(mut self, limit: impl Into<String>) -> Self {
352        self.config.memory_limit = Some(limit.into());
353        self
354    }
355
356    /// Set CPU limit
357    pub fn cpu_limit(mut self, limit: impl Into<String>) -> Self {
358        self.config.cpu_limit = Some(limit.into());
359        self
360    }
361
362    /// Build into a custom template
363    pub fn build(self) -> CustomTemplate {
364        CustomTemplate {
365            config: self.config,
366        }
367    }
368}
369
370/// A custom template created from `TemplateBuilder`
371pub 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// Compatibility re-exports for backward compatibility
391// These allow users to still import directly from template::
392#[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;