1use serde::{Deserialize, Serialize};
8use serde_json::{json, Value};
9
10use crate::error::SailError;
11use crate::http::{HttpCore, IdempotencyKey, Method, RequestSpec};
12use crate::retry::DEFAULT_RETRY_POLICY;
13
14#[derive(Debug, Clone, Serialize)]
16pub struct App {
17 pub id: String,
19 pub name: String,
21 pub created_at: i64,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct AppOverview {
28 #[serde(default)]
30 pub app_id: String,
31 #[serde(default)]
33 pub app_name: String,
34 #[serde(default)]
36 pub sailbox_count: i64,
37 #[serde(default)]
39 pub running_count: i64,
40 #[serde(default)]
42 pub paused_count: i64,
43 #[serde(default)]
45 pub sleeping_count: i64,
46 #[serde(default)]
48 pub failed_count: i64,
49 #[serde(default)]
51 pub cpu_requested_vcpu: i64,
52 #[serde(default)]
54 pub cpu_used_vcpu: f64,
55 #[serde(default)]
57 pub memory_requested_bytes: i64,
58 #[serde(default)]
60 pub memory_used_bytes: i64,
61 #[serde(default)]
63 pub disk_requested_bytes: i64,
64 #[serde(default)]
66 pub disk_used_bytes: i64,
67}
68
69pub async fn find_app(
71 api_http: &HttpCore,
72 name: &str,
73 mint_if_missing: bool,
74) -> Result<App, SailError> {
75 let body = json!({ "name": name, "mint_if_missing": mint_if_missing });
76 let bytes = serde_json::to_vec(&body).map_err(|e| SailError::Internal {
77 message: format!("failed to serialize request body: {e}"),
78 })?;
79 let (status, data) = api_http
80 .request(&RequestSpec {
81 method: Method::Post,
82 path: "/v1/apps/find".to_string(),
83 query: Vec::new(),
84 body: Some(bytes),
85 extra_headers: Vec::new(),
86 timeout: None,
87 policy: DEFAULT_RETRY_POLICY,
88 idempotency_key: IdempotencyKey::Auto,
89 })
90 .await?;
91 if status == 404 {
93 return Err(SailError::NotFound {
94 message: format!("App {name:?} not found"),
95 });
96 }
97 raise_app_error(status, &data, "Failed to find app")?;
98 Ok(App {
99 id: data
100 .get("id")
101 .and_then(Value::as_str)
102 .unwrap_or("")
103 .to_string(),
104 name: data
105 .get("name")
106 .and_then(Value::as_str)
107 .unwrap_or(name)
108 .to_string(),
109 created_at: data.get("created_at").and_then(Value::as_i64).unwrap_or(0),
110 })
111}
112
113pub async fn list_apps(sailbox_http: &HttpCore) -> Result<Vec<AppOverview>, SailError> {
115 let (status, data) = sailbox_http
116 .request(&RequestSpec {
117 method: Method::Get,
118 path: "/v1/sailboxes/overview".to_string(),
119 query: Vec::new(),
120 body: None,
121 extra_headers: Vec::new(),
122 timeout: None,
123 policy: DEFAULT_RETRY_POLICY,
124 idempotency_key: IdempotencyKey::Auto,
125 })
126 .await?;
127 raise_app_error(status, &data, "Failed to list apps")?;
128 let Some(rows) = data.get("data").and_then(Value::as_array) else {
129 return Ok(Vec::new());
130 };
131 rows.iter()
132 .map(|row| {
133 serde_json::from_value::<AppOverview>(row.clone()).map_err(|e| SailError::Internal {
134 message: format!("failed to parse app overview: {e}"),
135 })
136 })
137 .collect()
138}
139
140fn raise_app_error(status: u16, data: &Value, context: &str) -> Result<(), SailError> {
143 if status < 300 {
144 return Ok(());
145 }
146 let detail = data
147 .get("error")
148 .and_then(|e| e.get("message"))
149 .and_then(Value::as_str)
150 .filter(|s| !s.is_empty())
151 .unwrap_or("unknown error");
152 let message = format!("{context}: {detail}");
153 if status == 401 || status == 403 {
154 return Err(SailError::PermissionDenied { message });
155 }
156 Err(SailError::Api {
157 status,
158 message,
159 body: data.clone(),
160 })
161}