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