Skip to main content

clevercloud_sdk/v4/functions/
deployments.rs

1//! # Deployment module
2//!
3//! This module provides structures to interact with functions' deployments.
4
5use std::{
6    fmt::{self, Debug, Display, Formatter},
7    str::FromStr,
8};
9
10use crate::oauth10a::{
11    ClientError, Request, RestClient,
12    reqwest::{
13        self, Body, Method,
14        header::{CONTENT_LENGTH, CONTENT_TYPE, HeaderValue},
15    },
16    url,
17};
18use chrono::{DateTime, Utc};
19#[cfg(feature = "logging")]
20use log::{Level, debug, log_enabled};
21use serde::{Deserialize, Serialize};
22
23use crate::Client;
24
25// -----------------------------------------------------------------------------
26// Constants
27
28pub const MIME_APPLICATION_WASM: &str = "application/wasm";
29
30// ----------------------------------------------------------------------------
31// Error
32
33#[derive(thiserror::Error, Debug)]
34pub enum Error {
35    #[error(
36        "failed to parse the webassembly platform '{0}', available values are 'rust', 'javascript' ('js'), 'tiny_go' ('go') and 'assemblyscript'"
37    )]
38    ParsePlatform(String),
39    #[error(
40        "failed to parse the status '{0}', available values are 'waiting_for_upload', 'deploying', 'packaging', 'ready' and 'error'"
41    )]
42    ParseStatus(String),
43    #[error("failed to parse endpoint '{0}', {1}")]
44    ParseUrl(String, url::ParseError),
45    #[error("failed to list deployments for function '{0}' of organisation '{1}', {2}")]
46    List(String, String, ClientError),
47    #[error("failed to create deployment for function '{0}' on organisation '{1}', {2}")]
48    Create(String, String, ClientError),
49    #[error("failed to get deployment '{0}' of function '{1}' on organisation '{2}', {3}")]
50    Get(String, String, String, ClientError),
51    #[error("failed to trigger deployment '{0}' of function '{1}' on organisation '{2}', {3}")]
52    Trigger(String, String, String, ClientError),
53    #[error("failed to delete deployment '{0}' of function '{1}' on organisation '{2}', {3}")]
54    Delete(String, String, String, ClientError),
55    #[error("failed to create request, {0}")]
56    Request(reqwest::Error),
57    #[error("failed to execute request, {0}")]
58    Execute(ClientError),
59    #[error("failed to execute request, got status code {0}")]
60    StatusCode(u16),
61}
62
63// ----------------------------------------------------------------------------
64// Platform
65
66#[derive(Serialize, Deserialize, Hash, Ord, PartialOrd, Eq, PartialEq, Clone, Debug)]
67pub enum Platform {
68    #[serde(rename = "RUST")]
69    Rust,
70    #[serde(rename = "ASSEMBLY_SCRIPT")]
71    AssemblyScript,
72    #[serde(rename = "TINY_GO")]
73    TinyGo,
74    #[serde(rename = "JAVA_SCRIPT")]
75    JavaScript,
76}
77
78impl Display for Platform {
79    fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
80        match self {
81            Self::Rust => write!(f, "RUST"),
82            Self::AssemblyScript => write!(f, "ASSEMBLY_SCRIPT"),
83            Self::JavaScript => write!(f, "JAVA_SCRIPT"),
84            Self::TinyGo => write!(f, "TINY_GO"),
85        }
86    }
87}
88
89impl FromStr for Platform {
90    type Err = Error;
91
92    fn from_str(s: &str) -> Result<Self, Self::Err> {
93        match s.to_lowercase().trim().replace('_', "").as_str() {
94            "rust" => Ok(Self::Rust),
95            "javascript" | "js" => Ok(Self::JavaScript),
96            "tinygo" | "go" => Ok(Self::TinyGo),
97            "assemblyscript" => Ok(Self::AssemblyScript),
98            _ => Err(Error::ParsePlatform(s.to_string())),
99        }
100    }
101}
102
103// ----------------------------------------------------------------------------
104// Status
105
106#[derive(Serialize, Deserialize, Hash, Ord, PartialOrd, Eq, PartialEq, Clone, Debug)]
107pub enum Status {
108    #[serde(rename = "WAITING_FOR_UPLOAD")]
109    WaitingForUpload,
110    #[serde(rename = "PACKAGING")]
111    Packaging,
112    #[serde(rename = "DEPLOYING")]
113    Deploying,
114    #[serde(rename = "READY")]
115    Ready,
116    #[serde(rename = "ERROR")]
117    Error,
118}
119
120impl Display for Status {
121    fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
122        match self {
123            Self::WaitingForUpload => write!(f, "WAITING_FOR_UPLOAD"),
124            Self::Packaging => write!(f, "PACKAGING"),
125            Self::Deploying => write!(f, "DEPLOYING"),
126            Self::Ready => write!(f, "READY"),
127            Self::Error => write!(f, "ERROR"),
128        }
129    }
130}
131
132impl FromStr for Status {
133    type Err = Error;
134
135    fn from_str(s: &str) -> Result<Self, Self::Err> {
136        match s.to_lowercase().trim().replace('_', "").as_str() {
137            "waitingforupload" => Ok(Self::WaitingForUpload),
138            "packaging" => Ok(Self::Packaging),
139            "deploying" => Ok(Self::Deploying),
140            "ready" => Ok(Self::Ready),
141            "error" => Ok(Self::Error),
142            _ => Err(Error::ParseStatus(s.to_string())),
143        }
144    }
145}
146
147// ----------------------------------------------------------------------------
148// Opts
149
150#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
151pub struct Opts {
152    #[serde(rename = "name")]
153    pub name: Option<String>,
154    #[serde(rename = "description")]
155    pub description: Option<String>,
156    #[serde(rename = "tag")]
157    pub tag: Option<String>,
158    #[serde(rename = "platform")]
159    pub platform: Platform,
160}
161
162// ----------------------------------------------------------------------------
163// DeploymentCreation
164
165#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
166pub struct DeploymentCreation {
167    #[serde(rename = "id")]
168    pub id: String,
169    #[serde(rename = "functionId")]
170    pub function_id: String,
171    #[serde(rename = "name")]
172    pub name: Option<String>,
173    #[serde(rename = "description")]
174    pub description: Option<String>,
175    #[serde(rename = "tag")]
176    pub tag: Option<String>,
177    #[serde(rename = "platform")]
178    pub platform: Platform,
179    #[serde(rename = "status")]
180    pub status: Status,
181    #[serde(rename = "errorReason")]
182    pub reason: Option<String>,
183    #[serde(rename = "uploadUrl")]
184    pub upload_url: String,
185    #[serde(rename = "createdAt")]
186    pub created_at: DateTime<Utc>,
187    #[serde(rename = "updatedAt")]
188    pub updated_at: DateTime<Utc>,
189}
190
191// ----------------------------------------------------------------------------
192// Deployment
193
194#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
195pub struct Deployment {
196    #[serde(rename = "id")]
197    pub id: String,
198    #[serde(rename = "functionId")]
199    pub function_id: String,
200    #[serde(rename = "name")]
201    pub name: Option<String>,
202    #[serde(rename = "description")]
203    pub description: Option<String>,
204    #[serde(rename = "tag")]
205    pub tag: Option<String>,
206    #[serde(rename = "platform")]
207    pub platform: Platform,
208    #[serde(rename = "status")]
209    pub status: Status,
210    #[serde(rename = "errorReason")]
211    pub reason: Option<String>,
212    #[serde(rename = "url")]
213    pub url: Option<String>,
214    #[serde(rename = "createdAt")]
215    pub created_at: DateTime<Utc>,
216    #[serde(rename = "updatedAt")]
217    pub updated_at: DateTime<Utc>,
218}
219
220// ----------------------------------------------------------------------------
221// Helpers
222
223#[cfg_attr(feature = "tracing", tracing::instrument)]
224/// returns the list of deployments for a function
225pub async fn list(
226    client: &Client,
227    organisation_id: &str,
228    function_id: &str,
229) -> Result<Vec<Deployment>, Error> {
230    let path = format!(
231        "{}/v4/functions/organisations/{organisation_id}/functions/{function_id}/deployments",
232        client.endpoint
233    );
234
235    #[cfg(feature = "logging")]
236    if log_enabled!(Level::Debug) {
237        debug!(
238            "execute a request to list deployments for functions, path: '{path}', organisation: '{organisation_id}', function_id: '{function_id}'"
239        );
240    }
241
242    client
243        .get(&path)
244        .await
245        .map_err(|err| Error::List(function_id.to_string(), organisation_id.to_string(), err))
246}
247
248#[cfg_attr(feature = "tracing", tracing::instrument)]
249/// create a deployment on the given function
250pub async fn create(
251    client: &Client,
252    organisation_id: &str,
253    function_id: &str,
254    opts: &Opts,
255) -> Result<DeploymentCreation, Error> {
256    let path = format!(
257        "{}/v4/functions/organisations/{organisation_id}/functions/{function_id}/deployments",
258        client.endpoint
259    );
260
261    #[cfg(feature = "logging")]
262    if log_enabled!(Level::Debug) {
263        debug!(
264            "execute a request to create deployment, path: '{path}', organisation: {organisation_id}, function_id: '{function_id}'"
265        );
266    }
267
268    client
269        .post(&path, opts)
270        .await
271        .map_err(|err| Error::Create(function_id.to_string(), organisation_id.to_string(), err))
272}
273
274#[cfg_attr(feature = "tracing", tracing::instrument)]
275/// returns the deployment information of the function
276pub async fn get(
277    client: &Client,
278    organisation_id: &str,
279    function_id: &str,
280    deployment_id: &str,
281) -> Result<Deployment, Error> {
282    let path = format!(
283        "{}/v4/functions/organisations/{organisation_id}/functions/{function_id}/deployments/{deployment_id}",
284        client.endpoint
285    );
286
287    #[cfg(feature = "logging")]
288    if log_enabled!(Level::Debug) {
289        debug!(
290            "execute a request to get deployment, path: '{path}', organisation: {organisation_id}, function: {function_id}, deployment: {deployment_id}"
291        );
292    }
293
294    client.get(&path).await.map_err(|err| {
295        Error::Get(
296            deployment_id.to_string(),
297            function_id.to_string(),
298            organisation_id.to_string(),
299            err,
300        )
301    })
302}
303
304#[cfg_attr(feature = "tracing", tracing::instrument)]
305/// trigger the deployment of the function once the WebAssembly has been uploaded
306pub async fn trigger(
307    client: &Client,
308    organisation_id: &str,
309    function_id: &str,
310    deployment_id: &str,
311) -> Result<(), Error> {
312    let path = format!(
313        "{}/v4/functions/organisations/{organisation_id}/functions/{function_id}/deployments/{deployment_id}/trigger",
314        client.endpoint
315    );
316
317    #[cfg(feature = "logging")]
318    if log_enabled!(Level::Debug) {
319        debug!(
320            "execute a request to get deployment, path: '{path}', organisation: {organisation_id}, function: {function_id}, deployment: {deployment_id}"
321        );
322    }
323
324    let req = reqwest::Request::new(
325        Method::POST,
326        path.parse().map_err(|err| Error::ParseUrl(path, err))?,
327    );
328
329    let res = client.execute(req).await.map_err(Error::Execute)?;
330    let status = res.status();
331    if !status.is_success() {
332        return Err(Error::StatusCode(status.as_u16()));
333    }
334
335    Ok(())
336}
337
338#[cfg_attr(feature = "tracing", tracing::instrument)]
339/// Upload the WebAssembly on the endpoint
340pub async fn upload(client: &Client, endpoint: &str, buf: Vec<u8>) -> Result<(), Error> {
341    let mut req = reqwest::Request::new(
342        Method::PUT,
343        endpoint
344            .parse()
345            .map_err(|err| Error::ParseUrl(endpoint.to_string(), err))?,
346    );
347
348    req.headers_mut().insert(
349        CONTENT_TYPE,
350        HeaderValue::from_static(MIME_APPLICATION_WASM),
351    );
352    req.headers_mut()
353        .insert(CONTENT_LENGTH, HeaderValue::from(buf.len()));
354    *req.body_mut() = Some(Body::from(buf));
355
356    #[cfg(feature = "logging")]
357    if log_enabled!(Level::Debug) {
358        debug!("execute a request to upload webassembly, endpoint: '{endpoint}'");
359    }
360
361    let res = client
362        .inner()
363        .execute(req)
364        .await
365        .map_err(|err| Error::Execute(ClientError::Request(err)))?;
366
367    let status = res.status();
368    if !status.is_success() {
369        return Err(Error::StatusCode(status.as_u16()));
370    }
371
372    Ok(())
373}
374
375#[cfg_attr(feature = "tracing", tracing::instrument)]
376/// delete the deployment from the function
377pub async fn delete(
378    client: &Client,
379    organisation_id: &str,
380    function_id: &str,
381    deployment_id: &str,
382) -> Result<(), Error> {
383    let path = format!(
384        "{}/v4/functions/organisations/{organisation_id}/functions/{function_id}/deployments/{deployment_id}",
385        client.endpoint
386    );
387
388    #[cfg(feature = "logging")]
389    if log_enabled!(Level::Debug) {
390        debug!(
391            "execute a request to delete deployment, path: '{path}', organisation: {organisation_id}, function: {function_id}, deployment: {deployment_id}"
392        );
393    }
394
395    client.delete(&path).await.map_err(|err| {
396        Error::Delete(
397            deployment_id.to_string(),
398            function_id.to_string(),
399            organisation_id.to_string(),
400            err,
401        )
402    })
403}