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
10pub 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 #[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}