Pando, Pando the Aspen colony.

This commit is contained in:
Kalzu Rekku committed 2026-10-27 21:27:30 +02:00
1 parent a6d014601d
commit ac73968de7
11 files changed
+152 -89

No files matched your search

+2 -1
View File
@@ -140,8 +140,9 @@ pub async fn get_info() -> Json<ApiResponse> {
uuid: Uuid::new_v4(),
date: Utc::now().to_rfc3339(),
data: serde_json::json!({
"service": "pando",
"version": 1,
"message": "Hello from Rust!"
"message": "Hello from Pando! 🌳"
}),
})
}
+59 -40
View File
@@ -40,8 +40,12 @@ use crate::{
},
};
fn env_parse<T: std::str::FromStr>(name: &str, default: T) -> T {
std::env::var(name)
fn get_env(pando_name: &str, app_name: &str) -> Result<String, std::env::VarError> {
std::env::var(pando_name).or_else(|_| std::env::var(app_name))
}
fn env_parse<T: std::str::FromStr>(pando_name: &str, app_name: &str, default: T) -> T {
get_env(pando_name, app_name)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(default)
@@ -56,33 +60,53 @@ async fn main() {
.with_target(false)
.init();
let config_path = std::env::var("APP_CONFIG").unwrap_or_else(|_| "config.json".into());
let schema = load_config(&config_path).unwrap_or_else(|e| panic!("Failed to load configuration: {e}"));
info!("Starting Pando 🌳 node...");
info!(path = %config_path, fields = schema.fields.len(), "Configuration loaded");
let config_path = get_env("PANDO_CONFIG", "APP_CONFIG").unwrap_or_else(|_| {
if std::path::Path::new("pando.json").exists() {
"pando.json".into()
} else if std::path::Path::new("config.json").exists() {
"config.json".into()
} else {
"pando.json".into()
}
});
let schema = load_config(&config_path).unwrap_or_else(|e| panic!("Failed to load configuration from {config_path}: {e}"));
let role = match std::env::var("APP_ROLE").as_deref() {
info!(path = %config_path, fields = schema.fields.len(), "Pando configuration loaded");
let role = match get_env("PANDO_ROLE", "APP_ROLE").as_deref() {
Ok("replica") => Role::Replica,
Ok("primary") | Err(_) => Role::Primary,
Ok(other) => panic!("APP_ROLE must be 'primary' or 'replica', got '{other}'"),
Ok(other) => panic!("Role must be 'primary' or 'replica', got '{other}'"),
};
let primary_url = std::env::var("APP_PRIMARY_URL").ok();
let repl_token = std::env::var("APP_REPL_TOKEN").ok().filter(|t| !t.is_empty());
let primary_url = get_env("PANDO_PRIMARY_URL", "APP_PRIMARY_URL").ok();
let repl_token = get_env("PANDO_REPL_TOKEN", "APP_REPL_TOKEN").ok().filter(|t| !t.is_empty());
if role == Role::Replica && (primary_url.is_none() || repl_token.is_none()) {
panic!("Replica requires APP_PRIMARY_URL and APP_REPL_TOKEN");
panic!("Replica requires PANDO_PRIMARY_URL (or APP_PRIMARY_URL) and PANDO_REPL_TOKEN (or APP_REPL_TOKEN)");
}
if repl_token.is_none() {
warn!("APP_REPL_TOKEN not set: /api/replication/changes is disabled");
warn!("Replication token not set: /api/replication/changes is disabled");
}
let _sync_cfg = SyncConfig {
poll_interval: Duration::from_secs(env_parse("APP_SYNC_INTERVAL_SECS", 5)),
max_backoff: Duration::from_secs(env_parse("APP_SYNC_MAX_BACKOFF_SECS", 60)),
batch: env_parse("APP_SYNC_BATCH", 200i64).clamp(1, 1000),
let sync_cfg = SyncConfig {
poll_interval: Duration::from_secs(env_parse("PANDO_SYNC_INTERVAL_SECS", "APP_SYNC_INTERVAL_SECS", 5)),
max_backoff: Duration::from_secs(env_parse("PANDO_SYNC_MAX_BACKOFF_SECS", "APP_SYNC_MAX_BACKOFF_SECS", 60)),
batch: env_parse("PANDO_SYNC_BATCH", "APP_SYNC_BATCH", 200i64).clamp(1, 1000),
};
let mut db = Connection::open("messages.db").expect("Failed to open SQLite database");
let db_path = get_env("PANDO_DB", "APP_DB").unwrap_or_else(|_| {
if std::path::Path::new("pando.db").exists() {
"pando.db".into()
} else if std::path::Path::new("messages.db").exists() {
"messages.db".into()
} else {
"pando.db".into()
}
});
let mut db = Connection::open(&db_path).expect("Failed to open SQLite database");
init_db(&mut db).expect("Failed to initialize database");
let instance_id = load_or_create_instance_id(&db).expect("Failed to load instance id");
@@ -105,7 +129,7 @@ async fn main() {
meta_set(&db, "tls_ca_key_pem", &ca_key).unwrap();
ca_cert_pem = Some(ca_cert);
ca_key_pem = Some(ca_key);
info!("Cluster Root CA initialized");
info!("Pando Cluster Root CA initialized");
}
// Initialize Primary Server Certificate
@@ -113,7 +137,7 @@ async fn main() {
let (cert, key) = issue_leaf_cert(
ca_cert_pem.as_ref().unwrap(),
ca_key_pem.as_ref().unwrap(),
"primary",
"pando-primary",
vec!["localhost".into(), "127.0.0.1".into()],
)
.expect("Failed to issue primary server certificate");
@@ -124,8 +148,8 @@ async fn main() {
info!("Primary server certificate generated");
}
// Preload an initial bootstrap join token if APP_JOIN_TOKEN is passed
if let Ok(seed_token) = std::env::var("APP_JOIN_TOKEN") {
// Preload an initial bootstrap join token if PANDO_JOIN_TOKEN / APP_JOIN_TOKEN is passed
if let Ok(seed_token) = get_env("PANDO_JOIN_TOKEN", "APP_JOIN_TOKEN") {
join_tokens.write().unwrap().insert(
seed_token.clone(),
JoinToken {
@@ -138,16 +162,16 @@ async fn main() {
} else {
// Replica: Check if dynamic bootstrap enrollment is needed
if node_cert_pem.is_none() {
// APP_JOIN_URL lets the replication source (APP_PRIMARY_URL) differ from the
// CA enrollment target (APP_JOIN_URL). This is required in chain topologies
// PANDO_JOIN_URL lets the replication source (PANDO_PRIMARY_URL) differ from the
// CA enrollment target (PANDO_JOIN_URL). This is required in chain topologies
// where intermediate replicas serve data but cannot issue certificates.
// Falls back to APP_PRIMARY_URL when not set.
let join_url = std::env::var("APP_JOIN_URL")
// Falls back to PANDO_PRIMARY_URL when not set.
let join_url = get_env("PANDO_JOIN_URL", "APP_JOIN_URL")
.ok()
.or_else(|| primary_url.clone())
.expect("APP_PRIMARY_URL (or APP_JOIN_URL) required for replica enrollment");
let token = std::env::var("APP_JOIN_TOKEN")
.expect("APP_JOIN_TOKEN required for initial replica enrollment");
.expect("PANDO_PRIMARY_URL (or PANDO_JOIN_URL) required for replica enrollment");
let token = get_env("PANDO_JOIN_TOKEN", "APP_JOIN_TOKEN")
.expect("PANDO_JOIN_TOKEN required for initial replica enrollment");
info!(join_url = %join_url, "Bootstrapping mTLS credentials via join token...");
bootstrap_replica_join(&join_url, &token, instance_id, &db)
@@ -175,19 +199,14 @@ async fn main() {
instance_id = %instance_id,
role = role.as_str(),
last_applied_seq,
"Instance initialised"
db = %db_path,
"pando-node initialised"
);
let sync_cfg = SyncConfig {
poll_interval: Duration::from_secs(env_parse("APP_SYNC_INTERVAL_SECS", 5)),
max_backoff: Duration::from_secs(env_parse("APP_SYNC_MAX_BACKOFF_SECS", 60)),
batch: env_parse("APP_SYNC_BATCH", 200i64).clamp(1, 1000),
};
let state = AppState {
db: Arc::new(Mutex::new(db)),
schema: Arc::new(RwLock::new(schema)),
config_path: Arc::new(config_path),
config_path: Arc::new(config_path.clone()),
instance_id,
role: Arc::new(RwLock::new(role)),
primary_url: Arc::new(RwLock::new(primary_url)),
@@ -232,8 +251,8 @@ async fn main() {
.layer(tower_http::compression::CompressionLayer::new())
.with_state(state);
let bind_str = std::env::var("APP_BIND").unwrap_or_else(|_| "127.0.0.1:3000".into());
let addr: SocketAddr = bind_str.parse().expect("Failed to parse APP_BIND address");
let bind_str = get_env("PANDO_BIND", "APP_BIND").unwrap_or_else(|_| "127.0.0.1:3000".into());
let addr: SocketAddr = bind_str.parse().expect("Failed to parse bind address");
// Build mTLS ServerConfig
let server_config = create_mtls_server_config(
@@ -243,12 +262,12 @@ async fn main() {
)
.expect("Failed to build mTLS server configuration");
let app = match std::env::var("APP_CORS_ORIGIN") {
let app = match get_env("PANDO_CORS_ORIGIN", "APP_CORS_ORIGIN") {
Ok(origin) => {
info!(origin = %origin, "CORS enabled");
app.layer(
tower_http::cors::CorsLayer::new()
.allow_origin(origin.parse::<HeaderValue>().expect("invalid APP_CORS_ORIGIN"))
.allow_origin(origin.parse::<HeaderValue>().expect("invalid CORS origin"))
.allow_methods([Method::GET, Method::POST, Method::OPTIONS])
.allow_headers([CONTENT_TYPE, AUTHORIZATION]),
)
@@ -258,7 +277,7 @@ async fn main() {
let rustls_config = RustlsConfig::from_config(Arc::new(server_config));
info!(address = %addr, "mTLS HTTPS server listening");
info!(address = %addr, db = %db_path, config = %config_path, "Pando 🌳 node listening (mTLS HTTPS)");
axum_server::bind_rustls(addr, rustls_config)
.serve(app.into_make_service())
.await
+5
View File
@@ -1,3 +1,8 @@
//! pando-sync: The replication and data synchronization engine of Pando 🌳.
//!
//! Handles pull-based replication across nodes, monotonic sequence tracking,
//! dynamic peer synchronization, and failover support.
use axum::{
extract::{Query, State},
http::{header::AUTHORIZATION, HeaderMap, StatusCode},
+1 -1
View File
@@ -49,7 +49,7 @@ pub fn generate_ca() -> Result<(String, String), String> {
let mut ca_params = CertificateParams::new(vec![])
.map_err(|e| format!("CA params error: {e}"))?;
ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
ca_params.distinguished_name.push(DnType::CommonName, "Cluster Root CA");
ca_params.distinguished_name.push(DnType::CommonName, "Pando Cluster Root CA");
ca_params.key_usages = vec![KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::CrlSign];
let ca_cert = ca_params