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