Skip to main content

incus_client/resources/
projects.rs

1//! Project CRUD.
2
3use serde::Deserialize;
4
5use crate::error::{Error, Result};
6use crate::transport::{
7    resource_error_or, sync_metadata, Client, Method, RecursionQuery, WithEtag,
8};
9
10#[derive(Debug, Clone, Deserialize)]
11pub struct Project {
12    pub name: String,
13    #[serde(default)]
14    pub description: String,
15    #[serde(default)]
16    pub config: serde_json::Value,
17}
18
19impl Client {
20    pub async fn list_projects(&self, recursion: bool) -> Result<Vec<serde_json::Value>> {
21        let recursion_query = RecursionQuery::new(recursion);
22        let envelope = self
23            .request(
24                Method::Get,
25                "/1.0/projects",
26                &recursion_query.as_query(),
27                None,
28                None,
29            )
30            .await?;
31        Ok(serde_json::from_value(sync_metadata(envelope, "list")?)?)
32    }
33
34    /// Fetches one project by name, along with its ETag for use as a later
35    /// `If-Match` precondition.
36    pub async fn get_project(&self, name: &str) -> Result<WithEtag<Project>> {
37        let path = format!("/1.0/projects/{name}");
38        let envelope = self
39            .request(Method::Get, &path, &[], None, None)
40            .await
41            .map_err(|err| resource_error_or(err, name))?;
42        match envelope {
43            crate::transport::IncusEnvelope::Sync { metadata, etag } => Ok(WithEtag {
44                value: serde_json::from_value(metadata)?,
45                etag,
46            }),
47            other => Err(Error::InvalidResponse(format!(
48                "expected a sync project response, got {other:?}"
49            ))),
50        }
51    }
52
53    /// Creates a project. Synchronous: the project exists by the time this
54    /// returns, with no operation to wait on. Verified against
55    /// `cmd/incusd/api_project.go`'s `projectsPost` on the `lxc/incus`
56    /// `main` branch, which always returns `response.SyncResponseLocation`.
57    pub async fn create_project(&self, params: &serde_json::Value) -> Result<()> {
58        self.request(Method::Post, "/1.0/projects", &[], Some(params), None)
59            .await?;
60        Ok(())
61    }
62
63    /// Full replacement update (PUT). `etag`, if provided, is sent as
64    /// `If-Match` for optimistic concurrency; a stale ETag surfaces as
65    /// `Error::PreconditionFailed`, not the generic `Error::Api`.
66    ///
67    /// Synchronous, like [`Client::create_project`] - verified against
68    /// `cmd/incusd/api_project.go`'s `projectChange`, which always returns
69    /// `response.EmptySyncResponse`.
70    pub async fn update_project(
71        &self,
72        name: &str,
73        new_definition: &serde_json::Value,
74        etag: Option<&str>,
75    ) -> Result<()> {
76        let path = format!("/1.0/projects/{name}");
77        self.request(Method::Put, &path, &[], Some(new_definition), etag)
78            .await
79            .map_err(|err| resource_error_or(err, name))?;
80        Ok(())
81    }
82
83    /// Same as [`Client::update_project`], but takes the `WithEtag` from a
84    /// prior [`Client::get_project`] call directly instead of a bare
85    /// `etag: Option<&str>` - see `instances::Client::update_instance_guarded`'s
86    /// doc comment for why this exists alongside the raw-`etag` version.
87    pub async fn update_project_guarded(
88        &self,
89        fetched: &WithEtag<Project>,
90        new_definition: &serde_json::Value,
91    ) -> Result<()> {
92        self.update_project(&fetched.value().name, new_definition, fetched.etag())
93            .await
94    }
95
96    /// Synchronous - verified against `cmd/incusd/api_project.go`'s
97    /// `projectDelete`, which always returns `response.EmptySyncResponse`.
98    pub async fn delete_project(&self, name: &str) -> Result<()> {
99        let path = format!("/1.0/projects/{name}");
100        self.request(Method::Delete, &path, &[], None, None)
101            .await
102            .map_err(|err| resource_error_or(err, name))?;
103        Ok(())
104    }
105}
106
107#[cfg(test)]
108#[path = "projects_tests.rs"]
109mod tests;