1use crate::error::{ContainerError, Result};
6use std::collections::HashMap;
7use std::path::PathBuf;
8use std::time::Duration;
9
10#[derive(Debug, Clone)]
12pub struct VolumeMount {
13 pub host_path: PathBuf,
15 pub container_path: PathBuf,
17 pub read_only: bool,
19}
20
21impl VolumeMount {
22 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 pub fn read_only(mut self) -> Self {
33 self.read_only = true;
34 self
35 }
36}
37
38#[derive(Debug, Clone)]
40pub struct ContainerConfig {
41 pub image: String,
43 pub tag: String,
45 pub env_vars: HashMap<String, String>,
47 pub volumes: Vec<VolumeMount>,
49 pub startup_timeout: Duration,
51 pub command: Option<Vec<String>>,
53 pub cleanup_on_success: bool,
55 pub cleanup_on_failure: bool,
57 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 pub fn new() -> Self {
80 Self::default()
81 }
82
83 pub fn with_image(mut self, image: impl Into<String>) -> Self {
85 self.image = image.into();
86 self
87 }
88
89 pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
91 self.tag = tag.into();
92 self
93 }
94
95 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 pub fn with_envs(mut self, envs: HashMap<String, String>) -> Self {
103 self.env_vars.extend(envs);
104 self
105 }
106
107 pub fn with_volume(mut self, volume: VolumeMount) -> Self {
109 self.volumes.push(volume);
110 self
111 }
112
113 pub fn with_startup_timeout(mut self, timeout: Duration) -> Self {
115 self.startup_timeout = timeout;
116 self
117 }
118
119 pub fn with_command(mut self, command: Vec<String>) -> Self {
121 self.command = Some(command);
122 self
123 }
124
125 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 pub fn image_ref(&self) -> String {
167 format!("{}:{}", self.image, self.tag)
168 }
169
170 pub fn has_volumes(&self) -> bool {
172 !self.volumes.is_empty()
173 }
174
175 pub fn has_env_vars(&self) -> bool {
177 !self.env_vars.is_empty()
178 }
179
180 pub fn cleanup_on_success(mut self, cleanup: bool) -> Self {
182 self.cleanup_on_success = cleanup;
183 self
184 }
185
186 pub fn cleanup_on_failure(mut self, cleanup: bool) -> Self {
188 self.cleanup_on_failure = cleanup;
189 self
190 }
191
192 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}