Skip to content

Commit a9be47a

Browse files
committed
Split handlers.rs into per-resource modules for better navigability
Break the 1387-line handlers.rs into handlers/{mod,cluster,networks,nics,nodes,vms}.rs with shared types (AppState, ApiError) in mod.rs.
1 parent 254c32b commit a9be47a

10 files changed

Lines changed: 1583 additions & 1423 deletions

File tree

mvirt-api/src/rest/handlers.rs

Lines changed: 0 additions & 1386 deletions
This file was deleted.
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
use axum::{
2+
Json,
3+
extract::{Path, State},
4+
};
5+
use serde::{Deserialize, Serialize};
6+
use std::sync::Arc;
7+
use utoipa::ToSchema;
8+
9+
use super::{ApiError, AppState};
10+
11+
/// Cluster information
12+
#[derive(Serialize, ToSchema)]
13+
pub struct ClusterInfo {
14+
pub cluster_id: String,
15+
pub leader_id: Option<u64>,
16+
pub current_term: u64,
17+
pub commit_index: u64,
18+
pub nodes: Vec<NodeInfo>,
19+
}
20+
21+
/// Node information
22+
#[derive(Serialize, ToSchema)]
23+
pub struct NodeInfo {
24+
pub id: u64,
25+
pub name: String,
26+
pub address: String,
27+
pub state: String,
28+
pub is_leader: bool,
29+
}
30+
31+
/// Get cluster information
32+
#[utoipa::path(
33+
get,
34+
path = "/v1/cluster",
35+
responses(
36+
(status = 200, description = "Cluster information", body = ClusterInfo)
37+
),
38+
tag = "cluster"
39+
)]
40+
pub async fn get_cluster_info(
41+
State(state): State<Arc<AppState>>,
42+
) -> Result<Json<ClusterInfo>, ApiError> {
43+
let info = state.store.get_cluster_info().await?;
44+
let membership = state.store.get_membership().await?;
45+
46+
let nodes: Vec<NodeInfo> = membership
47+
.nodes
48+
.into_iter()
49+
.map(|n| NodeInfo {
50+
id: n.id,
51+
name: format!("node-{}", n.id),
52+
address: n.address,
53+
state: if Some(n.id) == info.leader_id {
54+
"leader".to_string()
55+
} else {
56+
"follower".to_string()
57+
},
58+
is_leader: Some(n.id) == info.leader_id,
59+
})
60+
.collect();
61+
62+
Ok(Json(ClusterInfo {
63+
cluster_id: info.cluster_id,
64+
leader_id: info.leader_id,
65+
current_term: info.current_term,
66+
commit_index: info.commit_index,
67+
nodes,
68+
}))
69+
}
70+
71+
/// Cluster membership information
72+
#[derive(Serialize, ToSchema)]
73+
pub struct ClusterMembership {
74+
pub voters: Vec<u64>,
75+
pub learners: Vec<u64>,
76+
pub nodes: Vec<MembershipNode>,
77+
}
78+
79+
/// Node in membership
80+
#[derive(Serialize, ToSchema)]
81+
pub struct MembershipNode {
82+
pub id: u64,
83+
pub address: String,
84+
pub role: String,
85+
}
86+
87+
/// Get cluster membership
88+
#[utoipa::path(
89+
get,
90+
path = "/v1/cluster/membership",
91+
responses(
92+
(status = 200, description = "Cluster membership", body = ClusterMembership)
93+
),
94+
tag = "cluster"
95+
)]
96+
pub async fn get_membership(
97+
State(state): State<Arc<AppState>>,
98+
) -> Result<Json<ClusterMembership>, ApiError> {
99+
let membership = state.store.get_membership().await?;
100+
101+
let nodes: Vec<MembershipNode> = membership
102+
.nodes
103+
.into_iter()
104+
.map(|n| MembershipNode {
105+
id: n.id,
106+
address: n.address,
107+
role: n.role,
108+
})
109+
.collect();
110+
111+
Ok(Json(ClusterMembership {
112+
voters: membership.voters,
113+
learners: membership.learners,
114+
nodes,
115+
}))
116+
}
117+
118+
/// Request to create a join token
119+
#[derive(Deserialize, ToSchema)]
120+
pub struct CreateJoinTokenRequest {
121+
/// Node ID that will use this token
122+
pub node_id: u64,
123+
/// Token validity in seconds (default: 3600 = 1 hour)
124+
pub valid_for_secs: Option<u64>,
125+
}
126+
127+
/// Join token response
128+
#[derive(Serialize, ToSchema)]
129+
pub struct CreateJoinTokenResponse {
130+
pub token: String,
131+
pub node_id: u64,
132+
pub valid_for_secs: u64,
133+
}
134+
135+
/// Create a join token for a new node
136+
#[utoipa::path(
137+
post,
138+
path = "/v1/cluster/join-token",
139+
request_body = CreateJoinTokenRequest,
140+
responses(
141+
(status = 200, description = "Join token created", body = CreateJoinTokenResponse),
142+
(status = 503, description = "Not the leader or cluster secret not configured", body = ApiError)
143+
),
144+
tag = "cluster"
145+
)]
146+
pub async fn create_join_token(
147+
State(state): State<Arc<AppState>>,
148+
Json(req): Json<CreateJoinTokenRequest>,
149+
) -> Result<Json<CreateJoinTokenResponse>, ApiError> {
150+
let valid_for = req.valid_for_secs.unwrap_or(3600);
151+
let token = state
152+
.store
153+
.create_join_token(req.node_id, valid_for)
154+
.await
155+
.map_err(|e| ApiError {
156+
error: format!("Failed to create join token: {}", e),
157+
code: 503,
158+
})?;
159+
160+
Ok(Json(CreateJoinTokenResponse {
161+
token,
162+
node_id: req.node_id,
163+
valid_for_secs: valid_for,
164+
}))
165+
}
166+
167+
/// Request to remove a node from the cluster
168+
#[derive(Deserialize, ToSchema)]
169+
#[allow(dead_code)]
170+
pub struct RemoveNodeRequest {
171+
/// Force remove even if it would break quorum (not yet implemented)
172+
pub force: Option<bool>,
173+
}
174+
175+
/// Response for remove node
176+
#[derive(Serialize, ToSchema)]
177+
pub struct RemoveNodeResponse {
178+
pub removed: bool,
179+
pub node_id: u64,
180+
}
181+
182+
/// Remove a node from the cluster
183+
#[utoipa::path(
184+
delete,
185+
path = "/v1/cluster/nodes/{id}",
186+
params(
187+
("id" = u64, Path, description = "Node ID to remove")
188+
),
189+
responses(
190+
(status = 200, description = "Node removed", body = RemoveNodeResponse),
191+
(status = 404, description = "Node not found", body = ApiError),
192+
(status = 503, description = "Not the leader", body = ApiError)
193+
),
194+
tag = "cluster"
195+
)]
196+
pub async fn remove_node(
197+
State(state): State<Arc<AppState>>,
198+
Path(node_id): Path<u64>,
199+
) -> Result<Json<RemoveNodeResponse>, ApiError> {
200+
state.store.remove_node(node_id).await?;
201+
state.audit.node_removed(node_id);
202+
203+
Ok(Json(RemoveNodeResponse {
204+
removed: true,
205+
node_id,
206+
}))
207+
}

