clnrm_core/services/
generic.rs1use 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 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 let image = GenericImage::new(self.image.clone(), self.tag.clone());
70
71 let mut container_request: testcontainers::core::ContainerRequest<GenericImage> = image.into();
73
74 for (key, value) in &self.env_vars {
76 container_request = container_request.with_env_var(key, value);
77 }
78
79 for port in &self.ports {
81 container_request = container_request.with_mapped_port(*port, testcontainers::core::ContainerPort::Tcp(*port));
82 }
83
84 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 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 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; }
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}