From 6218d4070c7d4c7156aee2dffbeb8586f18576d3 Mon Sep 17 00:00:00 2001 From: andrussal Date: Thu, 12 Mar 2026 10:00:10 +0100 Subject: [PATCH] Polish cfgsync runtime ergonomics --- cfgsync/README.md | 37 ++++++++++++++--------- cfgsync/runtime/src/client.rs | 57 +++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 15 deletions(-) diff --git a/cfgsync/README.md b/cfgsync/README.md index 899d32c..83ca201 100644 --- a/cfgsync/README.md +++ b/cfgsync/README.md @@ -143,6 +143,24 @@ What does not belong in cfgsync core is equally important. Generic cfgsync shoul Those belong in the adapter or in the consuming application. +## Start here + +If you want the shortest path into the library, start with the end-to-end +runtime example: + +- `cfgsync/runtime/examples/minimal_cfgsync.rs` + +It shows the full loop: + +- define a snapshot materializer +- serve cfgsync +- register a node +- fetch artifacts +- write them locally + +After that, the only concepts you usually need to learn are the ones in the +next section. + ## Minimal integration path For a new application, the shortest sensible path is: @@ -155,18 +173,6 @@ For a new application, the shortest sensible path is: That gives you the main value of the library without forcing extra application logic into cfgsync itself. -## Minimal example - -A minimal standalone example lives in: - -- `cfgsync/runtime/examples/minimal_cfgsync.rs` - -It shows the smallest complete path: - -- registration snapshot materializer -- cfgsync server -- node artifact generation - ## Code sketch Typed registration payload: @@ -241,9 +247,10 @@ Fetching and writing artifacts: use cfgsync_runtime::{Client, OutputMap}; # async fn run(registration: cfgsync_core::NodeRegistration) -> anyhow::Result<()> { -let outputs = OutputMap::new() - .route("/config.yaml", "/node-data/node-1/config.yaml") - .route("deployment-settings.yaml", "/node-data/shared/deployment-settings.yaml"); +let outputs = OutputMap::config_and_shared( + "/node-data/node-1/config.yaml", + "/node-data/shared", +); Client::new("http://127.0.0.1:4400") .fetch_and_write(®istration, &outputs) diff --git a/cfgsync/runtime/src/client.rs b/cfgsync/runtime/src/client.rs index 04484fa..c1c4994 100644 --- a/cfgsync/runtime/src/client.rs +++ b/cfgsync/runtime/src/client.rs @@ -21,6 +21,13 @@ const FETCH_RETRY_DELAY: Duration = Duration::from_millis(250); #[derive(Debug, Clone, Default)] pub struct OutputMap { routes: HashMap, + fallback: Option, +} + +#[derive(Debug, Clone)] +enum FallbackRoute { + Under(PathBuf), + Shared { dir: PathBuf }, } impl OutputMap { @@ -41,12 +48,62 @@ impl OutputMap { self } + /// Writes payload files under `root`, preserving each artifact path. + /// + /// For example, `/config.yaml` is written to `/config.yaml` and + /// `shared/deployment-settings.yaml` is written to + /// `/shared/deployment-settings.yaml`. + #[must_use] + pub fn under(root: impl Into) -> Self { + Self { + routes: HashMap::new(), + fallback: Some(FallbackRoute::Under(root.into())), + } + } + + /// Writes the node config to `config_path` and all other files under + /// `shared_dir`, preserving their relative artifact paths. + #[must_use] + pub fn config_and_shared( + config_path: impl Into, + shared_dir: impl Into, + ) -> Self { + let config_path = config_path.into(); + let shared_dir = shared_dir.into(); + + Self::default() + .route("/config.yaml", config_path.clone()) + .route("config.yaml", config_path) + .with_fallback(FallbackRoute::Shared { dir: shared_dir }) + } + fn resolve_path(&self, file: &NodeArtifactFile) -> PathBuf { self.routes .get(&file.path) .cloned() + .or_else(|| { + self.fallback + .as_ref() + .map(|fallback| fallback.resolve(&file.path)) + }) .unwrap_or_else(|| PathBuf::from(&file.path)) } + + fn with_fallback(mut self, fallback: FallbackRoute) -> Self { + self.fallback = Some(fallback); + self + } +} + +impl FallbackRoute { + fn resolve(&self, artifact_path: &str) -> PathBuf { + let relative = artifact_path.trim_start_matches('/'); + + match self { + FallbackRoute::Under(root) => root.join(relative), + FallbackRoute::Shared { dir } => dir.join(relative), + } + } } /// Runtime-oriented cfgsync client that handles registration, fetch, and local