mvirt-api/src/rest/handlers/mod.rs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// Allow dead code for legacy handlers during transition to UI-compatible API
2+
#[allow(dead_code)]
3+
mod cluster;
4+
#[allow(dead_code)]
5+
mod networks;
6+
#[allow(dead_code)]
7+
mod nics;
8+
#[allow(dead_code)]
9+
mod nodes;
10+
#[allow(dead_code)]
11+
mod vms;
12+
13+
use axum::{Json, http::StatusCode, response::IntoResponse};
14+
use mraft::NodeId;
15+
use serde::Serialize;
16+
use std::sync::Arc;
17+
use utoipa::ToSchema;
18+
19+
use crate::audit::ApiAuditLogger;
20+
use crate::store::{DataStore, StoreError};
21+
22+
#[allow(unused_imports)]
23+
pub use cluster::*;
24+
#[allow(unused_imports)]
25+
pub use networks::*;
26+
#[allow(unused_imports)]
27+
pub use nics::*;
28+
#[allow(unused_imports)]
29+
pub use nodes::*;
30+
#[allow(unused_imports)]
31+
pub use vms::*;
32+
33+
/// Shared application state
34+
pub struct AppState {
35+
pub store: Arc<dyn DataStore>,
36+
pub audit: Arc<ApiAuditLogger>,
37+
pub node_id: NodeId,
38+
pub log_endpoint: String,
39+
}
40+
41+
/// API error response
42+
#[derive(Serialize, ToSchema)]
43+
pub struct ApiError {
44+
pub error: String,
45+
pub code: u32,
46+
}
47+
48+
impl IntoResponse for ApiError {
49+
fn into_response(self) -> axum::response::Response {
50+
let status = match self.code {
51+
404 => StatusCode::NOT_FOUND,
52+
409 => StatusCode::CONFLICT,
53+
400 => StatusCode::BAD_REQUEST,
54+
503 => StatusCode::SERVICE_UNAVAILABLE,
55+
_ => StatusCode::INTERNAL_SERVER_ERROR,
56+
};
57+
(status, Json(self)).into_response()
58+
}
59+
}
60+
61+
impl From<StoreError> for ApiError {
62+
fn from(e: StoreError) -> Self {
63+
match e {
64+
StoreError::NotFound(msg) => ApiError {
65+
error: msg,
66+
code: 404,
67+
},
68+
StoreError::Conflict(msg) => ApiError {
69+
error: msg,
70+
code: 409,
71+
},
72+
StoreError::NotLeader { .. } => ApiError {
73+
error: "Not leader".to_string(),
74+
code: 503,
75+
},
76+
StoreError::ScheduleFailed(msg) => ApiError {
77+
error: msg,
78+
code: 503, // Service unavailable - no nodes can handle the request
79+
},
80+
StoreError::Internal(msg) => ApiError {
81+
error: msg,
82+
code: 500,
83+
},
84+
StoreError::VersionMismatch { expected, actual } => ApiError {
85+
error: format!("Version mismatch: expected {}, got {}", expected, actual),
86+
code: 409,
87+
},
88+
}
89+
}
90+
}
91+
92+
/// Version information
93+
#[derive(Serialize, ToSchema)]
94+
pub struct VersionInfo {
95+
pub version: String,
96+
}
97+
98+
/// Get service version
99+
#[utoipa::path(
100+
get,
101+
path = "/v1/version",
102+
responses(
103+
(status = 200, description = "Service version", body = VersionInfo)
104+
),
105+
tag = "system"
106+
)]
107+
pub async fn get_version() -> Json<VersionInfo> {
108+
Json(VersionInfo {
109+
version: env!("CARGO_PKG_VERSION").to_string(),
110+
})
111+
}

0 commit comments

Comments
 (0)