Polish cfgsync runtime ergonomics

This commit is contained in:
andrussal 2026-03-12 10:00:10 +01:00
parent 4712f93a68
commit 6218d4070c
2 changed files with 79 additions and 15 deletions

View File

@ -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(&registration, &outputs)

View File

@ -21,6 +21,13 @@ const FETCH_RETRY_DELAY: Duration = Duration::from_millis(250);
#[derive(Debug, Clone, Default)]
pub struct OutputMap {
routes: HashMap<String, PathBuf>,
fallback: Option<FallbackRoute>,
}
#[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 `<root>/config.yaml` and
/// `shared/deployment-settings.yaml` is written to
/// `<root>/shared/deployment-settings.yaml`.
#[must_use]
pub fn under(root: impl Into<PathBuf>) -> 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<PathBuf>,
shared_dir: impl Into<PathBuf>,
) -> 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