Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions src/backends/qemu/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -83,11 +83,7 @@ impl QemuConfig {
/// built-in defaults.
pub fn from_dir(dir: &Path) -> Result<Self, ConfigError> {
let path = Path::new(&dir.join("qemu.json"));
if path.exists() {
load_json_with_includes(path)
} else {
Ok(Self::default())
}
config::load_config_or_default(&path)
}
}

Expand Down
35 changes: 23 additions & 12 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,25 +169,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_config_or_default<T: for<'de> Deserialize<'de> + Default>(
path: impl AsRef<std::path::Path>,
) -> Result<T, ConfigError> {
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<T: for<'de> Deserialize<'de>>(
path: impl AsRef<Path>,
path: impl AsRef<std::path::Path>,
) -> Result<T, ConfigError> {
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<serde_json::Value, ConfigError> {
fn load_config_value(path: &std::path::Path) -> Result<serde_json::Value, ConfigError> {
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())
};
Expand Down Expand Up @@ -297,7 +308,7 @@ mod tests {
)
.unwrap();

let cfg: Config = load_json_with_includes(Path::new(path.to_str().unwrap())).unwrap();
let cfg: Config = load_config_or_default(Path::new(path.to_str().unwrap())).unwrap();
assert_eq!(cfg.engine, "threshold");
assert!((cfg.scale_poll_secs - 7.5).abs() < f64::EPSILON);
assert_eq!(
Expand All @@ -324,7 +335,7 @@ mod tests {
)
.unwrap();

let err = load_json_with_includes::<Config>(Path::new(path.to_str().unwrap())).unwrap_err();
let err = load_config_or_default::<Config>(Path::new(path.to_str().unwrap())).unwrap_err();
assert!(matches!(err, ConfigError::SerdeJson(_)));
}

Expand Down
9 changes: 2 additions & 7 deletions src/engines/threshold.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize};
use tokio::sync::Mutex;

use crate::{
config::{ConfigError, deserialize_percent, serialize_percent},
config::{self, ConfigError, deserialize_percent, serialize_percent},
engines::{
AppliedOutcome, EngineError, EngineRegistration, EngineTickContext, ScaleAction,
ScalingEngine,
Expand Down Expand Up @@ -409,12 +409,7 @@ impl ThresholdEngine {
/// Load `threshold.json`, falling back to built-in defaults when absent.
pub fn from_config_dir(dir: &Path) -> Result<Self, EngineError> {
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_config_or_default(&path)?;
cfg.validate()?;
Ok(Self::new(cfg))
}
Expand Down
21 changes: 4 additions & 17 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,15 @@

//! `io-thread-controller` daemon entry point.

use std::io::ErrorKind;

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_config_or_default, validate_config},
daemon::{DaemonError, VERSION, run},
util::Path,
};
use thiserror::Error;
use tracing::{debug, info, instrument};
use tracing::{debug, info};
use tracing_subscriber::fmt::format::FmtSpan;

#[derive(Error, Debug)]
Expand Down Expand Up @@ -130,7 +127,7 @@ async fn main() -> Result<(), IoThreadControllerError> {
"io-thread-controller starting"
);

let mut cfg = load_daemon_config(&cli.config)?;
let mut cfg: Config = load_config_or_default(&cli.config)?;
debug!("loaded configuration {}", &cli.config);
if cli.print_status_header {
cfg.print_status_header = true;
Expand All @@ -144,16 +141,6 @@ async fn main() -> Result<(), IoThreadControllerError> {
Ok(())
}

/// Load defaults only for an absent file; propagate every other open failure.
#[instrument]
fn load_daemon_config(path: &Path) -> Result<Config, ConfigError> {
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) {
// Auto-detection: systemd sets `JOURNAL_STREAM=<dev>:<ino>`
Expand Down
Loading