Private
Public Access
1
0
Files
linux_patch_api/src/config/loader.rs
Echo 7c55c99e48
Some checks failed
CI/CD Pipeline / Code Format (push) Failing after 3s
CI/CD Pipeline / Clippy Lints (push) Failing after 44s
CI/CD Pipeline / Enrollment Tests (push) Has been skipped
CI/CD Pipeline / Verify Enrollment CLI Flag (push) Has been skipped
CI/CD Pipeline / All Unit Tests (push) Successful in 1m12s
CI/CD Pipeline / Build Debian Package (push) Has been skipped
CI/CD Pipeline / Build Debian Package (Ubuntu 22.04) (push) Has been skipped
CI/CD Pipeline / Build RPM Package (push) Has been skipped
CI/CD Pipeline / Build Alpine Package (push) Has been skipped
CI/CD Pipeline / Build Arch Package (push) Has been skipped
CI/CD Pipeline / Security Audit (push) Successful in 5s
fix(enrollment): filter Docker bridge IPs and add report_interface/report_ip config
- identity.rs: filter 172.16.0.0/12 (Docker bridge) and 169.254.0.0/16 (link-local)
  from get_ip_addresses() auto-detection
- identity.rs: add is_container_bridge(), is_link_local(),
  get_ip_for_interface(), get_primary_ip() functions
- client.rs: add report_interface/report_ip fields to EnrollmentClient,
  new with_ip_overrides() constructor, register() uses get_primary_ip()
- loader.rs: add report_interface/report_ip to EnrollmentConfig
- mod.rs: wire config overrides through to EnrollmentClient
- config.yaml.example: document new report_interface/report_ip options
- Tests: add 18 new bridge filtering/IP override tests, fix Docker
  container compatibility in existing tests
2026-05-18 02:02:54 +00:00

308 lines
9.0 KiB
Rust

//! Configuration Loader - YAML config loading
//!
//! Loads and parses YAML configuration files.
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
/// 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()
}
/// Enrollment polling configuration
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct EnrollmentConfig {
#[serde(default)]
pub manager_url: String,
#[serde(default)]
pub polling_token: String,
#[serde(default = "default_polling_interval")]
pub polling_interval_seconds: u64,
#[serde(default = "default_max_poll_attempts")]
pub max_poll_attempts: u32,
/// Network interface whose IPv4 address is reported to the manager.
/// Overrides auto-detection. Example: `"eth0"`, `"ens192"`.
#[serde(default)]
pub report_interface: Option<String>,
/// Explicit IPv4 address reported to the manager.
/// Highest priority — overrides both `report_interface` and auto-detect.
#[serde(default)]
pub report_ip: Option<String>,
}
fn default_polling_interval() -> u64 {
60
}
fn default_max_poll_attempts() -> u32 {
1440
}
/// 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>,
#[serde(default)]
pub enrollment: Option<EnrollmentConfig>,
}
impl AppConfig {
/// Load configuration from a YAML file
pub fn load(path: &str, skip_tls_validation: bool) -> 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 (skip during enrollment bootstrap)
if let Some(ref tls) = config.tls {
if tls.enabled && !skip_tls_validation {
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", false);
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", false);
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, false);
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", false);
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", false);
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", false);
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", false);
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,
enrollment: 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"
);
}
}