#![warn(clippy::pedantic)]
#![warn(missing_docs)]
#![allow(clippy::module_name_repetitions)]
use serde::{Deserialize, Serialize};
use shikumi::TieredConfig;
mod cluster;
mod consistency;
mod controllers;
mod error;
mod revoada;
mod scheduler;
mod teia;
pub use cluster::ClusterConfig;
pub use consistency::{ConsistencyConfig, ConsistencyTierKind};
pub use controllers::{ControllersConfig, ControllerEnable};
pub use error::ConfigError;
pub use revoada::{RevoadaConfig, TopologyConfig, TopologyStrategyKind};
pub use scheduler::{SchedulerConfig, SchedulerStrategyKind};
pub use teia::TeiaConfig;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct EngenhoConfig {
pub cluster: ClusterConfig,
pub revoada: RevoadaConfig,
pub teia: TeiaConfig,
pub scheduler: SchedulerConfig,
pub controllers: ControllersConfig,
pub consistency: ConsistencyConfig,
}
impl TieredConfig for EngenhoConfig {
fn bare() -> Self {
Self {
cluster: ClusterConfig::bare(),
revoada: RevoadaConfig::bare(),
teia: TeiaConfig::bare(),
scheduler: SchedulerConfig::bare(),
controllers: ControllersConfig::bare(),
consistency: ConsistencyConfig::bare(),
}
}
fn prescribed_default() -> Self {
Self {
cluster: ClusterConfig::prescribed_default(),
revoada: RevoadaConfig::prescribed_default(),
teia: TeiaConfig::prescribed_default(),
scheduler: SchedulerConfig::prescribed_default(),
controllers: ControllersConfig::prescribed_default(),
consistency: ConsistencyConfig::prescribed_default(),
}
}
fn extend(self, base: &Self) -> Self {
Self {
cluster: self.cluster.extend(&base.cluster),
revoada: self.revoada.extend(&base.revoada),
teia: self.teia.extend(&base.teia),
scheduler: self.scheduler.extend(&base.scheduler),
controllers: self.controllers.extend(&base.controllers),
consistency: self.consistency.extend(&base.consistency),
}
}
}
impl Default for EngenhoConfig {
fn default() -> Self {
Self::prescribed_default()
}
}
impl EngenhoConfig {
pub fn validate(&self) -> Result<(), ConfigError> {
self.cluster.validate()?;
self.revoada.validate()?;
self.teia.validate()?;
self.scheduler.validate()?;
self.controllers.validate()?;
self.consistency.validate()?;
if matches!(
self.revoada.topology.strategy,
TopologyStrategyKind::Quorum3M | TopologyStrategyKind::Cluster3MNW
) && self.revoada.topology.min_nodes < 3
{
return Err(ConfigError::Incoherent(format!(
"topology {:?} requires min_nodes >= 3 but config has {}",
self.revoada.topology.strategy, self.revoada.topology.min_nodes
)));
}
Ok(())
}
pub fn from_yaml_with_defaults(yaml: &str) -> Result<Self, ConfigError> {
let default_v: serde_yaml::Value =
serde_yaml::to_value(Self::prescribed_default()).map_err(|e| {
ConfigError::Parse(format!("serialize default: {e}"))
})?;
let overlay: serde_yaml::Value = serde_yaml::from_str(yaml)
.map_err(|e| ConfigError::Parse(format!("operator YAML: {e}")))?;
let merged = merge_yaml(default_v, overlay);
let cfg: Self = serde_yaml::from_value(merged)
.map_err(|e| ConfigError::Parse(format!("merge round-trip: {e}")))?;
cfg.validate()?;
Ok(cfg)
}
}
fn merge_yaml(base: serde_yaml::Value, overlay: serde_yaml::Value) -> serde_yaml::Value {
match (base, overlay) {
(base, serde_yaml::Value::Null) => base,
(serde_yaml::Value::Mapping(mut b), serde_yaml::Value::Mapping(o)) => {
for (k, v) in o {
let entry = b.remove(&k).unwrap_or(serde_yaml::Value::Null);
b.insert(k, merge_yaml(entry, v));
}
serde_yaml::Value::Mapping(b)
}
(_, overlay) => overlay,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn prescribed_default_is_valid() {
EngenhoConfig::prescribed_default().validate().unwrap();
}
#[test]
fn default_is_prescribed_default() {
assert_eq!(EngenhoConfig::default(), EngenhoConfig::prescribed_default());
}
#[test]
fn bare_differs_from_prescribed_default() {
assert_ne!(EngenhoConfig::bare(), EngenhoConfig::prescribed_default());
}
#[test]
fn empty_overlay_yields_defaults() {
let cfg = EngenhoConfig::from_yaml_with_defaults("").unwrap();
assert_eq!(cfg, EngenhoConfig::prescribed_default());
}
#[test]
fn partial_overlay_changes_only_specified_fields() {
let yaml = "\
cluster:
name: rio
";
let cfg = EngenhoConfig::from_yaml_with_defaults(yaml).unwrap();
assert_eq!(cfg.cluster.name, "rio");
let default = EngenhoConfig::prescribed_default();
assert_eq!(cfg.scheduler, default.scheduler);
assert_eq!(cfg.controllers, default.controllers);
}
#[test]
fn validate_rejects_incoherent_quorum_topology() {
let mut cfg = EngenhoConfig::prescribed_default();
cfg.revoada.topology.strategy = TopologyStrategyKind::Quorum3M;
cfg.revoada.topology.min_nodes = 1;
let err = cfg.validate().unwrap_err();
assert!(matches!(err, ConfigError::Incoherent(_)));
}
#[test]
fn round_trip_through_yaml() {
let original = EngenhoConfig::prescribed_default();
let yaml = serde_yaml::to_string(&original).unwrap();
let parsed: EngenhoConfig = serde_yaml::from_str(&yaml).unwrap();
assert_eq!(parsed, original);
}
#[test]
fn extend_overlays_self_on_base() {
let mut overlay = EngenhoConfig::bare();
overlay.cluster.name = "override".into();
let base = EngenhoConfig::prescribed_default();
let merged = overlay.extend(&base);
assert_eq!(merged.cluster.name, "override");
}
}