mirror of https://github.com/breard-r/acmed.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
173 lines
5.1 KiB
173 lines
5.1 KiB
mod account;
|
|
mod certificate;
|
|
mod duration;
|
|
mod endpoint;
|
|
mod global;
|
|
mod hook;
|
|
mod rate_limit;
|
|
|
|
pub use account::*;
|
|
pub use certificate::*;
|
|
pub use duration::*;
|
|
pub use endpoint::*;
|
|
pub use global::*;
|
|
pub use hook::*;
|
|
pub use rate_limit::*;
|
|
|
|
use anyhow::{Context, Result};
|
|
use config::{Config, File};
|
|
use serde_derive::Deserialize;
|
|
use std::collections::HashMap;
|
|
use std::path::{Path, PathBuf};
|
|
use walkdir::WalkDir;
|
|
|
|
const ALLOWED_FILE_EXT: &[&str] = &["toml"];
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct AcmedConfig {
|
|
pub(in crate::config) global: Option<GlobalOptions>,
|
|
#[serde(default)]
|
|
pub(in crate::config) endpoint: HashMap<String, Endpoint>,
|
|
#[serde(default, rename = "rate-limit")]
|
|
pub(in crate::config) rate_limit: HashMap<String, RateLimit>,
|
|
#[serde(default)]
|
|
pub(in crate::config) hook: Vec<Hook>,
|
|
#[serde(default)]
|
|
pub(in crate::config) group: Vec<Group>,
|
|
#[serde(default)]
|
|
pub(in crate::config) account: HashMap<String, Account>,
|
|
#[serde(default)]
|
|
pub(in crate::config) certificate: Vec<Certificate>,
|
|
}
|
|
|
|
pub fn load<P: AsRef<Path>>(config_dir: P) -> Result<AcmedConfig> {
|
|
let config_dir = config_dir.as_ref();
|
|
tracing::debug!("loading config directory: {config_dir:?}");
|
|
let settings = Config::builder()
|
|
.add_source(
|
|
get_files(config_dir)?
|
|
.iter()
|
|
.map(|path| File::from(path.as_path()))
|
|
.collect::<Vec<_>>(),
|
|
)
|
|
.build()?;
|
|
tracing::trace!("loaded config: {settings:?}");
|
|
let config: AcmedConfig = settings.try_deserialize().context("invalid setting")?;
|
|
tracing::debug!("computed config: {config:?}");
|
|
Ok(config)
|
|
}
|
|
|
|
fn get_files(config_dir: &Path) -> Result<Vec<PathBuf>> {
|
|
let mut file_lst = Vec::new();
|
|
for entry in WalkDir::new(config_dir).follow_links(true) {
|
|
let path = entry?.path().to_path_buf();
|
|
if path.is_file() {
|
|
if let Some(ext) = path.extension() {
|
|
if ALLOWED_FILE_EXT.iter().any(|&e| e == ext) {
|
|
std::fs::File::open(&path).with_context(|| path.display().to_string())?;
|
|
file_lst.push(path);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
file_lst.sort();
|
|
tracing::debug!("configuration files found: {file_lst:?}");
|
|
Ok(file_lst)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
fn load_str<'de, T: serde::de::Deserialize<'de>>(config_str: &str) -> Result<T> {
|
|
let settings = Config::builder()
|
|
.add_source(File::from_str(config_str, config::FileFormat::Toml))
|
|
.build()?;
|
|
let config: T = settings.try_deserialize().context("invalid setting")?;
|
|
Ok(config)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn empty() {
|
|
let cfg = load("tests/config/empty").unwrap();
|
|
assert!(cfg.global.is_none());
|
|
assert!(cfg.rate_limit.is_empty());
|
|
assert!(cfg.endpoint.is_empty());
|
|
assert!(cfg.hook.is_empty());
|
|
assert!(cfg.group.is_empty());
|
|
assert!(cfg.account.is_empty());
|
|
assert!(cfg.certificate.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn simple() {
|
|
let cfg = load("tests/config/simple").unwrap();
|
|
assert!(cfg.global.is_some());
|
|
let global = cfg.global.unwrap();
|
|
assert_eq!(
|
|
global.accounts_directory,
|
|
PathBuf::from("/tmp/example/account/dir")
|
|
);
|
|
assert_eq!(
|
|
global.certificates_directory,
|
|
PathBuf::from("/tmp/example/cert/dir")
|
|
);
|
|
assert!(cfg.rate_limit.is_empty());
|
|
assert!(cfg.endpoint.is_empty());
|
|
assert!(cfg.hook.is_empty());
|
|
assert!(cfg.group.is_empty());
|
|
assert_eq!(cfg.account.len(), 1);
|
|
let account = cfg.account.get("example").unwrap();
|
|
assert_eq!(account.contacts.len(), 1);
|
|
assert!(account.env.is_empty());
|
|
assert!(account.external_account.is_none());
|
|
assert!(account.hooks.is_empty());
|
|
assert_eq!(account.key_type, AccountKeyType::EcDsaP256);
|
|
assert_eq!(
|
|
account.signature_algorithm,
|
|
Some(AccountSignatureAlgorithm::Hs384)
|
|
);
|
|
assert!(cfg.certificate.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn setting_override() {
|
|
let cfg = load("tests/config/override").unwrap();
|
|
assert!(cfg.global.is_some());
|
|
let global = cfg.global.unwrap();
|
|
assert_eq!(
|
|
global.accounts_directory,
|
|
PathBuf::from("/tmp/other/account/dir")
|
|
);
|
|
assert_eq!(
|
|
global.certificates_directory,
|
|
PathBuf::from("/tmp/example/cert/dir")
|
|
);
|
|
assert!(cfg.rate_limit.is_empty());
|
|
assert_eq!(cfg.endpoint.len(), 2);
|
|
println!("Debug: cfg.endpoint: {:?}", cfg.endpoint);
|
|
let ac1 = cfg.endpoint.get("test ac 1").unwrap();
|
|
assert_eq!(ac1.url, "https://acme-v02.ac1.example.org/directory");
|
|
assert_eq!(ac1.tos_agreed, true);
|
|
assert!(ac1.random_early_renew.is_none());
|
|
assert!(ac1.root_certificates.is_empty());
|
|
let ac2 = cfg.endpoint.get("test ac 2").unwrap();
|
|
assert_eq!(ac2.url, "https://acme-v02.ac2.example.org/directory");
|
|
assert_eq!(ac2.tos_agreed, false);
|
|
assert_eq!(ac2.random_early_renew, Some(Duration::from_secs(10)));
|
|
assert_eq!(ac2.root_certificates, vec![PathBuf::from("test.pem")]);
|
|
assert!(cfg.hook.is_empty());
|
|
assert!(cfg.group.is_empty());
|
|
assert_eq!(cfg.account.len(), 1);
|
|
let account = cfg.account.get("example").unwrap();
|
|
assert_eq!(account.contacts.len(), 1);
|
|
assert!(account.env.is_empty());
|
|
assert!(account.external_account.is_none());
|
|
assert!(account.hooks.is_empty());
|
|
assert_eq!(account.key_type, AccountKeyType::EcDsaP256);
|
|
assert!(account.signature_algorithm.is_none());
|
|
assert!(cfg.certificate.is_empty());
|
|
}
|
|
}
|