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 remove components from a bundle.
39#[derive(Clone, Debug, Serialize, Deserialize)]
40pub struct BundleRemoveRequest {
41    /// Target bundle path.
42    pub bundle_path: PathBuf,
43    /// Packs to remove.
44    #[serde(default)]
45    pub packs: Vec<PackRemoveSelection>,
46    /// Provider IDs to remove.
47    #[serde(default)]
48    pub providers: Vec<String>,
49    /// Tenants/teams to remove.
50    #[serde(default)]
51    pub tenants: Vec<TenantSelection>,
52    /// If true, only plan without executing.
53    #[serde(default)]
54    pub dry_run: bool,
55}
56
57// ── QA setup ────────────────────────────────────────────────────────────────
58
59/// Request to get the QA FormSpec for a pack.
60#[derive(Clone, Debug, Serialize, Deserialize)]
61pub struct QaSpecRequest {
62    /// Bundle path.
63    pub bundle_path: PathBuf,
64    /// Provider ID to get spec for.
65    pub provider_id: String,
66    /// Locale for i18n resolution.
67    #[serde(default = "default_locale")]
68    pub locale: String,
69}
70
71/// Request to validate QA answers against a FormSpec.
72#[derive(Clone, Debug, Serialize, Deserialize)]
73pub struct QaValidateRequest {
74    /// Bundle path.
75    pub bundle_path: PathBuf,
76    /// Provider ID.
77    pub provider_id: String,
78    /// Answers to validate.
79    pub answers: Value,
80}
81
82/// Request to submit and persist QA answers.
83#[derive(Clone, Debug, Serialize, Deserialize)]
84pub struct QaSubmitRequest {
85    /// Bundle path.
86    pub bundle_path: PathBuf,
87    /// Provider ID.
88    pub provider_id: String,
89    /// Tenant ID.
90    pub tenant: String,
91    /// Team ID.
92    #[serde(default)]
93    pub team: Option<String>,
94    /// Answers to persist.
95    pub answers: Value,
96    /// Whether to trigger a hot reload after persisting.
97    #[serde(default)]
98    pub reload: bool,
99}
100
101// ── Responses ───────────────────────────────────────────────────────────────
102
103/// Generic admin API response wrapper.
104#[derive(Clone, Debug, Serialize, Deserialize)]
105pub struct AdminResponse<T: Serialize> {
106    pub success: bool,
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub data: Option<T>,
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub error: Option<String>,
111}
112
113impl<T: Serialize> AdminResponse<T> {
114    pub fn ok(data: T) -> Self {
115        Self {
116            success: true,
117            data: Some(data),
118            error: None,
119        }
120    }
121
122    pub fn err(message: impl Into<String>) -> Self {
123        Self {
124            success: false,
125            data: None,
126            error: Some(message.into()),
127        }
128    }
129}
130
131/// Bundle status information.
132#[derive(Clone, Debug, Serialize, Deserialize)]
133pub struct BundleStatusResponse {
134    pub bundle_path: PathBuf,
135    pub status: BundleStatus,
136    pub pack_count: usize,
137    pub tenant_count: usize,
138    pub provider_count: usize,
139}
140
141/// Bundle lifecycle status.
142#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
143#[serde(rename_all = "snake_case")]
144pub enum BundleStatus {
145    Active,
146    Inactive,
147    Deploying,
148    Stopping,
149    Removing,
150    Error,
151}
152
153/// Response listing all managed bundles.
154#[derive(Clone, Debug, Serialize, Deserialize)]
155pub struct BundleListResponse {
156    pub bundles: Vec<BundleStatusResponse>,
157}
158
159/// Request to start a bundle runtime.
160#[derive(Clone, Debug, Serialize, Deserialize)]
161pub struct BundleStartRequest {
162    pub bundle_path: PathBuf,
163}
164
165/// Request to stop a bundle runtime.
166#[derive(Clone, Debug, Serialize, Deserialize)]
167pub struct BundleStopRequest {
168    pub bundle_path: PathBuf,
169}
170
171/// Request to update a bundle (add/upgrade packs, tenants).
172#[derive(Clone, Debug, Serialize, Deserialize)]
173pub struct BundleUpdateRequest {
174    /// Target bundle path.
175    pub bundle_path: PathBuf,
176    /// Pack references to add or upgrade.
177    #[serde(default)]
178    pub pack_refs: Vec<String>,
179    /// Tenant selections.
180    #[serde(default)]
181    pub tenants: Vec<TenantSelection>,
182    /// Pre-collected QA answers.
183    #[serde(default)]
184    pub answers: Value,
185    /// If true, only plan without executing.
186    #[serde(default)]
187    pub dry_run: bool,
188}
189
190/// A single admin client entry.
191#[derive(Clone, Debug, Serialize, Deserialize)]
192pub struct AdminClientEntry {
193    pub client_cn: String,
194}
195
196/// Response listing admin clients.
197#[derive(Clone, Debug, Serialize, Deserialize)]
198pub struct AdminClientListResponse {
199    pub admins: Vec<AdminClientEntry>,
200}
201
202/// Request to add an admin client.
203#[derive(Clone, Debug, Serialize, Deserialize)]
204pub struct AdminClientAddRequest {
205    pub bundle_path: PathBuf,
206    pub client_cn: String,
207}
208
209/// Request to remove an admin client.
210#[derive(Clone, Debug, Serialize, Deserialize)]
211pub struct AdminClientRemoveRequest {
212    pub bundle_path: PathBuf,
213    pub client_cn: String,
214}
215
216/// Unified admin request type for routing.
217#[derive(Clone, Debug, Serialize, Deserialize)]
218#[serde(tag = "action", rename_all = "snake_case")]
219pub enum AdminRequest {
220    Deploy(BundleDeployRequest),
221    Remove(BundleRemoveRequest),
222    QaSpec(QaSpecRequest),
223    QaValidate(QaValidateRequest),
224    QaSubmit(QaSubmitRequest),
225    Status { bundle_path: PathBuf },
226    List,
227}
228
229fn default_locale() -> String {
230    "en".to_string()
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236
237    #[test]
238    fn admin_response_ok() {
239        let resp = AdminResponse::ok("hello");
240        assert!(resp.success);
241        assert_eq!(resp.data.unwrap(), "hello");
242        assert!(resp.error.is_none());
243    }
244
245    #[test]
246    fn admin_response_err() {
247        let resp = AdminResponse::<()>::err("bad request");
248        assert!(!resp.success);
249        assert!(resp.data.is_none());
250        assert_eq!(resp.error.unwrap(), "bad request");
251    }
252
253    #[test]
254    fn deploy_request_serde_roundtrip() {
255        let req = BundleDeployRequest {
256            bundle_path: PathBuf::from("/tmp/bundle"),
257            bundle_name: Some("test".into()),
258            pack_refs: vec!["oci://test:latest".into()],
259            tenants: vec![],
260            answers: Value::Object(Default::default()),
261            dry_run: false,
262        };
263        let json = serde_json::to_string(&req).unwrap();
264        let parsed: BundleDeployRequest = serde_json::from_str(&json).unwrap();
265        assert_eq!(parsed.bundle_path, PathBuf::from("/tmp/bundle"));
266    }
267
268    #[test]
269    fn admin_request_tagged_enum() {
270        let json = r#"{"action":"list"}"#;
271        let req: AdminRequest = serde_json::from_str(json).unwrap();
272        assert!(matches!(req, AdminRequest::List));
273    }
274
275    #[test]
276    fn bundle_status_serde() {
277        let status = BundleStatus::Active;
278        let json = serde_json::to_string(&status).unwrap();
279        assert_eq!(json, "\"active\"");
280    }
281}