manta_server/server/handlers/
configuration.rs

1//! CFS-configuration handlers.
2//!
3//! - `GET    /api/v1/configurations` → [`get_configurations`] —
4//!   wraps `service::configuration::get_configurations_with_analysis`;
5//!   each row carries a `safe_to_delete` verdict derived from CFS
6//!   components only.
7//! - `DELETE /api/v1/configurations` → [`delete_configurations`] —
8//!   wraps `service::configuration::{get_deletion_candidates,
9//!   delete_configurations_and_derivatives}`; with `?dry_run=true`,
10//!   returns the candidate set without deleting.
11
12use axum::{Json, extract::Query, http::StatusCode, response::IntoResponse};
13
14use super::{
15  ErrorResponse, RequestCtx, SiteHeader, parse_iso_datetime, serialize_or_500,
16  to_handler_error,
17};
18use crate::service;
19use manta_shared::types::api::configuration_analysis::ConfigurationAnalysis;
20
21// ---------------------------------------------------------------------------
22// GET /api/v1/configurations
23// ---------------------------------------------------------------------------
24
25pub use manta_shared::types::api::queries::{
26  ConfigurationQuery, DeleteConfigurationsQuery,
27};
28
29/// GET /configurations — list CFS configurations with optional
30/// name/pattern/group filters. Every row carries the full
31/// `CfsConfigurationResponse` plus a `safe_to_delete` verdict
32/// derived from CFS components only (a configuration is unsafe iff
33/// some component lists it as `desired_config`).
34#[utoipa::path(get, path = "/configurations", tag = "configurations",
35  params(ConfigurationQuery, SiteHeader),
36  security(("bearerAuth" = [])),
37  responses(
38    (status = 200, description = "Configurations with components-only safe_to_delete verdict",
39     body = Vec<ConfigurationAnalysis>),
40    (status = 401, description = "Unauthorized",           body = ErrorResponse),
41    (status = 500, description = "Internal error",         body = ErrorResponse),
42  )
43)]
44#[tracing::instrument(skip_all)]
45pub async fn get_configurations(
46  ctx: RequestCtx,
47  Query(q): Query<ConfigurationQuery>,
48) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
49  let infra = ctx.infra();
50
51  let params = service::configuration::GetConfigurationParams {
52    name: q.name,
53    pattern: q.pattern,
54    group_name: q.hsm_group,
55    settings_hsm_group_name: None,
56    since: None,
57    until: None,
58    limit: q.limit,
59  };
60
61  let rows = service::configuration::get_configurations_with_analysis(
62    &infra, &ctx.token, &params,
63  )
64  .await
65  .map_err(to_handler_error)?;
66
67  Ok(Json(rows))
68}
69
70// ---------------------------------------------------------------------------
71// DELETE /api/v1/configurations — with ?pattern=...&since=...&until=...&dry_run=true
72// ---------------------------------------------------------------------------
73
74/// `DELETE /api/v1/configurations` — delete CFS configurations and all derived artifacts.
75#[utoipa::path(delete, path = "/configurations", tag = "configurations",
76  params(DeleteConfigurationsQuery, SiteHeader),
77  security(("bearerAuth" = [])),
78  responses(
79    // dry_run/real result union — kept as Value until the union shape is formalised
80    (status = 200, description = "Configurations deleted or preview", body = serde_json::Value),
81    (status = 400, description = "Bad request",                       body = ErrorResponse),
82    (status = 401, description = "Unauthorized",                      body = ErrorResponse),
83    (status = 500, description = "Internal error",                    body = ErrorResponse),
84  )
85)]
86#[tracing::instrument(skip_all)]
87pub async fn delete_configurations(
88  ctx: RequestCtx,
89  Query(q): Query<DeleteConfigurationsQuery>,
90) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
91  tracing::info!("delete_configurations dry_run={}", q.dry_run);
92  let infra = ctx.infra();
93
94  let since = q
95    .since
96    .as_deref()
97    .map(|s| parse_iso_datetime("since", s))
98    .transpose()?;
99  let until = q
100    .until
101    .as_deref()
102    .map(|s| parse_iso_datetime("until", s))
103    .transpose()?;
104
105  let candidates = service::configuration::get_deletion_candidates(
106    &infra,
107    &ctx.token,
108    None,
109    q.pattern.as_deref(),
110    since,
111    until,
112  )
113  .await
114  .map_err(to_handler_error)?;
115
116  if q.dry_run {
117    return Ok((StatusCode::OK, Json(serialize_or_500(&candidates)?)));
118  }
119
120  service::configuration::delete_configurations_and_derivatives(
121    &infra,
122    &ctx.token,
123    &candidates,
124  )
125  .await
126  .map_err(to_handler_error)?;
127
128  Ok((
129    StatusCode::OK,
130    Json(serde_json::json!({
131      "deleted_configurations": candidates.configuration_names,
132      "deleted_images": candidates.image_ids,
133    })),
134  ))
135}
136
137// ===========================================================================
138// BATCH A — MEDIUM-COMPLEXITY WRITE ENDPOINTS
139// ===========================================================================