clnrm_core/services/
generic.rs

1//! Generic container service plugin
2//!
3//! Provides a generic container service that can run any Docker image
4//! with configurable environment variables, ports, and commands.
5
6use crate::cleanroom::{ServicePlugin, ServiceHandle, HealthStatus};
7use crate::error::{CleanroomError, Result};
8use std::collections::HashMap;
9use std::sync::Arc;
10use tokio::sync::RwLock;
11use uuid::Uuid;
12use testcontainers::runners::AsyncRunner;
13use testcontainers::{GenericImage, ImageExt};
14use std::future::Future;
15use std::pin::Pin;
16
17pub struct GenericContainerPlugin {
18    name: String,
19    image: String,
20    tag: String,
21    container_id: Arc<RwLock<Option<String>>>,
22    env_vars: HashMap<String, String>,
23    ports: Vec<u16>,
24}
25
26impl GenericContainerPlugin {
27    pub fn new(name: &str, image: &str) -> Self {
28        let (image_name, image_tag) = if let Some((name, tag)) = image.split_once(':') {
29            (name.to_string(), tag.to_string())
30        } else {
31            (image.to_string(), "latest".to_string())
32        };
33
34        Self {
35            name: name.to_string(),
36            image: image_name,
37            tag: image_tag,
38            container_id: Arc::new(RwLock::new(None)),
39            env_vars: HashMap::new(),
40            ports: Vec::new(),
41        }
42    }
43
44    pub fn with_env(mut self, key: &str, value: &str) -> Self {
45        self.env_vars.insert(key.to_string(), value.to_string());
46        self
47    }
48
49    pub fn with_port(mut self, port: u16) -> Self {
50        self.ports.push(port);
51        self
52    }
53
54    async fn verify_connection(&self, _host_port: u16) -> Result<()> {
55        // For generic containers, we assume they're healthy if they start successfully
56        // Specific health checks would need to be implemented per container type
57        Ok(())
58    }
59}
60
61impl ServicePlugin for GenericContainerPlugin {
62    fn name(&self) -> &str {
63        &self.name
64    }
65
66    fn start(&self) -> Pin<Box<dyn Future<Output = Result<ServiceHandle>> + Send + '_>> {
67        Box::pin(async move {
68            // Create container configuration
69            let image = GenericImage::new(self.image.clone(), self.tag.clone());
70
71            // Build container request with environment variables and ports
72            let mut container_request: testcontainers::core::ContainerRequest<GenericImage> = image.into();
73
74            // Add environment variables
75            for (key, value) in &self.env_vars {
76                container_request = container_request.with_env_var(key, value);
77            }
78
79            // Add port mappings
80            for port in &self.ports {
81                container_request = container_request.with_mapped_port(*port, testcontainers::core::ContainerPort::Tcp(*port));
82            }
83
84            // Start container
85            let node = container_request
86                .start()
87                .await
88                .map_err(|e| CleanroomError::container_error("Failed to start generic container")
89                    .with_context("Container startup failed")
90                    .with_source(e.to_string()))?;
91
92            let mut metadata = HashMap::new();
93            metadata.insert("image".to_string(), format!("{}:{}", self.image, self.tag));
94            metadata.insert("container_type".to_string(), "generic".to_string());
95            
96            // Add port information
97            for port in &self.ports {
98                if let Ok(host_port) = node.get_host_port_ipv4(*port).await {
99                    metadata.insert(format!("port_{}", port), host_port.to_string());
100                }
101            }
102
103            // Store container reference
104            let mut container_guard = self.container_id.write().await;
105            *container_guard = Some(format!("generic-{}", Uuid::new_v4()));
106
107            Ok(ServiceHandle {
108                id: Uuid::new_v4().to_string(),
109                service_name: self.name.clone(),
110                metadata,
111            })
112        })
113    }
114
115    fn stop(&self, _handle: ServiceHandle) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>> {
116        Box::pin(async move {
117            let mut container_guard = self.container_id.write().await;
118            if container_guard.is_some() {
119                *container_guard = None; // Drop triggers container cleanup
120            }
121            Ok(())
122        })
123    }
124
125    fn health_check(&self, handle: &ServiceHandle) -> HealthStatus {
126        if handle.metadata.contains_key("image") && 
127           handle.metadata.contains_key("container_type") {
128            HealthStatus::Healthy
129        } else {
130            HealthStatus::Unknown
131        }
132    }
133}