systemprompt_api/routes/admin/services/
mod.rs1mod refresh;
20mod status;
21
22use std::sync::Arc;
23
24use axum::routing::{get, post};
25use axum::{Extension, Router};
26use chrono::{DateTime, Utc};
27use serde::{Deserialize, Serialize};
28use systemprompt_loader::services_root::{ActiveServicesRoot, ServicesProvenance};
29use systemprompt_manifest::services::bundle::ServicesBundleState;
30use systemprompt_runtime::AppContext;
31
32pub use refresh::{RefreshQuery, ServicesRefresh, process_refresh_lock, refresh};
33pub use status::build_status;
34
35pub(super) fn router() -> Router<AppContext> {
36 Router::new()
37 .route("/status", get(status::status))
38 .route("/refresh", post(refresh))
39 .layer(Extension(process_refresh_lock()))
40}
41
42#[derive(Debug, Clone, Default)]
48pub struct RefreshLock(Arc<tokio::sync::Mutex<()>>);
49
50impl RefreshLock {
51 pub fn try_acquire(&self) -> Option<tokio::sync::OwnedMutexGuard<()>> {
52 Arc::clone(&self.0).try_lock_owned().ok()
53 }
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57pub struct SourceView {
58 pub name: String,
59 pub digest: String,
60 pub version: String,
61 pub content_hash: String,
62 pub fetched_at: DateTime<Utc>,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
66pub struct ProvenanceView {
67 pub kind: String,
68
69 #[serde(skip_serializing_if = "Option::is_none")]
70 pub composed_hash: Option<String>,
71
72 #[serde(skip_serializing_if = "Option::is_none")]
73 pub error: Option<String>,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
77pub struct ServicesStatusResponse {
78 pub active_root: String,
79 pub provenance: ProvenanceView,
80 pub sources: Vec<SourceView>,
81
82 #[serde(skip_serializing_if = "Option::is_none")]
83 pub last_reconciled_hash: Option<String>,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
87pub struct ServicesRefreshResponse {
88 pub changed: bool,
89
90 #[serde(skip_serializing_if = "Option::is_none")]
91 pub composed_hash: Option<String>,
92
93 pub sources: Vec<SourceView>,
94 pub reconciled: bool,
95 pub restart_recommended: bool,
96 pub restarting: bool,
97}
98
99pub fn source_views(state: &ServicesBundleState) -> Vec<SourceView> {
100 state
101 .sources
102 .iter()
103 .map(|(name, s)| SourceView {
104 name: name.clone(),
105 digest: s.digest.clone(),
106 version: s.version.clone(),
107 content_hash: s.content_hash.clone(),
108 fetched_at: s.fetched_at,
109 })
110 .collect()
111}
112
113pub fn provenance_view(provenance: &ServicesProvenance) -> ProvenanceView {
114 match provenance {
115 ServicesProvenance::Bundled => ProvenanceView {
116 kind: "bundled".to_owned(),
117 composed_hash: None,
118 error: None,
119 },
120 ServicesProvenance::Fetched { composed_hash, .. } => ProvenanceView {
121 kind: "fetched".to_owned(),
122 composed_hash: Some(composed_hash.clone()),
123 error: None,
124 },
125 ServicesProvenance::LastGood {
126 composed_hash,
127 error,
128 } => ProvenanceView {
129 kind: "last_good".to_owned(),
130 composed_hash: Some(composed_hash.clone()),
131 error: Some(error.clone()),
132 },
133 ServicesProvenance::BundledFallback { error } => ProvenanceView {
134 kind: "bundled".to_owned(),
135 composed_hash: None,
136 error: Some(error.clone()),
137 },
138 }
139}
140
141pub const fn composed_hash_of(active: &ActiveServicesRoot) -> Option<&str> {
142 match &active.provenance {
143 ServicesProvenance::Fetched { composed_hash, .. }
144 | ServicesProvenance::LastGood { composed_hash, .. } => Some(composed_hash.as_str()),
145 ServicesProvenance::Bundled | ServicesProvenance::BundledFallback { .. } => None,
146 }
147}