diff --git a/Cargo.lock b/Cargo.lock index 52ea07c9..c73ef931 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -708,6 +708,7 @@ dependencies = [ "tikv-jemallocator", "tokio", "tokio-metrics", + "toml", "tracing", "tracing-core", "tracing-subscriber", diff --git a/Cargo.toml b/Cargo.toml index b65ba7ad..66ba7385 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,11 @@ version = "0.2.8" version = "0.20" features = ["features"] +[workspace.dependencies.toml] +version = "0.8.14" +default-features = false +features = ["parse"] + [workspace.dependencies.sanitize-filename] version = "0.5.0" diff --git a/src/core/Cargo.toml b/src/core/Cargo.toml index ea772a83..f915c43e 100644 --- a/src/core/Cargo.toml +++ b/src/core/Cargo.toml @@ -86,6 +86,7 @@ tikv-jemalloc-sys.optional = true tikv-jemalloc-sys.workspace = true tokio.workspace = true tokio-metrics.workspace = true +toml.workspace = true tracing-core.workspace = true tracing-subscriber.workspace = true tracing.workspace = true diff --git a/src/core/config/mod.rs b/src/core/config/mod.rs index ae273545..a9443fd2 100644 --- a/src/core/config/mod.rs +++ b/src/core/config/mod.rs @@ -9,10 +9,8 @@ use either::{ Either, Either::{Left, Right}, }; -use figment::{ - providers::{Env, Format, Toml}, - Figment, -}; +use figment::providers::{Env, Format, Toml}; +pub use figment::{value::Value as FigmentValue, Figment}; use itertools::Itertools; use regex::RegexSet; use ruma::{ @@ -408,8 +406,8 @@ const DEPRECATED_KEYS: &[&str; 9] = &[ ]; impl Config { - /// Initialize config - pub fn new(path: &Option) -> Result { + /// Pre-initialize config + pub fn load(path: &Option) -> Result { let raw_config = if let Some(config_file_env) = Env::var("CONDUIT_CONFIG") { Figment::new() .merge(Toml::file(config_file_env).nested()) @@ -431,13 +429,18 @@ impl Config { .merge(Env::prefixed("CONDUWUIT_").global().split("__")) }; + Ok(raw_config) + } + + /// Finalize config + pub fn new(raw_config: &Figment) -> Result { let config = match raw_config.extract::() { Err(e) => return Err!("There was a problem with your configuration file: {e}"), Ok(config) => config, }; // don't start if we're listening on both UNIX sockets and TCP at same time - check::is_dual_listening(&raw_config)?; + check::is_dual_listening(raw_config)?; Ok(config) } diff --git a/src/core/error/mod.rs b/src/core/error/mod.rs index 8664d740..5406d5ff 100644 --- a/src/core/error/mod.rs +++ b/src/core/error/mod.rs @@ -57,6 +57,12 @@ pub enum Error { HttpHeader(#[from] http::header::InvalidHeaderValue), #[error("{0}")] CargoToml(#[from] cargo_toml::Error), + #[error("{0}")] + FigmentError(#[from] figment::error::Error), + #[error("{0}")] + TomlSerError(#[from] toml::ser::Error), + #[error("{0}")] + TomlDeError(#[from] toml::de::Error), // ruma #[error("{0}")] diff --git a/src/core/mod.rs b/src/core/mod.rs index b302fdcc..5ed5ea15 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -10,6 +10,7 @@ pub mod pdu; pub mod server; pub mod utils; +pub use ::toml; pub use config::Config; pub use error::Error; pub use info::{rustc_flags_capture, version, version::version}; diff --git a/src/main/clap.rs b/src/main/clap.rs index 16d88978..92bd73c1 100644 --- a/src/main/clap.rs +++ b/src/main/clap.rs @@ -3,16 +3,24 @@ use std::path::PathBuf; use clap::Parser; -use conduit::{Config, Result}; +use conduit::{ + config::{Figment, FigmentValue}, + err, toml, Err, Result, +}; /// Commandline arguments #[derive(Parser, Debug)] #[clap(version = conduit::version(), about, long_about = None)] pub(crate) struct Args { #[arg(short, long)] - /// Optional argument to the path of a conduwuit config TOML file + /// Path to the config TOML file (optional) pub(crate) config: Option, + /// Override a configuration variable using TOML 'key=value' syntax + #[arg(long, short('O'))] + pub(crate) option: Vec, + + #[cfg(feature = "console")] /// Activate admin command console automatically after startup. #[arg(long, num_args(0))] pub(crate) console: bool, @@ -23,10 +31,38 @@ pub(crate) struct Args { pub(super) fn parse() -> Args { Args::parse() } /// Synthesize any command line options with configuration file options. -pub(crate) fn update(config: &mut Config, args: &Args) -> Result<()> { +pub(crate) fn update(mut config: Figment, args: &Args) -> Result { + #[cfg(feature = "console")] // Indicate the admin console should be spawned automatically if the // configuration file hasn't already. - config.admin_console_automatic |= args.console; + if args.console { + config = config.join(("admin_console_automatic", true)); + } - Ok(()) + // All other individual overrides can go last in case we have options which + // set multiple conf items at once and the user still needs granular overrides. + for option in &args.option { + let (key, val) = option + .split_once('=') + .ok_or_else(|| err!("Missing '=' in -O/--option: {option:?}"))?; + + if key.is_empty() { + return Err!("Missing key= in -O/--option: {option:?}"); + } + + if val.is_empty() { + return Err!("Missing =val in -O/--option: {option:?}"); + } + + // The value has to pass for what would appear as a line in the TOML file. + let val = toml::from_str::(option)?; + let FigmentValue::Dict(_, val) = val else { + panic!("Unexpected Figment Value: {val:#?}"); + }; + + // Figment::merge() overrides existing + config = config.merge((key, val[key].clone())); + } + + Ok(config) } diff --git a/src/main/server.rs b/src/main/server.rs index b1cd6936..71cdadce 100644 --- a/src/main/server.rs +++ b/src/main/server.rs @@ -22,8 +22,9 @@ pub(crate) struct Server { impl Server { pub(crate) fn build(args: &Args, runtime: Option<&runtime::Handle>) -> Result, Error> { - let mut config = Config::new(&args.config)?; - crate::clap::update(&mut config, args)?; + let raw_config = Config::load(&args.config)?; + let raw_config = crate::clap::update(raw_config, args)?; + let config = Config::new(&raw_config)?; #[cfg(feature = "sentry_telemetry")] let sentry_guard = crate::sentry::init(&config);