Skip to main content

greentic_setup/admin/
routes.rs

1//! Admin API request/response types for bundle lifecycle management.
2//!
3//! These types define the contract between the admin API and consumers.
4//! The actual HTTP routing is implemented in the consuming crate
5//! (e.g. greentic-operator), which maps these to Axum handlers.
6
7use std::path::PathBuf;
8
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11
12use crate::plan::{PackRemoveSelection, TenantSelection};
13
14// ── Bundle deployment ───────────────────────────────────────────────────────
15
16/// Request to deploy a new bundle or upgrade an existing one.
17#[derive(Clone, Debug, Serialize, Deserialize)]
18pub struct BundleDeployRequest {
19    /// Target bundle path on the server.
20    pub bundle_path: PathBuf,
21    /// Optional display name for the bundle.
22    #[serde(default)]
23    pub bundle_name: Option<String>,
24    /// Pack references to resolve and install.
25    #[serde(default)]
26    pub pack_refs: Vec<String>,
27    /// Tenant selections with allow rules.
28    #[serde(default)]
29    pub tenants: Vec<TenantSelection>,
30    /// Pre-collected QA answers (provider_id → answers map).
31    #[serde(default)]
32    pub answers: Value,
33    /// If true, only plan without executing.
34    #[serde(default)]
35    pub dry_run: bool,
36}
37
38/// Request to update an existing bundle.
39#[derive(Clone, Debug, Serialize, Deserialize)]
40pub struct BundleUpdateRequest {
41    /// Target bundle path on the server.
42    pub bundle_path: PathBuf,
43    /// Optional display name for the bundle.
44    #[serde(default)]
45    pub bundle_name: Option<String>,
46    /// Pack references to resolve and install.
47    #[serde(default)]
48    pub pack_refs: Vec<String>,
49    /// Tenant selections with allow rules.
50    #[serde(default)]
51    pub tenants: Vec<TenantSelection>,
52    /// Pre-collected QA answers (provider_id → answers map).
53    #[serde(default)]
54    pub answers: Value,
55    /// If true, only plan without executing.
56    #[serde(default)]
57    pub dry_run: bool,
58}
59
60/// Request to start a managed bundle runtime.
61#[derive(Clone, Debug, Serialize, Deserialize)]
62pub struct BundleStartRequest {
63    /// Target bundle path.
64    pub bundle_path: PathBuf,
65}
66
67/// Request to stop a managed bundle runtime.
68#[derive(Clone, Debug, Serialize, Deserialize)]
69pub struct BundleStopRequest {
70    /// Target bundle path.
71    pub bundle_path: PathBuf,
72}
73
74/// Request to add an admin client CN to the runtime allowlist.
75#[derive(Clone, Debug, Serialize, Deserialize)]
76pub struct AdminClientAddRequest {
77    /// Target bundle path.
78    pub bundle_path: PathBuf,
79    /// Client CN to allow.
80    pub client_cn: String,
81}
82
83/// Request to remove an admin client CN from the runtime allowlist.
84#[derive(Clone, Debug, Serialize, Deserialize)]
85pub struct AdminClientRemoveRequest {
86    /// Target bundle path.
87    pub bundle_path: PathBuf,
88    /// Client CN to remove.
89    pub client_cn: String,
90}
91
92/// Request to remove components from a bundle.
93#[derive(Clone, Debug, Serialize, Deserialize)]
94pub struct BundleRemoveRequest {
95    /// Target bundle path.
96    pub bundle_path: PathBuf,
97    /// Packs to remove.
98    #[serde(default)]
99    pub packs: Vec<PackRemoveSelection>,
100    /// Provider IDs to remove.
101    #[serde(default)]
102    pub providers: Vec<String>,
103    /// Tenants/teams to remove.
104    #[serde(default)]
105    pub tenants: Vec<TenantSelection>,
106    /// If true, only plan without executing.
107    #[serde(default)]
108    pub dry_run: bool,
109}
110
111// ── QA setup ────────────────────────────────────────────────────────────────
112
113/// Request to get the QA FormSpec for a pack.
114#[derive(Clone, Debug, Serialize, Deserialize)]
115pub struct QaSpecRequest {
116    /// Bundle path.
117    pub bundle_path: PathBuf,
118    /// Provider ID to get spec for.
119    pub provider_id: String,
120    /// Locale for i18n resolution.
121    #[serde(default = "default_locale")]
122    pub locale: String,
123}
124
125/// Request to validate QA answers against a FormSpec.
126#[derive(Clone, Debug, Serialize, Deserialize)]
127pub struct QaValidateRequest {
128    /// Bundle path.
129    pub bundle_path: PathBuf,
130    /// Provider ID.
131    pub provider_id: String,
132    /// Answers to validate.
133    pub answers: Value,
134}
135
136/// Request to submit and persist QA answers.
137#[derive(Clone, Debug, Serialize, Deserialize)]
138pub struct QaSubmitRequest {
139    /// Bundle path.
140    pub bundle_path: PathBuf,
141    /// Provider ID.
142    pub provider_id: String,
143    /// Tenant ID.
144    pub tenant: String,
145    /// Team ID.
146    #[serde(default)]
147    pub team: Option<String>,
148    /// Answers to persist.
149    pub answers: Value,
150    /// Whether to trigger a hot reload after persisting.
151    #[serde(default)]
152    pub reload: bool,
153}
154
155// ── Responses ───────────────────────────────────────────────────────────────
156
157/// Generic admin API response wrapper.
158#[derive(Clone, Debug, Serialize, Deserialize)]
159pub struct AdminResponse<T: Serialize> {
160    pub success: bool,
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub data: Option<T>,
163    #[serde(skip_serializing_if = "Option::is_none")]
164    pub error: Option<String>,
165}
166
167impl<T: Serialize> AdminResponse<T> {
168    pub fn ok(data: T) -> Self {
169        Self {
170            success: true,
171            data: Some(data),
172            error: None,
173        }
174    }
175
176    pub fn err(message: impl Into<String>) -> Self {
177        Self {
178            success: false,
179            data: None,
180            error: Some(message.into()),
181        }
182    }
183}
184
185/// Bundle status information.
186#[derive(Clone, Debug, Serialize, Deserialize)]
187pub struct BundleStatusResponse {
188    pub bundle_path: PathBuf,
189    pub status: BundleStatus,
190    pub pack_count: usize,
191    pub tenant_count: usize,
192    pub provider_count: usize,
193}
194
195/// Bundle inventory listing.
196#[derive(Clone, Debug, Serialize, Deserialize)]
197pub struct BundleListResponse {
198    pub bundles: Vec<BundleStatusResponse>,
199}
200
201/// One admin client entry in the runtime allowlist registry.
202#[derive(Clone, Debug, Serialize, Deserialize)]
203pub struct AdminClientEntry {
204    pub client_cn: String,
205}
206
207/// Runtime admin allowlist inventory.
208#[derive(Clone, Debug, Serialize, Deserialize)]
209pub struct AdminClientListResponse {
210    pub admins: Vec<AdminClientEntry>,
211}
212
213/// Bundle lifecycle status.
214#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
215#[serde(rename_all = "snake_case")]
216pub enum BundleStatus {
217    Inactive,
218    Active,
219    Deploying,
220    Updating,
221    Stopping,
222    Stopped,
223    Removing,
224    Error,
225}
226
227/// Unified admin request type for routing.
228#[derive(Clone, Debug, Serialize, Deserialize)]
229#[serde(tag = "action", rename_all = "snake_case")]
230pub enum AdminRequest {
231    Deploy(BundleDeployRequest),
232    Update(BundleUpdateRequest),
233    Remove(BundleRemoveRequest),
234    Start(BundleStartRequest),
235    Stop(BundleStopRequest),
236    AddAdminClient(AdminClientAddRequest),
237    RemoveAdminClient(AdminClientRemoveRequest),
238    QaSpec(QaSpecRequest),
239    QaValidate(QaValidateRequest),
240    QaSubmit(QaSubmitRequest),
241    Status { bundle_path: PathBuf },
242    List,
243}
244
245fn default_locale() -> String {
246    "en".to_string()
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252
253    #[test]
254    fn admin_response_ok() {
255        let resp = AdminResponse::ok("hello");
256        assert!(resp.success);
257        assert_eq!(resp.data.unwrap(), "hello");
258        assert!(resp.error.is_none());
259    }
260
261    #[test]
262    fn admin_response_err() {
263        let resp = AdminResponse::<()>::err("bad request");
264        assert!(!resp.success);
265        assert!(resp.data.is_none());
266        assert_eq!(resp.error.unwrap(), "bad request");
267    }
268
269    #[test]
270    fn deploy_request_serde_roundtrip() {
271        let req = BundleDeployRequest {
272            bundle_path: PathBuf::from("/tmp/bundle"),
273            bundle_name: Some("test".into()),
274            pack_refs: vec!["oci://test:latest".into()],
275            tenants: vec![],
276            answers: Value::Object(Default::default()),
277            dry_run: false,
278        };
279        let json = serde_json::to_string(&req).unwrap();
280        let parsed: BundleDeployRequest = serde_json::from_str(&json).unwrap();
281        assert_eq!(parsed.bundle_path, PathBuf::from("/tmp/bundle"));
282    }
283
284    #[test]
285    fn update_request_serde_roundtrip() {
286        let req = BundleUpdateRequest {
287            bundle_path: PathBuf::from("/tmp/bundle"),
288            bundle_name: Some("test".into()),
289            pack_refs: vec!["oci://test:latest".into()],
290            tenants: vec![],
291            answers: Value::Object(Default::default()),
292            dry_run: true,
293        };
294        let json = serde_json::to_string(&req).unwrap();
295        let parsed: BundleUpdateRequest = serde_json::from_str(&json).unwrap();
296        assert_eq!(parsed.bundle_path, PathBuf::from("/tmp/bundle"));
297        assert!(parsed.dry_run);
298    }
299
300    #[test]
301    fn admin_request_tagged_enum() {
302        let json = r#"{"action":"list"}"#;
303        let req: AdminRequest = serde_json::from_str(json).unwrap();
304        assert!(matches!(req, AdminRequest::List));
305    }
306
307    #[test]
308    fn bundle_status_serde() {
309        let status = BundleStatus::Active;
310        let json = serde_json::to_string(&status).unwrap();
311        assert_eq!(json, "\"active\"");
312    }
313
314    #[test]
315    fn bundle_status_stopped_serde() {
316        let status = BundleStatus::Stopped;
317        let json = serde_json::to_string(&status).unwrap();
318        assert_eq!(json, "\"stopped\"");
319    }
320}