Skip to main content

wrkflw_executor/
podman.rs

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// Map to track customized images for a job
16#[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        // Check if podman command is available
31        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    // Add a method to store and retrieve customized images (e.g., with Python installed)
43    #[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    /// Find a customized image key by prefix
66    #[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        // Look for any key that starts with the prefix
77        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    /// Get a customized image with language-specific dependencies
87    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    /// Set a customized image with language-specific dependencies
113    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    /// Execute a podman command with proper error handling and timeout
138    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); // 6 minutes timeout
144
145        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            // Send input if provided
164            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    // Use a very short timeout for the entire availability check
206    let overall_timeout = std::time::Duration::from_secs(3);
207
208    // Spawn a thread with the timeout to prevent blocking the main thread
209    let handle = std::thread::spawn(move || {
210        // Use safe FD redirection utility to suppress Podman error messages
211        match fd::with_stderr_to_null(|| {
212            // First, check if podman CLI is available as a quick test
213            if cfg!(target_os = "linux") || cfg!(target_os = "macos") {
214                // Try a simple podman version command with a short timeout
215                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                        // Set a very short timeout for the process
226                        let status = std::thread::scope(|_| {
227                            // Try to wait for a short time
228                            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                            // Kill it if it takes too long
238                            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            // Try to run a simple podman command to check if the daemon is responsive
254            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    // Manual implementation of join with timeout
316    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
337// Add container to tracking
338pub fn track_container(id: &str) {
339    if let Ok(mut containers) = RUNNING_CONTAINERS.lock() {
340        containers.push(id.to_string());
341    }
342}
343
344// Remove container from tracking
345pub fn untrack_container(id: &str) {
346    if let Ok(mut containers) = RUNNING_CONTAINERS.lock() {
347        containers.retain(|c| c != id);
348    }
349}
350
351// Clean up all tracked resources
352pub async fn cleanup_resources() {
353    // Use a global timeout for the entire cleanup process
354    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
368// Clean up all tracked containers
369pub async fn cleanup_containers() -> Result<(), String> {
370    // Getting the containers to clean up should not take a long time
371    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    // Process each container with a timeout
400    for container_id in containers_to_cleanup {
401        // First try to stop the container
402        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        // Then try to remove it
430        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        // Always untrack the container whether or not we succeeded to avoid future cleanup attempts
458        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        // Print detailed debugging info
475        wrkflw_logging::info(&format!("Podman: Running container with image: {}", image));
476
477        let timeout_duration = std::time::Duration::from_secs(360); // 6 minutes timeout
478
479        // Run the entire container operation with a timeout
480        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        // Add a timeout for pull operations
498        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                // Return success to allow continuing with existing image
508                Ok(())
509            }
510        }
511    }
512
513    async fn build_image(&self, dockerfile: &Path, tag: &str) -> Result<(), ContainerError> {
514        // Add a timeout for build operations
515        let timeout_duration = std::time::Duration::from_secs(120); // 2 minutes timeout for builds
516
517        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        // Check if we already have a customized image for this language and version
539        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        // Create a temporary Dockerfile for customization
545        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        // Add language-specific setup based on the language
553        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        // Write the Dockerfile
652        std::fs::write(&dockerfile_path, dockerfile_content).map_err(|e| {
653            ContainerError::ContainerStart(format!("Failed to write Dockerfile: {}", e))
654        })?;
655
656        // Build the customized image
657        let image_tag = format!("wrkflw-{}-{}", language, version.unwrap_or("latest"));
658        self.build_image(&dockerfile_path, &image_tag).await?;
659
660        // Store the customized image
661        Self::set_language_specific_image("", language, version, &image_tag);
662
663        Ok(image_tag)
664    }
665}
666
667// Implementation of internal methods
668impl 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        // Generate a unique container name
682        let container_name = format!("wrkflw-{}", uuid::Uuid::new_v4());
683
684        // Build the podman run command and store temporary strings
685        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        // Prepare environment variable strings
690        for (key, value) in env_vars {
691            env_strings.push(format!("{}={}", key, value));
692        }
693
694        // Prepare volume mount strings
695        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        // Only use --rm if we don't want to preserve containers on failure
706        // When preserve_containers_on_failure is true, we skip --rm so failed containers remain
707        if !self.preserve_containers_on_failure {
708            args.insert(1, "--rm"); // Insert after "run"
709        }
710
711        // Add environment variables
712        for env_string in &env_strings {
713            args.push("-e");
714            args.push(env_string);
715        }
716
717        // Add volume mounts
718        for volume_string in &volume_strings {
719            args.push("-v");
720            args.push(volume_string);
721        }
722
723        // Add the image
724        args.push(image);
725
726        // Add the command
727        args.extend(cmd);
728
729        // Track the container (even though we use --rm, track it for consistency)
730        track_container(&container_name);
731
732        // Execute the command
733        let result = self.execute_podman_command(&args, None).await;
734
735        // Handle container cleanup based on result and settings
736        match &result {
737            Ok(output) => {
738                if output.exit_code == 0 {
739                    // Success - always clean up successful containers
740                    if self.preserve_containers_on_failure {
741                        // We didn't use --rm, so manually remove successful container
742                        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                    // If not preserving, container was auto-removed with --rm
768                    untrack_container(&container_name);
769                } else {
770                    // Failed container
771                    if self.preserve_containers_on_failure {
772                        // Failed and we want to preserve - don't clean up but untrack from auto-cleanup
773                        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                        // Failed but we don't want to preserve - container was auto-removed with --rm
780                        untrack_container(&container_name);
781                    }
782                }
783            }
784            Err(_) => {
785                // Command failed to execute properly - clean up if container exists and not preserving
786                if !self.preserve_containers_on_failure {
787                    // Container was created with --rm, so it should be auto-removed
788                    untrack_container(&container_name);
789                } else {
790                    // Container was created without --rm, try to clean it up since execution failed
791                    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// Public accessor functions for testing
870#[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}