Skip to main content

ciab_sandbox/
client.rs

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}