1use async_trait::async_trait;
2use once_cell::sync::Lazy;
3use std::collections::HashMap;
4use std::path::Path;
5use std::process::Stdio;
6use std::sync::Mutex;
7use tempfile;
8use tokio::process::Command;
9use wrkflw_logging;
10use wrkflw_runtime::container::{ContainerError, ContainerOutput, ContainerRuntime};
11use wrkflw_utils;
12use wrkflw_utils::fd;
13
14static RUNNING_CONTAINERS: Lazy<Mutex<Vec<String>>> = Lazy::new(|| Mutex::new(Vec::new()));
15#[allow(dead_code)]
17static CUSTOMIZED_IMAGES: Lazy<Mutex<HashMap<String, String>>> =
18 Lazy::new(|| Mutex::new(HashMap::new()));
19
20pub struct PodmanRuntime {
21 preserve_containers_on_failure: bool,
22}
23
24impl PodmanRuntime {
25 pub fn new() -> Result<Self, ContainerError> {
26 Self::new_with_config(false)
27 }
28
29 pub fn new_with_config(preserve_containers_on_failure: bool) -> Result<Self, ContainerError> {
30 if !is_available() {
32 return Err(ContainerError::ContainerStart(
33 "Podman is not available on this system".to_string(),
34 ));
35 }
36
37 Ok(PodmanRuntime {
38 preserve_containers_on_failure,
39 })
40 }
41
42 #[allow(dead_code)]
44 pub fn get_customized_image(base_image: &str, customization: &str) -> Option<String> {
45 let key = format!("{}:{}", base_image, customization);
46 match CUSTOMIZED_IMAGES.lock() {
47 Ok(images) => images.get(&key).cloned(),
48 Err(e) => {
49 wrkflw_logging::error(&format!("Failed to acquire lock: {}", e));
50 None
51 }
52 }
53 }
54
55 #[allow(dead_code)]
56 pub fn set_customized_image(base_image: &str, customization: &str, new_image: &str) {
57 let key = format!("{}:{}", base_image, customization);
58 if let Err(e) = CUSTOMIZED_IMAGES.lock().map(|mut images| {
59 images.insert(key, new_image.to_string());
60 }) {
61 wrkflw_logging::error(&format!("Failed to acquire lock: {}", e));
62 }
63 }
64
65 #[allow(dead_code)]
67 pub fn find_customized_image_key(image: &str, prefix: &str) -> Option<String> {
68 let image_keys = match CUSTOMIZED_IMAGES.lock() {
69 Ok(keys) => keys,
70 Err(e) => {
71 wrkflw_logging::error(&format!("Failed to acquire lock: {}", e));
72 return None;
73 }
74 };
75
76 for (key, _) in image_keys.iter() {
78 if key.starts_with(prefix) {
79 return Some(key.clone());
80 }
81 }
82
83 None
84 }
85
86 pub fn get_language_specific_image(
88 base_image: &str,
89 language: &str,
90 version: Option<&str>,
91 ) -> Option<String> {
92 let key = match (language, version) {
93 ("python", Some(ver)) => format!("python:{}", ver),
94 ("node", Some(ver)) => format!("node:{}", ver),
95 ("java", Some(ver)) => format!("eclipse-temurin:{}", ver),
96 ("go", Some(ver)) => format!("golang:{}", ver),
97 ("dotnet", Some(ver)) => format!("mcr.microsoft.com/dotnet/sdk:{}", ver),
98 ("rust", Some(ver)) => format!("rust:{}", ver),
99 (lang, Some(ver)) => format!("{}:{}", lang, ver),
100 (lang, None) => lang.to_string(),
101 };
102
103 match CUSTOMIZED_IMAGES.lock() {
104 Ok(images) => images.get(&key).cloned(),
105 Err(e) => {
106 wrkflw_logging::error(&format!("Failed to acquire lock: {}", e));
107 None
108 }
109 }
110 }
111
112 pub fn set_language_specific_image(
114 base_image: &str,
115 language: &str,
116 version: Option<&str>,
117 new_image: &str,
118 ) {
119 let key = match (language, version) {
120 ("python", Some(ver)) => format!("python:{}", ver),
121 ("node", Some(ver)) => format!("node:{}", ver),
122 ("java", Some(ver)) => format!("eclipse-temurin:{}", ver),
123 ("go", Some(ver)) => format!("golang:{}", ver),
124 ("dotnet", Some(ver)) => format!("mcr.microsoft.com/dotnet/sdk:{}", ver),
125 ("rust", Some(ver)) => format!("rust:{}", ver),
126 (lang, Some(ver)) => format!("{}:{}", lang, ver),
127 (lang, None) => lang.to_string(),
128 };
129
130 if let Err(e) = CUSTOMIZED_IMAGES.lock().map(|mut images| {
131 images.insert(key, new_image.to_string());
132 }) {
133 wrkflw_logging::error(&format!("Failed to acquire lock: {}", e));
134 }
135 }
136
137 async fn execute_podman_command(
139 &self,
140 args: &[&str],
141 input: Option<&str>,
142 ) -> Result<ContainerOutput, ContainerError> {
143 let timeout_duration = std::time::Duration::from_secs(360); let result = tokio::time::timeout(timeout_duration, async {
146 let mut cmd = Command::new("podman");
147 cmd.args(args);
148
149 if input.is_some() {
150 cmd.stdin(Stdio::piped());
151 }
152 cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
153
154 wrkflw_logging::debug(&format!(
155 "Running Podman command: podman {}",
156 args.join(" ")
157 ));
158
159 let mut child = cmd.spawn().map_err(|e| {
160 ContainerError::ContainerStart(format!("Failed to spawn podman command: {}", e))
161 })?;
162
163 if let Some(input_data) = input {
165 if let Some(stdin) = child.stdin.take() {
166 use tokio::io::AsyncWriteExt;
167 let mut stdin = stdin;
168 stdin.write_all(input_data.as_bytes()).await.map_err(|e| {
169 ContainerError::ContainerExecution(format!(
170 "Failed to write to stdin: {}",
171 e
172 ))
173 })?;
174 stdin.shutdown().await.map_err(|e| {
175 ContainerError::ContainerExecution(format!("Failed to close stdin: {}", e))
176 })?;
177 }
178 }
179
180 let output = child.wait_with_output().await.map_err(|e| {
181 ContainerError::ContainerExecution(format!("Podman command failed: {}", e))
182 })?;
183
184 Ok(ContainerOutput {
185 stdout: String::from_utf8_lossy(&output.stdout).to_string(),
186 stderr: String::from_utf8_lossy(&output.stderr).to_string(),
187 exit_code: output.status.code().unwrap_or(-1),
188 })
189 })
190 .await;
191
192 match result {
193 Ok(output) => output,
194 Err(_) => {
195 wrkflw_logging::error("Podman operation timed out after 360 seconds");
196 Err(ContainerError::ContainerExecution(
197 "Operation timed out".to_string(),
198 ))
199 }
200 }
201 }
202}
203
204pub fn is_available() -> bool {
205 let overall_timeout = std::time::Duration::from_secs(3);
207
208 let handle = std::thread::spawn(move || {
210 match fd::with_stderr_to_null(|| {
212 if cfg!(target_os = "linux") || cfg!(target_os = "macos") {
214 let process = std::process::Command::new("podman")
216 .arg("version")
217 .arg("--format")
218 .arg("{{.Version}}")
219 .stdout(std::process::Stdio::null())
220 .stderr(std::process::Stdio::null())
221 .spawn();
222
223 match process {
224 Ok(mut child) => {
225 let status = std::thread::scope(|_| {
227 for _ in 0..10 {
229 match child.try_wait() {
230 Ok(Some(status)) => return status.success(),
231 Ok(None) => {
232 std::thread::sleep(std::time::Duration::from_millis(100))
233 }
234 Err(_) => return false,
235 }
236 }
237 let _ = child.kill();
239 false
240 });
241
242 if !status {
243 return false;
244 }
245 }
246 Err(_) => {
247 wrkflw_logging::debug("Podman CLI is not available");
248 return false;
249 }
250 }
251 }
252
253 let runtime = match tokio::runtime::Builder::new_current_thread()
255 .enable_all()
256 .build()
257 {
258 Ok(rt) => rt,
259 Err(e) => {
260 wrkflw_logging::error(&format!(
261 "Failed to create runtime for Podman availability check: {}",
262 e
263 ));
264 return false;
265 }
266 };
267
268 runtime.block_on(async {
269 match tokio::time::timeout(std::time::Duration::from_secs(2), async {
270 let mut cmd = Command::new("podman");
271 cmd.args(["info", "--format", "{{.Host.Hostname}}"]);
272 cmd.stdout(Stdio::null()).stderr(Stdio::null());
273
274 match tokio::time::timeout(std::time::Duration::from_secs(1), cmd.output())
275 .await
276 {
277 Ok(Ok(output)) => {
278 if output.status.success() {
279 true
280 } else {
281 wrkflw_logging::debug("Podman info command failed");
282 false
283 }
284 }
285 Ok(Err(e)) => {
286 wrkflw_logging::debug(&format!("Podman info command error: {}", e));
287 false
288 }
289 Err(_) => {
290 wrkflw_logging::debug("Podman info command timed out after 1 second");
291 false
292 }
293 }
294 })
295 .await
296 {
297 Ok(result) => result,
298 Err(_) => {
299 wrkflw_logging::debug("Podman availability check timed out");
300 false
301 }
302 }
303 })
304 }) {
305 Ok(result) => result,
306 Err(_) => {
307 wrkflw_logging::debug(
308 "Failed to redirect stderr when checking Podman availability",
309 );
310 false
311 }
312 }
313 });
314
315 let start = std::time::Instant::now();
317
318 while start.elapsed() < overall_timeout {
319 if handle.is_finished() {
320 return match handle.join() {
321 Ok(result) => result,
322 Err(_) => {
323 wrkflw_logging::warning("Podman availability check thread panicked");
324 false
325 }
326 };
327 }
328 std::thread::sleep(std::time::Duration::from_millis(50));
329 }
330
331 wrkflw_logging::warning(
332 "Podman availability check timed out, assuming Podman is not available",
333 );
334 false
335}
336
337pub fn track_container(id: &str) {
339 if let Ok(mut containers) = RUNNING_CONTAINERS.lock() {
340 containers.push(id.to_string());
341 }
342}
343
344pub fn untrack_container(id: &str) {
346 if let Ok(mut containers) = RUNNING_CONTAINERS.lock() {
347 containers.retain(|c| c != id);
348 }
349}
350
351pub async fn cleanup_resources() {
353 let cleanup_timeout = std::time::Duration::from_secs(5);
355
356 match tokio::time::timeout(cleanup_timeout, cleanup_containers()).await {
357 Ok(result) => {
358 if let Err(e) = result {
359 wrkflw_logging::error(&format!("Error during container cleanup: {}", e));
360 }
361 }
362 Err(_) => wrkflw_logging::warning(
363 "Podman cleanup timed out, some resources may not have been removed",
364 ),
365 }
366}
367
368pub async fn cleanup_containers() -> Result<(), String> {
370 let containers_to_cleanup =
372 match tokio::time::timeout(std::time::Duration::from_millis(500), async {
373 match RUNNING_CONTAINERS.try_lock() {
374 Ok(containers) => containers.clone(),
375 Err(_) => {
376 wrkflw_logging::error("Could not acquire container lock for cleanup");
377 vec![]
378 }
379 }
380 })
381 .await
382 {
383 Ok(containers) => containers,
384 Err(_) => {
385 wrkflw_logging::error("Timeout while trying to get containers for cleanup");
386 vec![]
387 }
388 };
389
390 if containers_to_cleanup.is_empty() {
391 return Ok(());
392 }
393
394 wrkflw_logging::info(&format!(
395 "Cleaning up {} containers",
396 containers_to_cleanup.len()
397 ));
398
399 for container_id in containers_to_cleanup {
401 let stop_result = tokio::time::timeout(
403 std::time::Duration::from_millis(1000),
404 Command::new("podman")
405 .args(["stop", &container_id])
406 .stdout(Stdio::null())
407 .stderr(Stdio::null())
408 .output(),
409 )
410 .await;
411
412 match stop_result {
413 Ok(Ok(output)) => {
414 if output.status.success() {
415 wrkflw_logging::debug(&format!("Stopped container: {}", container_id));
416 } else {
417 wrkflw_logging::warning(&format!("Error stopping container {}", container_id));
418 }
419 }
420 Ok(Err(e)) => wrkflw_logging::warning(&format!(
421 "Error stopping container {}: {}",
422 container_id, e
423 )),
424 Err(_) => {
425 wrkflw_logging::warning(&format!("Timeout stopping container: {}", container_id))
426 }
427 }
428
429 let remove_result = tokio::time::timeout(
431 std::time::Duration::from_millis(1000),
432 Command::new("podman")
433 .args(["rm", &container_id])
434 .stdout(Stdio::null())
435 .stderr(Stdio::null())
436 .output(),
437 )
438 .await;
439
440 match remove_result {
441 Ok(Ok(output)) => {
442 if output.status.success() {
443 wrkflw_logging::debug(&format!("Removed container: {}", container_id));
444 } else {
445 wrkflw_logging::warning(&format!("Error removing container {}", container_id));
446 }
447 }
448 Ok(Err(e)) => wrkflw_logging::warning(&format!(
449 "Error removing container {}: {}",
450 container_id, e
451 )),
452 Err(_) => {
453 wrkflw_logging::warning(&format!("Timeout removing container: {}", container_id))
454 }
455 }
456
457 untrack_container(&container_id);
459 }
460
461 Ok(())
462}
463
464#[async_trait]
465impl ContainerRuntime for PodmanRuntime {
466 async fn run_container(
467 &self,
468 image: &str,
469 cmd: &[&str],
470 env_vars: &[(&str, &str)],
471 working_dir: &Path,
472 volumes: &[(&Path, &Path)],
473 ) -> Result<ContainerOutput, ContainerError> {
474 wrkflw_logging::info(&format!("Podman: Running container with image: {}", image));
476
477 let timeout_duration = std::time::Duration::from_secs(360); match tokio::time::timeout(
481 timeout_duration,
482 self.run_container_inner(image, cmd, env_vars, working_dir, volumes),
483 )
484 .await
485 {
486 Ok(result) => result,
487 Err(_) => {
488 wrkflw_logging::error("Podman operation timed out after 360 seconds");
489 Err(ContainerError::ContainerExecution(
490 "Operation timed out".to_string(),
491 ))
492 }
493 }
494 }
495
496 async fn pull_image(&self, image: &str) -> Result<(), ContainerError> {
497 let timeout_duration = std::time::Duration::from_secs(30);
499
500 match tokio::time::timeout(timeout_duration, self.pull_image_inner(image)).await {
501 Ok(result) => result,
502 Err(_) => {
503 wrkflw_logging::warning(&format!(
504 "Pull of image {} timed out, continuing with existing image",
505 image
506 ));
507 Ok(())
509 }
510 }
511 }
512
513 async fn build_image(&self, dockerfile: &Path, tag: &str) -> Result<(), ContainerError> {
514 let timeout_duration = std::time::Duration::from_secs(120); match tokio::time::timeout(timeout_duration, self.build_image_inner(dockerfile, tag)).await
518 {
519 Ok(result) => result,
520 Err(_) => {
521 wrkflw_logging::error(&format!(
522 "Building image {} timed out after 120 seconds",
523 tag
524 ));
525 Err(ContainerError::ImageBuild(
526 "Operation timed out".to_string(),
527 ))
528 }
529 }
530 }
531
532 async fn prepare_language_environment(
533 &self,
534 language: &str,
535 version: Option<&str>,
536 additional_packages: Option<Vec<String>>,
537 ) -> Result<String, ContainerError> {
538 let key = format!("{}-{}", language, version.unwrap_or("latest"));
540 if let Some(customized_image) = Self::get_language_specific_image("", language, version) {
541 return Ok(customized_image);
542 }
543
544 let temp_dir = tempfile::tempdir().map_err(|e| {
546 ContainerError::ContainerStart(format!("Failed to create temp directory: {}", e))
547 })?;
548
549 let dockerfile_path = temp_dir.path().join("Dockerfile");
550 let mut dockerfile_content = String::new();
551
552 match language {
554 "python" => {
555 let base_image =
556 version.map_or("python:3.11-slim".to_string(), |v| format!("python:{}", v));
557 dockerfile_content.push_str(&format!("FROM {}\n\n", base_image));
558 dockerfile_content.push_str(
559 "RUN apt-get update && apt-get install -y --no-install-recommends \\\n",
560 );
561 dockerfile_content.push_str(" build-essential \\\n");
562 dockerfile_content.push_str(" && rm -rf /var/lib/apt/lists/*\n");
563
564 if let Some(packages) = additional_packages {
565 for package in packages {
566 dockerfile_content.push_str(&format!("RUN pip install {}\n", package));
567 }
568 }
569 }
570 "node" => {
571 let base_image =
572 version.map_or("node:20-slim".to_string(), |v| format!("node:{}", v));
573 dockerfile_content.push_str(&format!("FROM {}\n\n", base_image));
574 dockerfile_content.push_str(
575 "RUN apt-get update && apt-get install -y --no-install-recommends \\\n",
576 );
577 dockerfile_content.push_str(" build-essential \\\n");
578 dockerfile_content.push_str(" && rm -rf /var/lib/apt/lists/*\n");
579
580 if let Some(packages) = additional_packages {
581 for package in packages {
582 dockerfile_content.push_str(&format!("RUN npm install -g {}\n", package));
583 }
584 }
585 }
586 "java" => {
587 let base_image = version.map_or("eclipse-temurin:17-jdk".to_string(), |v| {
588 format!("eclipse-temurin:{}", v)
589 });
590 dockerfile_content.push_str(&format!("FROM {}\n\n", base_image));
591 dockerfile_content.push_str(
592 "RUN apt-get update && apt-get install -y --no-install-recommends \\\n",
593 );
594 dockerfile_content.push_str(" maven \\\n");
595 dockerfile_content.push_str(" && rm -rf /var/lib/apt/lists/*\n");
596 }
597 "go" => {
598 let base_image =
599 version.map_or("golang:1.21-slim".to_string(), |v| format!("golang:{}", v));
600 dockerfile_content.push_str(&format!("FROM {}\n\n", base_image));
601 dockerfile_content.push_str(
602 "RUN apt-get update && apt-get install -y --no-install-recommends \\\n",
603 );
604 dockerfile_content.push_str(" git \\\n");
605 dockerfile_content.push_str(" && rm -rf /var/lib/apt/lists/*\n");
606
607 if let Some(packages) = additional_packages {
608 for package in packages {
609 dockerfile_content.push_str(&format!("RUN go install {}\n", package));
610 }
611 }
612 }
613 "dotnet" => {
614 let base_image = version
615 .map_or("mcr.microsoft.com/dotnet/sdk:7.0".to_string(), |v| {
616 format!("mcr.microsoft.com/dotnet/sdk:{}", v)
617 });
618 dockerfile_content.push_str(&format!("FROM {}\n\n", base_image));
619
620 if let Some(packages) = additional_packages {
621 for package in packages {
622 dockerfile_content
623 .push_str(&format!("RUN dotnet tool install -g {}\n", package));
624 }
625 }
626 }
627 "rust" => {
628 let base_image =
629 version.map_or("rust:latest".to_string(), |v| format!("rust:{}", v));
630 dockerfile_content.push_str(&format!("FROM {}\n\n", base_image));
631 dockerfile_content.push_str(
632 "RUN apt-get update && apt-get install -y --no-install-recommends \\\n",
633 );
634 dockerfile_content.push_str(" build-essential \\\n");
635 dockerfile_content.push_str(" && rm -rf /var/lib/apt/lists/*\n");
636
637 if let Some(packages) = additional_packages {
638 for package in packages {
639 dockerfile_content.push_str(&format!("RUN cargo install {}\n", package));
640 }
641 }
642 }
643 _ => {
644 return Err(ContainerError::ContainerStart(format!(
645 "Unsupported language: {}",
646 language
647 )));
648 }
649 }
650
651 std::fs::write(&dockerfile_path, dockerfile_content).map_err(|e| {
653 ContainerError::ContainerStart(format!("Failed to write Dockerfile: {}", e))
654 })?;
655
656 let image_tag = format!("wrkflw-{}-{}", language, version.unwrap_or("latest"));
658 self.build_image(&dockerfile_path, &image_tag).await?;
659
660 Self::set_language_specific_image("", language, version, &image_tag);
662
663 Ok(image_tag)
664 }
665}
666
667impl PodmanRuntime {
669 async fn run_container_inner(
670 &self,
671 image: &str,
672 cmd: &[&str],
673 env_vars: &[(&str, &str)],
674 working_dir: &Path,
675 volumes: &[(&Path, &Path)],
676 ) -> Result<ContainerOutput, ContainerError> {
677 wrkflw_logging::debug(&format!("Running command in Podman: {:?}", cmd));
678 wrkflw_logging::debug(&format!("Environment: {:?}", env_vars));
679 wrkflw_logging::debug(&format!("Working directory: {}", working_dir.display()));
680
681 let container_name = format!("wrkflw-{}", uuid::Uuid::new_v4());
683
684 let working_dir_str = working_dir.to_string_lossy().to_string();
686 let mut env_strings = Vec::new();
687 let mut volume_strings = Vec::new();
688
689 for (key, value) in env_vars {
691 env_strings.push(format!("{}={}", key, value));
692 }
693
694 for (host_path, container_path) in volumes {
696 volume_strings.push(format!(
697 "{}:{}",
698 host_path.to_string_lossy(),
699 container_path.to_string_lossy()
700 ));
701 }
702
703 let mut args = vec!["run", "--name", &container_name, "-w", &working_dir_str];
704
705 if !self.preserve_containers_on_failure {
708 args.insert(1, "--rm"); }
710
711 for env_string in &env_strings {
713 args.push("-e");
714 args.push(env_string);
715 }
716
717 for volume_string in &volume_strings {
719 args.push("-v");
720 args.push(volume_string);
721 }
722
723 args.push(image);
725
726 args.extend(cmd);
728
729 track_container(&container_name);
731
732 let result = self.execute_podman_command(&args, None).await;
734
735 match &result {
737 Ok(output) => {
738 if output.exit_code == 0 {
739 if self.preserve_containers_on_failure {
741 let cleanup_result = tokio::time::timeout(
743 std::time::Duration::from_millis(1000),
744 Command::new("podman")
745 .args(["rm", &container_name])
746 .stdout(Stdio::null())
747 .stderr(Stdio::null())
748 .output(),
749 )
750 .await;
751
752 match cleanup_result {
753 Ok(Ok(cleanup_output)) => {
754 if !cleanup_output.status.success() {
755 wrkflw_logging::debug(&format!(
756 "Failed to remove successful container {}",
757 container_name
758 ));
759 }
760 }
761 _ => wrkflw_logging::debug(&format!(
762 "Timeout removing successful container {}",
763 container_name
764 )),
765 }
766 }
767 untrack_container(&container_name);
769 } else {
770 if self.preserve_containers_on_failure {
772 wrkflw_logging::info(&format!(
774 "Preserving failed container {} for debugging (exit code: {}). Use 'podman exec -it {} bash' to inspect.",
775 container_name, output.exit_code, container_name
776 ));
777 untrack_container(&container_name);
778 } else {
779 untrack_container(&container_name);
781 }
782 }
783 }
784 Err(_) => {
785 if !self.preserve_containers_on_failure {
787 untrack_container(&container_name);
789 } else {
790 let cleanup_result = tokio::time::timeout(
792 std::time::Duration::from_millis(1000),
793 Command::new("podman")
794 .args(["rm", "-f", &container_name])
795 .stdout(Stdio::null())
796 .stderr(Stdio::null())
797 .output(),
798 )
799 .await;
800
801 match cleanup_result {
802 Ok(Ok(_)) => wrkflw_logging::debug(&format!(
803 "Cleaned up failed execution container {}",
804 container_name
805 )),
806 _ => wrkflw_logging::debug(&format!(
807 "Failed to clean up execution failure container {}",
808 container_name
809 )),
810 }
811 untrack_container(&container_name);
812 }
813 }
814 }
815
816 match &result {
817 Ok(output) => {
818 if output.exit_code != 0 {
819 wrkflw_logging::info(&format!(
820 "Podman command failed with exit code: {}",
821 output.exit_code
822 ));
823 wrkflw_logging::debug(&format!("Failed command: {:?}", cmd));
824 wrkflw_logging::debug(&format!("Working directory: {}", working_dir.display()));
825 wrkflw_logging::debug(&format!("STDERR: {}", output.stderr));
826 }
827 }
828 Err(e) => {
829 wrkflw_logging::error(&format!("Podman execution error: {}", e));
830 }
831 }
832
833 result
834 }
835
836 async fn pull_image_inner(&self, image: &str) -> Result<(), ContainerError> {
837 let args = vec!["pull", image];
838 let output = self.execute_podman_command(&args, None).await?;
839
840 if output.exit_code != 0 {
841 return Err(ContainerError::ImagePull(format!(
842 "Failed to pull image {}: {}",
843 image, output.stderr
844 )));
845 }
846
847 Ok(())
848 }
849
850 async fn build_image_inner(&self, dockerfile: &Path, tag: &str) -> Result<(), ContainerError> {
851 let context_dir = dockerfile.parent().unwrap_or(Path::new("."));
852 let dockerfile_str = dockerfile.to_string_lossy().to_string();
853 let context_dir_str = context_dir.to_string_lossy().to_string();
854 let args = vec!["build", "-f", &dockerfile_str, "-t", tag, &context_dir_str];
855
856 let output = self.execute_podman_command(&args, None).await?;
857
858 if output.exit_code != 0 {
859 return Err(ContainerError::ImageBuild(format!(
860 "Failed to build image {}: {}",
861 tag, output.stderr
862 )));
863 }
864
865 Ok(())
866 }
867}
868
869#[cfg(test)]
871pub fn get_tracked_containers() -> Vec<String> {
872 if let Ok(containers) = RUNNING_CONTAINERS.lock() {
873 containers.clone()
874 } else {
875 vec![]
876 }
877}