From aa5973bb963f75bf7fd515f997ae5be36f2da137 Mon Sep 17 00:00:00 2001 From: centra Date: Fri, 19 Dec 2025 15:31:01 +0900 Subject: [PATCH] feat(chainfire): Add /admin/member/add legacy endpoint for cluster join MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AddMemberRequestLegacy accepts string node ID (e.g., 'node01') - string_to_node_id converts to numeric node_id for Raft - Required by first-boot-automation.nix cluster join logic - Also fixes axum 0.8 route syntax (:param -> {param}) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- chainfire/crates/chainfire-server/src/rest.rs | 60 ++++++++++++++++++- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/chainfire/crates/chainfire-server/src/rest.rs b/chainfire/crates/chainfire-server/src/rest.rs index c91b48f..d014d32 100644 --- a/chainfire/crates/chainfire-server/src/rest.rs +++ b/chainfire/crates/chainfire-server/src/rest.rs @@ -116,6 +116,15 @@ pub struct AddMemberRequest { pub raft_addr: String, } +/// Add member request (legacy format from first-boot-automation) +/// Accepts string id and converts to numeric node_id +#[derive(Debug, Deserialize)] +pub struct AddMemberRequestLegacy { + /// Node ID as string (e.g., "node01", "node02") + pub id: String, + pub raft_addr: String, +} + /// Query parameters for prefix scan #[derive(Debug, Deserialize)] pub struct PrefixQuery { @@ -125,12 +134,14 @@ pub struct PrefixQuery { /// Build the REST API router pub fn build_router(state: RestApiState) -> Router { Router::new() - .route("/api/v1/kv/:key", get(get_kv)) - .route("/api/v1/kv/:key", put(put_kv)) - .route("/api/v1/kv/:key", delete(delete_kv)) + .route("/api/v1/kv/{key}", get(get_kv)) + .route("/api/v1/kv/{key}", put(put_kv)) + .route("/api/v1/kv/{key}", delete(delete_kv)) .route("/api/v1/kv", get(list_kv)) .route("/api/v1/cluster/status", get(cluster_status)) .route("/api/v1/cluster/members", post(add_member)) + // Legacy endpoint for first-boot-automation compatibility + .route("/admin/member/add", post(add_member_legacy)) .route("/health", get(health_check)) .with_state(state) } @@ -258,6 +269,22 @@ async fn cluster_status( }))) } +/// Convert string node ID to numeric (e.g., "node01" -> 1, "node02" -> 2) +fn string_to_node_id(s: &str) -> u64 { + // Try to extract number from string like "node01", "node02" + if let Some(num_str) = s.strip_prefix("node") { + if let Ok(num) = num_str.parse::() { + return num; + } + } + // Fallback: use hash of the string + use std::collections::hash_map::DefaultHasher; + use std::hash::{Hash, Hasher}; + let mut hasher = DefaultHasher::new(); + s.hash(&mut hasher); + hasher.finish() +} + /// POST /api/v1/cluster/members - Add member async fn add_member( State(state): State, @@ -286,6 +313,33 @@ async fn add_member( )) } +/// POST /admin/member/add - Add member (legacy format for first-boot-automation) +async fn add_member_legacy( + State(state): State, + Json(req): Json, +) -> Result<(StatusCode, Json>), (StatusCode, Json)> { + let node_id = string_to_node_id(&req.id); + + let rpc_client = state + .rpc_client + .as_ref() + .ok_or_else(|| error_response(StatusCode::SERVICE_UNAVAILABLE, "SERVICE_UNAVAILABLE", "RPC client not available"))?; + + // Add node to RPC client's routing table + rpc_client.add_node(node_id, req.raft_addr.clone()).await; + + Ok(( + StatusCode::CREATED, + Json(SuccessResponse::new(serde_json::json!({ + "id": req.id, + "node_id": node_id, + "raft_addr": req.raft_addr, + "success": true, + "note": "Node registered in RPC client routing table (legacy API)" + }))), + )) +} + /// Helper to create error response fn error_response( status: StatusCode,