diff --git a/src/backends/qemu/mod.rs b/src/backends/qemu/mod.rs index a840953..34e98a1 100644 --- a/src/backends/qemu/mod.rs +++ b/src/backends/qemu/mod.rs @@ -31,7 +31,7 @@ use serde::{Deserialize, Serialize}; use crate::{ backends::{Backend, BackendClientError, BackendRegistration}, - config::{ConfigError, load_json_with_includes}, + config::{self, ConfigError}, instance::{Instance, InstanceClient}, util::Path, }; @@ -83,11 +83,7 @@ impl QemuConfig { /// built-in defaults. pub fn from_dir(dir: &Path) -> Result { let path = Path::new(&dir.join("qemu.json")); - if path.exists() { - load_json_with_includes(path) - } else { - Ok(Self::default()) - } + config::load_or_default(&path) } } diff --git a/src/config.rs b/src/config.rs index fc7808e..0e4cf37 100644 --- a/src/config.rs +++ b/src/config.rs @@ -267,25 +267,36 @@ struct ConfigEnvelope { rest: serde_json::Value, } -/// Load any JSON document that opts into the `include` -/// mechanism. Engines call this on their own config file so -/// site-specific overrides can live in a sibling file. +/// Attempt to load a JSON config file. If not found, fall back on the Default +/// implementation. +pub fn load_or_default Deserialize<'de> + Default>( + path: impl AsRef, +) -> Result { + match load_json_with_includes(path) { + r @ Ok(_) => r, + Err(ConfigError::Io(e)) if e.kind() == std::io::ErrorKind::NotFound => { + Ok(Default::default()) + } + e @ Err(_) => e, + } +} + +/// Load any JSON document that opts into the `include` mechanism. Engines +/// call this on their own config file so site-specific overrides can live in a +/// sibling file. pub fn load_json_with_includes Deserialize<'de>>( - path: impl AsRef, + path: impl AsRef, ) -> Result { let path = path.as_ref(); let value = load_config_value(path)?; - let out: T = serde_json::from_value(value)?; - Ok(out) + Ok(serde_json::from_value(value)?) } -fn load_config_value(path: &Path) -> Result { +fn load_config_value(path: &std::path::Path) -> Result { let data = std::fs::read_to_string(path)?; let env: ConfigEnvelope = serde_json::from_str(&data)?; let mut merged = if let Some(rel) = env.include { - let path = Path::new("."); - let inc_path = Path::new(&path.parent().unwrap_or_else(|| &path).join(&rel)); - load_config_value(&inc_path)? + load_config_value(&path.parent().unwrap_or(path).join(&rel))? } else { serde_json::Value::Object(Default::default()) }; @@ -479,7 +490,7 @@ mod tests { ) .unwrap(); - let cfg: Config = load_json_with_includes(Path::new(path.to_str().unwrap())).unwrap(); + let cfg: Config = load_or_default(Path::new(path.to_str().unwrap())).unwrap(); assert_eq!(cfg.engine, "threshold"); assert_eq!(cfg.scale_poll_secs, 7); assert_eq!( @@ -506,7 +517,7 @@ mod tests { ) .unwrap(); - let err = load_json_with_includes::(Path::new(path.to_str().unwrap())).unwrap_err(); + let err = load_or_default::(Path::new(path.to_str().unwrap())).unwrap_err(); assert!(matches!(err, ConfigError::SerdeJson(_))); } diff --git a/src/engines/threshold.rs b/src/engines/threshold.rs index dd9727a..6458335 100644 --- a/src/engines/threshold.rs +++ b/src/engines/threshold.rs @@ -16,7 +16,7 @@ use serde::{Deserialize, Serialize}; use tokio::sync::Mutex; use crate::{ - config::{ConfigError, PollConverter, deserialize_percent, serialize_percent}, + config::{self, ConfigError, PollConverter, deserialize_percent, serialize_percent}, engines::{ AppliedOutcome, EngineError, EngineRegistration, EngineTickContext, ScaleAction, ScalingEngine, @@ -477,12 +477,8 @@ impl ThresholdEngine { /// Load `threshold.json`, falling back to built-in defaults when absent. pub fn from_config_dir(dir: &Path, durations: &mut PollConverter) -> Result { let path = Path::new(&dir.join(format!("{ENGINE_NAME}.json"))); - // TODO TOCTOU, blindly load and return default if ENOENT - let cfg: ThresholdConfig = if path.exists() { - crate::config::load_json_with_includes(path)? - } else { - ThresholdConfig::default() - }; + let cfg: ThresholdConfig = config::load_or_default(&path)?; + cfg.validate()?; Ok(Self::new(cfg, durations)?) } diff --git a/src/main.rs b/src/main.rs index 05b42bd..0d55b6d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,20 +5,19 @@ //! `io-thread-controller` daemon entry point. -use std::{io::ErrorKind, process::ExitCode}; +use std::process::ExitCode; use clap::{self, CommandFactory, FromArgMatches, Parser}; use io_thread_controller::{ - backends::BackendClientError, - backends::registered_backends, - config::{Config, ConfigError, dump_default_config, load_json_with_includes, validate_config}, + backends::{BackendClientError, registered_backends}, + config::{Config, ConfigError, dump_default_config, load_or_default, validate_config}, daemon::{DaemonError, VERSION, run}, engines::{EngineError, load_registered_engine}, per_vm_log::{self, PER_VM_LOG_DIR, PerVmLog}, util::Path, }; use thiserror::Error; -use tracing::{debug, info, instrument}; +use tracing::{debug, info}; use tracing_subscriber::{Layer, filter::LevelFilter, fmt, layer::SubscriberExt}; #[derive(Error, Debug)] @@ -150,7 +149,7 @@ async fn run_daemon() -> Result<(), IoThreadControllerError> { "io-thread-controller starting" ); - let mut cfg = load_daemon_config(&cli.config)?; + let mut cfg: Config = load_or_default(&cli.config)?; debug!("loaded configuration {}", &cli.config); if cli.print_status_header { cfg.print_status_header = true; @@ -173,16 +172,6 @@ async fn run_daemon() -> Result<(), IoThreadControllerError> { Ok(()) } -/// Load defaults only for an absent file; propagate every other open failure. -#[instrument] -fn load_daemon_config(path: &Path) -> Result { - match std::fs::File::open(path) { - Ok(_) => load_json_with_includes(path), - Err(error) if error.kind() == ErrorKind::NotFound => Ok(Config::default()), - Err(error) => Err(error)?, - } -} - /// Install the process-wide tracing subscriber. fn init_logging(filter: &str, style: LogStyle, per_vm_log: PerVmLog) { // Auto-detection: systemd sets `JOURNAL_STREAM=:`