clevercloud_sdk/v4/functions/
mod.rs

1//! # Functions module
2//!
3//! This module provides all structures and helpers to interact with functions
4//! product at Clever Cloud.
5
6use std::{collections::BTreeMap, fmt::Debug};
7
8use chrono::{DateTime, Utc};
9use log::{Level, debug, log_enabled};
10use oauth10a::client::{
11    ClientError, RestClient,
12    bytes::Buf,
13    reqwest::{self, Method},
14    url,
15};
16use serde::{Deserialize, Serialize};
17
18use crate::Client;
19
20pub mod deployments;
21
22// -----------------------------------------------------------------------------
23// Error
24
25#[derive(thiserror::Error, Debug)]
26pub enum Error {
27    #[error("failed to parse endpoint '{0}', {1}")]
28    ParseUrl(String, url::ParseError),
29    #[error("failed to list functions for organisation '{0}', {1}")]
30    List(String, ClientError),
31    #[error("failed to create function on organisation '{0}', {1}")]
32    Create(String, ClientError),
33    #[error("failed to get function '{0}' for organisation '{1}', {2}")]
34    Get(String, String, ClientError),
35    #[error("failed to update function '{0}' of organisation '{1}', {2}")]
36    Update(String, String, ClientError),
37    #[error("failed to delete function '{0}' of organisation '{1}', {2}")]
38    Delete(String, String, ClientError),
39    #[error("failed to aggregate body, {0}")]
40    BodyAggregation(reqwest::Error),
41    #[error("failed to deserialize execute response payload, {0}")]
42    Deserialize(serde_json::Error),
43    #[error("failed to execute request, {0}")]
44    Execute(reqwest::Error),
45    #[error("failed to execute request, got status code {0}")]
46    StatusCode(u16),
47}
48
49// -----------------------------------------------------------------------------
50// CreateOpts structure
51
52#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
53pub struct Opts {
54    #[serde(rename = "name")]
55    pub name: Option<String>,
56    #[serde(rename = "description")]
57    pub description: Option<String>,
58    #[serde(rename = "tag")]
59    pub tag: Option<String>,
60    #[serde(rename = "environment")]
61    pub environment: BTreeMap<String, String>,
62    #[serde(rename = "maxMemory")]
63    pub max_memory: u64,
64    #[serde(rename = "maxInstances")]
65    pub max_instances: u64,
66}
67
68impl Default for Opts {
69    fn default() -> Self {
70        Self {
71            name: None,
72            description: None,
73            tag: None,
74            environment: BTreeMap::new(),
75            max_memory: 512 * 1024 * 1024,
76            max_instances: 1,
77        }
78    }
79}
80
81// -----------------------------------------------------------------------------
82// Function structure
83
84#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
85pub struct Function {
86    #[serde(rename = "id")]
87    pub id: String,
88    #[serde(rename = "ownerId")]
89    pub owner_id: String,
90    #[serde(rename = "name", skip_serializing_if = "Option::is_none")]
91    pub name: Option<String>,
92    #[serde(rename = "description", skip_serializing_if = "Option::is_none")]
93    pub description: Option<String>,
94    #[serde(rename = "tag", skip_serializing_if = "Option::is_none")]
95    pub tag: Option<String>,
96    #[serde(rename = "environment")]
97    pub environment: BTreeMap<String, String>,
98    #[serde(rename = "maxMemory")]
99    pub max_memory: u64,
100    #[serde(rename = "maxInstances")]
101    pub max_instances: u64,
102    #[serde(rename = "createdAt")]
103    pub created_at: DateTime<Utc>,
104    #[serde(rename = "updatedAt")]
105    pub updated_at: DateTime<Utc>,
106}
107
108// -----------------------------------------------------------------------------
109// ExecuteResult structure
110
111#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
112#[serde(untagged)]
113pub enum ExecutionResult {
114    Ok {
115        #[serde(rename = "stdout")]
116        stdout: String,
117        #[serde(rename = "stderr")]
118        stderr: String,
119        #[serde(rename = "dmesg")]
120        dmesg: String,
121        #[serde(rename = "current_pages")]
122        current_pages: Option<u64>,
123    },
124    Err {
125        #[serde(rename = "error")]
126        error: String,
127    },
128}
129
130impl ExecutionResult {
131    #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
132    pub fn ok<T, U, V>(stdout: T, stderr: U, dmesg: V, current_pages: Option<u64>) -> Self
133    where
134        T: ToString,
135        U: ToString,
136        V: ToString,
137    {
138        Self::Ok {
139            stdout: stdout.to_string(),
140            stderr: stderr.to_string(),
141            dmesg: dmesg.to_string(),
142            current_pages,
143        }
144    }
145
146    #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
147    pub fn err<T>(error: T) -> Self
148    where
149        T: ToString,
150    {
151        Self::Err {
152            error: error.to_string(),
153        }
154    }
155
156    #[cfg_attr(feature = "tracing", tracing::instrument)]
157    pub fn is_ok(&self) -> bool {
158        matches!(self, Self::Ok { .. })
159    }
160
161    #[cfg_attr(feature = "tracing", tracing::instrument)]
162    pub fn is_err(&self) -> bool {
163        !self.is_ok()
164    }
165}
166
167// -----------------------------------------------------------------------------
168// Helpers
169
170#[cfg_attr(feature = "tracing", tracing::instrument)]
171/// returns the list of function for an organisation
172pub async fn list(client: &Client, organisation_id: &str) -> Result<Vec<Function>, Error> {
173    let path = format!(
174        "{}/v4/functions/organisations/{organisation_id}/functions",
175        client.endpoint
176    );
177
178    #[cfg(feature = "logging")]
179    if log_enabled!(Level::Debug) {
180        debug!(
181            "execute a request to list functions for organisation, path: '{path}', organisation: '{organisation_id}'"
182        );
183    }
184
185    client
186        .get(&path)
187        .await
188        .map_err(|err| Error::List(organisation_id.to_string(), err))
189}
190
191#[cfg_attr(feature = "tracing", tracing::instrument)]
192/// create a function on the given organisation
193pub async fn create(
194    client: &Client,
195    organisation_id: &str,
196    opts: &Opts,
197) -> Result<Function, Error> {
198    let path = format!(
199        "{}/v4/functions/organisations/{organisation_id}/functions",
200        client.endpoint
201    );
202
203    #[cfg(feature = "logging")]
204    if log_enabled!(Level::Debug) {
205        debug!(
206            "execute a request to create function, path: '{path}', organisation: {organisation_id}"
207        );
208    }
209
210    client
211        .post(&path, opts)
212        .await
213        .map_err(|err| Error::Create(organisation_id.to_string(), err))
214}
215
216#[cfg_attr(feature = "tracing", tracing::instrument)]
217/// returns the function information of the organisation
218pub async fn get(
219    client: &Client,
220    organisation_id: &str,
221    function_id: &str,
222) -> Result<Function, Error> {
223    let path = format!(
224        "{}/v4/functions/organisations/{organisation_id}/functions/{function_id}",
225        client.endpoint
226    );
227
228    #[cfg(feature = "logging")]
229    if log_enabled!(Level::Debug) {
230        debug!(
231            "execute a request to get function, path: '{path}', organisation: {organisation_id}, function: {function_id}"
232        );
233    }
234
235    client
236        .get(&path)
237        .await
238        .map_err(|err| Error::Get(function_id.to_string(), organisation_id.to_string(), err))
239}
240
241#[cfg_attr(feature = "tracing", tracing::instrument)]
242/// Update the function information of the organisation
243pub async fn update(
244    client: &Client,
245    organisation_id: &str,
246    function_id: &str,
247    opts: &Opts,
248) -> Result<Function, Error> {
249    let path = format!(
250        "{}/v4/functions/organisations/{organisation_id}/functions/{function_id}",
251        client.endpoint
252    );
253
254    #[cfg(feature = "logging")]
255    if log_enabled!(Level::Debug) {
256        debug!(
257            "execute a request to update function, path: '{path}', organisation: {organisation_id}, function: {function_id}"
258        );
259    }
260
261    client
262        .put(&path, opts)
263        .await
264        .map_err(|err| Error::Update(function_id.to_string(), organisation_id.to_string(), err))
265}
266
267#[cfg_attr(feature = "tracing", tracing::instrument)]
268/// returns the function information of the organisation
269pub async fn delete(
270    client: &Client,
271    organisation_id: &str,
272    function_id: &str,
273) -> Result<(), Error> {
274    let path = format!(
275        "{}/v4/functions/organisations/{organisation_id}/functions/{function_id}",
276        client.endpoint
277    );
278
279    #[cfg(feature = "logging")]
280    if log_enabled!(Level::Debug) {
281        debug!(
282            "execute a request to delete function, path: '{path}', organisation: {organisation_id}, function: {function_id}"
283        );
284    }
285
286    client
287        .delete(&path)
288        .await
289        .map_err(|err| Error::Delete(function_id.to_string(), organisation_id.to_string(), err))
290}
291
292#[cfg_attr(feature = "tracing", tracing::instrument)]
293/// Execute a GET HTTP request on the given endpoint
294pub async fn execute(client: &Client, endpoint: &str) -> Result<ExecutionResult, Error> {
295    let req = reqwest::Request::new(
296        Method::GET,
297        endpoint
298            .parse()
299            .map_err(|err| Error::ParseUrl(endpoint.to_string(), err))?,
300    );
301
302    let res = client.inner().execute(req).await.map_err(Error::Execute)?;
303    let buf = res.bytes().await.map_err(Error::BodyAggregation)?;
304
305    serde_json::from_reader(buf.reader()).map_err(Error::Deserialize)
306}