//! Configuration loading and management for the orchestrator //! //! This module handles loading configuration from TOML files with support for: //! - Default configuration (config.defaults.toml) //! - User overrides (config.user.toml) //! - Environment-specific configuration //! - CLI argument overrides use std::path::{Path, PathBuf}; use anyhow::{Context, Result}; use platform_config::ConfigLoader; use serde::{Deserialize, Serialize}; /// Complete orchestrator configuration #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct OrchestratorConfig { pub orchestrator: OrchestratorSettings, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct OrchestratorSettings { pub enabled: bool, pub name: String, pub version: String, pub server: ServerConfig, pub paths: PathsConfig, pub storage: StorageConfig, pub queue: QueueConfig, pub batch: BatchConfig, pub monitoring: MonitoringConfig, pub rollback: RollbackConfig, pub state: StateConfig, pub logging: LoggingConfig, pub dns: DnsConfig, pub oci: OciConfig, pub extensions: ExtensionsConfig, pub services: ServicesConfig, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ServerConfig { pub host: String, pub port: u16, pub workers: usize, pub keep_alive: u64, pub max_connections: usize, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PathsConfig { pub base: String, pub data_dir: String, pub logs_dir: String, pub queue_dir: String, pub nu_path: String, pub provisioning_path: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct StorageConfig { #[serde(rename = "type")] pub storage_type: String, pub backend_path: String, pub surrealdb: SurrealDbConfig, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SurrealDbConfig { pub url: String, pub namespace: String, pub database: String, pub username: String, pub password: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct QueueConfig { pub max_concurrent_tasks: usize, pub retry_attempts: u32, pub retry_delay_seconds: u64, pub task_timeout_minutes: u64, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BatchConfig { pub parallel_limit: usize, pub operation_timeout_minutes: u64, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MonitoringConfig { pub enabled: bool, pub metrics_interval_seconds: u64, pub health_check_interval_seconds: u64, pub min_memory_mb: u64, pub max_cpu_percent: f64, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RollbackConfig { pub checkpoint_interval_seconds: u64, pub auto_checkpoint_enabled: bool, pub strategy: String, pub max_checkpoints: usize, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct StateConfig { pub snapshot_interval_seconds: u64, pub max_snapshots_per_workflow: usize, pub cleanup_interval_minutes: u64, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LoggingConfig { pub level: String, pub format: String, pub max_file_size: String, pub max_files: usize, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DnsConfig { pub coredns_url: String, pub auto_register: bool, pub ttl: u32, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct OciConfig { pub registry_url: String, pub namespace: String, pub cache_dir: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ExtensionsConfig { pub auto_load: bool, pub cache_dir: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ServicesConfig { pub manager_enabled: bool, pub auto_start_dependencies: bool, } impl OrchestratorConfig { /// Load configuration from file pub fn from_file>(path: P) -> Result { let content = std::fs::read_to_string(path.as_ref()) .with_context(|| format!("Failed to read config file: {:?}", path.as_ref()))?; let config: OrchestratorConfig = toml::from_str(&content) .with_context(|| format!("Failed to parse config file: {:?}", path.as_ref()))?; Ok(config) } /// Load configuration with hierarchical fallback logic: /// 1. Environment variable ORCHESTRATOR_CONFIG (explicit config path) /// 2. Mode-specific config: /// provisioning/platform/config/orchestrator.{mode}.toml /// 3. User config: config.user.toml (legacy support) /// 4. Default config: config.defaults.toml (fallback) /// /// Then environment variables (ORCHESTRATOR_*) override specific fields. pub fn load() -> Result { let mut config = Self::load_from_hierarchy()?; Self::apply_env_overrides(&mut config)?; Ok(config) } /// Internal: Load configuration from hierarchy without env var overrides fn load_from_hierarchy() -> Result { // Priority 1: Explicit config path from environment variable if let Ok(config_path) = std::env::var("ORCHESTRATOR_CONFIG") { return Self::from_file(&config_path); } // Priority 2: Mode-specific config (provisioning/platform/config/) if let Ok(mode) = std::env::var("ORCHESTRATOR_MODE") { let mode_config_path = format!("provisioning/platform/config/orchestrator.{}.toml", mode); if Path::new(&mode_config_path).exists() { return Self::from_file(&mode_config_path); } } // Priority 3: User override (legacy) let base_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let user_config_path = base_path.join("config.user.toml"); if user_config_path.exists() { return Self::from_file(user_config_path); } // Priority 4: System defaults let defaults_path = base_path.join("config.defaults.toml"); if defaults_path.exists() { return Self::from_file(defaults_path); } anyhow::bail!( "No configuration file found. Set ORCHESTRATOR_CONFIG, ORCHESTRATOR_MODE, or ensure \ config.defaults.toml exists" ) } /// Apply environment variable overrides to configuration /// Environment variables use format: ORCHESTRATOR_{SECTION}_{KEY}=value /// Example: ORCHESTRATOR_SERVER_PORT=9999 fn apply_env_overrides(config: &mut Self) -> Result<()> { // Server overrides if let Ok(host) = std::env::var("ORCHESTRATOR_SERVER_HOST") { config.orchestrator.server.host = host; } if let Ok(port) = std::env::var("ORCHESTRATOR_SERVER_PORT") { config.orchestrator.server.port = port .parse() .context("ORCHESTRATOR_SERVER_PORT must be a valid port number")?; } if let Ok(workers) = std::env::var("ORCHESTRATOR_SERVER_WORKERS") { config.orchestrator.server.workers = workers .parse() .context("ORCHESTRATOR_SERVER_WORKERS must be a valid number")?; } // Storage overrides if let Ok(storage_type) = std::env::var("ORCHESTRATOR_STORAGE_TYPE") { config.orchestrator.storage.storage_type = storage_type; } if let Ok(backend_path) = std::env::var("ORCHESTRATOR_STORAGE_PATH") { config.orchestrator.storage.backend_path = backend_path; } // SurrealDB overrides if let Ok(url) = std::env::var("ORCHESTRATOR_SURREALDB_URL") { config.orchestrator.storage.surrealdb.url = url; } if let Ok(namespace) = std::env::var("ORCHESTRATOR_SURREALDB_NAMESPACE") { config.orchestrator.storage.surrealdb.namespace = namespace; } if let Ok(database) = std::env::var("ORCHESTRATOR_SURREALDB_DATABASE") { config.orchestrator.storage.surrealdb.database = database; } if let Ok(username) = std::env::var("ORCHESTRATOR_SURREALDB_USERNAME") { config.orchestrator.storage.surrealdb.username = username; } if let Ok(password) = std::env::var("ORCHESTRATOR_SURREALDB_PASSWORD") { config.orchestrator.storage.surrealdb.password = password; } // Queue overrides if let Ok(max_tasks) = std::env::var("ORCHESTRATOR_QUEUE_MAX_CONCURRENT_TASKS") { config.orchestrator.queue.max_concurrent_tasks = max_tasks .parse() .context("ORCHESTRATOR_QUEUE_MAX_CONCURRENT_TASKS must be a valid number")?; } if let Ok(retries) = std::env::var("ORCHESTRATOR_QUEUE_RETRY_ATTEMPTS") { config.orchestrator.queue.retry_attempts = retries .parse() .context("ORCHESTRATOR_QUEUE_RETRY_ATTEMPTS must be a valid number")?; } // Logging overrides if let Ok(level) = std::env::var("ORCHESTRATOR_LOG_LEVEL") { config.orchestrator.logging.level = level; } if let Ok(format) = std::env::var("ORCHESTRATOR_LOG_FORMAT") { config.orchestrator.logging.format = format; } Ok(()) } /// Apply CLI argument overrides to the configuration pub fn apply_cli_overrides(&mut self, args: &crate::Args) { // Override port self.orchestrator.server.port = args.port; // Override data directory self.orchestrator.paths.data_dir = args.data_dir.clone(); // Override storage type self.orchestrator.storage.storage_type = args.storage_type.clone(); // Override SurrealDB settings if provided if let Some(url) = &args.surrealdb_url { self.orchestrator.storage.surrealdb.url = url.clone(); } if let Some(namespace) = &args.surrealdb_namespace { self.orchestrator.storage.surrealdb.namespace = namespace.clone(); } if let Some(database) = &args.surrealdb_database { self.orchestrator.storage.surrealdb.database = database.clone(); } if let Some(username) = &args.surrealdb_username { self.orchestrator.storage.surrealdb.username = username.clone(); } if let Some(password) = &args.surrealdb_password { self.orchestrator.storage.surrealdb.password = password.clone(); } // Override Nushell path self.orchestrator.paths.nu_path = args.nu_path.clone(); // Override provisioning path self.orchestrator.paths.provisioning_path = args.provisioning_path.clone(); } /// Get server configuration pub fn server(&self) -> &ServerConfig { &self.orchestrator.server } /// Get paths configuration pub fn paths(&self) -> &PathsConfig { &self.orchestrator.paths } /// Get storage configuration pub fn storage(&self) -> &StorageConfig { &self.orchestrator.storage } /// Get queue configuration pub fn queue(&self) -> &QueueConfig { &self.orchestrator.queue } /// Get logging configuration pub fn logging(&self) -> &LoggingConfig { &self.orchestrator.logging } /// Get DNS configuration pub fn dns(&self) -> &DnsConfig { &self.orchestrator.dns } /// Get OCI configuration pub fn oci(&self) -> &OciConfig { &self.orchestrator.oci } /// Get extensions configuration pub fn extensions(&self) -> &ExtensionsConfig { &self.orchestrator.extensions } /// Get services configuration pub fn services(&self) -> &ServicesConfig { &self.orchestrator.services } } impl ConfigLoader for OrchestratorConfig { fn service_name() -> &'static str { "orchestrator" } fn load_from_hierarchy() -> std::result::Result> { let service = Self::service_name(); if let Some(path) = platform_config::resolve_config_path(service) { return Self::from_path(&path); } // Fallback to defaults Ok(Self::default()) } fn apply_env_overrides( &mut self, ) -> std::result::Result<(), Box> { Self::apply_env_overrides(self).map_err(|e| { Box::new(std::io::Error::other(e.to_string())) as Box }) } fn from_path>( path: P, ) -> std::result::Result> { let path = path.as_ref(); let json_value = platform_config::format::load_config(path).map_err(|e| { let err: Box = Box::new(e); err })?; serde_json::from_value(json_value).map_err(|e| { let err_msg = format!( "Failed to deserialize orchestrator config from {:?}: {}", path, e ); Box::new(std::io::Error::new( std::io::ErrorKind::InvalidData, err_msg, )) as Box }) } } impl Default for OrchestratorSettings { fn default() -> Self { Self { enabled: true, name: "orchestrator".to_string(), version: "1.0.0".to_string(), server: ServerConfig::default(), paths: PathsConfig::default(), storage: StorageConfig::default(), queue: QueueConfig::default(), batch: BatchConfig::default(), monitoring: MonitoringConfig::default(), rollback: RollbackConfig::default(), state: StateConfig::default(), logging: LoggingConfig::default(), dns: DnsConfig::default(), oci: OciConfig::default(), extensions: ExtensionsConfig::default(), services: ServicesConfig::default(), } } } impl Default for ServerConfig { fn default() -> Self { Self { host: "127.0.0.1".to_string(), port: 9090, workers: 4, keep_alive: 75, max_connections: 1000, } } } impl Default for PathsConfig { fn default() -> Self { Self { base: "/tmp/.orchestrator".to_string(), data_dir: "/tmp/.orchestrator/data".to_string(), logs_dir: "/tmp/.orchestrator/logs".to_string(), queue_dir: "/tmp/.orchestrator/data/queue".to_string(), nu_path: "/usr/local/bin/nu".to_string(), provisioning_path: "provisioning".to_string(), } } } impl Default for StorageConfig { fn default() -> Self { Self { storage_type: "surrealdb".to_string(), backend_path: "/tmp/.orchestrator/storage".to_string(), surrealdb: SurrealDbConfig::default(), } } } impl Default for SurrealDbConfig { fn default() -> Self { Self { url: "ws://127.0.0.1:8000".to_string(), namespace: "provisioning".to_string(), database: "orchestrator".to_string(), username: "root".to_string(), password: "root".to_string(), } } } impl Default for QueueConfig { fn default() -> Self { Self { max_concurrent_tasks: 10, retry_attempts: 3, retry_delay_seconds: 5, task_timeout_minutes: 30, } } } impl Default for BatchConfig { fn default() -> Self { Self { parallel_limit: 5, operation_timeout_minutes: 60, } } } impl Default for MonitoringConfig { fn default() -> Self { Self { enabled: true, metrics_interval_seconds: 60, health_check_interval_seconds: 30, min_memory_mb: 512, max_cpu_percent: 80.0, } } } impl Default for RollbackConfig { fn default() -> Self { Self { checkpoint_interval_seconds: 300, auto_checkpoint_enabled: true, strategy: "incremental".to_string(), max_checkpoints: 10, } } } impl Default for StateConfig { fn default() -> Self { Self { snapshot_interval_seconds: 600, max_snapshots_per_workflow: 5, cleanup_interval_minutes: 1440, } } } impl Default for LoggingConfig { fn default() -> Self { Self { level: "info".to_string(), format: "json".to_string(), max_file_size: "100MB".to_string(), max_files: 10, } } } impl Default for DnsConfig { fn default() -> Self { Self { coredns_url: "http://127.0.0.1:9053".to_string(), auto_register: true, ttl: 300, } } } impl Default for OciConfig { fn default() -> Self { Self { registry_url: "http://127.0.0.1:5000".to_string(), namespace: "provisioning".to_string(), cache_dir: "/tmp/.orchestrator/oci-cache".to_string(), } } } impl Default for ExtensionsConfig { fn default() -> Self { Self { auto_load: true, cache_dir: "/tmp/.orchestrator/extensions".to_string(), } } } impl Default for ServicesConfig { fn default() -> Self { Self { manager_enabled: true, auto_start_dependencies: true, } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_config_deserialization() { let toml_content = r#" [orchestrator] enabled = true name = "orchestrator" version = "1.0.0" [orchestrator.server] host = "127.0.0.1" port = 9090 workers = 4 keep_alive = 75 max_connections = 1000 [orchestrator.paths] base = "/tmp/.orchestrator" data_dir = "/tmp/.orchestrator/data" logs_dir = "/tmp/.orchestrator/logs" queue_dir = "/tmp/.orchestrator/data/queue" nu_path = "/usr/local/bin/nu" provisioning_path = "/usr/local/bin/provisioning" [orchestrator.storage] type = "filesystem" backend_path = "/tmp/.orchestrator/data/queue.rkvs" [orchestrator.storage.surrealdb] url = "ws://localhost:8000" namespace = "orchestrator" database = "tasks" username = "" password = "" [orchestrator.queue] max_concurrent_tasks = 5 retry_attempts = 3 retry_delay_seconds = 5 task_timeout_minutes = 60 [orchestrator.batch] parallel_limit = 5 operation_timeout_minutes = 30 [orchestrator.monitoring] enabled = true metrics_interval_seconds = 60 health_check_interval_seconds = 30 min_memory_mb = 1024 max_cpu_percent = 80.0 [orchestrator.rollback] checkpoint_interval_seconds = 300 auto_checkpoint_enabled = true strategy = "config-driven" max_checkpoints = 50 [orchestrator.state] snapshot_interval_seconds = 120 max_snapshots_per_workflow = 10 cleanup_interval_minutes = 60 [orchestrator.logging] level = "info" format = "json" max_file_size = "100MB" max_files = 10 [orchestrator.dns] coredns_url = "http://localhost:53" auto_register = true ttl = 300 [orchestrator.oci] registry_url = "http://localhost:5000" namespace = "provisioning-extensions" cache_dir = "/tmp/oci-cache" [orchestrator.extensions] auto_load = true cache_dir = "/tmp/extensions" [orchestrator.services] manager_enabled = true auto_start_dependencies = true "#; let config: OrchestratorConfig = toml::from_str(toml_content).unwrap(); assert_eq!(config.orchestrator.name, "orchestrator"); assert_eq!(config.orchestrator.server.port, 9090); assert_eq!(config.orchestrator.storage.storage_type, "filesystem"); assert_eq!(config.orchestrator.queue.max_concurrent_tasks, 5); } }