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    Deploying,
147    Removing,
148    Error,
149}
150
151/// Unified admin request type for routing.
152#[derive(Clone, Debug, Serialize, Deserialize)]
153#[serde(tag = "action", rename_all = "snake_case")]
154pub enum AdminRequest {
155    Deploy(BundleDeployRequest),
156    Remove(BundleRemoveRequest),
157    QaSpec(QaSpecRequest),
158    QaValidate(QaValidateRequest),
159    QaSubmit(QaSubmitRequest),
160    Status { bundle_path: PathBuf },
161    List,
162}
163
164fn default_locale() -> String {
165    "en".to_string()
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn admin_response_ok() {
174        let resp = AdminResponse::ok("hello");
175        assert!(resp.success);
176        assert_eq!(resp.data.unwrap(), "hello");
177        assert!(resp.error.is_none());
178    }
179
180    #[test]
181    fn admin_response_err() {
182        let resp = AdminResponse::<()>::err("bad request");
183        assert!(!resp.success);
184        assert!(resp.data.is_none());
185        assert_eq!(resp.error.unwrap(), "bad request");
186    }
187
188    #[test]
189    fn deploy_request_serde_roundtrip() {
190        let req = BundleDeployRequest {
191            bundle_path: PathBuf::from("/tmp/bundle"),
192            bundle_name: Some("test".into()),
193            pack_refs: vec!["oci://test:latest".into()],
194            tenants: vec![],
195            answers: Value::Object(Default::default()),
196            dry_run: false,
197        };
198        let json = serde_json::to_string(&req).unwrap();
199        let parsed: BundleDeployRequest = serde_json::from_str(&json).unwrap();
200        assert_eq!(parsed.bundle_path, PathBuf::from("/tmp/bundle"));
201    }
202
203    #[test]
204    fn admin_request_tagged_enum() {
205        let json = r#"{"action":"list"}"#;
206        let req: AdminRequest = serde_json::from_str(json).unwrap();
207        assert!(matches!(req, AdminRequest::List));
208    }
209
210    #[test]
211    fn bundle_status_serde() {
212        let status = BundleStatus::Active;
213        let json = serde_json::to_string(&status).unwrap();
214        assert_eq!(json, "\"active\"");
215    }
216}