Pando, Pando the Aspen colony.
This commit is contained in:
1 parent
a6d014601d
commit
ac73968de7
11 files changed
+152
-89
No files matched your search
+2
-1
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user