From 12d2ef1aa90ee416bc609dab5a996e1bbde5dd74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Lemaire-Giroud?= Date: Fri, 30 May 2025 08:11:38 +0200 Subject: [PATCH 1/3] build: bump oauth10 --- Cargo.toml | 39 +- README.md | 45 +- examples/cleverctl/src/cfg.rs | 6 +- .../cmd/addon/config_provider/environment.rs | 4 +- .../src/cmd/addon/config_provider/mod.rs | 4 +- examples/cleverctl/src/cmd/addon/mod.rs | 4 +- .../src/cmd/functions/deployments.rs | 5 +- examples/cleverctl/src/cmd/functions/mod.rs | 5 +- examples/cleverctl/src/cmd/mod.rs | 8 +- examples/cleverctl/src/cmd/myself.rs | 4 +- examples/cleverctl/src/cmd/zone.rs | 7 +- examples/cleverctl/src/logging.rs | 2 +- examples/cleverctl/src/main.rs | 2 +- src/lib.rs | 431 ++++++------------ src/logging.rs | 51 +++ src/v2/addon.rs | 182 ++++---- src/v2/error.rs | 12 + src/v2/mod.rs | 4 + src/v2/myself.rs | 33 +- src/v2/plan.rs | 28 +- .../config_provider/addon/environment.rs | 78 ++-- src/v4/addon_provider/elasticsearch.rs | 58 ++- src/v4/addon_provider/mod.rs | 22 +- src/v4/addon_provider/mongodb.rs | 59 ++- src/v4/addon_provider/mysql.rs | 59 ++- src/v4/addon_provider/postgresql.rs | 64 ++- src/v4/addon_provider/redis.rs | 58 ++- src/v4/error.rs | 155 +++++++ src/v4/functions/deployments.rs | 250 +++++----- src/v4/functions/mod.rs | 217 +++++---- src/v4/mod.rs | 7 +- src/v4/products/zones.rs | 40 +- 32 files changed, 1001 insertions(+), 942 deletions(-) mode change 100644 => 100755 Cargo.toml mode change 100644 => 100755 README.md mode change 100644 => 100755 examples/cleverctl/src/cfg.rs mode change 100644 => 100755 examples/cleverctl/src/cmd/addon/config_provider/environment.rs mode change 100644 => 100755 examples/cleverctl/src/cmd/addon/config_provider/mod.rs mode change 100644 => 100755 examples/cleverctl/src/cmd/addon/mod.rs mode change 100644 => 100755 examples/cleverctl/src/cmd/functions/deployments.rs mode change 100644 => 100755 examples/cleverctl/src/cmd/functions/mod.rs mode change 100644 => 100755 examples/cleverctl/src/cmd/mod.rs mode change 100644 => 100755 examples/cleverctl/src/cmd/myself.rs mode change 100644 => 100755 examples/cleverctl/src/cmd/zone.rs mode change 100644 => 100755 examples/cleverctl/src/logging.rs mode change 100644 => 100755 examples/cleverctl/src/main.rs mode change 100644 => 100755 src/lib.rs create mode 100755 src/logging.rs mode change 100644 => 100755 src/v2/addon.rs create mode 100755 src/v2/error.rs mode change 100644 => 100755 src/v2/mod.rs mode change 100644 => 100755 src/v2/myself.rs mode change 100644 => 100755 src/v2/plan.rs mode change 100644 => 100755 src/v4/addon_provider/config_provider/addon/environment.rs mode change 100644 => 100755 src/v4/addon_provider/elasticsearch.rs mode change 100644 => 100755 src/v4/addon_provider/mod.rs mode change 100644 => 100755 src/v4/addon_provider/mongodb.rs mode change 100644 => 100755 src/v4/addon_provider/mysql.rs mode change 100644 => 100755 src/v4/addon_provider/postgresql.rs mode change 100644 => 100755 src/v4/addon_provider/redis.rs create mode 100755 src/v4/error.rs mode change 100644 => 100755 src/v4/functions/deployments.rs mode change 100644 => 100755 src/v4/functions/mod.rs mode change 100644 => 100755 src/v4/products/zones.rs diff --git a/Cargo.toml b/Cargo.toml old mode 100644 new mode 100755 index c2777fc..8022228 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "clevercloud-sdk" -description = "A rust client and structures to interact with the Clever-Cloud API." +description = "A Rust client and structures to interact with the Clever-Cloud API." version = "0.15.0" edition = "2024" rust-version = "1.85.0" @@ -12,12 +12,26 @@ keywords = ["clevercloud", "sdk", "logging", "metrics", "jsonschemas"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +default = ["logging"] +jsonschemas = ["dep:schemars"] +logging = ["dep:log", "oauth10a/logging", "tracing/log-always"] +metrics = ["oauth10a/metrics"] +tracing = ["oauth10a/tracing", "dep:tracing"] +network-group = ["dep:base64", "x25519-dalek", "dep:rand_core", "dep:zeroize"] + [dependencies] -chrono = { version = "^0.4.40", features = ["serde"] } -oauth10a = { version = "^2.1.1", default-features = false, features = [ +base64 = { version = "^0.22.1", optional = true } +chrono = { version = "^0.4.41", features = ["serde"] } +oauth10a = { version = "^3.0.0", default-features = false, features = [ "client", + "serde", + "rest", + "sse", + "zeroize", ] } -log = { version = "^0.4.26", optional = true } +log = { version = "^0.4.27", optional = true } +rand_core = { version = "^0.9.3", features = ["os_rng"], optional = true } schemars = { version = "^0.8.22", features = [ "chrono", "indexmap1", @@ -30,11 +44,12 @@ serde_repr = "^0.1.20" serde_json = "^1.0.140" thiserror = "^2.0.12" tracing = { version = "^0.1.41", optional = true } -uuid = { version = "^1.16.0", features = ["serde", "v4"] } - -[features] -default = ["logging"] -jsonschemas = ["schemars"] -logging = ["oauth10a/logging", "tracing/log-always", "log"] -metrics = ["oauth10a/metrics"] -tracing = ["oauth10a/tracing", "dep:tracing"] +uuid = { version = "^1.17.0", features = ["serde", "v4"] } +x25519-dalek = { version = "^2.0.1", features = [ + "zeroize", + "static_secrets", +], optional = true } +zeroize = { version = "^1.8.1", features = [ + "serde", + "derive", +], optional = true } diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 7f84644..db522e4 --- a/README.md +++ b/README.md @@ -4,8 +4,7 @@ [![Released API docs](https://docs.rs/clevercloud-sdk/badge.svg)](https://docs.rs/clevercloud-sdk) [![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) -> This crate provides structures and a client to interact with the Clever-Cloud -> API. +> This crate provides structures and a client to interact with the Clever-Cloud API. ## Status @@ -25,17 +24,19 @@ Below, you will find an example of executing a request to get information about myself. ```rust -use std::error::Error; - -use clevercloud_sdk::{Client, v2::myself::{self, Myself}}; +use clevercloud_sdk::{ + Client, + oauth10a::credentials::Credentials, + v2::myself::{self, Myself}, +}; #[tokio::main] -async fn main() -> Result<(), Box> { - let client = Client::from(Credentials { - token: "".to_string(), - secret: "".to_string(), - consumer_key: "".to_string(), - consumer_secret: "".to_string(), +async fn main() -> Result<(), Box> { + let client = Client::from(Credentials::OAuth1 { + token: "", + secret: "", + consumer_key: "", + consumer_secret: "", }); let _myself: Myself = myself::get(&client).await?; @@ -48,21 +49,23 @@ You could found more examples of how you could use the clevercloud-sdk by lookin ## Features -| name | description | -| ----------- |--------------------------------------------------------------------------------------------------| -| trace | Use `tracing` crate to expose traces | -| jsonschemas | Use `schemars` to add a derive instruction to generate json schemas representation of structures | -| logging | Use the `log` facility crate to print logs. Implies `oauth10a/logging` feature | -| metrics | Expose HTTP metrics through `oauth10a` crate feature. | +| name | description | +| ------------- |--------------------------------------------------------------------------------------------------| +| trace | Use `tracing` crate to expose traces | +| jsonschemas | Use `schemars` to add a derive instruction to generate json schemas representation of structures | +| logging | Use the `log` facility crate to print logs. Implies `oauth10a/logging` feature | +| metrics | Expose HTTP metrics through `oauth10a` crate feature. | +| network-group | Enables Clever-Cloud Network Group API. | ### Metrics Below, the exposed metrics gathered by prometheus: -| name | labels | kind | description | -| -------------------------------- | --------------------------------------------------------------- | ------- | -------------------------- | -| oauth10a_client_request | endpoint: String, method: String, status: Integer | Counter | number of request on api | -| oauth10a_client_request_duration | endpoint: String, method: String, status: Integer, unit: String | Counter | duration of request on api | +| name | labels | kind | description | +| -------------------------------- | --------------------------------------------------------------- | ------- | ---------------------------------- | +| oauth10a_client_request | endpoint: String, method: String, status: Integer | Counter | number of request on API | +| oauth10a_client_request_duration | endpoint: String, method: String, status: Integer, unit: String | Counter | duration of request on API | +| oauth10a_client_sse | endpoint: String | Counter | number of events received from API | ## License diff --git a/examples/cleverctl/src/cfg.rs b/examples/cleverctl/src/cfg.rs old mode 100644 new mode 100755 index 0e77ca2..1f2c001 --- a/examples/cleverctl/src/cfg.rs +++ b/examples/cleverctl/src/cfg.rs @@ -4,14 +4,14 @@ use std::path::PathBuf; -use clevercloud_sdk::Credentials; +use clevercloud_sdk::oauth10a::credentials::Credentials; use config::{Config, ConfigError, File}; use serde::{Deserialize, Serialize}; // ----------------------------------------------------------------------------- // Error enumeration -#[derive(thiserror::Error, Debug)] +#[derive(Debug, thiserror::Error)] pub enum Error { #[error("failed to load configuration from file '{0}', {1}")] LoadConfiguration(String, ConfigError), @@ -24,7 +24,7 @@ pub enum Error { // ----------------------------------------------------------------------------- // Configuration structure -#[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Configuration { #[serde(rename = "credentials", flatten)] pub credentials: Credentials, diff --git a/examples/cleverctl/src/cmd/addon/config_provider/environment.rs b/examples/cleverctl/src/cmd/addon/config_provider/environment.rs old mode 100644 new mode 100755 index 8e3c377..7d6c192 --- a/examples/cleverctl/src/cmd/addon/config_provider/environment.rs +++ b/examples/cleverctl/src/cmd/addon/config_provider/environment.rs @@ -16,7 +16,7 @@ use crate::{ // ----------------------------------------------------------------------------- // Error enumeration -#[derive(thiserror::Error, Debug)] +#[derive(Debug, thiserror::Error)] pub enum Error { #[error("failed to format output, {0}")] FormatOutput(Box), @@ -37,7 +37,7 @@ pub enum Error { // ----------------------------------------------------------------------------- // Environment enumeration -#[derive(Subcommand, PartialEq, Eq, Clone, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, Subcommand)] pub enum Environment { #[clap(name = "get", about = "Get environment variables")] Get { diff --git a/examples/cleverctl/src/cmd/addon/config_provider/mod.rs b/examples/cleverctl/src/cmd/addon/config_provider/mod.rs old mode 100644 new mode 100755 index d6d06af..c2b7153 --- a/examples/cleverctl/src/cmd/addon/config_provider/mod.rs +++ b/examples/cleverctl/src/cmd/addon/config_provider/mod.rs @@ -16,7 +16,7 @@ pub mod environment; // ----------------------------------------------------------------------------- // Error enumeration -#[derive(thiserror::Error, Debug)] +#[derive(Debug, thiserror::Error)] pub enum Error { #[error("failed to execute command on config-provider environment, {0}")] Environment(environment::Error), @@ -25,7 +25,7 @@ pub enum Error { // ----------------------------------------------------------------------------- // ConfigProvider structure -#[derive(Subcommand, Eq, PartialEq, Clone, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, Subcommand)] pub enum ConfigProvider { #[clap(name = "environment", aliases = &["env"], subcommand, about = "Interact with config-provider environment")] Environment(Environment), diff --git a/examples/cleverctl/src/cmd/addon/mod.rs b/examples/cleverctl/src/cmd/addon/mod.rs old mode 100644 new mode 100755 index 9176a68..a7910cf --- a/examples/cleverctl/src/cmd/addon/mod.rs +++ b/examples/cleverctl/src/cmd/addon/mod.rs @@ -16,7 +16,7 @@ pub mod config_provider; // ----------------------------------------------------------------------------- // Error enumeration -#[derive(thiserror::Error, Debug)] +#[derive(Debug, thiserror::Error)] pub enum Error { #[error("failed to format output, {0}")] FormatOutput(Box), @@ -33,7 +33,7 @@ pub enum Error { // ----------------------------------------------------------------------------- // Addon enumeration -#[derive(Subcommand, Eq, PartialEq, Clone, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, Subcommand)] pub enum Command { #[clap(name = "list", about = "List addons of an organisation")] List { diff --git a/examples/cleverctl/src/cmd/functions/deployments.rs b/examples/cleverctl/src/cmd/functions/deployments.rs old mode 100644 new mode 100755 index b0d1b1e..b21e347 --- a/examples/cleverctl/src/cmd/functions/deployments.rs +++ b/examples/cleverctl/src/cmd/functions/deployments.rs @@ -5,7 +5,6 @@ use std::{collections::BTreeMap, path::PathBuf, sync::Arc}; -use clap::Subcommand; use clevercloud_sdk::{ Client, oauth10a::reqwest, @@ -22,7 +21,7 @@ use crate::{ // ---------------------------------------------------------------------------- // Error -#[derive(thiserror::Error, Debug)] +#[derive(Debug, thiserror::Error)] pub enum Error { #[error("failed to format output, {0}")] FormatOutput(Box), @@ -49,7 +48,7 @@ pub enum Error { // ---------------------------------------------------------------------------- // Command -#[derive(Subcommand, PartialEq, Eq, Clone, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, clap::Subcommand)] pub enum Command { #[clap(name = "list", aliases = &["l"], about = "List functions information of an organisation")] List { diff --git a/examples/cleverctl/src/cmd/functions/mod.rs b/examples/cleverctl/src/cmd/functions/mod.rs old mode 100644 new mode 100755 index aee6df1..6b2ae65 --- a/examples/cleverctl/src/cmd/functions/mod.rs +++ b/examples/cleverctl/src/cmd/functions/mod.rs @@ -3,7 +3,6 @@ //! This module provides command implementation related to functions product use std::{collections::BTreeMap, sync::Arc}; -use clap::Subcommand; use clevercloud_sdk::{Client, oauth10a::reqwest, v4::functions}; use tracing::info; @@ -23,7 +22,7 @@ pub const DEFAULT_MAX_MEMORY: u64 = 64 * 1024 * 1024; // ----------------------------------------------------------------------------- // Error enumeration -#[derive(thiserror::Error, Debug)] +#[derive(Debug, thiserror::Error)] pub enum Error { #[error("failed to format output, {0}")] FormatOutput(Box), @@ -57,7 +56,7 @@ pub enum Error { // Command /// Command enum contains all operations that could be achieved on the user -#[derive(Subcommand, Eq, PartialEq, Clone, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, clap::Subcommand)] pub enum Command { #[clap(name = "list", aliases = &["l"], about = "List functions information of an organisation")] List { diff --git a/examples/cleverctl/src/cmd/mod.rs b/examples/cleverctl/src/cmd/mod.rs old mode 100644 new mode 100755 index 02bdada..62ef5da --- a/examples/cleverctl/src/cmd/mod.rs +++ b/examples/cleverctl/src/cmd/mod.rs @@ -25,7 +25,7 @@ pub mod zone; // ----------------------------------------------------------------------------- // Error enumeration -#[derive(thiserror::Error, Debug)] +#[derive(Debug, thiserror::Error)] pub enum Error { #[error("failed to parse output '{0}', available options are 'json' or 'yaml'")] ParseOutput(String), @@ -46,7 +46,7 @@ pub enum Error { // ----------------------------------------------------------------------------- // Output enumeration -#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug, Default)] +#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum Output { #[default] Json, @@ -103,7 +103,7 @@ pub trait Executor { // Command enumeration /// Command enum contains all operations that the command line could handle -#[derive(Subcommand, Eq, PartialEq, Clone, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, Subcommand)] pub enum Command { #[clap(name = "self", aliases = &["sel", "se", "s"], subcommand, about = "Interact with the current user")] Myself(myself::Command), @@ -133,7 +133,7 @@ impl Executor for Command { /// Args structure contains all commands and global flags that the command line /// supports -#[derive(Parser, Eq, PartialEq, Clone, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, Parser)] #[clap(author, version, about)] pub struct Args { /// Specify a configuration file diff --git a/examples/cleverctl/src/cmd/myself.rs b/examples/cleverctl/src/cmd/myself.rs old mode 100644 new mode 100755 index 8897dc1..492773b --- a/examples/cleverctl/src/cmd/myself.rs +++ b/examples/cleverctl/src/cmd/myself.rs @@ -14,7 +14,7 @@ use crate::{ // ----------------------------------------------------------------------------- // Error enumeration -#[derive(thiserror::Error, Debug)] +#[derive(Debug, thiserror::Error)] pub enum Error { #[error("failed to format output, {0}")] FormatOutput(Box), @@ -28,7 +28,7 @@ pub enum Error { // Command enumeration /// Command enum contains all operations that could be achieved on the user -#[derive(Subcommand, Eq, PartialEq, Clone, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, Subcommand)] pub enum Command { #[clap(name = "get", aliases = &["ge", "g"], about = "Get information about the current user")] Get { diff --git a/examples/cleverctl/src/cmd/zone.rs b/examples/cleverctl/src/cmd/zone.rs old mode 100644 new mode 100755 index f61acf3..6611d3c --- a/examples/cleverctl/src/cmd/zone.rs +++ b/examples/cleverctl/src/cmd/zone.rs @@ -3,7 +3,6 @@ //! This module provides command implementation related to the zone API use std::sync::Arc; -use clap::Subcommand; use clevercloud_sdk::{Client, oauth10a::reqwest, v4::products::zones}; use crate::{ @@ -14,7 +13,7 @@ use crate::{ // ----------------------------------------------------------------------------- // Error enumeration -#[derive(thiserror::Error, Debug)] +#[derive(Debug, thiserror::Error)] pub enum Error { #[error("failed to format output, {0}")] FormatOutput(Box), @@ -28,7 +27,7 @@ pub enum Error { // Command enumeration /// Command enum contains all operations that could be achieved on the zone API -#[derive(Subcommand, Eq, PartialEq, Clone, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, clap::Subcommand)] pub enum Command { #[clap(name = "list", aliases = &["l"], about = "List available zones")] List { @@ -66,7 +65,7 @@ impl Executor for Command { // helpers pub async fn list(config: Arc, output: &Output) -> Result<(), Error> { - let client = Client::from(config.credentials.to_owned()); + let client = Client::from(&config.credentials); let zones = zones::list(&client).await.map_err(Error::List)?; println!( diff --git a/examples/cleverctl/src/logging.rs b/examples/cleverctl/src/logging.rs old mode 100644 new mode 100755 index 133b930..e108999 --- a/examples/cleverctl/src/logging.rs +++ b/examples/cleverctl/src/logging.rs @@ -7,7 +7,7 @@ use tracing::Level; // ----------------------------------------------------------------------------- // Error enumeration -#[derive(thiserror::Error, Debug)] +#[derive(Debug, thiserror::Error)] pub enum Error { #[error("failed to set global default subscriber, {0}")] GlobalDefaultSubscriber(tracing::subscriber::SetGlobalDefaultError), diff --git a/examples/cleverctl/src/main.rs b/examples/cleverctl/src/main.rs old mode 100644 new mode 100755 index 3e0856b..1e249b3 --- a/examples/cleverctl/src/main.rs +++ b/examples/cleverctl/src/main.rs @@ -19,7 +19,7 @@ pub mod logging; // ----------------------------------------------------------------------------- // Error enumeration -#[derive(thiserror::Error, Debug)] +#[derive(Debug, thiserror::Error)] pub enum Error { #[error("failed to load configuration, {0}")] Configuration(cfg::Error), diff --git a/src/lib.rs b/src/lib.rs old mode 100644 new mode 100755 index 8a60552..b817497 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,353 +1,222 @@ -//! # Clever-Cloud Sdk +//! # Clever-Cloud Software Development Kit (SDK) //! -//! This module provides a client and structures to interact with clever-cloud -//! api. +//! This module provides a client and structures to interact with the Clever-Cloud API. -use std::fmt::Debug; +use core::{fmt, str}; +use std::sync::OnceLock; -use serde::{Deserialize, Serialize, de::DeserializeOwned}; - -use crate::oauth10a::{ - Client as OAuthClient, ClientError, Request, RestClient, - reqwest::{self, Method}, +use oauth10a::{ + client::Client as OAuthClient, + credentials::{Credentials, CredentialsBuilder}, + execute::ExecuteRequest, + reqwest::{self, Request, Response, Url}, }; +#[macro_use] +mod logging; + pub mod v2; pub mod v4; -// ----------------------------------------------------------------------------- -// Exports +// TYPE ALIASES //////////////////////////////////////////////////////////////// -pub use oauth10a::client as oauth10a; +pub type ClientError = oauth10a::client::ClientError; -// ----------------------------------------------------------------------------- -// Constants +pub type RestError = oauth10a::rest::RestError; -pub const PUBLIC_ENDPOINT: &str = "https://api.clever-cloud.com"; -pub const PUBLIC_API_BRIDGE_ENDPOINT: &str = "https://api-bridge.clever-cloud.com"; +type UrlParseError = ::Err; -// Consumer key and secret reported here are one from the clever-tools and is -// available publicly. -// the disclosure of these tokens is not considered as a vulnerability. -// Do not report this to our security service. -// -// See: -// - -pub const DEFAULT_CONSUMER_KEY: &str = "T5nFjKeHH4AIlEveuGhB5S3xg8T19e"; -pub fn default_consumer_key() -> String { - DEFAULT_CONSUMER_KEY.to_string() -} +// RE-EXPORTS ////////////////////////////////////////////////////////////////// -pub const DEFAULT_CONSUMER_SECRET: &str = "MgVMqTr6fWlf2M0tkC2MXOnhfqBWDT"; -pub fn default_consumer_secret() -> String { - DEFAULT_CONSUMER_SECRET.to_string() -} +pub use oauth10a; -// ----------------------------------------------------------------------------- -// Credentials structure - -#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)] -#[serde(untagged)] -pub enum Credentials { - OAuth1 { - #[serde(rename = "token")] - token: String, - #[serde(rename = "secret")] - secret: String, - #[serde(rename = "consumer-key", default = "default_consumer_key")] - consumer_key: String, - #[serde(rename = "consumer-secret", default = "default_consumer_secret")] - consumer_secret: String, - }, - Basic { - #[serde(rename = "username")] - username: String, - #[serde(rename = "password")] - password: String, - }, - Bearer { - #[serde(rename = "token")] - token: String, - }, -} +// CLEVER CLOUD API //////////////////////////////////////////////////////////// -impl Default for Credentials { - #[tracing::instrument(skip_all)] - fn default() -> Self { - Self::OAuth1 { - token: String::new(), - secret: String::new(), - consumer_key: DEFAULT_CONSUMER_KEY.to_string(), - consumer_secret: DEFAULT_CONSUMER_SECRET.to_string(), - } - } -} +pub const PUBLIC_API_ENDPOINT: &str = "https://api.clever-cloud.com"; -impl From for Credentials { - #[tracing::instrument(skip_all)] - fn from(credentials: oauth10a::Credentials) -> Self { - match credentials { - oauth10a::Credentials::Bearer { token } => Self::Bearer { token }, - oauth10a::Credentials::Basic { username, password } => { - Self::Basic { username, password } - } - oauth10a::Credentials::OAuth1 { - token, - secret, - consumer_key, - consumer_secret, - } => Self::OAuth1 { - token, - secret, - consumer_key, - consumer_secret, - }, - } - } -} +pub fn public_api_endpoint() -> &'static Url { + static URL: OnceLock = OnceLock::new(); -#[allow(clippy::from_over_into)] -impl Into for Credentials { - #[tracing::instrument(skip_all)] - fn into(self) -> oauth10a::Credentials { - match self { - Self::Bearer { token } => oauth10a::Credentials::Bearer { token }, - Self::Basic { username, password } => { - oauth10a::Credentials::Basic { username, password } - } - Self::OAuth1 { - token, - secret, - consumer_key, - consumer_secret, - } => oauth10a::Credentials::OAuth1 { - token, - secret, - consumer_key, - consumer_secret, - }, - } - } + URL.get_or_init(|| { + PUBLIC_API_ENDPOINT + .parse() + .expect("valid URL for public API endpoint") + }) } -impl Credentials { - #[tracing::instrument(skip_all)] - pub fn bearer(token: String) -> Self { - Self::Bearer { token } - } +pub const PUBLIC_API_BRIDGE_ENDPOINT: &str = "https://api-bridge.clever-cloud.com"; - #[tracing::instrument(skip_all)] - pub fn basic(username: String, password: String) -> Self { - Self::Basic { username, password } - } +pub fn public_api_bridge_endpoint() -> &'static Url { + static URL: OnceLock = OnceLock::new(); - #[tracing::instrument(skip_all)] - pub fn oauth1( - token: String, - secret: String, - consumer_key: String, - consumer_secret: String, - ) -> Self { - Self::OAuth1 { - token, - secret, - consumer_key, - consumer_secret, - } - } + URL.get_or_init(|| { + PUBLIC_API_BRIDGE_ENDPOINT + .parse() + .expect("valid URL for public API bridge endpoint") + }) } -// ----------------------------------------------------------------------------- -// Builder structure +// CLEVER TOOLS //////////////////////////////////////////////////////////////// -#[derive(Clone, Debug, Default)] -pub struct Builder { - endpoint: Option, - credentials: Option, -} +/// Default OAuth1 consumer. +#[derive(Debug)] +pub struct CleverTools; -impl Builder { - #[cfg_attr(feature = "tracing", tracing::instrument)] - pub fn with_endpoint(mut self, endpoint: String) -> Self { - self.endpoint = Some(endpoint); - self - } +impl CleverTools { + // Consumer key and secret of the clever-tools are publicly available. + // The disclosure of these tokens is not considered a vulnerability. + // Do not report this to our security service. + // + // See: + // - - #[cfg_attr(feature = "tracing", tracing::instrument)] - pub fn with_credentials(mut self, credentials: Credentials) -> Self { - self.credentials = Some(credentials); - self - } + pub const CONSUMER_KEY: &'static str = "T5nFjKeHH4AIlEveuGhB5S3xg8T19e"; + pub const CONSUMER_SECRET: &'static str = "MgVMqTr6fWlf2M0tkC2MXOnhfqBWDT"; +} - #[cfg_attr(feature = "tracing", tracing::instrument)] - pub fn build(self, client: reqwest::Client) -> Client { - let endpoint = match self.endpoint { - Some(endpoint) => endpoint, - None => { - if matches!(self.credentials, Some(Credentials::Bearer { .. })) { - PUBLIC_API_BRIDGE_ENDPOINT.to_string() - } else { - PUBLIC_ENDPOINT.to_string() - } - } - }; - - Client { - inner: OAuthClient::new(client, self.credentials.map(Into::into)), - endpoint, - } - } +// ENDPOINT ERROR ////////////////////////////////////////////////////////////// + +#[derive(Debug, thiserror::Error)] +#[error("failed to build request's endpoint URL on API: '{api}', path: '{path}', error: {error}")] +pub struct EndpointError { + api: Url, + path: Box, + error: UrlParseError, } -// ----------------------------------------------------------------------------- -// Client structure +// CLIENT ////////////////////////////////////////////////////////////////////// -#[derive(Clone, Debug)] +/// HTTP client specialized for Clever Cloud API. +/// +/// +/// +/// +#[derive(Debug, Default, Clone)] pub struct Client { inner: OAuthClient, - endpoint: String, + api_endpoint: Option, } -impl Request for Client { - type Error = ClientError; +impl Client { + pub fn new() -> Self { + Self { + inner: OAuthClient::new(), + api_endpoint: None, + } + } - #[cfg_attr(feature = "tracing", tracing::instrument)] - fn request( - &self, - method: &Method, - endpoint: &str, - payload: &T, - ) -> impl Future> - where - T: Serialize + Debug + Send + Sync, - U: DeserializeOwned + Debug + Send + Sync, - { - self.inner.request(method, endpoint, payload) + /// Sets the credentials that will be used by this client to authorize subsequent HTTP requests. + /// + /// When `consumer_keys` and/or `consumer_secret` are missing, the client will + /// use the values of the [`CleverTools`]. + pub fn set_credentials>(&mut self, credentials: Option) { + self.inner.set_credentials(credentials.map(|credentials| { + credentials + .into() + .with_consumer(CleverTools::CONSUMER_SECRET, CleverTools::CONSUMER_SECRET) + })); } - #[cfg_attr(feature = "tracing", tracing::instrument)] - fn execute( - &self, - request: reqwest::Request, - ) -> impl Future> { - self.inner.execute(request) + pub fn with_credentials>(mut self, credentials: Option) -> Self { + self.set_credentials(credentials); + self } -} -impl RestClient for Client { - type Error = ClientError; + pub fn credentials(&self) -> Option> { + self.inner.credentials() + } #[cfg_attr(feature = "tracing", tracing::instrument)] - fn get(&self, endpoint: &str) -> impl Future> - where - T: DeserializeOwned + Debug + Send + Sync, - { - self.inner.get(endpoint) + pub fn set_endpoint(&mut self, api_endpoint: Option) { + self.api_endpoint = api_endpoint; } - #[cfg_attr(feature = "tracing", tracing::instrument)] - fn post( - &self, - endpoint: &str, - payload: &T, - ) -> impl Future> - where - T: Serialize + Debug + Send + Sync, - U: DeserializeOwned + Debug + Send + Sync, - { - self.inner.post(endpoint, payload) + pub fn with_endpoint(mut self, api_endpoint: Option) -> Self { + self.set_endpoint(api_endpoint); + self } - #[cfg_attr(feature = "tracing", tracing::instrument)] - fn put(&self, endpoint: &str, payload: &T) -> impl Future> - where - T: Serialize + Debug + Send + Sync, - U: DeserializeOwned + Debug + Send + Sync, - { - self.inner.put(endpoint, payload) + pub fn api_endpoint(&self) -> &Url { + if let Some(ref url) = self.api_endpoint { + url + } else if let Some(Credentials::Bearer { .. }) = self.credentials() { + public_api_bridge_endpoint() + } else { + public_api_endpoint() + } } + /// Joins `path` to this client's API endpoint. #[cfg_attr(feature = "tracing", tracing::instrument)] - fn patch( + pub(crate) fn endpoint( &self, - endpoint: &str, - payload: &T, - ) -> impl Future> - where - T: Serialize + Debug + Send + Sync, - U: DeserializeOwned + Debug + Send + Sync, - { - self.inner.patch(endpoint, payload) + path: T, + ) -> Result { + let api = self.api_endpoint(); + let path = path.to_string(); + + Url::options() + .base_url(Some(api)) + .parse(&path) + .map_err(|error| EndpointError { + api: api.clone(), + path: path.into(), + error, + }) } - #[cfg_attr(feature = "tracing", tracing::instrument)] - fn delete(&self, endpoint: &str) -> impl Future> { - self.inner.delete(endpoint) + pub fn inner(&self) -> &reqwest::Client { + self.inner.inner() } } impl From for Client { - #[cfg_attr(feature = "tracing", tracing::instrument)] - fn from(client: reqwest::Client) -> Self { - Self::builder().build(client) + fn from(value: reqwest::Client) -> Self { + Self { + inner: oauth10a::client::Client::from(value), + ..Default::default() + } } } -impl From for Client { - #[cfg_attr(feature = "tracing", tracing::instrument)] - fn from(credentials: Credentials) -> Self { - match &credentials { - Credentials::Bearer { .. } => Self::builder() - .with_endpoint(PUBLIC_API_BRIDGE_ENDPOINT.to_string()) - .with_credentials(credentials) - .build(reqwest::Client::new()), - _ => Self::builder() - .with_credentials(credentials) - .build(reqwest::Client::new()), - } +impl From for Client { + fn from(value: CredentialsBuilder) -> Self { + Self::new().with_credentials(Some(value)) } } -impl Default for Client { - #[cfg_attr(feature = "tracing", tracing::instrument)] - fn default() -> Self { - Self::builder().build(reqwest::Client::new()) +impl From<&CredentialsBuilder> for Client { + fn from(value: &CredentialsBuilder) -> Self { + Self::from(value.clone()) } } -impl Client { - #[cfg_attr(feature = "tracing", tracing::instrument)] - pub fn new( - client: reqwest::Client, - endpoint: String, - credentials: Option, - ) -> Self { - let mut builder = Self::builder().with_endpoint(endpoint); - - if let Some(credentials) = credentials { - builder = builder.with_credentials(credentials); +impl>> From> for Client { + fn from(value: Credentials) -> Self { + Self { + inner: oauth10a::client::Client::from(value), + ..Default::default() } - - builder.build(client) - } - - #[cfg_attr(feature = "tracing", tracing::instrument)] - pub fn builder() -> Builder { - Builder::default() } +} - #[cfg_attr(feature = "tracing", tracing::instrument)] - pub fn set_endpoint(&mut self, endpoint: String) { - self.endpoint = endpoint; +impl From<&Credentials> for Client { + fn from(value: &Credentials) -> Self { + Self { + inner: oauth10a::client::Client::from(value), + ..Default::default() + } } +} - #[cfg_attr(feature = "tracing", tracing::instrument)] - pub fn set_credentials(&mut self, credentials: Option) { - self.inner.set_credentials(credentials.map(Into::into)); - } +impl ExecuteRequest for Client { + type Error = ClientError; - #[cfg_attr(feature = "tracing", tracing::instrument)] - pub fn inner(&self) -> &reqwest::Client { - self.inner.inner() + #[inline] + fn execute_request( + &self, + request: Request, + ) -> impl Future> + Send + 'static { + self.inner.execute_request(request) } } diff --git a/src/logging.rs b/src/logging.rs new file mode 100755 index 0000000..7106c22 --- /dev/null +++ b/src/logging.rs @@ -0,0 +1,51 @@ +#![allow(unused_macros, dead_code)] + +macro_rules! trace { + ($($arg:tt)*) => { + #[cfg(feature = "logging")] + if ::log::log_enabled!(log::Level::Trace) { + ::tracing::trace!($($arg)*); + } + }; + () => () +} + +macro_rules! debug { + ($($arg:tt)*) => { + #[cfg(feature = "logging")] + if ::log::log_enabled!(log::Level::Debug) { + ::tracing::debug!($($arg)*); + } + }; + () => () +} + +macro_rules! info { + ($($arg:tt)*) => { + #[cfg(feature = "logging")] + if ::log::log_enabled!(log::Level::Info) { + ::tracing::info!($($arg)*); + } + }; + () => () +} + +macro_rules! warn { + ($($arg:tt)*) => { + #[cfg(feature = "logging")] + if ::log::log_enabled!(log::Level::Warn) { + ::tracing::warn!($($arg)*); + } + }; + () => () +} + +macro_rules! error { + ($($arg:tt)*) => { + #[cfg(feature = "logging")] + if ::log::log_enabled!(log::Level::Error) { + ::tracing::error!($($arg)*); + } + }; + () => () +} diff --git a/src/v2/addon.rs b/src/v2/addon.rs old mode 100644 new mode 100755 index a2dd6e1..3a6147a --- a/src/v2/addon.rs +++ b/src/v2/addon.rs @@ -3,22 +3,23 @@ //! This module expose structures and helpers to interact with the addon api //! version 2 -use std::{collections::BTreeMap, fmt::Debug}; +use std::collections::BTreeMap; -#[cfg(feature = "logging")] -use log::{Level, debug, log_enabled}; -use oauth10a::client::{ClientError, RestClient}; +use oauth10a::rest::RestClient; #[cfg(feature = "jsonschemas")] use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use crate::{Client, v4::addon_provider::config_provider::addon::environment::Variable}; +use crate::{ + Client, EndpointError, RestError, v2::ErrorResponse, + v4::addon_provider::config_provider::addon::environment::Variable, +}; // ----------------------------------------------------------------------------- // Provider structure #[cfg_attr(feature = "jsonschemas", derive(JsonSchema))] -#[derive(Serialize, Deserialize, PartialEq, PartialOrd, Clone, Debug)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Serialize, Deserialize)] pub struct Provider { #[serde(rename = "id")] pub id: String, @@ -56,7 +57,7 @@ pub struct Provider { // Feature structure #[cfg_attr(feature = "jsonschemas", derive(JsonSchema))] -#[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Clone, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Serialize, Deserialize)] pub struct Feature { #[serde(rename = "name")] pub name: String, @@ -74,7 +75,7 @@ pub struct Feature { // Plan structure #[cfg_attr(feature = "jsonschemas", derive(JsonSchema))] -#[derive(Serialize, Deserialize, PartialEq, PartialOrd, Clone, Debug)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Serialize, Deserialize)] pub struct Plan { #[serde(rename = "id")] pub id: String, @@ -96,7 +97,7 @@ pub struct Plan { // Addon structure #[cfg_attr(feature = "jsonschemas", derive(JsonSchema))] -#[derive(Serialize, Deserialize, PartialEq, PartialOrd, Clone, Debug)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Serialize, Deserialize)] pub struct Addon { #[serde(rename = "id")] pub id: String, @@ -120,7 +121,7 @@ pub struct Addon { // Opts enum #[cfg_attr(feature = "jsonschemas", derive(JsonSchema))] -#[derive(Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord, Clone, Debug, Default)] +#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub struct Opts { #[serde(rename = "version", skip_serializing_if = "Option::is_none")] pub version: Option, @@ -134,7 +135,7 @@ pub struct Opts { // CreateOpts structure #[cfg_attr(feature = "jsonschemas", derive(JsonSchema))] -#[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Clone, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Serialize, Deserialize)] pub struct CreateOpts { #[serde(rename = "name")] pub name: String, @@ -151,18 +152,22 @@ pub struct CreateOpts { // ----------------------------------------------------------------------------- // Error enumerations -#[derive(thiserror::Error, Debug)] +#[derive(Debug, thiserror::Error)] pub enum Error { + #[error(transparent)] + Endpoint(#[from] EndpointError), #[error("failed to list addons of organisation '{0}', {1}")] - List(String, ClientError), + List(String, RestError), #[error("failed to get addon '{0}' of organisation '{1}', {2}")] - Get(String, String, ClientError), + Get(String, String, RestError), #[error("failed to get addon '{0}' environment of organisation '{1}', {2}")] - Environment(String, String, ClientError), + Environment(String, String, RestError), #[error("failed to create addon for organisation '{0}', {1}")] - Create(String, ClientError), + Create(String, RestError), #[error("failed to delete addon '{0}' for organisation '{1}', {2}")] - Delete(String, String, ClientError), + Delete(String, String, RestError), + #[error(transparent)] + StatusCode(#[from] ErrorResponse), } // ----------------------------------------------------------------------------- @@ -171,45 +176,38 @@ pub enum Error { #[cfg_attr(feature = "tracing", tracing::instrument)] /// returns the list of addons for the given organisation pub async fn list(client: &Client, organisation_id: &str) -> Result, Error> { - let path = format!( - "{}/v2/organisations/{}/addons", - client.endpoint, organisation_id, - ); + let endpoint = client.endpoint(format_args!("/v2/organisations/{organisation_id}/addons"))?; - #[cfg(feature = "logging")] - if log_enabled!(Level::Debug) { - debug!( - "execute a request to get the list of addons, path: '{}', organisation: '{}'", - &path, organisation_id - ); - } + debug!( + %endpoint, + organisation = organisation_id, + "execute a request to get the list of addons" + ); - client - .get(&path) + Ok(client + .get(endpoint) .await - .map_err(|err| Error::List(organisation_id.to_owned(), err)) + .map_err(|e| Error::List(organisation_id.to_owned(), e))??) } #[cfg_attr(feature = "tracing", tracing::instrument)] /// returns the addon for the given the organisation and identifier -pub async fn get(client: &Client, organisation_id: &str, id: &str) -> Result { - let path = format!( - "{}/v2/organisations/{}/addons/{}", - client.endpoint, organisation_id, id +pub async fn get(client: &Client, organisation_id: &str, addon_id: &str) -> Result { + let endpoint = client.endpoint(format_args!( + "/v2/organisations/{organisation_id}/addons/{addon_id}" + ))?; + + debug!( + %endpoint, + organisation = organisation_id, + addon = addon_id, + "execute a request to get information about an addon", ); - #[cfg(feature = "logging")] - if log_enabled!(Level::Debug) { - debug!( - "execute a request to get information about an addon, path: '{}', organisation: '{}', id: '{}'", - &path, organisation_id, id - ); - } - - client - .get(&path) + Ok(client + .get(endpoint) .await - .map_err(|err| Error::Get(id.to_owned(), organisation_id.to_owned(), err)) + .map_err(|e| Error::Get(addon_id.to_owned(), organisation_id.to_owned(), e))??) } #[cfg_attr(feature = "tracing", tracing::instrument)] @@ -219,50 +217,43 @@ pub async fn create( organisation_id: &str, opts: &CreateOpts, ) -> Result { - let path = format!( - "{}/v2/organisations/{}/addons", - client.endpoint, organisation_id + let endpoint = client.endpoint(format_args!("/v2/organisations/{organisation_id}/addons"))?; + + debug!( + %endpoint, + organisation = organisation_id, + name = opts.name, + region = opts.region, + plan = opts.plan, + provider_id = opts.provider_id, + "execute a request to create an addon", + ); - #[cfg(feature = "logging")] - if log_enabled!(Level::Debug) { - debug!( - "execute a request to create an addon, path: '{}', organisation: '{}', name: '{}', region: '{}', plan: '{}', provider-id: '{}'", - &path, - organisation_id, - &opts.name, - &opts.region, - &opts.plan, - &opts.provider_id.to_string() - ); - } - - client - .post(&path, opts) + Ok(client + .post(endpoint, opts) .await - .map_err(|err| Error::Create(organisation_id.to_owned(), err)) + .map_err(|e| Error::Create(organisation_id.to_owned(), e))??) } #[cfg_attr(feature = "tracing", tracing::instrument)] /// delete the given addon -pub async fn delete(client: &Client, organisation_id: &str, id: &str) -> Result<(), Error> { - let path = format!( - "{}/v2/organisations/{}/addons/{}", - client.endpoint, organisation_id, id +pub async fn delete(client: &Client, organisation_id: &str, addon_id: &str) -> Result<(), Error> { + let endpoint = client.endpoint(format_args!( + "/v2/organisations/{organisation_id}/addons/{addon_id}" + ))?; + + debug!( + %endpoint, + organisation = organisation_id, + addon = addon_id, + "execute a request to delete an addon", ); - #[cfg(feature = "logging")] - if log_enabled!(Level::Debug) { - debug!( - "execute a request to delete an addon, path: '{}', organisation: '{}', id: '{}'", - &path, organisation_id, id - ); - } - - client - .delete(&path) + Ok(client + .delete(endpoint) .await - .map_err(|err| Error::Delete(id.to_owned(), organisation_id.to_owned(), err)) + .map_err(|e| Error::Delete(addon_id.to_owned(), organisation_id.to_owned(), e))??) } #[cfg_attr(feature = "tracing", tracing::instrument)] @@ -270,28 +261,23 @@ pub async fn delete(client: &Client, organisation_id: &str, id: &str) -> Result< pub async fn environment( client: &Client, organisation_id: &str, - id: &str, + addon_id: &str, ) -> Result, Error> { - let path = format!( - "{}/v2/organisations/{}/addons/{}/env", - client.endpoint, organisation_id, id + let endpoint = client.endpoint(format_args!( + "/v2/organisations/{organisation_id}/addons/{addon_id}/env" + ))?; + + debug!( + %endpoint, + organisation = organisation_id, + addon = addon_id, + "execute a request to get secret of a addon" ); - #[cfg(feature = "logging")] - if log_enabled!(Level::Debug) { - debug!( - "execute a request to get secret of a addon, path: '{}', organisation: '{}', id: '{}'", - &path, organisation_id, id - ); - } - let env: Vec = client - .get(&path) + .get(endpoint) .await - .map_err(|err| Error::Environment(id.to_owned(), organisation_id.to_owned(), err))?; + .map_err(|e| Error::Environment(addon_id.to_owned(), organisation_id.to_owned(), e))??; - Ok(env.iter().fold(BTreeMap::new(), |mut acc, var| { - acc.insert(var.name.to_owned(), var.value.to_owned()); - acc - })) + Ok(env.into_iter().map(|var| (var.name, var.value)).collect()) } diff --git a/src/v2/error.rs b/src/v2/error.rs new file mode 100755 index 0000000..a4ec135 --- /dev/null +++ b/src/v2/error.rs @@ -0,0 +1,12 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, thiserror::Error)] +#[error("got response {kind} ({id}), {message}")] +pub struct InvalidResponseBody { + #[serde(rename = "id")] + pub id: u32, + #[serde(rename = "message")] + pub message: String, + #[serde(rename = "type")] + pub kind: String, +} diff --git a/src/v2/mod.rs b/src/v2/mod.rs old mode 100644 new mode 100755 index 74462e6..07f052b --- a/src/v2/mod.rs +++ b/src/v2/mod.rs @@ -2,6 +2,10 @@ //! //! This module expose resources under the version 2 of the Clever-Cloud Api. +pub mod error; + +pub type ErrorResponse = oauth10a::rest::ErrorResponse; + pub mod addon; pub mod myself; pub mod plan; diff --git a/src/v2/myself.rs b/src/v2/myself.rs old mode 100644 new mode 100755 index 6bd5031..a83f586 --- a/src/v2/myself.rs +++ b/src/v2/myself.rs @@ -3,22 +3,18 @@ //! This module provides structures and helpers to interact with the user api //! version 2 -use std::fmt::Debug; - -#[cfg(feature = "logging")] -use log::{Level, debug, log_enabled}; -use oauth10a::client::{ClientError, RestClient}; +use oauth10a::rest::RestClient; #[cfg(feature = "jsonschemas")] use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use crate::Client; +use crate::{Client, EndpointError, RestError, v2::ErrorResponse}; // ----------------------------------------------------------------------------- // Myself structure and helpers #[cfg_attr(feature = "jsonschemas", derive(JsonSchema))] -#[derive(Serialize, PartialEq, Eq, Deserialize, Clone, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Myself { #[serde(rename = "id")] pub id: String, @@ -59,10 +55,14 @@ pub struct Myself { // ----------------------------------------------------------------------------- // Error enumeration -#[derive(thiserror::Error, Debug)] +#[derive(Debug, thiserror::Error)] pub enum Error { + #[error(transparent)] + Endpoint(#[from] EndpointError), #[error("failed to get information about the current user, {0}")] - Get(ClientError), + Get(RestError), + #[error(transparent)] + StatusCode(#[from] ErrorResponse), } // ----------------------------------------------------------------------------- @@ -71,15 +71,12 @@ pub enum Error { #[cfg_attr(feature = "tracing", tracing::instrument)] /// returns information about the person logged in pub async fn get(client: &Client) -> Result { - let path = format!("{}/v2/self", client.endpoint); + let endpoint = client.endpoint("/v2/self")?; - #[cfg(feature = "logging")] - if log_enabled!(Level::Debug) { - debug!( - "execute a request to get information about the logged in user, path: '{}'", - &path - ); - } + debug!( + %endpoint, + "execute a request to get information about the logged in user" + ); - client.get(&path).await.map_err(Error::Get) + Ok(client.get(endpoint).await.map_err(Error::Get)??) } diff --git a/src/v2/plan.rs b/src/v2/plan.rs old mode 100644 new mode 100755 index e7a7308..0f0be3f --- a/src/v2/plan.rs +++ b/src/v2/plan.rs @@ -3,13 +3,14 @@ //! This module provides helpers and structures to interact with the plan api of //! the addon providers -#[cfg(feature = "logging")] -use log::{Level, debug, log_enabled}; -use oauth10a::client::{ClientError, RestClient}; +use oauth10a::rest::RestClient; use crate::{ - Client, - v2::addon::{Plan, Provider}, + Client, EndpointError, RestError, + v2::{ + ErrorResponse, + addon::{Plan, Provider}, + }, v4::addon_provider::AddonProviderId, }; @@ -22,14 +23,18 @@ pub const CONFIG_PROVIDER: &str = "plan_5d8e9596-dd73-4b73-84d9-e165372c5324"; // ----------------------------------------------------------------------------- // Error enumeration -#[derive(thiserror::Error, Debug)] +#[derive(Debug, thiserror::Error)] pub enum Error { + #[error(transparent)] + Endpoint(#[from] EndpointError), #[error("failed to fetch list of addon providers, {0}")] - List(ClientError), + List(RestError), #[error("failed to fetch details of addon provider '{0}'")] Get(AddonProviderId), #[error("failed to find plan '{0}' for addon provider '{1}' amongst available options: {2}")] Plan(String, AddonProviderId, String), + #[error(transparent)] + StatusCode(#[from] ErrorResponse), } // ----------------------------------------------------------------------------- @@ -38,14 +43,11 @@ pub enum Error { #[cfg_attr(feature = "tracing", tracing::instrument)] /// Returns the list of details relative to the addon providers. pub async fn list(client: &Client) -> Result, Error> { - let path = format!("{}/v2/products/addonproviders", client.endpoint); + let endpoint = client.endpoint("/v2/products/addonproviders")?; - #[cfg(feature = "logging")] - if log_enabled!(Level::Debug) { - debug!("execute a request to list plans of the addon-provider, path: '{path}'"); - } + debug!(%endpoint, "execute a request to list plans of the addon-provider"); - client.get(&path).await.map_err(Error::List) + Ok(client.get(endpoint).await.map_err(Error::List)??) } #[cfg_attr(feature = "tracing", tracing::instrument)] diff --git a/src/v4/addon_provider/config_provider/addon/environment.rs b/src/v4/addon_provider/config_provider/addon/environment.rs old mode 100644 new mode 100755 index 8984997..6602275 --- a/src/v4/addon_provider/config_provider/addon/environment.rs +++ b/src/v4/addon_provider/config_provider/addon/environment.rs @@ -3,22 +3,23 @@ //! This module provide helpers and structures to interact with the config //! provider addon's environment -use std::{collections::HashMap, fmt::Debug}; +use std::collections::HashMap; -#[cfg(feature = "logging")] -use log::{Level, debug, log_enabled}; -use oauth10a::client::{ClientError, RestClient}; +use oauth10a::rest::RestClient; #[cfg(feature = "jsonschemas")] use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use crate::{Client, v4::addon_provider::AddonProviderId}; +use crate::{ + Client, EndpointError, RestError, + v4::{ErrorResponse, addon_provider::AddonProviderId}, +}; // ----------------------------------------------------------------------------- // Variable structure #[cfg_attr(feature = "jsonschemas", derive(JsonSchema))] -#[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub struct Variable { #[serde(rename = "name")] pub name: String, @@ -43,12 +44,16 @@ impl Variable { // ----------------------------------------------------------------------------- // Error enumeration -#[derive(thiserror::Error, Debug)] +#[derive(Debug, thiserror::Error)] pub enum Error { + #[error(transparent)] + Endpoint(#[from] EndpointError), #[error("failed to get variables of config-provider addon '{0}', {1}")] - Get(String, ClientError), + Get(String, RestError), #[error("failed to update variables of config-provider addon '{0}', {1}")] - Put(String, ClientError), + Put(String, RestError), + #[error(transparent)] + StatusCode(#[from] ErrorResponse), } // ----------------------------------------------------------------------------- @@ -57,25 +62,20 @@ pub enum Error { /// Retrieve environment variables of the config provider addon #[cfg_attr(feature = "tracing", tracing::instrument)] pub async fn get(client: &Client, id: &str) -> Result, Error> { - let path = format!( - "{}/v4/addon-providers/{}/addons/{}/env", - client.endpoint, - AddonProviderId::ConfigProvider, - id + let endpoint = client.endpoint(format_args!( + "/v4/addon-providers/{}/addons/{id}/env", + AddonProviderId::ConfigProvider + ))?; + + debug!( + %endpoint, %id, + "execute a request to get information about the config-provider addon" ); - #[cfg(feature = "logging")] - if log_enabled!(Level::Debug) { - debug!( - "execute a request to get information about the config-provider addon, path: '{}', id: '{}'", - &path, id - ); - } - - client - .get(&path) + Ok(client + .get(endpoint) .await - .map_err(|err| Error::Get(id.to_string(), err)) + .map_err(|e| Error::Get(id.to_string(), e))??) } /// Update environment variables of the config provider addon @@ -85,25 +85,21 @@ pub async fn put( id: &str, variables: &Vec, ) -> Result, Error> { - let path = format!( - "{}/v4/addon-providers/{}/addons/{}/env", - client.endpoint, - AddonProviderId::ConfigProvider, - id + let endpoint = client.endpoint(format_args!( + "/v4/addon-providers/{}/addons/{id}/env", + AddonProviderId::ConfigProvider + ))?; + + debug!( + %endpoint, + %id, + "execute a request to update information about the config-provider addon", ); - #[cfg(feature = "logging")] - if log_enabled!(Level::Debug) { - debug!( - "execute a request to update information about the config-provider addon, path: '{}', id: '{}'", - &path, id - ); - } - - client - .put(&path, variables) + Ok(client + .put(endpoint, variables) .await - .map_err(|err| Error::Put(id.to_string(), err)) + .map_err(|e| Error::Put(id.to_string(), e))??) } /// Insert a new environment variable into config provider diff --git a/src/v4/addon_provider/elasticsearch.rs b/src/v4/addon_provider/elasticsearch.rs old mode 100644 new mode 100755 index 656fee0..5d77a7b --- a/src/v4/addon_provider/elasticsearch.rs +++ b/src/v4/addon_provider/elasticsearch.rs @@ -4,40 +4,41 @@ //! addon provider #![allow(deprecated)] -use std::{ - convert::TryFrom, - fmt::{self, Debug, Display, Formatter}, - str::FromStr, -}; +use core::{fmt, str::FromStr}; -#[cfg(feature = "logging")] -use log::{Level, debug, log_enabled}; -use oauth10a::client::{ClientError, RestClient}; +use oauth10a::rest::RestClient; #[cfg(feature = "jsonschemas")] use schemars::JsonSchema_repr as JsonSchemaRepr; use serde_repr::{Deserialize_repr as DeserializeRepr, Serialize_repr as SerializeRepr}; use crate::{ - Client, - v4::addon_provider::{AddonProvider, AddonProviderId}, + Client, EndpointError, RestError, + v4::{ + ErrorResponse, + addon_provider::{AddonProvider, AddonProviderId}, + }, }; // ----------------------------------------------------------------------------- // Error enumeration -#[derive(thiserror::Error, Debug)] +#[derive(Debug, thiserror::Error)] pub enum Error { + #[error(transparent)] + Endpoint(#[from] EndpointError), #[error("failed to parse version from '{0}', available version are 7 and 8")] ParseVersion(String), #[error("failed to get information about addon provider '{0}', {1}")] - Get(AddonProviderId, ClientError), + Get(AddonProviderId, RestError), + #[error(transparent)] + StatusCode(#[from] ErrorResponse), } // ----------------------------------------------------------------------------- // Version enum #[cfg_attr(feature = "jsonschemas", derive(JsonSchemaRepr))] -#[derive(SerializeRepr, DeserializeRepr, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, SerializeRepr, DeserializeRepr)] #[serde(untagged)] #[repr(i32)] pub enum Version { @@ -74,8 +75,8 @@ impl Into for Version { } } -impl Display for Version { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { +impl fmt::Display for Version { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Self::V7 => write!(f, "7"), Self::V8 => write!(f, "8"), @@ -89,23 +90,18 @@ impl Display for Version { /// returns information about the elasticsearch addon provider #[cfg_attr(feature = "tracing", tracing::instrument)] pub async fn get(client: &Client) -> Result, Error> { - let path = format!( - "{}/v4/addon-providers/{}", - client.endpoint, - AddonProviderId::ElasticSearch - ); + const ADDON_PROVIDER_ID: AddonProviderId = AddonProviderId::ElasticSearch; - #[cfg(feature = "logging")] - if log_enabled!(Level::Debug) { - debug!( - "execute a request to get information about the elasticsearch addon-provider, path: '{}', name: '{}'", - &path, - AddonProviderId::ElasticSearch - ); - } + let endpoint = client.endpoint(format_args!("/v4/addon-providers/{ADDON_PROVIDER_ID}"))?; + + debug!( + %endpoint, + addon_provider = %ADDON_PROVIDER_ID, + "execute a request to get information about the elasticsearch addon-provider" + ); - client - .get(&path) + Ok(client + .get(endpoint) .await - .map_err(|err| Error::Get(AddonProviderId::ElasticSearch, err)) + .map_err(|e| Error::Get(ADDON_PROVIDER_ID, e))??) } diff --git a/src/v4/addon_provider/mod.rs b/src/v4/addon_provider/mod.rs old mode 100644 new mode 100755 index 2231dd9..02b7c80 --- a/src/v4/addon_provider/mod.rs +++ b/src/v4/addon_provider/mod.rs @@ -3,13 +3,7 @@ //! This module provide structures and helpers to interact with clever-cloud's //! addon-provider -use std::{ - collections::BTreeMap, - convert::TryFrom, - fmt::{self, Debug, Display, Formatter}, - hash::Hash, - str::FromStr, -}; +use std::{collections::BTreeMap, fmt, hash::Hash, str::FromStr}; #[cfg(feature = "jsonschemas")] use schemars::JsonSchema; @@ -26,7 +20,7 @@ pub mod redis; // Feature structure #[cfg_attr(feature = "jsonschemas", derive(JsonSchema))] -#[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Clone, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Serialize, Deserialize)] pub struct Feature { #[serde(rename = "name")] pub name: String, @@ -38,7 +32,7 @@ pub struct Feature { // Cluster structure #[cfg_attr(feature = "jsonschemas", derive(JsonSchema))] -#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Cluster { #[serde(rename = "id")] pub id: String, @@ -56,7 +50,7 @@ pub struct Cluster { // AddonProvider structure #[cfg_attr(feature = "jsonschemas", derive(JsonSchema))] -#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct AddonProvider where T: Ord, @@ -74,7 +68,7 @@ where // ----------------------------------------------------------------------------- // Error enumeration -#[derive(thiserror::Error, Debug)] +#[derive(Debug, thiserror::Error)] pub enum Error { #[error( "failed to parse addon provider identifier '{0}', available options are \ @@ -89,7 +83,7 @@ pub enum Error { // AddonProviderName structure #[cfg_attr(feature = "jsonschemas", derive(JsonSchema))] -#[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] #[serde(untagged, try_from = "String", into = "String")] pub enum AddonProviderId { PostgreSql, @@ -171,8 +165,8 @@ impl Into for AddonProviderId { } } -impl Display for AddonProviderId { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { +impl fmt::Display for AddonProviderId { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(self.as_str(), f) } } diff --git a/src/v4/addon_provider/mongodb.rs b/src/v4/addon_provider/mongodb.rs old mode 100644 new mode 100755 index a6eb88b..618f75f --- a/src/v4/addon_provider/mongodb.rs +++ b/src/v4/addon_provider/mongodb.rs @@ -2,41 +2,43 @@ //! //! This module provides helpers and structures to interact with the mongodb //! addon provider +#![allow(deprecated)] -use std::{ - convert::TryFrom, - fmt::{self, Debug, Display, Formatter}, - str::FromStr, -}; +use core::{fmt, str::FromStr}; -#[cfg(feature = "logging")] -use log::{Level, debug, log_enabled}; -use oauth10a::client::{ClientError, RestClient}; +use oauth10a::rest::RestClient; #[cfg(feature = "jsonschemas")] use schemars::JsonSchema_repr as JsonSchemaRepr; use serde_repr::{Deserialize_repr as DeserializeRepr, Serialize_repr as SerializeRepr}; use crate::{ - Client, - v4::addon_provider::{AddonProvider, AddonProviderId}, + Client, EndpointError, RestError, + v4::{ + ErrorResponse, + addon_provider::{AddonProvider, AddonProviderId}, + }, }; // ----------------------------------------------------------------------------- // Error enumeration -#[derive(thiserror::Error, Debug)] +#[derive(Debug, thiserror::Error)] pub enum Error { + #[error(transparent)] + Endpoint(#[from] EndpointError), #[error("failed to parse version from '{0}', available version is 4.0.3")] ParseVersion(String), #[error("failed to get information about addon provider '{0}', {1}")] - Get(AddonProviderId, ClientError), + Get(AddonProviderId, RestError), + #[error(transparent)] + StatusCode(#[from] ErrorResponse), } // ----------------------------------------------------------------------------- // Version enum #[cfg_attr(feature = "jsonschemas", derive(JsonSchemaRepr))] -#[derive(SerializeRepr, DeserializeRepr, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, SerializeRepr, DeserializeRepr)] #[serde(untagged)] #[repr(i32)] pub enum Version { @@ -71,8 +73,8 @@ impl Into for Version { } } -impl Display for Version { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { +impl fmt::Display for Version { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::V4dot0dot3 => write!(f, "4.0.3"), } @@ -85,23 +87,18 @@ impl Display for Version { /// returns information about the mongodb addon provider #[cfg_attr(feature = "tracing", tracing::instrument)] pub async fn get(client: &Client) -> Result, Error> { - let path = format!( - "{}/v4/addon-providers/{}", - client.endpoint, - AddonProviderId::MongoDb - ); + const ADDON_PROVIDER_ID: AddonProviderId = AddonProviderId::MongoDb; - #[cfg(feature = "logging")] - if log_enabled!(Level::Debug) { - debug!( - "execute a request to get information about the mongodb addon-provider, path: '{}', name: '{}'", - &path, - AddonProviderId::MongoDb - ); - } + let endpoint = client.endpoint(format_args!("/v4/addon-providers/{ADDON_PROVIDER_ID}"))?; + + debug!( + %endpoint, + addon_provider = %ADDON_PROVIDER_ID, + "execute a request to get information about the mongodb addon-provider" + ); - client - .get(&path) + Ok(client + .get(endpoint) .await - .map_err(|err| Error::Get(AddonProviderId::MongoDb, err)) + .map_err(|e| Error::Get(ADDON_PROVIDER_ID, e))??) } diff --git a/src/v4/addon_provider/mysql.rs b/src/v4/addon_provider/mysql.rs old mode 100644 new mode 100755 index 9460b94..6d7e5c8 --- a/src/v4/addon_provider/mysql.rs +++ b/src/v4/addon_provider/mysql.rs @@ -2,41 +2,43 @@ //! //! This module provides helpers and structures to interact with the mysql //! addon provider +#![allow(deprecated)] -use std::{ - convert::TryFrom, - fmt::{self, Debug, Display, Formatter}, - str::FromStr, -}; +use core::{fmt, str::FromStr}; -#[cfg(feature = "logging")] -use log::{Level, debug, log_enabled}; -use oauth10a::client::{ClientError, RestClient}; +use oauth10a::rest::RestClient; #[cfg(feature = "jsonschemas")] use schemars::JsonSchema_repr as JsonSchemaRepr; use serde_repr::{Deserialize_repr as DeserializeRepr, Serialize_repr as SerializeRepr}; use crate::{ - Client, - v4::addon_provider::{AddonProvider, AddonProviderId}, + Client, EndpointError, RestError, + v4::{ + ErrorResponse, + addon_provider::{AddonProvider, AddonProviderId}, + }, }; // ----------------------------------------------------------------------------- // Error enumeration -#[derive(thiserror::Error, Debug)] +#[derive(Debug, thiserror::Error)] pub enum Error { + #[error(transparent)] + Endpoint(#[from] EndpointError), #[error("failed to parse version from '{0}', available versions are 5.7 and 8.0")] ParseVersion(String), #[error("failed to get information about addon provider '{0}', {1}")] - Get(AddonProviderId, ClientError), + Get(AddonProviderId, RestError), + #[error(transparent)] + StatusCode(#[from] ErrorResponse), } // ----------------------------------------------------------------------------- // Version enum #[cfg_attr(feature = "jsonschemas", derive(JsonSchemaRepr))] -#[derive(SerializeRepr, DeserializeRepr, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, SerializeRepr, DeserializeRepr)] #[serde(untagged)] #[repr(i32)] pub enum Version { @@ -75,8 +77,8 @@ impl Into for Version { } } -impl Display for Version { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { +impl fmt::Display for Version { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::V5dot7 => write!(f, "5.7"), Self::V8dot0 => write!(f, "8.0"), @@ -91,23 +93,18 @@ impl Display for Version { /// returns information about the mysql addon provider #[cfg_attr(feature = "tracing", tracing::instrument)] pub async fn get(client: &Client) -> Result, Error> { - let path = format!( - "{}/v4/addon-providers/{}", - client.endpoint, - AddonProviderId::MySql - ); + const ADDON_PROVIDER_ID: AddonProviderId = AddonProviderId::MySql; - #[cfg(feature = "logging")] - if log_enabled!(Level::Debug) { - debug!( - "execute a request to get information about the mysql addon-provider, path: '{}', name: '{}'", - &path, - AddonProviderId::MySql - ); - } + let endpoint = client.endpoint(format_args!("/v4/addon-providers/{ADDON_PROVIDER_ID}"))?; + + debug!( + %endpoint, + name = %ADDON_PROVIDER_ID, + "execute a request to get information about the mysql addon-provider" + ); - client - .get(&path) + Ok(client + .get(endpoint) .await - .map_err(|err| Error::Get(AddonProviderId::MySql, err)) + .map_err(|e| Error::Get(ADDON_PROVIDER_ID, e))??) } diff --git a/src/v4/addon_provider/postgresql.rs b/src/v4/addon_provider/postgresql.rs old mode 100644 new mode 100755 index 43d6e5c..09f222d --- a/src/v4/addon_provider/postgresql.rs +++ b/src/v4/addon_provider/postgresql.rs @@ -4,42 +4,43 @@ //! addon provider #![allow(deprecated)] -use std::{ - convert::TryFrom, - fmt::{self, Debug, Display, Formatter}, - str::FromStr, -}; +use core::{fmt, str::FromStr}; -#[cfg(feature = "logging")] -use log::{Level, debug, log_enabled}; -use oauth10a::client::{ClientError, RestClient}; +use oauth10a::rest::RestClient; #[cfg(feature = "jsonschemas")] use schemars::JsonSchema_repr as JsonSchemaRepr; use serde_repr::{Deserialize_repr as DeserializeRepr, Serialize_repr as SerializeRepr}; use crate::{ - Client, - v4::addon_provider::{AddonProvider, AddonProviderId}, + Client, EndpointError, RestError, + v4::{ + ErrorResponse, + addon_provider::{AddonProvider, AddonProviderId}, + }, }; // ----------------------------------------------------------------------------- // Error enumeration -#[derive(thiserror::Error, Debug)] +#[derive(Debug, thiserror::Error)] pub enum Error { + #[error(transparent)] + Endpoint(#[from] EndpointError), #[error( "failed to parse version from '{0}', available versions are 17, 16, 15, 14, 13, 12 and 11" )] ParseVersion(String), #[error("failed to get information about addon provider '{0}', {1}")] - Get(AddonProviderId, ClientError), + Get(AddonProviderId, RestError), + #[error(transparent)] + StatusCode(#[from] ErrorResponse), } // ----------------------------------------------------------------------------- // Version enum #[cfg_attr(feature = "jsonschemas", derive(JsonSchemaRepr))] -#[derive(SerializeRepr, DeserializeRepr, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, SerializeRepr, DeserializeRepr)] #[serde(untagged)] #[repr(i32)] pub enum Version { @@ -64,9 +65,7 @@ impl FromStr for Version { "13" => Self::V13, "12" => Self::V12, "11" => Self::V11, - _ => { - return Err(Error::ParseVersion(s.to_owned())); - } + _ => return Err(Error::ParseVersion(s.to_owned())), }) } } @@ -86,8 +85,8 @@ impl Into for Version { } } -impl Display for Version { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { +impl fmt::Display for Version { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Self::V17 => write!(f, "17"), Self::V16 => write!(f, "16"), @@ -103,26 +102,21 @@ impl Display for Version { // ----------------------------------------------------------------------------- // Helpers functions +/// Returns information about the postgresql addon provider. #[cfg_attr(feature = "tracing", tracing::instrument)] -/// returns information about the postgresql addon provider pub async fn get(client: &Client) -> Result, Error> { - let path = format!( - "{}/v4/addon-providers/{}", - client.endpoint, - AddonProviderId::PostgreSql - ); + const ADDON_PROVIDER_ID: AddonProviderId = AddonProviderId::PostgreSql; - #[cfg(feature = "logging")] - if log_enabled!(Level::Debug) { - debug!( - "execute a request to get information about the postgresql addon-provider, path: '{}', name: '{}'", - &path, - AddonProviderId::PostgreSql - ); - } + let endpoint = client.endpoint(format_args!("/v4/addon-providers/{ADDON_PROVIDER_ID}"))?; + + debug!( + %endpoint, + addon_provider = %ADDON_PROVIDER_ID, + "execute a request to get information about the postgresql addon-provider" + ); - client - .get(&path) + Ok(client + .get(endpoint) .await - .map_err(|err| Error::Get(AddonProviderId::PostgreSql, err)) + .map_err(|e| Error::Get(ADDON_PROVIDER_ID, e))??) } diff --git a/src/v4/addon_provider/redis.rs b/src/v4/addon_provider/redis.rs old mode 100644 new mode 100755 index 9558e04..6656959 --- a/src/v4/addon_provider/redis.rs +++ b/src/v4/addon_provider/redis.rs @@ -4,40 +4,41 @@ //! addon provider #![allow(deprecated)] -use std::{ - convert::TryFrom, - fmt::{self, Debug, Display, Formatter}, - str::FromStr, -}; +use core::{fmt, str::FromStr}; -#[cfg(feature = "logging")] -use log::{Level, debug, log_enabled}; -use oauth10a::client::{ClientError, RestClient}; +use oauth10a::rest::RestClient; #[cfg(feature = "jsonschemas")] use schemars::JsonSchema_repr as JsonSchemaRepr; use serde_repr::{Deserialize_repr as DeserializeRepr, Serialize_repr as SerializeRepr}; use crate::{ - Client, - v4::addon_provider::{AddonProvider, AddonProviderId}, + Client, EndpointError, RestError, + v4::{ + ErrorResponse, + addon_provider::{AddonProvider, AddonProviderId}, + }, }; // ----------------------------------------------------------------------------- // Error enumeration -#[derive(thiserror::Error, Debug)] +#[derive(Debug, thiserror::Error)] pub enum Error { + #[error(transparent)] + Endpoint(#[from] EndpointError), #[error("failed to parse version from {0}, available version is 7.2.4")] ParseVersion(String), #[error("failed to get information about addon provider '{0}', {1}")] - Get(AddonProviderId, ClientError), + Get(AddonProviderId, RestError), + #[error(transparent)] + StatusCode(#[from] ErrorResponse), } // ----------------------------------------------------------------------------- // Version enum #[cfg_attr(feature = "jsonschemas", derive(JsonSchemaRepr))] -#[derive(SerializeRepr, DeserializeRepr, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, SerializeRepr, DeserializeRepr)] #[serde(untagged)] #[repr(i32)] pub enum Version { @@ -72,8 +73,8 @@ impl Into for Version { } } -impl Display for Version { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { +impl fmt::Display for Version { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::V7dot2dot4 => write!(f, "7.2.4"), } @@ -86,23 +87,18 @@ impl Display for Version { #[cfg_attr(feature = "tracing", tracing::instrument)] /// returns information about the redis addon provider pub async fn get(client: &Client) -> Result, Error> { - let path = format!( - "{}/v4/addon-providers/{}", - client.endpoint, - AddonProviderId::Redis - ); + const ADDON_PROVIDER_ID: AddonProviderId = AddonProviderId::Redis; - #[cfg(feature = "logging")] - if log_enabled!(Level::Debug) { - debug!( - "execute a request to get information about the redis addon-provider, path: '{}', name: '{}'", - &path, - AddonProviderId::Redis - ); - } + let endpoint = client.endpoint(format_args!("/v4/addon-providers/{ADDON_PROVIDER_ID}",))?; + + debug!( + %endpoint, + name = %ADDON_PROVIDER_ID, + "execute a request to get information about the redis addon-provider" + ); - client - .get(&path) + Ok(client + .get(endpoint) .await - .map_err(|err| Error::Get(AddonProviderId::Redis, err)) + .map_err(|e| Error::Get(ADDON_PROVIDER_ID, e))??) } diff --git a/src/v4/error.rs b/src/v4/error.rs new file mode 100755 index 0000000..cce7374 --- /dev/null +++ b/src/v4/error.rs @@ -0,0 +1,155 @@ +use serde::{Deserialize, Serialize}; + +mod http_error_context { + use std::collections::HashMap; + + use serde::{Deserialize, Serialize}; + + #[derive(Debug, Serialize, Deserialize)] + pub struct Empty; + + #[derive(Debug, Serialize, Deserialize)] + pub struct FieldError { + #[serde(rename = "value")] + value: String, + #[serde(rename = "reason")] + reason: Option, + } + + #[derive(Debug, Serialize, Deserialize)] + pub struct Field { + #[serde(rename = "name")] + name: String, + #[serde(rename = "error")] + error: FieldError, + } + + #[derive(Debug, Serialize, Deserialize)] + pub struct GatewayError { + #[serde(rename = "originalStatus")] + original_status: i32, + #[serde(rename = "originalBody")] + original_body: String, + } + + #[derive(Debug, Serialize, Deserialize)] + pub struct Input { + #[serde(rename = "names")] + names: Vec, + } + + type MapFieldError = HashMap; + + #[derive(Debug, Serialize, Deserialize)] + pub struct MultipleFields { + #[serde(rename = "fields")] + fields: MapFieldError, + } + + #[derive(Debug, Serialize, Deserialize)] + pub struct Resource { + #[serde(rename = "kind")] + kind: String, + #[serde(rename = "name")] + name: String, + } + + #[derive(Debug, Serialize, Deserialize)] + pub struct Operation { + #[serde(rename = "operation")] + operation: String, + #[serde(rename = "kind")] + kind: String, + #[serde(rename = "name")] + name: Option, + } + + #[derive(Debug, Serialize, Deserialize)] + pub struct Selector { + #[serde(rename = "path")] + path: Vec, + } +} + +#[derive(Debug, Serialize)] +#[serde(untagged)] +pub enum HttpErrorContext { + Empty(http_error_context::Empty), + Field(http_error_context::Field), + Gateway(http_error_context::GatewayError), + Input(http_error_context::Input), + MultipleFields(http_error_context::MultipleFields), + Operation(http_error_context::Operation), + Resource(http_error_context::Resource), + Selector(http_error_context::Selector), +} + +impl<'de> Deserialize<'de> for HttpErrorContext { + fn deserialize>(deserializer: D) -> Result { + #[derive(Debug, Serialize, Deserialize)] + #[serde(tag = "type")] + pub enum TaggedHttpErrorContext { + Empty(http_error_context::Empty), + Field(http_error_context::Field), + Gateway(http_error_context::GatewayError), + Input(http_error_context::Input), + MultipleFields(http_error_context::MultipleFields), + Operation(http_error_context::Operation), + Resource(http_error_context::Resource), + Selector(http_error_context::Selector), + } + + #[derive(Debug, Serialize, Deserialize)] + #[serde(untagged)] + pub enum UntaggedHttpErrorContext { + Empty(http_error_context::Empty), + Field(http_error_context::Field), + Gateway(http_error_context::GatewayError), + Input(http_error_context::Input), + MultipleFields(http_error_context::MultipleFields), + Operation(http_error_context::Operation), + Resource(http_error_context::Resource), + Selector(http_error_context::Selector), + } + + #[derive(Deserialize)] + #[serde(untagged)] + pub enum MaybeTagged { + Untagged(UntaggedHttpErrorContext), + Tag(TaggedHttpErrorContext), + } + + Ok(match MaybeTagged::deserialize(deserializer)? { + MaybeTagged::Untagged(UntaggedHttpErrorContext::Empty(x)) + | MaybeTagged::Tag(TaggedHttpErrorContext::Empty(x)) => Self::Empty(x), + MaybeTagged::Untagged(UntaggedHttpErrorContext::Field(x)) + | MaybeTagged::Tag(TaggedHttpErrorContext::Field(x)) => Self::Field(x), + MaybeTagged::Untagged(UntaggedHttpErrorContext::Gateway(x)) + | MaybeTagged::Tag(TaggedHttpErrorContext::Gateway(x)) => Self::Gateway(x), + MaybeTagged::Untagged(UntaggedHttpErrorContext::Input(x)) + | MaybeTagged::Tag(TaggedHttpErrorContext::Input(x)) => Self::Input(x), + MaybeTagged::Untagged(UntaggedHttpErrorContext::MultipleFields(x)) + | MaybeTagged::Tag(TaggedHttpErrorContext::MultipleFields(x)) => { + Self::MultipleFields(x) + } + MaybeTagged::Untagged(UntaggedHttpErrorContext::Operation(x)) + | MaybeTagged::Tag(TaggedHttpErrorContext::Operation(x)) => Self::Operation(x), + MaybeTagged::Untagged(UntaggedHttpErrorContext::Resource(x)) + | MaybeTagged::Tag(TaggedHttpErrorContext::Resource(x)) => Self::Resource(x), + MaybeTagged::Untagged(UntaggedHttpErrorContext::Selector(x)) + | MaybeTagged::Tag(TaggedHttpErrorContext::Selector(x)) => Self::Selector(x), + }) + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct HttpError { + #[serde(rename = "apiRequestId")] + pub api_request_id: String, + #[serde(rename = "code")] + pub code: String, + #[serde(rename = "context", flatten)] + pub context: HttpErrorContext, + #[serde(rename = "error")] + pub error: String, +} diff --git a/src/v4/functions/deployments.rs b/src/v4/functions/deployments.rs old mode 100644 new mode 100755 index 173fe02..55954db --- a/src/v4/functions/deployments.rs +++ b/src/v4/functions/deployments.rs @@ -2,35 +2,34 @@ //! //! This module provides structures to interact with functions' deployments. -use std::{ - fmt::{self, Debug, Display, Formatter}, - str::FromStr, -}; +use core::{fmt, str::FromStr}; use chrono::{DateTime, Utc}; -use log::{Level, debug, log_enabled}; -use oauth10a::client::{ - ClientError, Request, RestClient, +use oauth10a::{ + client::ClientError, + execute::ExecuteRequest, reqwest::{ - self, Body, Method, - header::{CONTENT_LENGTH, CONTENT_TYPE, HeaderValue}, + self, Body, IntoUrl, Method, + header::{self, HeaderValue}, }, - url, + rest::RestClient, }; use serde::{Deserialize, Serialize}; -use crate::Client; +use crate::{Client, EndpointError, RestError, v4::ErrorResponse}; // ----------------------------------------------------------------------------- // Constants -pub const MIME_APPLICATION_WASM: &str = "application/wasm"; +pub const MIME_APPLICATION_WASM: HeaderValue = HeaderValue::from_static("application/wasm"); // ---------------------------------------------------------------------------- // Error -#[derive(thiserror::Error, Debug)] +#[derive(Debug, thiserror::Error)] pub enum Error { + #[error(transparent)] + Endpoint(#[from] EndpointError), #[error( "failed to parse the webassembly platform '{0}', available values are 'rust', 'javascript' ('js'), 'tiny_go' ('go') and 'assemblyscript'" )] @@ -39,30 +38,28 @@ pub enum Error { "failed to parse the status '{0}', available values are 'waiting_for_upload', 'deploying', 'packaging', 'ready' and 'error'" )] ParseStatus(String), - #[error("failed to parse endpoint '{0}', {1}")] - ParseUrl(String, url::ParseError), #[error("failed to list deployments for function '{0}' of organisation '{1}', {2}")] - List(String, String, ClientError), + List(String, String, RestError), #[error("failed to create deployment for function '{0}' on organisation '{1}', {2}")] - Create(String, String, ClientError), + Create(String, String, RestError), #[error("failed to get deployment '{0}' of function '{1}' on organisation '{2}', {3}")] - Get(String, String, String, ClientError), + Get(String, String, String, RestError), #[error("failed to trigger deployment '{0}' of function '{1}' on organisation '{2}', {3}")] - Trigger(String, String, String, ClientError), + Trigger(String, String, String, RestError), #[error("failed to delete deployment '{0}' of function '{1}' on organisation '{2}', {3}")] - Delete(String, String, String, ClientError), + Delete(String, String, String, RestError), #[error("failed to create request, {0}")] - Request(reqwest::Error), + Request(#[from] RestError), #[error("failed to execute request, {0}")] - Execute(ClientError), - #[error("failed to execute request, got status code {0}")] - StatusCode(u16), + Execute(#[from] ClientError), + #[error(transparent)] + StatusCode(#[from] ErrorResponse), } // ---------------------------------------------------------------------------- // Platform -#[derive(Serialize, Deserialize, Hash, Ord, PartialOrd, Eq, PartialEq, Clone, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] pub enum Platform { #[serde(rename = "RUST")] Rust, @@ -74,8 +71,8 @@ pub enum Platform { JavaScript, } -impl Display for Platform { - fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { +impl fmt::Display for Platform { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { match self { Self::Rust => write!(f, "RUST"), Self::AssemblyScript => write!(f, "ASSEMBLY_SCRIPT"), @@ -102,7 +99,7 @@ impl FromStr for Platform { // ---------------------------------------------------------------------------- // Status -#[derive(Serialize, Deserialize, Hash, Ord, PartialOrd, Eq, PartialEq, Clone, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] pub enum Status { #[serde(rename = "WAITING_FOR_UPLOAD")] WaitingForUpload, @@ -116,8 +113,8 @@ pub enum Status { Error, } -impl Display for Status { - fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { +impl fmt::Display for Status { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { match self { Self::WaitingForUpload => write!(f, "WAITING_FOR_UPLOAD"), Self::Packaging => write!(f, "PACKAGING"), @@ -146,7 +143,7 @@ impl FromStr for Status { // ---------------------------------------------------------------------------- // Opts -#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Opts { #[serde(rename = "name")] pub name: Option, @@ -161,7 +158,7 @@ pub struct Opts { // ---------------------------------------------------------------------------- // DeploymentCreation -#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct DeploymentCreation { #[serde(rename = "id")] pub id: String, @@ -190,7 +187,7 @@ pub struct DeploymentCreation { // ---------------------------------------------------------------------------- // Deployment -#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Deployment { #[serde(rename = "id")] pub id: String, @@ -226,22 +223,21 @@ pub async fn list( organisation_id: &str, function_id: &str, ) -> Result, Error> { - let path = format!( - "{}/v4/functions/organisations/{organisation_id}/functions/{function_id}/deployments", - client.endpoint + let endpoint = client.endpoint(format_args!( + "/v4/functions/organisations/{organisation_id}/functions/{function_id}/deployments" + ))?; + + debug!( + %endpoint, + organisation = organisation_id, + function = function_id, + "execute a request to list deployments for functions" ); - #[cfg(feature = "logging")] - if log_enabled!(Level::Debug) { - debug!( - "execute a request to list deployments for functions, path: '{path}', organisation: '{organisation_id}', function_id: '{function_id}'" - ); - } - - client - .get(&path) + Ok(client + .get(endpoint) .await - .map_err(|err| Error::List(function_id.to_string(), organisation_id.to_string(), err)) + .map_err(|e| Error::List(function_id.to_string(), organisation_id.to_string(), e))??) } #[cfg_attr(feature = "tracing", tracing::instrument)] @@ -252,22 +248,21 @@ pub async fn create( function_id: &str, opts: &Opts, ) -> Result { - let path = format!( - "{}/v4/functions/organisations/{organisation_id}/functions/{function_id}/deployments", - client.endpoint + let endpoint = client.endpoint(format_args!( + "/v4/functions/organisations/{organisation_id}/functions/{function_id}/deployments" + ))?; + + debug!( + %endpoint, + organisation = organisation_id, + function = function_id, + "execute a request to create deployment" ); - #[cfg(feature = "logging")] - if log_enabled!(Level::Debug) { - debug!( - "execute a request to create deployment, path: '{path}', organisation: {organisation_id}, function_id: '{function_id}'" - ); - } - - client - .post(&path, opts) + Ok(client + .post(endpoint, opts) .await - .map_err(|err| Error::Create(function_id.to_string(), organisation_id.to_string(), err)) + .map_err(|e| Error::Create(function_id.to_string(), organisation_id.to_string(), e))??) } #[cfg_attr(feature = "tracing", tracing::instrument)] @@ -278,26 +273,26 @@ pub async fn get( function_id: &str, deployment_id: &str, ) -> Result { - let path = format!( - "{}/v4/functions/organisations/{organisation_id}/functions/{function_id}/deployments/{deployment_id}", - client.endpoint + let endpoint = client.endpoint(format_args!( + "/v4/functions/organisations/{organisation_id}/functions/{function_id}/deployments/{deployment_id}" + ))?; + + debug!( + %endpoint, + organisation = organisation_id, + function = function_id, + deployment = deployment_id, + "execute a request to get deployment" ); - #[cfg(feature = "logging")] - if log_enabled!(Level::Debug) { - debug!( - "execute a request to get deployment, path: '{path}', organisation: {organisation_id}, function: {function_id}, deployment: {deployment_id}" - ); - } - - client.get(&path).await.map_err(|err| { + Ok(client.get(endpoint).await.map_err(|e| { Error::Get( deployment_id.to_string(), function_id.to_string(), organisation_id.to_string(), - err, + e, ) - }) + })??) } #[cfg_attr(feature = "tracing", tracing::instrument)] @@ -308,27 +303,31 @@ pub async fn trigger( function_id: &str, deployment_id: &str, ) -> Result<(), Error> { - let path = format!( - "{}/v4/functions/organisations/{organisation_id}/functions/{function_id}/deployments/{deployment_id}/trigger", - client.endpoint + let endpoint = client.endpoint(format_args!( + "/v4/functions/organisations/{organisation_id}/functions/{function_id}/deployments/{deployment_id}/trigger" + ))?; + + debug!( + %endpoint, + organisation = organisation_id, + function = function_id, + deployment = deployment_id, + "execute a request to get deployment" ); - #[cfg(feature = "logging")] - if log_enabled!(Level::Debug) { - debug!( - "execute a request to get deployment, path: '{path}', organisation: {organisation_id}, function: {function_id}, deployment: {deployment_id}" - ); - } + let request = reqwest::Request::new(Method::POST, endpoint); - let req = reqwest::Request::new( - Method::POST, - path.parse().map_err(|err| Error::ParseUrl(path, err))?, - ); + let response = client + .execute_request(request) + .await + .map_err(RestError::Execute)?; + + let status_code = response.status(); - let res = client.execute(req).await.map_err(Error::Execute)?; - let status = res.status(); - if !status.is_success() { - return Err(Error::StatusCode(status.as_u16())); + if !status_code.is_success() { + let full = response.bytes().await.map_err(RestError::BodyAggregation)?; + let value = serde_json::from_slice(&full).map_err(RestError::Deserialize)?; + return Err(Error::StatusCode(ErrorResponse { status_code, value })); } Ok(()) @@ -336,36 +335,37 @@ pub async fn trigger( #[cfg_attr(feature = "tracing", tracing::instrument)] /// Upload the WebAssembly on the endpoint -pub async fn upload(client: &Client, endpoint: &str, buf: Vec) -> Result<(), Error> { - let mut req = reqwest::Request::new( - Method::PUT, - endpoint - .parse() - .map_err(|err| Error::ParseUrl(endpoint.to_string(), err))?, - ); +pub async fn upload( + client: &Client, + endpoint: X, + buf: Vec, +) -> Result<(), Error> { + let url = endpoint.into_url().map_err(RestError::Url)?; - req.headers_mut().insert( - CONTENT_TYPE, - HeaderValue::from_static(MIME_APPLICATION_WASM), - ); - req.headers_mut() - .insert(CONTENT_LENGTH, HeaderValue::from(buf.len())); - *req.body_mut() = Some(Body::from(buf)); + let mut request = reqwest::Request::new(Method::PUT, url); - #[cfg(feature = "logging")] - if log_enabled!(Level::Debug) { - debug!("execute a request to upload webassembly, endpoint: '{endpoint}'"); - } + let headers = request.headers_mut(); + let _ = headers.insert(header::CONTENT_TYPE, MIME_APPLICATION_WASM); + let _ = headers.insert(header::CONTENT_LENGTH, HeaderValue::from(buf.len())); - let res = client + *request.body_mut() = Some(Body::from(buf)); + + debug!( + endpoint = %request.url(), + "execute a request to upload webassembly" + ); + + let response = client .inner() - .execute(req) + .execute(request) .await - .map_err(|err| Error::Execute(ClientError::Request(err)))?; + .map_err(ClientError::Execute)?; - let status = res.status(); - if !status.is_success() { - return Err(Error::StatusCode(status.as_u16())); + let status_code = response.status(); + if !status_code.is_success() { + let full = response.bytes().await.map_err(RestError::BodyAggregation)?; + let value = serde_json::from_slice(&full).map_err(RestError::Deserialize)?; + return Err(Error::StatusCode(ErrorResponse { status_code, value })); } Ok(()) @@ -379,24 +379,22 @@ pub async fn delete( function_id: &str, deployment_id: &str, ) -> Result<(), Error> { - let path = format!( - "{}/v4/functions/organisations/{organisation_id}/functions/{function_id}/deployments/{deployment_id}", - client.endpoint + let endpoint = client.endpoint(format_args!("/v4/functions/organisations/{organisation_id}/functions/{function_id}/deployments/{deployment_id}"))?; + + debug!( + %endpoint, + organisation = organisation_id, + function = function_id, + deployment = deployment_id, + "execute a request to delete deployment" ); - #[cfg(feature = "logging")] - if log_enabled!(Level::Debug) { - debug!( - "execute a request to delete deployment, path: '{path}', organisation: {organisation_id}, function: {function_id}, deployment: {deployment_id}" - ); - } - - client.delete(&path).await.map_err(|err| { + Ok(client.delete(endpoint).await.map_err(|e| { Error::Delete( deployment_id.to_string(), function_id.to_string(), organisation_id.to_string(), - err, + e, ) - }) + })??) } diff --git a/src/v4/functions/mod.rs b/src/v4/functions/mod.rs old mode 100644 new mode 100755 index 36c1bc6..1ffdf43 --- a/src/v4/functions/mod.rs +++ b/src/v4/functions/mod.rs @@ -3,53 +3,52 @@ //! This module provides all structures and helpers to interact with functions //! product at Clever Cloud. -use std::{collections::BTreeMap, fmt::Debug}; +use core::fmt; +use std::collections::BTreeMap; use chrono::{DateTime, Utc}; -use log::{Level, debug, log_enabled}; -use oauth10a::client::{ - ClientError, RestClient, - bytes::Buf, - reqwest::{self, Method}, - url, +use oauth10a::{ + reqwest::{self, IntoUrl, Method}, + rest::RestClient, }; use serde::{Deserialize, Serialize}; -use crate::Client; +use crate::{Client, EndpointError, RestError, v4::ErrorResponse}; pub mod deployments; // ----------------------------------------------------------------------------- // Error -#[derive(thiserror::Error, Debug)] +#[derive(Debug, thiserror::Error)] pub enum Error { - #[error("failed to parse endpoint '{0}', {1}")] - ParseUrl(String, url::ParseError), + #[error(transparent)] + Endpoint(#[from] EndpointError), #[error("failed to list functions for organisation '{0}', {1}")] - List(String, ClientError), + List(String, RestError), #[error("failed to create function on organisation '{0}', {1}")] - Create(String, ClientError), + Create(String, RestError), #[error("failed to get function '{0}' for organisation '{1}', {2}")] - Get(String, String, ClientError), + Get(String, String, RestError), #[error("failed to update function '{0}' of organisation '{1}', {2}")] - Update(String, String, ClientError), + Update(String, String, RestError), #[error("failed to delete function '{0}' of organisation '{1}', {2}")] - Delete(String, String, ClientError), + Delete(String, String, RestError), + #[error(transparent)] + StatusCode(#[from] ErrorResponse), + + #[error("failed to execute request, {0}")] + Execute(reqwest::Error), #[error("failed to aggregate body, {0}")] BodyAggregation(reqwest::Error), #[error("failed to deserialize execute response payload, {0}")] Deserialize(serde_json::Error), - #[error("failed to execute request, {0}")] - Execute(reqwest::Error), - #[error("failed to execute request, got status code {0}")] - StatusCode(u16), } // ----------------------------------------------------------------------------- -// CreateOpts structure +// Opts structure -#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Opts { #[serde(rename = "name")] pub name: Option, @@ -65,6 +64,12 @@ pub struct Opts { pub max_instances: u64, } +impl Opts { + pub const DEFAULT_MAX_MEMORY: u64 = 512 * 1024 * 1024; + + pub const DEFAULT_MAX_INSTANCES: u64 = 1; +} + impl Default for Opts { fn default() -> Self { Self { @@ -72,8 +77,8 @@ impl Default for Opts { description: None, tag: None, environment: BTreeMap::new(), - max_memory: 512 * 1024 * 1024, - max_instances: 1, + max_memory: Self::DEFAULT_MAX_MEMORY, + max_instances: Self::DEFAULT_MAX_INSTANCES, } } } @@ -81,7 +86,7 @@ impl Default for Opts { // ----------------------------------------------------------------------------- // Function structure -#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Function { #[serde(rename = "id")] pub id: String, @@ -108,7 +113,7 @@ pub struct Function { // ----------------------------------------------------------------------------- // ExecuteResult structure -#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(untagged)] pub enum ExecutionResult { Ok { @@ -131,9 +136,9 @@ impl ExecutionResult { #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] pub fn ok(stdout: T, stderr: U, dmesg: V, current_pages: Option) -> Self where - T: ToString, - U: ToString, - V: ToString, + T: fmt::Display, + U: fmt::Display, + V: fmt::Display, { Self::Ok { stdout: stdout.to_string(), @@ -146,7 +151,7 @@ impl ExecutionResult { #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))] pub fn err(error: T) -> Self where - T: ToString, + T: fmt::Display, { Self::Err { error: error.to_string(), @@ -167,140 +172,134 @@ impl ExecutionResult { // ----------------------------------------------------------------------------- // Helpers +/// Returns the list of function for an organisation. #[cfg_attr(feature = "tracing", tracing::instrument)] -/// returns the list of function for an organisation pub async fn list(client: &Client, organisation_id: &str) -> Result, Error> { - let path = format!( - "{}/v4/functions/organisations/{organisation_id}/functions", - client.endpoint + let endpoint = client.endpoint(format_args!( + "/v4/functions/organisations/{organisation_id}/functions" + ))?; + + debug!( + %endpoint, + organisation = organisation_id, + "execute a request to list functions" ); - #[cfg(feature = "logging")] - if log_enabled!(Level::Debug) { - debug!( - "execute a request to list functions for organisation, path: '{path}', organisation: '{organisation_id}'" - ); - } - - client - .get(&path) + Ok(client + .get(endpoint) .await - .map_err(|err| Error::List(organisation_id.to_string(), err)) + .map_err(|e| Error::List(organisation_id.to_string(), e))??) } +/// Creates a function on the given organisation. #[cfg_attr(feature = "tracing", tracing::instrument)] -/// create a function on the given organisation pub async fn create( client: &Client, organisation_id: &str, opts: &Opts, ) -> Result { - let path = format!( - "{}/v4/functions/organisations/{organisation_id}/functions", - client.endpoint + let endpoint = client.endpoint(format_args!( + "/v4/functions/organisations/{organisation_id}/functions" + ))?; + + debug!( + %endpoint, + organisation = organisation_id, + "execute a request to create function" ); - #[cfg(feature = "logging")] - if log_enabled!(Level::Debug) { - debug!( - "execute a request to create function, path: '{path}', organisation: {organisation_id}" - ); - } - - client - .post(&path, opts) + Ok(client + .post(endpoint, opts) .await - .map_err(|err| Error::Create(organisation_id.to_string(), err)) + .map_err(|e| Error::Create(organisation_id.to_string(), e))??) } +/// Returns the function information of the organisation. #[cfg_attr(feature = "tracing", tracing::instrument)] -/// returns the function information of the organisation pub async fn get( client: &Client, organisation_id: &str, function_id: &str, ) -> Result { - let path = format!( - "{}/v4/functions/organisations/{organisation_id}/functions/{function_id}", - client.endpoint + let endpoint = client.endpoint(format_args!( + "/v4/functions/organisations/{organisation_id}/functions/{function_id}", + ))?; + + debug!( + %endpoint, + organization = organisation_id, + function = function_id, + "execute a request to get function" ); - #[cfg(feature = "logging")] - if log_enabled!(Level::Debug) { - debug!( - "execute a request to get function, path: '{path}', organisation: {organisation_id}, function: {function_id}" - ); - } - - client - .get(&path) + Ok(client + .get(endpoint) .await - .map_err(|err| Error::Get(function_id.to_string(), organisation_id.to_string(), err)) + .map_err(|e| Error::Get(function_id.to_string(), organisation_id.to_string(), e))??) } +/// Updates the function information of the organisation. #[cfg_attr(feature = "tracing", tracing::instrument)] -/// Update the function information of the organisation pub async fn update( client: &Client, organisation_id: &str, function_id: &str, opts: &Opts, ) -> Result { - let path = format!( - "{}/v4/functions/organisations/{organisation_id}/functions/{function_id}", - client.endpoint + let endpoint = client.endpoint(format_args!( + "/v4/functions/organisations/{organisation_id}/functions/{function_id}", + ))?; + + debug!( + %endpoint, + organization = organisation_id, + function = function_id, + "execute a request to update function" ); - #[cfg(feature = "logging")] - if log_enabled!(Level::Debug) { - debug!( - "execute a request to update function, path: '{path}', organisation: {organisation_id}, function: {function_id}" - ); - } - - client - .put(&path, opts) + Ok(client + .put(endpoint, opts) .await - .map_err(|err| Error::Update(function_id.to_string(), organisation_id.to_string(), err)) + .map_err(|e| Error::Update(function_id.to_string(), organisation_id.to_string(), e))??) } +/// Returns the function information of the organisation. #[cfg_attr(feature = "tracing", tracing::instrument)] -/// returns the function information of the organisation pub async fn delete( client: &Client, organisation_id: &str, function_id: &str, ) -> Result<(), Error> { - let path = format!( - "{}/v4/functions/organisations/{organisation_id}/functions/{function_id}", - client.endpoint + let endpoint = client.endpoint(format_args!( + "/v4/functions/organisations/{organisation_id}/functions/{function_id}", + ))?; + + debug!( + %endpoint, + organization = organisation_id, + function = function_id, + "execute a request to delete function" ); - #[cfg(feature = "logging")] - if log_enabled!(Level::Debug) { - debug!( - "execute a request to delete function, path: '{path}', organisation: {organisation_id}, function: {function_id}" - ); - } - - client - .delete(&path) + Ok(client + .delete(endpoint) .await - .map_err(|err| Error::Delete(function_id.to_string(), organisation_id.to_string(), err)) + .map_err(|e| Error::Delete(function_id.to_string(), organisation_id.to_string(), e))??) } -#[cfg_attr(feature = "tracing", tracing::instrument)] /// Execute a GET HTTP request on the given endpoint -pub async fn execute(client: &Client, endpoint: &str) -> Result { - let req = reqwest::Request::new( - Method::GET, - endpoint - .parse() - .map_err(|err| Error::ParseUrl(endpoint.to_string(), err))?, - ); +#[cfg_attr(feature = "tracing", tracing::instrument)] +pub async fn execute( + client: &Client, + endpoint: X, +) -> Result { + let url = endpoint.into_url().map_err(Error::Execute)?; + + let req = reqwest::Request::new(Method::GET, url); let res = client.inner().execute(req).await.map_err(Error::Execute)?; + let buf = res.bytes().await.map_err(Error::BodyAggregation)?; - serde_json::from_reader(buf.reader()).map_err(Error::Deserialize) + serde_json::from_slice(&buf).map_err(Error::Deserialize) } diff --git a/src/v4/mod.rs b/src/v4/mod.rs index e8e2a62..9ba9038 100644 --- a/src/v4/mod.rs +++ b/src/v4/mod.rs @@ -1,6 +1,11 @@ //! # Api version 4 module //! -//! This module exposes resources under version 4 of the Clever-Cloud Api. +//! This module exposes resources under version 4 of the Clever-Cloud API. + +mod error; +pub use error::HttpError; + +pub type ErrorResponse = oauth10a::rest::ErrorResponse; pub mod addon_provider; pub mod functions; diff --git a/src/v4/products/zones.rs b/src/v4/products/zones.rs old mode 100644 new mode 100755 index 14a95bf..55fc987 --- a/src/v4/products/zones.rs +++ b/src/v4/products/zones.rs @@ -2,17 +2,14 @@ //! //! This module provide helpers and structures to interact with zones of products -use std::fmt::Debug; +use oauth10a::rest::RestClient; -#[cfg(feature = "logging")] -use log::{Level, debug, log_enabled}; -use oauth10a::client::{ClientError, RestClient}; #[cfg(feature = "jsonschemas")] use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::Client; +use crate::{Client, EndpointError, RestError, v4::ErrorResponse}; // ----------------------------------------------------------------------------- // Constants @@ -24,7 +21,7 @@ pub const TAG_HDS: &str = "certification:hds"; // Zone structure #[cfg_attr(feature = "jsonschemas", derive(JsonSchema))] -#[derive(Serialize, Deserialize, PartialEq, PartialOrd, Clone, Debug)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Serialize, Deserialize)] pub struct Zone { #[serde(rename = "id")] pub id: Uuid, @@ -47,48 +44,47 @@ pub struct Zone { // ----------------------------------------------------------------------------- // Error enumeration -#[derive(thiserror::Error, Debug)] +#[derive(Debug, thiserror::Error)] pub enum Error { + #[error(transparent)] + Endpoint(#[from] EndpointError), #[error("failed to list available zones, {0}")] - List(ClientError), + List(#[from] RestError), + #[error(transparent)] + Status(#[from] ErrorResponse), } // ----------------------------------------------------------------------------- // List zones +/// Returns the list of available zones. #[cfg_attr(feature = "tracing", tracing::instrument)] -/// returns the list of zones availables pub async fn list(client: &Client) -> Result, Error> { - let path = format!("{}/v4/products/zones", client.endpoint); + let endpoint = client.endpoint("/v4/products/zones")?; - #[cfg(feature = "logging")] - if log_enabled!(Level::Debug) { - debug!("execute a request to list zones, path: '{}'", &path); - } + debug!(%endpoint, "execute a request to list zones"); - client.get(&path).await.map_err(Error::List) + Ok(client.get(endpoint).await??) } +/// Returns the list of zones available for applications and addons. #[cfg_attr(feature = "tracing", tracing::instrument)] -/// applications returns the list of zones availables for applications and addons pub async fn applications(client: &Client) -> Result, Error> { Ok(list(client) .await? - .iter() + .into_iter() .filter(|zone| zone.tags.contains(&TAG_APPLICATION.to_string())) - .map(ToOwned::to_owned) .collect()) } +/// Returns the list of zones available for applications and addons with +/// Health Data Hosting (HDS) certification. #[cfg_attr(feature = "tracing", tracing::instrument)] -/// hds returns the list of zones availables for applications and addons with -/// hds certification pub async fn hds(client: &Client) -> Result, Error> { Ok(list(client) .await? - .iter() + .into_iter() .filter(|zone| zone.tags.contains(&TAG_APPLICATION.to_string())) .filter(|zone| zone.tags.contains(&TAG_HDS.to_string())) - .map(ToOwned::to_owned) .collect()) } From aad9ed20c3bf99abec4b03c209da575b8af632b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Lemaire-Giroud?= Date: Fri, 30 May 2025 08:13:18 +0200 Subject: [PATCH 2/3] feat(network-group): introduce Network Group API --- Cargo.toml | 16 +- src/clever_env.rs | 222 ++++++++++ src/clever_tools.rs | 87 ++++ src/lib.rs | 378 +++++++++++++----- src/logging.rs | 20 +- src/v4/functions/deployments.rs | 3 +- src/v4/mod.rs | 4 +- src/v4/network_group/clever_peer.rs | 23 ++ src/v4/network_group/component.rs | 51 +++ src/v4/network_group/delete.rs | 42 ++ src/v4/network_group/endpoint.rs | 39 ++ src/v4/network_group/external_peer.rs | 21 + src/v4/network_group/member.rs | 68 ++++ src/v4/network_group/member_id.rs | 78 ++++ src/v4/network_group/member_kind.rs | 38 ++ src/v4/network_group/mod.rs | 51 +++ src/v4/network_group/network_group_id.rs | 81 ++++ src/v4/network_group/owner_id.rs | 187 +++++++++ src/v4/network_group/peer_id.rs | 96 +++++ src/v4/network_group/peer_kind.rs | 39 ++ src/v4/network_group/wannabe_external_peer.rs | 106 +++++ src/v4/network_group/wannabe_member.rs | 62 +++ src/v4/network_group/wireguard.rs | 194 +++++++++ .../network_group/wireguard_configuration.rs | 167 ++++++++ 24 files changed, 1952 insertions(+), 121 deletions(-) create mode 100644 src/clever_env.rs create mode 100644 src/clever_tools.rs mode change 100644 => 100755 src/v4/mod.rs create mode 100644 src/v4/network_group/clever_peer.rs create mode 100644 src/v4/network_group/component.rs create mode 100755 src/v4/network_group/delete.rs create mode 100644 src/v4/network_group/endpoint.rs create mode 100644 src/v4/network_group/external_peer.rs create mode 100644 src/v4/network_group/member.rs create mode 100644 src/v4/network_group/member_id.rs create mode 100644 src/v4/network_group/member_kind.rs create mode 100755 src/v4/network_group/mod.rs create mode 100755 src/v4/network_group/network_group_id.rs create mode 100644 src/v4/network_group/owner_id.rs create mode 100644 src/v4/network_group/peer_id.rs create mode 100644 src/v4/network_group/peer_kind.rs create mode 100755 src/v4/network_group/wannabe_external_peer.rs create mode 100644 src/v4/network_group/wannabe_member.rs create mode 100755 src/v4/network_group/wireguard.rs create mode 100755 src/v4/network_group/wireguard_configuration.rs diff --git a/Cargo.toml b/Cargo.toml index 8022228..8047ac1 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,17 +13,18 @@ keywords = ["clevercloud", "sdk", "logging", "metrics", "jsonschemas"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -default = ["logging"] +default = ["logging", "network-group"] jsonschemas = ["dep:schemars"] logging = ["dep:log", "oauth10a/logging", "tracing/log-always"] metrics = ["oauth10a/metrics"] tracing = ["oauth10a/tracing", "dep:tracing"] -network-group = ["dep:base64", "x25519-dalek", "dep:rand_core", "dep:zeroize"] +network-group = ["dep:base64", "x25519-dalek", "dep:rand_core", "dep:cidr"] [dependencies] base64 = { version = "^0.22.1", optional = true } chrono = { version = "^0.4.41", features = ["serde"] } -oauth10a = { version = "^3.0.0", default-features = false, features = [ +cidr = { version = "^0.3.1", features = ["serde"], optional = true } +oauth10a = { path = "../oauth10a-rust", default-features = false, features = [ "client", "serde", "rest", @@ -49,7 +50,10 @@ x25519-dalek = { version = "^2.0.1", features = [ "zeroize", "static_secrets", ], optional = true } -zeroize = { version = "^1.8.1", features = [ +zeroize = { version = "^1.8.1", features = ["serde", "derive"] } +env-capture = { git = "https://gitlab.corp.clever-cloud.com/CedricLG/env-capture", features = [ "serde", - "derive", -], optional = true } +] } + +[dev-dependencies] +tracing-subscriber = { version = "^0.3.19", features = ["env-filter"] } diff --git a/src/clever_env.rs b/src/clever_env.rs new file mode 100644 index 0000000..35534eb --- /dev/null +++ b/src/clever_env.rs @@ -0,0 +1,222 @@ +use std::{borrow::Cow, fs, io, path::Path}; + +use env_capture::{Env, IgnoreAsciiCase}; +use oauth10a::{credentials::Credentials, url::Url}; + +use crate::{ + DEFAULT_SSH_GATEWAY, PartialCredentials, + clever_tools::{CleverTools, CleverToolsConfig, CleverToolsConfigError}, + default_api_host, default_auth_bridge_host, +}; + +// PARTIAL OAUTH ERROR ///////////////////////////////////////////////////////// + +#[derive(Debug, thiserror::Error)] +pub enum PartialOAuthError { + #[error("missing token")] + Token, + #[error("missing secret")] + Secret, + #[error("missing consumer token")] + ConsumerKey, + #[error("missing consumer secret")] + ConsumerSecret, +} + +// CLEVER ENV ////////////////////////////////////////////////////////////////// + +#[derive(Debug, thiserror::Error)] +pub enum CleverEnvError { + #[error("failed to capture environnement, {0}")] + Capture(#[from] env_capture::Error), + #[error(transparent)] + CleverToolsConfigFile(#[from] CleverToolsConfigError), + #[error("partial OAuth credentials: {0}")] + PartialOAuth(#[from] PartialOAuthError), + #[error("failed to create configuration directory")] + ConfigDir(io::Error), +} + +/// Snapshot of the clever environment variables, hydrated with the OAuth +/// configuration from the main configuration file of the `clever-tools`, if any. +#[derive(Debug, serde::Deserialize)] +pub struct CleverEnv { + #[serde(rename = "API_HOST")] + pub(crate) api_host: Option, + #[serde(rename = "AUTH_BRIDGE_API")] + pub(crate) auth_bridge_host: Option, + #[serde(rename = "SSH_GATEWAY")] + pub(crate) ssh_gateway: Option>, + #[serde(rename = "", flatten, default)] + pub(crate) credentials: Option, + #[serde(skip)] + pub(crate) config_dir: Option>, +} + +impl CleverEnv { + pub fn from_env() -> Result { + let env = Env::::from_env(); + + let mut env = env.with_prefix("CLEVER_").parse::()?; + + match &mut env.credentials { + credentials @ None => { + trace!("credentials not found in current process environment"); + + let config_dir = CleverToolsConfig::default_config_dir()?; + + let config_path = CleverToolsConfig::config_path_in(&config_dir); + + if config_path.exists() { + env.config_dir.replace(config_dir.into()); + + let CleverToolsConfig { + oauth_token, + oauth_secret, + } = CleverToolsConfig::from_path(&config_path)?; + + *credentials = Some(Credentials::OAuth1 { + token: oauth_token, + secret: oauth_secret, + consumer_key: None, + consumer_secret: None, + }); + + trace!("using credentials from `clever-tools` configuration file"); + } + } + Some(Credentials::OAuth1 { + consumer_key: Some(_), + consumer_secret: None, + .. + }) => { + return Err(CleverEnvError::PartialOAuth( + PartialOAuthError::ConsumerSecret, + )); + } + Some(Credentials::OAuth1 { + consumer_key: None, + consumer_secret: Some(_), + .. + }) => return Err(CleverEnvError::PartialOAuth(PartialOAuthError::ConsumerKey)), + Some(_) => trace!("using credentials from environment"), + } + + Ok(env) + } + + pub fn env_api_host(&self) -> Option<&Url> { + self.api_host.as_ref() + } + + pub fn api_host(&self) -> &Url { + match &self.api_host { + None => default_api_host(), + Some(v) => v, + } + } + + pub fn env_auth_bridge_host(&self) -> Option<&Url> { + self.auth_bridge_host.as_ref() + } + + pub fn auth_bridge_host(&self) -> &Url { + match &self.auth_bridge_host { + None => default_auth_bridge_host(), + Some(v) => v, + } + } + + pub fn env_ssh_gateway(&self) -> Option<&str> { + self.ssh_gateway.as_deref() + } + + pub const fn ssh_gateway(&self) -> &str { + match &self.ssh_gateway { + None => DEFAULT_SSH_GATEWAY, + Some(v) => v, + } + } + + pub const fn env_oauth_consumer_key(&self) -> Option<&str> { + match self.credentials { + Some(Credentials::OAuth1 { + consumer_key: Some(ref v), + .. + }) => Some(v), + _ => None, + } + } + + pub const fn oauth_consumer_key(&self) -> &str { + match self.env_oauth_consumer_key() { + Some(x) => x, + None => CleverTools::CONSUMER_KEY, + } + } + + pub const fn env_oauth_consumer_secret(&self) -> Option<&str> { + match self.credentials { + Some(Credentials::OAuth1 { + consumer_secret: Some(ref v), + .. + }) => Some(v), + _ => None, + } + } + + pub const fn oauth_consumer_secret(&self) -> &str { + match self.env_oauth_consumer_secret() { + Some(v) => v, + None => CleverTools::CONSUMER_SECRET, + } + } + + /// Returns the path to the directory where configuration files of clever apps are stored. + pub fn config_dir(&self) -> Result, CleverEnvError> { + let path = match self.config_dir { + Some(ref config_dir) => Cow::Borrowed(&**config_dir), + None => Cow::Owned(CleverToolsConfig::default_config_dir()?), + }; + + fs::create_dir_all(&*path).map_err(CleverEnvError::ConfigDir)?; + + Ok(path) + } + + pub fn credentials(&self) -> Option<&PartialCredentials> { + self.credentials.as_ref() + } +} + +#[cfg(test)] +mod tests { + use env_capture::set_tmp_var; + use oauth10a::credentials::Credentials; + + use crate::clever_env::CleverEnv; + + #[test] + fn test_env() { + let _ = unsafe { set_tmp_var("RUST_LOG", "trace") }; + + tracing_subscriber::fmt::fmt() + .with_level(true) + .with_line_number(true) + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .init(); + + let _ = unsafe { set_tmp_var("CLEVER_TOKEN", "my_token") }; + let _ = unsafe { set_tmp_var("CLEVER_SECRET", "my_secret") }; + + if let Credentials::OAuth1 { + token, + secret, + consumer_key, + consumer_secret, + } = CleverEnv::from_env().unwrap().credentials().unwrap() + { + dbg!(token, secret, consumer_key, consumer_secret); + } + } +} diff --git a/src/clever_tools.rs b/src/clever_tools.rs new file mode 100644 index 0000000..49433a5 --- /dev/null +++ b/src/clever_tools.rs @@ -0,0 +1,87 @@ +//! Clever Tools + +use std::{ + env, fs, + io::{self, Read}, + path::{Path, PathBuf}, +}; + +// CLEVER TOOLS //////////////////////////////////////////////////////////////// + +/// Default OAuth1 consumer. +#[derive(Debug)] +pub struct CleverTools; + +impl CleverTools { + // Consumer key and secret of the clever-tools are publicly available. + // The disclosure of these tokens is not considered a vulnerability. + // Do not report this to our security service. + // + // See: + // - + + pub const CONSUMER_KEY: &'static str = "T5nFjKeHH4AIlEveuGhB5S3xg8T19e"; + pub const CONSUMER_SECRET: &'static str = "MgVMqTr6fWlf2M0tkC2MXOnhfqBWDT"; +} + +// CLEVER TOOLS CONFIG ERROR /////////////////////////////////////////////////// + +#[derive(Debug, thiserror::Error)] +pub enum CleverToolsConfigError { + #[error("failed to resolve home directory")] + HomeDir, + #[error("failed to open clever-tools configuration file")] + Open(io::Error), + #[error("failed to read clever-tools configuration file's contents")] + Read(io::Error), + #[error("failed to parse clever-tools configuration file's contents")] + Json(serde_json::Error), +} + +// CLEVER TOOLS CONFIG ///////////////////////////////////////////////////////// + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct CleverToolsConfig { + #[serde(rename = "token")] + pub oauth_token: Box, + #[serde(rename = "secret")] + pub oauth_secret: Box, +} + +fn config_dir() -> Result { + Ok(match env::var_os("XDG_CONFIG_HOME") { + Some(config_dir) => PathBuf::from(config_dir), + None => env::home_dir() + .ok_or(CleverToolsConfigError::HomeDir)? + .join(".config"), + }) +} + +impl CleverToolsConfig { + pub fn default_config_dir() -> Result { + Ok(config_dir()?.join("clever-cloud")) + } + + pub fn config_path_in(config_dir: &Path) -> PathBuf { + config_dir.join("clever-tools.json") + } + + pub fn config_path() -> Result { + Ok(Self::config_path_in(&Self::default_config_dir()?)) + } + + pub fn from_path(path: &Path) -> Result { + let buf = { + let mut buf = String::new(); + + let _ = fs::File::open(path) + .map_err(CleverToolsConfigError::Open)? + .read_to_string(&mut buf) + .map_err(CleverToolsConfigError::Read)?; + + buf + }; + + serde_json::from_str(&buf).map_err(CleverToolsConfigError::Json) + } +} diff --git a/src/lib.rs b/src/lib.rs index b817497..ded9023 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,13 +1,13 @@ //! # Clever-Cloud Software Development Kit (SDK) //! -//! This module provides a client and structures to interact with the Clever-Cloud API. +//! This crate provides an HTTP client and structures to interact with the Clever-Cloud API. -use core::{fmt, str}; +use core::fmt; use std::sync::OnceLock; use oauth10a::{ - client::Client as OAuthClient, - credentials::{Credentials, CredentialsBuilder}, + authorize::Authorize, + credentials::{AuthorizationError, Credentials}, execute::ExecuteRequest, reqwest::{self, Request, Response, Url}, }; @@ -15,16 +15,27 @@ use oauth10a::{ #[macro_use] mod logging; +pub mod clever_env; +use self::clever_env::{CleverEnv, CleverEnvError}; + +pub mod clever_tools; +use self::clever_tools::CleverTools; + pub mod v2; pub mod v4; // TYPE ALIASES //////////////////////////////////////////////////////////////// -pub type ClientError = oauth10a::client::ClientError; - pub type RestError = oauth10a::rest::RestError; -type UrlParseError = ::Err; +pub type ClientError = oauth10a::client::ClientError; + +type UrlParseError = ::Err; + +/// Credentials that supports missing OAuth 1.0a consumer details. +pub type PartialCredentials = Credentials, Option>>; + +type OAuthClient = oauth10a::client::Client; // RE-EXPORTS ////////////////////////////////////////////////////////////////// @@ -32,47 +43,32 @@ pub use oauth10a; // CLEVER CLOUD API //////////////////////////////////////////////////////////// -pub const PUBLIC_API_ENDPOINT: &str = "https://api.clever-cloud.com"; +pub const DEFAULT_API_HOST: &str = "https://api.clever-cloud.com"; -pub fn public_api_endpoint() -> &'static Url { +pub fn default_api_host() -> &'static Url { static URL: OnceLock = OnceLock::new(); URL.get_or_init(|| { - PUBLIC_API_ENDPOINT + DEFAULT_API_HOST .parse() - .expect("valid URL for public API endpoint") + .expect("valid URL for default API host") }) } -pub const PUBLIC_API_BRIDGE_ENDPOINT: &str = "https://api-bridge.clever-cloud.com"; +pub const DEFAULT_AUTH_BRIDGE_HOST: &str = "https://api-bridge.clever-cloud.com"; -pub fn public_api_bridge_endpoint() -> &'static Url { +pub fn default_auth_bridge_host() -> &'static Url { static URL: OnceLock = OnceLock::new(); URL.get_or_init(|| { - PUBLIC_API_BRIDGE_ENDPOINT + DEFAULT_AUTH_BRIDGE_HOST .parse() - .expect("valid URL for public API bridge endpoint") + .expect("valid URL for default auth bridge host") }) } -// CLEVER TOOLS //////////////////////////////////////////////////////////////// - -/// Default OAuth1 consumer. -#[derive(Debug)] -pub struct CleverTools; - -impl CleverTools { - // Consumer key and secret of the clever-tools are publicly available. - // The disclosure of these tokens is not considered a vulnerability. - // Do not report this to our security service. - // - // See: - // - - - pub const CONSUMER_KEY: &'static str = "T5nFjKeHH4AIlEveuGhB5S3xg8T19e"; - pub const CONSUMER_SECRET: &'static str = "MgVMqTr6fWlf2M0tkC2MXOnhfqBWDT"; -} +pub const DEFAULT_SSH_GATEWAY: &str = + "ssh@sshgateway-clevercloud-customers.services.clever-cloud.com'"; // ENDPOINT ERROR ////////////////////////////////////////////////////////////// @@ -84,73 +80,283 @@ pub struct EndpointError { error: UrlParseError, } +// INTO PARTIAL CREDENTIALS //////////////////////////////////////////////////// + +/// Utility trait for types that can be used to create Credentials for Clever Cloud's HTTP [`Client`]. +pub trait IntoPartialCredentials: fmt::Debug { + fn into_partial_credentials(self) -> PartialCredentials; +} + +impl>> IntoPartialCredentials for Credentials> { + fn into_partial_credentials(self) -> PartialCredentials { + match self { + Self::Bearer { token } => Credentials::Bearer { + token: token.into(), + }, + Self::Basic { username, password } => Credentials::Basic { + username: username.into(), + password: password.map(Into::into), + }, + Self::OAuth1 { + token, + secret, + consumer_key, + consumer_secret, + } => Credentials::OAuth1 { + token: token.into(), + secret: secret.into(), + consumer_key: consumer_key.map(Into::into), + consumer_secret: consumer_secret.map(Into::into), + }, + } + } +} + +impl>> IntoPartialCredentials for Credentials { + #[inline] + fn into_partial_credentials(self) -> PartialCredentials { + match self { + Self::Bearer { token } => Credentials::Bearer { + token: token.into(), + }, + Self::Basic { username, password } => Credentials::Basic { + username: username.into(), + password: password.map(Into::into), + }, + Self::OAuth1 { + token, + secret, + consumer_key, + consumer_secret, + } => Credentials::OAuth1 { + token: token.into(), + secret: secret.into(), + consumer_key: Some(consumer_key.into()), + consumer_secret: Some(consumer_secret.into()), + }, + } + } +} + +impl IntoPartialCredentials for &T { + #[inline] + fn into_partial_credentials(self) -> PartialCredentials { + self.clone().into_partial_credentials() + } +} + +// AUTHORIZER ////////////////////////////////////////////////////////////////// + +/// HTTP Request [`Authorize`] implementation that supports missing OAuth 1.0a consumer details. +#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] +struct Authorizer { + credentials: Option, +} + +impl Authorizer { + #[inline(always)] + pub const fn new(credentials: Option) -> Self { + Self { credentials } + } + + #[cfg_attr(feature = "tracing", tracing::instrument)] + #[inline(always)] + pub fn set_credentials(&mut self, credentials: PartialCredentials) { + let _ = self.credentials.replace(credentials); + } + + #[cfg_attr(feature = "tracing", tracing::instrument)] + #[inline(always)] + pub fn clear_credentials(&mut self) { + let _ = self.credentials.take(); + } + + #[inline(always)] + pub const fn credentials(&self) -> Option> { + match &self.credentials { + None => None, + Some(Credentials::OAuth1 { + token, + secret, + consumer_key, + consumer_secret, + }) => Some(Credentials::OAuth1 { + token, + secret, + consumer_key: match consumer_key { + None => CleverTools::CONSUMER_KEY, + Some(v) => v, + }, + consumer_secret: match consumer_secret { + None => CleverTools::CONSUMER_SECRET, + Some(v) => v, + }, + }), + Some(Credentials::Basic { username, password }) => Some(Credentials::Basic { + username, + password: match password { + None => None, + Some(v) => Some(v), + }, + }), + Some(Credentials::Bearer { token }) => Some(Credentials::Bearer { token }), + } + } +} + +impl Authorize for Authorizer { + type Error = ::Error; + + #[inline(always)] + fn authorize(&self, request: &mut Request) -> Result { + match self.credentials() { + None => Ok(false), + Some(credentials) => credentials.authorize(request), + } + } +} + +impl Drop for Authorizer { + fn drop(&mut self) { + use zeroize::Zeroize; + + if let Some(mut credentials) = self.credentials.take() { + credentials.zeroize(); + } + } +} + // CLIENT ////////////////////////////////////////////////////////////////////// -/// HTTP client specialized for Clever Cloud API. -/// -/// -/// -/// -#[derive(Debug, Default, Clone)] +/// HTTP client specialized for interacting with the Clever Cloud API. +#[derive(Debug, Clone)] pub struct Client { - inner: OAuthClient, - api_endpoint: Option, + client: OAuthClient, + api_host: Option, + auth_bridge_host: Option, +} + +impl Default for Client { + fn default() -> Self { + Self::from(reqwest::Client::default()) + } +} + +impl From for Client { + fn from(value: reqwest::Client) -> Self { + Self { + client: OAuthClient::new(value, Authorizer::default()), + api_host: None, + auth_bridge_host: None, + } + } } impl Client { - pub fn new() -> Self { + #[cfg_attr(feature = "tracing", tracing::instrument)] + pub fn new( + client: reqwest::Client, + credentials: Option, + api_host: Option, + auth_bridge_host: Option, + ) -> Self { Self { - inner: OAuthClient::new(), - api_endpoint: None, + api_host, + auth_bridge_host, + client: OAuthClient::new(client, Authorizer::new(credentials)), } } + pub fn from_env_snapshot(env: &CleverEnv) -> Self { + Self::new( + reqwest::Client::new(), + env.credentials.clone(), + env.env_api_host().cloned(), + env.env_auth_bridge_host().cloned(), + ) + } + + pub fn from_env() -> Result { + Ok(Self::from_env_snapshot(&CleverEnv::from_env()?)) + } + /// Sets the credentials that will be used by this client to authorize subsequent HTTP requests. /// /// When `consumer_keys` and/or `consumer_secret` are missing, the client will /// use the values of the [`CleverTools`]. - pub fn set_credentials>(&mut self, credentials: Option) { - self.inner.set_credentials(credentials.map(|credentials| { - credentials - .into() - .with_consumer(CleverTools::CONSUMER_SECRET, CleverTools::CONSUMER_SECRET) - })); + pub fn set_credentials(&mut self, credentials: impl IntoPartialCredentials) { + self.client + .authorizer_mut() + .set_credentials(credentials.into_partial_credentials()); } - pub fn with_credentials>(mut self, credentials: Option) -> Self { + /// Fills the `credentials` to be used by the client to authorize HTTP request, + /// discarding the current value, if any. + /// + /// When `consumer_keys` and/or `consumer_secret` are missing, the client will + /// use the credentials of the default consumer: [`CleverTools`]. + pub fn with_credentials(mut self, credentials: impl IntoPartialCredentials) -> Self { self.set_credentials(credentials); self } + pub fn clear_credentials(&mut self) { + self.client.authorizer_mut().clear_credentials(); + } + pub fn credentials(&self) -> Option> { - self.inner.credentials() + self.client.authorizer().credentials() } #[cfg_attr(feature = "tracing", tracing::instrument)] - pub fn set_endpoint(&mut self, api_endpoint: Option) { - self.api_endpoint = api_endpoint; + pub fn set_api_host(&mut self, api_host: impl Into> + fmt::Debug) { + self.api_host = api_host.into(); } - pub fn with_endpoint(mut self, api_endpoint: Option) -> Self { - self.set_endpoint(api_endpoint); + pub fn with_api_host(mut self, api_host: impl Into> + fmt::Debug) -> Self { + self.set_api_host(api_host); self } + #[cfg_attr(feature = "tracing", tracing::instrument)] + pub fn set_auth_bridge_host(&mut self, auth_bridge_host: impl Into> + fmt::Debug) { + self.auth_bridge_host = auth_bridge_host.into(); + } + + pub fn with_auth_bridge_host( + mut self, + auth_bridge_host: impl Into> + fmt::Debug, + ) -> Self { + self.set_auth_bridge_host(auth_bridge_host); + self + } + + pub const fn api_host(&self) -> Option<&Url> { + self.api_host.as_ref() + } + + pub const fn auth_bridge_host(&self) -> Option<&Url> { + self.auth_bridge_host.as_ref() + } + pub fn api_endpoint(&self) -> &Url { - if let Some(ref url) = self.api_endpoint { - url - } else if let Some(Credentials::Bearer { .. }) = self.credentials() { - public_api_bridge_endpoint() - } else { - public_api_endpoint() + match self.credentials() { + Some(Credentials::Bearer { .. }) => match self.auth_bridge_host { + Some(ref url) => url, + None => default_auth_bridge_host(), + }, + _ => match self.api_host { + Some(ref url) => url, + None => default_api_host(), + }, } } /// Joins `path` to this client's API endpoint. #[cfg_attr(feature = "tracing", tracing::instrument)] - pub(crate) fn endpoint( + pub(crate) fn endpoint( &self, - path: T, + path: impl fmt::Display + fmt::Debug, ) -> Result { let api = self.api_endpoint(); let path = path.to_string(); @@ -166,57 +372,25 @@ impl Client { } pub fn inner(&self) -> &reqwest::Client { - self.inner.inner() - } -} - -impl From for Client { - fn from(value: reqwest::Client) -> Self { - Self { - inner: oauth10a::client::Client::from(value), - ..Default::default() - } - } -} - -impl From for Client { - fn from(value: CredentialsBuilder) -> Self { - Self::new().with_credentials(Some(value)) + self.client.executer() } } -impl From<&CredentialsBuilder> for Client { - fn from(value: &CredentialsBuilder) -> Self { - Self::from(value.clone()) - } -} - -impl>> From> for Client { - fn from(value: Credentials) -> Self { - Self { - inner: oauth10a::client::Client::from(value), - ..Default::default() - } - } -} - -impl From<&Credentials> for Client { - fn from(value: &Credentials) -> Self { - Self { - inner: oauth10a::client::Client::from(value), - ..Default::default() - } +impl From for Client { + #[cfg_attr(feature = "tracing", tracing::instrument)] + fn from(value: T) -> Self { + Client::default().with_credentials(value) } } impl ExecuteRequest for Client { type Error = ClientError; - #[inline] + #[inline(always)] fn execute_request( &self, request: Request, ) -> impl Future> + Send + 'static { - self.inner.execute_request(request) + self.client.execute_request(request) } } diff --git a/src/logging.rs b/src/logging.rs index 7106c22..216c5dd 100755 --- a/src/logging.rs +++ b/src/logging.rs @@ -1,51 +1,51 @@ #![allow(unused_macros, dead_code)] macro_rules! trace { - ($($arg:tt)*) => { + ($($arg:tt)*) => {{ #[cfg(feature = "logging")] if ::log::log_enabled!(log::Level::Trace) { ::tracing::trace!($($arg)*); } - }; + }}; () => () } macro_rules! debug { - ($($arg:tt)*) => { + ($($arg:tt)*) => {{ #[cfg(feature = "logging")] if ::log::log_enabled!(log::Level::Debug) { ::tracing::debug!($($arg)*); } - }; + }}; () => () } macro_rules! info { - ($($arg:tt)*) => { + ($($arg:tt)*) => {{ #[cfg(feature = "logging")] if ::log::log_enabled!(log::Level::Info) { ::tracing::info!($($arg)*); } - }; + }}; () => () } macro_rules! warn { - ($($arg:tt)*) => { + ($($arg:tt)*) => {{ #[cfg(feature = "logging")] if ::log::log_enabled!(log::Level::Warn) { ::tracing::warn!($($arg)*); } - }; + }}; () => () } macro_rules! error { - ($($arg:tt)*) => { + ($($arg:tt)*) => {{ #[cfg(feature = "logging")] if ::log::log_enabled!(log::Level::Error) { ::tracing::error!($($arg)*); } - }; + }}; () => () } diff --git a/src/v4/functions/deployments.rs b/src/v4/functions/deployments.rs index 55954db..a23e836 100755 --- a/src/v4/functions/deployments.rs +++ b/src/v4/functions/deployments.rs @@ -6,7 +6,6 @@ use core::{fmt, str::FromStr}; use chrono::{DateTime, Utc}; use oauth10a::{ - client::ClientError, execute::ExecuteRequest, reqwest::{ self, Body, IntoUrl, Method, @@ -16,7 +15,7 @@ use oauth10a::{ }; use serde::{Deserialize, Serialize}; -use crate::{Client, EndpointError, RestError, v4::ErrorResponse}; +use crate::{Client, ClientError, EndpointError, RestError, v4::ErrorResponse}; // ----------------------------------------------------------------------------- // Constants diff --git a/src/v4/mod.rs b/src/v4/mod.rs old mode 100644 new mode 100755 index 9ba9038..749b82c --- a/src/v4/mod.rs +++ b/src/v4/mod.rs @@ -5,8 +5,10 @@ mod error; pub use error::HttpError; -pub type ErrorResponse = oauth10a::rest::ErrorResponse; +pub type ErrorResponse = oauth10a::rest::ErrorResponse>; pub mod addon_provider; pub mod functions; +#[cfg(feature = "network-group")] +pub mod network_group; pub mod products; diff --git a/src/v4/network_group/clever_peer.rs b/src/v4/network_group/clever_peer.rs new file mode 100644 index 0000000..6234d68 --- /dev/null +++ b/src/v4/network_group/clever_peer.rs @@ -0,0 +1,23 @@ +use crate::v4::network_group::{ + endpoint::Endpoint, peer_id::PeerId, wireguard::WireGuardPublicKey, +}; + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct CleverPeer { + #[serde(rename = "id")] + peer_id: PeerId, + #[serde(rename = "label")] + label: Option, + #[serde(rename = "publicKey")] + public_key: WireGuardPublicKey, + #[serde(rename = "endpoint")] + endpoint: Endpoint, + #[serde(rename = "hostname")] + hostname: String, + #[serde(rename = "parentMember")] + parent_member: String, + #[serde(rename = "parentEvent")] + parent_event: Option, + #[serde(rename = "hv")] + hv: String, +} diff --git a/src/v4/network_group/component.rs b/src/v4/network_group/component.rs new file mode 100644 index 0000000..61f4519 --- /dev/null +++ b/src/v4/network_group/component.rs @@ -0,0 +1,51 @@ +use oauth10a::rest::RestClient; + +use crate::{ + Client, EndpointError, RestError, + v4::{ + ErrorResponse, + network_group::{ + NetworkGroup, clever_peer::CleverPeer, external_peer::ExternalPeer, member::Member, + owner_id::OwnerId, + }, + }, +}; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + Endpoint(#[from] EndpointError), + #[error("failed to get Network Group component for owner '{0}', query '{1}', {0}")] + Get(OwnerId, Box, RestError), + #[error(transparent)] + Status(#[from] ErrorResponse), +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub enum NetworkGroupComponent { + CleverPeer(CleverPeer), + ExternalPeer(ExternalPeer), + Member(Member), + NetworkGroup(NetworkGroup), +} + +impl NetworkGroupComponent { + /// Search a Network Group component (network group, member, or peer by its id or label). + pub async fn get(client: &Client, owner_id: &OwnerId, query: String) -> Result { + let endpoint = client.endpoint(format_args!( + "/v4/networkgroups/organisations/{owner_id}/networkgroups/search?{query}" + ))?; + + debug!( + %endpoint, + owner = %owner_id, + query = %query, + "execute a request to search a Network Group component" + ); + + Ok(client + .get(endpoint) + .await + .map_err(|e| Error::Get(*owner_id, query.into_boxed_str(), e))??) + } +} diff --git a/src/v4/network_group/delete.rs b/src/v4/network_group/delete.rs new file mode 100755 index 0000000..ad1d2f0 --- /dev/null +++ b/src/v4/network_group/delete.rs @@ -0,0 +1,42 @@ +use oauth10a::rest::RestClient; + +use crate::{Client, EndpointError, RestError, v4::ErrorResponse}; + +use crate::v4::network_group::{ + network_group_id::NetworkGroupId, owner_id::OwnerId, peer_id::PeerId, +}; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + Endpoint(#[from] EndpointError), + #[error("failed to delete for owner '{0}', network group '{1}', peer id '{2}', {0}")] + Delete(OwnerId, NetworkGroupId, PeerId, RestError), + #[error(transparent)] + Status(#[from] ErrorResponse), +} + +/// Delete a Network Group. +pub async fn delete( + client: &Client, + owner_id: &OwnerId, + ng_id: &NetworkGroupId, + peer_id: PeerId, +) -> Result<(), Error> { + let endpoint = client.endpoint(format_args!( + "/v4/networkgroups/organisations/{owner_id}/networkgroups/{ng_id}/external-peers/{peer_id}" + ))?; + + debug!( + %endpoint, + owner = %owner_id, + network_group = %ng_id, + peer = %peer_id, + "execute a request to delete peer from Network Group" + ); + + Ok(client + .delete(endpoint) + .await + .map_err(|e| Error::Delete(*owner_id, *ng_id, peer_id, e))??) +} diff --git a/src/v4/network_group/endpoint.rs b/src/v4/network_group/endpoint.rs new file mode 100644 index 0000000..163347a --- /dev/null +++ b/src/v4/network_group/endpoint.rs @@ -0,0 +1,39 @@ +use std::net::{IpAddr, SocketAddr, ToSocketAddrs}; + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct ClientEndpoint { + #[serde(rename = "ngIp")] + ng_ip: IpAddr, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +#[serde(rename = "ngTerm")] +pub struct NetworkGroupTerm { + #[serde(rename = "host")] + host: IpAddr, + #[serde(rename = "port")] + port: u16, +} + +impl ToSocketAddrs for NetworkGroupTerm { + type Iter = std::option::IntoIter; + + fn to_socket_addrs(&self) -> std::io::Result { + (self.host, self.port).to_socket_addrs() + } +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct ServerEndpoint { + #[serde(rename = "ngTerm")] + ng_term: NetworkGroupTerm, + #[serde(rename = "publicTerm")] + public_term: NetworkGroupTerm, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +#[serde(untagged)] +pub enum Endpoint { + Client(ClientEndpoint), + Server(ServerEndpoint), +} diff --git a/src/v4/network_group/external_peer.rs b/src/v4/network_group/external_peer.rs new file mode 100644 index 0000000..27e372f --- /dev/null +++ b/src/v4/network_group/external_peer.rs @@ -0,0 +1,21 @@ +use crate::v4::network_group::{ + endpoint::Endpoint, peer_id::PeerId, wireguard::WireGuardPublicKey, +}; + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct ExternalPeer { + #[serde(rename = "id")] + peer_id: PeerId, + #[serde(rename = "label")] + label: Option, + #[serde(rename = "publicKey")] + public_key: WireGuardPublicKey, + #[serde(rename = "endpoint")] + endpoint: Endpoint, + #[serde(rename = "hostname")] + hostname: String, + #[serde(rename = "parentMember")] + parent_member: String, + #[serde(rename = "parentEvent")] + parent_event: Option, +} diff --git a/src/v4/network_group/member.rs b/src/v4/network_group/member.rs new file mode 100644 index 0000000..1e7dd1d --- /dev/null +++ b/src/v4/network_group/member.rs @@ -0,0 +1,68 @@ +use oauth10a::rest::RestClient; + +use crate::{ + Client, EndpointError, RestError, + v4::{ + ErrorResponse, + network_group::{ + member_id::MemberId, member_kind::MemberKind, network_group_id::NetworkGroupId, + owner_id::OwnerId, + }, + }, +}; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + Endpoint(#[from] EndpointError), + #[error( + "failed to post Wannabe Network Group member for owner '{0}', network group '{1}', {0}" + )] + Post(OwnerId, NetworkGroupId, RestError), + #[error("failed to get Network Group member for owner '{0}', network group '{1}', {0}")] + Get(OwnerId, NetworkGroupId, RestError), + #[error(transparent)] + Status(#[from] ErrorResponse), +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct Member { + #[serde(rename = "id")] + member_id: MemberId, + #[serde(rename = "label")] + label: String, + #[serde(rename = "domainName")] + domain_name: String, + #[serde(rename = "kind")] + kind: MemberKind, +} + +impl Member { + /// Get a Member of a Network Group. + pub async fn get( + client: &Client, + owner_id: &OwnerId, + ng_id: NetworkGroupId, + member_id: MemberId, + ) -> Result { + let endpoint = client.endpoint(format_args!( + "/v4/networkgroups/organisations/{owner_id}/networkgroups/{ng_id}/members/{member_id}" + ))?; + + debug!( + %endpoint, + owner = %owner_id, + network_group = %ng_id, + "execute a request to add member to Network Group" + ); + + let response: Self = client + .get(endpoint) + .await + .map_err(|e| Error::Get(owner_id.to_owned(), ng_id, e))??; + + debug_assert_eq!(response.member_id, member_id); + + Ok(response) + } +} diff --git a/src/v4/network_group/member_id.rs b/src/v4/network_group/member_id.rs new file mode 100644 index 0000000..363073f --- /dev/null +++ b/src/v4/network_group/member_id.rs @@ -0,0 +1,78 @@ +use core::fmt; +use core::str::FromStr; + +use uuid::Uuid; + +use crate::v4::network_group::{member_kind::MemberKind, owner_id::UnknownVariant}; + +// MEMBER ID PARSE ERROR KIND ////////////////////////////////////////////////// + +#[derive(Debug, thiserror::Error)] +pub enum MemberIdParseErrorKind { + #[error("missing prefix")] + MissingPrefix, + #[error(transparent)] + Prefix(UnknownVariant), + #[error("invalid UUID, {0}")] + Uuid(uuid::Error), +} + +// MEMBER ID PARSE ERROR /////////////////////////////////////////////////////// + +#[derive(Debug, thiserror::Error)] +#[error("invalid owner identifier: {kind}")] +pub struct MemberIdParseError { + kind: MemberIdParseErrorKind, +} + +// MEMBER ID //////////////////////////////////////////////////////////////://// + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct MemberId { + kind: MemberKind, + uuid: Uuid, +} + +impl fmt::Display for MemberId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}_{}", self.kind.as_prefix(), self.uuid.as_hyphenated()) + } +} + +impl FromStr for MemberId { + type Err = MemberIdParseError; + + fn from_str(s: &str) -> Result { + let kind = match s.split_once('_') { + Some((prefix, s)) => match ( + MemberKind::from_prefix(prefix), + s.parse::(), + ) { + (Ok(kind), Ok(hyphenated)) => { + return Ok(Self { + kind, + uuid: hyphenated.into_uuid(), + }); + } + (Err(e), _) => MemberIdParseErrorKind::Prefix(e), + (_, Err(e)) => MemberIdParseErrorKind::Uuid(e), + }, + None => MemberIdParseErrorKind::MissingPrefix, + }; + Err(MemberIdParseError { kind }) + } +} + +impl serde::Serialize for MemberId { + fn serialize(&self, serializer: S) -> Result { + self.to_string().serialize(serializer) + } +} + +impl<'de> serde::Deserialize<'de> for MemberId { + fn deserialize>(deserializer: D) -> Result { + String::deserialize(deserializer)? + .parse() + .map_err(serde::de::Error::custom) + } +} diff --git a/src/v4/network_group/member_kind.rs b/src/v4/network_group/member_kind.rs new file mode 100644 index 0000000..6aec41b --- /dev/null +++ b/src/v4/network_group/member_kind.rs @@ -0,0 +1,38 @@ +// MEMBER KIND ///////////////////////////////////////////////////////////////// + +use crate::v4::network_group::owner_id::UnknownVariant; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize)] +pub enum MemberKind { + #[serde(rename = "ADDON")] + Addon, + #[serde(rename = "APPLICATION")] + Application, + #[serde(rename = "EXTERNAL")] + External, +} + +impl MemberKind { + pub const fn as_prefix(&self) -> &'static str { + match self { + Self::Addon => "addon", + Self::Application => "app", + Self::External => "external", + } + } + + pub fn from_prefix(prefix: &str) -> Result { + Ok(match prefix { + "addon" => Self::Addon, + "app" => Self::Application, + "external" => Self::External, + _ => { + return Err(UnknownVariant { + name: "member kind", + variants: &["addon", "app", "external"], + found: Some(prefix.into()), + }); + } + }) + } +} diff --git a/src/v4/network_group/mod.rs b/src/v4/network_group/mod.rs new file mode 100755 index 0000000..7ad9ec7 --- /dev/null +++ b/src/v4/network_group/mod.rs @@ -0,0 +1,51 @@ +//! # Network Group module +//! +//! This module provide structures and helpers to interact with Clever Cloud's +//! Network Group API. + +use std::net::IpAddr; + +use cidr::IpCidr; + +use crate::v4::network_group::{ + clever_peer::CleverPeer, network_group_id::NetworkGroupId, owner_id::OwnerId, +}; + +pub mod clever_peer; +pub mod component; +pub mod delete; +pub mod endpoint; +pub mod external_peer; +pub mod member; +pub mod member_id; +pub mod member_kind; +pub mod network_group_id; +pub mod owner_id; +pub mod peer_id; +pub mod peer_kind; +pub mod wannabe_external_peer; +pub mod wannabe_member; +pub mod wireguard; +pub mod wireguard_configuration; + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct NetworkGroup { + #[serde(rename = "id")] + pub ng_id: NetworkGroupId, + #[serde(rename = "ownerId")] + pub owner_id: OwnerId, + #[serde(rename = "label")] + pub label: String, + #[serde(rename = "description", default)] + pub description: Option, + #[serde(rename = "networkIp")] + pub ng_ip: IpCidr, + #[serde(rename = "lastAllocatedIp")] + pub last_allocated_ip: IpAddr, + #[serde(rename = "tags", default)] + pub tags: Vec, + #[serde(rename = "peers", default)] + pub peers: Vec, +} + +// /v4/networkgroups/organisations/{ownerId}/networkgroups/{networkGroupId} diff --git a/src/v4/network_group/network_group_id.rs b/src/v4/network_group/network_group_id.rs new file mode 100755 index 0000000..63ff01a --- /dev/null +++ b/src/v4/network_group/network_group_id.rs @@ -0,0 +1,81 @@ +use core::{fmt, str}; +use std::path::PathBuf; + +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +// NETWORK GROUP ID PARSE ERROR //////////////////////////////////////////////// + +#[derive(Debug, thiserror::Error)] +enum NetworkGroupIdParseErrorKind { + #[error("expected `ng_` prefix")] + MissingPrefix, + #[error("invalid UUID, {0}")] + Uuid(uuid::Error), +} + +#[derive(Debug, thiserror::Error)] +#[error("invalid network group identifier: {kind}")] +pub struct NetworkGroupIdParseError { + kind: NetworkGroupIdParseErrorKind, +} + +// NETWORK GROUP ID //////////////////////////////////////////////////////////// + +/// A network group identifier. +/// +/// It is represented as an hyphenate lowercased UUID prefixed with `ng_`. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct NetworkGroupId(Uuid); + +impl NetworkGroupId { + /// Returns the name of the file in which the peer identifier is stored. + pub fn ng_info_file_name(&self) -> PathBuf { + PathBuf::from(self.to_string()).with_extension("id") + } + + /// Returns the associated Wireguard interface name. + pub fn wg_interface_name(&self) -> String { + // requirement: `^[a-zA-Z0-9_=+.-]{1,15}$`. + let time_low = &self.0.to_string()[..8]; + format!("wgcc{time_low}") + } +} + +impl str::FromStr for NetworkGroupId { + type Err = NetworkGroupIdParseError; + + fn from_str(s: &str) -> Result { + match s.strip_prefix("ng_") { + Some(s) => match s.parse::() { + Ok(hyphenated) => Ok(Self(hyphenated.into_uuid())), + Err(e) => Err(NetworkGroupIdParseError { + kind: NetworkGroupIdParseErrorKind::Uuid(e), + }), + }, + None => Err(NetworkGroupIdParseError { + kind: NetworkGroupIdParseErrorKind::MissingPrefix, + }), + } + } +} + +impl fmt::Display for NetworkGroupId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "ng_{}", self.0.as_hyphenated()) + } +} + +impl<'de> Deserialize<'de> for NetworkGroupId { + fn deserialize>(deserializer: D) -> Result { + String::deserialize(deserializer)? + .parse() + .map_err(serde::de::Error::custom) + } +} + +impl Serialize for NetworkGroupId { + fn serialize(&self, serializer: S) -> Result { + self.to_string().serialize(serializer) + } +} diff --git a/src/v4/network_group/owner_id.rs b/src/v4/network_group/owner_id.rs new file mode 100644 index 0000000..3cab726 --- /dev/null +++ b/src/v4/network_group/owner_id.rs @@ -0,0 +1,187 @@ +use core::fmt; +use std::str::FromStr; + +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +// UNKNOWN VARIANT ERROR /////////////////////////////////////////////////////// + +#[derive(Debug, Clone)] +pub struct UnknownVariant { + pub name: &'static str, + pub variants: &'static [&'static str], + pub found: Option>, +} + +impl fmt::Display for UnknownVariant { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let Self { + name, + variants, + ref found, + } = *self; + + if let Some(found) = found { + write!(f, "unknown {name} `{found}`")?; + } else { + write!(f, "missing {name}")?; + } + + if let Some((tail, variants)) = variants.split_last() { + f.write_str(", expected ")?; + let mut variants = variants.iter(); + if let Some(head) = variants.next() { + write!(f, "one of `{head}`")?; + for variant in variants { + write!(f, ", `{variant}`")?; + } + write!(f, " or ")?; + } + write!(f, "`{tail}`")?; + } + + Ok(()) + } +} + +impl core::error::Error for UnknownVariant {} + +// OWNER KIND ////////////////////////////////////////////////////////////////// + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum OwnerKind { + User, + Organisation, +} + +impl OwnerKind { + pub const fn as_str(&self) -> &'static str { + match self { + Self::User => "user", + Self::Organisation => "orga", + } + } + + /// Returns `true` if the owner kind is [`User`]. + /// + /// [`User`]: OwnerKind::User + #[must_use] + pub const fn is_user(&self) -> bool { + matches!(self, Self::User) + } + + /// Returns `true` if the owner kind is [`Organisation`]. + /// + /// [`Organisation`]: OwnerKind::Organisation + #[must_use] + pub const fn is_organisation(&self) -> bool { + matches!(self, Self::Organisation) + } +} + +impl fmt::Display for OwnerKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} + +impl FromStr for OwnerKind { + type Err = UnknownVariant; + + fn from_str(s: &str) -> Result { + Ok(match s.to_ascii_lowercase().as_str() { + "user" => Self::User, + "orga" => Self::Organisation, + _ => { + return Err(UnknownVariant { + name: "owner kind", + variants: &["user", "kind"], + found: Some(s.into()), + }); + } + }) + } +} + +// OWNER ID PARSE ERROR KIND /////////////////////////////////////////////////// + +#[derive(Debug, thiserror::Error)] +enum OwnerIdParseErrorKind { + #[error("missing prefix `ng_` prefix")] + MissingPrefix, + #[error(transparent)] + Prefix(UnknownVariant), + #[error("invalid UUID, {0}")] + Uuid(uuid::Error), +} + +// OWNER ID PARSE ERROR //////////////////////////////////////////////////////// + +#[derive(Debug, thiserror::Error)] +#[error("invalid owner identifier: {kind}")] +pub struct OwnerIdParseError { + kind: OwnerIdParseErrorKind, +} + +// OWNER ID //////////////////////////////////////////////////////////////// + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct OwnerId { + kind: OwnerKind, + uuid: Uuid, +} + +impl OwnerId { + pub const fn kind(&self) -> OwnerKind { + self.kind + } + + pub const fn is_user(&self) -> bool { + self.kind.is_user() + } + + pub const fn is_organisation(&self) -> bool { + self.kind.is_organisation() + } +} + +impl fmt::Display for OwnerId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}_{}", self.kind, self.uuid.as_hyphenated()) + } +} + +impl FromStr for OwnerId { + type Err = OwnerIdParseError; + + fn from_str(s: &str) -> Result { + let kind = match s.split_once('_') { + Some((prefix, s)) => match (prefix.parse(), s.parse::()) { + (Ok(kind), Ok(hyphenated)) => { + return Ok(Self { + kind, + uuid: hyphenated.into_uuid(), + }); + } + (Err(e), _) => OwnerIdParseErrorKind::Prefix(e), + (_, Err(e)) => OwnerIdParseErrorKind::Uuid(e), + }, + None => OwnerIdParseErrorKind::MissingPrefix, + }; + Err(OwnerIdParseError { kind }) + } +} + +impl Serialize for OwnerId { + fn serialize(&self, serializer: S) -> Result { + self.to_string().serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for OwnerId { + fn deserialize>(deserializer: D) -> Result { + String::deserialize(deserializer)? + .parse() + .map_err(serde::de::Error::custom) + } +} diff --git a/src/v4/network_group/peer_id.rs b/src/v4/network_group/peer_id.rs new file mode 100644 index 0000000..cc6d706 --- /dev/null +++ b/src/v4/network_group/peer_id.rs @@ -0,0 +1,96 @@ +use core::{fmt, str}; + +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::v4::network_group::{owner_id::UnknownVariant, peer_kind::PeerKind}; + +// PEER ID PARSE ERROR KIND //////////////////////////////////////////////////// + +#[derive(Debug, thiserror::Error)] +enum PeerIdParseErrorKind { + #[error(transparent)] + PeerKind(UnknownVariant), + #[error("invalid UUID, {0}")] + Uuid(uuid::Error), +} + +// PEER ID PARSE ERROR ///////////////////////////////////////////////////////// + +#[derive(Debug, thiserror::Error)] +#[error("invalid peer identifier: {kind}")] +pub struct PeerIdParseError { + kind: PeerIdParseErrorKind, +} + +// PEER ID ///////////////////////////////////////////////////////////////////// + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +pub struct PeerId { + peer_kind: PeerKind, + uuid: Uuid, +} + +impl PeerId { + pub const fn peer_kind(&self) -> PeerKind { + self.peer_kind + } + + pub const fn uuid(&self) -> &Uuid { + &self.uuid + } + + pub const fn is_external(&self) -> bool { + self.peer_kind.is_external() + } +} + +impl fmt::Display for PeerId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(prefix) = self.peer_kind.prefix() { + write!(f, "{prefix}_")?; + } + write!(f, "{}", self.uuid.as_hyphenated()) + } +} + +impl str::FromStr for PeerId { + type Err = PeerIdParseError; + + fn from_str(s: &str) -> Result { + let (peer_kind, s) = match s.split_once('_') { + None => (PeerKind::default(), s), + Some((prefix, s)) => match PeerKind::from_prefix(prefix) { + Ok(peer_kind) => (peer_kind, s), + Err(e) => { + return Err(PeerIdParseError { + kind: PeerIdParseErrorKind::PeerKind(e), + }); + } + }, + }; + match s.parse::() { + Ok(hyphenated) => Ok(Self { + uuid: hyphenated.into_uuid(), + peer_kind, + }), + Err(e) => Err(PeerIdParseError { + kind: PeerIdParseErrorKind::Uuid(e), + }), + } + } +} + +impl<'de> Deserialize<'de> for PeerId { + fn deserialize>(deserializer: D) -> Result { + String::deserialize(deserializer)? + .parse() + .map_err(serde::de::Error::custom) + } +} + +impl Serialize for PeerId { + fn serialize(&self, serializer: S) -> Result { + self.to_string().serialize(serializer) + } +} diff --git a/src/v4/network_group/peer_kind.rs b/src/v4/network_group/peer_kind.rs new file mode 100644 index 0000000..4aae1b9 --- /dev/null +++ b/src/v4/network_group/peer_kind.rs @@ -0,0 +1,39 @@ +// PEER KIND /////////////////////////////////////////////////////////////////// + +use crate::v4::network_group::owner_id::UnknownVariant; + +#[derive( + Debug, Default, Clone, Copy, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize, +)] +pub enum PeerKind { + #[default] + #[serde(rename = "CLEVER")] + Clever, + #[serde(rename = "EXTERNAL")] + External, +} + +impl PeerKind { + /// Returns `true` if the peer kind is [`External`](PeerKind::External). + pub const fn is_external(&self) -> bool { + matches!(self, Self::External) + } + + pub const fn prefix(self) -> Option<&'static str> { + match self { + Self::Clever => None, + Self::External => Some("external"), + } + } + + pub fn from_prefix(prefix: &str) -> Result { + match prefix { + "external" => Ok(Self::External), + _ => Err(UnknownVariant { + name: "peer kind", + variants: &["external"], + found: Some(prefix.into()), + }), + } + } +} diff --git a/src/v4/network_group/wannabe_external_peer.rs b/src/v4/network_group/wannabe_external_peer.rs new file mode 100755 index 0000000..e2120bc --- /dev/null +++ b/src/v4/network_group/wannabe_external_peer.rs @@ -0,0 +1,106 @@ +use core::net::IpAddr; + +use oauth10a::rest::RestClient; +use serde::{Deserialize, Serialize}; + +use crate::{ + Client, EndpointError, RestError, + v4::{ + ErrorResponse, + network_group::{ + member_id::MemberId, network_group_id::NetworkGroupId, owner_id::OwnerId, + peer_id::PeerId, wireguard::WireGuardPublicKey, + }, + }, +}; + +// PEER ROLE /////////////////////////////////////////////////////////////////// + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum PeerRole { + #[serde(rename = "CLIENT")] + Client, + #[serde(rename = "SERVER")] + Server, +} + +// PEER CREATED //////////////////////////////////////////////////////////////// + +/// Response from [`WannabePeer`] and [`WannabeExternalPeer`] requests. +#[derive(Debug, Deserialize)] +pub struct PeerCreated { + #[serde(rename = "peerId")] + pub peer_id: PeerId, +} + +// ERROR /////////////////////////////////////////////////////////////////////// + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + Endpoint(#[from] EndpointError), + #[error("failed to post Wannabe External Peer for owner '{0}', network group '{1}', {0}")] + Post(OwnerId, NetworkGroupId, RestError), + #[error(transparent)] + Status(#[from] ErrorResponse), +} + +// WANNABE EXTERNAL PEER /////////////////////////////////////////////////////// + +/// Request to join a Network Group as an external peer. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct WannabeExternalPeer { + /// Label of a Network Group peer. + #[serde(rename = "label")] + pub label: String, + /// The public IP v4 or v6 on which the peer is listening, + /// when `peer_role` is [`PeerRole::Server`]. + #[serde(rename = "ip", skip_serializing_if = "Option::is_none")] + pub ip: Option, + /// The public TCP or UDP port number on which the peer is listening, + /// when `peer_role` is [`PeerRole::Server`]. + #[serde(rename = "port", skip_serializing_if = "Option::is_none")] + pub port: Option, + /// The role of this peer in the Network Group. + #[serde(rename = "peerRole")] + pub peer_role: PeerRole, + /// Base64-encoded WireGuard public key of the peer. + #[serde(rename = "publicKey")] + pub public_key: WireGuardPublicKey, + /// Host name of the peer. + #[serde(rename = "hostname", skip_serializing_if = "Option::is_none")] + pub hostname: Option, + /// Event that created the peer within the Network Group. + #[serde(rename = "parentEvent", skip_serializing_if = "Option::is_none")] + pub parent_event: Option, + /// Unique ID of a Network Group member. + #[serde(rename = "parentMember")] + pub parent_member: MemberId, +} + +impl WannabeExternalPeer { + pub async fn post( + &self, + client: &Client, + owner_id: &OwnerId, + ng_id: &NetworkGroupId, + ) -> Result { + let endpoint = client.endpoint(format_args!( + "/v4/networkgroups/organisations/{owner_id}/networkgroups/{ng_id}/external-peers" + ))?; + + error!("{}", serde_json::to_string(&self).unwrap()); + + debug!( + %endpoint, + owner = %owner_id, + network_group = %ng_id, + "execute a request to join Network Group" + ); + + Ok(client + .post(endpoint, self) + .await + .map_err(|e| Error::Post(*owner_id, *ng_id, e))??) + } +} diff --git a/src/v4/network_group/wannabe_member.rs b/src/v4/network_group/wannabe_member.rs new file mode 100644 index 0000000..2936666 --- /dev/null +++ b/src/v4/network_group/wannabe_member.rs @@ -0,0 +1,62 @@ +use oauth10a::rest::RestClient; + +use crate::{ + Client, EndpointError, RestError, + v4::{ + ErrorResponse, + network_group::{ + member_id::MemberId, member_kind::MemberKind, network_group_id::NetworkGroupId, + owner_id::OwnerId, + }, + }, +}; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + Endpoint(#[from] EndpointError), + #[error( + "failed to post Wannabe Network Group member for owner '{0}', network group '{1}', {0}" + )] + Post(OwnerId, NetworkGroupId, RestError), + #[error(transparent)] + Status(#[from] ErrorResponse), +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct WannabeNetworkGroupMember { + #[serde(rename = "id")] + member_id: MemberId, + #[serde(rename = "label", default, skip_serializing_if = "Option::is_none")] + label: Option, + #[serde(rename = "domainName")] + domain_name: String, + #[serde(rename = "kind")] + kind: MemberKind, +} + +impl WannabeNetworkGroupMember { + /// Add a Member to a Network Group. + pub async fn post( + &self, + client: &Client, + owner_id: &OwnerId, + ng_id: NetworkGroupId, + ) -> Result<(), Error> { + let endpoint = client.endpoint(format_args!( + "/v4/networkgroups/organisations/{owner_id}/networkgroups/{ng_id}/members" + ))?; + + debug!( + %endpoint, + owner = %owner_id, + network_group = %ng_id, + "execute a request to add member to Network Group" + ); + + Ok(client + .post(endpoint, self) + .await + .map_err(|e| Error::Post(*owner_id, ng_id, e))??) + } +} diff --git a/src/v4/network_group/wireguard.rs b/src/v4/network_group/wireguard.rs new file mode 100755 index 0000000..8ff8d33 --- /dev/null +++ b/src/v4/network_group/wireguard.rs @@ -0,0 +1,194 @@ +use core::{cmp, fmt, hash}; +use std::{io, process::Command}; + +use base64::Engine; +use rand_core::{OsError, OsRng, TryCryptoRng}; +use serde::{Deserialize, Serialize}; +use x25519_dalek::{PublicKey, StaticSecret}; +use zeroize::Zeroizing; + +static REDACTED: &str = ""; + +// WIREGUARD PRIVATE KEY ERROR ///////////////////////////////////////////////// + +/// Error related to [`WireGuardPrivateKey`]. +#[derive(Debug, thiserror::Error)] +pub enum WireGuardPrivateKeyError { + #[error("failed to decode base64-encoded private key, {0}")] + Base64(#[source] base64::DecodeError), + #[error("private key is {0} bytes when it must be exactly 32 bytes")] + InvalidLength(usize), +} + +// WIREGUARD PRIVATE KEY /////////////////////////////////////////////////////// + +/// WireGuard private key. +/// +/// This is a thin wrapper around a Diffie-Hellman secret key as used by WireGuard. +/// +/// * [`serde::Deserialize`] implementation based on the **base64-encoded string** representation +/// * redacted [`fmt::Debug`] and [`fmt::Display`] implementations to prevent +/// accidentally leaking the secret (e.g. in logs) +/// * [`Zeroize`](zeroize::Zeroize) on [`Drop`] +#[derive(Clone)] +pub struct WireGuardPrivateKey(Zeroizing); + +impl WireGuardPrivateKey { + /// Generates a new private key using the given random number generator. + /// + /// # Errors + /// + /// * If `rng` fails to produce 32 random bytes. + pub fn new(rng: &mut R) -> Result { + let mut buf = [0; 32]; + match rng.try_fill_bytes(&mut buf) { + Err(error) => Err(error), + Ok(()) => Ok(Self(Zeroizing::new(StaticSecret::from(buf)))), + } + } + + /// Generates a new private key using operating system's random data source. + /// + /// # Errors + /// + /// * if [`OsRng`] fails to produce 32 random bytes. + pub fn from_os_rng() -> Result { + Self::new(&mut OsRng) + } + + /// Creates a new private key from base64-encoded bytes. + /// + /// # Errors + /// + /// * If decoding the base64-encoded output fails + /// * If decoded output is not 32 bytes long + pub fn from_base64( + input: &(impl ?Sized + AsRef<[u8]>), + ) -> Result { + match base64::engine::general_purpose::STANDARD.decode(input) { + Err(error) => Err(WireGuardPrivateKeyError::Base64(error)), + Ok(bytes) => match <[u8; 32]>::try_from(bytes) { + Err(error) => Err(WireGuardPrivateKeyError::InvalidLength(error.len())), + Ok(array) => Ok(Self(Zeroizing::new(StaticSecret::from(array)))), + }, + } + } + + /// Exposes the private key in a base64-encoded string. + pub fn to_base64(&self) -> Zeroizing { + Zeroizing::new(base64::engine::general_purpose::STANDARD.encode(self.0.as_bytes())) + } + + /// Generates a new private key using `wg genkey` command in a child process. + /// + /// Requires WireGuard command-line interface (`wg`). + /// + /// # Errors + /// + /// * If `wg` command fails + /// * If decoding the base64-encoded output fails + /// * If decoded output is not 32 bytes long + pub fn from_wg_genkey() -> io::Result { + match Command::new("wg").arg("genkey").output() { + Err(error) => Err(error), + Ok(output) => match Self::from_base64(output.stdout.trim_ascii()) { + Err(error) => Err(io::Error::new(io::ErrorKind::InvalidData, error)), + Ok(private_key) => Ok(private_key), + }, + } + } + + /// Returns the associated public key. + pub fn public_key(&self) -> WireGuardPublicKey { + WireGuardPublicKey(PublicKey::from(&*self.0)) + } +} + +impl From<[u8; 32]> for WireGuardPrivateKey { + fn from(value: [u8; 32]) -> Self { + Self(Zeroizing::new(StaticSecret::from(value))) + } +} + +impl TryFrom<&str> for WireGuardPrivateKey { + type Error = WireGuardPrivateKeyError; + + fn try_from(value: &str) -> Result { + Self::from_base64(value) + } +} + +impl<'de> Deserialize<'de> for WireGuardPrivateKey { + fn deserialize>(deserializer: D) -> Result { + let base64_encoded = String::deserialize(deserializer)?; + Self::from_base64(&base64_encoded).map_err(serde::de::Error::custom) + } +} + +// REDACTED implementations + +impl fmt::Display for WireGuardPrivateKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(REDACTED, f) + } +} + +impl fmt::Debug for WireGuardPrivateKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(REDACTED, f) + } +} + +impl hash::Hash for WireGuardPrivateKey { + fn hash(&self, state: &mut H) { + REDACTED.hash(state); + } +} + +impl PartialEq for WireGuardPrivateKey { + fn eq(&self, _other: &Self) -> bool { + false + } +} + +impl PartialOrd for WireGuardPrivateKey { + fn partial_cmp(&self, _other: &Self) -> Option { + None + } +} + +impl Serialize for WireGuardPrivateKey { + fn serialize(&self, serializer: S) -> Result { + REDACTED.serialize(serializer) + } +} + +// WIREGUARD PUBLIC KEY //////////////////////////////////////////////////////// + +/// WireGuard public key. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct WireGuardPublicKey(pub PublicKey); + +impl Serialize for WireGuardPublicKey { + fn serialize(&self, serializer: S) -> Result { + base64::engine::general_purpose::STANDARD + .encode(self.0) + .serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for WireGuardPublicKey { + fn deserialize>(deserializer: D) -> Result { + let base64_encoded = String::deserialize(deserializer)?; + let decoded = base64::engine::general_purpose::STANDARD + .decode(&base64_encoded) + .map_err(serde::de::Error::custom)?; + let data = <[u8; 32]>::try_from(decoded).map_err(|error| { + serde::de::Error::custom(format!( + "invalid wireguard public key: length is {} bytes when it must be exactly 32 bytes", + error.len() + )) + })?; + Ok(Self(PublicKey::from(data))) + } +} diff --git a/src/v4/network_group/wireguard_configuration.rs b/src/v4/network_group/wireguard_configuration.rs new file mode 100755 index 0000000..3dc7189 --- /dev/null +++ b/src/v4/network_group/wireguard_configuration.rs @@ -0,0 +1,167 @@ +use core::{net::IpAddr, str}; + +use base64::Engine; +use oauth10a::{ + rest::RestClient, + sse::{Json, SseClient}, +}; +use serde::{Deserialize, Serialize}; + +use crate::{Client, EndpointError, RestError, v4::ErrorResponse}; + +use super::{network_group_id::NetworkGroupId, owner_id::OwnerId, peer_id::PeerId}; + +// ERROR /////////////////////////////////////////////////////////////////////// + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + Endpoint(#[from] EndpointError), + #[error("failed to get WireGuard configuration for owner '{0}', network group '{1}', {0}")] + Get(OwnerId, NetworkGroupId, RestError), + #[error("failed to build SSE stream, {0}")] + Sse(#[from] Box), + #[error(transparent)] + Status(#[from] ErrorResponse), + #[error("invalid network group identifier, expected {expected}, found {found}")] + NetworkGroupIdMismatch { + expected: NetworkGroupId, + found: NetworkGroupId, + }, +} + +impl From for Error { + fn from(value: WireGuardConfigurationStreamError) -> Self { + Self::Sse(Box::new(value)) + } +} + +// PEER VIEW /////////////////////////////////////////////////////////////////// + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PeerView { + /// the uuid of the instance + #[serde(rename = "peer_id")] + pub peer_id: PeerId, + /// typically "10.101.2.4" + #[serde(rename = "peer_ip")] + pub peer_ip: IpAddr, + /// typically "app_something.m.ng_abcdefg.ng.clever-cloud.com" + #[serde(rename = "peer_hostname")] + pub peer_hostname: String, +} + +// RAW WIREGUARD CONFIG //////////////////////////////////////////////////////// + +/// WireGuard configuration decoded from Base64. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RawWireGuardConfig(pub String); + +impl RawWireGuardConfig { + pub const PREFIX: &'static str = "\n####networkgroups start\n"; + pub const SUFFIX: &'static str = "\n####networkgroups end\n"; + pub const WG_PRIVATE_KEY_PLACEHOLDER: &'static str = "<%PrivateKey%>"; +} + +impl AsRef for RawWireGuardConfig { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl Serialize for RawWireGuardConfig { + fn serialize(&self, serializer: S) -> Result { + base64::engine::general_purpose::STANDARD + .encode(&self.0) + .serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for RawWireGuardConfig { + fn deserialize>(deserializer: D) -> Result { + let base64_encoded = String::deserialize(deserializer)?; + let decoded = base64::engine::general_purpose::STANDARD + .decode(&base64_encoded) + .map_err(serde::de::Error::custom)?; + let decoded_utf8 = str::from_utf8(&decoded).map_err(serde::de::Error::custom)?; + Ok(Self(decoded_utf8.trim().to_owned())) + } +} + +// WIREGUARD CONFIGURATION ///////////////////////////////////////////////////// + +pub type WireGuardConfigurationStreamBuilder = + oauth10a::sse::SseStreamBuilder>; + +pub type WireGuardConfigurationStream = + oauth10a::sse::SseStream>; + +pub type WireGuardConfigurationStreamError = + oauth10a::sse::SseErrorOf>; + +/// Event generated by API on configuration updates. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct WireGuardConfiguration { + #[serde(rename = "ngId")] + pub ng_id: NetworkGroupId, + #[serde(rename = "peerId")] + pub peer_id: PeerId, + #[serde(rename = "peers")] + pub peers: Vec, + #[serde(rename = "version")] + pub version: i32, + #[serde(rename = "configuration")] + pub configuration: RawWireGuardConfig, +} + +impl WireGuardConfiguration { + pub async fn get( + client: &Client, + owner_id: &OwnerId, + ng_id: &NetworkGroupId, + ) -> Result { + let endpoint = client.endpoint(format_args!( + "v4/networkgroups/organisations/{owner_id}/networkgroups/{ng_id}/external-peers" + ))?; + + debug!( + %endpoint, + owner = %owner_id, + network_group = %ng_id, + "execute a request to get WireGuard configuration" + ); + + let wg_configuration: WireGuardConfiguration = client + .get(endpoint) + .await + .map_err(|e| Error::Get(*owner_id, *ng_id, e))??; + + if wg_configuration.ng_id != *ng_id { + return Err(Error::NetworkGroupIdMismatch { + expected: *ng_id, + found: wg_configuration.ng_id, + }); + } + + Ok(wg_configuration) + } + + pub fn sse( + client: &Client, + owner_id: &OwnerId, + ng_id: &NetworkGroupId, + peer_id: &PeerId, + ) -> Result { + let endpoint = client.endpoint(format_args!("/v4/networkgroups/organisations/{owner_id}/networkgroups/{ng_id}/peers/{peer_id}/wireguard/configuration/stream"))?; + + debug!( + %endpoint, + owner = %owner_id, + network_group = %ng_id, + peer = %peer_id, + "create SSE stream builder to subscribe to WireGuard configuration's" + ); + + Ok(client.sse(endpoint)) + } +} From 688125548cf68ff5b33af7928cca0a0916f5c1ce Mon Sep 17 00:00:00 2001 From: Florentin Dubois Date: Wed, 2 Jul 2025 16:27:18 +0200 Subject: [PATCH 3/3] chore: remove clever-env and add helpers on clever-tools Signed-off-by: Florentin Dubois --- Cargo.toml | 2 +- src/clever_env.rs | 222 -------------------------------------------- src/clever_tools.rs | 65 ++++++++----- src/lib.rs | 23 +---- 4 files changed, 48 insertions(+), 264 deletions(-) delete mode 100644 src/clever_env.rs diff --git a/Cargo.toml b/Cargo.toml index 8047ac1..cb54f79 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,7 +51,7 @@ x25519-dalek = { version = "^2.0.1", features = [ "static_secrets", ], optional = true } zeroize = { version = "^1.8.1", features = ["serde", "derive"] } -env-capture = { git = "https://gitlab.corp.clever-cloud.com/CedricLG/env-capture", features = [ +env-capture = { path = "../env-capture", features = [ "serde", ] } diff --git a/src/clever_env.rs b/src/clever_env.rs deleted file mode 100644 index 35534eb..0000000 --- a/src/clever_env.rs +++ /dev/null @@ -1,222 +0,0 @@ -use std::{borrow::Cow, fs, io, path::Path}; - -use env_capture::{Env, IgnoreAsciiCase}; -use oauth10a::{credentials::Credentials, url::Url}; - -use crate::{ - DEFAULT_SSH_GATEWAY, PartialCredentials, - clever_tools::{CleverTools, CleverToolsConfig, CleverToolsConfigError}, - default_api_host, default_auth_bridge_host, -}; - -// PARTIAL OAUTH ERROR ///////////////////////////////////////////////////////// - -#[derive(Debug, thiserror::Error)] -pub enum PartialOAuthError { - #[error("missing token")] - Token, - #[error("missing secret")] - Secret, - #[error("missing consumer token")] - ConsumerKey, - #[error("missing consumer secret")] - ConsumerSecret, -} - -// CLEVER ENV ////////////////////////////////////////////////////////////////// - -#[derive(Debug, thiserror::Error)] -pub enum CleverEnvError { - #[error("failed to capture environnement, {0}")] - Capture(#[from] env_capture::Error), - #[error(transparent)] - CleverToolsConfigFile(#[from] CleverToolsConfigError), - #[error("partial OAuth credentials: {0}")] - PartialOAuth(#[from] PartialOAuthError), - #[error("failed to create configuration directory")] - ConfigDir(io::Error), -} - -/// Snapshot of the clever environment variables, hydrated with the OAuth -/// configuration from the main configuration file of the `clever-tools`, if any. -#[derive(Debug, serde::Deserialize)] -pub struct CleverEnv { - #[serde(rename = "API_HOST")] - pub(crate) api_host: Option, - #[serde(rename = "AUTH_BRIDGE_API")] - pub(crate) auth_bridge_host: Option, - #[serde(rename = "SSH_GATEWAY")] - pub(crate) ssh_gateway: Option>, - #[serde(rename = "", flatten, default)] - pub(crate) credentials: Option, - #[serde(skip)] - pub(crate) config_dir: Option>, -} - -impl CleverEnv { - pub fn from_env() -> Result { - let env = Env::::from_env(); - - let mut env = env.with_prefix("CLEVER_").parse::()?; - - match &mut env.credentials { - credentials @ None => { - trace!("credentials not found in current process environment"); - - let config_dir = CleverToolsConfig::default_config_dir()?; - - let config_path = CleverToolsConfig::config_path_in(&config_dir); - - if config_path.exists() { - env.config_dir.replace(config_dir.into()); - - let CleverToolsConfig { - oauth_token, - oauth_secret, - } = CleverToolsConfig::from_path(&config_path)?; - - *credentials = Some(Credentials::OAuth1 { - token: oauth_token, - secret: oauth_secret, - consumer_key: None, - consumer_secret: None, - }); - - trace!("using credentials from `clever-tools` configuration file"); - } - } - Some(Credentials::OAuth1 { - consumer_key: Some(_), - consumer_secret: None, - .. - }) => { - return Err(CleverEnvError::PartialOAuth( - PartialOAuthError::ConsumerSecret, - )); - } - Some(Credentials::OAuth1 { - consumer_key: None, - consumer_secret: Some(_), - .. - }) => return Err(CleverEnvError::PartialOAuth(PartialOAuthError::ConsumerKey)), - Some(_) => trace!("using credentials from environment"), - } - - Ok(env) - } - - pub fn env_api_host(&self) -> Option<&Url> { - self.api_host.as_ref() - } - - pub fn api_host(&self) -> &Url { - match &self.api_host { - None => default_api_host(), - Some(v) => v, - } - } - - pub fn env_auth_bridge_host(&self) -> Option<&Url> { - self.auth_bridge_host.as_ref() - } - - pub fn auth_bridge_host(&self) -> &Url { - match &self.auth_bridge_host { - None => default_auth_bridge_host(), - Some(v) => v, - } - } - - pub fn env_ssh_gateway(&self) -> Option<&str> { - self.ssh_gateway.as_deref() - } - - pub const fn ssh_gateway(&self) -> &str { - match &self.ssh_gateway { - None => DEFAULT_SSH_GATEWAY, - Some(v) => v, - } - } - - pub const fn env_oauth_consumer_key(&self) -> Option<&str> { - match self.credentials { - Some(Credentials::OAuth1 { - consumer_key: Some(ref v), - .. - }) => Some(v), - _ => None, - } - } - - pub const fn oauth_consumer_key(&self) -> &str { - match self.env_oauth_consumer_key() { - Some(x) => x, - None => CleverTools::CONSUMER_KEY, - } - } - - pub const fn env_oauth_consumer_secret(&self) -> Option<&str> { - match self.credentials { - Some(Credentials::OAuth1 { - consumer_secret: Some(ref v), - .. - }) => Some(v), - _ => None, - } - } - - pub const fn oauth_consumer_secret(&self) -> &str { - match self.env_oauth_consumer_secret() { - Some(v) => v, - None => CleverTools::CONSUMER_SECRET, - } - } - - /// Returns the path to the directory where configuration files of clever apps are stored. - pub fn config_dir(&self) -> Result, CleverEnvError> { - let path = match self.config_dir { - Some(ref config_dir) => Cow::Borrowed(&**config_dir), - None => Cow::Owned(CleverToolsConfig::default_config_dir()?), - }; - - fs::create_dir_all(&*path).map_err(CleverEnvError::ConfigDir)?; - - Ok(path) - } - - pub fn credentials(&self) -> Option<&PartialCredentials> { - self.credentials.as_ref() - } -} - -#[cfg(test)] -mod tests { - use env_capture::set_tmp_var; - use oauth10a::credentials::Credentials; - - use crate::clever_env::CleverEnv; - - #[test] - fn test_env() { - let _ = unsafe { set_tmp_var("RUST_LOG", "trace") }; - - tracing_subscriber::fmt::fmt() - .with_level(true) - .with_line_number(true) - .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) - .init(); - - let _ = unsafe { set_tmp_var("CLEVER_TOKEN", "my_token") }; - let _ = unsafe { set_tmp_var("CLEVER_SECRET", "my_secret") }; - - if let Credentials::OAuth1 { - token, - secret, - consumer_key, - consumer_secret, - } = CleverEnv::from_env().unwrap().credentials().unwrap() - { - dbg!(token, secret, consumer_key, consumer_secret); - } - } -} diff --git a/src/clever_tools.rs b/src/clever_tools.rs index 49433a5..079739f 100644 --- a/src/clever_tools.rs +++ b/src/clever_tools.rs @@ -6,6 +6,49 @@ use std::{ path::{Path, PathBuf}, }; +// CLEVER TOOLS CONFIG ERROR /////////////////////////////////////////////////// + +#[derive(Debug, thiserror::Error)] +pub enum CleverToolsConfigError { + #[error("failed to resolve home directory")] + HomeDir, + #[error("failed to open clever-tools configuration file")] + Open(io::Error), + #[error("failed to read clever-tools configuration file's contents")] + Read(io::Error), + #[error("failed to parse clever-tools configuration file's contents")] + Json(serde_json::Error), + #[error("failed to resolve environment variable {0}")] + EnvironmentVariable(&'static str, env::VarError), +} + +// HELPERS ///////////////////////////////////////////////////////////////////// + +pub fn xdg_home_dir() -> Result { + env::var("XDG_DATA_HOME") + .map(PathBuf::from) + .map_err(|err| CleverToolsConfigError::EnvironmentVariable("XDG_DATA_HOME", err)) +} + +pub fn xdg_config_dir() -> Result { + env::var("XDG_CONFIG_HOME") + .map(PathBuf::from) + .map_err(|err| CleverToolsConfigError::EnvironmentVariable("XDG_CONFIG_HOME", err)) +} + +pub fn home_dir() -> Result { + env::home_dir() + .map(Ok) + .unwrap_or_else(xdg_home_dir) +} + +pub fn config_dir() -> Result { + Ok(match xdg_config_dir() { + Ok(config_dir) => PathBuf::from(config_dir), + Err(_) => home_dir()?.join(".config"), + }) +} + // CLEVER TOOLS //////////////////////////////////////////////////////////////// /// Default OAuth1 consumer. @@ -24,19 +67,6 @@ impl CleverTools { pub const CONSUMER_SECRET: &'static str = "MgVMqTr6fWlf2M0tkC2MXOnhfqBWDT"; } -// CLEVER TOOLS CONFIG ERROR /////////////////////////////////////////////////// - -#[derive(Debug, thiserror::Error)] -pub enum CleverToolsConfigError { - #[error("failed to resolve home directory")] - HomeDir, - #[error("failed to open clever-tools configuration file")] - Open(io::Error), - #[error("failed to read clever-tools configuration file's contents")] - Read(io::Error), - #[error("failed to parse clever-tools configuration file's contents")] - Json(serde_json::Error), -} // CLEVER TOOLS CONFIG ///////////////////////////////////////////////////////// @@ -48,14 +78,7 @@ pub struct CleverToolsConfig { pub oauth_secret: Box, } -fn config_dir() -> Result { - Ok(match env::var_os("XDG_CONFIG_HOME") { - Some(config_dir) => PathBuf::from(config_dir), - None => env::home_dir() - .ok_or(CleverToolsConfigError::HomeDir)? - .join(".config"), - }) -} + impl CleverToolsConfig { pub fn default_config_dir() -> Result { diff --git a/src/lib.rs b/src/lib.rs index ded9023..3e05f50 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,15 +12,11 @@ use oauth10a::{ reqwest::{self, Request, Response, Url}, }; -#[macro_use] -mod logging; - -pub mod clever_env; -use self::clever_env::{CleverEnv, CleverEnvError}; +use crate::clever_tools::CleverTools; +#[macro_use] +pub mod logging; pub mod clever_tools; -use self::clever_tools::CleverTools; - pub mod v2; pub mod v4; @@ -267,19 +263,6 @@ impl Client { } } - pub fn from_env_snapshot(env: &CleverEnv) -> Self { - Self::new( - reqwest::Client::new(), - env.credentials.clone(), - env.env_api_host().cloned(), - env.env_auth_bridge_host().cloned(), - ) - } - - pub fn from_env() -> Result { - Ok(Self::from_env_snapshot(&CleverEnv::from_env()?)) - } - /// Sets the credentials that will be used by this client to authorize subsequent HTTP requests. /// /// When `consumer_keys` and/or `consumer_secret` are missing, the client will