2026-03-09 08:48:05 +01:00
|
|
|
use std::{fs, path::Path, sync::Arc};
|
|
|
|
|
|
|
|
|
|
use anyhow::Context as _;
|
2026-03-10 14:28:19 +01:00
|
|
|
use axum::Router;
|
2026-03-10 13:56:27 +01:00
|
|
|
use cfgsync_adapter::{
|
2026-03-12 07:44:20 +01:00
|
|
|
CachedSnapshotMaterializer, MaterializedArtifacts, MaterializedArtifactsSink,
|
|
|
|
|
PersistingSnapshotMaterializer, RegistrationConfigSource, RegistrationSnapshotMaterializer,
|
2026-03-10 13:56:27 +01:00
|
|
|
};
|
2026-03-10 11:03:51 +01:00
|
|
|
use cfgsync_core::{
|
2026-03-12 08:27:44 +01:00
|
|
|
BundleConfigSource, CfgsyncServerState, NodeConfigSource, RunCfgsyncError,
|
2026-03-12 09:39:16 +01:00
|
|
|
serve_cfgsync as serve_cfgsync_state,
|
2026-03-10 11:03:51 +01:00
|
|
|
};
|
2026-03-12 09:39:16 +01:00
|
|
|
use serde::Deserialize;
|
2026-03-10 11:06:16 +01:00
|
|
|
use thiserror::Error;
|
2026-03-09 08:48:05 +01:00
|
|
|
|
|
|
|
|
/// Runtime cfgsync server config loaded from YAML.
|
2026-03-12 09:39:16 +01:00
|
|
|
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
2026-03-12 09:51:03 +01:00
|
|
|
pub struct ServerConfig {
|
2026-03-12 07:35:22 +01:00
|
|
|
/// HTTP port to bind the cfgsync server on.
|
2026-03-09 08:48:05 +01:00
|
|
|
pub port: u16,
|
2026-03-12 07:30:01 +01:00
|
|
|
/// Source used by the runtime-managed cfgsync server.
|
2026-03-12 09:51:03 +01:00
|
|
|
pub source: ServerSource,
|
2026-03-09 08:48:05 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-12 07:30:01 +01:00
|
|
|
/// Runtime cfgsync source loaded from config.
|
|
|
|
|
///
|
|
|
|
|
/// This type is intentionally runtime-oriented:
|
|
|
|
|
/// - `Bundle` serves a static precomputed bundle directly
|
2026-03-12 08:27:44 +01:00
|
|
|
/// - `Registration` serves precomputed artifacts through the registration
|
2026-03-12 07:30:01 +01:00
|
|
|
/// protocol, which is useful when the consumer wants clients to register
|
|
|
|
|
/// before receiving already-materialized artifacts
|
2026-03-10 14:44:28 +01:00
|
|
|
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
|
|
|
|
#[serde(tag = "kind", rename_all = "snake_case")]
|
2026-03-12 09:51:03 +01:00
|
|
|
pub enum ServerSource {
|
2026-03-12 07:35:22 +01:00
|
|
|
/// Serve a static precomputed artifact bundle directly.
|
2026-03-10 14:44:28 +01:00
|
|
|
Bundle { bundle_path: String },
|
2026-03-12 08:27:44 +01:00
|
|
|
/// Require node registration before serving precomputed artifacts.
|
2026-03-12 09:39:16 +01:00
|
|
|
Registration { artifacts_path: String },
|
2026-03-10 14:44:28 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-10 11:06:16 +01:00
|
|
|
#[derive(Debug, Error)]
|
2026-03-12 09:51:03 +01:00
|
|
|
pub enum LoadServerConfigError {
|
2026-03-10 11:06:16 +01:00
|
|
|
#[error("failed to read cfgsync config file {path}: {source}")]
|
|
|
|
|
Read {
|
|
|
|
|
path: String,
|
|
|
|
|
#[source]
|
|
|
|
|
source: std::io::Error,
|
|
|
|
|
},
|
|
|
|
|
#[error("failed to parse cfgsync config file {path}: {source}")]
|
|
|
|
|
Parse {
|
|
|
|
|
path: String,
|
|
|
|
|
#[source]
|
|
|
|
|
source: serde_yaml::Error,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-12 09:51:03 +01:00
|
|
|
impl ServerConfig {
|
2026-03-09 08:48:05 +01:00
|
|
|
/// Loads cfgsync runtime server config from a YAML file.
|
2026-03-12 09:51:03 +01:00
|
|
|
pub fn load_from_file(path: &Path) -> Result<Self, LoadServerConfigError> {
|
2026-03-10 11:06:16 +01:00
|
|
|
let config_path = path.display().to_string();
|
|
|
|
|
let config_content =
|
2026-03-12 09:51:03 +01:00
|
|
|
fs::read_to_string(path).map_err(|source| LoadServerConfigError::Read {
|
2026-03-10 11:06:16 +01:00
|
|
|
path: config_path.clone(),
|
|
|
|
|
source,
|
|
|
|
|
})?;
|
|
|
|
|
|
2026-03-12 09:51:03 +01:00
|
|
|
let config: ServerConfig = serde_yaml::from_str(&config_content).map_err(|source| {
|
|
|
|
|
LoadServerConfigError::Parse {
|
|
|
|
|
path: config_path,
|
|
|
|
|
source,
|
|
|
|
|
}
|
|
|
|
|
})?;
|
2026-03-10 14:44:28 +01:00
|
|
|
|
2026-03-12 09:39:16 +01:00
|
|
|
Ok(config)
|
2026-03-10 11:06:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub fn for_bundle(port: u16, bundle_path: impl Into<String>) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
port,
|
2026-03-12 09:51:03 +01:00
|
|
|
source: ServerSource::Bundle {
|
2026-03-10 14:44:28 +01:00
|
|
|
bundle_path: bundle_path.into(),
|
|
|
|
|
},
|
2026-03-10 11:06:16 +01:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-09 08:48:05 +01:00
|
|
|
|
2026-03-12 07:35:22 +01:00
|
|
|
/// Builds a config that serves a static bundle behind the registration
|
|
|
|
|
/// flow.
|
2026-03-10 11:06:16 +01:00
|
|
|
#[must_use]
|
2026-03-12 08:27:44 +01:00
|
|
|
pub fn for_registration(port: u16, artifacts_path: impl Into<String>) -> Self {
|
2026-03-10 11:06:16 +01:00
|
|
|
Self {
|
|
|
|
|
port,
|
2026-03-12 09:51:03 +01:00
|
|
|
source: ServerSource::Registration {
|
2026-03-12 08:27:44 +01:00
|
|
|
artifacts_path: artifacts_path.into(),
|
2026-03-10 14:44:28 +01:00
|
|
|
},
|
2026-03-10 11:06:16 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-10 11:03:51 +01:00
|
|
|
fn load_bundle_provider(bundle_path: &Path) -> anyhow::Result<Arc<dyn NodeConfigSource>> {
|
|
|
|
|
let provider = BundleConfigSource::from_yaml_file(bundle_path)
|
2026-03-09 08:48:05 +01:00
|
|
|
.with_context(|| format!("loading cfgsync provider from {}", bundle_path.display()))?;
|
|
|
|
|
|
|
|
|
|
Ok(Arc::new(provider))
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-12 08:27:44 +01:00
|
|
|
fn load_registration_source(artifacts_path: &Path) -> anyhow::Result<Arc<dyn NodeConfigSource>> {
|
|
|
|
|
let materialized = load_materialized_artifacts_yaml(artifacts_path)?;
|
2026-03-12 07:44:20 +01:00
|
|
|
let provider = RegistrationConfigSource::new(materialized);
|
2026-03-10 09:18:29 +01:00
|
|
|
|
|
|
|
|
Ok(Arc::new(provider))
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-12 08:27:44 +01:00
|
|
|
fn load_materialized_artifacts_yaml(
|
|
|
|
|
artifacts_path: &Path,
|
|
|
|
|
) -> anyhow::Result<MaterializedArtifacts> {
|
|
|
|
|
let raw = fs::read_to_string(artifacts_path).with_context(|| {
|
|
|
|
|
format!(
|
|
|
|
|
"reading cfgsync materialized artifacts from {}",
|
|
|
|
|
artifacts_path.display()
|
|
|
|
|
)
|
|
|
|
|
})?;
|
|
|
|
|
|
|
|
|
|
serde_yaml::from_str(&raw).with_context(|| {
|
|
|
|
|
format!(
|
|
|
|
|
"parsing cfgsync materialized artifacts from {}",
|
|
|
|
|
artifacts_path.display()
|
|
|
|
|
)
|
|
|
|
|
})
|
2026-03-10 09:18:29 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-09 08:48:05 +01:00
|
|
|
fn resolve_bundle_path(config_path: &Path, bundle_path: &str) -> std::path::PathBuf {
|
|
|
|
|
let path = Path::new(bundle_path);
|
|
|
|
|
if path.is_absolute() {
|
|
|
|
|
return path.to_path_buf();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
config_path
|
|
|
|
|
.parent()
|
|
|
|
|
.unwrap_or_else(|| Path::new("."))
|
|
|
|
|
.join(path)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Loads runtime config and starts cfgsync HTTP server process.
|
2026-03-12 09:51:03 +01:00
|
|
|
pub async fn serve_from_config(config_path: &Path) -> anyhow::Result<()> {
|
|
|
|
|
let config = ServerConfig::load_from_file(config_path)?;
|
2026-03-10 14:44:28 +01:00
|
|
|
let bundle_path = resolve_source_path(config_path, &config.source);
|
2026-03-09 08:48:05 +01:00
|
|
|
|
2026-03-10 09:18:29 +01:00
|
|
|
let state = build_server_state(&config, &bundle_path)?;
|
2026-03-12 09:39:16 +01:00
|
|
|
serve_cfgsync_state(config.port, state).await?;
|
2026-03-09 08:48:05 +01:00
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-12 09:39:16 +01:00
|
|
|
/// Builds the default registration-backed cfgsync router from a snapshot
|
2026-03-10 14:28:19 +01:00
|
|
|
/// materializer.
|
2026-03-12 07:30:01 +01:00
|
|
|
///
|
|
|
|
|
/// This is the main code-driven entrypoint for apps that want cfgsync to own:
|
|
|
|
|
/// - node registration
|
|
|
|
|
/// - readiness polling
|
|
|
|
|
/// - artifact serving
|
|
|
|
|
///
|
|
|
|
|
/// while the app owns only snapshot materialization logic.
|
2026-03-12 09:51:03 +01:00
|
|
|
pub fn build_router<M>(materializer: M) -> Router
|
2026-03-10 14:28:19 +01:00
|
|
|
where
|
|
|
|
|
M: RegistrationSnapshotMaterializer + 'static,
|
|
|
|
|
{
|
2026-03-12 07:44:20 +01:00
|
|
|
let provider = RegistrationConfigSource::new(CachedSnapshotMaterializer::new(materializer));
|
2026-03-12 09:39:16 +01:00
|
|
|
cfgsync_core::build_cfgsync_router(CfgsyncServerState::new(Arc::new(provider)))
|
2026-03-10 14:28:19 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Builds a registration-backed cfgsync router with a persistence hook for
|
|
|
|
|
/// ready materialization results.
|
2026-03-12 07:30:01 +01:00
|
|
|
///
|
|
|
|
|
/// Use this when the application wants cfgsync to persist or publish shared
|
|
|
|
|
/// artifacts after a snapshot becomes ready.
|
2026-03-12 09:51:03 +01:00
|
|
|
pub fn build_persisted_router<M, S>(materializer: M, sink: S) -> Router
|
2026-03-10 14:28:19 +01:00
|
|
|
where
|
|
|
|
|
M: RegistrationSnapshotMaterializer + 'static,
|
|
|
|
|
S: MaterializedArtifactsSink + 'static,
|
|
|
|
|
{
|
2026-03-12 07:44:20 +01:00
|
|
|
let provider = RegistrationConfigSource::new(CachedSnapshotMaterializer::new(
|
2026-03-10 14:28:19 +01:00
|
|
|
PersistingSnapshotMaterializer::new(materializer, sink),
|
|
|
|
|
));
|
|
|
|
|
|
2026-03-12 09:39:16 +01:00
|
|
|
cfgsync_core::build_cfgsync_router(CfgsyncServerState::new(Arc::new(provider)))
|
2026-03-10 14:28:19 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-12 09:39:16 +01:00
|
|
|
/// Runs the default registration-backed cfgsync server directly from a snapshot
|
2026-03-10 13:56:27 +01:00
|
|
|
/// materializer.
|
2026-03-12 07:30:01 +01:00
|
|
|
///
|
|
|
|
|
/// This is the simplest runtime entrypoint when the application already has a
|
|
|
|
|
/// materializer value and does not need to compose extra routes.
|
2026-03-12 09:51:03 +01:00
|
|
|
pub async fn serve<M>(port: u16, materializer: M) -> Result<(), RunCfgsyncError>
|
2026-03-10 13:56:27 +01:00
|
|
|
where
|
|
|
|
|
M: RegistrationSnapshotMaterializer + 'static,
|
|
|
|
|
{
|
2026-03-12 09:51:03 +01:00
|
|
|
let router = build_router(materializer);
|
2026-03-10 14:28:19 +01:00
|
|
|
serve_router(port, router).await
|
|
|
|
|
}
|
2026-03-10 13:56:27 +01:00
|
|
|
|
2026-03-10 14:28:19 +01:00
|
|
|
/// Runs a registration-backed cfgsync server with a persistence hook for ready
|
|
|
|
|
/// materialization results.
|
2026-03-12 07:30:01 +01:00
|
|
|
///
|
|
|
|
|
/// This is the direct serving counterpart to
|
2026-03-12 09:51:03 +01:00
|
|
|
/// [`build_persisted_router`].
|
|
|
|
|
pub async fn serve_persisted<M, S>(
|
2026-03-10 14:28:19 +01:00
|
|
|
port: u16,
|
|
|
|
|
materializer: M,
|
|
|
|
|
sink: S,
|
|
|
|
|
) -> Result<(), RunCfgsyncError>
|
|
|
|
|
where
|
|
|
|
|
M: RegistrationSnapshotMaterializer + 'static,
|
|
|
|
|
S: MaterializedArtifactsSink + 'static,
|
|
|
|
|
{
|
2026-03-12 09:51:03 +01:00
|
|
|
let router = build_persisted_router(materializer, sink);
|
2026-03-10 14:28:19 +01:00
|
|
|
serve_router(port, router).await
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn serve_router(port: u16, router: Router) -> Result<(), RunCfgsyncError> {
|
|
|
|
|
let bind_addr = format!("0.0.0.0:{port}");
|
|
|
|
|
let listener = tokio::net::TcpListener::bind(&bind_addr)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|source| RunCfgsyncError::Bind { bind_addr, source })?;
|
|
|
|
|
|
|
|
|
|
axum::serve(listener, router)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|source| RunCfgsyncError::Serve { source })?;
|
|
|
|
|
|
|
|
|
|
Ok(())
|
2026-03-10 13:56:27 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-10 09:18:29 +01:00
|
|
|
fn build_server_state(
|
2026-03-12 09:51:03 +01:00
|
|
|
config: &ServerConfig,
|
2026-03-10 14:44:28 +01:00
|
|
|
source_path: &Path,
|
2026-03-10 11:03:51 +01:00
|
|
|
) -> anyhow::Result<CfgsyncServerState> {
|
2026-03-10 14:44:28 +01:00
|
|
|
let repo = match &config.source {
|
2026-03-12 09:51:03 +01:00
|
|
|
ServerSource::Bundle { .. } => load_bundle_provider(source_path)?,
|
|
|
|
|
ServerSource::Registration { .. } => load_registration_source(source_path)?,
|
2026-03-10 09:18:29 +01:00
|
|
|
};
|
2026-03-09 08:48:05 +01:00
|
|
|
|
2026-03-10 11:03:51 +01:00
|
|
|
Ok(CfgsyncServerState::new(repo))
|
2026-03-09 08:48:05 +01:00
|
|
|
}
|
2026-03-10 14:44:28 +01:00
|
|
|
|
2026-03-12 09:51:03 +01:00
|
|
|
fn resolve_source_path(config_path: &Path, source: &ServerSource) -> std::path::PathBuf {
|
2026-03-10 14:44:28 +01:00
|
|
|
match source {
|
2026-03-12 09:51:03 +01:00
|
|
|
ServerSource::Bundle { bundle_path } => resolve_bundle_path(config_path, bundle_path),
|
|
|
|
|
ServerSource::Registration { artifacts_path } => {
|
2026-03-12 08:27:44 +01:00
|
|
|
resolve_bundle_path(config_path, artifacts_path)
|
|
|
|
|
}
|
2026-03-10 14:44:28 +01:00
|
|
|
}
|
|
|
|
|
}
|