1use std::collections::HashMap;
2
3use reqwest::Client;
4use serde::{Deserialize, Serialize};
5
6use ciab_core::error::{CiabError, CiabResult};
7use ciab_core::types::sandbox::PortMapping;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct CreateSandboxRequest {
11 pub image: String,
12 #[serde(default, skip_serializing_if = "Option::is_none")]
13 pub cpu: Option<f32>,
14 #[serde(default, skip_serializing_if = "Option::is_none")]
15 pub memory_mb: Option<u32>,
16 #[serde(default, skip_serializing_if = "Option::is_none")]
17 pub disk_mb: Option<u32>,
18 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
19 pub env: HashMap<String, String>,
20 #[serde(default, skip_serializing_if = "Vec::is_empty")]
21 pub ports: Vec<PortMapping>,
22 #[serde(default, skip_serializing_if = "Option::is_none")]
23 pub timeout_secs: Option<u64>,
24 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
25 pub labels: HashMap<String, String>,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct OpenSandboxResponse {
30 pub id: String,
31 pub status: String,
32 #[serde(default)]
33 pub endpoint_url: Option<String>,
34 pub created_at: String,
35 #[serde(default)]
36 pub labels: HashMap<String, String>,
37}
38
39#[derive(Debug, Clone, Serialize)]
40struct RenewRequest {
41 duration_secs: u64,
42}
43
44#[derive(Clone)]
45pub struct OpenSandboxClient {
46 base_url: String,
47 api_key: Option<String>,
48 client: Client,
49}
50
51impl OpenSandboxClient {
52 pub fn new(base_url: String, api_key: Option<String>) -> Self {
53 Self {
54 base_url,
55 api_key,
56 client: Client::new(),
57 }
58 }
59
60 fn request(&self, method: reqwest::Method, path: &str) -> reqwest::RequestBuilder {
61 let url = format!("{}{}", self.base_url, path);
62 let mut builder = self.client.request(method, &url);
63 if let Some(ref key) = self.api_key {
64 builder = builder.header("Authorization", format!("Bearer {}", key));
65 }
66 builder
67 }
68
69 pub async fn create_sandbox(
70 &self,
71 request: &CreateSandboxRequest,
72 ) -> CiabResult<OpenSandboxResponse> {
73 let resp = self
74 .request(reqwest::Method::POST, "/api/v1/sandboxes")
75 .json(request)
76 .send()
77 .await
78 .map_err(|e| CiabError::OpenSandboxError(e.to_string()))?;
79
80 let status = resp.status();
81 if !status.is_success() {
82 let body = resp.text().await.unwrap_or_default();
83 return Err(CiabError::OpenSandboxError(format!(
84 "create sandbox failed ({}): {}",
85 status, body
86 )));
87 }
88
89 resp.json::<OpenSandboxResponse>()
90 .await
91 .map_err(|e| CiabError::OpenSandboxError(e.to_string()))
92 }
93
94 pub async fn get_sandbox(&self, sandbox_id: &str) -> CiabResult<OpenSandboxResponse> {
95 let path = format!("/api/v1/sandboxes/{}", sandbox_id);
96 let resp = self
97 .request(reqwest::Method::GET, &path)
98 .send()
99 .await
100 .map_err(|e| CiabError::OpenSandboxError(e.to_string()))?;
101
102 let status = resp.status();
103 if !status.is_success() {
104 let body = resp.text().await.unwrap_or_default();
105 return Err(CiabError::OpenSandboxError(format!(
106 "get sandbox failed ({}): {}",
107 status, body
108 )));
109 }
110
111 resp.json::<OpenSandboxResponse>()
112 .await
113 .map_err(|e| CiabError::OpenSandboxError(e.to_string()))
114 }
115
116 pub async fn list_sandboxes(&self) -> CiabResult<Vec<OpenSandboxResponse>> {
117 let resp = self
118 .request(reqwest::Method::GET, "/api/v1/sandboxes")
119 .send()
120 .await
121 .map_err(|e| CiabError::OpenSandboxError(e.to_string()))?;
122
123 let status = resp.status();
124 if !status.is_success() {
125 let body = resp.text().await.unwrap_or_default();
126 return Err(CiabError::OpenSandboxError(format!(
127 "list sandboxes failed ({}): {}",
128 status, body
129 )));
130 }
131
132 resp.json::<Vec<OpenSandboxResponse>>()
133 .await
134 .map_err(|e| CiabError::OpenSandboxError(e.to_string()))
135 }
136
137 pub async fn delete_sandbox(&self, sandbox_id: &str) -> CiabResult<()> {
138 let path = format!("/api/v1/sandboxes/{}", sandbox_id);
139 let resp = self
140 .request(reqwest::Method::DELETE, &path)
141 .send()
142 .await
143 .map_err(|e| CiabError::OpenSandboxError(e.to_string()))?;
144
145 let status = resp.status();
146 if !status.is_success() {
147 let body = resp.text().await.unwrap_or_default();
148 return Err(CiabError::OpenSandboxError(format!(
149 "delete sandbox failed ({}): {}",
150 status, body
151 )));
152 }
153
154 Ok(())
155 }
156
157 pub async fn pause_sandbox(&self, sandbox_id: &str) -> CiabResult<()> {
158 let path = format!("/api/v1/sandboxes/{}/pause", sandbox_id);
159 let resp = self
160 .request(reqwest::Method::POST, &path)
161 .send()
162 .await
163 .map_err(|e| CiabError::OpenSandboxError(e.to_string()))?;
164
165 let status = resp.status();
166 if !status.is_success() {
167 let body = resp.text().await.unwrap_or_default();
168 return Err(CiabError::OpenSandboxError(format!(
169 "pause sandbox failed ({}): {}",
170 status, body
171 )));
172 }
173
174 Ok(())
175 }
176
177 pub async fn resume_sandbox(&self, sandbox_id: &str) -> CiabResult<()> {
178 let path = format!("/api/v1/sandboxes/{}/resume", sandbox_id);
179 let resp = self
180 .request(reqwest::Method::POST, &path)
181 .send()
182 .await
183 .map_err(|e| CiabError::OpenSandboxError(e.to_string()))?;
184
185 let status = resp.status();
186 if !status.is_success() {
187 let body = resp.text().await.unwrap_or_default();
188 return Err(CiabError::OpenSandboxError(format!(
189 "resume sandbox failed ({}): {}",
190 status, body
191 )));
192 }
193
194 Ok(())
195 }
196
197 pub async fn renew_expiration(&self, sandbox_id: &str, duration_secs: u64) -> CiabResult<()> {
198 let path = format!("/api/v1/sandboxes/{}/renew", sandbox_id);
199 let resp = self
200 .request(reqwest::Method::POST, &path)
201 .json(&RenewRequest { duration_secs })
202 .send()
203 .await
204 .map_err(|e| CiabError::OpenSandboxError(e.to_string()))?;
205
206 let status = resp.status();
207 if !status.is_success() {
208 let body = resp.text().await.unwrap_or_default();
209 return Err(CiabError::OpenSandboxError(format!(
210 "renew sandbox failed ({}): {}",
211 status, body
212 )));
213 }
214
215 Ok(())
216 }
217}