manta_server/server/handlers/
hw_cluster.rs

1//! Hardware-cluster membership and configuration handlers.
2//!
3//! - `POST   /api/v1/hardware-clusters/{target}/members`        → [`add_hw_component`]
4//! - `DELETE /api/v1/hardware-clusters/{target}/members`        → [`delete_hw_component`]
5//! - `POST   /api/v1/hardware-clusters/{target}/configuration`  → [`apply_hw_configuration`]
6//!
7//! All wrap `crate::service::hw_cluster::*`. Each handler runs
8//! `service::authorization::validate_user_group_access` against both
9//! the target cluster and (where applicable) the parent cluster
10//! before mutating state.
11
12use axum::{Json, extract::Path, http::StatusCode, response::IntoResponse};
13
14use super::{ErrorResponse, RequestCtx, SiteHeader, to_handler_error};
15use crate::service;
16
17pub use manta_shared::types::api::hw_cluster::{
18  AddHwComponentRequest, ApplyHwConfigurationRequest, DeleteHwComponentRequest,
19  HwClusterMode,
20};
21
22/// `POST /api/v1/hardware-clusters/{target}/members` — move nodes matching a hardware pattern into a cluster.
23#[utoipa::path(post, path = "/hardware-clusters/{target}/members", tag = "hardware",
24  params(("target" = String, Path, description = "Target cluster name"), SiteHeader),
25  request_body = AddHwComponentRequest,
26  security(("bearerAuth" = [])),
27  responses(
28    // dry_run/real result union — kept as Value until the union shape is formalised
29    (status = 200, description = "Members added or preview", body = serde_json::Value),
30    (status = 401, description = "Unauthorized",             body = ErrorResponse),
31    (status = 500, description = "Internal error",           body = ErrorResponse),
32  )
33)]
34#[tracing::instrument(skip_all)]
35pub async fn add_hw_component(
36  ctx: RequestCtx,
37  Path(target): Path<String>,
38  Json(body): Json<AddHwComponentRequest>,
39) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
40  tracing::info!(
41    "add_hw_component target={} parent={} dry_run={}",
42    target,
43    body.parent_cluster,
44    body.dry_run
45  );
46  let infra = ctx.infra();
47
48  service::authorization::validate_user_group_access(
49    &infra, &ctx.token, &target,
50  )
51  .await
52  .map_err(to_handler_error)?;
53  service::authorization::validate_user_group_access(
54    &infra,
55    &ctx.token,
56    &body.parent_cluster,
57  )
58  .await
59  .map_err(to_handler_error)?;
60
61  let result = crate::service::hw_cluster::add_hw_component(
62    &infra,
63    &ctx.token,
64    &target,
65    &body.parent_cluster,
66    &body.pattern,
67    body.dry_run,
68    body.create_hsm_group,
69  )
70  .await
71  .map_err(to_handler_error)?;
72
73  Ok(Json(serde_json::json!({
74    "dry_run": body.dry_run,
75    "nodes_moved": result.nodes_moved,
76    "target_cluster": target,
77    "target_nodes": result.target_nodes,
78    "parent_cluster": body.parent_cluster,
79    "parent_nodes": result.parent_nodes,
80  })))
81}
82
83// ---------------------------------------------------------------------------
84// DELETE /api/v1/hardware-clusters/{target}/members
85// ---------------------------------------------------------------------------
86
87/// `DELETE /api/v1/hardware-clusters/{target}/members` — move nodes back to parent cluster by hardware pattern.
88#[utoipa::path(delete, path = "/hardware-clusters/{target}/members", tag = "hardware",
89  params(("target" = String, Path, description = "Target cluster name"), SiteHeader),
90  request_body = DeleteHwComponentRequest,
91  security(("bearerAuth" = [])),
92  responses(
93    // dry_run/real result union — kept as Value until the union shape is formalised
94    (status = 200, description = "Members removed or preview", body = serde_json::Value),
95    (status = 401, description = "Unauthorized",               body = ErrorResponse),
96    (status = 500, description = "Internal error",             body = ErrorResponse),
97  )
98)]
99#[tracing::instrument(skip_all)]
100pub async fn delete_hw_component(
101  ctx: RequestCtx,
102  Path(target): Path<String>,
103  Json(body): Json<DeleteHwComponentRequest>,
104) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
105  tracing::info!(
106    "delete_hw_component target={} parent={} dry_run={}",
107    target,
108    body.parent_cluster,
109    body.dry_run
110  );
111  let infra = ctx.infra();
112
113  service::authorization::validate_user_group_access(
114    &infra, &ctx.token, &target,
115  )
116  .await
117  .map_err(to_handler_error)?;
118  service::authorization::validate_user_group_access(
119    &infra,
120    &ctx.token,
121    &body.parent_cluster,
122  )
123  .await
124  .map_err(to_handler_error)?;
125
126  let result = crate::service::hw_cluster::delete_hw_component(
127    &infra,
128    &ctx.token,
129    &target,
130    &body.parent_cluster,
131    &body.pattern,
132    body.dry_run,
133    body.delete_hsm_group,
134  )
135  .await
136  .map_err(to_handler_error)?;
137
138  Ok(Json(serde_json::json!({
139    "dry_run": body.dry_run,
140    "nodes_moved": result.nodes_moved,
141    "target_cluster": target,
142    "target_nodes": result.target_nodes,
143    "parent_cluster": body.parent_cluster,
144    "parent_nodes": result.parent_nodes,
145  })))
146}
147
148// ---------------------------------------------------------------------------
149// POST /api/v1/hardware-clusters/{target}/configuration
150// ---------------------------------------------------------------------------
151
152/// `POST /api/v1/hardware-clusters/{target}/configuration` — pin or unpin nodes between clusters by hardware pattern.
153#[utoipa::path(post, path = "/hardware-clusters/{target}/configuration", tag = "hardware",
154  params(("target" = String, Path, description = "Target cluster name"), SiteHeader),
155  request_body = ApplyHwConfigurationRequest,
156  security(("bearerAuth" = [])),
157  responses(
158    // dry_run/real result union — kept as Value until the union shape is formalised
159    (status = 200, description = "Configuration applied or preview", body = serde_json::Value),
160    (status = 401, description = "Unauthorized",                     body = ErrorResponse),
161    (status = 500, description = "Internal error",                   body = ErrorResponse),
162  )
163)]
164#[tracing::instrument(skip_all)]
165pub async fn apply_hw_configuration(
166  ctx: RequestCtx,
167  Path(target): Path<String>,
168  Json(body): Json<ApplyHwConfigurationRequest>,
169) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
170  tracing::info!(
171    "apply_hw_configuration target={} parent={} dry_run={}",
172    target,
173    body.parent_cluster,
174    body.dry_run
175  );
176  let infra = ctx.infra();
177
178  service::authorization::validate_user_group_access(
179    &infra, &ctx.token, &target,
180  )
181  .await
182  .map_err(to_handler_error)?;
183  service::authorization::validate_user_group_access(
184    &infra,
185    &ctx.token,
186    &body.parent_cluster,
187  )
188  .await
189  .map_err(to_handler_error)?;
190
191  let result = crate::service::hw_cluster::apply_hw_configuration(
192    &infra,
193    &ctx.token,
194    crate::service::hw_cluster::ApplyHwConfigurationParams {
195      mode: body.mode,
196      target_group_name: &target,
197      parent_group_name: &body.parent_cluster,
198      pattern: &body.pattern,
199      dryrun: body.dry_run,
200      create_target_group: body.create_target_hsm_group,
201      delete_empty_parent_group: body.delete_empty_parent_hsm_group,
202    },
203  )
204  .await
205  .map_err(to_handler_error)?;
206
207  Ok(Json(serde_json::json!({
208    "dry_run": body.dry_run,
209    "target_cluster": target,
210    "target_nodes": result.target_nodes,
211    "parent_cluster": body.parent_cluster,
212    "parent_nodes": result.parent_nodes,
213  })))
214}