1use crate::{machines::MachineRegions, API_BASE_URL};
2use reqwest::Client;
3use serde::{Deserialize, Serialize};
4use std::error::Error;
5use tracing::debug;
6
7#[derive(Debug, Deserialize, Serialize)]
8pub struct Volume {
9 pub attached_alloc_id: Option<String>,
10 pub attached_machine_id: Option<String>,
11 pub auto_backup_enabled: Option<bool>,
12 pub block_size: Option<u64>,
13 pub blocks: Option<u64>,
14 pub blocks_avail: Option<u64>,
15 pub blocks_free: Option<u64>,
16 pub created_at: Option<String>,
17 pub encrypted: Option<bool>,
18 pub fstype: Option<String>,
19 pub host_status: Option<String>,
20 pub id: Option<String>,
21 pub name: Option<String>,
22 pub region: Option<String>,
23 pub size_gb: Option<u64>,
24 pub snapshot_retention: Option<u64>,
25 pub state: Option<String>,
26 pub zone: Option<String>,
27}
28
29#[derive(Debug, Serialize)]
30pub struct Compute {
31 pub cpu_kind: Option<String>,
32 pub cpus: Option<u32>,
33 pub gpu_kind: Option<String>,
34 pub gpus: Option<u32>,
35 pub host_dedication_id: Option<String>,
36 pub kernel_args: Option<Vec<String>>,
37 pub memory_mb: Option<u32>,
38 pub compute_image: Option<String>,
39}
40impl Default for Compute {
41 fn default() -> Self {
42 Self {
43 cpu_kind: Some("shared".to_string()),
44 cpus: Some(1),
45 gpu_kind: None,
46 gpus: None,
47 host_dedication_id: None,
48 kernel_args: None,
49 memory_mb: Some(512),
50 compute_image: None,
51 }
52 }
53}
54
55impl Compute {
56 pub fn new(
57 cpu_kind: Option<String>,
58 cpus: Option<u32>,
59 gpu_kind: Option<String>,
60 gpus: Option<u32>,
61 host_dedication_id: Option<String>,
62 kernel_args: Option<Vec<String>>,
63 memory_mb: Option<u32>,
64 compute_image: Option<String>,
65 ) -> Self {
66 Self {
67 cpu_kind,
68 cpus,
69 gpu_kind,
70 gpus,
71 host_dedication_id,
72 kernel_args,
73 memory_mb,
74 compute_image,
75 }
76 }
77}
78
79#[derive(Debug, Serialize)]
80pub struct CreateVolumeRequest {
81 pub name: String,
82 pub region: MachineRegions,
83 pub size_gb: u64,
84 pub encrypted: bool,
85 pub fstype: String,
86 pub require_unique_zone: bool,
87 pub compute: Option<Compute>,
88 pub snapshot_id: Option<String>,
89 pub snapshot_retention: Option<u32>,
90 pub source_volume_id: Option<String>,
91}
92
93impl CreateVolumeRequest {
94 pub fn builder(name: &str, region: MachineRegions, size_gb: u64) -> CreateVolumeRequestBuilder {
95 CreateVolumeRequestBuilder::new(name.to_string(), region, size_gb)
96 }
97}
98
99pub struct CreateVolumeRequestBuilder {
100 name: String,
101 region: MachineRegions,
102 size_gb: u64,
103 encrypted: bool,
104 fstype: String,
105 require_unique_zone: bool,
106 compute: Option<Compute>,
107 snapshot_id: Option<String>,
108 snapshot_retention: Option<u32>,
109 source_volume_id: Option<String>,
110}
111
112impl CreateVolumeRequestBuilder {
113 pub fn new(name: String, region: MachineRegions, size_gb: u64) -> Self {
114 Self {
115 name,
116 region,
117 size_gb,
118 encrypted: false,
119 fstype: "ext4".to_string(),
120 require_unique_zone: true,
121 compute: Some(Compute::default()),
122 snapshot_id: None,
123 snapshot_retention: None,
124 source_volume_id: None,
125 }
126 }
127
128 pub fn encrypted(mut self, encrypted: bool) -> Self {
129 self.encrypted = encrypted;
130 self
131 }
132
133 pub fn fstype(mut self, fstype: String) -> Self {
134 self.fstype = fstype;
135 self
136 }
137
138 pub fn require_unique_zone(mut self, require_unique_zone: bool) -> Self {
139 self.require_unique_zone = require_unique_zone;
140 self
141 }
142
143 pub fn compute(mut self, compute: Compute) -> Self {
144 self.compute = Some(compute);
145 self
146 }
147
148 pub fn snapshot_id(mut self, snapshot_id: Option<String>) -> Self {
149 self.snapshot_id = snapshot_id;
150 self
151 }
152
153 pub fn snapshot_retention(mut self, snapshot_retention: Option<u32>) -> Self {
154 self.snapshot_retention = snapshot_retention;
155 self
156 }
157
158 pub fn source_volume_id(mut self, source_volume_id: Option<String>) -> Self {
159 self.source_volume_id = source_volume_id;
160 self
161 }
162
163 pub fn build(self) -> CreateVolumeRequest {
164 CreateVolumeRequest {
165 name: self.name,
166 region: self.region,
167 size_gb: self.size_gb,
168 encrypted: self.encrypted,
169 fstype: self.fstype,
170 require_unique_zone: self.require_unique_zone,
171 compute: self.compute,
172 snapshot_id: self.snapshot_id,
173 snapshot_retention: self.snapshot_retention,
174 source_volume_id: self.source_volume_id,
175 }
176 }
177}
178
179#[derive(Debug, Serialize)]
180pub struct UpdateVolumeRequest {
181 pub auto_backup_enabled: bool,
182 pub snapshot_retention: u64,
183}
184
185#[derive(Debug, Serialize)]
186pub struct ExtendVolumeRequest {
187 pub size_gb: u64,
188}
189
190#[derive(Debug, Deserialize)]
191pub struct Snapshot {
192 pub created_at: String,
193 pub digest: String,
194 pub id: String,
195 pub retention_days: u64,
196 pub size: u64,
197 pub status: String,
198}
199
200pub struct VolumeManager {
201 client: Client,
202 api_token: String,
203}
204
205impl VolumeManager {
206 pub fn new(client: Client, api_token: String) -> Self {
207 Self { client, api_token }
208 }
209
210 pub async fn list_volumes(
211 &self,
212 app_name: &str,
213 summary: bool,
214 ) -> Result<Vec<Volume>, Box<dyn Error>> {
215 let url = format!(
216 "{API_BASE_URL}/apps/{}/volumes?summary={}",
217 app_name, summary
218 );
219 let response = self
220 .client
221 .get(&url)
222 .bearer_auth(&self.api_token)
223 .send()
224 .await?;
225
226 if response.status().is_success() {
227 let volumes = response.json::<Vec<Volume>>().await?;
228 debug!("Successfully fetched volumes: {:?}", volumes);
229 Ok(volumes)
230 } else {
231 Err(format!("Failed to fetch volumes: {}", response.status()).into())
232 }
233 }
234
235 pub async fn create_volume(
236 &self,
237 app_name: &str,
238 volume_request: CreateVolumeRequest,
239 ) -> Result<Volume, Box<dyn Error>> {
240 debug!("Creating volume: {:?}", volume_request);
241 let url = format!("{API_BASE_URL}/apps/{}/volumes", app_name);
242
243 let response = self
246 .client
247 .post(&url)
248 .bearer_auth(&self.api_token)
249 .json(&volume_request)
250 .send()
251 .await?;
252
253 let status = response.status();
254 if status.is_success() {
255 let volume = response.json::<Volume>().await?;
256 Ok(volume)
257 } else {
258 let error_text = response.text().await?;
259 Err(format!("Failed to create volume: {} - {}", status, error_text).into())
260 }
261 }
262
263 pub async fn get_volume(
264 &self,
265 app_name: &str,
266 volume_id: &str,
267 ) -> Result<Volume, Box<dyn Error>> {
268 let url = format!("{API_BASE_URL}/apps/{}/volumes/{}", app_name, volume_id);
269 let response = self
270 .client
271 .get(&url)
272 .bearer_auth(&self.api_token)
273 .send()
274 .await?;
275
276 if response.status().is_success() {
277 let volume = response.json::<Volume>().await?;
278 debug!("Successfully fetched volume details: {:?}", volume);
279 Ok(volume)
280 } else {
281 Err(format!("Failed to fetch volume: {}", response.status()).into())
282 }
283 }
284
285 pub async fn update_volume(
286 &self,
287 app_name: &str,
288 volume_id: &str,
289 update_request: UpdateVolumeRequest,
290 ) -> Result<Volume, Box<dyn Error>> {
291 let url = format!("{API_BASE_URL}/apps/{}/volumes/{}", app_name, volume_id);
292 let response = self
293 .client
294 .put(&url)
295 .bearer_auth(&self.api_token)
296 .json(&update_request)
297 .send()
298 .await?;
299
300 if response.status().is_success() {
301 let volume = response.json::<Volume>().await?;
302 debug!("Successfully updated volume: {:?}", volume);
303 Ok(volume)
304 } else {
305 Err(format!("Failed to update volume: {}", response.status()).into())
306 }
307 }
308
309 pub async fn destroy_volume(
310 &self,
311 app_name: &str,
312 volume_id: &str,
313 ) -> Result<(), Box<dyn Error>> {
314 let url = format!("{API_BASE_URL}/apps/{}/volumes/{}", app_name, volume_id);
315 let response = self
316 .client
317 .delete(&url)
318 .bearer_auth(&self.api_token)
319 .send()
320 .await?;
321
322 if response.status().is_success() {
323 debug!("Successfully deleted volume with ID: {}", volume_id);
324 Ok(())
325 } else {
326 Err(format!("Failed to delete volume: {}", response.status()).into())
327 }
328 }
329
330 pub async fn extend_volume(
331 &self,
332 app_name: &str,
333 volume_id: &str,
334 extend_request: ExtendVolumeRequest,
335 ) -> Result<Volume, Box<dyn Error>> {
336 let url = format!(
337 "{API_BASE_URL}/apps/{}/volumes/{}/extend",
338 app_name, volume_id
339 );
340 let response = self
341 .client
342 .put(&url)
343 .bearer_auth(&self.api_token)
344 .json(&extend_request)
345 .send()
346 .await?;
347
348 if response.status().is_success() {
349 let volume = response.json::<Volume>().await?;
350 debug!("Successfully extended volume size: {:?}", volume);
351 Ok(volume)
352 } else {
353 Err(format!("Failed to extend volume size: {}", response.status()).into())
354 }
355 }
356
357 pub async fn list_snapshots(
358 &self,
359 app_name: &str,
360 volume_id: &str,
361 ) -> Result<Vec<Snapshot>, Box<dyn Error>> {
362 let url = format!(
363 "{API_BASE_URL}/apps/{}/volumes/{}/snapshots",
364 app_name, volume_id
365 );
366 let response = self
367 .client
368 .get(&url)
369 .bearer_auth(&self.api_token)
370 .send()
371 .await?;
372
373 if response.status().is_success() {
374 let snapshots = response.json::<Vec<Snapshot>>().await?;
375 debug!("Successfully fetched snapshots: {:?}", snapshots);
376 Ok(snapshots)
377 } else {
378 Err(format!("Failed to fetch snapshots: {}", response.status()).into())
379 }
380 }
381
382 pub async fn create_snapshot(
383 &self,
384 app_name: &str,
385 volume_id: &str,
386 ) -> Result<(), Box<dyn Error>> {
387 let url = format!(
388 "{API_BASE_URL}/apps/{}/volumes/{}/snapshots",
389 app_name, volume_id
390 );
391 let response = self
392 .client
393 .post(&url)
394 .bearer_auth(&self.api_token)
395 .send()
396 .await?;
397
398 if response.status().is_success() {
399 debug!("Successfully created snapshot for volume: {}", volume_id);
400 Ok(())
401 } else {
402 Err(format!("Failed to create snapshot: {}", response.status()).into())
403 }
404 }
405}