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    async fn verify_connection(&self, _host_port: u16) -> Result<()> {
86        // For generic containers, we assume they're healthy if they start successfully
87        // Specific health checks would need to be implemented per container type
88        Ok(())
89    }
90}
91
92impl ServicePlugin for GenericContainerPlugin {
93    fn name(&self) -> &str {
94        &self.name
95    }
96
97    fn start(&self) -> Result<ServiceHandle> {
98        // Use tokio::task::block_in_place for async operations
99        tokio::task::block_in_place(|| {
100            tokio::runtime::Handle::current().block_on(async {
101                // Create container configuration
102                let image = GenericImage::new(self.image.clone(), self.tag.clone());
103
104                // Build container request with environment variables and ports
105                let mut container_request: testcontainers::core::ContainerRequest<GenericImage> =
106                    image.into();
107
108                // Add environment variables
109                for (key, value) in &self.env_vars {
110                    container_request = container_request.with_env_var(key, value);
111                }
112
113                // Add port mappings
114                for port in &self.ports {
115                    container_request = container_request
116                        .with_mapped_port(*port, testcontainers::core::ContainerPort::Tcp(*port));
117                }
118
119                // Add volume mounts
120                for mount in &self.volumes {
121                    use testcontainers::core::{AccessMode, Mount};
122
123                    let access_mode = if mount.is_read_only() {
124                        AccessMode::ReadOnly
125                    } else {
126                        AccessMode::ReadWrite
127                    };
128
129                    let bind_mount = Mount::bind_mount(
130                        mount.host_path().to_string_lossy().to_string(),
131                        mount.container_path().to_string_lossy().to_string(),
132                    )
133                    .with_access_mode(access_mode);
134
135                    container_request = container_request.with_mount(bind_mount);
136                }
137
138                // Start container
139                let node = container_request.start().await.map_err(|e| {
140                    CleanroomError::container_error("Failed to start generic container")
141                        .with_context("Container startup failed")
142                        .with_source(e.to_string())
143                })?;
144
145                let mut metadata = HashMap::new();
146                metadata.insert("image".to_string(), format!("{}:{}", self.image, self.tag));
147                metadata.insert("container_type".to_string(), "generic".to_string());
148
149                // Add port information
150                for port in &self.ports {
151                    if let Ok(host_port) = node.get_host_port_ipv4(*port).await {
152                        metadata.insert(format!("port_{}", port), host_port.to_string());
153                    }
154                }
155
156                // Store container reference
157                let mut container_guard = self.container_id.write().await;
158                *container_guard = Some(format!("generic-{}", Uuid::new_v4()));
159
160                Ok(ServiceHandle {
161                    id: Uuid::new_v4().to_string(),
162                    service_name: self.name.clone(),
163                    metadata,
164                })
165            })
166        })
167    }
168
169    fn stop(&self, _handle: ServiceHandle) -> Result<()> {
170        // Use tokio::task::block_in_place for async operations
171        tokio::task::block_in_place(|| {
172            tokio::runtime::Handle::current().block_on(async {
173                let mut container_guard = self.container_id.write().await;
174                if container_guard.is_some() {
175                    *container_guard = None; // Drop triggers container cleanup
176                }
177                Ok(())
178            })
179        })
180    }
181
182    fn health_check(&self, handle: &ServiceHandle) -> HealthStatus {
183        if handle.metadata.contains_key("image") && handle.metadata.contains_key("container_type") {
184            HealthStatus::Healthy
185        } else {
186            HealthStatus::Unknown
187        }
188    }
189}