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::backend::volume::VolumeMount;
7use crate::cleanroom::{HealthStatus, ServiceHandle, ServicePlugin};
8use crate::error::{CleanroomError, Result};
9use std::collections::HashMap;
10use std::sync::Arc;
11use testcontainers::runners::AsyncRunner;
12use testcontainers::{GenericImage, ImageExt};
13use tokio::sync::RwLock;
14use uuid::Uuid;
15
16#[derive(Debug)]
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    volumes: Vec<VolumeMount>,
25}
26
27impl GenericContainerPlugin {
28    pub fn new(name: &str, image: &str) -> Self {
29        let (image_name, image_tag) = if let Some((name, tag)) = image.split_once(':') {
30            (name.to_string(), tag.to_string())
31        } else {
32            (image.to_string(), "latest".to_string())
33        };
34
35        Self {
36            name: name.to_string(),
37            image: image_name,
38            tag: image_tag,
39            container_id: Arc::new(RwLock::new(None)),
40            env_vars: HashMap::new(),
41            ports: Vec::new(),
42            volumes: Vec::new(),
43        }
44    }
45
46    pub fn with_env(mut self, key: &str, value: &str) -> Self {
47        self.env_vars.insert(key.to_string(), value.to_string());
48        self
49    }
50
51    pub fn with_port(mut self, port: u16) -> Self {
52        self.ports.push(port);
53        self
54    }
55
56    /// Add volume mount
57    ///
58    /// # Arguments
59    ///
60    /// * `host_path` - Path on the host system
61    /// * `container_path` - Path inside the container
62    /// * `read_only` - Whether mount is read-only
63    ///
64    /// # Errors
65    ///
66    /// Returns error if volume validation fails
67    pub fn with_volume(
68        mut self,
69        host_path: &str,
70        container_path: &str,
71        read_only: bool,
72    ) -> Result<Self> {
73        let mount = VolumeMount::new(host_path, container_path, read_only)?;
74        self.volumes.push(mount);
75        Ok(self)
76    }
77
78    /// Add read-only volume mount
79    ///
80    /// Convenience method for adding read-only mounts
81    pub fn with_volume_ro(self, host_path: &str, container_path: &str) -> Result<Self> {
82        self.with_volume(host_path, container_path, true)
83    }
84}
85
86impl ServicePlugin for GenericContainerPlugin {
87    fn name(&self) -> &str {
88        &self.name
89    }
90
91    fn start(&self) -> Result<ServiceHandle> {
92        // Use tokio::task::block_in_place for async operations
93        tokio::task::block_in_place(|| {
94            tokio::runtime::Handle::current().block_on(async {
95                // Create container configuration
96                let image = GenericImage::new(self.image.clone(), self.tag.clone());
97
98                // Build container request with environment variables and ports
99                let mut container_request: testcontainers::core::ContainerRequest<GenericImage> =
100                    image.into();
101
102                // Add environment variables
103                for (key, value) in &self.env_vars {
104                    container_request = container_request.with_env_var(key, value);
105                }
106
107                // Add port mappings
108                for port in &self.ports {
109                    container_request = container_request
110                        .with_mapped_port(*port, testcontainers::core::ContainerPort::Tcp(*port));
111                }
112
113                // Add volume mounts
114                for mount in &self.volumes {
115                    use testcontainers::core::{AccessMode, Mount};
116
117                    let access_mode = if mount.is_read_only() {
118                        AccessMode::ReadOnly
119                    } else {
120                        AccessMode::ReadWrite
121                    };
122
123                    let bind_mount = Mount::bind_mount(
124                        mount.host_path().to_string_lossy().to_string(),
125                        mount.container_path().to_string_lossy().to_string(),
126                    )
127                    .with_access_mode(access_mode);
128
129                    container_request = container_request.with_mount(bind_mount);
130                }
131
132                // Start container
133                let node = container_request.start().await.map_err(|e| {
134                    CleanroomError::container_error("Failed to start generic container")
135                        .with_context("Container startup failed")
136                        .with_source(e.to_string())
137                })?;
138
139                // Generate container ID
140                let container_id = format!("generic-{}", Uuid::new_v4());
141
142                let mut metadata = HashMap::new();
143                metadata.insert("image".to_string(), format!("{}:{}", self.image, self.tag));
144                metadata.insert("container_type".to_string(), "generic".to_string());
145                metadata.insert("container_id".to_string(), container_id.clone());
146
147                // Add port information
148                for port in &self.ports {
149                    if let Ok(host_port) = node.get_host_port_ipv4(*port).await {
150                        metadata.insert(format!("port_{}", port), host_port.to_string());
151                    }
152                }
153
154                // Store container reference
155                let mut container_guard = self.container_id.write().await;
156                *container_guard = Some(container_id);
157
158                Ok(ServiceHandle {
159                    id: Uuid::new_v4().to_string(),
160                    service_name: self.name.clone(),
161                    metadata,
162                })
163            })
164        })
165    }
166
167    fn stop(&self, _handle: ServiceHandle) -> Result<()> {
168        // Use tokio::task::block_in_place for async operations
169        tokio::task::block_in_place(|| {
170            tokio::runtime::Handle::current().block_on(async {
171                let mut container_guard = self.container_id.write().await;
172                if container_guard.is_some() {
173                    *container_guard = None; // Drop triggers container cleanup
174                }
175                Ok(())
176            })
177        })
178    }
179
180    fn health_check(&self, handle: &ServiceHandle) -> HealthStatus {
181        if handle.metadata.contains_key("image") && handle.metadata.contains_key("container_type") {
182            HealthStatus::Healthy
183        } else {
184            HealthStatus::Unknown
185        }
186    }
187}