1use platform_core::{AppError, ErrorCode, RequestContext, StoryDisplayDescriptor};
7use platform_http::{ApiErrorResponse, ApiOpenApiRouter, OpenApiRouter, routes};
8use platform_module::{
9 AdminActionSource, AdminDataSource, AdminQuerySource, AdminSchema, AdminSurface,
10 ConsoleContribution, ConsoleSlot, ConsoleSurface, EventSurface, LifecycleSurface,
11 ModuleHttpRoute, ModuleLoadStatus, ModuleSource, RuntimeSurface,
12};
13use std::sync::{Arc, OnceLock, RwLock};
14use std::time::Instant;
15
16mod dto;
17mod handlers;
18
19pub use dto::*;
20#[allow(clippy::wildcard_imports)]
21use handlers::*;
22
23#[derive(Clone, Debug)]
26pub struct AdminModule {
27 pub module_name: String,
29 pub source: ModuleSource,
31 pub load_status: ModuleLoadStatus,
34 pub schema: AdminSchema,
36 pub admin: Option<AdminSurface>,
38 pub listed_in_schema: bool,
42 pub data_source: Option<Arc<dyn AdminDataSource>>,
45 pub action_source: Option<Arc<dyn AdminActionSource>>,
47 pub query_source: Option<Arc<dyn AdminQuerySource>>,
49}
50
51#[derive(Clone, Debug)]
54pub struct AdminModuleMetadata {
55 pub module_name: String,
57 pub source: ModuleSource,
59 pub load_status: ModuleLoadStatus,
61 pub http_routes: Vec<ModuleHttpRoute>,
64 pub runtime: Option<RuntimeSurface>,
67 pub events: Option<EventSurface>,
70 pub lifecycle: Option<LifecycleSurface>,
73 pub console: Vec<ConsoleSurface>,
75 pub console_slots: Vec<ConsoleSlot>,
77 pub console_contributions: Vec<ConsoleContribution>,
79 pub story_display: Vec<StoryDisplayDescriptor>,
82 pub capabilities: Vec<String>,
84 pub dependencies: Vec<String>,
86 pub admin: Option<AdminSurface>,
89 pub source_diagnostics: Option<AdminModuleSourceDiagnostics>,
92}
93
94#[derive(Clone, Debug)]
95pub enum AdminModuleSourceDiagnostics {
96 Remote(AdminRemoteModuleDiagnostics),
97}
98
99#[derive(Clone, Debug)]
100pub struct AdminRemoteModuleDiagnostics {
101 pub transport: String,
102 pub base_url: String,
103 pub manifest_url: String,
104 pub timeout_ms: u64,
105 pub auth_configured: bool,
106 pub load_duration_ms: Option<u64>,
107 pub last_checked_at: Option<String>,
108 pub last_load_error: Option<String>,
109}
110
111#[derive(Clone, Debug, Default)]
112struct AdminModuleMetadataSnapshot {
113 modules: Vec<AdminModuleMetadata>,
114 refreshed_at: Option<String>,
115 refresh_error: Option<String>,
116 refresh_history: Vec<AdminModuleMetadataRefreshRecord>,
117}
118
119#[derive(Clone, Debug)]
120pub struct AdminModuleMetadataRefreshRecord {
121 pub id: String,
122 pub status: AdminModuleMetadataRefreshStatus,
123 pub started_at: String,
124 pub completed_at: String,
125 pub duration_ms: u64,
126 pub module_count: usize,
127 pub error: Option<String>,
128 pub module_results: Vec<AdminModuleMetadataRefreshModuleResult>,
129}
130
131#[derive(Clone, Debug)]
132pub struct AdminModuleMetadataRefreshModuleResult {
133 pub module_name: String,
134 pub source: ModuleSource,
135 pub status: AdminModuleMetadataRefreshModuleStatus,
136 pub duration_ms: Option<u64>,
137 pub endpoint: Option<String>,
138 pub error: Option<String>,
139}
140
141#[derive(Clone, Copy, Debug)]
142pub enum AdminModuleMetadataRefreshModuleStatus {
143 Loaded,
144 Error,
145}
146
147#[derive(Clone, Copy, Debug)]
148pub enum AdminModuleMetadataRefreshStatus {
149 Success,
150 Error,
151}
152
153static ADMIN_REGISTRY: OnceLock<RwLock<Vec<AdminModule>>> = OnceLock::new();
154static ADMIN_METADATA_REGISTRY: OnceLock<RwLock<AdminModuleMetadataSnapshot>> = OnceLock::new();
155static ADMIN_REFRESHER: OnceLock<RwLock<Option<Arc<dyn AdminModuleRefresher>>>> = OnceLock::new();
156static ADMIN_METADATA_REFRESHER: OnceLock<RwLock<Option<Arc<dyn AdminModuleMetadataRefresher>>>> =
157 OnceLock::new();
158
159#[async_trait::async_trait]
160pub trait AdminModuleRefresher: Send + Sync {
161 async fn refresh_admin_modules(&self) -> platform_core::AppResult<Vec<AdminModule>>;
162}
163
164#[async_trait::async_trait]
165pub trait AdminModuleMetadataRefresher: Send + Sync {
166 async fn refresh_admin_module_metadata(
167 &self,
168 ) -> platform_core::AppResult<Vec<AdminModuleMetadata>>;
169}
170
171struct StaticAdminModuleRefresher<F>(F);
172struct StaticAdminModuleMetadataRefresher<F>(F);
173
174#[async_trait::async_trait]
175impl<F, Fut> AdminModuleRefresher for StaticAdminModuleRefresher<F>
176where
177 F: Fn() -> Fut + Send + Sync,
178 Fut: std::future::Future<Output = platform_core::AppResult<Vec<AdminModule>>> + Send,
179{
180 async fn refresh_admin_modules(&self) -> platform_core::AppResult<Vec<AdminModule>> {
181 (self.0)().await
182 }
183}
184
185#[async_trait::async_trait]
186impl<F, Fut> AdminModuleMetadataRefresher for StaticAdminModuleMetadataRefresher<F>
187where
188 F: Fn() -> Fut + Send + Sync,
189 Fut: std::future::Future<Output = platform_core::AppResult<Vec<AdminModuleMetadata>>> + Send,
190{
191 async fn refresh_admin_module_metadata(
192 &self,
193 ) -> platform_core::AppResult<Vec<AdminModuleMetadata>> {
194 (self.0)().await
195 }
196}
197
198pub fn install_admin_modules(modules: Vec<AdminModule>) {
202 let registry = ADMIN_REGISTRY.get_or_init(|| RwLock::new(Vec::new()));
203 *registry.write().expect("admin registry lock poisoned") = modules;
204}
205
206pub fn install_admin_module_metadata(modules: Vec<AdminModuleMetadata>) {
208 let registry =
209 ADMIN_METADATA_REGISTRY.get_or_init(|| RwLock::new(AdminModuleMetadataSnapshot::default()));
210 *registry
211 .write()
212 .expect("admin metadata registry lock poisoned") = AdminModuleMetadataSnapshot {
213 modules,
214 refreshed_at: Some(current_timestamp()),
215 refresh_error: None,
216 refresh_history: Vec::new(),
217 };
218}
219
220pub(crate) fn record_admin_module_metadata_refresh_success(
221 modules: Vec<AdminModuleMetadata>,
222 started_at: String,
223 started: Instant,
224) -> AdminModuleMetadataSnapshot {
225 let registry =
226 ADMIN_METADATA_REGISTRY.get_or_init(|| RwLock::new(AdminModuleMetadataSnapshot::default()));
227 let mut snapshot = registry
228 .write()
229 .expect("admin metadata registry lock poisoned");
230 let completed_at = current_timestamp();
231 let record = AdminModuleMetadataRefreshRecord {
232 id: format!(
233 "module_refresh_{}",
234 completed_at.replace([':', '.', '+'], "_")
235 ),
236 status: AdminModuleMetadataRefreshStatus::Success,
237 started_at,
238 completed_at: completed_at.clone(),
239 duration_ms: duration_ms(started),
240 module_count: modules.len(),
241 error: None,
242 module_results: refresh_module_results(&modules),
243 };
244 snapshot.modules = modules;
245 snapshot.refreshed_at = Some(completed_at);
246 snapshot.refresh_error = None;
247 push_refresh_record(&mut snapshot.refresh_history, record);
248 snapshot.clone()
249}
250
251pub fn install_admin_module_refresher(refresher: Arc<dyn AdminModuleRefresher>) {
256 let registry = ADMIN_REFRESHER.get_or_init(|| RwLock::new(None));
257 *registry.write().expect("admin refresher lock poisoned") = Some(refresher);
258}
259
260pub fn install_admin_module_refresh_fn<F, Fut>(refresh: F)
261where
262 F: Fn() -> Fut + Send + Sync + 'static,
263 Fut: std::future::Future<Output = platform_core::AppResult<Vec<AdminModule>>> + Send + 'static,
264{
265 install_admin_module_refresher(Arc::new(StaticAdminModuleRefresher(refresh)));
266}
267
268pub fn install_admin_module_metadata_refresher(refresher: Arc<dyn AdminModuleMetadataRefresher>) {
270 let registry = ADMIN_METADATA_REFRESHER.get_or_init(|| RwLock::new(None));
271 *registry
272 .write()
273 .expect("admin metadata refresher lock poisoned") = Some(refresher);
274}
275
276pub fn install_admin_module_metadata_refresh_fn<F, Fut>(refresh: F)
277where
278 F: Fn() -> Fut + Send + Sync + 'static,
279 Fut: std::future::Future<Output = platform_core::AppResult<Vec<AdminModuleMetadata>>>
280 + Send
281 + 'static,
282{
283 install_admin_module_metadata_refresher(Arc::new(StaticAdminModuleMetadataRefresher(refresh)));
284}
285
286fn admin_modules() -> Vec<AdminModule> {
287 ADMIN_REGISTRY
288 .get()
289 .map(|registry| {
290 registry
291 .read()
292 .expect("admin registry lock poisoned")
293 .clone()
294 })
295 .unwrap_or_default()
296}
297
298fn admin_module_metadata_snapshot() -> AdminModuleMetadataSnapshot {
299 ADMIN_METADATA_REGISTRY
300 .get()
301 .map(|registry| {
302 registry
303 .read()
304 .expect("admin metadata registry lock poisoned")
305 .clone()
306 })
307 .unwrap_or_default()
308}
309
310pub(crate) fn record_admin_module_metadata_refresh_error(
311 error: String,
312 started_at: String,
313 started: Instant,
314) -> AdminModuleMetadataSnapshot {
315 let registry =
316 ADMIN_METADATA_REGISTRY.get_or_init(|| RwLock::new(AdminModuleMetadataSnapshot::default()));
317 let mut snapshot = registry
318 .write()
319 .expect("admin metadata registry lock poisoned");
320 snapshot.refresh_error = Some(error);
321 let completed_at = current_timestamp();
322 let record = AdminModuleMetadataRefreshRecord {
323 id: format!(
324 "module_refresh_{}",
325 completed_at.replace([':', '.', '+'], "_")
326 ),
327 status: AdminModuleMetadataRefreshStatus::Error,
328 started_at,
329 completed_at,
330 duration_ms: duration_ms(started),
331 module_count: snapshot.modules.len(),
332 error: snapshot.refresh_error.clone(),
333 module_results: Vec::new(),
334 };
335 push_refresh_record(&mut snapshot.refresh_history, record);
336 snapshot.clone()
337}
338
339pub(crate) fn current_timestamp() -> String {
340 use platform_core::Clock;
341 platform_core::SystemClock.now().to_rfc3339()
342}
343
344fn duration_ms(started: Instant) -> u64 {
345 u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX)
346}
347
348fn push_refresh_record(
349 history: &mut Vec<AdminModuleMetadataRefreshRecord>,
350 record: AdminModuleMetadataRefreshRecord,
351) {
352 history.insert(0, record);
353 history.truncate(10);
354}
355
356fn refresh_module_results(
357 modules: &[AdminModuleMetadata],
358) -> Vec<AdminModuleMetadataRefreshModuleResult> {
359 modules
360 .iter()
361 .map(|module| {
362 let remote = match &module.source_diagnostics {
363 Some(AdminModuleSourceDiagnostics::Remote(remote)) => Some(remote),
364 None => None,
365 };
366 AdminModuleMetadataRefreshModuleResult {
367 module_name: module.module_name.clone(),
368 source: module.source,
369 status: match module.load_status {
370 ModuleLoadStatus::Loaded => AdminModuleMetadataRefreshModuleStatus::Loaded,
371 ModuleLoadStatus::Error { .. } => AdminModuleMetadataRefreshModuleStatus::Error,
372 },
373 duration_ms: remote.and_then(|diagnostics| diagnostics.load_duration_ms),
374 endpoint: remote.map(|diagnostics| diagnostics.base_url.clone()),
375 error: match &module.load_status {
376 ModuleLoadStatus::Loaded => {
377 remote.and_then(|diagnostics| diagnostics.last_load_error.clone())
378 }
379 ModuleLoadStatus::Error { message } => Some(message.clone()),
380 },
381 }
382 })
383 .collect()
384}
385
386fn admin_refresher() -> Option<Arc<dyn AdminModuleRefresher>> {
387 ADMIN_REFRESHER.get().and_then(|registry| {
388 registry
389 .read()
390 .expect("admin refresher lock poisoned")
391 .clone()
392 })
393}
394
395fn admin_metadata_refresher() -> Option<Arc<dyn AdminModuleMetadataRefresher>> {
396 ADMIN_METADATA_REFRESHER.get().and_then(|registry| {
397 registry
398 .read()
399 .expect("admin metadata refresher lock poisoned")
400 .clone()
401 })
402}
403
404fn find_module(module: &str, ctx: &RequestContext) -> Result<AdminModule, ApiErrorResponse> {
405 admin_modules()
406 .into_iter()
407 .find(|m| m.module_name == module)
408 .ok_or_else(|| {
409 ApiErrorResponse::with_context(
410 AppError::new(ErrorCode::NotFound, format!("unknown module: {module}")),
411 ctx,
412 )
413 })
414}
415
416fn find_loaded_module(module: &str, ctx: &RequestContext) -> Result<AdminModule, ApiErrorResponse> {
417 let admin_module = find_module(module, ctx)?;
418 if admin_module.data_source.is_some() {
419 Ok(admin_module)
420 } else {
421 Err(ApiErrorResponse::with_context(
422 AppError::new(
423 ErrorCode::ExternalDependency,
424 format!("module {module} is not loaded"),
425 )
426 .retryable(),
427 ctx,
428 ))
429 }
430}
431
432fn find_loaded_action_module(
433 module: &str,
434 ctx: &RequestContext,
435) -> Result<AdminModule, ApiErrorResponse> {
436 let admin_module = find_module(module, ctx)?;
437 if matches!(admin_module.load_status, ModuleLoadStatus::Loaded) {
438 Ok(admin_module)
439 } else {
440 Err(ApiErrorResponse::with_context(
441 AppError::new(
442 ErrorCode::ExternalDependency,
443 format!("module {module} is not loaded"),
444 )
445 .retryable(),
446 ctx,
447 ))
448 }
449}
450
451fn find_loaded_query_module(
452 module: &str,
453 ctx: &RequestContext,
454) -> Result<AdminModule, ApiErrorResponse> {
455 let admin_module = find_module(module, ctx)?;
456 if matches!(admin_module.load_status, ModuleLoadStatus::Loaded) {
457 Ok(admin_module)
458 } else {
459 Err(ApiErrorResponse::with_context(
460 AppError::new(
461 ErrorCode::ExternalDependency,
462 format!("module {module} is not loaded"),
463 )
464 .retryable(),
465 ctx,
466 ))
467 }
468}
469
470pub fn router() -> ApiOpenApiRouter {
472 OpenApiRouter::new()
473 .routes(routes!(list_modules))
474 .routes(routes!(refresh_modules))
475 .routes(routes!(available_modules))
476 .routes(routes!(service_modules))
477 .routes(routes!(service_system))
478 .routes(routes!(service_system_drift))
479 .routes(routes!(service_system_release_train))
480 .routes(routes!(service_system_runbooks))
481 .routes(routes!(launchpad))
482 .routes(routes!(launchpad_doctor))
483 .routes(routes!(launchpad_proof))
484 .routes(routes!(launchpad_change_plan))
485 .routes(routes!(install_available_module))
486 .routes(routes!(uninstall_available_module))
487 .routes(routes!(module_registry_snapshot))
488 .routes(routes!(list_schemas))
489 .routes(routes!(refresh_schemas))
490 .routes(routes!(invoke_action))
491 .routes(routes!(query_value))
492 .routes(routes!(list_records))
493 .routes(routes!(get_record))
494}