use std::net::IpAddr; use config::{Config, ConfigError}; use tracing::{Level, debug, error, warn}; const LOGGING_LEVEL_KEY: &str = "LOGGING.LEVEL"; const LOGGING_UTC_KEY: &str = "LOGGING.UTC"; const SERVER_ADDRESS_KEY: &str = "SERVER.ADDRESS"; const SERVER_PORT_KEY: &str = "SERVER.PORT"; const DATABASE_URL_KEY: &str = "DATABASE.URL"; const DATABASE_MAX_CONNECTIONS_KEY: &str = "DATABASE.MAX_CONNECTIONS"; const DATABASE_MIGRATE_ON_STARTUP_KEY: &str = "DATABASE.MIGRATION.MIGRATE_ON_STARTUP"; trait FromConfig: Sized { fn from_config(config: &Config) -> Result; fn validate(&self) -> Result<(), String>; } #[derive(Debug, Clone)] pub struct ProgramSettings { pub logging: LoggingSettings, pub database: DatabaseSettings, pub server: ServerSettings, } #[derive(Debug, Clone)] pub struct LoggingSettings { pub level: Level, pub utc: bool, } #[derive(Debug, Clone)] pub struct DatabaseSettings { pub url: String, pub max_connections: u32, pub migrate_on_startup: bool, } #[derive(Debug, Clone)] pub struct ServerSettings { pub address: IpAddr, pub port: u16, } impl FromConfig for ProgramSettings { fn from_config(_config: &Config) -> Result { let config = ProgramSettings { logging: LoggingSettings::from_config(_config)?, database: DatabaseSettings::from_config(_config)?, server: ServerSettings::from_config(_config)?, }; config.validate()?; Ok(config) } fn validate(&self) -> Result<(), String> { self.logging.validate()?; self.database.validate()?; self.server.validate()?; Ok(()) } } impl FromConfig for LoggingSettings { fn from_config(_config: &Config) -> Result { const DEFAULT_LOGGING_LEVEL: Level = Level::INFO; Ok(LoggingSettings { level: _config .get_string(LOGGING_LEVEL_KEY) .unwrap_or_else(|err| { warn!( "Failed to read {} from configuration, defaulting to {}. Error: {}", LOGGING_LEVEL_KEY, DEFAULT_LOGGING_LEVEL, err ); DEFAULT_LOGGING_LEVEL.to_string() }) .parse() .unwrap_or_else(|err| { warn!( "Invalid logging level in configuration, defaulting to {}. Error: {}", DEFAULT_LOGGING_LEVEL, err ); DEFAULT_LOGGING_LEVEL }), utc: _config .get_bool(LOGGING_UTC_KEY) .unwrap_or_else(|err: ConfigError| { const DEFAULT_UTC: bool = false; warn!( "Invalid UTC setting in configuration, defaulting to {}. Error: {}", DEFAULT_UTC, err ); DEFAULT_UTC }), }) } fn validate(&self) -> Result<(), String> { Ok(()) } } impl FromConfig for DatabaseSettings { fn from_config(_config: &Config) -> Result { Ok(DatabaseSettings { url: _config .get_string(DATABASE_URL_KEY) .map_err(|op| match op { ConfigError::NotFound(_) => "Database URL not found in configuration".into(), err => { format!("Failed to read Database URL from configuration {err}") } })?, max_connections: _config .get_int(DATABASE_MAX_CONNECTIONS_KEY) .unwrap_or_else(|err| { const DEFAULT_MAX_CONNECTIONS: i64 = 10; warn!( "{} not set or invalid in configuration, defaulting to {}. Error: {}", DATABASE_MAX_CONNECTIONS_KEY, DEFAULT_MAX_CONNECTIONS, err ); DEFAULT_MAX_CONNECTIONS }) as u32, migrate_on_startup: _config .get_bool(DATABASE_MIGRATE_ON_STARTUP_KEY) .unwrap_or_else(|err| { const DEFAULT_MIGRATE_ON_STARTUP: bool = true; warn!( "{} not set or invalid in configuration, defaulting to {}. Error: {}", DATABASE_MIGRATE_ON_STARTUP_KEY, DEFAULT_MIGRATE_ON_STARTUP, err ); DEFAULT_MIGRATE_ON_STARTUP }), }) } fn validate(&self) -> Result<(), String> { Ok(()) } } impl FromConfig for ServerSettings { fn from_config(_config: &Config) -> Result { Ok(ServerSettings { address: _config .get_string(SERVER_ADDRESS_KEY) .unwrap_or_else(|err| { const DEFAULT_ADDRESS: &str = "0.0.0.0"; match err { ConfigError::NotFound(_) => {} _ => { warn!( "Failed to read {} from configuration, defaulting to {}. Error: {}", SERVER_ADDRESS_KEY, DEFAULT_ADDRESS, err ); } }; DEFAULT_ADDRESS.to_string() }) .parse() .map_err(|e| format!("Invalid {} in configuration: {}", SERVER_ADDRESS_KEY, e))?, port: _config.get_int(SERVER_PORT_KEY).unwrap_or_else(|err| { const DEFAULT_PORT: i64 = 8080; warn!( "{} not set or invalid in configuration, defaulting to {}. Error: {}", SERVER_PORT_KEY, DEFAULT_PORT, err ); DEFAULT_PORT }) as u16, }) } fn validate(&self) -> Result<(), String> { #[allow(clippy::absurd_extreme_comparisons, unused_comparisons)] if self.port == 0 || self.port > 65535 { return Err("Server port must be between 1 and 65535".into()); } Ok(()) } } pub fn get_program_settings() -> ProgramSettings { debug!("Loading program settings from configuration sources"); let settings = Config::builder() // dev / generated config has the highest priority (Overwrite by user config files) .add_source(config::File::with_name("generated-config.yaml").required(false)) // user config files .add_source( config::File::with_name("/etc/yet-another-nginx-proxy-manager/config").required(false), ) .add_source( config::File::with_name("$HOME/.config/yet-another-nginx-proxy-manager/config") .required(false), ) .add_source(config::File::with_name("config.yaml").required(false)) // environment variables have the highest priority (Overwrite all config files) .add_source( config::Environment::with_prefix("YANPM") .separator("__") .prefix_separator("_"), ) .build() .expect("Failed to build configuration"); debug!("Configuration sources loaded successfully"); debug!("Parsing program settings from configuration"); ProgramSettings::from_config(&settings) .inspect_err(|err| { error!("Configuration error: {}", err); debug!("Current configurations: {:#?}", settings); }) .inspect(|_| { debug!("Program settings parsed successfully"); }) .expect("Failed to load program settings from configuration") }