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