Skip to main content

bindizr_core/config/
mod.rs

1#[cfg(test)]
2mod tests;
3
4use std::{env, fmt, net::IpAddr, path::PathBuf};
5
6use config::{Config, File, FileFormat};
7use once_cell::sync::OnceCell;
8use serde::{Deserialize, Serialize};
9
10// Config file path
11pub const BINDIZR_CONF_DIR: &str = "/etc/bindizr";
12pub const BINDIZR_CONF_PATH: &str = "/etc/bindizr/bindizr.conf.toml";
13
14static BINDIZR_CONFIG: OnceCell<BindizrConfig> = OnceCell::new();
15
16#[derive(Clone, Debug, Deserialize, Serialize)]
17pub struct BindizrConfig {
18    pub api: ApiConfig,
19    pub database: DatabaseConfig,
20    pub dns: DnsConfig,
21    pub logging: LoggingConfig,
22}
23
24#[derive(Clone, Debug, Deserialize, Serialize)]
25pub struct ApiConfig {
26    pub listen_addr: IpAddr,
27    #[serde(alias = "port")]
28    pub listen_port: u16,
29    pub require_authentication: bool,
30}
31
32#[derive(Clone, Debug, Deserialize, Serialize)]
33pub struct DatabaseConfig {
34    #[serde(rename = "type")]
35    pub database_type: DatabaseType,
36    #[serde(default)]
37    pub mysql: MysqlConfig,
38    #[serde(default)]
39    pub sqlite: SqliteConfig,
40    #[serde(alias = "postgres", default)]
41    pub postgresql: PostgresqlConfig,
42}
43
44#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
45#[serde(rename_all = "lowercase")]
46pub enum DatabaseType {
47    Mysql,
48    Sqlite,
49    Postgresql,
50}
51
52impl fmt::Display for DatabaseType {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        let value = match self {
55            DatabaseType::Mysql => "mysql",
56            DatabaseType::Sqlite => "sqlite",
57            DatabaseType::Postgresql => "postgresql",
58        };
59        write!(f, "{}", value)
60    }
61}
62
63#[derive(Clone, Debug, Default, Deserialize, Serialize)]
64pub struct MysqlConfig {
65    pub server_url: String,
66}
67
68#[derive(Clone, Debug, Default, Deserialize, Serialize)]
69pub struct SqliteConfig {
70    pub file_path: String,
71}
72
73#[derive(Clone, Debug, Default, Deserialize, Serialize)]
74pub struct PostgresqlConfig {
75    pub server_url: String,
76}
77
78#[derive(Clone, Debug, Deserialize, Serialize)]
79pub struct DnsConfig {
80    pub listen_addr: IpAddr,
81    pub listen_port: u16,
82    pub secondary_addrs: String,
83    #[serde(default = "default_notify_after_update")]
84    pub notify_after_update: bool,
85    #[serde(default)]
86    pub notify_on_startup: bool,
87    #[serde(default = "default_notify_retries")]
88    pub notify_retries: u32,
89    #[serde(default = "default_notify_timeout_secs")]
90    pub notify_timeout_secs: u64,
91    /// Both name and key must be non-empty to enable nsupdate TSIG authentication.
92    #[serde(default)]
93    pub nsupdate_tsig_key_name: String,
94    #[serde(default)]
95    pub nsupdate_tsig_key: String,
96}
97
98fn default_notify_after_update() -> bool {
99    true
100}
101
102fn default_notify_retries() -> u32 {
103    3
104}
105
106fn default_notify_timeout_secs() -> u64 {
107    5
108}
109
110#[derive(Clone, Debug, Deserialize, Serialize)]
111pub struct LoggingConfig {
112    pub log_level: LogLevel,
113}
114
115#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
116#[serde(rename_all = "lowercase")]
117pub enum LogLevel {
118    Trace,
119    Debug,
120    Info,
121    Warn,
122    Error,
123}
124
125impl fmt::Display for LogLevel {
126    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127        let value = match self {
128            LogLevel::Trace => "trace",
129            LogLevel::Debug => "debug",
130            LogLevel::Info => "info",
131            LogLevel::Warn => "warn",
132            LogLevel::Error => "error",
133        };
134        write!(f, "{}", value)
135    }
136}
137
138pub fn initialize(conf_file_path: Option<&str>) {
139    let conf_file_path = conf_file_path
140        .map(str::to_string)
141        .or_else(|| env::var("BINDIZR_CONFIG_PATH").ok())
142        .unwrap_or_else(|| BINDIZR_CONF_PATH.to_string());
143
144    if !PathBuf::from(&conf_file_path).exists() {
145        exit_config_error(format!("Bindizr config does not exist: {}", conf_file_path));
146    }
147
148    println!("Initializing configuration from file: {}", conf_file_path);
149
150    let cfg = load_raw_config(&conf_file_path).unwrap_or_else(|err| exit_config_error(err));
151    let bindizr_config = parse_bindizr_config(cfg).unwrap_or_else(|err| exit_config_error(err));
152
153    BINDIZR_CONFIG.get_or_init(|| bindizr_config);
154}
155
156fn load_raw_config(conf_file_path: &str) -> Result<Config, String> {
157    Config::builder()
158        .add_source(File::new(conf_file_path, FileFormat::Toml).required(true))
159        .build()
160        .map_err(|e| {
161            format!(
162                "Failed to build configuration from file '{}': {}",
163                conf_file_path, e
164            )
165        })
166}
167
168fn parse_bindizr_config(cfg: Config) -> Result<BindizrConfig, String> {
169    parse_bindizr_config_with_env(cfg, |name| env::var(name).ok())
170}
171
172fn parse_bindizr_config_with_env(
173    cfg: Config,
174    get_env: impl Fn(&str) -> Option<String>,
175) -> Result<BindizrConfig, String> {
176    let mut bindizr_config = cfg
177        .try_deserialize::<BindizrConfig>()
178        .map_err(|e| format!("Invalid Bindizr configuration: {}", e))?;
179
180    apply_env_overrides_from(&mut bindizr_config, get_env)?;
181    validate_database_config(&bindizr_config.database)?;
182
183    Ok(bindizr_config)
184}
185
186fn apply_env_overrides_from(
187    config: &mut BindizrConfig,
188    get_env: impl Fn(&str) -> Option<String>,
189) -> Result<(), String> {
190    if let Some(value) = get_env("BINDIZR_API_LISTEN_ADDR") {
191        config.api.listen_addr = parse_env_value("BINDIZR_API_LISTEN_ADDR", &value)?;
192    }
193    if let Some(value) = get_env("BINDIZR_API_PORT") {
194        config.api.listen_port = parse_env_value("BINDIZR_API_PORT", &value)?;
195    }
196    if let Some(value) = get_env("BINDIZR_API_REQUIRE_AUTHENTICATION") {
197        config.api.require_authentication =
198            parse_env_value("BINDIZR_API_REQUIRE_AUTHENTICATION", &value)?;
199    }
200    if let Some(value) = get_env("BINDIZR_DATABASE_TYPE") {
201        config.database.database_type = parse_database_type_env("BINDIZR_DATABASE_TYPE", &value)?;
202    }
203    if let Some(value) = get_env("BINDIZR_MYSQL_SERVER_URL") {
204        config.database.mysql.server_url = value;
205    }
206    if let Some(value) = get_env("BINDIZR_POSTGRESQL_SERVER_URL") {
207        config.database.postgresql.server_url = value;
208    }
209    if let Some(value) = get_env("BINDIZR_SQLITE_FILE_PATH") {
210        config.database.sqlite.file_path = value;
211    }
212    if let Some(value) = get_env("BINDIZR_DATABASE_URL") {
213        match config.database.database_type {
214            DatabaseType::Mysql => config.database.mysql.server_url = value,
215            DatabaseType::Postgresql => config.database.postgresql.server_url = value,
216            DatabaseType::Sqlite => {}
217        }
218    }
219    if let Some(value) = get_env("BINDIZR_DNS_PORT") {
220        config.dns.listen_port = parse_env_value("BINDIZR_DNS_PORT", &value)?;
221    }
222    if let Some(value) = get_env("BINDIZR_DNS_LISTEN_ADDR") {
223        config.dns.listen_addr = parse_env_value("BINDIZR_DNS_LISTEN_ADDR", &value)?;
224    }
225    if let Some(value) = get_env("BINDIZR_SECONDARY_ADDRS") {
226        config.dns.secondary_addrs = value;
227    }
228    if let Some(value) = get_env("BINDIZR_NSUPDATE_TSIG_KEY") {
229        config.dns.nsupdate_tsig_key = value;
230    } else if let Some(value) = get_env("TSIG_SECRET") {
231        config.dns.nsupdate_tsig_key = value;
232    }
233    if let Some(value) = get_env("BINDIZR_NSUPDATE_TSIG_KEY_NAME") {
234        config.dns.nsupdate_tsig_key_name = value;
235    }
236    if let Some(value) = get_env("BINDIZR_NOTIFY_AFTER_UPDATE") {
237        config.dns.notify_after_update = parse_env_value("BINDIZR_NOTIFY_AFTER_UPDATE", &value)?;
238    }
239    if let Some(value) = get_env("BINDIZR_NOTIFY_ON_STARTUP") {
240        config.dns.notify_on_startup = parse_env_value("BINDIZR_NOTIFY_ON_STARTUP", &value)?;
241    }
242    if let Some(value) = get_env("BINDIZR_NOTIFY_RETRIES") {
243        config.dns.notify_retries = parse_env_value("BINDIZR_NOTIFY_RETRIES", &value)?;
244    }
245    if let Some(value) = get_env("BINDIZR_NOTIFY_TIMEOUT_SECS") {
246        config.dns.notify_timeout_secs = parse_env_value("BINDIZR_NOTIFY_TIMEOUT_SECS", &value)?;
247    }
248    if let Some(value) = get_env("BINDIZR_LOG_LEVEL") {
249        config.logging.log_level = parse_log_level_env("BINDIZR_LOG_LEVEL", &value)?;
250    }
251
252    Ok(())
253}
254
255fn parse_env_value<T>(name: &str, value: &str) -> Result<T, String>
256where
257    T: std::str::FromStr,
258    T::Err: fmt::Display,
259{
260    value
261        .parse::<T>()
262        .map_err(|e| format!("Invalid {} environment variable '{}': {}", name, value, e))
263}
264
265fn parse_database_type_env(name: &str, value: &str) -> Result<DatabaseType, String> {
266    match value {
267        "mysql" => Ok(DatabaseType::Mysql),
268        "postgresql" => Ok(DatabaseType::Postgresql),
269        "sqlite" => Ok(DatabaseType::Sqlite),
270        _ => Err(format!(
271            "Invalid {} environment variable '{}': expected mysql, postgresql, or sqlite",
272            name, value
273        )),
274    }
275}
276
277fn parse_log_level_env(name: &str, value: &str) -> Result<LogLevel, String> {
278    match value {
279        "trace" => Ok(LogLevel::Trace),
280        "debug" => Ok(LogLevel::Debug),
281        "info" => Ok(LogLevel::Info),
282        "warn" => Ok(LogLevel::Warn),
283        "error" => Ok(LogLevel::Error),
284        _ => Err(format!(
285            "Invalid {} environment variable '{}': expected trace, debug, info, warn, or error",
286            name, value
287        )),
288    }
289}
290
291fn validate_database_config(config: &DatabaseConfig) -> Result<(), String> {
292    match config.database_type {
293        DatabaseType::Mysql if config.mysql.server_url.trim().is_empty() => Err(
294            "database.mysql.server_url must not be empty when database.type is mysql".to_string(),
295        ),
296        DatabaseType::Postgresql if config.postgresql.server_url.trim().is_empty() => Err(
297            "database.postgresql.server_url must not be empty when database.type is postgresql"
298                .to_string(),
299        ),
300        DatabaseType::Sqlite if config.sqlite.file_path.trim().is_empty() => Err(
301            "database.sqlite.file_path must not be empty when database.type is sqlite".to_string(),
302        ),
303        _ => Ok(()),
304    }
305}
306
307fn exit_config_error(message: String) -> ! {
308    eprintln!("{}", message);
309    std::process::exit(1);
310}
311
312pub fn get_bindizr_config() -> &'static BindizrConfig {
313    BINDIZR_CONFIG.get().expect("Configuration not initialized")
314}