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
- 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
308 lines
9.0 KiB
Rust
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"
|
|
);
|
|
}
|
|
}
|