clnrm_core/services/
generic.rs1use 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 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 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 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 tokio::task::block_in_place(|| {
100 tokio::runtime::Handle::current().block_on(async {
101 let image = GenericImage::new(self.image.clone(), self.tag.clone());
103
104 let mut container_request: testcontainers::core::ContainerRequest<GenericImage> =
106 image.into();
107
108 for (key, value) in &self.env_vars {
110 container_request = container_request.with_env_var(key, value);
111 }
112
113 for port in &self.ports {
115 container_request = container_request
116 .with_mapped_port(*port, testcontainers::core::ContainerPort::Tcp(*port));
117 }
118
119 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 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 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 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 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; }
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}