2026-01-08 21:32:59 +00:00
|
|
|
use crate::error::{ControlCenterError, Result, infrastructure};
|
2025-10-07 10:59:52 +01:00
|
|
|
use serde::{Deserialize, Serialize};
|
2026-01-08 21:32:59 +00:00
|
|
|
use std::path::{Path, PathBuf};
|
2025-10-07 10:59:52 +01:00
|
|
|
use std::time::Duration;
|
|
|
|
|
use tracing::info;
|
2026-01-08 21:32:59 +00:00
|
|
|
use platform_config::ConfigLoader;
|
2025-10-07 10:59:52 +01:00
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct ControlCenterConfig {
|
|
|
|
|
pub server: ServerConfig,
|
|
|
|
|
pub database: DatabaseConfig,
|
|
|
|
|
pub policies: PolicyConfig,
|
|
|
|
|
pub auth: AuthConfig,
|
|
|
|
|
pub compliance: ComplianceConfig,
|
|
|
|
|
pub anomaly: AnomalyConfig,
|
|
|
|
|
pub logging: LoggingConfig,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct ServerConfig {
|
|
|
|
|
pub host: String,
|
|
|
|
|
pub port: u16,
|
|
|
|
|
pub cors_origins: Vec<String>,
|
|
|
|
|
pub request_timeout_ms: u64,
|
|
|
|
|
pub max_request_size: usize,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct DatabaseConfig {
|
|
|
|
|
pub url: String,
|
|
|
|
|
pub namespace: String,
|
|
|
|
|
pub database: String,
|
|
|
|
|
pub username: Option<String>,
|
|
|
|
|
pub password: Option<String>,
|
|
|
|
|
pub connection_timeout_ms: u64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct PolicyConfig {
|
|
|
|
|
pub policy_dir: PathBuf,
|
|
|
|
|
pub schema_dir: PathBuf,
|
|
|
|
|
pub templates_dir: PathBuf,
|
|
|
|
|
pub enable_versioning: bool,
|
|
|
|
|
pub enable_caching: bool,
|
|
|
|
|
pub cache_ttl_seconds: u64,
|
|
|
|
|
pub validation_strict: bool,
|
|
|
|
|
pub hooks: HooksConfig,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct HooksConfig {
|
|
|
|
|
pub pre_execution: Vec<String>,
|
|
|
|
|
pub post_execution: Vec<String>,
|
|
|
|
|
pub on_policy_violation: Vec<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct AuthConfig {
|
|
|
|
|
pub jwt_secret: String,
|
|
|
|
|
pub jwt_expiry_hours: u64,
|
|
|
|
|
pub require_mfa: bool,
|
|
|
|
|
pub password_policy: PasswordPolicy,
|
|
|
|
|
pub session_timeout_minutes: u64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct PasswordPolicy {
|
|
|
|
|
pub min_length: usize,
|
|
|
|
|
pub require_uppercase: bool,
|
|
|
|
|
pub require_lowercase: bool,
|
|
|
|
|
pub require_numbers: bool,
|
|
|
|
|
pub require_special_chars: bool,
|
|
|
|
|
pub max_age_days: u64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct ComplianceConfig {
|
|
|
|
|
pub soc2: Soc2Config,
|
|
|
|
|
pub hipaa: HipaaConfig,
|
|
|
|
|
pub reports: ReportsConfig,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct Soc2Config {
|
|
|
|
|
pub enabled: bool,
|
|
|
|
|
pub audit_log_retention_days: u64,
|
|
|
|
|
pub require_approval_for_production: bool,
|
|
|
|
|
pub sensitive_data_encryption: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct HipaaConfig {
|
|
|
|
|
pub enabled: bool,
|
|
|
|
|
pub require_data_minimization: bool,
|
|
|
|
|
pub audit_all_access: bool,
|
|
|
|
|
pub encrypt_at_rest: bool,
|
|
|
|
|
pub encrypt_in_transit: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct ReportsConfig {
|
|
|
|
|
pub output_dir: PathBuf,
|
|
|
|
|
pub formats: Vec<String>,
|
|
|
|
|
pub schedule_cron: Option<String>,
|
|
|
|
|
pub email_notifications: Vec<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct AnomalyConfig {
|
|
|
|
|
pub enabled: bool,
|
|
|
|
|
pub baseline_window_hours: u64,
|
|
|
|
|
pub detection_threshold: f64,
|
|
|
|
|
pub statistical_methods: Vec<String>,
|
|
|
|
|
pub alert_channels: Vec<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct LoggingConfig {
|
|
|
|
|
pub level: String,
|
|
|
|
|
pub file_path: Option<PathBuf>,
|
|
|
|
|
pub structured: bool,
|
|
|
|
|
pub include_spans: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for ControlCenterConfig {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
server: ServerConfig {
|
|
|
|
|
host: "127.0.0.1".to_string(),
|
|
|
|
|
port: 8080,
|
|
|
|
|
cors_origins: vec!["*".to_string()],
|
|
|
|
|
request_timeout_ms: 30000,
|
|
|
|
|
max_request_size: 10 * 1024 * 1024, // 10MB
|
|
|
|
|
},
|
|
|
|
|
database: DatabaseConfig {
|
|
|
|
|
url: "memory".to_string(),
|
|
|
|
|
namespace: "control_center".to_string(),
|
|
|
|
|
database: "policies".to_string(),
|
|
|
|
|
username: None,
|
|
|
|
|
password: None,
|
|
|
|
|
connection_timeout_ms: 5000,
|
|
|
|
|
},
|
|
|
|
|
policies: PolicyConfig {
|
|
|
|
|
policy_dir: "policies".into(),
|
|
|
|
|
schema_dir: "schemas".into(),
|
|
|
|
|
templates_dir: "templates".into(),
|
|
|
|
|
enable_versioning: true,
|
|
|
|
|
enable_caching: true,
|
|
|
|
|
cache_ttl_seconds: 300,
|
|
|
|
|
validation_strict: true,
|
|
|
|
|
hooks: HooksConfig {
|
|
|
|
|
pre_execution: vec![],
|
|
|
|
|
post_execution: vec![],
|
|
|
|
|
on_policy_violation: vec![],
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
auth: AuthConfig {
|
|
|
|
|
jwt_secret: "change_me_in_production".to_string(),
|
|
|
|
|
jwt_expiry_hours: 24,
|
|
|
|
|
require_mfa: false,
|
|
|
|
|
password_policy: PasswordPolicy {
|
|
|
|
|
min_length: 8,
|
|
|
|
|
require_uppercase: true,
|
|
|
|
|
require_lowercase: true,
|
|
|
|
|
require_numbers: true,
|
|
|
|
|
require_special_chars: true,
|
|
|
|
|
max_age_days: 90,
|
|
|
|
|
},
|
|
|
|
|
session_timeout_minutes: 60,
|
|
|
|
|
},
|
|
|
|
|
compliance: ComplianceConfig {
|
|
|
|
|
soc2: Soc2Config {
|
|
|
|
|
enabled: false,
|
|
|
|
|
audit_log_retention_days: 365,
|
|
|
|
|
require_approval_for_production: true,
|
|
|
|
|
sensitive_data_encryption: true,
|
|
|
|
|
},
|
|
|
|
|
hipaa: HipaaConfig {
|
|
|
|
|
enabled: false,
|
|
|
|
|
require_data_minimization: true,
|
|
|
|
|
audit_all_access: true,
|
|
|
|
|
encrypt_at_rest: true,
|
|
|
|
|
encrypt_in_transit: true,
|
|
|
|
|
},
|
|
|
|
|
reports: ReportsConfig {
|
|
|
|
|
output_dir: "reports".into(),
|
|
|
|
|
formats: vec!["json".to_string(), "html".to_string()],
|
|
|
|
|
schedule_cron: None,
|
|
|
|
|
email_notifications: vec![],
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
anomaly: AnomalyConfig {
|
|
|
|
|
enabled: true,
|
|
|
|
|
baseline_window_hours: 24,
|
|
|
|
|
detection_threshold: 2.0,
|
|
|
|
|
statistical_methods: vec![
|
|
|
|
|
"zscore".to_string(),
|
|
|
|
|
"iqr".to_string(),
|
|
|
|
|
"isolation_forest".to_string(),
|
|
|
|
|
],
|
|
|
|
|
alert_channels: vec![],
|
|
|
|
|
},
|
|
|
|
|
logging: LoggingConfig {
|
|
|
|
|
level: "info".to_string(),
|
|
|
|
|
file_path: None,
|
|
|
|
|
structured: true,
|
|
|
|
|
include_spans: true,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ControlCenterConfig {
|
2026-01-08 21:32:59 +00:00
|
|
|
/// Load configuration with hierarchical fallback logic:
|
|
|
|
|
/// 1. Environment variable CONTROL_CENTER_CONFIG (explicit config path)
|
|
|
|
|
/// 2. Mode-specific config: provisioning/platform/config/control-center.{mode}.toml
|
|
|
|
|
/// 3. System defaults: config.defaults.toml
|
|
|
|
|
///
|
|
|
|
|
/// Then environment variables (CONTROL_CENTER_*) override specific fields.
|
|
|
|
|
pub fn load() -> Result<Self> {
|
|
|
|
|
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<Self> {
|
|
|
|
|
// Priority 1: Explicit config path from environment variable
|
|
|
|
|
if let Ok(config_path) = std::env::var("CONTROL_CENTER_CONFIG") {
|
|
|
|
|
return Self::from_file(&config_path);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Priority 2: Mode-specific config (provisioning/platform/config/)
|
|
|
|
|
if let Ok(mode) = std::env::var("CONTROL_CENTER_MODE") {
|
|
|
|
|
let mode_config_path = format!(
|
|
|
|
|
"provisioning/platform/config/control-center.{}.toml",
|
|
|
|
|
mode
|
|
|
|
|
);
|
|
|
|
|
if Path::new(&mode_config_path).exists() {
|
|
|
|
|
return Self::from_file(&mode_config_path);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Priority 3: System defaults
|
|
|
|
|
let defaults_path = Path::new("config/control-center.defaults.toml");
|
|
|
|
|
if defaults_path.exists() {
|
|
|
|
|
return Self::from_file(defaults_path);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Priority 4: Built-in defaults if file doesn't exist
|
|
|
|
|
Ok(Self::default())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Apply environment variable overrides to configuration
|
|
|
|
|
/// Environment variables use format: CONTROL_CENTER_{SECTION}_{KEY}=value
|
|
|
|
|
/// Example: CONTROL_CENTER_SERVER_PORT=8888
|
|
|
|
|
fn apply_env_overrides(config: &mut Self) -> Result<()> {
|
|
|
|
|
// Server overrides
|
|
|
|
|
if let Ok(host) = std::env::var("CONTROL_CENTER_SERVER_HOST") {
|
|
|
|
|
config.server.host = host;
|
|
|
|
|
}
|
|
|
|
|
if let Ok(port) = std::env::var("CONTROL_CENTER_SERVER_PORT") {
|
|
|
|
|
config.server.port = port.parse()
|
|
|
|
|
.map_err(|_| ControlCenterError::Infrastructure(infrastructure::InfrastructureError::Configuration(
|
|
|
|
|
"CONTROL_CENTER_SERVER_PORT must be a valid port number".to_string()
|
|
|
|
|
)))?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Auth overrides
|
|
|
|
|
if let Ok(jwt_secret) = std::env::var("CONTROL_CENTER_JWT_SECRET") {
|
|
|
|
|
config.auth.jwt_secret = jwt_secret;
|
|
|
|
|
}
|
|
|
|
|
if let Ok(require_mfa) = std::env::var("CONTROL_CENTER_REQUIRE_MFA") {
|
|
|
|
|
config.auth.require_mfa = require_mfa.to_lowercase() == "true";
|
|
|
|
|
}
|
|
|
|
|
if let Ok(session_timeout) = std::env::var("CONTROL_CENTER_SESSION_TIMEOUT_MINUTES") {
|
|
|
|
|
config.auth.session_timeout_minutes = session_timeout.parse()
|
|
|
|
|
.map_err(|_| ControlCenterError::Infrastructure(infrastructure::InfrastructureError::Configuration(
|
|
|
|
|
"CONTROL_CENTER_SESSION_TIMEOUT_MINUTES must be a valid number".to_string()
|
|
|
|
|
)))?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Database overrides
|
|
|
|
|
if let Ok(url) = std::env::var("CONTROL_CENTER_DATABASE_URL") {
|
|
|
|
|
config.database.url = url;
|
|
|
|
|
}
|
|
|
|
|
if let Ok(username) = std::env::var("CONTROL_CENTER_DATABASE_USERNAME") {
|
|
|
|
|
config.database.username = Some(username);
|
|
|
|
|
}
|
|
|
|
|
if let Ok(password) = std::env::var("CONTROL_CENTER_DATABASE_PASSWORD") {
|
|
|
|
|
config.database.password = Some(password);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Logging overrides
|
|
|
|
|
if let Ok(level) = std::env::var("CONTROL_CENTER_LOG_LEVEL") {
|
|
|
|
|
config.logging.level = level;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Compliance overrides
|
|
|
|
|
if let Ok(soc2_enabled) = std::env::var("CONTROL_CENTER_SOC2_ENABLED") {
|
|
|
|
|
config.compliance.soc2.enabled = soc2_enabled.to_lowercase() == "true";
|
|
|
|
|
}
|
|
|
|
|
if let Ok(hipaa_enabled) = std::env::var("CONTROL_CENTER_HIPAA_ENABLED") {
|
|
|
|
|
config.compliance.hipaa.enabled = hipaa_enabled.to_lowercase() == "true";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-07 10:59:52 +01:00
|
|
|
/// Load configuration from file with environment variable interpolation
|
|
|
|
|
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
|
|
|
|
|
let content = std::fs::read_to_string(path.as_ref())
|
2026-01-08 21:32:59 +00:00
|
|
|
.map_err(|e| ControlCenterError::Infrastructure(infrastructure::InfrastructureError::Configuration(
|
2025-10-07 10:59:52 +01:00
|
|
|
format!("Failed to read config file {:?}: {}", path.as_ref(), e)
|
2026-01-08 21:32:59 +00:00
|
|
|
)))?;
|
2025-10-07 10:59:52 +01:00
|
|
|
|
|
|
|
|
// Interpolate environment variables
|
|
|
|
|
let interpolated = Self::interpolate_env_vars(&content)?;
|
|
|
|
|
|
|
|
|
|
let config: Self = toml::from_str(&interpolated)
|
2026-01-08 21:32:59 +00:00
|
|
|
.map_err(|e| ControlCenterError::Infrastructure(infrastructure::InfrastructureError::Configuration(
|
2025-10-07 10:59:52 +01:00
|
|
|
format!("Failed to parse config: {}", e)
|
2026-01-08 21:32:59 +00:00
|
|
|
)))?;
|
2025-10-07 10:59:52 +01:00
|
|
|
|
|
|
|
|
config.validate()?;
|
|
|
|
|
Ok(config)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Interpolate environment variables in configuration
|
|
|
|
|
fn interpolate_env_vars(content: &str) -> Result<String> {
|
|
|
|
|
let mut result = content.to_string();
|
|
|
|
|
|
|
|
|
|
// Replace ${VAR_NAME} with environment variable values
|
|
|
|
|
let re = regex::Regex::new(r"\$\{([^}]+)\}")
|
2026-01-08 21:32:59 +00:00
|
|
|
.map_err(|e| ControlCenterError::Infrastructure(infrastructure::InfrastructureError::Configuration(
|
2025-10-07 10:59:52 +01:00
|
|
|
format!("Invalid regex pattern: {}", e)
|
2026-01-08 21:32:59 +00:00
|
|
|
)))?;
|
2025-10-07 10:59:52 +01:00
|
|
|
|
|
|
|
|
for captures in re.captures_iter(content) {
|
|
|
|
|
let var_name = &captures[1];
|
|
|
|
|
if let Ok(var_value) = std::env::var(var_name) {
|
|
|
|
|
let placeholder = format!("${{{}}}", var_name);
|
|
|
|
|
result = result.replace(&placeholder, &var_value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(result)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Validate configuration values
|
|
|
|
|
pub fn validate(&self) -> Result<()> {
|
|
|
|
|
// Validate server config
|
|
|
|
|
if self.server.port == 0 {
|
2026-01-08 21:32:59 +00:00
|
|
|
return Err(ControlCenterError::Infrastructure(infrastructure::InfrastructureError::Configuration(
|
2025-10-07 10:59:52 +01:00
|
|
|
"Server port cannot be 0".to_string()
|
2026-01-08 21:32:59 +00:00
|
|
|
)));
|
2025-10-07 10:59:52 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate policy directories exist
|
|
|
|
|
if !self.policies.policy_dir.exists() {
|
2026-01-08 21:32:59 +00:00
|
|
|
return Err(ControlCenterError::Infrastructure(infrastructure::InfrastructureError::Configuration(
|
2025-10-07 10:59:52 +01:00
|
|
|
format!("Policy directory does not exist: {:?}", self.policies.policy_dir)
|
2026-01-08 21:32:59 +00:00
|
|
|
)));
|
2025-10-07 10:59:52 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate auth config
|
|
|
|
|
if self.auth.jwt_secret == "change_me_in_production" {
|
|
|
|
|
tracing::warn!("Using default JWT secret - change in production!");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if self.auth.jwt_secret.len() < 32 {
|
2026-01-08 21:32:59 +00:00
|
|
|
return Err(ControlCenterError::Infrastructure(infrastructure::InfrastructureError::Configuration(
|
2025-10-07 10:59:52 +01:00
|
|
|
"JWT secret must be at least 32 characters".to_string()
|
2026-01-08 21:32:59 +00:00
|
|
|
)));
|
2025-10-07 10:59:52 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate password policy
|
|
|
|
|
if self.auth.password_policy.min_length < 8 {
|
2026-01-08 21:32:59 +00:00
|
|
|
return Err(ControlCenterError::Infrastructure(infrastructure::InfrastructureError::Configuration(
|
2025-10-07 10:59:52 +01:00
|
|
|
"Password minimum length must be at least 8 characters".to_string()
|
2026-01-08 21:32:59 +00:00
|
|
|
)));
|
2025-10-07 10:59:52 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get configuration as JSON string
|
|
|
|
|
pub fn to_json(&self) -> Result<String> {
|
|
|
|
|
serde_json::to_string_pretty(self)
|
2026-01-08 21:32:59 +00:00
|
|
|
.map_err(|e| ControlCenterError::Infrastructure(infrastructure::InfrastructureError::Configuration(
|
2025-10-07 10:59:52 +01:00
|
|
|
format!("Failed to serialize config to JSON: {}", e)
|
2026-01-08 21:32:59 +00:00
|
|
|
)))
|
2025-10-07 10:59:52 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Create a default configuration file
|
|
|
|
|
pub fn create_default_config<P: AsRef<Path>>(path: P) -> Result<()> {
|
|
|
|
|
let config = Self::default();
|
|
|
|
|
let toml_content = toml::to_string_pretty(&config)
|
2026-01-08 21:32:59 +00:00
|
|
|
.map_err(|e| ControlCenterError::Infrastructure(infrastructure::InfrastructureError::Configuration(
|
2025-10-07 10:59:52 +01:00
|
|
|
format!("Failed to serialize default config: {}", e)
|
2026-01-08 21:32:59 +00:00
|
|
|
)))?;
|
2025-10-07 10:59:52 +01:00
|
|
|
|
|
|
|
|
std::fs::write(path.as_ref(), toml_content)
|
2026-01-08 21:32:59 +00:00
|
|
|
.map_err(|e| ControlCenterError::Infrastructure(infrastructure::InfrastructureError::Configuration(
|
2025-10-07 10:59:52 +01:00
|
|
|
format!("Failed to write config file {:?}: {}", path.as_ref(), e)
|
2026-01-08 21:32:59 +00:00
|
|
|
)))?;
|
2025-10-07 10:59:52 +01:00
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
2026-01-08 21:32:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ConfigLoader for ControlCenterConfig {
|
|
|
|
|
fn service_name() -> &'static str {
|
|
|
|
|
"control-center"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn load_from_hierarchy() -> std::result::Result<Self, Box<dyn std::error::Error + Send + Sync>> {
|
|
|
|
|
let service = Self::service_name();
|
|
|
|
|
|
|
|
|
|
if let Some(path) = platform_config::resolve_config_path(service) {
|
|
|
|
|
return Self::from_path(&path)
|
|
|
|
|
.map_err(|e| Box::new(std::io::Error::other(e.to_string())) as Box<dyn std::error::Error + Send + Sync>);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(Self::default())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn apply_env_overrides(&mut self) -> std::result::Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
|
|
|
Self::apply_env_overrides(self)
|
|
|
|
|
.map_err(|e| Box::new(std::io::Error::other(e.to_string())) as Box<dyn std::error::Error + Send + Sync>)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn from_path<P: AsRef<Path>>(path: P) -> std::result::Result<Self, Box<dyn std::error::Error + Send + Sync>> {
|
|
|
|
|
let path = path.as_ref();
|
|
|
|
|
let json_value = platform_config::format::load_config(path)
|
|
|
|
|
.map_err(|e| {
|
|
|
|
|
let err: Box<dyn std::error::Error + Send + Sync> = Box::new(e);
|
|
|
|
|
err
|
|
|
|
|
})?;
|
|
|
|
|
|
|
|
|
|
serde_json::from_value(json_value)
|
|
|
|
|
.map_err(|e| {
|
|
|
|
|
let err_msg = format!("Failed to deserialize control-center config from {:?}: {}", path, e);
|
|
|
|
|
Box::new(std::io::Error::new(std::io::ErrorKind::InvalidData, err_msg)) as Box<dyn std::error::Error + Send + Sync>
|
|
|
|
|
})
|
|
|
|
|
}
|
2025-10-07 10:59:52 +01:00
|
|
|
}
|