manta_server/server/handlers/
mod.rs1use std::sync::Arc;
17
18use axum::{
19 Json,
20 extract::FromRequestParts,
21 http::{StatusCode, header, request::Parts},
22 response::IntoResponse,
23};
24use manta_backend_dispatcher::error::Error as BackendError;
25use serde::Serialize;
26use utoipa::{IntoParams, ToSchema};
27
28use super::ServerState;
29use super::common::app_context::InfraContext;
30
31mod auth;
32mod boot_parameters;
33mod cluster;
34mod configuration;
35mod console;
36mod ephemeral_env;
37mod group;
38mod hardware;
39mod hw_cluster;
40mod image;
41mod kernel_parameters;
42mod migrate;
43mod node;
44mod power;
45mod redfish_endpoints;
46mod sat_file;
47mod session;
48mod template;
49
50pub use auth::*;
51pub use boot_parameters::*;
52pub use cluster::*;
53pub use configuration::*;
54pub use console::*;
55pub use ephemeral_env::*;
56pub use group::*;
57pub use hardware::*;
58pub use hw_cluster::*;
59pub use image::*;
60pub use kernel_parameters::*;
61pub use migrate::*;
62pub use node::*;
63pub use power::*;
64pub use redfish_endpoints::*;
65pub use sat_file::*;
66pub use session::*;
67pub use template::*;
68
69pub struct BearerToken(pub String);
75
76impl<S: Send + Sync> FromRequestParts<S> for BearerToken {
77 type Rejection = (StatusCode, Json<ErrorResponse>);
78
79 async fn from_request_parts(
80 parts: &mut Parts,
81 _state: &S,
82 ) -> Result<Self, Self::Rejection> {
83 let auth_header = parts
84 .headers
85 .get(header::AUTHORIZATION)
86 .and_then(|v| v.to_str().ok())
87 .ok_or_else(|| {
88 (
89 StatusCode::UNAUTHORIZED,
90 Json(ErrorResponse {
91 error: "Missing Authorization header".to_string(),
92 }),
93 )
94 })?;
95
96 let token = auth_header
97 .strip_prefix("Bearer ")
98 .or_else(|| auth_header.strip_prefix("bearer "))
99 .ok_or_else(|| {
100 (
101 StatusCode::UNAUTHORIZED,
102 Json(ErrorResponse {
103 error: "Authorization header must use Bearer scheme".to_string(),
104 }),
105 )
106 })?;
107
108 Ok(BearerToken(token.to_string()))
109 }
110}
111
112pub struct SiteName(pub String);
117
118impl<S: Send + Sync> FromRequestParts<S> for SiteName {
119 type Rejection = (StatusCode, Json<ErrorResponse>);
120
121 async fn from_request_parts(
122 parts: &mut Parts,
123 _state: &S,
124 ) -> Result<Self, Self::Rejection> {
125 let site = parts
126 .headers
127 .get("X-Manta-Site")
128 .and_then(|v| v.to_str().ok())
129 .ok_or_else(|| {
130 (
131 StatusCode::BAD_REQUEST,
132 Json(ErrorResponse {
133 error: "Missing X-Manta-Site header".to_string(),
134 }),
135 )
136 })?;
137 Ok(SiteName(site.to_string()))
138 }
139}
140
141#[derive(IntoParams)]
149#[into_params(parameter_in = Header)]
150#[allow(dead_code)]
151pub struct SiteHeader {
152 #[param(required = true, rename = "X-Manta-Site")]
154 pub x_manta_site: String,
155}
156
157pub struct RequestCtx {
171 pub state: Arc<ServerState>,
174 pub token: String,
176 pub site_name: String,
179}
180
181impl FromRequestParts<Arc<ServerState>> for RequestCtx {
182 type Rejection = (StatusCode, Json<ErrorResponse>);
183
184 async fn from_request_parts(
185 parts: &mut Parts,
186 state: &Arc<ServerState>,
187 ) -> Result<Self, Self::Rejection> {
188 let BearerToken(token) =
189 BearerToken::from_request_parts(parts, state).await?;
190 let SiteName(site_name) =
191 SiteName::from_request_parts(parts, state).await?;
192 state.infra_context(&site_name).map_err(to_handler_error)?;
197 Ok(Self {
198 state: Arc::clone(state),
199 token,
200 site_name,
201 })
202 }
203}
204
205impl RequestCtx {
206 pub fn infra(&self) -> InfraContext<'_> {
211 self
212 .state
213 .infra_context(&self.site_name)
214 .expect("site validated during RequestCtx extraction")
215 }
216}
217
218fn format_with_causes(e: &(dyn std::error::Error + 'static)) -> String {
226 let mut out = e.to_string();
227 let mut src = e.source();
228 while let Some(cause) = src {
229 out.push_str("\n caused by: ");
230 out.push_str(&cause.to_string());
231 src = cause.source();
232 }
233 out
234}
235
236pub fn to_handler_error(e: BackendError) -> (StatusCode, Json<ErrorResponse>) {
241 let status = match &e {
242 BackendError::NotFound(_)
243 | BackendError::SessionNotFound
244 | BackendError::ConfigurationNotFound => StatusCode::NOT_FOUND,
245 BackendError::Conflict(_)
246 | BackendError::ConfigurationAlreadyExistsError(_) => StatusCode::CONFLICT,
247 BackendError::BadRequest(_)
248 | BackendError::InvalidPattern(_)
249 | BackendError::UnsupportedBackend(_)
250 | BackendError::InvalidNodeId(_) => StatusCode::BAD_REQUEST,
251 BackendError::AuthenticationTokenNotFound(_)
252 | BackendError::JwtMalformed(_) => StatusCode::UNAUTHORIZED,
253 BackendError::InsufficientResources(_) => StatusCode::UNPROCESSABLE_ENTITY,
254 _ => StatusCode::INTERNAL_SERVER_ERROR,
255 };
256 let chain = format_with_causes(&e);
257 if status == StatusCode::INTERNAL_SERVER_ERROR {
258 tracing::error!("Internal error: {}", chain);
259 } else {
260 tracing::debug!("Service error {}: {}", status, chain);
261 }
262 (
263 status,
264 Json(ErrorResponse {
265 error: e.to_string(),
266 }),
267 )
268}
269
270pub(super) fn display_error<E: std::error::Error + 'static>(
277 e: E,
278) -> (StatusCode, Json<ErrorResponse>) {
279 let body_msg = e.to_string();
280 tracing::error!("Internal error: {}", format_with_causes(&e));
281 (
282 StatusCode::INTERNAL_SERVER_ERROR,
283 Json(ErrorResponse { error: body_msg }),
284 )
285}
286
287pub(super) fn serialize_or_500<T: Serialize>(
288 v: &T,
289) -> Result<serde_json::Value, (StatusCode, Json<ErrorResponse>)> {
290 serde_json::to_value(v).map_err(|e| {
291 let chain = format_with_causes(&e);
292 tracing::error!("Failed to serialize: {}", chain);
293 (
294 StatusCode::INTERNAL_SERVER_ERROR,
295 Json(ErrorResponse {
296 error: format!("Failed to serialize: {e}"),
297 }),
298 )
299 })
300}
301
302pub(super) fn require_vault(
303 url: Option<&str>,
304) -> Result<&str, (StatusCode, Json<ErrorResponse>)> {
305 url.ok_or_else(|| {
306 (
307 StatusCode::NOT_IMPLEMENTED,
308 Json(ErrorResponse {
309 error: "vault_base_url not configured on this server".into(),
310 }),
311 )
312 })
313}
314
315pub(super) fn require_k8s_url(
316 url: Option<&str>,
317) -> Result<&str, (StatusCode, Json<ErrorResponse>)> {
318 url.ok_or_else(|| {
319 (
320 StatusCode::NOT_IMPLEMENTED,
321 Json(ErrorResponse {
322 error: "k8s_api_url not configured on this server".into(),
323 }),
324 )
325 })
326}
327
328pub(super) fn validate_repo_list_lengths(
329 repo_names: &[String],
330 repo_last_commit_ids: &[String],
331) -> Result<(), (StatusCode, Json<ErrorResponse>)> {
332 if repo_names.len() != repo_last_commit_ids.len() {
333 return Err((
334 StatusCode::BAD_REQUEST,
335 Json(ErrorResponse {
336 error: format!(
337 "repo_names ({}) and repo_last_commit_ids ({}) must have the same length",
338 repo_names.len(),
339 repo_last_commit_ids.len()
340 ),
341 }),
342 ));
343 }
344 Ok(())
345}
346
347pub(super) fn default_true() -> bool {
348 true
349}
350
351pub(super) fn parse_iso_datetime(
352 field: &str,
353 value: &str,
354) -> Result<chrono::NaiveDateTime, (StatusCode, Json<ErrorResponse>)> {
355 chrono::NaiveDateTime::parse_from_str(value, "%Y-%m-%dT%H:%M:%S").map_err(
356 |e| {
357 (
358 StatusCode::BAD_REQUEST,
359 Json(ErrorResponse {
360 error: format!("Invalid '{field}' datetime '{value}': {e}"),
361 }),
362 )
363 },
364 )
365}
366
367#[derive(Serialize, ToSchema)]
373pub struct ErrorResponse {
374 pub error: String,
377}
378
379#[utoipa::path(get, path = "/health", tag = "system",
385 responses(
386 (status = 200, description = "Server is healthy"),
387 )
388)]
389#[tracing::instrument(skip_all)]
390pub async fn health() -> impl IntoResponse {
391 Json(serde_json::json!({ "status": "ok" }))
392}
393
394async fn resolve_xnames_from_request(
401 backend: &crate::manta_backend_dispatcher::StaticBackendDispatcher,
402 token: &str,
403 xnames_expression: Option<&str>,
404 hsm_group: Option<&str>,
405) -> Result<Vec<String>, (StatusCode, Json<ErrorResponse>)> {
406 if let Some(expr) = xnames_expression
407 && !expr.is_empty()
408 {
409 return crate::server::common::node_ops::resolve_hosts_expression(
410 backend, token, expr, false,
411 )
412 .await
413 .map_err(display_error);
414 }
415 if let Some(group) = hsm_group {
416 return crate::server::common::node_ops::resolve_target_nodes(
417 backend,
418 token,
419 None,
420 Some(group),
421 None,
422 )
423 .await
424 .map_err(display_error);
425 }
426 Err((
427 StatusCode::BAD_REQUEST,
428 Json(ErrorResponse {
429 error: "At least one of 'xnames' or 'hsm_group' must be provided"
430 .to_string(),
431 }),
432 ))
433}