1use std::convert::Infallible;
16
17use axum::{
18 Json,
19 extract::{Path, Query},
20 http::StatusCode,
21 response::{
22 IntoResponse,
23 sse::{Event, KeepAlive, Sse},
24 },
25};
26use futures::{AsyncBufReadExt, StreamExt};
27use manta_backend_dispatcher::types::{K8sAuth, K8sDetails};
28
29use super::{
30 ErrorResponse, RequestCtx, SiteHeader, require_k8s_url, require_vault,
31 serialize_or_500, to_handler_error, validate_repo_list_lengths,
32};
33use crate::service;
34
35pub use manta_shared::types::api::queries::{DeleteSessionQuery, SessionQuery};
40
41#[utoipa::path(get, path = "/sessions", tag = "sessions",
43 params(SessionQuery, SiteHeader),
44 security(("bearerAuth" = [])),
45 responses(
46 (status = 200, description = "List of sessions", body = serde_json::Value),
49 (status = 400, description = "Bad request", body = ErrorResponse),
50 (status = 401, description = "Unauthorized", body = ErrorResponse),
51 (status = 500, description = "Internal error", body = ErrorResponse),
52 )
53)]
54#[tracing::instrument(skip_all)]
55pub async fn get_sessions(
56 ctx: RequestCtx,
57 Query(q): Query<SessionQuery>,
58) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
59 let infra = ctx.infra();
60
61 let xnames: Vec<String> = q
62 .xnames
63 .map(|s| {
64 s.split(',')
65 .map(str::trim)
66 .filter(|v| !v.is_empty())
67 .map(str::to_string)
68 .collect()
69 })
70 .unwrap_or_default();
71
72 let params = service::session::GetSessionParams {
73 group: q.hsm_group,
74 xnames,
75 min_age: q.min_age,
76 max_age: q.max_age,
77 session_type: q.session_type,
78 status: q.status,
79 name: q.name,
80 limit: q.limit,
81 };
82
83 let sessions = service::session::get_sessions(&infra, &ctx.token, ¶ms)
84 .await
85 .map_err(to_handler_error)?;
86
87 Ok(Json(sessions))
88}
89
90#[utoipa::path(delete, path = "/sessions/{name}", tag = "sessions",
96 params(("name" = String, Path, description = "Session name"), DeleteSessionQuery, SiteHeader),
97 security(("bearerAuth" = [])),
98 responses(
99 (status = 200, description = "Session deleted or deletion preview", body = serde_json::Value),
101 (status = 401, description = "Unauthorized", body = ErrorResponse),
102 (status = 404, description = "Not found", body = ErrorResponse),
103 (status = 500, description = "Internal error", body = ErrorResponse),
104 )
105)]
106#[tracing::instrument(skip_all)]
107pub async fn delete_session(
108 ctx: RequestCtx,
109 Path(name): Path<String>,
110 Query(q): Query<DeleteSessionQuery>,
111) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
112 tracing::info!("delete_session name={} dry_run={}", name, q.dry_run);
113 let infra = ctx.infra();
114
115 let deletion_ctx =
116 service::session::prepare_session_deletion(&infra, &ctx.token, &name, None)
117 .await
118 .map_err(to_handler_error)?;
119
120 if q.dry_run {
121 return Ok((StatusCode::OK, Json(serialize_or_500(&deletion_ctx)?)));
122 }
123
124 service::session::execute_session_deletion(
125 &infra,
126 &ctx.token,
127 &deletion_ctx,
128 false,
129 )
130 .await
131 .map_err(to_handler_error)?;
132
133 Ok((StatusCode::OK, Json(serde_json::json!({ "deleted": name }))))
134}
135
136pub use manta_shared::types::api::session::CreateSessionRequest;
141
142#[utoipa::path(post, path = "/sessions", tag = "sessions",
144 params(SiteHeader),
145 request_body = CreateSessionRequest,
146 security(("bearerAuth" = [])),
147 responses(
148 (status = 201, description = "Session created", body = manta_shared::types::api::responses::CreateSessionResponse),
149 (status = 400, description = "Bad request", body = ErrorResponse),
150 (status = 401, description = "Unauthorized", body = ErrorResponse),
151 (status = 500, description = "Internal error", body = ErrorResponse),
152 (status = 501, description = "Vault not configured", body = ErrorResponse),
153 )
154)]
155#[tracing::instrument(skip_all)]
156pub async fn create_session(
157 ctx: RequestCtx,
158 Json(body): Json<CreateSessionRequest>,
159) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
160 validate_repo_list_lengths(&body.repo_names, &body.repo_last_commit_ids)?;
161 tracing::info!("create_session repos={:?}", body.repo_names);
162 let infra = ctx.infra();
163
164 if let Some(ref hsm_group) = body.hsm_group {
166 service::authorization::validate_user_group_access(
167 &infra, &ctx.token, hsm_group,
168 )
169 .await
170 .map_err(to_handler_error)?;
171 }
172
173 if let Some(ref ansible_limit) = body.ansible_limit {
176 service::authorization::validate_ansible_limit_membership_access(
177 &infra,
178 &ctx.token,
179 ansible_limit,
180 )
181 .await
182 .map_err(to_handler_error)?;
183 }
184
185 let vault_base_url = require_vault(infra.vault_base_url)?;
186
187 let gitea_token =
188 crate::server::common::vault::http_client::get_shasta_vcs_token(
189 &ctx.token,
190 vault_base_url,
191 infra.site_name,
192 )
193 .await
194 .map_err(to_handler_error)?;
195
196 let repo_name_refs: Vec<&str> = body
197 .repo_names
198 .iter()
199 .map(std::string::String::as_str)
200 .collect();
201 let repo_commit_refs: Vec<&str> = body
202 .repo_last_commit_ids
203 .iter()
204 .map(std::string::String::as_str)
205 .collect();
206
207 let (session_name, config_name) = service::session::create_cfs_session(
208 &infra,
209 &ctx.token,
210 &gitea_token,
211 service::session::CreateCfsSessionParams {
212 cfs_conf_sess_name: body.cfs_conf_sess_name.as_deref(),
213 playbook_yaml_file_name: body.playbook_yaml_file_name.as_deref(),
214 group: body.hsm_group.as_deref(),
215 repo_names: &repo_name_refs,
216 repo_last_commit_ids: &repo_commit_refs,
217 ansible_limit: body.ansible_limit.as_deref(),
218 ansible_verbosity: body.ansible_verbosity.as_deref(),
219 ansible_passthrough: body.ansible_passthrough.as_deref(),
220 },
221 )
222 .await
223 .map_err(to_handler_error)?;
224
225 Ok((
226 StatusCode::CREATED,
227 Json(serde_json::json!({
228 "session_name": session_name,
229 "configuration_name": config_name,
230 })),
231 ))
232}
233
234pub use manta_shared::types::api::queries::SessionLogsQuery;
239
240#[utoipa::path(get, path = "/sessions/{name}/logs", tag = "sessions",
242 params(("name" = String, Path, description = "Session name"), SessionLogsQuery, SiteHeader),
243 security(("bearerAuth" = [])),
244 responses(
245 (status = 200, description = "SSE log stream"),
246 (status = 401, description = "Unauthorized", body = ErrorResponse),
247 (status = 500, description = "Internal error", body = ErrorResponse),
248 (status = 501, description = "Vault or k8s not configured", body = ErrorResponse),
249 )
250)]
251#[tracing::instrument(skip_all)]
252pub async fn get_session_logs(
253 ctx: RequestCtx,
254 Path(name): Path<String>,
255 Query(q): Query<SessionLogsQuery>,
256) -> Result<
257 Sse<impl futures::Stream<Item = Result<Event, Infallible>>>,
258 (StatusCode, Json<ErrorResponse>),
259> {
260 let infra = ctx.infra();
261
262 let k8s_api_url = require_k8s_url(infra.k8s_api_url)?;
263 let vault_base_url = require_vault(infra.vault_base_url)?;
264
265 service::session::validate_session_access(&infra, &ctx.token, &name)
271 .await
272 .map_err(to_handler_error)?;
273
274 let k8s = K8sDetails {
275 api_url: k8s_api_url.to_string(),
276 authentication: K8sAuth::Vault {
277 base_url: vault_base_url.to_string(),
278 },
279 };
280
281 let logs_stream = service::session::stream_logs(
282 &infra,
283 &ctx.token,
284 &name,
285 q.timestamps,
286 &k8s,
287 )
288 .await
289 .map_err(to_handler_error)?;
290
291 let sse_stream = logs_stream.lines().map(|result| {
292 Ok::<Event, Infallible>(
293 Event::default().data(result.unwrap_or_else(|e| format!("error: {e}"))),
294 )
295 });
296
297 Ok(Sse::new(sse_stream).keep_alive(KeepAlive::default()))
298}