2026-02-02 07:19:22 +01:00
|
|
|
use std::{fs, path::Path};
|
|
|
|
|
|
2026-02-24 03:46:54 +01:00
|
|
|
use anyhow::{Context as _, Result};
|
|
|
|
|
use serde_yaml::{Mapping, Value};
|
2026-03-09 08:48:05 +01:00
|
|
|
use thiserror::Error;
|
2026-02-02 07:19:22 +01:00
|
|
|
|
2026-03-09 08:48:05 +01:00
|
|
|
/// Rendered cfgsync outputs written for server startup.
|
2026-02-02 07:19:22 +01:00
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub struct RenderedCfgsync {
|
2026-03-09 08:48:05 +01:00
|
|
|
/// Serialized cfgsync server config YAML.
|
2026-02-02 07:19:22 +01:00
|
|
|
pub config_yaml: String,
|
2026-03-09 08:48:05 +01:00
|
|
|
/// Serialized node bundle YAML.
|
2026-02-02 07:19:22 +01:00
|
|
|
pub bundle_yaml: String,
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-09 08:48:05 +01:00
|
|
|
/// Output paths used when materializing rendered cfgsync files.
|
2026-02-02 07:19:22 +01:00
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
|
|
|
pub struct CfgsyncOutputPaths<'a> {
|
|
|
|
|
pub config_path: &'a Path,
|
|
|
|
|
pub bundle_path: &'a Path,
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-09 08:48:05 +01:00
|
|
|
/// Ensures bundle path override exists, defaulting to output bundle file name.
|
2026-02-02 07:19:22 +01:00
|
|
|
pub fn ensure_bundle_path(bundle_path: &mut Option<String>, output_bundle_path: &Path) {
|
|
|
|
|
if bundle_path.is_some() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
*bundle_path = Some(
|
|
|
|
|
output_bundle_path
|
|
|
|
|
.file_name()
|
|
|
|
|
.and_then(|name| name.to_str())
|
|
|
|
|
.unwrap_or("cfgsync.bundle.yaml")
|
|
|
|
|
.to_string(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-09 08:48:05 +01:00
|
|
|
/// Applies a minimum timeout floor to an existing timeout value.
|
2026-02-02 07:19:22 +01:00
|
|
|
pub fn apply_timeout_floor(timeout: &mut u64, min_timeout_secs: Option<u64>) {
|
|
|
|
|
if let Some(min_timeout_secs) = min_timeout_secs {
|
|
|
|
|
*timeout = (*timeout).max(min_timeout_secs);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-09 08:48:05 +01:00
|
|
|
/// Writes rendered cfgsync server and bundle YAML files.
|
2026-02-02 07:19:22 +01:00
|
|
|
pub fn write_rendered_cfgsync(
|
|
|
|
|
rendered: &RenderedCfgsync,
|
|
|
|
|
output: CfgsyncOutputPaths<'_>,
|
|
|
|
|
) -> Result<()> {
|
|
|
|
|
fs::write(output.config_path, &rendered.config_yaml)?;
|
|
|
|
|
fs::write(output.bundle_path, &rendered.bundle_yaml)?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
2026-02-24 03:46:54 +01:00
|
|
|
|
2026-03-09 08:48:05 +01:00
|
|
|
/// Optional overrides applied to a cfgsync template document.
|
2026-02-24 03:46:54 +01:00
|
|
|
#[derive(Debug, Clone, Default)]
|
|
|
|
|
pub struct CfgsyncConfigOverrides {
|
|
|
|
|
pub port: Option<u16>,
|
|
|
|
|
pub n_hosts: Option<usize>,
|
|
|
|
|
pub timeout_floor_secs: Option<u64>,
|
|
|
|
|
pub bundle_path: Option<String>,
|
|
|
|
|
pub metrics_otlp_ingest_url: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-09 08:48:05 +01:00
|
|
|
#[derive(Debug, Error)]
|
|
|
|
|
enum RenderTemplateError {
|
|
|
|
|
#[error("cfgsync template key `{key}` must be a YAML map")]
|
|
|
|
|
NonMappingEntry { key: String },
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Loads cfgsync template YAML from disk.
|
2026-02-24 03:46:54 +01:00
|
|
|
pub fn load_cfgsync_template_yaml(path: &Path) -> Result<Value> {
|
|
|
|
|
let file = fs::File::open(path)
|
|
|
|
|
.with_context(|| format!("opening cfgsync template at {}", path.display()))?;
|
|
|
|
|
serde_yaml::from_reader(file).context("parsing cfgsync template")
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-09 08:48:05 +01:00
|
|
|
/// Renders cfgsync config YAML by applying overrides to a template document.
|
2026-02-24 03:46:54 +01:00
|
|
|
pub fn render_cfgsync_yaml_from_template(
|
|
|
|
|
mut template: Value,
|
|
|
|
|
overrides: &CfgsyncConfigOverrides,
|
|
|
|
|
) -> Result<String> {
|
|
|
|
|
apply_cfgsync_overrides(&mut template, overrides)?;
|
|
|
|
|
serde_yaml::to_string(&template).context("serializing rendered cfgsync config")
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-09 08:48:05 +01:00
|
|
|
/// Applies cfgsync-specific override fields to a mutable YAML document.
|
2026-02-24 03:46:54 +01:00
|
|
|
pub fn apply_cfgsync_overrides(
|
|
|
|
|
template: &mut Value,
|
|
|
|
|
overrides: &CfgsyncConfigOverrides,
|
|
|
|
|
) -> Result<()> {
|
|
|
|
|
let root = mapping_mut(template)?;
|
|
|
|
|
|
|
|
|
|
if let Some(port) = overrides.port {
|
|
|
|
|
root.insert(
|
|
|
|
|
Value::String("port".to_string()),
|
|
|
|
|
serde_yaml::to_value(port)?,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(n_hosts) = overrides.n_hosts {
|
|
|
|
|
root.insert(
|
|
|
|
|
Value::String("n_hosts".to_string()),
|
|
|
|
|
serde_yaml::to_value(n_hosts)?,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(bundle_path) = &overrides.bundle_path {
|
|
|
|
|
root.insert(
|
|
|
|
|
Value::String("bundle_path".to_string()),
|
|
|
|
|
Value::String(bundle_path.clone()),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(timeout_floor_secs) = overrides.timeout_floor_secs {
|
|
|
|
|
let timeout_key = Value::String("timeout".to_string());
|
|
|
|
|
let timeout_value = root
|
|
|
|
|
.get(&timeout_key)
|
|
|
|
|
.and_then(Value::as_u64)
|
|
|
|
|
.unwrap_or(timeout_floor_secs)
|
|
|
|
|
.max(timeout_floor_secs);
|
|
|
|
|
root.insert(timeout_key, serde_yaml::to_value(timeout_value)?);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(endpoint) = &overrides.metrics_otlp_ingest_url {
|
2026-03-09 08:48:05 +01:00
|
|
|
let tracing_settings = nested_mapping_mut(root, "tracing_settings")?;
|
2026-02-24 03:46:54 +01:00
|
|
|
tracing_settings.insert(
|
|
|
|
|
Value::String("metrics".to_string()),
|
|
|
|
|
parse_otlp_metrics_layer(endpoint)?,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn mapping_mut(value: &mut Value) -> Result<&mut Mapping> {
|
|
|
|
|
value
|
|
|
|
|
.as_mapping_mut()
|
|
|
|
|
.context("cfgsync template root must be a YAML map")
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-09 08:48:05 +01:00
|
|
|
fn nested_mapping_mut<'a>(mapping: &'a mut Mapping, key: &str) -> Result<&'a mut Mapping> {
|
|
|
|
|
let key_name = key.to_owned();
|
|
|
|
|
let key = Value::String(key_name.clone());
|
2026-02-24 03:46:54 +01:00
|
|
|
let entry = mapping
|
|
|
|
|
.entry(key)
|
|
|
|
|
.or_insert_with(|| Value::Mapping(Mapping::new()));
|
|
|
|
|
|
|
|
|
|
if !entry.is_mapping() {
|
2026-03-09 08:48:05 +01:00
|
|
|
return Err(RenderTemplateError::NonMappingEntry { key: key_name }).map_err(Into::into);
|
2026-02-24 03:46:54 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
entry
|
|
|
|
|
.as_mapping_mut()
|
2026-03-09 08:48:05 +01:00
|
|
|
.context("cfgsync template entry should be a YAML map")
|
2026-02-24 03:46:54 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn parse_otlp_metrics_layer(endpoint: &str) -> Result<Value> {
|
|
|
|
|
let yaml = format!("!Otlp\nendpoint: \"{endpoint}\"\nhost_identifier: \"node\"\n");
|
|
|
|
|
serde_yaml::from_str(&yaml).context("building metrics OTLP layer")
|
|
|
|
|
}
|