Skip to main content

vane_core/config/
mod.rs

1//! Config loading entry point.
2//!
3//! See [`spec/crates/core.md` § _Config layers_](../../../../spec/crates/core.md#config-layers).
4//! Module responsibilities:
5//!
6//! 1. Best-effort `dotenvy` load of `<config_dir>/.env`. **OS env wins**
7//!    — `dotenvy::from_path` does not override pre-existing keys, which
8//!    matches operator expectations (systemd / supervisor unit files
9//!    are authoritative).
10//! 2. Scan `<config_dir>/rules/*.json` for [`RawRuleFile`]s.
11//! 3. Read every `VANE_*` deployment constant into a typed [`Env`]
12//!    snapshot.
13//!
14// TODO(config-json): parse `<config_dir>/config.json` (global daemon
15// settings — listeners, management, wasm pool config). Today this file
16// is silently ignored; the daemon's startup wires those values directly
17// from the env layer instead.
18
19mod env;
20mod loader;
21
22pub use env::{Env, EnvReader, ProcessEnv};
23pub use loader::scan_rules_dir;
24
25use std::path::Path;
26
27use crate::compile::merge::RawRuleFile;
28use crate::error::Error;
29
30/// Result of [`load`]: rule files (unmerged) plus the typed `Env`
31/// snapshot. Downstream callers thread `files` into
32/// [`crate::compile::compile`] and read `env` for deployment constants.
33#[derive(Debug, Clone)]
34pub struct LoadedConfig {
35	pub files: Vec<RawRuleFile>,
36	pub env: Env,
37}
38
39/// Load a vane config directory.
40///
41/// Order of operations:
42///
43/// 1. If `<config_dir>/.env` exists, run `dotenvy::from_path` on it.
44///    **Pre-existing OS env keys win** — operators who set values via
45///    systemd / `EnvironmentFile=` / docker `-e` flag override what's
46///    in `.env`. A missing `.env` is not an error; many deployments
47///    rely entirely on OS-level env.
48/// 2. Scan `<config_dir>/rules/*.json` via [`scan_rules_dir`].
49/// 3. Read `VANE_*` deployment constants into [`Env`].
50///
51/// # Errors
52/// - `<config_dir>/rules/` does not exist or is not a directory
53///   (propagated from [`scan_rules_dir`]).
54/// - Any `.json` under `rules/` fails to parse as `RawRuleFile`.
55/// - Any `VANE_*` env var has an invalid value (non-integer, not
56///   `"0"`/`"1"` for booleans, malformed `SocketAddr`, etc.).
57///
58/// **Not** an error:
59/// - `.env` file is missing.
60/// - `<config_dir>/config.json` is missing or malformed (it is not
61///   parsed at this stage).
62pub fn load(config_dir: &Path) -> Result<LoadedConfig, Error> {
63	let env_path = config_dir.join(".env");
64	if env_path.is_file() {
65		// `.env` is an optional operator override — a missing file is
66		// normal and must not produce noise. The `.is_file()` guard
67		// handles the common case; the `NotFound` arm below covers the
68		// race where the file disappears between the guard and the open.
69		// Other failures (malformed syntax, permission denied) are real
70		// problems the operator should see.
71		match dotenvy::from_path(&env_path) {
72			Ok(()) => {}
73			Err(dotenvy::Error::Io(ref io_err)) if io_err.kind() == std::io::ErrorKind::NotFound => {}
74			Err(e) => {
75				tracing::warn!(
76					path = %env_path.display(),
77					error = %e,
78					".env parse failed; using OS env only",
79				);
80			}
81		}
82	}
83
84	let rules_dir = config_dir.join("rules");
85	let files = scan_rules_dir(&rules_dir)?;
86	let env = Env::from_process_env(config_dir)?;
87
88	Ok(LoadedConfig { files, env })
89}