Private
Public Access
1
0
Files
linux_patch_api/src/config/loader.rs
Echo f1a76e33f3
Some checks failed
CI/CD Pipeline / Code Format (push) Failing after 12s
CI/CD Pipeline / Clippy Lints (push) Failing after 5m34s
CI/CD Pipeline / Unit Tests (push) Failing after 10m51s
CI/CD Pipeline / Build Debian Package (push) Failing after 1s
CI/CD Pipeline / Build RPM Package (push) Failing after 1s
CI/CD Pipeline / Build Alpine Package (push) Failing after 2s
CI/CD Pipeline / Build Arch Package (push) Failing after 2s
CI/CD Pipeline / Create Release (push) Has been skipped
CI/CD Pipeline / Security Audit (push) Failing after 15m40s
Fix clippy warnings: remove unused imports/variables/functions, derive Default, fix comparisons
2026-04-12 15:23:02 +00:00

276 lines
7.9 KiB
Rust

//! Configuration Loader - YAML config loading
//!
//! Loads and parses YAML configuration files.
use anyhow::{Context, Result};
use serde::Deserialize;
/// Server configuration
#[derive(Debug, Deserialize, Clone)]
pub struct ServerConfig {
pub port: u16,
pub bind: String,
#[serde(default = "default_timeout")]
pub timeout_seconds: u64,
}
fn default_timeout() -> u64 {
30
}
/// TLS/mTLS configuration
#[derive(Debug, Deserialize, Clone)]
pub struct TlsConfig {
#[serde(default = "default_true")]
pub enabled: bool,
pub port: u16,
pub ca_cert: String,
pub server_cert: String,
pub server_key: String,
#[serde(default = "default_tls_version")]
pub min_tls_version: String,
}
fn default_true() -> bool {
true
}
fn default_tls_version() -> String {
"1.3".to_string()
}
/// Jobs configuration
#[derive(Debug, Deserialize, Clone)]
pub struct JobsConfig {
pub max_concurrent: usize,
pub timeout_minutes: u64,
#[serde(default = "default_storage_path")]
pub storage_path: String,
}
fn default_storage_path() -> String {
"/var/lib/linux_patch_api/jobs".to_string()
}
/// Logging configuration
#[derive(Debug, Deserialize, Clone)]
pub struct LoggingConfig {
#[serde(default = "default_log_level")]
pub level: String,
#[serde(default = "default_true")]
pub journal_enabled: bool,
#[serde(default)]
pub syslog_enabled: bool,
#[serde(default)]
pub syslog_server: Option<String>,
#[serde(default = "default_log_path")]
pub file_path: String,
#[serde(default = "default_retention_days")]
pub retention_days: u64,
}
fn default_log_level() -> String {
"info".to_string()
}
fn default_log_path() -> String {
"/var/log/linux_patch_api/audit.log".to_string()
}
fn default_retention_days() -> u64 {
30
}
/// Whitelist configuration
#[derive(Debug, Deserialize, Clone)]
pub struct WhitelistConfig {
#[serde(default = "default_whitelist_path")]
pub path: String,
}
fn default_whitelist_path() -> String {
"/etc/linux_patch_api/whitelist.yaml".to_string()
}
/// Package manager configuration
#[derive(Debug, Deserialize, Clone)]
pub struct PackageManagerConfig {
#[serde(default = "default_backend")]
pub backend: String,
}
fn default_backend() -> String {
"auto".to_string()
}
/// Application configuration
#[derive(Debug, Deserialize, Clone)]
pub struct AppConfig {
pub server: ServerConfig,
#[serde(default)]
pub tls: Option<TlsConfig>,
pub jobs: JobsConfig,
pub logging: LoggingConfig,
#[serde(default)]
pub whitelist: Option<WhitelistConfig>,
#[serde(default)]
pub package_manager: Option<PackageManagerConfig>,
}
impl AppConfig {
/// Load configuration from a YAML file
pub fn load(path: &str) -> Result<Self> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("Failed to read config file: {}", path))?;
let config: AppConfig = serde_yaml::from_str(&content)
.with_context(|| format!("Failed to parse config file: {}", path))?;
// Validate TLS configuration if enabled
if let Some(ref tls) = config.tls {
if tls.enabled {
if !std::path::Path::new(&tls.ca_cert).exists() {
anyhow::bail!("TLS CA certificate not found: {}", tls.ca_cert);
}
if !std::path::Path::new(&tls.server_cert).exists() {
anyhow::bail!("TLS server certificate not found: {}", tls.server_cert);
}
if !std::path::Path::new(&tls.server_key).exists() {
anyhow::bail!("TLS server key not found: {}", tls.server_key);
}
}
}
Ok(config)
}
/// Get TLS configuration or default
pub fn tls_config(&self) -> Option<&TlsConfig> {
self.tls.as_ref().filter(|t| t.enabled)
}
/// Get whitelist configuration path
pub fn whitelist_path(&self) -> &str {
self.whitelist
.as_ref()
.map(|w| w.path.as_str())
.unwrap_or("/etc/linux_patch_api/whitelist.yaml")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_load_valid_yaml() {
let result = AppConfig::load("tests/fixtures/valid_config.yaml");
assert!(
result.is_ok(),
"Failed to load valid config: {:?}",
result.err()
);
let config = result.unwrap();
assert_eq!(config.server.port, 12443);
assert_eq!(config.server.bind, "127.0.0.1");
assert_eq!(config.jobs.max_concurrent, 5);
assert_eq!(config.jobs.timeout_minutes, 30);
assert_eq!(config.logging.level, "info");
}
#[test]
fn test_config_load_missing_file() {
let result = AppConfig::load("/nonexistent/path/config.yaml");
assert!(result.is_err(), "Should fail for missing file");
let err = result.unwrap_err();
assert!(err.to_string().contains("Failed to read config file"));
}
#[test]
fn test_config_load_invalid_yaml() {
let invalid_path = "/tmp/invalid_config_test.yaml";
std::fs::write(invalid_path, "invalid: yaml: content: [").unwrap();
let result = AppConfig::load(invalid_path);
assert!(result.is_err(), "Should fail for invalid yaml");
std::fs::remove_file(invalid_path).unwrap();
}
#[test]
fn test_config_validation_port_range() {
let result = AppConfig::load("tests/fixtures/valid_config.yaml");
assert!(result.is_ok());
let config = result.unwrap();
assert!(config.server.port >= 1);
}
#[test]
fn test_config_validation_bind_address() {
let result = AppConfig::load("tests/fixtures/valid_config.yaml");
assert!(result.is_ok());
let config = result.unwrap();
assert!(!config.server.bind.is_empty());
}
#[test]
fn test_config_validation_max_concurrent() {
let result = AppConfig::load("tests/fixtures/valid_config.yaml");
assert!(result.is_ok());
let config = result.unwrap();
assert!(config.jobs.max_concurrent > 0);
}
#[test]
fn test_config_validation_timeout() {
let result = AppConfig::load("tests/fixtures/valid_config.yaml");
assert!(result.is_ok());
let config = result.unwrap();
assert!(config.jobs.timeout_minutes >= 1 && config.jobs.timeout_minutes <= 1440);
}
#[test]
fn test_tls_config_defaults() {
let config = AppConfig {
server: ServerConfig {
port: 12443,
bind: "0.0.0.0".to_string(),
timeout_seconds: 30,
},
tls: Some(TlsConfig {
enabled: true,
port: 12443,
ca_cert: "/etc/linux_patch_api/certs/ca.pem".to_string(),
server_cert: "/etc/linux_patch_api/certs/server.pem".to_string(),
server_key: "/etc/linux_patch_api/certs/server.key".to_string(),
min_tls_version: "1.3".to_string(),
}),
jobs: JobsConfig {
max_concurrent: 5,
timeout_minutes: 30,
storage_path: "/var/lib/linux_patch_api/jobs".to_string(),
},
logging: LoggingConfig {
level: "info".to_string(),
journal_enabled: true,
syslog_enabled: false,
syslog_server: None,
file_path: "/var/log/linux_patch_api/audit.log".to_string(),
retention_days: 30,
},
whitelist: Some(WhitelistConfig {
path: "/etc/linux_patch_api/whitelist.yaml".to_string(),
}),
package_manager: None,
};
assert!(config.tls_config().is_some());
assert_eq!(config.tls_config().unwrap().min_tls_version, "1.3");
assert_eq!(
config.whitelist_path(),
"/etc/linux_patch_api/whitelist.yaml"
);
}
}