222 lines
6.4 KiB
Rust
Raw Normal View History

//! Centralized configuration management for all platform services
//!
//! The ConfigManager handles loading, validating, and caching configurations
//! for all platform services. It provides fail-fast validation on startup
//! to prevent cascading crashes.
use std::collections::HashMap;
use std::sync::RwLock;
use anyhow::Result;
use serde_json::Value;
use tracing::{debug, error, info};
/// Central config manager for all platform services
///
/// This struct manages loading and validating configs for all platform
/// services. It caches loaded configs to avoid repeated parsing and provides
/// fail-fast validation on startup.
pub struct ConfigManager {
/// Cache of loaded configs (JSON representation)
/// Key format: "{service_name}.{mode}" (e.g., "orchestrator.solo")
cache: RwLock<HashMap<String, Value>>,
/// Current deployment mode
mode: String,
}
impl ConfigManager {
/// Creates a new ConfigManager with the current deployment mode
pub fn new() -> Self {
let mode = std::env::var("PROVISIONING_MODE").unwrap_or_else(|_| "solo".to_string());
Self {
cache: RwLock::new(HashMap::new()),
mode,
}
}
/// Creates a ConfigManager with a specific mode
pub fn with_mode<S: Into<String>>(mode: S) -> Self {
Self {
cache: RwLock::new(HashMap::new()),
mode: mode.into(),
}
}
/// Loads a service config as JSON (generic, without type information)
///
/// This is useful for validation without knowing the concrete type.
pub fn load_as_json(&self, service_name: &str) -> Result<Value> {
let cache_key = format!("{}.{}", service_name, self.mode);
// Check cache first
{
let cache_read = self.cache.read().unwrap();
if let Some(cached) = cache_read.get(&cache_key) {
debug!("Config cache hit for {}", cache_key);
return Ok(cached.clone());
}
}
// Cache miss - load from disk
debug!("Config cache miss for {} - loading from disk", cache_key);
let path =
platform_config::find_config_file(service_name, &self.mode).ok_or_else(|| {
anyhow::anyhow!("Config file not found for {}.{}", service_name, self.mode)
})?;
let json_value: Value = platform_config::format::load_config(&path)
.map_err(|e| anyhow::anyhow!("Failed to load config for {}: {}", service_name, e))?;
// Store in cache
{
let mut cache_write = self.cache.write().unwrap();
cache_write.insert(cache_key.clone(), json_value.clone());
}
Ok(json_value)
}
/// Validates that ALL known platform services have valid configs
///
/// This is the main validation entry point for pre-flight checks.
/// Returns Ok if all configs are valid, Err with detailed failure info
/// otherwise.
pub fn validate_all_services(&self) -> Result<()> {
let services = vec![
"orchestrator",
"control-center",
"mcp-server",
"ai-service",
"extension-registry",
"rag",
"vault-service",
"provisioning-daemon",
];
info!(
"Validating configs for {} services (mode: {})",
services.len(),
self.mode
);
let mut errors = Vec::new();
for service in services {
match self.load_as_json(service) {
Ok(_) => {
debug!("✅ {}.{} config valid", service, self.mode);
}
Err(e) => {
let error_msg = format!("{}.{} config invalid: {}", service, self.mode, e);
error!("{}", error_msg);
errors.push(error_msg);
}
}
}
if !errors.is_empty() {
let summary = format!(
"Config validation failed for {} services:\n{}",
errors.len(),
errors.join("\n")
);
error!("{}", summary);
return Err(anyhow::anyhow!("{}", summary));
}
info!("✅ All service configs validated successfully");
Ok(())
}
/// Validates a subset of services
pub fn validate_services(&self, services: &[&str]) -> Result<()> {
debug!(
"Validating {} specific services (mode: {})",
services.len(),
self.mode
);
let mut errors = Vec::new();
for service in services {
match self.load_as_json(service) {
Ok(_) => {
debug!("✅ {}.{} config valid", service, self.mode);
}
Err(e) => {
errors.push(format!("{}: {}", service, e));
}
}
}
if !errors.is_empty() {
let summary = errors.join("\n");
error!("{}", summary);
return Err(anyhow::anyhow!("Validation failed:\n{}", summary));
}
Ok(())
}
/// Clears the entire config cache
pub fn clear_cache(&self) {
let mut cache = self.cache.write().unwrap();
cache.clear();
debug!("Config cache cleared");
}
/// Invalidates cache for a specific service
pub fn invalidate_service(&self, service_name: &str) {
let cache_key = format!("{}.{}", service_name, self.mode);
let mut cache = self.cache.write().unwrap();
cache.remove(&cache_key);
debug!("Invalidated cache for {}", cache_key);
}
/// Gets the current deployment mode
pub fn mode(&self) -> &str {
&self.mode
}
/// Gets the number of cached configs
pub fn cache_size(&self) -> usize {
self.cache.read().unwrap().len()
}
}
impl Default for ConfigManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_manager_creation() {
let mgr = ConfigManager::new();
assert!(!mgr.mode().is_empty());
}
#[test]
fn test_config_manager_with_mode() {
let mgr = ConfigManager::with_mode("test");
assert_eq!(mgr.mode(), "test");
}
#[test]
fn test_cache_operations() {
let mgr = ConfigManager::new();
assert_eq!(mgr.cache_size(), 0);
// Clear doesn't fail even when empty
mgr.clear_cache();
assert_eq!(mgr.cache_size(), 0);
}
}