clnrm_core/config/
services.rs

1//! Service and volume configuration types
2
3use crate::error::{CleanroomError, Result};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7/// Service configuration
8#[derive(Debug, Deserialize, Serialize, Clone)]
9pub struct ServiceConfig {
10    /// Service type (generic_container, database, ollama, etc.)
11    pub r#type: String,
12    /// Service plugin
13    pub plugin: String,
14    /// Service image (optional for network services)
15    pub image: Option<String>,
16    /// Service command arguments (v1.0 - default args for service)
17    /// Can be overridden by scenario.run
18    #[serde(default)]
19    pub args: Option<Vec<String>>,
20    /// Service environment variables
21    pub env: Option<HashMap<String, String>>,
22    /// Service ports
23    pub ports: Option<Vec<u16>>,
24    /// Service volumes
25    pub volumes: Option<Vec<VolumeConfig>>,
26    /// Service health check
27    pub health_check: Option<HealthCheckConfig>,
28    /// SurrealDB username (optional, defaults to root)
29    pub username: Option<String>,
30    /// SurrealDB password (optional, defaults to root)
31    pub password: Option<String>,
32    /// SurrealDB strict mode (optional, defaults to false)
33    pub strict: Option<bool>,
34    /// Span name to wait for before marking service as ready
35    /// Service will poll for this span in OTEL output until detected or timeout
36    pub wait_for_span: Option<String>,
37    /// Timeout in seconds for waiting for span (default: 30)
38    pub wait_for_span_timeout_secs: Option<u64>,
39}
40
41/// Volume configuration
42#[derive(Debug, Deserialize, Serialize, Clone)]
43pub struct VolumeConfig {
44    /// Host path
45    pub host_path: String,
46    /// Container path
47    pub container_path: String,
48    /// Whether volume is read-only
49    pub read_only: Option<bool>,
50}
51
52impl VolumeConfig {
53    /// Validate the volume configuration
54    ///
55    /// # Errors
56    ///
57    /// Returns error if:
58    /// - Host path is empty
59    /// - Container path is empty
60    /// - Paths contain invalid characters
61    pub fn validate(&self) -> Result<()> {
62        use std::path::Path;
63
64        // Validate host path is not empty
65        if self.host_path.trim().is_empty() {
66            return Err(CleanroomError::validation_error(
67                "Volume host path cannot be empty",
68            ));
69        }
70
71        // Validate container path is not empty
72        if self.container_path.trim().is_empty() {
73            return Err(CleanroomError::validation_error(
74                "Volume container path cannot be empty",
75            ));
76        }
77
78        // Validate host path is absolute
79        let host_path = Path::new(&self.host_path);
80        if !host_path.is_absolute() {
81            return Err(CleanroomError::validation_error(format!(
82                "Volume host path must be absolute: {}",
83                self.host_path
84            )));
85        }
86
87        // Validate container path is absolute
88        let container_path = Path::new(&self.container_path);
89        if !container_path.is_absolute() {
90            return Err(CleanroomError::validation_error(format!(
91                "Volume container path must be absolute: {}",
92                self.container_path
93            )));
94        }
95
96        Ok(())
97    }
98
99    /// Convert to VolumeMount with validation
100    ///
101    /// This helper method creates a VolumeMount from the configuration
102    /// with full validation including path existence checks.
103    pub fn to_volume_mount(&self) -> Result<crate::backend::volume::VolumeMount> {
104        use crate::backend::volume::VolumeMount;
105        VolumeMount::from_config(self)
106    }
107}
108
109/// Health check configuration
110#[derive(Debug, Deserialize, Serialize, Clone)]
111pub struct HealthCheckConfig {
112    /// Health check command
113    pub cmd: Vec<String>,
114    /// Health check interval in seconds
115    pub interval: Option<u64>,
116    /// Health check timeout in seconds
117    pub timeout: Option<u64>,
118    /// Number of retries
119    pub retries: Option<u32>,
120}
121
122impl ServiceConfig {
123    /// Validate the service configuration
124    pub fn validate(&self) -> Result<()> {
125        if self.r#type.trim().is_empty() {
126            return Err(CleanroomError::validation_error(
127                "Service type cannot be empty",
128            ));
129        }
130
131        if self.plugin.trim().is_empty() {
132            return Err(CleanroomError::validation_error(
133                "Service plugin cannot be empty",
134            ));
135        }
136
137        if let Some(ref image) = self.image {
138            if image.trim().is_empty() {
139                return Err(CleanroomError::validation_error(
140                    "Service image cannot be empty",
141                ));
142            }
143        } else if self.r#type != "network_service" && self.r#type != "ollama" {
144            // For container-based services, image is required
145            return Err(CleanroomError::validation_error(
146                "Service image is required for container-based services",
147            ));
148        }
149
150        // Validate volumes if present
151        if let Some(ref volumes) = self.volumes {
152            for (i, volume) in volumes.iter().enumerate() {
153                volume.validate().map_err(|e| {
154                    CleanroomError::validation_error(format!("Volume {}: {}", i, e))
155                })?;
156            }
157        }
158
159        Ok(())
160    }
161}