use serde::Deserialize; #[derive(Clone, Debug, Deserialize)] #[serde(default)] pub struct CiConfig { pub enabled: bool, pub secret_encryption_key: String, pub event_queue_topic: String, pub event_dlq_topic: String, pub lease_secs: u64, pub heartbeat_interval_secs: u64, pub long_poll_timeout_secs: u64, pub max_attempts: i32, pub job_timeout_default_secs: u64, pub job_timeout_max_secs: u64, pub log_chunk_max_bytes: u64, pub log_retention_days: i64, pub artifact_retention_days: i64, pub run_retention_days: i64, pub artifact_max_bytes: u64, pub outbound_max_attempts: i32, pub outbound_backoff_base_secs: i64, pub outbound_timeout_secs: u64, pub scheduler_interval_secs: u64, pub cancel_grace_secs: u64, pub runner_stale_secs: u64, pub actions_engine_binary: String, pub actions_engine_timeout_secs: u64, pub runtime_base_url: String, pub cache_retention_days: i64, pub cache_max_bytes: u64, pub oidc_private_key: String, } impl Default for CiConfig { fn default() -> Self { Self { enabled: false, secret_encryption_key: String::new(), event_queue_topic: "gitdata:events".to_string(), event_dlq_topic: "gitdata:events:dlq".to_string(), lease_secs: 60, heartbeat_interval_secs: 15, long_poll_timeout_secs: 30, max_attempts: 2, job_timeout_default_secs: 3600, job_timeout_max_secs: 21600, log_chunk_max_bytes: 262_144, log_retention_days: 30, artifact_retention_days: 30, run_retention_days: 180, artifact_max_bytes: 2 * 1024 * 1024 * 1024, outbound_max_attempts: 5, outbound_backoff_base_secs: 60, outbound_timeout_secs: 10, scheduler_interval_secs: 2, cancel_grace_secs: 120, runner_stale_secs: 180, actions_engine_binary: "gitdata-actions-engine".into(), actions_engine_timeout_secs: 30, runtime_base_url: String::new(), cache_retention_days: 7, cache_max_bytes: 10 * 1024 * 1024 * 1024, oidc_private_key: String::new(), } } } impl CiConfig { pub fn validate(&self) -> Result<(), String> { if !self.enabled { return Ok(()); } if self.secret_encryption_key.trim().is_empty() { return Err( "ci.secret_encryption_key must not be empty when ci is enabled".to_string(), ); } let decoded = base64::Engine::decode( &base64::engine::general_purpose::STANDARD, self.secret_encryption_key.trim(), ) .map_err(|_| "ci.secret_encryption_key must be base64".to_string())?; if decoded.len() < 32 { return Err("ci.secret_encryption_key must decode to at least 32 bytes".to_string()); } if self.event_queue_topic.trim().is_empty() || self.event_dlq_topic.trim().is_empty() { return Err("ci event topics must not be empty".to_string()); } if self.lease_secs == 0 || self.heartbeat_interval_secs == 0 || self.long_poll_timeout_secs == 0 || self.job_timeout_default_secs == 0 || self.job_timeout_max_secs == 0 || self.job_timeout_max_secs < self.job_timeout_default_secs || self.log_chunk_max_bytes == 0 || self.scheduler_interval_secs == 0 || self.runner_stale_secs == 0 { return Err("ci timing and size limits must be greater than zero".to_string()); } if self.max_attempts <= 0 || self.outbound_max_attempts <= 0 { return Err("ci attempt limits must be greater than zero".to_string()); } if self.log_retention_days < 0 || self.artifact_retention_days < 0 || self.run_retention_days < 0 { return Err("ci retention days must not be negative".to_string()); } if self.heartbeat_interval_secs >= self.lease_secs { return Err("ci.heartbeat_interval_secs must be less than ci.lease_secs".to_string()); } Ok(()) } }