├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── Cargo.toml ├── LICENSE ├── README.md ├── examples └── cleverctl │ ├── Cargo.toml │ ├── README.md │ ├── rust-toolchain │ └── src │ ├── cfg.rs │ ├── cmd │ ├── addon │ │ ├── config_provider │ │ │ ├── environment.rs │ │ │ └── mod.rs │ │ └── mod.rs │ ├── functions │ │ ├── deployments.rs │ │ └── mod.rs │ ├── mod.rs │ ├── myself.rs │ └── zone.rs │ ├── logging.rs │ └── main.rs ├── rust-toolchain └── src ├── lib.rs ├── v2 ├── addon.rs ├── mod.rs ├── myself.rs └── plan.rs └── v4 ├── addon_provider ├── config_provider │ ├── addon │ │ ├── environment.rs │ │ └── mod.rs │ └── mod.rs ├── elasticsearch.rs ├── mod.rs ├── mongodb.rs ├── mysql.rs ├── postgresql.rs └── redis.rs ├── functions ├── deployments.rs └── mod.rs ├── mod.rs └── products ├── mod.rs └── zones.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Continuous integration 3 | on: [push, pull_request] 4 | jobs: 5 | build: 6 | name: Build library 7 | runs-on: ubuntu-latest 8 | strategy: 9 | fail-fast: true 10 | matrix: 11 | rust: 12 | - 1.85.0 13 | - stable 14 | - beta 15 | - nightly 16 | steps: 17 | - uses: actions/checkout@v2 18 | - uses: actions-rs/toolchain@v1 19 | with: 20 | toolchain: ${{ matrix.rust }} 21 | profile: minimal 22 | override: true 23 | - uses: actions-rs/cargo@v1 24 | with: 25 | command: build 26 | args: --verbose --features metrics --features jsonschemas --features tracing 27 | test: 28 | name: Test library 29 | runs-on: ubuntu-latest 30 | strategy: 31 | fail-fast: true 32 | matrix: 33 | rust: 34 | - 1.85.0 35 | - stable 36 | - beta 37 | - nightly 38 | steps: 39 | - uses: actions/checkout@v2 40 | - uses: actions-rs/toolchain@v1 41 | with: 42 | toolchain: ${{ matrix.rust }} 43 | profile: minimal 44 | override: true 45 | - uses: actions-rs/cargo@v1 46 | with: 47 | command: install 48 | args: cargo-tarpaulin 49 | - uses: actions-rs/cargo@v1 50 | with: 51 | command: tarpaulin 52 | args: --verbose 53 | format: 54 | name: Format source code 55 | runs-on: ubuntu-latest 56 | steps: 57 | - uses: actions/checkout@v2 58 | - uses: actions-rs/toolchain@v1 59 | with: 60 | toolchain: stable 61 | profile: minimal 62 | override: true 63 | - run: rustup component add rustfmt 64 | - uses: actions-rs/cargo@v1 65 | with: 66 | command: fmt 67 | args: --verbose --all -- --check 68 | clippy: 69 | name: Lint source code 70 | runs-on: ubuntu-latest 71 | steps: 72 | - uses: actions/checkout@v2 73 | - uses: actions-rs/toolchain@v1 74 | with: 75 | toolchain: stable 76 | profile: minimal 77 | override: true 78 | - run: rustup component add clippy 79 | - uses: actions-rs/cargo@v1 80 | with: 81 | command: clippy 82 | args: --verbose --features metrics --features jsonschemas --features tracing -- -D warnings 83 | doc: 84 | name: Build documentation 85 | runs-on: ubuntu-latest 86 | steps: 87 | - uses: actions/checkout@v2 88 | - uses: actions-rs/toolchain@v1 89 | with: 90 | toolchain: stable 91 | profile: minimal 92 | override: true 93 | - uses: actions-rs/cargo@v1 94 | with: 95 | command: doc 96 | args: --verbose --features metrics --features jsonschemas --features tracing 97 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Rust ### 2 | # Generated by Cargo 3 | # will have compiled files and executables 4 | debug/ 5 | target/ 6 | 7 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 8 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 9 | Cargo.lock 10 | !examples/cli/Cargo.lock 11 | 12 | # These are backup files generated by rustfmt 13 | **/*.rs.bk 14 | 15 | # MSVC Windows builds of rustc generate these, which store debugging information 16 | *.pdb 17 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | support@clever-cloud.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "clevercloud-sdk" 3 | description = "A rust client and structures to interact with the Clever-Cloud API." 4 | version = "0.15.0" 5 | edition = "2024" 6 | rust-version = "1.85.0" 7 | authors = ["Florentin Dubois "] 8 | license-file = "LICENSE" 9 | readme = "README.md" 10 | repository = "https://github.com/CleverCloud/clevercloud-sdk-rust" 11 | keywords = ["clevercloud", "sdk", "logging", "metrics", "jsonschemas"] 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [dependencies] 16 | chrono = { version = "^0.4.40", features = ["serde"] } 17 | oauth10a = { version = "^2.1.1", default-features = false, features = [ 18 | "client", 19 | ] } 20 | log = { version = "^0.4.26", optional = true } 21 | schemars = { version = "^0.8.22", features = [ 22 | "chrono", 23 | "indexmap1", 24 | "uuid1", 25 | "bytes", 26 | "url", 27 | ], optional = true } 28 | serde = { version = "^1.0.219", features = ["derive"] } 29 | serde_repr = "^0.1.20" 30 | serde_json = "^1.0.140" 31 | thiserror = "^2.0.12" 32 | tracing = { version = "^0.1.41", optional = true } 33 | uuid = { version = "^1.16.0", features = ["serde", "v4"] } 34 | 35 | [features] 36 | default = ["logging"] 37 | jsonschemas = ["schemars"] 38 | logging = ["oauth10a/logging", "tracing/log-always", "log"] 39 | metrics = ["oauth10a/metrics"] 40 | tracing = ["oauth10a/tracing", "dep:tracing"] 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Clever Cloud 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Clever-Cloud Software Development Kit - Rust edition 2 | 3 | [![crates.io](https://img.shields.io/crates/v/clevercloud-sdk.svg)](https://crates.io/crates/clevercloud-sdk) 4 | [![Released API docs](https://docs.rs/clevercloud-sdk/badge.svg)](https://docs.rs/clevercloud-sdk) 5 | [![Continuous integration](https://github.com/CleverCloud/clevercloud-sdk-rust/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/CleverCloud/clevercloud-sdk-rust/actions/workflows/ci.yml) 6 | 7 | > This crate provides structures and a client to interact with the Clever-Cloud 8 | > API. 9 | 10 | ## Status 11 | 12 | This crate is under development, you can use it, but it may have bugs or unimplemented features. 13 | 14 | ## Installation 15 | 16 | To install this dependency, just add the following line to your `Cargo.toml` manifest. 17 | 18 | ```toml 19 | clevercloud-sdk = { version = "^0.11.1", features = ["metrics", "jsonschemas"] } 20 | ``` 21 | 22 | ## Usage 23 | 24 | Below, you will find an example of executing a request to get information about 25 | myself. 26 | 27 | ```rust 28 | use std::error::Error; 29 | 30 | use clevercloud_sdk::{Client, v2::myself::{self, Myself}}; 31 | 32 | #[tokio::main] 33 | async fn main() -> Result<(), Box> { 34 | let client = Client::from(Credentials { 35 | token: "".to_string(), 36 | secret: "".to_string(), 37 | consumer_key: "".to_string(), 38 | consumer_secret: "".to_string(), 39 | }); 40 | 41 | let _myself: Myself = myself::get(&client).await?; 42 | 43 | Ok(()) 44 | } 45 | ``` 46 | 47 | You could found more examples of how you could use the clevercloud-sdk by looking at the [command line](examples/cli/README.md) example. 48 | 49 | ## Features 50 | 51 | | name | description | 52 | | ----------- |--------------------------------------------------------------------------------------------------| 53 | | trace | Use `tracing` crate to expose traces | 54 | | jsonschemas | Use `schemars` to add a derive instruction to generate json schemas representation of structures | 55 | | logging | Use the `log` facility crate to print logs. Implies `oauth10a/logging` feature | 56 | | metrics | Expose HTTP metrics through `oauth10a` crate feature. | 57 | 58 | ### Metrics 59 | 60 | Below, the exposed metrics gathered by prometheus: 61 | 62 | | name | labels | kind | description | 63 | | -------------------------------- | --------------------------------------------------------------- | ------- | -------------------------- | 64 | | oauth10a_client_request | endpoint: String, method: String, status: Integer | Counter | number of request on api | 65 | | oauth10a_client_request_duration | endpoint: String, method: String, status: Integer, unit: String | Counter | duration of request on api | 66 | 67 | ## License 68 | 69 | See the [license](LICENSE). 70 | 71 | ## Getting in touch 72 | 73 | - [@FlorentinDUBOIS](https://twitter.com/FlorentinDUBOIS) 74 | -------------------------------------------------------------------------------- /examples/cleverctl/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cleverctl" 3 | description = "A command line interface that use the clevercloud-sdk" 4 | authors = ["Florentin Dubois "] 5 | rust-version = "1.85.0" 6 | version = "0.15.0" 7 | edition = "2024" 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | clevercloud-sdk = { version = "0.15.0", path = "../..", features = [ 13 | "metrics", 14 | "tracing", 15 | "jsonschemas", 16 | ] } 17 | clap = { version = "^4.5.34", features = ["derive"] } 18 | config = "0.15.11" 19 | paw = "^1.0.0" 20 | serde = { version = "^1.0.218", features = ["derive"] } 21 | serde_json = "^1.0.140" 22 | serde_yaml = "^0.9.33" 23 | thiserror = "^2.0.12" 24 | tokio = { version = "^1.44.1", features = ["full"] } 25 | tracing = "^0.1.41" 26 | tracing-subscriber = { version = "^0.3.19", default-features = false, features = [ 27 | "std", 28 | "ansi", 29 | "tracing-log", 30 | ] } 31 | 32 | [profile.release] 33 | lto = true 34 | opt-level = 'z' 35 | codegen-units = 1 36 | -------------------------------------------------------------------------------- /examples/cleverctl/README.md: -------------------------------------------------------------------------------- 1 | # Command line interface example 2 | 3 | > An example of command line interface written in Rust using the clevercloud-sdk. 4 | 5 | ## Installation 6 | 7 | To install the command line interface, you will need the same requisites that 8 | the software development kit. 9 | 10 | Firstly, we will clone the git repository using the following command. 11 | 12 | ```shell 13 | $ git clone https://github.com/CleverCloud/clevercloud-sdk-rust.git 14 | ``` 15 | 16 | Then, go into the command line interface example. 17 | 18 | ```shell 19 | $ cd clevercloud-sdk-rust/examples/cleverctl 20 | ``` 21 | 22 | Now, we are able to build the command line interface. 23 | 24 | ```shell 25 | $ cargo build --release 26 | ``` 27 | 28 | The binary of the command line interface will be located at the following path 29 | `target/release/cli`. 30 | 31 | ## Usage 32 | 33 | Once, the command line interface is built, you can use it like this: 34 | 35 | ```shell 36 | $ target/release/cleverctl -h 37 | cleverctl 0.10.9 38 | Command enum contains all operations that the command line could handle 39 | 40 | USAGE: 41 | cleverctl [FLAGS] [OPTIONS] 42 | 43 | FLAGS: 44 | -t, --check Check the healthiness of the configuration 45 | -h, --help Prints help information 46 | -V, --version Prints version information 47 | -v Increase log verbosity 48 | 49 | OPTIONS: 50 | -c, --config Specify a configuration file 51 | 52 | SUBCOMMANDS: 53 | addon Interact with addons 54 | help Prints this message or the help of the given subcommand(s) 55 | self Interact with the current user 56 | ``` 57 | 58 | ## Get in touch 59 | 60 | - [@FlorentinDUBOIS](https://twitter.com/FlorentinDUBOIS) 61 | -------------------------------------------------------------------------------- /examples/cleverctl/rust-toolchain: -------------------------------------------------------------------------------- 1 | 1.85.0 2 | -------------------------------------------------------------------------------- /examples/cleverctl/src/cfg.rs: -------------------------------------------------------------------------------- 1 | //! # Configuration module 2 | //! 3 | //! This module provides utilities to retrieve and parse configuration 4 | 5 | use std::path::PathBuf; 6 | 7 | use clevercloud_sdk::Credentials; 8 | use config::{Config, ConfigError, File}; 9 | use serde::{Deserialize, Serialize}; 10 | 11 | // ----------------------------------------------------------------------------- 12 | // Error enumeration 13 | 14 | #[derive(thiserror::Error, Debug)] 15 | pub enum Error { 16 | #[error("failed to load configuration from file '{0}', {1}")] 17 | LoadConfiguration(String, ConfigError), 18 | #[error("failed to load configuration from default paths, {0}")] 19 | LoadDefaultConfiguration(ConfigError), 20 | #[error("failed to cast configuration, {0}")] 21 | Cast(ConfigError), 22 | } 23 | 24 | // ----------------------------------------------------------------------------- 25 | // Configuration structure 26 | 27 | #[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Debug)] 28 | pub struct Configuration { 29 | #[serde(rename = "credentials", flatten)] 30 | pub credentials: Credentials, 31 | } 32 | 33 | impl TryFrom<&PathBuf> for Configuration { 34 | type Error = Error; 35 | 36 | fn try_from(pb: &PathBuf) -> Result { 37 | Config::builder() 38 | .add_source(File::from(pb.as_path()).required(true)) 39 | .build() 40 | .map_err(|err| Error::LoadConfiguration(pb.display().to_string(), err))? 41 | .try_deserialize() 42 | .map_err(Error::Cast) 43 | } 44 | } 45 | 46 | impl Configuration { 47 | pub fn try_default() -> Result { 48 | let homedir = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string()); 49 | let paths = [ 50 | format!("/usr/share/{}/config", env!("CARGO_PKG_NAME")), 51 | format!("/etc/{}/config", env!("CARGO_PKG_NAME")), 52 | format!( 53 | "{}/.local/usr/share/{}/config", 54 | homedir, 55 | env!("CARGO_PKG_NAME") 56 | ), 57 | format!("{}/.local/etc/{}/config", homedir, env!("CARGO_PKG_NAME")), 58 | format!("{}/.config/{}/config", homedir, env!("CARGO_PKG_NAME")), 59 | format!("{}/.config/clever-cloud/clever-tools", homedir), 60 | "config".to_string(), 61 | ]; 62 | 63 | Config::builder() 64 | .add_source( 65 | paths 66 | .iter() 67 | .map(|path| File::with_name(path).required(false)) 68 | .collect::>(), 69 | ) 70 | .build() 71 | .map_err(Error::LoadDefaultConfiguration)? 72 | .try_deserialize() 73 | .map_err(Error::Cast) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /examples/cleverctl/src/cmd/addon/config_provider/environment.rs: -------------------------------------------------------------------------------- 1 | use std::{path::PathBuf, sync::Arc}; 2 | 3 | use clap::Subcommand; 4 | use clevercloud_sdk::{ 5 | Client, 6 | oauth10a::reqwest, 7 | v4::addon_provider::config_provider::addon::environment::{self, Variable}, 8 | }; 9 | use tokio::{fs, task::spawn_blocking as blocking}; 10 | 11 | use crate::{ 12 | cfg::Configuration, 13 | cmd::{self, Executor, Output}, 14 | }; 15 | 16 | // ----------------------------------------------------------------------------- 17 | // Error enumeration 18 | 19 | #[derive(thiserror::Error, Debug)] 20 | pub enum Error { 21 | #[error("failed to format output, {0}")] 22 | FormatOutput(Box), 23 | #[error("failed to get environment for config-provider, {0}")] 24 | Get(environment::Error), 25 | #[error("failed to update environment for config-provider, {0}")] 26 | Put(environment::Error), 27 | #[error("failed to create http client, {0}")] 28 | CreateClient(reqwest::Error), 29 | #[error("failed to read file, {0}")] 30 | Read(std::io::Error), 31 | #[error("failed to wait for thread to finish, {0}")] 32 | Join(tokio::task::JoinError), 33 | #[error("failed to serialize content of the file into json, {0}")] 34 | Serialize(serde_json::Error), 35 | } 36 | 37 | // ----------------------------------------------------------------------------- 38 | // Environment enumeration 39 | 40 | #[derive(Subcommand, PartialEq, Eq, Clone, Debug)] 41 | pub enum Environment { 42 | #[clap(name = "get", about = "Get environment variables")] 43 | Get { 44 | /// Specify the output format 45 | #[clap(short = 'o', long = "output", default_value_t)] 46 | output: Output, 47 | /// Specify the config-provider identifier 48 | #[clap(name = "config-provider-identifier")] 49 | id: String, 50 | }, 51 | #[clap(name = "insert", aliases = &["i"], about = "Insert an environment variable")] 52 | Insert { 53 | /// Specify the output format 54 | #[clap(short = 'o', long = "output", default_value_t)] 55 | output: Output, 56 | /// Specify the config-provider identifier 57 | #[clap(name = "config-provider-identifier")] 58 | id: String, 59 | /// Specify the name of the environment variable to insert 60 | #[clap(name = "name")] 61 | name: String, 62 | /// Specify the value of the environment variable to insert 63 | #[clap(name = "value")] 64 | value: String, 65 | }, 66 | #[clap(name = "put", about = "Update environment variables")] 67 | Put { 68 | /// Specify the output format 69 | #[clap(short = 'o', long = "output", default_value_t)] 70 | output: Output, 71 | /// Specify the config-provider identifier 72 | #[clap(name = "config-provider-identifier")] 73 | id: String, 74 | /// Specify the json file to read 75 | #[clap(name = "file")] 76 | file: PathBuf, 77 | }, 78 | #[clap(name = "remove", aliases = &["r"], about = "Remove an environment variable")] 79 | Remove { 80 | /// Specify the output format 81 | #[clap(short = 'o', long = "output", default_value_t)] 82 | output: Output, 83 | /// Specify the config-provider identifier 84 | #[clap(name = "config-provider-identifier")] 85 | id: String, 86 | /// Specify the name of the environment variable to remove 87 | #[clap(name = "name")] 88 | name: String, 89 | }, 90 | } 91 | 92 | impl Executor for Environment { 93 | type Error = Error; 94 | 95 | async fn execute(&self, config: Arc) -> Result<(), Self::Error> { 96 | match self { 97 | Self::Get { output, id } => get(config, output, id).await, 98 | Self::Insert { 99 | output, 100 | id, 101 | name, 102 | value, 103 | } => insert(config, output, id, name, value).await, 104 | Self::Put { output, id, file } => put(config, output, id, file).await, 105 | Self::Remove { output, id, name } => remove(config, output, id, name).await, 106 | } 107 | } 108 | } 109 | 110 | // ----------------------------------------------------------------------------- 111 | // Helpers 112 | 113 | pub async fn get(config: Arc, output: &Output, id: &str) -> Result<(), Error> { 114 | let client = Client::from(config.credentials.to_owned()); 115 | let variables = environment::get(&client, id).await.map_err(Error::Get)?; 116 | 117 | println!( 118 | "{}", 119 | output 120 | .format(&variables) 121 | .map_err(|err| Error::FormatOutput(Box::new(err)))? 122 | ); 123 | 124 | Ok(()) 125 | } 126 | 127 | pub async fn insert( 128 | config: Arc, 129 | output: &Output, 130 | id: &str, 131 | name: &str, 132 | value: &str, 133 | ) -> Result<(), Error> { 134 | let client = Client::from(config.credentials.to_owned()); 135 | let variables = environment::insert( 136 | &client, 137 | id, 138 | Variable::from((name.to_owned(), value.to_owned())), 139 | ) 140 | .await 141 | .map_err(Error::Get)?; 142 | 143 | println!( 144 | "{}", 145 | output 146 | .format(&variables) 147 | .map_err(|err| Error::FormatOutput(Box::new(err)))? 148 | ); 149 | 150 | Ok(()) 151 | } 152 | 153 | pub async fn put( 154 | config: Arc, 155 | output: &Output, 156 | id: &str, 157 | file: &PathBuf, 158 | ) -> Result<(), Error> { 159 | let content = fs::read_to_string(file).await.map_err(Error::Read)?; 160 | let variables = blocking(move || serde_json::from_str(&content)) 161 | .await 162 | .map_err(Error::Join)? 163 | .map_err(Error::Serialize)?; 164 | 165 | let client = Client::from(config.credentials.to_owned()); 166 | let variables = environment::put(&client, id, &variables) 167 | .await 168 | .map_err(Error::Put)?; 169 | 170 | println!( 171 | "{}", 172 | output 173 | .format(&variables) 174 | .map_err(|err| Error::FormatOutput(Box::new(err)))? 175 | ); 176 | 177 | Ok(()) 178 | } 179 | 180 | pub async fn remove( 181 | config: Arc, 182 | output: &Output, 183 | id: &str, 184 | name: &str, 185 | ) -> Result<(), Error> { 186 | let client = Client::from(config.credentials.to_owned()); 187 | let variables = environment::remove(&client, id, name) 188 | .await 189 | .map_err(Error::Get)?; 190 | 191 | println!( 192 | "{}", 193 | output 194 | .format(&variables) 195 | .map_err(|err| Error::FormatOutput(Box::new(err)))? 196 | ); 197 | 198 | Ok(()) 199 | } 200 | -------------------------------------------------------------------------------- /examples/cleverctl/src/cmd/addon/config_provider/mod.rs: -------------------------------------------------------------------------------- 1 | //! # ConfigProvider module 2 | //! 3 | //! This module provides commands to interact with ConfigProvider addon 4 | 5 | use std::sync::Arc; 6 | 7 | use clap::Subcommand; 8 | 9 | use crate::{ 10 | cfg::Configuration, 11 | cmd::{Executor, addon::config_provider::environment::Environment}, 12 | }; 13 | 14 | pub mod environment; 15 | 16 | // ----------------------------------------------------------------------------- 17 | // Error enumeration 18 | 19 | #[derive(thiserror::Error, Debug)] 20 | pub enum Error { 21 | #[error("failed to execute command on config-provider environment, {0}")] 22 | Environment(environment::Error), 23 | } 24 | 25 | // ----------------------------------------------------------------------------- 26 | // ConfigProvider structure 27 | 28 | #[derive(Subcommand, Eq, PartialEq, Clone, Debug)] 29 | pub enum ConfigProvider { 30 | #[clap(name = "environment", aliases = &["env"], subcommand, about = "Interact with config-provider environment")] 31 | Environment(Environment), 32 | } 33 | 34 | impl Executor for ConfigProvider { 35 | type Error = Error; 36 | 37 | async fn execute(&self, config: Arc) -> Result<(), Self::Error> { 38 | match self { 39 | Self::Environment(environment) => environment 40 | .execute(config) 41 | .await 42 | .map_err(Error::Environment), 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /examples/cleverctl/src/cmd/addon/mod.rs: -------------------------------------------------------------------------------- 1 | //! # Addon module 2 | //! 3 | //! This module provides command implementation related to addons 4 | use std::sync::Arc; 5 | 6 | use clap::Subcommand; 7 | use clevercloud_sdk::{Client, oauth10a::reqwest, v2::addon}; 8 | 9 | use crate::{ 10 | cfg::Configuration, 11 | cmd::{self, Executor, Output, addon::config_provider::ConfigProvider}, 12 | }; 13 | 14 | pub mod config_provider; 15 | 16 | // ----------------------------------------------------------------------------- 17 | // Error enumeration 18 | 19 | #[derive(thiserror::Error, Debug)] 20 | pub enum Error { 21 | #[error("failed to format output, {0}")] 22 | FormatOutput(Box), 23 | #[error("failed to list addons of organisation '{0}', {1}")] 24 | List(String, addon::Error), 25 | #[error("failed to get addon '{0}' of organisation '{1}', {2}")] 26 | Get(String, String, addon::Error), 27 | #[error("failed to create http client, {0}")] 28 | CreateClient(reqwest::Error), 29 | #[error("failed to execute command on config-provider addon, {0}")] 30 | ConfigProvider(config_provider::Error), 31 | } 32 | 33 | // ----------------------------------------------------------------------------- 34 | // Addon enumeration 35 | 36 | #[derive(Subcommand, Eq, PartialEq, Clone, Debug)] 37 | pub enum Command { 38 | #[clap(name = "list", about = "List addons of an organisation")] 39 | List { 40 | /// Specify the output format 41 | #[clap(short = 'o', long = "output", default_value_t)] 42 | output: Output, 43 | /// Specify the organisation identifier 44 | #[clap(name = "organisation-identifier")] 45 | organisation_id: String, 46 | }, 47 | #[clap(name = "get", about = "Get addon of an organisation")] 48 | Get { 49 | /// Specify the output format 50 | #[clap(short = 'o', long = "output", default_value_t)] 51 | output: Output, 52 | /// Specify the organisation identifier 53 | #[clap(name = "organisation-identifier")] 54 | organisation_id: String, 55 | /// Specify the addon identifier 56 | #[clap(name = "addon-identifier")] 57 | addon_id: String, 58 | }, 59 | #[clap(name = "config-provider", aliases = &["cp"], subcommand, about = "Interact with ConfigProvider addon")] 60 | ConfigProvider(ConfigProvider), 61 | } 62 | 63 | impl Executor for Command { 64 | type Error = Error; 65 | 66 | async fn execute(&self, config: Arc) -> Result<(), Self::Error> { 67 | match self { 68 | Self::List { 69 | output, 70 | organisation_id, 71 | } => list(config, output, organisation_id).await, 72 | Self::Get { 73 | output, 74 | organisation_id, 75 | addon_id, 76 | } => get(config, output, organisation_id, addon_id).await, 77 | Self::ConfigProvider(cmd) => cmd.execute(config).await.map_err(Error::ConfigProvider), 78 | } 79 | } 80 | } 81 | 82 | // ----------------------------------------------------------------------------- 83 | // helpers 84 | 85 | pub async fn list( 86 | config: Arc, 87 | output: &Output, 88 | organisation_id: &str, 89 | ) -> Result<(), Error> { 90 | let client = Client::from(config.credentials.to_owned()); 91 | let addons = addon::list(&client, organisation_id) 92 | .await 93 | .map_err(|err| Error::List(organisation_id.to_owned(), err))?; 94 | 95 | println!( 96 | "{}", 97 | output 98 | .format(&addons) 99 | .map_err(|err| Error::FormatOutput(Box::new(err)))? 100 | ); 101 | Ok(()) 102 | } 103 | 104 | pub async fn get( 105 | config: Arc, 106 | output: &Output, 107 | organisation_id: &str, 108 | addon_id: &str, 109 | ) -> Result<(), Error> { 110 | let client = Client::from(config.credentials.to_owned()); 111 | let addons = addon::get(&client, organisation_id, addon_id) 112 | .await 113 | .map_err(|err| Error::Get(addon_id.to_owned(), organisation_id.to_owned(), err))?; 114 | 115 | println!( 116 | "{}", 117 | output 118 | .format(&addons) 119 | .map_err(|err| Error::FormatOutput(Box::new(err)))? 120 | ); 121 | Ok(()) 122 | } 123 | -------------------------------------------------------------------------------- /examples/cleverctl/src/cmd/functions/deployments.rs: -------------------------------------------------------------------------------- 1 | //! # Deployment module 2 | //! 3 | //! This module provides all necessary commands to interact with a function's deployment including 4 | //! uploading and deploying it. 5 | 6 | use std::{collections::BTreeMap, path::PathBuf, sync::Arc}; 7 | 8 | use clap::Subcommand; 9 | use clevercloud_sdk::{ 10 | Client, 11 | oauth10a::reqwest, 12 | v4::functions::deployments::{self, Opts, Platform}, 13 | }; 14 | use tokio::fs::read; 15 | use tracing::info; 16 | 17 | use crate::{ 18 | cfg::Configuration, 19 | cmd::{self, Executor, Output, parse_btreemap}, 20 | }; 21 | 22 | // ---------------------------------------------------------------------------- 23 | // Error 24 | 25 | #[derive(thiserror::Error, Debug)] 26 | pub enum Error { 27 | #[error("failed to format output, {0}")] 28 | FormatOutput(Box), 29 | #[error("failed to create http client, {0}")] 30 | CreateClient(reqwest::Error), 31 | #[error("failed to list deployments of function '{0}' on organisation '{1}', {2}")] 32 | List(String, String, deployments::Error), 33 | #[error("failed to create deployment on function '{0}' for organisation '{1}', {2}")] 34 | Create(String, String, deployments::Error), 35 | #[error("failed to get deployment '{0}' of function '{1}' for organisation '{2}', {3}")] 36 | Get(String, String, String, deployments::Error), 37 | #[error("failed to delete deployment '{0}' of function '{1}' for organisation '{2}', {3}")] 38 | Delete(String, String, String, deployments::Error), 39 | #[error("failed to read file '{0}', {1}")] 40 | Read(String, std::io::Error), 41 | #[error( 42 | "failed to upload file located at '{0}' for deployment '{1}' of function '{2}' on organisation '{3}', {4}" 43 | )] 44 | Upload(String, String, String, String, deployments::Error), 45 | #[error("failed to trigger deployment '{0}' of function '{1}' for organisation '{2}', {3}")] 46 | Trigger(String, String, String, deployments::Error), 47 | } 48 | 49 | // ---------------------------------------------------------------------------- 50 | // Command 51 | 52 | #[derive(Subcommand, PartialEq, Eq, Clone, Debug)] 53 | pub enum Command { 54 | #[clap(name = "list", aliases = &["l"], about = "List functions information of an organisation")] 55 | List { 56 | /// Specify the output format 57 | #[clap(short = 'o', long = "output", default_value_t)] 58 | output: Output, 59 | /// Specify the organisation identifier 60 | #[clap(name = "organisation-identifier")] 61 | organisation_id: String, 62 | /// Specify the function identifier 63 | #[clap(name = "function-identifier")] 64 | function_id: String, 65 | }, 66 | #[clap(name = "create", aliases = &["c"], about = "Create a function within the given organisation")] 67 | Create { 68 | /// Specify the output format 69 | #[clap(short = 'o', long = "output", default_value_t)] 70 | output: Output, 71 | /// Specify the organisation identifier 72 | #[clap(name = "organisation-identifier")] 73 | organisation_id: String, 74 | /// Specify the function identifier 75 | #[clap(name = "function-identifier")] 76 | function_id: String, 77 | /// Specify a name for the function 78 | #[clap(short = 'n', long = "name")] 79 | name: Option, 80 | /// Specify a description for the function 81 | #[clap(short = 'd', long = "description")] 82 | description: Option, 83 | /// Specify tags for the function (format: k1=v1,k2=v2) 84 | #[clap(short = 't', long = "tags", value_parser = parse_btreemap)] 85 | tags: Option>, 86 | /// Specify the WebAssembly file to upload 87 | #[clap(short = 'f', long = "file")] 88 | file: PathBuf, 89 | /// Specify the language of the functions (available options are 'rust', 'javascript', 'tinygo' and 'assemblyscript') 90 | #[clap(short = 'p', long = "platform")] 91 | platform: Platform, 92 | }, 93 | #[clap(name = "get", aliases = &["g"], about = "Get information about a function")] 94 | Get { 95 | /// Specify the output format 96 | #[clap(short = 'o', long = "output", default_value_t)] 97 | output: Output, 98 | /// Specify the organisation identifier 99 | #[clap(name = "organisation-identifier")] 100 | organisation_id: String, 101 | /// Specify the function identifier 102 | #[clap(name = "function-identifier")] 103 | function_id: String, 104 | /// Specify the deployment identifier 105 | #[clap(name = "deployment-identifier")] 106 | deployment_id: String, 107 | }, 108 | #[clap(name = "delete", aliases = &["d"], about = "Delete a function")] 109 | Delete { 110 | /// Specify the organisation identifier 111 | #[clap(name = "organisation-identifier")] 112 | organisation_id: String, 113 | /// Function identifier 114 | #[clap(name = "function-id")] 115 | function_id: String, 116 | /// Specify the deployment identifier 117 | #[clap(name = "deployment-identifier")] 118 | deployment_id: String, 119 | }, 120 | } 121 | 122 | impl Executor for Command { 123 | type Error = Error; 124 | 125 | async fn execute(&self, config: Arc) -> Result<(), Self::Error> { 126 | match self { 127 | Self::List { 128 | output, 129 | organisation_id, 130 | function_id, 131 | } => list(config, output, organisation_id, function_id).await, 132 | Self::Create { 133 | output, 134 | organisation_id, 135 | function_id, 136 | name, 137 | description, 138 | tags, 139 | file, 140 | platform, 141 | } => { 142 | let tag = tags.as_ref().map(|tags| { 143 | tags.iter() 144 | .fold(vec![], |mut acc, (k, v)| { 145 | acc.push(format!("{k}:{v}")); 146 | acc 147 | }) 148 | .join(" ") 149 | }); 150 | 151 | let opts = deployments::Opts { 152 | name: name.to_owned(), 153 | description: description.to_owned(), 154 | tag, 155 | platform: platform.to_owned(), 156 | }; 157 | 158 | create(config, output, organisation_id, function_id, file, &opts).await 159 | } 160 | Self::Get { 161 | output, 162 | organisation_id, 163 | function_id, 164 | deployment_id, 165 | } => get(config, output, organisation_id, function_id, deployment_id).await, 166 | Self::Delete { 167 | organisation_id, 168 | function_id, 169 | deployment_id, 170 | } => delete(config, organisation_id, function_id, deployment_id).await, 171 | } 172 | } 173 | } 174 | 175 | // ------------------------------------------------------------------------------------------------- 176 | // Commands 177 | 178 | pub async fn list( 179 | config: Arc, 180 | output: &Output, 181 | organisation_id: &str, 182 | function_id: &str, 183 | ) -> Result<(), Error> { 184 | let client = Client::from(config.credentials.to_owned()); 185 | let deploymentz = deployments::list(&client, organisation_id, function_id) 186 | .await 187 | .map_err(|err| Error::List(function_id.to_string(), organisation_id.to_string(), err))?; 188 | 189 | println!( 190 | "{}", 191 | output 192 | .format(&deploymentz) 193 | .map_err(|err| Error::FormatOutput(Box::new(err)))? 194 | ); 195 | 196 | Ok(()) 197 | } 198 | 199 | pub async fn create( 200 | config: Arc, 201 | output: &Output, 202 | organisation_id: &str, 203 | function_id: &str, 204 | file: &PathBuf, 205 | opts: &Opts, 206 | ) -> Result<(), Error> { 207 | let client = Client::from(config.credentials.to_owned()); 208 | 209 | info!( 210 | organisation_id = organisation_id, 211 | function_id = function_id, 212 | "Create deployment for function" 213 | ); 214 | let deployment_c = deployments::create(&client, organisation_id, function_id, opts) 215 | .await 216 | .map_err(|err| Error::Create(function_id.to_string(), organisation_id.to_string(), err))?; 217 | 218 | info!( 219 | organisation_id = organisation_id, 220 | function_id = function_id, 221 | file = file.display().to_string(), 222 | deployment_id = deployment_c.id, 223 | "Read WebAssembly to a buffer" 224 | ); 225 | let buf = read(file) 226 | .await 227 | .map_err(|err| Error::Read(file.display().to_string(), err))?; 228 | 229 | info!( 230 | organisation_id = organisation_id, 231 | function_id = function_id, 232 | file = file.display().to_string(), 233 | deployment_id = deployment_c.id, 234 | "Upload WebAssembly for deployment" 235 | ); 236 | 237 | deployments::upload(&client, &deployment_c.upload_url, buf) 238 | .await 239 | .map_err(|err| { 240 | Error::Upload( 241 | file.display().to_string(), 242 | deployment_c.id.to_string(), 243 | function_id.to_string(), 244 | organisation_id.to_string(), 245 | err, 246 | ) 247 | })?; 248 | 249 | info!( 250 | organisation_id = organisation_id, 251 | function_id = function_id, 252 | deployment_id = deployment_c.id, 253 | "Trigger deployment of the function" 254 | ); 255 | 256 | deployments::trigger(&client, organisation_id, function_id, &deployment_c.id) 257 | .await 258 | .map_err(|err| { 259 | Error::Trigger( 260 | deployment_c.id.to_string(), 261 | function_id.to_string(), 262 | organisation_id.to_string(), 263 | err, 264 | ) 265 | })?; 266 | 267 | info!( 268 | organisation_id = organisation_id, 269 | function_id = function_id, 270 | deployment_id = deployment_c.id, 271 | "Retrieve deployment" 272 | ); 273 | 274 | let deployment = deployments::get(&client, organisation_id, function_id, &deployment_c.id) 275 | .await 276 | .map_err(|err| { 277 | Error::Get( 278 | deployment_c.id.to_string(), 279 | function_id.to_string(), 280 | organisation_id.to_string(), 281 | err, 282 | ) 283 | })?; 284 | 285 | println!( 286 | "{}", 287 | output 288 | .format(&deployment) 289 | .map_err(|err| Error::FormatOutput(Box::new(err)))? 290 | ); 291 | 292 | Ok(()) 293 | } 294 | 295 | pub async fn get( 296 | config: Arc, 297 | output: &Output, 298 | organisation_id: &str, 299 | function_id: &str, 300 | deployment_id: &str, 301 | ) -> Result<(), Error> { 302 | let client = Client::from(config.credentials.to_owned()); 303 | 304 | let deployment = deployments::get(&client, organisation_id, function_id, deployment_id) 305 | .await 306 | .map_err(|err| { 307 | Error::Get( 308 | deployment_id.to_string(), 309 | function_id.to_string(), 310 | organisation_id.to_string(), 311 | err, 312 | ) 313 | })?; 314 | 315 | println!( 316 | "{}", 317 | output 318 | .format(&deployment) 319 | .map_err(|err| Error::FormatOutput(Box::new(err)))? 320 | ); 321 | 322 | Ok(()) 323 | } 324 | 325 | pub async fn delete( 326 | config: Arc, 327 | organisation_id: &str, 328 | function_id: &str, 329 | deployment_id: &str, 330 | ) -> Result<(), Error> { 331 | let client = Client::from(config.credentials.to_owned()); 332 | 333 | deployments::delete(&client, organisation_id, function_id, deployment_id) 334 | .await 335 | .map_err(|err| { 336 | Error::Delete( 337 | deployment_id.to_string(), 338 | function_id.to_string(), 339 | organisation_id.to_string(), 340 | err, 341 | ) 342 | }) 343 | } 344 | -------------------------------------------------------------------------------- /examples/cleverctl/src/cmd/functions/mod.rs: -------------------------------------------------------------------------------- 1 | //! # Functions module 2 | //! 3 | //! This module provides command implementation related to functions product 4 | use std::{collections::BTreeMap, sync::Arc}; 5 | 6 | use clap::Subcommand; 7 | use clevercloud_sdk::{Client, oauth10a::reqwest, v4::functions}; 8 | use tracing::info; 9 | 10 | use crate::{ 11 | cfg::Configuration, 12 | cmd::{self, Executor, Output, parse_btreemap}, 13 | }; 14 | 15 | pub mod deployments; 16 | 17 | // ----------------------------------------------------------------------------- 18 | // Constants 19 | 20 | pub const DEFAULT_INSTANCES: u64 = 1; 21 | pub const DEFAULT_MAX_MEMORY: u64 = 64 * 1024 * 1024; 22 | 23 | // ----------------------------------------------------------------------------- 24 | // Error enumeration 25 | 26 | #[derive(thiserror::Error, Debug)] 27 | pub enum Error { 28 | #[error("failed to format output, {0}")] 29 | FormatOutput(Box), 30 | #[error("failed to create http client, {0}")] 31 | CreateClient(reqwest::Error), 32 | #[error("failed to list functions of organisation '{0}', {1}")] 33 | List(String, functions::Error), 34 | #[error("failed to create function on organisation '{0}', {1}")] 35 | Create(String, functions::Error), 36 | #[error("failed to get function '{0}' of organisation '{1}', {2}")] 37 | Get(String, String, functions::Error), 38 | #[error("failed to update function '{0}' of organisation '{1}', {2}")] 39 | Update(String, String, functions::Error), 40 | #[error("failed to delete function '{0}' of organisation '{1}', {2}")] 41 | Delete(String, String, functions::Error), 42 | #[error("{0}")] 43 | DeploymentCommand(deployments::Error), 44 | #[error("failed to list deployments of function '{0}' on organisation '{1}', {2}")] 45 | ListDeployment(String, String, functions::deployments::Error), 46 | #[error( 47 | "failed to execute function '{0}' of organisation '{1}', there is no such deployment to execute" 48 | )] 49 | NoSuchDeployment(String, String), 50 | #[error( 51 | "failed to execute request on endpoint '{0}' of deployment '{1}' for function '{2}', {3}" 52 | )] 53 | Execute(String, String, String, functions::Error), 54 | } 55 | 56 | // ----------------------------------------------------------------------------- 57 | // Command 58 | 59 | /// Command enum contains all operations that could be achieved on the user 60 | #[derive(Subcommand, Eq, PartialEq, Clone, Debug)] 61 | pub enum Command { 62 | #[clap(name = "list", aliases = &["l"], about = "List functions information of an organisation")] 63 | List { 64 | /// Specify the output format 65 | #[clap(short = 'o', long = "output", default_value_t)] 66 | output: Output, 67 | /// Specify the organisation identifier 68 | #[clap(name = "organisation-identifier")] 69 | organisation_id: String, 70 | }, 71 | #[clap(name = "create", aliases = &["c"], about = "Create a function within the given organisation")] 72 | Create { 73 | /// Specify the output format 74 | #[clap(short = 'o', long = "output", default_value_t)] 75 | output: Output, 76 | /// Specify the organisation identifier 77 | #[clap(name = "organisation-identifier")] 78 | organisation_id: String, 79 | /// Specify a name for the function 80 | #[clap(short = 'n', long = "name")] 81 | name: Option, 82 | /// Specify a description for the function 83 | #[clap(short = 'd', long = "description")] 84 | description: Option, 85 | /// Specify tags for the function (format: k1=v1,k2=v2) 86 | #[clap(short = 't', long = "tags", value_parser = parse_btreemap)] 87 | tags: Option>, 88 | /// Specify environment for the function (format: k1=v1,k2=v2) 89 | #[clap(short = 'e', long = "environment", value_parser = parse_btreemap)] 90 | environment: Option>, 91 | /// Specify the max memory of the function in byte. 92 | #[clap(short = 'm', long = "max-memory")] 93 | max_memory: Option, 94 | }, 95 | #[clap(name = "get", aliases = &["g"], about = "Get information about a function")] 96 | Get { 97 | /// Specify the output format 98 | #[clap(short = 'o', long = "output", default_value_t)] 99 | output: Output, 100 | /// Specify the organisation identifier 101 | #[clap(name = "organisation-identifier")] 102 | organisation_id: String, 103 | /// Specify the function identifier 104 | #[clap(name = "function-identifier")] 105 | function_id: String, 106 | }, 107 | #[clap(name = "update", aliases = &["u"], about = "Update information about a function")] 108 | Update { 109 | /// Specify the output format 110 | #[clap(short = 'o', long = "output", default_value_t)] 111 | output: Output, 112 | /// Specify the organisation identifier 113 | #[clap(name = "organisation-identifier")] 114 | organisation_id: String, 115 | /// Function identifier 116 | #[clap(name = "function-id")] 117 | function_id: String, 118 | /// Specify a name for the function 119 | #[clap(short = 'n', long = "name")] 120 | name: Option, 121 | /// Specify a description for the function 122 | #[clap(short = 'd', long = "description")] 123 | description: Option, 124 | /// Specify tags for the function (format: k1=v1,k2=v2) 125 | #[clap(short = 't', long = "tags", value_parser = parse_btreemap)] 126 | tags: Option>, 127 | /// Specify environment for the function (format: k1=v1,k2=v2) 128 | #[clap(short = 'e', long = "environment", value_parser = parse_btreemap)] 129 | environment: Option>, 130 | /// Specify the max memory of the function in byte. 131 | #[clap(short = 'm', long = "max-memory")] 132 | max_memory: Option, 133 | }, 134 | #[clap(name = "delete", aliases = &["d"], about = "Delete a function")] 135 | Delete { 136 | /// Specify the organisation identifier 137 | #[clap(name = "organisation-identifier")] 138 | organisation_id: String, 139 | /// Function identifier 140 | #[clap(name = "function-id")] 141 | function_id: String, 142 | }, 143 | #[clap(name = "execute", aliases = &["exec", "e"], about = "Execute a function")] 144 | Execute { 145 | /// Specify the output format 146 | #[clap(short = 'o', long = "output", default_value_t)] 147 | output: Output, 148 | /// Specify the organisation identifier 149 | #[clap(name = "organisation-identifier")] 150 | organisation_id: String, 151 | /// Specify the function identifier 152 | #[clap(name = "function-identifier")] 153 | function_id: String, 154 | }, 155 | #[clap(name = "deployments", aliases = &["deploy"], about = "Interact with deployments of a function", subcommand)] 156 | Deployments(deployments::Command), 157 | } 158 | 159 | impl Executor for Command { 160 | type Error = Error; 161 | 162 | async fn execute(&self, config: Arc) -> Result<(), Self::Error> { 163 | match self { 164 | Self::List { 165 | output, 166 | organisation_id, 167 | } => list(config, output, organisation_id).await, 168 | Self::Create { 169 | output, 170 | organisation_id, 171 | name, 172 | description, 173 | tags, 174 | environment, 175 | max_memory, 176 | } => { 177 | let tag = tags.as_ref().map(|tags| { 178 | tags.iter() 179 | .fold(vec![], |mut acc, (k, v)| { 180 | acc.push(format!("{k}:{v}")); 181 | acc 182 | }) 183 | .join(" ") 184 | }); 185 | 186 | let opts = functions::Opts { 187 | name: name.to_owned(), 188 | description: description.to_owned(), 189 | tag, 190 | environment: environment.to_owned().unwrap_or_default(), 191 | max_memory: max_memory.unwrap_or_else(|| DEFAULT_MAX_MEMORY), 192 | max_instances: DEFAULT_INSTANCES, 193 | }; 194 | 195 | create(config, output, organisation_id, &opts).await 196 | } 197 | Self::Get { 198 | output, 199 | organisation_id, 200 | function_id, 201 | } => get(config, output, organisation_id, function_id).await, 202 | Self::Update { 203 | output, 204 | organisation_id, 205 | function_id, 206 | name, 207 | description, 208 | tags, 209 | environment, 210 | max_memory, 211 | } => { 212 | let tag = tags.as_ref().map(|tags| { 213 | tags.iter() 214 | .fold(vec![], |mut acc, (k, v)| { 215 | acc.push(format!("{k}:{v}")); 216 | acc 217 | }) 218 | .join(" ") 219 | }); 220 | 221 | let opts = functions::Opts { 222 | name: name.to_owned(), 223 | description: description.to_owned(), 224 | tag, 225 | environment: environment.to_owned().unwrap_or_default(), 226 | max_memory: max_memory.unwrap_or_else(|| DEFAULT_MAX_MEMORY), 227 | max_instances: DEFAULT_INSTANCES, 228 | }; 229 | 230 | update(config, output, organisation_id, function_id, &opts).await 231 | } 232 | Self::Delete { 233 | organisation_id, 234 | function_id, 235 | } => delete(config, organisation_id, function_id).await, 236 | Self::Execute { 237 | output, 238 | organisation_id, 239 | function_id, 240 | } => execute(config, output, organisation_id, function_id).await, 241 | Self::Deployments(cmd) => cmd.execute(config).await.map_err(Error::DeploymentCommand), 242 | } 243 | } 244 | } 245 | 246 | // ------------------------------------------------------------------------------------------------- 247 | // Commands 248 | 249 | pub async fn list( 250 | config: Arc, 251 | output: &Output, 252 | organisation_id: &str, 253 | ) -> Result<(), Error> { 254 | let client = Client::from(config.credentials.to_owned()); 255 | 256 | let functionz = functions::list(&client, organisation_id) 257 | .await 258 | .map_err(|err| Error::List(organisation_id.to_string(), err))?; 259 | 260 | println!( 261 | "{}", 262 | output 263 | .format(&functionz) 264 | .map_err(|err| Error::FormatOutput(Box::new(err)))? 265 | ); 266 | 267 | Ok(()) 268 | } 269 | 270 | pub async fn create( 271 | config: Arc, 272 | output: &Output, 273 | organisation_id: &str, 274 | opts: &functions::Opts, 275 | ) -> Result<(), Error> { 276 | let client = Client::from(config.credentials.to_owned()); 277 | 278 | let function = functions::create(&client, organisation_id, opts) 279 | .await 280 | .map_err(|err| Error::Create(organisation_id.to_string(), err))?; 281 | 282 | println!( 283 | "{}", 284 | output 285 | .format(&function) 286 | .map_err(|err| Error::FormatOutput(Box::new(err)))? 287 | ); 288 | 289 | Ok(()) 290 | } 291 | 292 | pub async fn get( 293 | config: Arc, 294 | output: &Output, 295 | organisation_id: &str, 296 | function_id: &str, 297 | ) -> Result<(), Error> { 298 | let client = Client::from(config.credentials.to_owned()); 299 | 300 | let function = functions::get(&client, organisation_id, function_id) 301 | .await 302 | .map_err(|err| Error::Get(function_id.to_string(), organisation_id.to_string(), err))?; 303 | 304 | println!( 305 | "{}", 306 | output 307 | .format(&function) 308 | .map_err(|err| Error::FormatOutput(Box::new(err)))? 309 | ); 310 | 311 | Ok(()) 312 | } 313 | 314 | pub async fn update( 315 | config: Arc, 316 | output: &Output, 317 | organisation_id: &str, 318 | function_id: &str, 319 | opts: &functions::Opts, 320 | ) -> Result<(), Error> { 321 | let client = Client::from(config.credentials.to_owned()); 322 | let function = functions::update(&client, organisation_id, function_id, opts) 323 | .await 324 | .map_err(|err| Error::Update(function_id.to_string(), organisation_id.to_string(), err))?; 325 | 326 | println!( 327 | "{}", 328 | output 329 | .format(&function) 330 | .map_err(|err| Error::FormatOutput(Box::new(err)))? 331 | ); 332 | 333 | Ok(()) 334 | } 335 | 336 | pub async fn delete( 337 | config: Arc, 338 | organisation_id: &str, 339 | function_id: &str, 340 | ) -> Result<(), Error> { 341 | let client = Client::from(config.credentials.to_owned()); 342 | 343 | functions::delete(&client, organisation_id, function_id) 344 | .await 345 | .map_err(|err| Error::Delete(function_id.to_string(), organisation_id.to_string(), err)) 346 | } 347 | 348 | pub async fn execute( 349 | config: Arc, 350 | output: &Output, 351 | organisation_id: &str, 352 | function_id: &str, 353 | ) -> Result<(), Error> { 354 | let client = Client::from(config.credentials.to_owned()); 355 | info!( 356 | organisation_id = organisation_id, 357 | function_id = function_id, 358 | "List deployment to take the latest one" 359 | ); 360 | 361 | let mut deploymentz = functions::deployments::list(&client, organisation_id, function_id) 362 | .await 363 | .map_err(|err| { 364 | Error::ListDeployment(function_id.to_string(), organisation_id.to_string(), err) 365 | })? 366 | .into_iter() 367 | .filter(|deployment| { 368 | deployment.url.is_some() && functions::deployments::Status::Ready == deployment.status 369 | }) 370 | .collect::>(); 371 | 372 | deploymentz.sort_by(|a, b| a.created_at.cmp(&b.created_at)); 373 | let deployment = deploymentz.first().ok_or_else(|| { 374 | Error::NoSuchDeployment(function_id.to_string(), organisation_id.to_string()) 375 | })?; 376 | 377 | info!( 378 | organisation_id = organisation_id, 379 | function_id = function_id, 380 | deployment_id = deployment.id, 381 | endpoint = deployment.url, 382 | "Execute a GET request on function endpoint" 383 | ); 384 | 385 | match &deployment.url { 386 | None => Err(Error::NoSuchDeployment( 387 | function_id.to_string(), 388 | organisation_id.to_string(), 389 | )), 390 | Some(url) => { 391 | let result = functions::execute(&client, url).await.map_err(|err| { 392 | Error::Execute( 393 | url.to_string(), 394 | deployment.id.to_owned(), 395 | function_id.to_string(), 396 | err, 397 | ) 398 | })?; 399 | 400 | println!( 401 | "{}", 402 | output 403 | .format(&result) 404 | .map_err(|err| Error::FormatOutput(Box::new(err)))? 405 | ); 406 | 407 | Ok(()) 408 | } 409 | } 410 | } 411 | -------------------------------------------------------------------------------- /examples/cleverctl/src/cmd/mod.rs: -------------------------------------------------------------------------------- 1 | //! # Command line interface module 2 | //! 3 | //! This module provides structures and enums to interact with the command line 4 | //! interface 5 | use std::{ 6 | collections::BTreeMap, 7 | fmt::{self, Display, Formatter}, 8 | future::Future, 9 | path::PathBuf, 10 | str::FromStr, 11 | sync::Arc, 12 | }; 13 | 14 | use clap::{ArgAction, Parser, Subcommand}; 15 | use paw::ParseArgs; 16 | use serde::Serialize; 17 | 18 | use crate::cfg::Configuration; 19 | 20 | pub mod addon; 21 | pub mod functions; 22 | pub mod myself; 23 | pub mod zone; 24 | 25 | // ----------------------------------------------------------------------------- 26 | // Error enumeration 27 | 28 | #[derive(thiserror::Error, Debug)] 29 | pub enum Error { 30 | #[error("failed to parse output '{0}', available options are 'json' or 'yaml'")] 31 | ParseOutput(String), 32 | #[error("failed to serialize object into json, {0}")] 33 | SerializeJson(serde_json::Error), 34 | #[error("failed to serialize object into yaml, {0}")] 35 | SerializeYaml(serde_yaml::Error), 36 | #[error("failed to execute command relative to the current user, {0}")] 37 | MyselfCommand(myself::Error), 38 | #[error("failed to execute command relative to addons, {0}")] 39 | AddonCommand(addon::Error), 40 | #[error("failed to execute command relative to zones, {0}")] 41 | ZoneCommand(zone::Error), 42 | #[error("failed to execute command relative to functions, {0}")] 43 | FunctionCommand(functions::Error), 44 | } 45 | 46 | // ----------------------------------------------------------------------------- 47 | // Output enumeration 48 | 49 | #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug, Default)] 50 | pub enum Output { 51 | #[default] 52 | Json, 53 | Yaml, 54 | } 55 | 56 | impl FromStr for Output { 57 | type Err = Error; 58 | 59 | fn from_str(s: &str) -> Result { 60 | match s.to_lowercase().as_str() { 61 | "json" => Ok(Self::Json), 62 | "yaml" => Ok(Self::Yaml), 63 | _ => Err(Error::ParseOutput(s.to_owned())), 64 | } 65 | } 66 | } 67 | 68 | impl Display for Output { 69 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 70 | match self { 71 | Self::Json => write!(f, "json"), 72 | Self::Yaml => write!(f, "yaml"), 73 | } 74 | } 75 | } 76 | 77 | impl Output { 78 | pub fn format(&self, obj: &T) -> Result 79 | where 80 | T: Serialize, 81 | { 82 | Ok(match self { 83 | Output::Json => serde_json::to_string_pretty(obj).map_err(Error::SerializeJson)?, 84 | Output::Yaml => serde_yaml::to_string(obj).map_err(Error::SerializeYaml)?, 85 | }) 86 | } 87 | } 88 | 89 | // ----------------------------------------------------------------------------- 90 | // Executor trait 91 | 92 | /// Executor trait provides a common way to implement a command 93 | pub trait Executor { 94 | type Error; 95 | 96 | fn execute( 97 | &self, 98 | config: Arc, 99 | ) -> impl Future> + Send; 100 | } 101 | 102 | // ----------------------------------------------------------------------------- 103 | // Command enumeration 104 | 105 | /// Command enum contains all operations that the command line could handle 106 | #[derive(Subcommand, Eq, PartialEq, Clone, Debug)] 107 | pub enum Command { 108 | #[clap(name = "self", aliases = &["sel", "se", "s"], subcommand, about = "Interact with the current user")] 109 | Myself(myself::Command), 110 | #[clap(name = "addon", aliases = &["addo", "add", "ad", "a"], subcommand, about = "Interact with addons")] 111 | Addon(addon::Command), 112 | #[clap(name = "zone", aliases = &["zon", "zo", "z"], subcommand, about = "Interact with zones")] 113 | Zone(zone::Command), 114 | #[clap(name = "functions", aliases = &["functio", "functi", "funct", "func", "fun", "fu", "f"], subcommand, about = "Interact with functions")] 115 | Function(functions::Command), 116 | } 117 | 118 | impl Executor for Command { 119 | type Error = Error; 120 | 121 | async fn execute(&self, config: Arc) -> Result<(), Self::Error> { 122 | match self { 123 | Self::Myself(cmd) => cmd.execute(config).await.map_err(Error::MyselfCommand), 124 | Self::Addon(cmd) => cmd.execute(config).await.map_err(Error::AddonCommand), 125 | Self::Zone(cmd) => cmd.execute(config).await.map_err(Error::ZoneCommand), 126 | Self::Function(cmd) => cmd.execute(config).await.map_err(Error::FunctionCommand), 127 | } 128 | } 129 | } 130 | 131 | // ----------------------------------------------------------------------------- 132 | // Arguments structure 133 | 134 | /// Args structure contains all commands and global flags that the command line 135 | /// supports 136 | #[derive(Parser, Eq, PartialEq, Clone, Debug)] 137 | #[clap(author, version, about)] 138 | pub struct Args { 139 | /// Specify a configuration file 140 | #[clap(short = 'c', long = "config", global = true)] 141 | pub config: Option, 142 | /// Increase log verbosity 143 | #[clap(short = 'v', global = true, action = ArgAction::Count)] 144 | pub verbosity: u8, 145 | /// Check the healthiness of the configuration 146 | #[clap(long = "check", global = true)] 147 | pub check: bool, 148 | #[clap(subcommand)] 149 | pub cmd: Command, 150 | } 151 | 152 | impl ParseArgs for Args { 153 | type Error = Error; 154 | 155 | fn parse_args() -> Result { 156 | Ok(Args::parse()) 157 | } 158 | } 159 | 160 | // ------------------------------------------------------------------------------------------------- 161 | // Helpers 162 | 163 | pub fn parse_btreemap(value: &str) -> Result, String> { 164 | let mut btreemap: BTreeMap = BTreeMap::new(); 165 | 166 | for s in value.split(',') { 167 | if let Some((key, value)) = s.trim().split_once('=') { 168 | btreemap.insert(key.to_owned(), value.to_owned()); 169 | } else { 170 | return Err(format!( 171 | "failed to parse '{value}' as a map (a.k.a k1=v1,k2=v2)" 172 | )); 173 | } 174 | } 175 | 176 | Ok(btreemap) 177 | } 178 | -------------------------------------------------------------------------------- /examples/cleverctl/src/cmd/myself.rs: -------------------------------------------------------------------------------- 1 | //! # Myself module 2 | //! 3 | //! This module provides command implementation related to the current user 4 | use std::sync::Arc; 5 | 6 | use clap::Subcommand; 7 | use clevercloud_sdk::{Client, oauth10a::reqwest, v2::myself}; 8 | 9 | use crate::{ 10 | cfg::Configuration, 11 | cmd::{self, Executor, Output}, 12 | }; 13 | 14 | // ----------------------------------------------------------------------------- 15 | // Error enumeration 16 | 17 | #[derive(thiserror::Error, Debug)] 18 | pub enum Error { 19 | #[error("failed to format output, {0}")] 20 | FormatOutput(Box), 21 | #[error("failed to get current user information, {0}")] 22 | Get(myself::Error), 23 | #[error("failed to create http client, {0}")] 24 | CreateClient(reqwest::Error), 25 | } 26 | 27 | // ----------------------------------------------------------------------------- 28 | // Command enumeration 29 | 30 | /// Command enum contains all operations that could be achieved on the user 31 | #[derive(Subcommand, Eq, PartialEq, Clone, Debug)] 32 | pub enum Command { 33 | #[clap(name = "get", aliases = &["ge", "g"], about = "Get information about the current user")] 34 | Get { 35 | /// Specify the output format 36 | #[clap(short = 'o', long = "output", default_value_t)] 37 | output: Output, 38 | }, 39 | } 40 | 41 | impl Executor for Command { 42 | type Error = Error; 43 | 44 | async fn execute(&self, config: Arc) -> Result<(), Self::Error> { 45 | match self { 46 | Self::Get { output } => get(config, output).await, 47 | } 48 | } 49 | } 50 | 51 | pub async fn get(config: Arc, output: &Output) -> Result<(), Error> { 52 | let client = Client::from(config.credentials.to_owned()); 53 | let user = myself::get(&client).await.map_err(Error::Get)?; 54 | 55 | println!( 56 | "{}", 57 | output 58 | .format(&user) 59 | .map_err(|err| Error::FormatOutput(Box::new(err)))? 60 | ); 61 | 62 | Ok(()) 63 | } 64 | -------------------------------------------------------------------------------- /examples/cleverctl/src/cmd/zone.rs: -------------------------------------------------------------------------------- 1 | //! # Zone module 2 | //! 3 | //! This module provides command implementation related to the zone API 4 | use std::sync::Arc; 5 | 6 | use clap::Subcommand; 7 | use clevercloud_sdk::{Client, oauth10a::reqwest, v4::products::zones}; 8 | 9 | use crate::{ 10 | cfg::Configuration, 11 | cmd::{self, Executor, Output}, 12 | }; 13 | 14 | // ----------------------------------------------------------------------------- 15 | // Error enumeration 16 | 17 | #[derive(thiserror::Error, Debug)] 18 | pub enum Error { 19 | #[error("failed to format output, {0}")] 20 | FormatOutput(Box), 21 | #[error("failed to list available zones, {0}")] 22 | List(zones::Error), 23 | #[error("failed to create http client, {0}")] 24 | CreateClient(reqwest::Error), 25 | } 26 | 27 | // ----------------------------------------------------------------------------- 28 | // Command enumeration 29 | 30 | /// Command enum contains all operations that could be achieved on the zone API 31 | #[derive(Subcommand, Eq, PartialEq, Clone, Debug)] 32 | pub enum Command { 33 | #[clap(name = "list", aliases = &["l"], about = "List available zones")] 34 | List { 35 | /// Specify the output format 36 | #[clap(short = 'o', long = "output", default_value_t)] 37 | output: Output, 38 | }, 39 | #[clap(name = "application", aliases = &["app", "a"], about = "List application available zones")] 40 | Application { 41 | /// Specify the output format 42 | #[clap(short = 'o', long = "output", default_value_t)] 43 | output: Output, 44 | }, 45 | #[clap(name = "hds", aliases = &["h"], about = "List hds available zones")] 46 | Hds { 47 | /// Specify the output format 48 | #[clap(short = 'o', long = "output", default_value_t)] 49 | output: Output, 50 | }, 51 | } 52 | 53 | impl Executor for Command { 54 | type Error = Error; 55 | 56 | async fn execute(&self, config: Arc) -> Result<(), Self::Error> { 57 | match self { 58 | Self::List { output } => list(config, output).await, 59 | Self::Application { output } => applications(config, output).await, 60 | Self::Hds { output } => hds(config, output).await, 61 | } 62 | } 63 | } 64 | 65 | // ----------------------------------------------------------------------------- 66 | // helpers 67 | 68 | pub async fn list(config: Arc, output: &Output) -> Result<(), Error> { 69 | let client = Client::from(config.credentials.to_owned()); 70 | let zones = zones::list(&client).await.map_err(Error::List)?; 71 | 72 | println!( 73 | "{}", 74 | output 75 | .format(&zones) 76 | .map_err(|err| Error::FormatOutput(Box::new(err)))? 77 | ); 78 | Ok(()) 79 | } 80 | 81 | pub async fn applications(config: Arc, output: &Output) -> Result<(), Error> { 82 | let client = Client::from(config.credentials.to_owned()); 83 | let zones = zones::applications(&client).await.map_err(Error::List)?; 84 | 85 | println!( 86 | "{}", 87 | output 88 | .format(&zones) 89 | .map_err(|err| Error::FormatOutput(Box::new(err)))? 90 | ); 91 | Ok(()) 92 | } 93 | 94 | pub async fn hds(config: Arc, output: &Output) -> Result<(), Error> { 95 | let client = Client::from(config.credentials.to_owned()); 96 | let zones = zones::hds(&client).await.map_err(Error::List)?; 97 | 98 | println!( 99 | "{}", 100 | output 101 | .format(&zones) 102 | .map_err(|err| Error::FormatOutput(Box::new(err)))? 103 | ); 104 | Ok(()) 105 | } 106 | -------------------------------------------------------------------------------- /examples/cleverctl/src/logging.rs: -------------------------------------------------------------------------------- 1 | //! # Logging module 2 | //! 3 | //! This module provides logging facilities and helpers 4 | 5 | use tracing::Level; 6 | 7 | // ----------------------------------------------------------------------------- 8 | // Error enumeration 9 | 10 | #[derive(thiserror::Error, Debug)] 11 | pub enum Error { 12 | #[error("failed to set global default subscriber, {0}")] 13 | GlobalDefaultSubscriber(tracing::subscriber::SetGlobalDefaultError), 14 | } 15 | 16 | // ----------------------------------------------------------------------------- 17 | // helpers 18 | 19 | pub const fn level(verbosity: usize) -> Level { 20 | match verbosity { 21 | 0 => Level::ERROR, 22 | 1 => Level::WARN, 23 | 2 => Level::INFO, 24 | 3 => Level::DEBUG, 25 | _ => Level::TRACE, 26 | } 27 | } 28 | 29 | pub fn initialize(verbosity: usize) -> Result<(), Error> { 30 | tracing::subscriber::set_global_default( 31 | tracing_subscriber::fmt() 32 | .with_max_level(level(verbosity)) 33 | .with_thread_names(true) 34 | .with_line_number(true) 35 | .with_thread_ids(true) 36 | .with_target(true) 37 | .finish(), 38 | ) 39 | .map_err(Error::GlobalDefaultSubscriber) 40 | } 41 | -------------------------------------------------------------------------------- /examples/cleverctl/src/main.rs: -------------------------------------------------------------------------------- 1 | //! # Command line interface example 2 | //! 3 | //! Clever Cloud command line interface using the 4 | //! [clevercloud-sdk-rust](https://github.com/CleverCloud/clevercloud-sdk-rust) 5 | //! project 6 | use std::sync::Arc; 7 | 8 | use tracing::{debug, error, info}; 9 | 10 | use crate::{ 11 | cfg::Configuration, 12 | cmd::{Args, Executor}, 13 | }; 14 | 15 | pub mod cfg; 16 | pub mod cmd; 17 | pub mod logging; 18 | 19 | // ----------------------------------------------------------------------------- 20 | // Error enumeration 21 | 22 | #[derive(thiserror::Error, Debug)] 23 | pub enum Error { 24 | #[error("failed to load configuration, {0}")] 25 | Configuration(cfg::Error), 26 | #[error("failed to execute command, {0}")] 27 | Command(cmd::Error), 28 | #[error("failed to parse command line, {0}")] 29 | ParseCommandLine(std::io::Error), 30 | #[error("failed to initialize logging system, {0}")] 31 | Logging(logging::Error), 32 | } 33 | 34 | impl From for Error { 35 | fn from(err: std::io::Error) -> Self { 36 | Self::ParseCommandLine(err) 37 | } 38 | } 39 | 40 | impl From for Error { 41 | fn from(err: cmd::Error) -> Self { 42 | Self::Command(err) 43 | } 44 | } 45 | 46 | // ----------------------------------------------------------------------------- 47 | // main 48 | 49 | #[paw::main] 50 | #[tokio::main] 51 | pub async fn main(args: Args) -> Result<(), Error> { 52 | logging::initialize(args.verbosity as usize).map_err(Error::Logging)?; 53 | 54 | let result = match &args.config { 55 | Some(pb) => Configuration::try_from(pb).map_err(Error::Configuration), 56 | None => Configuration::try_default().map_err(Error::Configuration), 57 | }; 58 | 59 | let config = match result { 60 | Ok(config) => Arc::new(config), 61 | Err(err) => { 62 | error!(error = err.to_string(), "Could not load configuration"); 63 | return Err(err); 64 | } 65 | }; 66 | 67 | if args.check { 68 | info!("Configuration is healthy!"); 69 | debug!("Configuration is {:#?}", config); 70 | return Ok(()); 71 | } 72 | 73 | if let Err(err) = args.cmd.execute(config).await.map_err(Error::Command) { 74 | error!(error = err.to_string(), "Could not execute command"); 75 | return Err(err); 76 | } 77 | 78 | info!("Command successfully executed"); 79 | Ok(()) 80 | } 81 | -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | 1.85.0 2 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Clever-Cloud Sdk 2 | //! 3 | //! This module provides a client and structures to interact with clever-cloud 4 | //! api. 5 | 6 | use std::fmt::Debug; 7 | 8 | use serde::{Deserialize, Serialize, de::DeserializeOwned}; 9 | 10 | use crate::oauth10a::{ 11 | Client as OAuthClient, ClientError, Request, RestClient, 12 | reqwest::{self, Method}, 13 | }; 14 | 15 | pub mod v2; 16 | pub mod v4; 17 | 18 | // ----------------------------------------------------------------------------- 19 | // Exports 20 | 21 | pub use oauth10a::client as oauth10a; 22 | 23 | // ----------------------------------------------------------------------------- 24 | // Constants 25 | 26 | pub const PUBLIC_ENDPOINT: &str = "https://api.clever-cloud.com"; 27 | pub const PUBLIC_API_BRIDGE_ENDPOINT: &str = "https://api-bridge.clever-cloud.com"; 28 | 29 | // Consumer key and secret reported here are one from the clever-tools and is 30 | // available publicly. 31 | // the disclosure of these tokens is not considered as a vulnerability. 32 | // Do not report this to our security service. 33 | // 34 | // See: 35 | // - 36 | pub const DEFAULT_CONSUMER_KEY: &str = "T5nFjKeHH4AIlEveuGhB5S3xg8T19e"; 37 | pub fn default_consumer_key() -> String { 38 | DEFAULT_CONSUMER_KEY.to_string() 39 | } 40 | 41 | pub const DEFAULT_CONSUMER_SECRET: &str = "MgVMqTr6fWlf2M0tkC2MXOnhfqBWDT"; 42 | pub fn default_consumer_secret() -> String { 43 | DEFAULT_CONSUMER_SECRET.to_string() 44 | } 45 | 46 | // ----------------------------------------------------------------------------- 47 | // Credentials structure 48 | 49 | #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)] 50 | #[serde(untagged)] 51 | pub enum Credentials { 52 | OAuth1 { 53 | #[serde(rename = "token")] 54 | token: String, 55 | #[serde(rename = "secret")] 56 | secret: String, 57 | #[serde(rename = "consumer-key", default = "default_consumer_key")] 58 | consumer_key: String, 59 | #[serde(rename = "consumer-secret", default = "default_consumer_secret")] 60 | consumer_secret: String, 61 | }, 62 | Basic { 63 | #[serde(rename = "username")] 64 | username: String, 65 | #[serde(rename = "password")] 66 | password: String, 67 | }, 68 | Bearer { 69 | #[serde(rename = "token")] 70 | token: String, 71 | }, 72 | } 73 | 74 | impl Default for Credentials { 75 | #[tracing::instrument(skip_all)] 76 | fn default() -> Self { 77 | Self::OAuth1 { 78 | token: String::new(), 79 | secret: String::new(), 80 | consumer_key: DEFAULT_CONSUMER_KEY.to_string(), 81 | consumer_secret: DEFAULT_CONSUMER_SECRET.to_string(), 82 | } 83 | } 84 | } 85 | 86 | impl From for Credentials { 87 | #[tracing::instrument(skip_all)] 88 | fn from(credentials: oauth10a::Credentials) -> Self { 89 | match credentials { 90 | oauth10a::Credentials::Bearer { token } => Self::Bearer { token }, 91 | oauth10a::Credentials::Basic { username, password } => { 92 | Self::Basic { username, password } 93 | } 94 | oauth10a::Credentials::OAuth1 { 95 | token, 96 | secret, 97 | consumer_key, 98 | consumer_secret, 99 | } => Self::OAuth1 { 100 | token, 101 | secret, 102 | consumer_key, 103 | consumer_secret, 104 | }, 105 | } 106 | } 107 | } 108 | 109 | #[allow(clippy::from_over_into)] 110 | impl Into for Credentials { 111 | #[tracing::instrument(skip_all)] 112 | fn into(self) -> oauth10a::Credentials { 113 | match self { 114 | Self::Bearer { token } => oauth10a::Credentials::Bearer { token }, 115 | Self::Basic { username, password } => { 116 | oauth10a::Credentials::Basic { username, password } 117 | } 118 | Self::OAuth1 { 119 | token, 120 | secret, 121 | consumer_key, 122 | consumer_secret, 123 | } => oauth10a::Credentials::OAuth1 { 124 | token, 125 | secret, 126 | consumer_key, 127 | consumer_secret, 128 | }, 129 | } 130 | } 131 | } 132 | 133 | impl Credentials { 134 | #[tracing::instrument(skip_all)] 135 | pub fn bearer(token: String) -> Self { 136 | Self::Bearer { token } 137 | } 138 | 139 | #[tracing::instrument(skip_all)] 140 | pub fn basic(username: String, password: String) -> Self { 141 | Self::Basic { username, password } 142 | } 143 | 144 | #[tracing::instrument(skip_all)] 145 | pub fn oauth1( 146 | token: String, 147 | secret: String, 148 | consumer_key: String, 149 | consumer_secret: String, 150 | ) -> Self { 151 | Self::OAuth1 { 152 | token, 153 | secret, 154 | consumer_key, 155 | consumer_secret, 156 | } 157 | } 158 | } 159 | 160 | // ----------------------------------------------------------------------------- 161 | // Builder structure 162 | 163 | #[derive(Clone, Debug, Default)] 164 | pub struct Builder { 165 | endpoint: Option, 166 | credentials: Option, 167 | } 168 | 169 | impl Builder { 170 | #[cfg_attr(feature = "tracing", tracing::instrument)] 171 | pub fn with_endpoint(mut self, endpoint: String) -> Self { 172 | self.endpoint = Some(endpoint); 173 | self 174 | } 175 | 176 | #[cfg_attr(feature = "tracing", tracing::instrument)] 177 | pub fn with_credentials(mut self, credentials: Credentials) -> Self { 178 | self.credentials = Some(credentials); 179 | self 180 | } 181 | 182 | #[cfg_attr(feature = "tracing", tracing::instrument)] 183 | pub fn build(self, client: reqwest::Client) -> Client { 184 | let endpoint = match self.endpoint { 185 | Some(endpoint) => endpoint, 186 | None => { 187 | if matches!(self.credentials, Some(Credentials::Bearer { .. })) { 188 | PUBLIC_API_BRIDGE_ENDPOINT.to_string() 189 | } else { 190 | PUBLIC_ENDPOINT.to_string() 191 | } 192 | } 193 | }; 194 | 195 | Client { 196 | inner: OAuthClient::new(client, self.credentials.map(Into::into)), 197 | endpoint, 198 | } 199 | } 200 | } 201 | 202 | // ----------------------------------------------------------------------------- 203 | // Client structure 204 | 205 | #[derive(Clone, Debug)] 206 | pub struct Client { 207 | inner: OAuthClient, 208 | endpoint: String, 209 | } 210 | 211 | impl Request for Client { 212 | type Error = ClientError; 213 | 214 | #[cfg_attr(feature = "tracing", tracing::instrument)] 215 | fn request( 216 | &self, 217 | method: &Method, 218 | endpoint: &str, 219 | payload: &T, 220 | ) -> impl Future> 221 | where 222 | T: Serialize + Debug + Send + Sync, 223 | U: DeserializeOwned + Debug + Send + Sync, 224 | { 225 | self.inner.request(method, endpoint, payload) 226 | } 227 | 228 | #[cfg_attr(feature = "tracing", tracing::instrument)] 229 | fn execute( 230 | &self, 231 | request: reqwest::Request, 232 | ) -> impl Future> { 233 | self.inner.execute(request) 234 | } 235 | } 236 | 237 | impl RestClient for Client { 238 | type Error = ClientError; 239 | 240 | #[cfg_attr(feature = "tracing", tracing::instrument)] 241 | fn get(&self, endpoint: &str) -> impl Future> 242 | where 243 | T: DeserializeOwned + Debug + Send + Sync, 244 | { 245 | self.inner.get(endpoint) 246 | } 247 | 248 | #[cfg_attr(feature = "tracing", tracing::instrument)] 249 | fn post( 250 | &self, 251 | endpoint: &str, 252 | payload: &T, 253 | ) -> impl Future> 254 | where 255 | T: Serialize + Debug + Send + Sync, 256 | U: DeserializeOwned + Debug + Send + Sync, 257 | { 258 | self.inner.post(endpoint, payload) 259 | } 260 | 261 | #[cfg_attr(feature = "tracing", tracing::instrument)] 262 | fn put(&self, endpoint: &str, payload: &T) -> impl Future> 263 | where 264 | T: Serialize + Debug + Send + Sync, 265 | U: DeserializeOwned + Debug + Send + Sync, 266 | { 267 | self.inner.put(endpoint, payload) 268 | } 269 | 270 | #[cfg_attr(feature = "tracing", tracing::instrument)] 271 | fn patch( 272 | &self, 273 | endpoint: &str, 274 | payload: &T, 275 | ) -> impl Future> 276 | where 277 | T: Serialize + Debug + Send + Sync, 278 | U: DeserializeOwned + Debug + Send + Sync, 279 | { 280 | self.inner.patch(endpoint, payload) 281 | } 282 | 283 | #[cfg_attr(feature = "tracing", tracing::instrument)] 284 | fn delete(&self, endpoint: &str) -> impl Future> { 285 | self.inner.delete(endpoint) 286 | } 287 | } 288 | 289 | impl From for Client { 290 | #[cfg_attr(feature = "tracing", tracing::instrument)] 291 | fn from(client: reqwest::Client) -> Self { 292 | Self::builder().build(client) 293 | } 294 | } 295 | 296 | impl From for Client { 297 | #[cfg_attr(feature = "tracing", tracing::instrument)] 298 | fn from(credentials: Credentials) -> Self { 299 | match &credentials { 300 | Credentials::Bearer { .. } => Self::builder() 301 | .with_endpoint(PUBLIC_API_BRIDGE_ENDPOINT.to_string()) 302 | .with_credentials(credentials) 303 | .build(reqwest::Client::new()), 304 | _ => Self::builder() 305 | .with_credentials(credentials) 306 | .build(reqwest::Client::new()), 307 | } 308 | } 309 | } 310 | 311 | impl Default for Client { 312 | #[cfg_attr(feature = "tracing", tracing::instrument)] 313 | fn default() -> Self { 314 | Self::builder().build(reqwest::Client::new()) 315 | } 316 | } 317 | 318 | impl Client { 319 | #[cfg_attr(feature = "tracing", tracing::instrument)] 320 | pub fn new( 321 | client: reqwest::Client, 322 | endpoint: String, 323 | credentials: Option, 324 | ) -> Self { 325 | let mut builder = Self::builder().with_endpoint(endpoint); 326 | 327 | if let Some(credentials) = credentials { 328 | builder = builder.with_credentials(credentials); 329 | } 330 | 331 | builder.build(client) 332 | } 333 | 334 | #[cfg_attr(feature = "tracing", tracing::instrument)] 335 | pub fn builder() -> Builder { 336 | Builder::default() 337 | } 338 | 339 | #[cfg_attr(feature = "tracing", tracing::instrument)] 340 | pub fn set_endpoint(&mut self, endpoint: String) { 341 | self.endpoint = endpoint; 342 | } 343 | 344 | #[cfg_attr(feature = "tracing", tracing::instrument)] 345 | pub fn set_credentials(&mut self, credentials: Option) { 346 | self.inner.set_credentials(credentials.map(Into::into)); 347 | } 348 | 349 | #[cfg_attr(feature = "tracing", tracing::instrument)] 350 | pub fn inner(&self) -> &reqwest::Client { 351 | self.inner.inner() 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /src/v2/addon.rs: -------------------------------------------------------------------------------- 1 | //! # Addon module 2 | //! 3 | //! This module expose structures and helpers to interact with the addon api 4 | //! version 2 5 | 6 | use std::{collections::BTreeMap, fmt::Debug}; 7 | 8 | #[cfg(feature = "logging")] 9 | use log::{Level, debug, log_enabled}; 10 | use oauth10a::client::{ClientError, RestClient}; 11 | #[cfg(feature = "jsonschemas")] 12 | use schemars::JsonSchema; 13 | use serde::{Deserialize, Serialize}; 14 | 15 | use crate::{Client, v4::addon_provider::config_provider::addon::environment::Variable}; 16 | 17 | // ----------------------------------------------------------------------------- 18 | // Provider structure 19 | 20 | #[cfg_attr(feature = "jsonschemas", derive(JsonSchema))] 21 | #[derive(Serialize, Deserialize, PartialEq, PartialOrd, Clone, Debug)] 22 | pub struct Provider { 23 | #[serde(rename = "id")] 24 | pub id: String, 25 | #[serde(rename = "name")] 26 | pub name: String, 27 | #[serde(rename = "website")] 28 | pub website: String, 29 | #[serde(rename = "supportEmail")] 30 | pub support_email: String, 31 | #[serde(rename = "googlePlusName")] 32 | pub google_plus_name: String, 33 | #[serde(rename = "twitterName")] 34 | pub twitter_name: String, 35 | #[serde(rename = "analyticsId")] 36 | pub analytics_id: String, 37 | #[serde(rename = "shortDesc")] 38 | pub short_description: String, 39 | #[serde(rename = "longDesc")] 40 | pub long_description: String, 41 | #[serde(rename = "logoUrl")] 42 | pub logo_url: String, 43 | #[serde(rename = "status")] 44 | pub status: String, 45 | #[serde(rename = "openInNewTab")] 46 | pub open_in_new_tab: bool, 47 | #[serde(rename = "canUpgrade")] 48 | pub can_upgrade: bool, 49 | #[serde(rename = "regions")] 50 | pub regions: Vec, 51 | #[serde(default, rename = "plans")] 52 | pub plans: Vec, 53 | } 54 | 55 | // ----------------------------------------------------------------------------- 56 | // Feature structure 57 | 58 | #[cfg_attr(feature = "jsonschemas", derive(JsonSchema))] 59 | #[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Clone, Debug)] 60 | pub struct Feature { 61 | #[serde(rename = "name")] 62 | pub name: String, 63 | #[serde(rename = "type")] 64 | pub kind: String, 65 | #[serde(rename = "value")] 66 | pub value: String, 67 | #[serde(rename = "computable_value")] 68 | pub computable_value: Option, 69 | #[serde(rename = "name_code")] 70 | pub name_code: Option, 71 | } 72 | 73 | // ----------------------------------------------------------------------------- 74 | // Plan structure 75 | 76 | #[cfg_attr(feature = "jsonschemas", derive(JsonSchema))] 77 | #[derive(Serialize, Deserialize, PartialEq, PartialOrd, Clone, Debug)] 78 | pub struct Plan { 79 | #[serde(rename = "id")] 80 | pub id: String, 81 | #[serde(rename = "name")] 82 | pub name: String, 83 | #[serde(rename = "slug")] 84 | pub slug: String, 85 | #[serde(rename = "price")] 86 | pub price: f32, 87 | #[serde(rename = "price_id")] 88 | pub price_id: Option, 89 | #[serde(rename = "features")] 90 | pub features: Vec, 91 | #[serde(rename = "zones")] 92 | pub zones: Vec, 93 | } 94 | 95 | // ----------------------------------------------------------------------------- 96 | // Addon structure 97 | 98 | #[cfg_attr(feature = "jsonschemas", derive(JsonSchema))] 99 | #[derive(Serialize, Deserialize, PartialEq, PartialOrd, Clone, Debug)] 100 | pub struct Addon { 101 | #[serde(rename = "id")] 102 | pub id: String, 103 | #[serde(rename = "name")] 104 | pub name: Option, 105 | #[serde(rename = "realId")] 106 | pub real_id: String, 107 | #[serde(rename = "region")] 108 | pub region: String, 109 | #[serde(rename = "provider")] 110 | pub provider: Provider, 111 | #[serde(rename = "plan")] 112 | pub plan: Plan, 113 | #[serde(rename = "creationDate")] 114 | pub creation_date: u64, 115 | #[serde(rename = "configKeys")] 116 | pub config_keys: Vec, 117 | } 118 | 119 | // ----------------------------------------------------------------------------- 120 | // Opts enum 121 | 122 | #[cfg_attr(feature = "jsonschemas", derive(JsonSchema))] 123 | #[derive(Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord, Clone, Debug, Default)] 124 | pub struct Opts { 125 | #[serde(rename = "version", skip_serializing_if = "Option::is_none")] 126 | pub version: Option, 127 | #[serde(rename = "encryption", skip_serializing_if = "Option::is_none")] 128 | pub encryption: Option, 129 | #[serde(rename = "services", skip_serializing_if = "Option::is_none")] 130 | pub services: Option, 131 | } 132 | 133 | // ----------------------------------------------------------------------------- 134 | // CreateOpts structure 135 | 136 | #[cfg_attr(feature = "jsonschemas", derive(JsonSchema))] 137 | #[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Clone, Debug)] 138 | pub struct CreateOpts { 139 | #[serde(rename = "name")] 140 | pub name: String, 141 | #[serde(rename = "region")] 142 | pub region: String, 143 | #[serde(rename = "providerId")] 144 | pub provider_id: String, 145 | #[serde(rename = "plan")] 146 | pub plan: String, 147 | #[serde(rename = "options")] 148 | pub options: Opts, 149 | } 150 | 151 | // ----------------------------------------------------------------------------- 152 | // Error enumerations 153 | 154 | #[derive(thiserror::Error, Debug)] 155 | pub enum Error { 156 | #[error("failed to list addons of organisation '{0}', {1}")] 157 | List(String, ClientError), 158 | #[error("failed to get addon '{0}' of organisation '{1}', {2}")] 159 | Get(String, String, ClientError), 160 | #[error("failed to get addon '{0}' environment of organisation '{1}', {2}")] 161 | Environment(String, String, ClientError), 162 | #[error("failed to create addon for organisation '{0}', {1}")] 163 | Create(String, ClientError), 164 | #[error("failed to delete addon '{0}' for organisation '{1}', {2}")] 165 | Delete(String, String, ClientError), 166 | } 167 | 168 | // ----------------------------------------------------------------------------- 169 | // Helpers functions 170 | 171 | #[cfg_attr(feature = "tracing", tracing::instrument)] 172 | /// returns the list of addons for the given organisation 173 | pub async fn list(client: &Client, organisation_id: &str) -> Result, Error> { 174 | let path = format!( 175 | "{}/v2/organisations/{}/addons", 176 | client.endpoint, organisation_id, 177 | ); 178 | 179 | #[cfg(feature = "logging")] 180 | if log_enabled!(Level::Debug) { 181 | debug!( 182 | "execute a request to get the list of addons, path: '{}', organisation: '{}'", 183 | &path, organisation_id 184 | ); 185 | } 186 | 187 | client 188 | .get(&path) 189 | .await 190 | .map_err(|err| Error::List(organisation_id.to_owned(), err)) 191 | } 192 | 193 | #[cfg_attr(feature = "tracing", tracing::instrument)] 194 | /// returns the addon for the given the organisation and identifier 195 | pub async fn get(client: &Client, organisation_id: &str, id: &str) -> Result { 196 | let path = format!( 197 | "{}/v2/organisations/{}/addons/{}", 198 | client.endpoint, organisation_id, id 199 | ); 200 | 201 | #[cfg(feature = "logging")] 202 | if log_enabled!(Level::Debug) { 203 | debug!( 204 | "execute a request to get information about an addon, path: '{}', organisation: '{}', id: '{}'", 205 | &path, organisation_id, id 206 | ); 207 | } 208 | 209 | client 210 | .get(&path) 211 | .await 212 | .map_err(|err| Error::Get(id.to_owned(), organisation_id.to_owned(), err)) 213 | } 214 | 215 | #[cfg_attr(feature = "tracing", tracing::instrument)] 216 | /// create the addon and returns it 217 | pub async fn create( 218 | client: &Client, 219 | organisation_id: &str, 220 | opts: &CreateOpts, 221 | ) -> Result { 222 | let path = format!( 223 | "{}/v2/organisations/{}/addons", 224 | client.endpoint, organisation_id 225 | ); 226 | 227 | #[cfg(feature = "logging")] 228 | if log_enabled!(Level::Debug) { 229 | debug!( 230 | "execute a request to create an addon, path: '{}', organisation: '{}', name: '{}', region: '{}', plan: '{}', provider-id: '{}'", 231 | &path, 232 | organisation_id, 233 | &opts.name, 234 | &opts.region, 235 | &opts.plan, 236 | &opts.provider_id.to_string() 237 | ); 238 | } 239 | 240 | client 241 | .post(&path, opts) 242 | .await 243 | .map_err(|err| Error::Create(organisation_id.to_owned(), err)) 244 | } 245 | 246 | #[cfg_attr(feature = "tracing", tracing::instrument)] 247 | /// delete the given addon 248 | pub async fn delete(client: &Client, organisation_id: &str, id: &str) -> Result<(), Error> { 249 | let path = format!( 250 | "{}/v2/organisations/{}/addons/{}", 251 | client.endpoint, organisation_id, id 252 | ); 253 | 254 | #[cfg(feature = "logging")] 255 | if log_enabled!(Level::Debug) { 256 | debug!( 257 | "execute a request to delete an addon, path: '{}', organisation: '{}', id: '{}'", 258 | &path, organisation_id, id 259 | ); 260 | } 261 | 262 | client 263 | .delete(&path) 264 | .await 265 | .map_err(|err| Error::Delete(id.to_owned(), organisation_id.to_owned(), err)) 266 | } 267 | 268 | #[cfg_attr(feature = "tracing", tracing::instrument)] 269 | /// returns environment variables for an addon 270 | pub async fn environment( 271 | client: &Client, 272 | organisation_id: &str, 273 | id: &str, 274 | ) -> Result, Error> { 275 | let path = format!( 276 | "{}/v2/organisations/{}/addons/{}/env", 277 | client.endpoint, organisation_id, id 278 | ); 279 | 280 | #[cfg(feature = "logging")] 281 | if log_enabled!(Level::Debug) { 282 | debug!( 283 | "execute a request to get secret of a addon, path: '{}', organisation: '{}', id: '{}'", 284 | &path, organisation_id, id 285 | ); 286 | } 287 | 288 | let env: Vec = client 289 | .get(&path) 290 | .await 291 | .map_err(|err| Error::Environment(id.to_owned(), organisation_id.to_owned(), err))?; 292 | 293 | Ok(env.iter().fold(BTreeMap::new(), |mut acc, var| { 294 | acc.insert(var.name.to_owned(), var.value.to_owned()); 295 | acc 296 | })) 297 | } 298 | -------------------------------------------------------------------------------- /src/v2/mod.rs: -------------------------------------------------------------------------------- 1 | //! # Api version 2 module 2 | //! 3 | //! This module expose resources under the version 2 of the Clever-Cloud Api. 4 | 5 | pub mod addon; 6 | pub mod myself; 7 | pub mod plan; 8 | -------------------------------------------------------------------------------- /src/v2/myself.rs: -------------------------------------------------------------------------------- 1 | //! # Myself module 2 | //! 3 | //! This module provides structures and helpers to interact with the user api 4 | //! version 2 5 | 6 | use std::fmt::Debug; 7 | 8 | #[cfg(feature = "logging")] 9 | use log::{Level, debug, log_enabled}; 10 | use oauth10a::client::{ClientError, RestClient}; 11 | #[cfg(feature = "jsonschemas")] 12 | use schemars::JsonSchema; 13 | use serde::{Deserialize, Serialize}; 14 | 15 | use crate::Client; 16 | 17 | // ----------------------------------------------------------------------------- 18 | // Myself structure and helpers 19 | 20 | #[cfg_attr(feature = "jsonschemas", derive(JsonSchema))] 21 | #[derive(Serialize, PartialEq, Eq, Deserialize, Clone, Debug)] 22 | pub struct Myself { 23 | #[serde(rename = "id")] 24 | pub id: String, 25 | #[serde(rename = "name")] 26 | pub name: String, 27 | #[serde(rename = "email")] 28 | pub email: String, 29 | #[serde(rename = "phone")] 30 | pub phone: String, 31 | #[serde(rename = "address")] 32 | pub address: String, 33 | #[serde(rename = "city")] 34 | pub city: String, 35 | #[serde(rename = "zipcode")] 36 | pub zipcode: String, 37 | #[serde(rename = "country")] 38 | pub country: String, 39 | #[serde(rename = "avatar")] 40 | pub avatar: String, 41 | #[serde(rename = "creationDate")] 42 | pub creation_date: u64, 43 | #[serde(rename = "lang")] 44 | pub lang: String, 45 | #[serde(rename = "emailValidated")] 46 | pub email_validated: bool, 47 | #[serde(rename = "oauthApps")] 48 | pub oauth_apps: Vec, 49 | #[serde(rename = "admin")] 50 | pub admin: bool, 51 | #[serde(rename = "canPay")] 52 | pub can_pay: bool, 53 | #[serde(rename = "preferredMFA")] 54 | pub preferred_mfa: String, 55 | #[serde(rename = "hasPassword")] 56 | pub has_password: bool, 57 | } 58 | 59 | // ----------------------------------------------------------------------------- 60 | // Error enumeration 61 | 62 | #[derive(thiserror::Error, Debug)] 63 | pub enum Error { 64 | #[error("failed to get information about the current user, {0}")] 65 | Get(ClientError), 66 | } 67 | 68 | // ----------------------------------------------------------------------------- 69 | // Helpers functions 70 | 71 | #[cfg_attr(feature = "tracing", tracing::instrument)] 72 | /// returns information about the person logged in 73 | pub async fn get(client: &Client) -> Result { 74 | let path = format!("{}/v2/self", client.endpoint); 75 | 76 | #[cfg(feature = "logging")] 77 | if log_enabled!(Level::Debug) { 78 | debug!( 79 | "execute a request to get information about the logged in user, path: '{}'", 80 | &path 81 | ); 82 | } 83 | 84 | client.get(&path).await.map_err(Error::Get) 85 | } 86 | -------------------------------------------------------------------------------- /src/v2/plan.rs: -------------------------------------------------------------------------------- 1 | //! # Addon provider plan module 2 | //! 3 | //! This module provides helpers and structures to interact with the plan api of 4 | //! the addon providers 5 | 6 | #[cfg(feature = "logging")] 7 | use log::{Level, debug, log_enabled}; 8 | use oauth10a::client::{ClientError, RestClient}; 9 | 10 | use crate::{ 11 | Client, 12 | v2::addon::{Plan, Provider}, 13 | v4::addon_provider::AddonProviderId, 14 | }; 15 | 16 | // ----------------------------------------------------------------------------- 17 | // Constants 18 | 19 | /// Config Provider addon have an unique and hard-coded plan as it is free to use 20 | pub const CONFIG_PROVIDER: &str = "plan_5d8e9596-dd73-4b73-84d9-e165372c5324"; 21 | 22 | // ----------------------------------------------------------------------------- 23 | // Error enumeration 24 | 25 | #[derive(thiserror::Error, Debug)] 26 | pub enum Error { 27 | #[error("failed to fetch list of addon providers, {0}")] 28 | List(ClientError), 29 | #[error("failed to fetch details of addon provider '{0}'")] 30 | Get(AddonProviderId), 31 | #[error("failed to find plan '{0}' for addon provider '{1}' amongst available options: {2}")] 32 | Plan(String, AddonProviderId, String), 33 | } 34 | 35 | // ----------------------------------------------------------------------------- 36 | // Helpers method 37 | 38 | #[cfg_attr(feature = "tracing", tracing::instrument)] 39 | /// Returns the list of details relative to the addon providers. 40 | pub async fn list(client: &Client) -> Result, Error> { 41 | let path = format!("{}/v2/products/addonproviders", client.endpoint); 42 | 43 | #[cfg(feature = "logging")] 44 | if log_enabled!(Level::Debug) { 45 | debug!("execute a request to list plans of the addon-provider, path: '{path}'"); 46 | } 47 | 48 | client.get(&path).await.map_err(Error::List) 49 | } 50 | 51 | #[cfg_attr(feature = "tracing", tracing::instrument)] 52 | /// Returns the plan matching `pattern` for the given addon provider, if any. 53 | /// 54 | /// # Errors 55 | /// 56 | /// * [`Error::List`]: failed to fetch list of details relative to addon providers. 57 | /// * [`Error::Get`]: failed to fetch details of addon provider. 58 | /// * [`Error::Plan`]: failed to find plan. 59 | pub async fn find( 60 | client: &Client, 61 | addon_provider_id: &AddonProviderId, 62 | pattern: &str, 63 | ) -> Result, Error> { 64 | let providers = list(client).await?; 65 | let addon_provider_id_str = addon_provider_id.as_str(); 66 | 67 | // Find the provider matching the addon provider id 68 | let provider = providers 69 | .into_iter() 70 | .find(|provider| provider.id == addon_provider_id_str) 71 | .ok_or(Error::Get(addon_provider_id.to_owned()))?; 72 | 73 | // It seems that some addon providers may validly not have plans 74 | // NOTE: this is error prone, maybe we should set the field to `Option>` instead 75 | if provider.plans.is_empty() { 76 | return Ok(None); 77 | } 78 | 79 | // Find the plan matching the pattern 80 | if let Some(plan) = provider.plans.iter().find(|plan| { 81 | plan.slug.eq_ignore_ascii_case(pattern) 82 | || plan.name.eq_ignore_ascii_case(pattern) 83 | || plan.id.eq_ignore_ascii_case(pattern) 84 | }) { 85 | return Ok(Some(plan.to_owned())); 86 | } 87 | 88 | // No match 89 | Err(Error::Plan( 90 | pattern.to_owned(), 91 | addon_provider_id.to_owned(), 92 | provider 93 | .plans 94 | .into_iter() 95 | .map(|plan| format!("'{}' ('{}')", plan.name, plan.slug)) 96 | .collect::>() 97 | .join(", "), 98 | )) 99 | } 100 | -------------------------------------------------------------------------------- /src/v4/addon_provider/config_provider/addon/environment.rs: -------------------------------------------------------------------------------- 1 | //! # ConfigProvider addon's environment module 2 | //! 3 | //! This module provide helpers and structures to interact with the config 4 | //! provider addon's environment 5 | 6 | use std::{collections::HashMap, fmt::Debug}; 7 | 8 | #[cfg(feature = "logging")] 9 | use log::{Level, debug, log_enabled}; 10 | use oauth10a::client::{ClientError, RestClient}; 11 | #[cfg(feature = "jsonschemas")] 12 | use schemars::JsonSchema; 13 | use serde::{Deserialize, Serialize}; 14 | 15 | use crate::{Client, v4::addon_provider::AddonProviderId}; 16 | 17 | // ----------------------------------------------------------------------------- 18 | // Variable structure 19 | 20 | #[cfg_attr(feature = "jsonschemas", derive(JsonSchema))] 21 | #[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone, Debug)] 22 | pub struct Variable { 23 | #[serde(rename = "name")] 24 | pub name: String, 25 | #[serde(rename = "value")] 26 | pub value: String, 27 | } 28 | 29 | impl From<(String, String)> for Variable { 30 | #[cfg_attr(feature = "tracing", tracing::instrument)] 31 | fn from((name, value): (String, String)) -> Self { 32 | Self::new(name, value) 33 | } 34 | } 35 | 36 | impl Variable { 37 | #[cfg_attr(feature = "tracing", tracing::instrument)] 38 | pub fn new(name: String, value: String) -> Self { 39 | Self { name, value } 40 | } 41 | } 42 | 43 | // ----------------------------------------------------------------------------- 44 | // Error enumeration 45 | 46 | #[derive(thiserror::Error, Debug)] 47 | pub enum Error { 48 | #[error("failed to get variables of config-provider addon '{0}', {1}")] 49 | Get(String, ClientError), 50 | #[error("failed to update variables of config-provider addon '{0}', {1}")] 51 | Put(String, ClientError), 52 | } 53 | 54 | // ----------------------------------------------------------------------------- 55 | // Helpers 56 | 57 | /// Retrieve environment variables of the config provider addon 58 | #[cfg_attr(feature = "tracing", tracing::instrument)] 59 | pub async fn get(client: &Client, id: &str) -> Result, Error> { 60 | let path = format!( 61 | "{}/v4/addon-providers/{}/addons/{}/env", 62 | client.endpoint, 63 | AddonProviderId::ConfigProvider, 64 | id 65 | ); 66 | 67 | #[cfg(feature = "logging")] 68 | if log_enabled!(Level::Debug) { 69 | debug!( 70 | "execute a request to get information about the config-provider addon, path: '{}', id: '{}'", 71 | &path, id 72 | ); 73 | } 74 | 75 | client 76 | .get(&path) 77 | .await 78 | .map_err(|err| Error::Get(id.to_string(), err)) 79 | } 80 | 81 | /// Update environment variables of the config provider addon 82 | #[cfg_attr(feature = "tracing", tracing::instrument)] 83 | pub async fn put( 84 | client: &Client, 85 | id: &str, 86 | variables: &Vec, 87 | ) -> Result, Error> { 88 | let path = format!( 89 | "{}/v4/addon-providers/{}/addons/{}/env", 90 | client.endpoint, 91 | AddonProviderId::ConfigProvider, 92 | id 93 | ); 94 | 95 | #[cfg(feature = "logging")] 96 | if log_enabled!(Level::Debug) { 97 | debug!( 98 | "execute a request to update information about the config-provider addon, path: '{}', id: '{}'", 99 | &path, id 100 | ); 101 | } 102 | 103 | client 104 | .put(&path, variables) 105 | .await 106 | .map_err(|err| Error::Put(id.to_string(), err)) 107 | } 108 | 109 | /// Insert a new environment variable into config provider 110 | #[cfg_attr(feature = "tracing", tracing::instrument)] 111 | pub async fn insert(client: &Client, id: &str, var: Variable) -> Result, Error> { 112 | bulk_insert(client, id, &[var]).await 113 | } 114 | 115 | /// Insert multiple new environment variables into config provider 116 | #[cfg_attr(feature = "tracing", tracing::instrument)] 117 | pub async fn bulk_insert( 118 | client: &Client, 119 | id: &str, 120 | vars: &[Variable], 121 | ) -> Result, Error> { 122 | let mut v = get(client, id) 123 | .await? 124 | .iter() 125 | .fold(HashMap::new(), |mut acc, v| { 126 | acc.insert(v.name.to_owned(), v.value.to_owned()); 127 | acc 128 | }); 129 | 130 | for var in vars { 131 | v.insert(var.name.to_owned(), var.value.to_owned()); 132 | } 133 | 134 | let v = v.iter().fold(vec![], |mut acc, (k, v)| { 135 | acc.push(Variable::from((k.to_owned(), v.to_owned()))); 136 | acc 137 | }); 138 | 139 | put(client, id, &v).await 140 | } 141 | 142 | /// Remove an environment variable from config provider 143 | #[cfg_attr(feature = "tracing", tracing::instrument)] 144 | pub async fn remove(client: &Client, id: &str, name: &str) -> Result, Error> { 145 | bulk_remove(client, id, &[name]).await 146 | } 147 | 148 | /// Remove multiples environment variables from config provider 149 | #[cfg_attr(feature = "tracing", tracing::instrument)] 150 | pub async fn bulk_remove( 151 | client: &Client, 152 | id: &str, 153 | names: &[&str], 154 | ) -> Result, Error> { 155 | let v: Vec<_> = get(client, id) 156 | .await? 157 | .iter() 158 | .filter(|v| !names.contains(&v.name.as_str())) 159 | .cloned() 160 | .collect(); 161 | 162 | put(client, id, &v).await 163 | } 164 | -------------------------------------------------------------------------------- /src/v4/addon_provider/config_provider/addon/mod.rs: -------------------------------------------------------------------------------- 1 | //! # ConfigProvider addon module 2 | //! 3 | //! This module provide helpers and structures to interact with the config 4 | //! provider addons 5 | 6 | pub mod environment; 7 | -------------------------------------------------------------------------------- /src/v4/addon_provider/config_provider/mod.rs: -------------------------------------------------------------------------------- 1 | //! # ConfigProvider addon provider module 2 | //! 3 | //! This module provides helpers and structures to interact with the config 4 | //! provider addon provider 5 | 6 | pub mod addon; 7 | -------------------------------------------------------------------------------- /src/v4/addon_provider/elasticsearch.rs: -------------------------------------------------------------------------------- 1 | //! # ElasticSearch addon provider module 2 | //! 3 | //! This module provides helpers and structures to interact with the elasticsearch 4 | //! addon provider 5 | #![allow(deprecated)] 6 | 7 | use std::{ 8 | convert::TryFrom, 9 | fmt::{self, Debug, Display, Formatter}, 10 | str::FromStr, 11 | }; 12 | 13 | #[cfg(feature = "logging")] 14 | use log::{Level, debug, log_enabled}; 15 | use oauth10a::client::{ClientError, RestClient}; 16 | #[cfg(feature = "jsonschemas")] 17 | use schemars::JsonSchema_repr as JsonSchemaRepr; 18 | use serde_repr::{Deserialize_repr as DeserializeRepr, Serialize_repr as SerializeRepr}; 19 | 20 | use crate::{ 21 | Client, 22 | v4::addon_provider::{AddonProvider, AddonProviderId}, 23 | }; 24 | 25 | // ----------------------------------------------------------------------------- 26 | // Error enumeration 27 | 28 | #[derive(thiserror::Error, Debug)] 29 | pub enum Error { 30 | #[error("failed to parse version from '{0}', available version are 7 and 8")] 31 | ParseVersion(String), 32 | #[error("failed to get information about addon provider '{0}', {1}")] 33 | Get(AddonProviderId, ClientError), 34 | } 35 | 36 | // ----------------------------------------------------------------------------- 37 | // Version enum 38 | 39 | #[cfg_attr(feature = "jsonschemas", derive(JsonSchemaRepr))] 40 | #[derive(SerializeRepr, DeserializeRepr, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug)] 41 | #[serde(untagged)] 42 | #[repr(i32)] 43 | pub enum Version { 44 | V7 = 7, 45 | V8 = 8, 46 | } 47 | 48 | impl FromStr for Version { 49 | type Err = Error; 50 | 51 | fn from_str(s: &str) -> Result { 52 | Ok(match s { 53 | "7" => Self::V7, 54 | "8" => Self::V8, 55 | _ => { 56 | return Err(Error::ParseVersion(s.to_owned())); 57 | } 58 | }) 59 | } 60 | } 61 | 62 | impl TryFrom for Version { 63 | type Error = Error; 64 | 65 | fn try_from(s: String) -> Result { 66 | Self::from_str(&s) 67 | } 68 | } 69 | 70 | #[allow(clippy::from_over_into)] 71 | impl Into for Version { 72 | fn into(self) -> String { 73 | self.to_string() 74 | } 75 | } 76 | 77 | impl Display for Version { 78 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 79 | match self { 80 | Self::V7 => write!(f, "7"), 81 | Self::V8 => write!(f, "8"), 82 | } 83 | } 84 | } 85 | 86 | // ----------------------------------------------------------------------------- 87 | // Helpers functions 88 | 89 | /// returns information about the elasticsearch addon provider 90 | #[cfg_attr(feature = "tracing", tracing::instrument)] 91 | pub async fn get(client: &Client) -> Result, Error> { 92 | let path = format!( 93 | "{}/v4/addon-providers/{}", 94 | client.endpoint, 95 | AddonProviderId::ElasticSearch 96 | ); 97 | 98 | #[cfg(feature = "logging")] 99 | if log_enabled!(Level::Debug) { 100 | debug!( 101 | "execute a request to get information about the elasticsearch addon-provider, path: '{}', name: '{}'", 102 | &path, 103 | AddonProviderId::ElasticSearch 104 | ); 105 | } 106 | 107 | client 108 | .get(&path) 109 | .await 110 | .map_err(|err| Error::Get(AddonProviderId::ElasticSearch, err)) 111 | } 112 | -------------------------------------------------------------------------------- /src/v4/addon_provider/mod.rs: -------------------------------------------------------------------------------- 1 | //! # Addon provider module 2 | //! 3 | //! This module provide structures and helpers to interact with clever-cloud's 4 | //! addon-provider 5 | 6 | use std::{ 7 | collections::BTreeMap, 8 | convert::TryFrom, 9 | fmt::{self, Debug, Display, Formatter}, 10 | hash::Hash, 11 | str::FromStr, 12 | }; 13 | 14 | #[cfg(feature = "jsonschemas")] 15 | use schemars::JsonSchema; 16 | use serde::{Deserialize, Serialize}; 17 | 18 | pub mod config_provider; 19 | pub mod elasticsearch; 20 | pub mod mongodb; 21 | pub mod mysql; 22 | pub mod postgresql; 23 | pub mod redis; 24 | 25 | // ----------------------------------------------------------------------------- 26 | // Feature structure 27 | 28 | #[cfg_attr(feature = "jsonschemas", derive(JsonSchema))] 29 | #[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Clone, Debug)] 30 | pub struct Feature { 31 | #[serde(rename = "name")] 32 | pub name: String, 33 | #[serde(rename = "enabled")] 34 | pub enabled: bool, 35 | } 36 | 37 | // ----------------------------------------------------------------------------- 38 | // Cluster structure 39 | 40 | #[cfg_attr(feature = "jsonschemas", derive(JsonSchema))] 41 | #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)] 42 | pub struct Cluster { 43 | #[serde(rename = "id")] 44 | pub id: String, 45 | #[serde(rename = "label")] 46 | pub label: String, 47 | #[serde(rename = "zone")] 48 | pub zone: String, 49 | #[serde(rename = "features")] 50 | pub features: Vec, 51 | #[serde(rename = "version")] 52 | pub version: T, 53 | } 54 | 55 | // ----------------------------------------------------------------------------- 56 | // AddonProvider structure 57 | 58 | #[cfg_attr(feature = "jsonschemas", derive(JsonSchema))] 59 | #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)] 60 | pub struct AddonProvider 61 | where 62 | T: Ord, 63 | { 64 | #[serde(rename = "providerId")] 65 | pub provider_id: AddonProviderId, 66 | #[serde(rename = "clusters")] 67 | pub clusters: Vec>, 68 | #[serde(rename = "dedicated")] 69 | pub dedicated: BTreeMap>, 70 | #[serde(rename = "defaultDedicatedVersion")] 71 | pub default: T, 72 | } 73 | 74 | // ----------------------------------------------------------------------------- 75 | // Error enumeration 76 | 77 | #[derive(thiserror::Error, Debug)] 78 | pub enum Error { 79 | #[error( 80 | "failed to parse addon provider identifier '{0}', available options are \ 81 | 'postgresql-addon', 'redis-addon', 'mysql-addon', 'mongodb-addon', \ 82 | 'addon-pulsar', 'config-provider', 'es-addon', 'kv', 'metabase', 'keycloak', \ 83 | 'cellar-addon', 'addon-matomo', 'addon-otoroshi' and 'azimutt'" 84 | )] 85 | Parse(String), 86 | } 87 | 88 | // ----------------------------------------------------------------------------- 89 | // AddonProviderName structure 90 | 91 | #[cfg_attr(feature = "jsonschemas", derive(JsonSchema))] 92 | #[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug)] 93 | #[serde(untagged, try_from = "String", into = "String")] 94 | pub enum AddonProviderId { 95 | PostgreSql, 96 | Redis, 97 | MySql, 98 | MongoDb, 99 | Pulsar, 100 | KV, 101 | ConfigProvider, 102 | ElasticSearch, 103 | Metabase, 104 | Keycloak, 105 | Cellar, 106 | Matomo, 107 | Otoroshi, 108 | Azimutt, 109 | } 110 | 111 | impl AddonProviderId { 112 | pub const fn as_str(&self) -> &'static str { 113 | match self { 114 | Self::PostgreSql => "postgresql-addon", 115 | Self::Redis => "redis-addon", 116 | Self::MySql => "mysql-addon", 117 | Self::MongoDb => "mongodb-addon", 118 | Self::Pulsar => "addon-pulsar", 119 | Self::KV => "kv", 120 | Self::ConfigProvider => "config-provider", 121 | Self::ElasticSearch => "es-addon", 122 | Self::Metabase => "metabase", 123 | Self::Keycloak => "keycloak", 124 | Self::Cellar => "cellar-addon", 125 | Self::Matomo => "addon-matomo", 126 | Self::Otoroshi => "otoroshi", 127 | Self::Azimutt => "azimutt", 128 | } 129 | } 130 | } 131 | 132 | impl FromStr for AddonProviderId { 133 | type Err = Error; 134 | 135 | #[cfg_attr(feature = "tracing", tracing::instrument)] 136 | fn from_str(s: &str) -> Result { 137 | Ok(match s.to_lowercase().as_str() { 138 | "postgresql-addon" => Self::PostgreSql, 139 | "redis-addon" => Self::Redis, 140 | "mysql-addon" => Self::MySql, 141 | "mongodb-addon" => Self::MongoDb, 142 | "addon-pulsar" => Self::Pulsar, 143 | "kv" => Self::KV, 144 | "config-provider" => Self::ConfigProvider, 145 | "es-addon" => Self::ElasticSearch, 146 | "metabase" => Self::Metabase, 147 | "cellar-addon" => Self::Cellar, 148 | "keycloak" => Self::Keycloak, 149 | "addon-matomo" => Self::Matomo, 150 | "otoroshi" => Self::Otoroshi, 151 | "azimutt" => Self::Azimutt, 152 | _ => return Err(Error::Parse(s.to_owned())), 153 | }) 154 | } 155 | } 156 | 157 | impl TryFrom for AddonProviderId { 158 | type Error = Error; 159 | 160 | #[cfg_attr(feature = "tracing", tracing::instrument)] 161 | fn try_from(s: String) -> Result { 162 | Self::from_str(&s) 163 | } 164 | } 165 | 166 | #[allow(clippy::from_over_into)] 167 | impl Into for AddonProviderId { 168 | #[cfg_attr(feature = "tracing", tracing::instrument)] 169 | fn into(self) -> String { 170 | self.to_string() 171 | } 172 | } 173 | 174 | impl Display for AddonProviderId { 175 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 176 | fmt::Display::fmt(self.as_str(), f) 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/v4/addon_provider/mongodb.rs: -------------------------------------------------------------------------------- 1 | //! # MongoDb addon provider module 2 | //! 3 | //! This module provides helpers and structures to interact with the mongodb 4 | //! addon provider 5 | 6 | use std::{ 7 | convert::TryFrom, 8 | fmt::{self, Debug, Display, Formatter}, 9 | str::FromStr, 10 | }; 11 | 12 | #[cfg(feature = "logging")] 13 | use log::{Level, debug, log_enabled}; 14 | use oauth10a::client::{ClientError, RestClient}; 15 | #[cfg(feature = "jsonschemas")] 16 | use schemars::JsonSchema_repr as JsonSchemaRepr; 17 | use serde_repr::{Deserialize_repr as DeserializeRepr, Serialize_repr as SerializeRepr}; 18 | 19 | use crate::{ 20 | Client, 21 | v4::addon_provider::{AddonProvider, AddonProviderId}, 22 | }; 23 | 24 | // ----------------------------------------------------------------------------- 25 | // Error enumeration 26 | 27 | #[derive(thiserror::Error, Debug)] 28 | pub enum Error { 29 | #[error("failed to parse version from '{0}', available version is 4.0.3")] 30 | ParseVersion(String), 31 | #[error("failed to get information about addon provider '{0}', {1}")] 32 | Get(AddonProviderId, ClientError), 33 | } 34 | 35 | // ----------------------------------------------------------------------------- 36 | // Version enum 37 | 38 | #[cfg_attr(feature = "jsonschemas", derive(JsonSchemaRepr))] 39 | #[derive(SerializeRepr, DeserializeRepr, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug)] 40 | #[serde(untagged)] 41 | #[repr(i32)] 42 | pub enum Version { 43 | V4dot0dot3 = 403, 44 | } 45 | 46 | impl FromStr for Version { 47 | type Err = Error; 48 | 49 | fn from_str(s: &str) -> Result { 50 | Ok(match s { 51 | "4.0.3" => Self::V4dot0dot3, 52 | _ => { 53 | return Err(Error::ParseVersion(s.to_owned())); 54 | } 55 | }) 56 | } 57 | } 58 | 59 | impl TryFrom for Version { 60 | type Error = Error; 61 | 62 | fn try_from(s: String) -> Result { 63 | Self::from_str(&s) 64 | } 65 | } 66 | 67 | #[allow(clippy::from_over_into)] 68 | impl Into for Version { 69 | fn into(self) -> String { 70 | self.to_string() 71 | } 72 | } 73 | 74 | impl Display for Version { 75 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 76 | match self { 77 | Self::V4dot0dot3 => write!(f, "4.0.3"), 78 | } 79 | } 80 | } 81 | 82 | // ----------------------------------------------------------------------------- 83 | // Helpers functions 84 | 85 | /// returns information about the mongodb addon provider 86 | #[cfg_attr(feature = "tracing", tracing::instrument)] 87 | pub async fn get(client: &Client) -> Result, Error> { 88 | let path = format!( 89 | "{}/v4/addon-providers/{}", 90 | client.endpoint, 91 | AddonProviderId::MongoDb 92 | ); 93 | 94 | #[cfg(feature = "logging")] 95 | if log_enabled!(Level::Debug) { 96 | debug!( 97 | "execute a request to get information about the mongodb addon-provider, path: '{}', name: '{}'", 98 | &path, 99 | AddonProviderId::MongoDb 100 | ); 101 | } 102 | 103 | client 104 | .get(&path) 105 | .await 106 | .map_err(|err| Error::Get(AddonProviderId::MongoDb, err)) 107 | } 108 | -------------------------------------------------------------------------------- /src/v4/addon_provider/mysql.rs: -------------------------------------------------------------------------------- 1 | //! # MySql addon provider module 2 | //! 3 | //! This module provides helpers and structures to interact with the mysql 4 | //! addon provider 5 | 6 | use std::{ 7 | convert::TryFrom, 8 | fmt::{self, Debug, Display, Formatter}, 9 | str::FromStr, 10 | }; 11 | 12 | #[cfg(feature = "logging")] 13 | use log::{Level, debug, log_enabled}; 14 | use oauth10a::client::{ClientError, RestClient}; 15 | #[cfg(feature = "jsonschemas")] 16 | use schemars::JsonSchema_repr as JsonSchemaRepr; 17 | use serde_repr::{Deserialize_repr as DeserializeRepr, Serialize_repr as SerializeRepr}; 18 | 19 | use crate::{ 20 | Client, 21 | v4::addon_provider::{AddonProvider, AddonProviderId}, 22 | }; 23 | 24 | // ----------------------------------------------------------------------------- 25 | // Error enumeration 26 | 27 | #[derive(thiserror::Error, Debug)] 28 | pub enum Error { 29 | #[error("failed to parse version from '{0}', available versions are 5.7 and 8.0")] 30 | ParseVersion(String), 31 | #[error("failed to get information about addon provider '{0}', {1}")] 32 | Get(AddonProviderId, ClientError), 33 | } 34 | 35 | // ----------------------------------------------------------------------------- 36 | // Version enum 37 | 38 | #[cfg_attr(feature = "jsonschemas", derive(JsonSchemaRepr))] 39 | #[derive(SerializeRepr, DeserializeRepr, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug)] 40 | #[serde(untagged)] 41 | #[repr(i32)] 42 | pub enum Version { 43 | V5dot7 = 57, 44 | V8dot0 = 80, 45 | V8dot4 = 84, 46 | } 47 | 48 | impl FromStr for Version { 49 | type Err = Error; 50 | 51 | fn from_str(s: &str) -> Result { 52 | Ok(match s { 53 | "5.7" => Self::V5dot7, 54 | "8.0" => Self::V8dot0, 55 | "8.4" => Self::V8dot4, 56 | _ => { 57 | return Err(Error::ParseVersion(s.to_owned())); 58 | } 59 | }) 60 | } 61 | } 62 | 63 | impl TryFrom for Version { 64 | type Error = Error; 65 | 66 | fn try_from(s: String) -> Result { 67 | Self::from_str(&s) 68 | } 69 | } 70 | 71 | #[allow(clippy::from_over_into)] 72 | impl Into for Version { 73 | fn into(self) -> String { 74 | self.to_string() 75 | } 76 | } 77 | 78 | impl Display for Version { 79 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 80 | match self { 81 | Self::V5dot7 => write!(f, "5.7"), 82 | Self::V8dot0 => write!(f, "8.0"), 83 | Self::V8dot4 => write!(f, "8.4"), 84 | } 85 | } 86 | } 87 | 88 | // ----------------------------------------------------------------------------- 89 | // Helpers functions 90 | 91 | /// returns information about the mysql addon provider 92 | #[cfg_attr(feature = "tracing", tracing::instrument)] 93 | pub async fn get(client: &Client) -> Result, Error> { 94 | let path = format!( 95 | "{}/v4/addon-providers/{}", 96 | client.endpoint, 97 | AddonProviderId::MySql 98 | ); 99 | 100 | #[cfg(feature = "logging")] 101 | if log_enabled!(Level::Debug) { 102 | debug!( 103 | "execute a request to get information about the mysql addon-provider, path: '{}', name: '{}'", 104 | &path, 105 | AddonProviderId::MySql 106 | ); 107 | } 108 | 109 | client 110 | .get(&path) 111 | .await 112 | .map_err(|err| Error::Get(AddonProviderId::MySql, err)) 113 | } 114 | -------------------------------------------------------------------------------- /src/v4/addon_provider/postgresql.rs: -------------------------------------------------------------------------------- 1 | //! # Postgresql addon provider module 2 | //! 3 | //! This module provide helpers and structures to interact with the postgresql 4 | //! addon provider 5 | #![allow(deprecated)] 6 | 7 | use std::{ 8 | convert::TryFrom, 9 | fmt::{self, Debug, Display, Formatter}, 10 | str::FromStr, 11 | }; 12 | 13 | #[cfg(feature = "logging")] 14 | use log::{Level, debug, log_enabled}; 15 | use oauth10a::client::{ClientError, RestClient}; 16 | #[cfg(feature = "jsonschemas")] 17 | use schemars::JsonSchema_repr as JsonSchemaRepr; 18 | use serde_repr::{Deserialize_repr as DeserializeRepr, Serialize_repr as SerializeRepr}; 19 | 20 | use crate::{ 21 | Client, 22 | v4::addon_provider::{AddonProvider, AddonProviderId}, 23 | }; 24 | 25 | // ----------------------------------------------------------------------------- 26 | // Error enumeration 27 | 28 | #[derive(thiserror::Error, Debug)] 29 | pub enum Error { 30 | #[error( 31 | "failed to parse version from '{0}', available versions are 17, 16, 15, 14, 13, 12 and 11" 32 | )] 33 | ParseVersion(String), 34 | #[error("failed to get information about addon provider '{0}', {1}")] 35 | Get(AddonProviderId, ClientError), 36 | } 37 | 38 | // ----------------------------------------------------------------------------- 39 | // Version enum 40 | 41 | #[cfg_attr(feature = "jsonschemas", derive(JsonSchemaRepr))] 42 | #[derive(SerializeRepr, DeserializeRepr, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug)] 43 | #[serde(untagged)] 44 | #[repr(i32)] 45 | pub enum Version { 46 | V11 = 11, 47 | V12 = 12, 48 | V13 = 13, 49 | V14 = 14, 50 | V15 = 15, 51 | V16 = 16, 52 | V17 = 17, 53 | } 54 | 55 | impl FromStr for Version { 56 | type Err = Error; 57 | 58 | fn from_str(s: &str) -> Result { 59 | Ok(match s { 60 | "17" => Self::V17, 61 | "16" => Self::V16, 62 | "15" => Self::V15, 63 | "14" => Self::V14, 64 | "13" => Self::V13, 65 | "12" => Self::V12, 66 | "11" => Self::V11, 67 | _ => { 68 | return Err(Error::ParseVersion(s.to_owned())); 69 | } 70 | }) 71 | } 72 | } 73 | 74 | impl TryFrom for Version { 75 | type Error = Error; 76 | 77 | fn try_from(s: String) -> Result { 78 | Self::from_str(&s) 79 | } 80 | } 81 | 82 | #[allow(clippy::from_over_into)] 83 | impl Into for Version { 84 | fn into(self) -> String { 85 | self.to_string() 86 | } 87 | } 88 | 89 | impl Display for Version { 90 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 91 | match self { 92 | Self::V17 => write!(f, "17"), 93 | Self::V16 => write!(f, "16"), 94 | Self::V15 => write!(f, "15"), 95 | Self::V14 => write!(f, "14"), 96 | Self::V13 => write!(f, "13"), 97 | Self::V12 => write!(f, "12"), 98 | Self::V11 => write!(f, "11"), 99 | } 100 | } 101 | } 102 | 103 | // ----------------------------------------------------------------------------- 104 | // Helpers functions 105 | 106 | #[cfg_attr(feature = "tracing", tracing::instrument)] 107 | /// returns information about the postgresql addon provider 108 | pub async fn get(client: &Client) -> Result, Error> { 109 | let path = format!( 110 | "{}/v4/addon-providers/{}", 111 | client.endpoint, 112 | AddonProviderId::PostgreSql 113 | ); 114 | 115 | #[cfg(feature = "logging")] 116 | if log_enabled!(Level::Debug) { 117 | debug!( 118 | "execute a request to get information about the postgresql addon-provider, path: '{}', name: '{}'", 119 | &path, 120 | AddonProviderId::PostgreSql 121 | ); 122 | } 123 | 124 | client 125 | .get(&path) 126 | .await 127 | .map_err(|err| Error::Get(AddonProviderId::PostgreSql, err)) 128 | } 129 | -------------------------------------------------------------------------------- /src/v4/addon_provider/redis.rs: -------------------------------------------------------------------------------- 1 | //! # Redis addon provider module 2 | //! 3 | //! This module provide helpers and structures to interact with the redis 4 | //! addon provider 5 | #![allow(deprecated)] 6 | 7 | use std::{ 8 | convert::TryFrom, 9 | fmt::{self, Debug, Display, Formatter}, 10 | str::FromStr, 11 | }; 12 | 13 | #[cfg(feature = "logging")] 14 | use log::{Level, debug, log_enabled}; 15 | use oauth10a::client::{ClientError, RestClient}; 16 | #[cfg(feature = "jsonschemas")] 17 | use schemars::JsonSchema_repr as JsonSchemaRepr; 18 | use serde_repr::{Deserialize_repr as DeserializeRepr, Serialize_repr as SerializeRepr}; 19 | 20 | use crate::{ 21 | Client, 22 | v4::addon_provider::{AddonProvider, AddonProviderId}, 23 | }; 24 | 25 | // ----------------------------------------------------------------------------- 26 | // Error enumeration 27 | 28 | #[derive(thiserror::Error, Debug)] 29 | pub enum Error { 30 | #[error("failed to parse version from {0}, available version is 7.2.4")] 31 | ParseVersion(String), 32 | #[error("failed to get information about addon provider '{0}', {1}")] 33 | Get(AddonProviderId, ClientError), 34 | } 35 | 36 | // ----------------------------------------------------------------------------- 37 | // Version enum 38 | 39 | #[cfg_attr(feature = "jsonschemas", derive(JsonSchemaRepr))] 40 | #[derive(SerializeRepr, DeserializeRepr, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug)] 41 | #[serde(untagged)] 42 | #[repr(i32)] 43 | pub enum Version { 44 | V7dot2dot4 = 724, 45 | } 46 | 47 | impl FromStr for Version { 48 | type Err = Error; 49 | 50 | fn from_str(s: &str) -> Result { 51 | Ok(match s { 52 | "7.2.4" => Self::V7dot2dot4, 53 | _ => { 54 | return Err(Error::ParseVersion(s.to_owned())); 55 | } 56 | }) 57 | } 58 | } 59 | 60 | impl TryFrom for Version { 61 | type Error = Error; 62 | 63 | fn try_from(s: String) -> Result { 64 | Self::from_str(&s) 65 | } 66 | } 67 | 68 | #[allow(clippy::from_over_into)] 69 | impl Into for Version { 70 | fn into(self) -> String { 71 | self.to_string() 72 | } 73 | } 74 | 75 | impl Display for Version { 76 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 77 | match self { 78 | Self::V7dot2dot4 => write!(f, "7.2.4"), 79 | } 80 | } 81 | } 82 | 83 | // ----------------------------------------------------------------------------- 84 | // Helpers functions 85 | 86 | #[cfg_attr(feature = "tracing", tracing::instrument)] 87 | /// returns information about the redis addon provider 88 | pub async fn get(client: &Client) -> Result, Error> { 89 | let path = format!( 90 | "{}/v4/addon-providers/{}", 91 | client.endpoint, 92 | AddonProviderId::Redis 93 | ); 94 | 95 | #[cfg(feature = "logging")] 96 | if log_enabled!(Level::Debug) { 97 | debug!( 98 | "execute a request to get information about the redis addon-provider, path: '{}', name: '{}'", 99 | &path, 100 | AddonProviderId::Redis 101 | ); 102 | } 103 | 104 | client 105 | .get(&path) 106 | .await 107 | .map_err(|err| Error::Get(AddonProviderId::Redis, err)) 108 | } 109 | -------------------------------------------------------------------------------- /src/v4/functions/deployments.rs: -------------------------------------------------------------------------------- 1 | //! # Deployment module 2 | //! 3 | //! This module provides structures to interact with functions' deployments. 4 | 5 | use std::{ 6 | fmt::{self, Debug, Display, Formatter}, 7 | str::FromStr, 8 | }; 9 | 10 | use chrono::{DateTime, Utc}; 11 | use log::{Level, debug, log_enabled}; 12 | use oauth10a::client::{ 13 | ClientError, Request, RestClient, 14 | reqwest::{ 15 | self, Body, Method, 16 | header::{CONTENT_LENGTH, CONTENT_TYPE, HeaderValue}, 17 | }, 18 | url, 19 | }; 20 | use serde::{Deserialize, Serialize}; 21 | 22 | use crate::Client; 23 | 24 | // ----------------------------------------------------------------------------- 25 | // Constants 26 | 27 | pub const MIME_APPLICATION_WASM: &str = "application/wasm"; 28 | 29 | // ---------------------------------------------------------------------------- 30 | // Error 31 | 32 | #[derive(thiserror::Error, Debug)] 33 | pub 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)] 66 | pub 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 | 77 | impl 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 | 88 | impl FromStr for Platform { 89 | type Err = Error; 90 | 91 | fn from_str(s: &str) -> Result { 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)] 106 | pub 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 | 119 | impl 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 | 131 | impl FromStr for Status { 132 | type Err = Error; 133 | 134 | fn from_str(s: &str) -> Result { 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)] 150 | pub struct Opts { 151 | #[serde(rename = "name")] 152 | pub name: Option, 153 | #[serde(rename = "description")] 154 | pub description: Option, 155 | #[serde(rename = "tag")] 156 | pub tag: Option, 157 | #[serde(rename = "platform")] 158 | pub platform: Platform, 159 | } 160 | 161 | // ---------------------------------------------------------------------------- 162 | // DeploymentCreation 163 | 164 | #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)] 165 | pub 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, 172 | #[serde(rename = "description")] 173 | pub description: Option, 174 | #[serde(rename = "tag")] 175 | pub tag: Option, 176 | #[serde(rename = "platform")] 177 | pub platform: Platform, 178 | #[serde(rename = "status")] 179 | pub status: Status, 180 | #[serde(rename = "errorReason")] 181 | pub reason: Option, 182 | #[serde(rename = "uploadUrl")] 183 | pub upload_url: String, 184 | #[serde(rename = "createdAt")] 185 | pub created_at: DateTime, 186 | #[serde(rename = "updatedAt")] 187 | pub updated_at: DateTime, 188 | } 189 | 190 | // ---------------------------------------------------------------------------- 191 | // Deployment 192 | 193 | #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)] 194 | pub 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, 201 | #[serde(rename = "description")] 202 | pub description: Option, 203 | #[serde(rename = "tag")] 204 | pub tag: Option, 205 | #[serde(rename = "platform")] 206 | pub platform: Platform, 207 | #[serde(rename = "status")] 208 | pub status: Status, 209 | #[serde(rename = "errorReason")] 210 | pub reason: Option, 211 | #[serde(rename = "url")] 212 | pub url: Option, 213 | #[serde(rename = "createdAt")] 214 | pub created_at: DateTime, 215 | #[serde(rename = "updatedAt")] 216 | pub updated_at: DateTime, 217 | } 218 | 219 | // ---------------------------------------------------------------------------- 220 | // Helpers 221 | 222 | #[cfg_attr(feature = "tracing", tracing::instrument)] 223 | /// returns the list of deployments for a function 224 | pub async fn list( 225 | client: &Client, 226 | organisation_id: &str, 227 | function_id: &str, 228 | ) -> Result, 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 249 | pub async fn create( 250 | client: &Client, 251 | organisation_id: &str, 252 | function_id: &str, 253 | opts: &Opts, 254 | ) -> Result { 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 275 | pub async fn get( 276 | client: &Client, 277 | organisation_id: &str, 278 | function_id: &str, 279 | deployment_id: &str, 280 | ) -> Result { 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 305 | pub 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 339 | pub async fn upload(client: &Client, endpoint: &str, buf: Vec) -> 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 376 | pub 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 | } 403 | -------------------------------------------------------------------------------- /src/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 | 6 | use std::{collections::BTreeMap, fmt::Debug}; 7 | 8 | use chrono::{DateTime, Utc}; 9 | use log::{Level, debug, log_enabled}; 10 | use oauth10a::client::{ 11 | ClientError, RestClient, 12 | bytes::Buf, 13 | reqwest::{self, Method}, 14 | url, 15 | }; 16 | use serde::{Deserialize, Serialize}; 17 | 18 | use crate::Client; 19 | 20 | pub mod deployments; 21 | 22 | // ----------------------------------------------------------------------------- 23 | // Error 24 | 25 | #[derive(thiserror::Error, Debug)] 26 | pub 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)] 53 | pub struct Opts { 54 | #[serde(rename = "name")] 55 | pub name: Option, 56 | #[serde(rename = "description")] 57 | pub description: Option, 58 | #[serde(rename = "tag")] 59 | pub tag: Option, 60 | #[serde(rename = "environment")] 61 | pub environment: BTreeMap, 62 | #[serde(rename = "maxMemory")] 63 | pub max_memory: u64, 64 | #[serde(rename = "maxInstances")] 65 | pub max_instances: u64, 66 | } 67 | 68 | impl 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)] 85 | pub 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, 92 | #[serde(rename = "description", skip_serializing_if = "Option::is_none")] 93 | pub description: Option, 94 | #[serde(rename = "tag", skip_serializing_if = "Option::is_none")] 95 | pub tag: Option, 96 | #[serde(rename = "environment")] 97 | pub environment: BTreeMap, 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, 104 | #[serde(rename = "updatedAt")] 105 | pub updated_at: DateTime, 106 | } 107 | 108 | // ----------------------------------------------------------------------------- 109 | // ExecuteResult structure 110 | 111 | #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)] 112 | #[serde(untagged)] 113 | pub 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, 123 | }, 124 | Err { 125 | #[serde(rename = "error")] 126 | error: String, 127 | }, 128 | } 129 | 130 | impl ExecutionResult { 131 | #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] 132 | pub fn ok(stdout: T, stderr: U, dmesg: V, current_pages: Option) -> 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(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 172 | pub async fn list(client: &Client, organisation_id: &str) -> Result, 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 193 | pub async fn create( 194 | client: &Client, 195 | organisation_id: &str, 196 | opts: &Opts, 197 | ) -> Result { 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 218 | pub async fn get( 219 | client: &Client, 220 | organisation_id: &str, 221 | function_id: &str, 222 | ) -> Result { 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 243 | pub async fn update( 244 | client: &Client, 245 | organisation_id: &str, 246 | function_id: &str, 247 | opts: &Opts, 248 | ) -> Result { 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 269 | pub 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 294 | pub async fn execute(client: &Client, endpoint: &str) -> Result { 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 | } 307 | -------------------------------------------------------------------------------- /src/v4/mod.rs: -------------------------------------------------------------------------------- 1 | //! # Api version 4 module 2 | //! 3 | //! This module exposes resources under version 4 of the Clever-Cloud Api. 4 | 5 | pub mod addon_provider; 6 | pub mod functions; 7 | pub mod products; 8 | -------------------------------------------------------------------------------- /src/v4/products/mod.rs: -------------------------------------------------------------------------------- 1 | //! # Product module 2 | //! 3 | //! This module provide structures and helpers to interact with clever-cloud's 4 | //! products api 5 | 6 | pub mod zones; 7 | -------------------------------------------------------------------------------- /src/v4/products/zones.rs: -------------------------------------------------------------------------------- 1 | //! # Zones module 2 | //! 3 | //! This module provide helpers and structures to interact with zones of products 4 | 5 | use std::fmt::Debug; 6 | 7 | #[cfg(feature = "logging")] 8 | use log::{Level, debug, log_enabled}; 9 | use oauth10a::client::{ClientError, RestClient}; 10 | #[cfg(feature = "jsonschemas")] 11 | use schemars::JsonSchema; 12 | use serde::{Deserialize, Serialize}; 13 | use uuid::Uuid; 14 | 15 | use crate::Client; 16 | 17 | // ----------------------------------------------------------------------------- 18 | // Constants 19 | 20 | pub const TAG_APPLICATION: &str = "for:applications"; 21 | pub const TAG_HDS: &str = "certification:hds"; 22 | 23 | // ----------------------------------------------------------------------------- 24 | // Zone structure 25 | 26 | #[cfg_attr(feature = "jsonschemas", derive(JsonSchema))] 27 | #[derive(Serialize, Deserialize, PartialEq, PartialOrd, Clone, Debug)] 28 | pub struct Zone { 29 | #[serde(rename = "id")] 30 | pub id: Uuid, 31 | #[serde(rename = "city")] 32 | pub city: String, 33 | #[serde(rename = "country")] 34 | pub country: String, 35 | #[serde(rename = "name")] 36 | pub name: String, 37 | #[serde(rename = "countryCode")] 38 | pub coutry_code: String, 39 | #[serde(rename = "lat")] 40 | pub latitude: f64, 41 | #[serde(rename = "lon")] 42 | pub longitude: f64, 43 | #[serde(rename = "tags")] 44 | pub tags: Vec, 45 | } 46 | 47 | // ----------------------------------------------------------------------------- 48 | // Error enumeration 49 | 50 | #[derive(thiserror::Error, Debug)] 51 | pub enum Error { 52 | #[error("failed to list available zones, {0}")] 53 | List(ClientError), 54 | } 55 | 56 | // ----------------------------------------------------------------------------- 57 | // List zones 58 | 59 | #[cfg_attr(feature = "tracing", tracing::instrument)] 60 | /// returns the list of zones availables 61 | pub async fn list(client: &Client) -> Result, Error> { 62 | let path = format!("{}/v4/products/zones", client.endpoint); 63 | 64 | #[cfg(feature = "logging")] 65 | if log_enabled!(Level::Debug) { 66 | debug!("execute a request to list zones, path: '{}'", &path); 67 | } 68 | 69 | client.get(&path).await.map_err(Error::List) 70 | } 71 | 72 | #[cfg_attr(feature = "tracing", tracing::instrument)] 73 | /// applications returns the list of zones availables for applications and addons 74 | pub async fn applications(client: &Client) -> Result, Error> { 75 | Ok(list(client) 76 | .await? 77 | .iter() 78 | .filter(|zone| zone.tags.contains(&TAG_APPLICATION.to_string())) 79 | .map(ToOwned::to_owned) 80 | .collect()) 81 | } 82 | 83 | #[cfg_attr(feature = "tracing", tracing::instrument)] 84 | /// hds returns the list of zones availables for applications and addons with 85 | /// hds certification 86 | pub async fn hds(client: &Client) -> Result, Error> { 87 | Ok(list(client) 88 | .await? 89 | .iter() 90 | .filter(|zone| zone.tags.contains(&TAG_APPLICATION.to_string())) 91 | .filter(|zone| zone.tags.contains(&TAG_HDS.to_string())) 92 | .map(ToOwned::to_owned) 93 | .collect()) 94 | } 95 | --------------------------------------------------------------------------------