daytona_client/
workspace.rs

1//! Workspace management operations
2
3use tracing::{debug, info};
4use uuid::Uuid;
5
6use crate::{client::DaytonaClient, error::Result, models::workspace::*};
7
8/// Manager for workspace operations
9pub struct WorkspaceManager<'a> {
10    client: &'a DaytonaClient,
11}
12
13impl<'a> WorkspaceManager<'a> {
14    /// Create a new workspace manager
15    pub fn new(client: &'a DaytonaClient) -> Self {
16        Self { client }
17    }
18
19    /// Create a new workspace
20    pub async fn create(&self, params: CreateWorkspaceParams) -> Result<Workspace> {
21        info!("Creating new workspace: {}", params.name);
22
23        let response = self
24            .client
25            .build_request(reqwest::Method::POST, "/workspace")
26            .json(&params)
27            .send()
28            .await?;
29
30        let workspace = self.client.handle_response::<Workspace>(response).await?;
31        info!("Created workspace: {} ({})", workspace.name, workspace.id);
32
33        Ok(workspace)
34    }
35
36    /// Get workspace by ID
37    pub async fn get(&self, workspace_id: &Uuid) -> Result<Workspace> {
38        debug!("Getting workspace: {}", workspace_id);
39
40        let response = self
41            .client
42            .build_request(
43                reqwest::Method::GET,
44                &format!("/workspace/{}", workspace_id),
45            )
46            .send()
47            .await?;
48
49        self.client.handle_response(response).await
50    }
51
52    /// List all workspaces
53    pub async fn list(&self) -> Result<Vec<Workspace>> {
54        debug!("Listing workspaces");
55
56        let response = self
57            .client
58            .build_request(reqwest::Method::GET, "/workspace")
59            .send()
60            .await?;
61
62        self.client.handle_response(response).await
63    }
64
65    /// Update workspace
66    pub async fn update(
67        &self,
68        workspace_id: &Uuid,
69        params: UpdateWorkspaceParams,
70    ) -> Result<Workspace> {
71        info!("Updating workspace: {}", workspace_id);
72
73        let response = self
74            .client
75            .build_request(
76                reqwest::Method::PUT,
77                &format!("/workspace/{}", workspace_id),
78            )
79            .json(&params)
80            .send()
81            .await?;
82
83        self.client.handle_response(response).await
84    }
85
86    /// Delete workspace
87    pub async fn delete(&self, workspace_id: &Uuid) -> Result<()> {
88        self.delete_with_force(workspace_id, false).await
89    }
90
91    /// Delete workspace with force option
92    pub async fn delete_with_force(&self, workspace_id: &Uuid, force: bool) -> Result<()> {
93        info!("Deleting workspace: {} (force: {})", workspace_id, force);
94
95        let url = if force {
96            format!("/workspace/{}?force=true", workspace_id)
97        } else {
98            format!("/workspace/{}", workspace_id)
99        };
100
101        let response = self
102            .client
103            .build_request(reqwest::Method::DELETE, &url)
104            .send()
105            .await?;
106
107        if response.status().is_success() {
108            Ok(())
109        } else {
110            let error_text = response.text().await?;
111            Err(crate::error::DaytonaError::RequestFailed(format!(
112                "Failed to delete workspace: {}",
113                error_text
114            )))
115        }
116    }
117
118    /// Start workspace
119    pub async fn start(&self, workspace_id: &Uuid) -> Result<()> {
120        info!("Starting workspace: {}", workspace_id);
121
122        let response = self
123            .client
124            .build_request(
125                reqwest::Method::POST,
126                &format!("/workspace/{}/start", workspace_id),
127            )
128            .send()
129            .await?;
130
131        if response.status().is_success() {
132            Ok(())
133        } else {
134            let error_text = response.text().await?;
135            Err(crate::error::DaytonaError::RequestFailed(format!(
136                "Failed to start workspace: {}",
137                error_text
138            )))
139        }
140    }
141
142    /// Stop workspace
143    pub async fn stop(&self, workspace_id: &Uuid) -> Result<()> {
144        info!("Stopping workspace: {}", workspace_id);
145
146        let response = self
147            .client
148            .build_request(
149                reqwest::Method::POST,
150                &format!("/workspace/{}/stop", workspace_id),
151            )
152            .send()
153            .await?;
154
155        if response.status().is_success() {
156            Ok(())
157        } else {
158            let error_text = response.text().await?;
159            Err(crate::error::DaytonaError::RequestFailed(format!(
160                "Failed to stop workspace: {}",
161                error_text
162            )))
163        }
164    }
165
166    /// Archive workspace
167    pub async fn archive(&self, workspace_id: &Uuid) -> Result<()> {
168        info!("Archiving workspace: {}", workspace_id);
169
170        let response = self
171            .client
172            .build_request(
173                reqwest::Method::POST,
174                &format!("/workspace/{}/archive", workspace_id),
175            )
176            .send()
177            .await?;
178
179        if response.status().is_success() {
180            Ok(())
181        } else {
182            let error_text = response.text().await?;
183            Err(crate::error::DaytonaError::RequestFailed(format!(
184                "Failed to archive workspace: {}",
185                error_text
186            )))
187        }
188    }
189
190    /// Restore archived workspace
191    pub async fn restore(&self, workspace_id: &Uuid) -> Result<()> {
192        info!("Restoring workspace: {}", workspace_id);
193
194        let response = self
195            .client
196            .build_request(
197                reqwest::Method::POST,
198                &format!("/workspace/{}/restore", workspace_id),
199            )
200            .send()
201            .await?;
202
203        if response.status().is_success() {
204            Ok(())
205        } else {
206            let error_text = response.text().await?;
207            Err(crate::error::DaytonaError::RequestFailed(format!(
208                "Failed to restore workspace: {}",
209                error_text
210            )))
211        }
212    }
213
214    /// Create workspace backup
215    pub async fn backup(
216        &self,
217        workspace_id: &Uuid,
218        name: String,
219        description: Option<String>,
220    ) -> Result<WorkspaceBackup> {
221        info!("Creating backup for workspace: {}", workspace_id);
222
223        let body = serde_json::json!({
224            "name": name,
225            "description": description
226        });
227
228        let response = self
229            .client
230            .build_request(
231                reqwest::Method::POST,
232                &format!("/workspace/{}/backup", workspace_id),
233            )
234            .json(&body)
235            .send()
236            .await?;
237
238        self.client.handle_response(response).await
239    }
240
241    /// List workspace backups
242    pub async fn list_backups(&self, workspace_id: &Uuid) -> Result<Vec<WorkspaceBackup>> {
243        debug!("Listing backups for workspace: {}", workspace_id);
244
245        let response = self
246            .client
247            .build_request(
248                reqwest::Method::GET,
249                &format!("/workspace/{}/backup", workspace_id),
250            )
251            .send()
252            .await?;
253
254        self.client.handle_response(response).await
255    }
256
257    /// Restore from backup
258    pub async fn restore_from_backup(&self, workspace_id: &Uuid, backup_id: &Uuid) -> Result<()> {
259        info!(
260            "Restoring workspace {} from backup {}",
261            workspace_id, backup_id
262        );
263
264        let response = self
265            .client
266            .build_request(
267                reqwest::Method::POST,
268                &format!("/workspace/{}/backup/{}/restore", workspace_id, backup_id),
269            )
270            .send()
271            .await?;
272
273        if response.status().is_success() {
274            Ok(())
275        } else {
276            let error_text = response.text().await?;
277            Err(crate::error::DaytonaError::RequestFailed(format!(
278                "Failed to restore from backup: {}",
279                error_text
280            )))
281        }
282    }
283
284    /// Get port preview URL
285    pub async fn get_port_preview(&self, workspace_id: &Uuid, port: u16) -> Result<PortPreview> {
286        debug!(
287            "Getting port preview for workspace {} port {}",
288            workspace_id, port
289        );
290
291        let response = self
292            .client
293            .build_request(
294                reqwest::Method::GET,
295                &format!("/workspace/{}/port/{}/preview", workspace_id, port),
296            )
297            .send()
298            .await?;
299
300        self.client.handle_response(response).await
301    }
302
303    /// Create port preview
304    pub async fn create_port_preview(
305        &self,
306        workspace_id: &Uuid,
307        port: u16,
308        public: bool,
309    ) -> Result<PortPreview> {
310        info!(
311            "Creating port preview for workspace {} port {}",
312            workspace_id, port
313        );
314
315        let body = serde_json::json!({ "public": public });
316
317        let response = self
318            .client
319            .build_request(
320                reqwest::Method::POST,
321                &format!("/workspace/{}/port/{}/preview", workspace_id, port),
322            )
323            .json(&body)
324            .send()
325            .await?;
326
327        self.client.handle_response(response).await
328    }
329
330    /// Get build logs for a workspace
331    pub async fn get_build_logs(&self, workspace_id: &Uuid, follow: bool) -> Result<String> {
332        debug!(
333            "Getting build logs for workspace: {} (follow: {})",
334            workspace_id, follow
335        );
336
337        let url = if follow {
338            format!("/workspace/{}/build-logs?follow=true", workspace_id)
339        } else {
340            format!("/workspace/{}/build-logs", workspace_id)
341        };
342
343        let response = self
344            .client
345            .build_request(reqwest::Method::GET, &url)
346            .send()
347            .await?;
348
349        if response.status().is_success() {
350            Ok(response.text().await?)
351        } else {
352            let error_text = response.text().await?;
353            Err(crate::error::DaytonaError::RequestFailed(format!(
354                "Failed to get build logs: {}",
355                error_text
356            )))
357        }
358    }
359
360    /// Replace labels for a workspace
361    pub async fn replace_labels(
362        &self,
363        workspace_id: &Uuid,
364        labels: std::collections::HashMap<String, String>,
365    ) -> Result<()> {
366        info!("Replacing labels for workspace: {}", workspace_id);
367
368        let response = self
369            .client
370            .build_request(
371                reqwest::Method::PUT,
372                &format!("/workspace/{}/labels", workspace_id),
373            )
374            .json(&labels)
375            .send()
376            .await?;
377
378        if response.status().is_success() {
379            Ok(())
380        } else {
381            let error_text = response.text().await?;
382            Err(crate::error::DaytonaError::RequestFailed(format!(
383                "Failed to replace labels: {}",
384                error_text
385            )))
386        }
387    }
388
389    /// Set auto-archive interval for a workspace
390    pub async fn set_auto_archive_interval(&self, workspace_id: &Uuid, minutes: u32) -> Result<()> {
391        info!(
392            "Setting auto-archive interval for workspace {}: {} minutes",
393            workspace_id, minutes
394        );
395
396        let body = serde_json::json!({ "minutes": minutes });
397
398        let response = self
399            .client
400            .build_request(
401                reqwest::Method::POST,
402                &format!("/workspace/{}/auto-archive", workspace_id),
403            )
404            .json(&body)
405            .send()
406            .await?;
407
408        if response.status().is_success() {
409            Ok(())
410        } else {
411            let error_text = response.text().await?;
412            Err(crate::error::DaytonaError::RequestFailed(format!(
413                "Failed to set auto-archive interval: {}",
414                error_text
415            )))
416        }
417    }
418
419    /// Set auto-stop interval for a workspace
420    pub async fn set_autostop_interval(&self, workspace_id: &Uuid, minutes: u32) -> Result<()> {
421        info!(
422            "Setting auto-stop interval for workspace {}: {} minutes",
423            workspace_id, minutes
424        );
425
426        let body = serde_json::json!({ "minutes": minutes });
427
428        let response = self
429            .client
430            .build_request(
431                reqwest::Method::POST,
432                &format!("/workspace/{}/auto-stop", workspace_id),
433            )
434            .json(&body)
435            .send()
436            .await?;
437
438        if response.status().is_success() {
439            Ok(())
440        } else {
441            let error_text = response.text().await?;
442            Err(crate::error::DaytonaError::RequestFailed(format!(
443                "Failed to set auto-stop interval: {}",
444                error_text
445            )))
446        }
447    }
448
449    /// Update public status of a workspace
450    pub async fn update_public_status(&self, workspace_id: &Uuid, is_public: bool) -> Result<()> {
451        info!(
452            "Updating public status for workspace {}: {}",
453            workspace_id, is_public
454        );
455
456        let body = serde_json::json!({ "public": is_public });
457
458        let response = self
459            .client
460            .build_request(
461                reqwest::Method::PATCH,
462                &format!("/workspace/{}/public", workspace_id),
463            )
464            .json(&body)
465            .send()
466            .await?;
467
468        if response.status().is_success() {
469            Ok(())
470        } else {
471            let error_text = response.text().await?;
472            Err(crate::error::DaytonaError::RequestFailed(format!(
473                "Failed to update public status: {}",
474                error_text
475            )))
476        }
477    }
478
479    /// Wait for workspace to reach a specific state
480    pub async fn wait_for_state(
481        &self,
482        workspace_id: &Uuid,
483        target_state: WorkspaceState,
484        max_wait_seconds: u64,
485    ) -> Result<Workspace> {
486        let start = std::time::Instant::now();
487        let max_duration = std::time::Duration::from_secs(max_wait_seconds);
488
489        loop {
490            let workspace = self.get(workspace_id).await?;
491
492            if matches!(workspace.state, ref s if std::mem::discriminant(s) == std::mem::discriminant(&target_state))
493            {
494                return Ok(workspace);
495            }
496
497            if let WorkspaceState::Error = workspace.state {
498                return Err(crate::error::DaytonaError::RequestFailed(format!(
499                    "Workspace entered error state: {:?}",
500                    workspace.error_reason
501                )));
502            }
503
504            if start.elapsed() > max_duration {
505                return Err(crate::error::DaytonaError::RequestFailed(format!(
506                    "Timeout waiting for workspace to reach {:?} state",
507                    target_state
508                )));
509            }
510
511            tokio::time::sleep(std::time::Duration::from_secs(2)).await;
512        }
513    }
514}
515
516impl DaytonaClient {
517    /// Get workspace manager
518    pub fn workspaces(&self) -> WorkspaceManager<'_> {
519        WorkspaceManager::new(self)
520    }
521}