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,