feat(chainfire): Add /admin/member/add legacy endpoint for cluster join

- 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 <noreply@anthropic.com>
This commit is contained in:
centra 2025-12-19 15:31:01 +09:00
parent 9a72c8d3ec
commit aa5973bb96

View file

@ -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::<u64>() {
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<RestApiState>,
@ -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<RestApiState>,
Json(req): Json<AddMemberRequestLegacy>,
) -> Result<(StatusCode, Json<SuccessResponse<serde_json::Value>>), (StatusCode, Json<ErrorResponse>)> {
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,