packages/ak-common/config: init (#21256)

This commit is contained in:
Marc 'risson' Schmitt
2026-04-02 13:05:35 +00:00
committed by GitHub
parent ba82c97409
commit 3355669274
9 changed files with 749 additions and 14 deletions

View File

@@ -2,12 +2,14 @@
allow = [
"Apache-2.0",
"BSD-3-Clause",
"CC0-1.0",
"CDLA-Permissive-2.0",
"ISC",
"MIT",
"MPL-2.0",
"OpenSSL",
"Unicode-3.0",
"Zlib",
]
[licenses.private]

199
Cargo.lock generated
View File

@@ -76,6 +76,12 @@ dependencies = [
"rustversion",
]
[[package]]
name = "arraydeque"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
[[package]]
name = "async-trait"
version = "0.1.89"
@@ -115,12 +121,20 @@ dependencies = [
name = "authentik-common"
version = "2026.5.0-rc1"
dependencies = [
"arc-swap",
"axum-server",
"config",
"eyre",
"glob",
"nix",
"notify",
"serde",
"serde_json",
"tempfile",
"tokio",
"tokio-util",
"tracing",
"url",
]
[[package]]
@@ -201,7 +215,7 @@ version = "0.72.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
dependencies = [
"bitflags",
"bitflags 2.11.0",
"cexpr",
"clang-sys",
"itertools",
@@ -215,6 +229,12 @@ dependencies = [
"syn",
]
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.11.0"
@@ -367,6 +387,19 @@ dependencies = [
"memchr",
]
[[package]]
name = "config"
version = "0.15.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e68cfe19cd7d23ffde002c24ffa5cda73931913ef394d5eaaa32037dc940c0c"
dependencies = [
"async-trait",
"pathdiff",
"serde_core",
"winnow",
"yaml-rust2",
]
[[package]]
name = "core-foundation"
version = "0.9.4"
@@ -477,6 +510,12 @@ dependencies = [
"once_cell",
]
[[package]]
name = "fastrand"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
@@ -520,6 +559,15 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
[[package]]
name = "fsevent-sys"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
dependencies = [
"libc",
]
[[package]]
name = "futures-channel"
version = "0.3.32"
@@ -660,6 +708,15 @@ version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
[[package]]
name = "hashlink"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
dependencies = [
"hashbrown 0.15.5",
]
[[package]]
name = "heck"
version = "0.5.0"
@@ -907,6 +964,26 @@ dependencies = [
"serde_core",
]
[[package]]
name = "inotify"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd5b3eaf1a28b758ac0faa5a4254e8ab2705605496f1b1f3fbbc3988ad73d199"
dependencies = [
"bitflags 2.11.0",
"inotify-sys",
"libc",
]
[[package]]
name = "inotify-sys"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
dependencies = [
"libc",
]
[[package]]
name = "ipnet"
version = "2.12.0"
@@ -1008,6 +1085,26 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "kqueue"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a"
dependencies = [
"kqueue-sys",
"libc",
]
[[package]]
name = "kqueue-sys"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b"
dependencies = [
"bitflags 1.3.2",
"libc",
]
[[package]]
name = "leb128fmt"
version = "0.1.0"
@@ -1030,6 +1127,12 @@ dependencies = [
"windows-link",
]
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "litemap"
version = "0.8.1"
@@ -1092,6 +1195,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
dependencies = [
"libc",
"log",
"wasi",
"windows-sys 0.61.2",
]
@@ -1102,7 +1206,7 @@ version = "0.31.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3"
dependencies = [
"bitflags",
"bitflags 2.11.0",
"cfg-if",
"cfg_aliases",
"libc",
@@ -1118,6 +1222,33 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "notify"
version = "8.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3"
dependencies = [
"bitflags 2.11.0",
"fsevent-sys",
"inotify",
"kqueue",
"libc",
"log",
"mio",
"notify-types",
"walkdir",
"windows-sys 0.60.2",
]
[[package]]
name = "notify-types"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a"
dependencies = [
"bitflags 2.11.0",
]
[[package]]
name = "num-conv"
version = "0.2.0"
@@ -1174,6 +1305,12 @@ dependencies = [
"windows-link",
]
[[package]]
name = "pathdiff"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
[[package]]
name = "percent-encoding"
version = "2.3.2"
@@ -1347,7 +1484,7 @@ version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
"bitflags",
"bitflags 2.11.0",
]
[[package]]
@@ -1459,6 +1596,19 @@ version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "rustix"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
"bitflags 2.11.0",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.61.2",
]
[[package]]
name = "rustls"
version = "0.23.37"
@@ -1577,7 +1727,7 @@ version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
dependencies = [
"bitflags",
"bitflags 2.11.0",
"core-foundation 0.10.1",
"core-foundation-sys",
"libc",
@@ -1773,7 +1923,7 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
dependencies = [
"bitflags",
"bitflags 2.11.0",
"core-foundation 0.9.4",
"system-configuration-sys",
]
@@ -1788,6 +1938,19 @@ dependencies = [
"libc",
]
[[package]]
name = "tempfile"
version = "3.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom 0.4.2",
"once_cell",
"rustix",
"windows-sys 0.61.2",
]
[[package]]
name = "thiserror"
version = "1.0.69"
@@ -1949,7 +2112,7 @@ version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
dependencies = [
"bitflags",
"bitflags 2.11.0",
"bytes",
"futures-util",
"http",
@@ -2219,7 +2382,7 @@ version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
dependencies = [
"bitflags",
"bitflags 2.11.0",
"hashbrown 0.15.5",
"indexmap",
"semver",
@@ -2520,6 +2683,15 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]]
name = "winnow"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5"
dependencies = [
"memchr",
]
[[package]]
name = "wit-bindgen"
version = "0.51.0"
@@ -2578,7 +2750,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
dependencies = [
"anyhow",
"bitflags",
"bitflags 2.11.0",
"indexmap",
"log",
"serde",
@@ -2614,6 +2786,17 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
[[package]]
name = "yaml-rust2"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2462ea039c445496d8793d052e13787f2b90e750b833afee748e601c17621ed9"
dependencies = [
"arraydeque",
"encoding_rs",
"hashlink",
]
[[package]]
name = "yoke"
version = "0.8.1"

View File

@@ -18,13 +18,20 @@ license-file = "LICENSE"
publish = false
[workspace.dependencies]
arc-swap = "= 1.9.0"
axum-server = { version = "= 0.8.0", features = ["tls-rustls-no-provider"] }
aws-lc-rs = { version = "= 1.16.2", features = ["fips"] }
clap = { version = "= 4.6.0", features = ["derive", "env"] }
colored = "= 3.1.1"
config-rs = { package = "config", version = "= 0.15.22", default-features = false, features = [
"yaml",
"async",
] }
dotenvy = "= 0.15.7"
eyre = "= 0.6.12"
glob = "= 0.3.3"
nix = { version = "= 0.31.2", features = ["signal"] }
notify = "= 8.2.0"
regex = "= 1.12.3"
reqwest = { version = "= 0.13.2", features = [
"form",
@@ -48,6 +55,7 @@ serde_repr = "= 0.1.20"
serde_with = { version = "= 3.18.0", default-features = false, features = [
"base64",
] }
tempfile = "= 3.27.0"
tokio = { version = "= 1.50.0", features = ["full", "tracing"] }
tokio-util = { version = "= 0.7.18", features = ["full"] }
tracing = "= 0.1.44"

View File

@@ -47,6 +47,7 @@ listen:
- "[::]:9300"
debug: 0.0.0.0:9900
debug_py: 0.0.0.0:9901
debug_tokio: "[::]:6669"
trusted_proxy_cidrs:
- 127.0.0.0/8
- 10.0.0.0/8
@@ -73,6 +74,19 @@ log_level: info
log:
http_headers:
- User-Agent
rust_log:
"console_subscriber": info
"h2": info
"hyper_util": warn
"mio": info
"notify": info
"reqwest": info
"runtime": info
"rustls": info
"sqlx": info
"sqlx_postgres": info
"tokio": info
"tungstenite": info
sessions:
unauthenticated_age: days=1

View File

@@ -10,14 +10,22 @@ license-file.workspace = true
publish.workspace = true
[dependencies]
arc-swap.workspace = true
axum-server.workspace = true
config-rs.workspace = true
eyre.workspace = true
tokio.workspace = true
glob.workspace = true
notify.workspace = true
serde.workspace = true
serde_json.workspace = true
tokio-util.workspace = true
tokio.workspace = true
tracing.workspace = true
url.workspace = true
[dev-dependencies]
nix.workspace = true
tempfile.workspace = true
[lints]
workspace = true

View File

@@ -293,8 +293,8 @@ impl Arbiter {
pub enum Event {
/// A signal has been received.
Signal(SignalKind),
#[cfg(test)]
Noop,
/// The configuration has been reloaded from sources.
ConfigChanged,
}
impl From<SignalKind> for Event {
@@ -414,15 +414,15 @@ mod tests {
let mut events_rx1 = arbiter.events_subscribe();
let mut events_rx2 = arbiter.events_subscribe();
let _ = arbiter.send_event(Event::Noop);
let _ = arbiter.send_event(Event::ConfigChanged);
assert_eq!(
events_rx1.recv().await.expect("failed to receive event"),
Event::Noop,
Event::ConfigChanged,
);
assert_eq!(
events_rx2.recv().await.expect("failed to receive event"),
Event::Noop,
Event::ConfigChanged,
);
}
}

View File

@@ -0,0 +1,429 @@
use std::{
env,
fs::{self, read_to_string},
path::PathBuf,
sync::{Arc, OnceLock},
};
use arc_swap::ArcSwap;
use eyre::Result;
use notify::{RecommendedWatcher, Watcher as _};
use serde_json::{Map, Value};
use tokio::sync::mpsc;
use tracing::{error, info, warn};
use url::Url;
pub mod schema;
pub use schema::Config;
use crate::arbiter::{Arbiter, Event, Tasks};
static DEFAULT_CONFIG: &str = include_str!("../../../../authentik/lib/default.yml");
static CONFIG_MANAGER: OnceLock<ConfigManager> = OnceLock::new();
/// List of paths from where to read YAML configuration.
fn config_paths() -> Vec<PathBuf> {
let mut config_paths = vec![
PathBuf::from("/etc/authentik/config.yml"),
PathBuf::from(""),
];
if let Ok(workspace) = env::var("WORKSPACE_DIR") {
let _ = env::set_current_dir(workspace);
}
if let Ok(paths) = glob::glob("/etc/authentik/config.d/*.yml") {
config_paths.extend(paths.filter_map(Result::ok));
}
let environment = env::var("AUTHENTIK_ENV").unwrap_or_else(|_| "local".to_owned());
let mut computed_paths = Vec::new();
for path in config_paths {
if let Ok(metadata) = fs::metadata(&path) {
if !metadata.is_dir() {
computed_paths.push(path);
}
} else {
let env_paths = vec![
path.join(format!("{environment}.yml")),
path.join(format!("{environment}.env.yml")),
];
for env_path in env_paths {
if let Ok(metadata) = fs::metadata(&env_path)
&& !metadata.is_dir()
{
computed_paths.push(env_path);
}
}
}
}
computed_paths
}
impl Config {
/// Load the configuration from files and environment into a [`Value`], allowing for extra
/// processing later.
fn load_raw(config_paths: &[PathBuf]) -> Result<Value> {
let mut builder = config_rs::Config::builder().add_source(config_rs::File::from_str(
DEFAULT_CONFIG,
config_rs::FileFormat::Yaml,
));
for path in config_paths {
builder = builder.add_source(
config_rs::File::from(path.as_path()).format(config_rs::FileFormat::Yaml),
);
}
builder = builder.add_source(
config_rs::Environment::with_prefix("AUTHENTIK")
.prefix_separator("_")
.separator("__"),
);
let config = builder.build()?;
let raw = config.try_deserialize::<Value>()?;
Ok(raw)
}
/// Expand a value if it matches an env:// or file:// protocol.
///
/// If expanded from a file, returns the file path for it to be watched later.
fn expand_value(value: &str) -> (String, Option<PathBuf>) {
let value = value.trim();
if let Ok(uri) = Url::parse(value) {
let fallback = uri.query().unwrap_or("").to_owned();
match uri.scheme() {
"file" => {
let path = uri.path();
match read_to_string(path).map(|s| s.trim().to_owned()) {
Ok(value) => return (value, Some(PathBuf::from(path))),
Err(err) => {
error!("failed to read config value from {path}: {err}");
return (fallback, Some(PathBuf::from(path)));
}
}
}
"env" => {
if let Some(var) = uri.host_str() {
if let Ok(value) = env::var(var) {
return (value, None);
}
return (fallback, None);
}
}
_ => {}
}
}
(value.to_owned(), None)
}
/// Expand the configuration for env:// and file:// values.
///
/// Returns the expanded configuration and a list of file paths for which to watch changes.
fn expand(mut raw: Value) -> (Value, Vec<PathBuf>) {
let mut file_paths = Vec::new();
let value = match &mut raw {
Value::String(s) => {
let (v, path) = Self::expand_value(s);
if let Some(path) = path {
file_paths.push(path);
}
Value::String(v)
}
Value::Array(arr) => {
let mut res = Vec::with_capacity(arr.len());
for v in arr {
let (expanded, paths) = Self::expand(v.clone());
file_paths.extend(paths);
res.push(expanded);
}
Value::Array(res)
}
Value::Object(map) => {
let mut res = Map::with_capacity(map.len());
for (k, v) in map {
let (expanded, paths) = Self::expand(v.clone());
file_paths.extend(paths);
res.insert(k.clone(), expanded);
}
Value::Object(res)
}
_ => raw,
};
(value, file_paths)
}
/// Load the configuration.
fn load(config_paths: &[PathBuf]) -> Result<(Self, Vec<PathBuf>)> {
let raw = Self::load_raw(config_paths)?;
let (expanded, file_paths) = Self::expand(raw);
let config: Self = serde_json::from_value(expanded)?;
Ok((config, file_paths))
}
}
/// Manager of the config. Handles reloading when changed on disk.
struct ConfigManager {
config: ArcSwap<Config>,
config_paths: Vec<PathBuf>,
watch_paths: Vec<PathBuf>,
}
/// Initialize the configuration. It relies on a global [`OnceLock`] and must be called before
/// other methods are called.
pub fn init() -> Result<()> {
info!("loading config");
let config_paths = config_paths();
init_with_paths(config_paths)?;
info!("config loaded");
Ok(())
}
/// Initialize the configuration from a list of specific paths to read if from.
fn init_with_paths(config_paths: Vec<PathBuf>) -> Result<()> {
let (config, mut other_paths) = Config::load(&config_paths)?;
let mut watch_paths = config_paths.clone();
watch_paths.append(&mut other_paths);
let manager = ConfigManager {
config: ArcSwap::from_pointee(config),
config_paths,
watch_paths,
};
CONFIG_MANAGER.get_or_init(|| manager);
Ok(())
}
/// Watch for configuration changes, reload the configuration in memory and send events.
///
/// [`init`] must be called before this is used.
async fn watch_config(arbiter: Arbiter) -> Result<()> {
let (tx, mut rx) = mpsc::channel(100);
let mut watcher = RecommendedWatcher::new(
move |res: notify::Result<notify::Event>| {
if let Ok(event) = res
&& let notify::EventKind::Modify(_) = &event.kind
{
let _ = tx.blocking_send(());
}
},
notify::Config::default(),
)?;
let watch_paths = &CONFIG_MANAGER
.get()
.expect("failed to get config, has it been initialized?")
.watch_paths;
for path in watch_paths {
watcher.watch(path.as_ref(), notify::RecursiveMode::NonRecursive)?;
}
let _ = arbiter.send_event(Event::ConfigChanged);
info!("config file watcher started on paths: {:?}", watch_paths);
loop {
tokio::select! {
res = rx.recv() => {
info!("a configuration file changed, reloading config");
if res.is_none() {
break;
}
let manager = CONFIG_MANAGER.get().expect("failed to get config, has it been initialized?");
match tokio::task::spawn_blocking(|| Config::load(&manager.config_paths)).await? {
Ok((new_config, _)) => {
info!("configuration reloaded");
manager.config.store(Arc::new(new_config));
if let Err(err) = arbiter.send_event(Event::ConfigChanged) {
warn!("failed to notify of config change, aborting: {err:?}");
break;
}
}
Err(err) => {
warn!("failed to reload config, continuing with previous config: {err:?}");
}
}
},
() = arbiter.shutdown() => break,
}
}
info!("stopping config file watcher");
Ok(())
}
/// Start the configuration watcher.
///
/// [`init`] must be called before this is used.
pub fn run(tasks: &mut Tasks) -> Result<()> {
info!("starting config file watcher");
let arbiter = tasks.arbiter();
tasks
.build_task()
.name(&format!("{}::watch_config", module_path!()))
.spawn(watch_config(arbiter))?;
Ok(())
}
/// Get the currently stored configuration.
///
/// [`init`] must be called before this is used.
pub fn get() -> arc_swap::Guard<Arc<Config>> {
let manager = CONFIG_MANAGER
.get()
.expect("failed to get config, has it been initialized?");
manager.config.load()
}
#[cfg(test)]
mod tests {
use std::{env, fs::File, io::Write as _, path::PathBuf};
use tempfile::tempdir;
use crate::arbiter::{Event, Tasks};
#[test]
fn default_config() {
let (config, _) = super::Config::load(&[]).expect("default config doesn't load");
assert_eq!(config.secret_key, "");
}
#[test]
fn config_paths() {
let temp_dir = tempdir().expect("failed to create temp dir");
for f in &[
"local.env.yml",
"local.env.yaml",
"test_config_paths.yml",
"test_config_paths.env.yml",
"test_config_paths.env.yaml",
] {
File::create(temp_dir.path().join(f)).expect("failed to create file");
}
#[expect(unsafe_code, reason = "testing")]
// SAFETY: testing
unsafe {
env::set_var("WORKSPACE_DIR", temp_dir.path());
env::set_var("AUTHENTIK_ENV", "test_config_paths");
}
let paths = super::config_paths();
assert_eq!(
&paths,
&[
PathBuf::from("test_config_paths.yml"),
PathBuf::from("test_config_paths.env.yml"),
]
);
}
#[test]
fn expand() {
let temp_dir = tempdir().expect("failed to create temp dir");
let secret_key_path = temp_dir.path().join("secret_key");
let mut secret_key_file = File::create(&secret_key_path).expect("failed to create file");
write!(secret_key_file, "my_secret_key").expect("failed to write to file");
let config_file_path = temp_dir.path().join("config");
let mut config_file = File::create(&config_file_path).expect("failed to create file");
writeln!(
config_file,
"secret_key: file://{}\npostgresql:\n password: env://TEST_CONFIG_POSTGRES_PASS",
secret_key_path.display()
)
.expect("failed to write to file");
#[expect(unsafe_code, reason = "testing")]
// SAFETY: testing
unsafe {
env::set_var("TEST_CONFIG_POSTGRES_PASS", "my_postgres_pass");
}
let (config, config_paths) =
super::Config::load(&[config_file_path]).expect("failed to load config");
assert_eq!(config.secret_key, "my_secret_key");
assert_eq!(config.postgresql.password, "my_postgres_pass");
assert_eq!(config_paths, &[secret_key_path]);
}
#[test]
fn init() {
super::init_with_paths(vec![]).expect("failed to init config");
}
#[tokio::test]
async fn watcher() {
let temp_dir = tempdir().expect("failed to create temp dir");
let secret_key_path = temp_dir.path().join("secret_key");
let mut secret_key_file = File::create(&secret_key_path).expect("failed to create file");
write!(secret_key_file, "my_secret_key").expect("failed to write to file");
drop(secret_key_file);
let config_file_path = temp_dir.path().join("config");
let mut config_file = File::create(&config_file_path).expect("failed to create file");
writeln!(
config_file,
"secret_key: file://{}\npostgresql:\n password: my_postgres_pass",
secret_key_path.display()
)
.expect("failed to write to file");
drop(config_file);
super::init_with_paths(vec![config_file_path.clone()]).expect("failed to init config");
let mut tasks = Tasks::new().expect("failed to create tasks");
let arbiter = tasks.arbiter();
let mut events_rx = arbiter.events_subscribe();
super::run(&mut tasks).expect("failed to start watcher");
assert_eq!(super::get().secret_key, "my_secret_key");
assert_eq!(super::get().postgresql.password, "my_postgres_pass");
let _ = events_rx.recv().await;
let mut secret_key_file = File::create(&secret_key_path).expect("failed to open file");
write!(secret_key_file, "my_other_secret_key").expect("failed to write to file");
drop(secret_key_file);
assert_eq!(
events_rx.recv().await.expect("failed to receive event"),
Event::ConfigChanged,
);
while !events_rx.is_empty() {
assert_eq!(
events_rx.recv().await.expect("failed to receive event"),
Event::ConfigChanged,
);
}
assert_eq!(super::get().secret_key, "my_other_secret_key");
assert_eq!(super::get().postgresql.password, "my_postgres_pass");
let mut config_file = File::create(&config_file_path).expect("failed to open file");
writeln!(
config_file,
"secret_key: file://{}\npostgresql:\n password: my_new_postgres_pass",
secret_key_path.display()
)
.expect("failed to write to file");
drop(config_file);
assert_eq!(
events_rx.recv().await.expect("failed to receive event"),
Event::ConfigChanged,
);
while !events_rx.is_empty() {
assert_eq!(
events_rx.recv().await.expect("failed to receive event"),
Event::ConfigChanged,
);
}
assert_eq!(super::get().secret_key, "my_other_secret_key");
assert_eq!(super::get().postgresql.password, "my_new_postgres_pass");
}
}

View File

@@ -0,0 +1,90 @@
use std::{collections::HashMap, net::SocketAddr, num::NonZeroUsize};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
pub postgresql: PostgreSQLConfig,
pub listen: ListenConfig,
pub debug: bool,
#[serde(default)]
pub secret_key: String,
pub log_level: String,
pub log: LogConfig,
pub error_reporting: ErrorReportingConfig,
pub compliance: ComplianceConfig,
pub web: WebConfig,
pub worker: WorkerConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PostgreSQLConfig {
pub host: String,
pub port: u16,
pub user: String,
pub password: String,
pub name: String,
pub sslmode: String,
pub sslrootcert: Option<String>,
pub sslcert: Option<String>,
pub sslkey: Option<String>,
pub conn_max_age: Option<u64>,
pub conn_health_checks: bool,
pub default_schema: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListenConfig {
pub http: Vec<SocketAddr>,
pub metrics: Vec<SocketAddr>,
pub debug_tokio: SocketAddr,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogConfig {
pub http_headers: Vec<String>,
pub rust_log: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorReportingConfig {
pub enabled: bool,
pub sentry_dsn: Option<String>,
pub environment: String,
pub send_pii: bool,
pub sample_rate: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComplianceConfig {
pub fips: ComplianceFipsConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComplianceFipsConfig {
pub enabled: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebConfig {
pub path: String,
pub timeout_http_read_header: String,
pub timeout_http_read: String,
pub timeout_http_write: String,
pub timeout_http_idle: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkerConfig {
pub processes: NonZeroUsize,
}

View File

@@ -2,6 +2,7 @@
pub mod arbiter;
pub use arbiter::{Arbiter, Event, Tasks};
pub mod config;
pub const VERSION: &str = env!("CARGO_PKG_VERSION");