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
86impl ServicePlugin for GenericContainerPlugin {
87 fn name(&self) -> &str {
88 &self.name
89 }
90
91 fn start(&self) -> Result<ServiceHandle> {
92 tokio::task::block_in_place(|| {
94 tokio::runtime::Handle::current().block_on(async {
95 let image = GenericImage::new(self.image.clone(), self.tag.clone());
97
98 let mut container_request: testcontainers::core::ContainerRequest<GenericImage> =
100 image.into();
101
102 for (key, value) in &self.env_vars {
104 container_request = container_request.with_env_var(key, value);
105 }
106
107 for port in &self.ports {
109 container_request = container_request
110 .with_mapped_port(*port, testcontainers::core::ContainerPort::Tcp(*port));
111 }
112
113 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 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 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 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 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 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; }
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}