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::{
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// Map to track customized images for a job
18#[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        // Check if podman command is available
33        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    // Add a method to store and retrieve customized images (e.g., with Python installed)
45    #[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    /// Find a customized image key by prefix
68    #[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        // Look for any key that starts with the prefix
79        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    /// Get a customized image with language-specific dependencies
89    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    /// Set a customized image with language-specific dependencies
115    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    /// Execute a podman command with proper error handling and timeout
140    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); // 6 minutes timeout
146
147        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            // Send input if provided
166            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    // Use a very short timeout for the entire availability check
208    let overall_timeout = std::time::Duration::from_secs(3);
209
210    // Spawn a thread with the timeout to prevent blocking the main thread
211    let handle = std::thread::spawn(move || {
212        // Use safe FD redirection utility to suppress Podman error messages
213        match fd::with_stderr_to_null(|| {
214            // First, check if podman CLI is available as a quick test
215            if cfg!(target_os = "linux") || cfg!(target_os = "macos") {
216                // Try a simple podman version command with a short timeout
217                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                        // Set a very short timeout for the process
228                        let status = std::thread::scope(|_| {
229                            // Try to wait for a short time
230                            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                            // Kill it if it takes too long
240                            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            // Try to run a simple podman command to check if the daemon is responsive
256            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    // Manual implementation of join with timeout
318    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
339// Add container to tracking
340pub fn track_container(id: &str) {
341    if let Ok(mut containers) = RUNNING_CONTAINERS.lock() {
342        containers.push(id.to_string());
343    }
344}
345
346// Remove container from tracking
347pub fn untrack_container(id: &str) {
348    if let Ok(mut containers) = RUNNING_CONTAINERS.lock() {
349        containers.retain(|c| c != id);
350    }
351}
352
353// Clean up all tracked resources
354pub async fn cleanup_resources() {
355    // Use a global timeout for the entire cleanup process
356    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
370// Clean up all tracked containers
371pub async fn cleanup_containers() -> Result<(), String> {
372    // Getting the containers to clean up should not take a long time
373    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    // Process each container with a timeout
402    for container_id in containers_to_cleanup {
403        // First try to stop the container
404        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        // Then try to remove it
432        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        // Always untrack the container whether or not we succeeded to avoid future cleanup attempts
460        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        // Print detailed debugging info
478        wrkflw_logging::info(&format!("Podman: Running container with image: {}", image));
479
480        let timeout_duration = std::time::Duration::from_secs(360); // 6 minutes timeout
481
482        // Run the entire container operation with a timeout
483        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        // Add a timeout for pull operations
501        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                // Return success to allow continuing with existing image
511                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        // Add a timeout for build operations
523        let timeout_duration = std::time::Duration::from_secs(120); // 2 minutes timeout for builds
524
525        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        // Check if we already have a customized image for this language and version
551        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        // Create a temporary Dockerfile for customization
557        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        // Add language-specific setup based on the language
565        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        // Write the Dockerfile
664        std::fs::write(&dockerfile_path, dockerfile_content).map_err(|e| {
665            ContainerError::ContainerStart(format!("Failed to write Dockerfile: {}", e))
666        })?;
667
668        // Build the customized image
669        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        // Store the customized image
674        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
693// Implementation of internal methods
694impl 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        // Generate a unique container name
709        let container_name = format!("wrkflw-{}", uuid::Uuid::new_v4());
710
711        // Build the podman run command and store temporary strings
712        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        // Prepare environment variable strings
717        for (key, value) in env_vars {
718            env_strings.push(format!("{}={}", key, value));
719        }
720
721        // Prepare volume mount strings
722        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        // Skip registry pull for locally-built images (e.g., combined runtime images).
733        if image.starts_with(LOCAL_IMAGE_PREFIX) {
734            args.push("--pull=never");
735        }
736
737        // Only use --rm if we don't want to preserve containers on failure
738        // When preserve_containers_on_failure is true, we skip --rm so failed containers remain
739        if !self.preserve_containers_on_failure {
740            args.insert(1, "--rm"); // Insert after "run"
741        }
742
743        // Add environment variables
744        for env_string in &env_strings {
745            args.push("-e");
746            args.push(env_string);
747        }
748
749        // Add volume mounts
750        for volume_string in &volume_strings {
751            args.push("-v");
752            args.push(volume_string);
753        }
754
755        // Override entrypoint if specified by action.yml
756        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        // Add the image
764        args.push(image);
765
766        // Add the command. If cmd is empty, nothing is appended and the
767        // image's built-in ENTRYPOINT/CMD is used.
768        args.extend(cmd);
769
770        // Track the container (even though we use --rm, track it for consistency)
771        track_container(&container_name);
772
773        // Execute the command
774        let result = self.execute_podman_command(&args, None).await;
775
776        // Handle container cleanup based on result and settings
777        match &result {
778            Ok(output) => {
779                if output.exit_code == 0 {
780                    // Success - always clean up successful containers
781                    if self.preserve_containers_on_failure {
782                        // We didn't use --rm, so manually remove successful container
783                        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                    // If not preserving, container was auto-removed with --rm
809                    untrack_container(&container_name);
810                } else {
811                    // Failed container
812                    if self.preserve_containers_on_failure {
813                        // Failed and we want to preserve - don't clean up but untrack from auto-cleanup
814                        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                        // Failed but we don't want to preserve - container was auto-removed with --rm
821                        untrack_container(&container_name);
822                    }
823                }
824            }
825            Err(_) => {
826                // Command failed to execute properly - clean up if container exists and not preserving
827                if !self.preserve_containers_on_failure {
828                    // Container was created with --rm, so it should be auto-removed
829                    untrack_container(&container_name);
830                } else {
831                    // Container was created without --rm, try to clean it up since execution failed
832                    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// Public accessor functions for testing
915#[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}