fly_sdk/
volumes.rs

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 payload = serde_json::to_string(&volume_request)?;
244
245        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}