ggen_e2e/
container.rs

1//! Container configuration and lifecycle management
2//!
3//! Manages testcontainer configuration, volume mounts, and lifecycle operations.
4
5use crate::error::{ContainerError, Result};
6use std::collections::HashMap;
7use std::path::PathBuf;
8use std::time::Duration;
9
10/// Volume mount configuration
11#[derive(Debug, Clone)]
12pub struct VolumeMount {
13    /// Host path
14    pub host_path: PathBuf,
15    /// Container path
16    pub container_path: PathBuf,
17    /// Whether mounted as read-only
18    pub read_only: bool,
19}
20
21impl VolumeMount {
22    /// Create a new volume mount
23    pub fn new(host_path: impl Into<PathBuf>, container_path: impl Into<PathBuf>) -> Self {
24        VolumeMount {
25            host_path: host_path.into(),
26            container_path: container_path.into(),
27            read_only: false,
28        }
29    }
30
31    /// Create a read-only volume mount
32    pub fn read_only(mut self) -> Self {
33        self.read_only = true;
34        self
35    }
36}
37
38/// Container configuration for testcontainers
39#[derive(Debug, Clone)]
40pub struct ContainerConfig {
41    /// Docker image name (e.g., "ubuntu")
42    pub image: String,
43    /// Image tag (e.g., "22.04")
44    pub tag: String,
45    /// Environment variables
46    pub env_vars: HashMap<String, String>,
47    /// Volume mounts
48    pub volumes: Vec<VolumeMount>,
49    /// Container startup timeout
50    pub startup_timeout: Duration,
51    /// Command override (entrypoint)
52    pub command: Option<Vec<String>>,
53    /// Container cleanup on success
54    pub cleanup_on_success: bool,
55    /// Container cleanup on failure
56    pub cleanup_on_failure: bool,
57    /// Keep logs before cleanup
58    pub keep_logs: bool,
59}
60
61impl Default for ContainerConfig {
62    fn default() -> Self {
63        ContainerConfig {
64            image: "ubuntu".to_string(),
65            tag: "22.04".to_string(),
66            env_vars: HashMap::new(),
67            volumes: Vec::new(),
68            startup_timeout: Duration::from_secs(120),
69            command: None,
70            cleanup_on_success: true,
71            cleanup_on_failure: true,
72            keep_logs: true,
73        }
74    }
75}
76
77impl ContainerConfig {
78    /// Create a new default container config
79    pub fn new() -> Self {
80        Self::default()
81    }
82
83    /// Set the Docker image
84    pub fn with_image(mut self, image: impl Into<String>) -> Self {
85        self.image = image.into();
86        self
87    }
88
89    /// Set the image tag
90    pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
91        self.tag = tag.into();
92        self
93    }
94
95    /// Add an environment variable
96    pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
97        self.env_vars.insert(key.into(), value.into());
98        self
99    }
100
101    /// Add multiple environment variables
102    pub fn with_envs(mut self, envs: HashMap<String, String>) -> Self {
103        self.env_vars.extend(envs);
104        self
105    }
106
107    /// Add a volume mount
108    pub fn with_volume(mut self, volume: VolumeMount) -> Self {
109        self.volumes.push(volume);
110        self
111    }
112
113    /// Set startup timeout
114    pub fn with_startup_timeout(mut self, timeout: Duration) -> Self {
115        self.startup_timeout = timeout;
116        self
117    }
118
119    /// Set the container command
120    pub fn with_command(mut self, command: Vec<String>) -> Self {
121        self.command = Some(command);
122        self
123    }
124
125    /// Validate container configuration
126    pub fn validate(&self) -> Result<()> {
127        if self.image.is_empty() {
128            return Err(
129                ContainerError::Configuration("Image name cannot be empty".to_string()).into(),
130            );
131        }
132
133        if self.tag.is_empty() {
134            return Err(
135                ContainerError::Configuration("Image tag cannot be empty".to_string()).into(),
136            );
137        }
138
139        for volume in &self.volumes {
140            if !volume.host_path.to_string_lossy().chars().any(|_| true) {
141                return Err(ContainerError::Configuration(format!(
142                    "Invalid host path: {:?}",
143                    volume.host_path
144                ))
145                .into());
146            }
147
148            if !volume
149                .container_path
150                .to_string_lossy()
151                .chars()
152                .any(|_| true)
153            {
154                return Err(ContainerError::Configuration(format!(
155                    "Invalid container path: {:?}",
156                    volume.container_path
157                ))
158                .into());
159            }
160        }
161
162        Ok(())
163    }
164
165    /// Get the full image reference
166    pub fn image_ref(&self) -> String {
167        format!("{}:{}", self.image, self.tag)
168    }
169
170    /// Check if any volumes are configured
171    pub fn has_volumes(&self) -> bool {
172        !self.volumes.is_empty()
173    }
174
175    /// Check if any environment variables are configured
176    pub fn has_env_vars(&self) -> bool {
177        !self.env_vars.is_empty()
178    }
179
180    /// Set cleanup behavior on success
181    pub fn cleanup_on_success(mut self, cleanup: bool) -> Self {
182        self.cleanup_on_success = cleanup;
183        self
184    }
185
186    /// Set cleanup behavior on failure
187    pub fn cleanup_on_failure(mut self, cleanup: bool) -> Self {
188        self.cleanup_on_failure = cleanup;
189        self
190    }
191
192    /// Set whether to keep logs before cleanup
193    pub fn keep_logs(mut self, keep: bool) -> Self {
194        self.keep_logs = keep;
195        self
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202
203    #[test]
204    fn test_volume_mount_creation() {
205        let mount = VolumeMount::new("/host/path", "/container/path");
206        assert_eq!(mount.host_path, PathBuf::from("/host/path"));
207        assert_eq!(mount.container_path, PathBuf::from("/container/path"));
208        assert!(!mount.read_only);
209    }
210
211    #[test]
212    fn test_volume_mount_read_only() {
213        let mount = VolumeMount::new("/host/path", "/container/path").read_only();
214        assert!(mount.read_only);
215    }
216
217    #[test]
218    fn test_container_config_default() {
219        let config = ContainerConfig::default();
220        assert_eq!(config.image, "ubuntu");
221        assert_eq!(config.tag, "22.04");
222        assert_eq!(config.startup_timeout, Duration::from_secs(120));
223        assert!(config.cleanup_on_success);
224        assert!(config.cleanup_on_failure);
225    }
226
227    #[test]
228    fn test_container_config_builder() {
229        let config = ContainerConfig::new()
230            .with_image("alpine")
231            .with_tag("latest")
232            .with_env("KEY", "value")
233            .with_volume(VolumeMount::new("/src", "/dst"));
234
235        assert_eq!(config.image, "alpine");
236        assert_eq!(config.tag, "latest");
237        assert_eq!(config.env_vars.get("KEY"), Some(&"value".to_string()));
238        assert_eq!(config.volumes.len(), 1);
239    }
240
241    #[test]
242    fn test_container_config_image_ref() {
243        let config = ContainerConfig::new()
244            .with_image("ubuntu")
245            .with_tag("22.04");
246        assert_eq!(config.image_ref(), "ubuntu:22.04");
247    }
248
249    #[test]
250    fn test_container_config_validate() {
251        let config = ContainerConfig::new()
252            .with_image("ubuntu")
253            .with_tag("22.04")
254            .with_volume(VolumeMount::new("/host", "/container"));
255
256        assert!(config.validate().is_ok());
257    }
258
259    #[test]
260    fn test_container_config_has_volumes() {
261        let config1 = ContainerConfig::new();
262        assert!(!config1.has_volumes());
263
264        let config2 = ContainerConfig::new().with_volume(VolumeMount::new("/host", "/container"));
265        assert!(config2.has_volumes());
266    }
267
268    #[test]
269    fn test_container_config_has_env_vars() {
270        let config1 = ContainerConfig::new();
271        assert!(!config1.has_env_vars());
272
273        let config2 = ContainerConfig::new().with_env("KEY", "value");
274        assert!(config2.has_env_vars());
275    }
276}