From f2acdc58fca5d084ef1d6d29b2ef381f3791c042 Mon Sep 17 00:00:00 2001 From: "Ch.-David Blot" Date: Wed, 7 Jan 2026 11:20:53 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20implement=20standard=2012-f?= =?UTF-8?q?actor=20profile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.toml | 5 +- examples/config_file.rs | 38 +-- examples/keypair.rs | 15 +- examples/region.rs | 19 +- examples/volume.rs | 15 +- gen.yml | 5 +- src/apis/configuration_file.rs | 164 ---------- src/apis/mod.rs | 2 +- src/apis/profile.rs | 498 +++++++++++++++++++++++++++++ templates/Cargo.mustache | 6 +- templates/configuration_file.rs | 164 ---------- templates/profile.rs | 498 +++++++++++++++++++++++++++++ templates/reqwest/api_mod.mustache | 2 +- 13 files changed, 1020 insertions(+), 411 deletions(-) delete mode 100644 src/apis/configuration_file.rs create mode 100644 src/apis/profile.rs delete mode 100644 templates/configuration_file.rs create mode 100644 templates/profile.rs diff --git a/Cargo.toml b/Cargo.toml index 40761f11..0ee56b13 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ description = "Outscale API SDK" repository = "https://github.com/outscale/osc-sdk-rust/" [dependencies] +base64 = { version = "0.22", optional = true } home = "0.5.3" rand = "~0.9" serde = "^1.0" @@ -27,5 +28,5 @@ default-features = false [features] default = ["reqwest/default-tls"] -native-tls = ["reqwest/native-tls"] -rustls-tls = ["reqwest/rustls-tls"] +native-tls = ["reqwest/native-tls", "dep:base64"] +rustls-tls = ["reqwest/rustls-tls", "dep:base64"] diff --git a/examples/config_file.rs b/examples/config_file.rs index 68d1ec2d..faefc20f 100644 --- a/examples/config_file.rs +++ b/examples/config_file.rs @@ -1,17 +1,16 @@ -use outscale_api::apis::configuration_file::{ConfigurationFile, Endpoint}; +use outscale_api::apis::profile::ProfileBuilder; use outscale_api::apis::volume_api::read_volumes; use outscale_api::models::ReadVolumesRequest; -use std::env; use std::path::PathBuf; /* Show how to configure SDK to load configuration file */ fn main() { - // You can also use ConfigurationFile::load_default() to get configuration located in ~/.osc/config.json let path = PathBuf::from("examples/config_example.json"); - let mut config_file = ConfigurationFile::load(&path).unwrap(); - ignore_me(&mut config_file); - let config = config_file.configuration("default").unwrap(); + let config = ProfileBuilder::from_standard_configuration(path, None) + .and_then(|pb| pb.build().try_into()) + .unwrap(); + let request = ReadVolumesRequest::new(); if let Err(error) = read_volumes(&config, Some(request)) { eprintln!("Error: {:?}", error); @@ -19,30 +18,3 @@ fn main() { } println!("OK"); } - -// Access Key and Secret Key can be put in configuration file -// but we add it here just to avoid commiting credentials in examples. -#[allow(unused_mut)] -fn ignore_me(config_file: &mut ConfigurationFile) { - let mut profile = config_file.0.get_mut(&"default".to_string()).unwrap(); - profile.access_key = Some(env::var("OSC_ACCESS_KEY").unwrap()); - profile.secret_key = Some(env::var("OSC_SECRET_KEY").unwrap()); - profile.region = Some(env::var("OSC_REGION").unwrap()); - match env::var("OSC_PROTOCOL") { - Ok(p) => profile.protocol = Some(p), - _ => (), - }; - match env::var("OSC_ENDPOINT_API_NOPROTO") { - Ok(enpoint) => { - profile.endpoints = Some(Endpoint { - api: Some(enpoint), - icu: None, - eim: None, - fcu: None, - lbu: None, - oos: None, - }) - } - _ => (), - }; -} diff --git a/examples/keypair.rs b/examples/keypair.rs index 7cca947b..0f710271 100644 --- a/examples/keypair.rs +++ b/examples/keypair.rs @@ -1,24 +1,13 @@ -use outscale_api::apis::configuration::{AWSv4Key, Configuration}; use outscale_api::apis::keypair_api::{create_keypair, delete_keypair, read_keypairs}; +use outscale_api::apis::profile::Profile; use outscale_api::models::{ CreateKeypairRequest, DeleteKeypairRequest, FiltersKeypair, ReadKeypairsRequest, }; use rand::Rng; -use std::env; fn main() { - let mut config = Configuration::new(); - config.aws_v4_key = Some(AWSv4Key { - access_key: env::var("OSC_ACCESS_KEY").unwrap(), - secret_key: env::var("OSC_SECRET_KEY").unwrap().into(), - region: "eu-west-2".to_string(), - service: "oapi".to_string(), - }); + let config = Profile::default().and_then(|p| p.try_into()).unwrap(); - match env::var("OSC_ENDPOINT_API") { - Ok(enpoint) => config.base_path = enpoint, - _ => (), - }; // Example reading all keypairs print!("Reading all keypairs... "); let request = ReadKeypairsRequest::new(); diff --git a/examples/region.rs b/examples/region.rs index 178e410b..66eb0756 100644 --- a/examples/region.rs +++ b/examples/region.rs @@ -1,25 +1,14 @@ -use outscale_api::apis::configuration::{AWSv4Key, Configuration}; +use outscale_api::apis::profile::ProfileBuilder; use outscale_api::apis::volume_api::read_volumes; use outscale_api::models::ReadVolumesRequest; -use std::env; /* Show how to configure SDK for a specific region */ fn main() { let region = "eu-west-2"; - let mut config = Configuration::new(); - config.base_path = format!("https://api.{}.outscale.com/api/v1", region); - config.aws_v4_key = Some(AWSv4Key { - access_key: env::var("OSC_ACCESS_KEY").unwrap(), - secret_key: env::var("OSC_SECRET_KEY").unwrap().into(), - region: region.to_string(), - service: "oapi".to_string(), - }); - - match env::var("OSC_ENDPOINT_API") { - Ok(enpoint) => config.base_path = enpoint, - _ => (), - }; + let config = ProfileBuilder::from_standard_configuration(None, None) + .and_then(|pb| pb.region(region).build().try_into()) + .unwrap(); print!("Action on specific region ({})... ", region); let request = ReadVolumesRequest::new(); diff --git a/examples/volume.rs b/examples/volume.rs index ec510688..24aaeb15 100644 --- a/examples/volume.rs +++ b/examples/volume.rs @@ -1,23 +1,12 @@ -use outscale_api::apis::configuration::{AWSv4Key, Configuration}; +use outscale_api::apis::profile::Profile; use outscale_api::apis::volume_api::{create_volume, delete_volume, read_volumes}; use outscale_api::models::{ CreateVolumeRequest, DeleteVolumeRequest, FiltersVolume, ReadVolumesRequest, }; -use std::env; fn main() { - let mut config = Configuration::new(); - config.aws_v4_key = Some(AWSv4Key { - access_key: env::var("OSC_ACCESS_KEY").unwrap(), - secret_key: env::var("OSC_SECRET_KEY").unwrap().into(), - region: "eu-west-2".to_string(), - service: "oapi".to_string(), - }); + let config = Profile::default().and_then(|p| p.try_into()).unwrap(); - match env::var("OSC_ENDPOINT_API") { - Ok(enpoint) => config.base_path = enpoint, - _ => (), - }; // Example reading all volumes print!("Reading all volumes... "); let request = ReadVolumesRequest::new(); diff --git a/gen.yml b/gen.yml index f56cb294..fb42a6c1 100644 --- a/gen.yml +++ b/gen.yml @@ -3,9 +3,10 @@ packageName: outscale_api supportAsync: false withAWSV4Signature: true files: - configuration_file.rs: + middleware.rs: templateType: SupportingFiles folder: src/apis - middleware.rs: + profile.rs: templateType: SupportingFiles folder: src/apis + diff --git a/src/apis/configuration_file.rs b/src/apis/configuration_file.rs deleted file mode 100644 index f272d984..00000000 --- a/src/apis/configuration_file.rs +++ /dev/null @@ -1,164 +0,0 @@ -/* -Copyright (c) 2022 OUTSCALE SAS. All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, -this list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, -this list of conditions and the following disclaimer in the documentation -and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its contributors -may be used to endorse or promote products derived from this software without -specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE -USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ - -use crate::apis::configuration::{AWSv4Key, Configuration}; -use crate::apis::middleware::{BackoffParams, LimiterParams}; -use home::home_dir; -use serde::Deserialize; -use std::collections::HashMap; -use std::error::Error; -use std::fmt; -use std::fs::File; -use std::io::BufReader; -use std::path::PathBuf; - -#[derive(Deserialize, Debug)] -pub struct ConfigurationFile(pub HashMap); - -#[derive(Deserialize, Debug, Clone)] -pub struct Profile { - pub access_key: Option, - pub secret_key: Option, - pub x509_client_cert: Option, - pub x509_client_key: Option, - pub protocol: Option, - pub method: Option, - pub region: Option, - pub endpoints: Option, - #[serde(flatten)] - pub backoff_params: BackoffParams, - #[serde(skip)] - pub limiter_params: LimiterParams, -} - -#[derive(Deserialize, Debug, Clone)] -pub struct Endpoint { - pub api: Option, - pub fcu: Option, - pub lbu: Option, - pub eim: Option, - pub icu: Option, - pub oos: Option, -} - -#[derive(Debug, Clone)] -pub enum ConfigurationFileError { - CannotGetDefaultPath, - ProfileNotFound, -} - -impl fmt::Display for ConfigurationFileError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - ConfigurationFileError::CannotGetDefaultPath => { - write!(f, "cannot find default configuration file path") - } - ConfigurationFileError::ProfileNotFound => write!(f, "profile not found"), - } - } -} - -impl Error for ConfigurationFileError {} - -impl ConfigurationFile { - pub fn default_path() -> Result { - let mut path = match home_dir() { - Some(p) => p, - None => return Err(ConfigurationFileError::CannotGetDefaultPath), - }; - path.push(".osc"); - path.push("config.json"); - Ok(path) - } - - pub fn load_default() -> Result> { - let path = ConfigurationFile::default_path()?; - ConfigurationFile::load(&path) - } - - pub fn load(path: &PathBuf) -> Result> { - let file = File::open(path)?; - let reader = BufReader::new(file); - let configuration_file = serde_json::from_reader(reader)?; - Ok(configuration_file) - } - - pub fn configuration>( - &self, - profile_name: S, - ) -> Result> { - let profile_name = profile_name.into(); - let profile = match self.0.get(&profile_name) { - Some(profile) => profile.clone(), - None => return Err(Box::new(ConfigurationFileError::ProfileNotFound)), - }; - - let mut config = Configuration { - client: super::middleware::ClientWithBackoff::new( - reqwest::blocking::Client::new(), - profile.backoff_params.clone(), - profile.limiter_params.clone(), - ), - ..Default::default() - }; - - if let Some(ref region) = profile.region { - config.base_path = format!("https://api.{}.outscale.com/api/v1", region); - } - - if let Some(endpoints) = profile.endpoints { - if let Some(api_endpoint) = endpoints.api { - match profile.protocol { - Some(protocol) => { - config.base_path = format!("{}://{}", protocol, api_endpoint); - } - None => { - config.base_path = format!("https://{}", api_endpoint); - } - } - } - }; - - if let Some(access_key) = profile.access_key { - if let Some(secret_key) = profile.secret_key { - let region = match profile.region { - Some(r) => r.clone(), - None => "eu-west-2".to_string(), - }; - config.aws_v4_key = Some(AWSv4Key { - access_key, - secret_key: secret_key.into(), - region, - service: "oapi".to_string(), - }); - } - } - Ok(config) - } -} diff --git a/src/apis/mod.rs b/src/apis/mod.rs index 74051ea7..f4a4e4bc 100644 --- a/src/apis/mod.rs +++ b/src/apis/mod.rs @@ -117,5 +117,5 @@ pub mod volume_api; pub mod vpn_connection_api; pub mod configuration; -pub mod configuration_file; pub mod middleware; +pub mod profile; diff --git a/src/apis/profile.rs b/src/apis/profile.rs new file mode 100644 index 00000000..b5654086 --- /dev/null +++ b/src/apis/profile.rs @@ -0,0 +1,498 @@ +use crate::apis::{ + configuration::Configuration, + middleware::{BackoffParams, LimiterParams}, +}; + +pub struct Endpoint { + pub api: String, + pub fcu: String, + pub lbu: String, + pub eim: String, + pub icu: String, + pub oos: String, +} + +/// builder for constructing an endpoint configuration. +/// +/// this struct is used to configure the various api endpoints, +/// allowing for overrides via environment variables or explicit setting +#[derive(Deserialize, Default)] +pub struct EndpointBuilder { + api: Option, + fcu: Option, + lbu: Option, + eim: Option, + icu: Option, + oos: Option, +} + +impl EndpointBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn from_env(self) -> Result { + macro_rules! get_from_env { + ($field:ident, $env:literal) => { + match std::env::var($env) { + Ok(v) => Some(v), + Err(std::env::VarError::NotPresent) => self.$field, + _ => { + return Err(ConfigurationFileError::InvalidEnvironmentVariable( + $env.to_string(), + )) + } + } + }; + } + + Ok(Self { + api: get_from_env!(api, "OSC_ENDPOINT_API"), + fcu: get_from_env!(fcu, "OSC_ENDPOINT_FCU"), + lbu: get_from_env!(lbu, "OSC_ENDPOINT_LBU"), + eim: get_from_env!(eim, "OSC_ENDPOINT_EIM"), + icu: get_from_env!(icu, "OSC_ENDPOINT_ICU"), + oos: get_from_env!(oos, "OSC_ENDPOINT_OOS"), + }) + } + + pub fn build( + self, + protocol: impl ToString + std::fmt::Display, + region: impl ToString + std::fmt::Display, + ) -> Endpoint { + Endpoint { + api: self + .api + .unwrap_or_else(|| format!("{}://api.{}.outscale.com/api/v1", protocol, region)), + fcu: self + .fcu + .unwrap_or_else(|| format!("{}://fcu.{}.outscale.com", protocol, region)), + lbu: self + .lbu + .unwrap_or_else(|| format!("{}://lbu.{}.outscale.com", protocol, region)), + eim: self + .eim + .unwrap_or_else(|| format!("{}://eim.{}.outscale.com", protocol, region)), + icu: self + .icu + .unwrap_or_else(|| format!("{}://icu.{}.outscale.com", protocol, region)), + oos: self + .oos + .unwrap_or_else(|| format!("{}://oos.{}.outscale.com", protocol, region)), + } + } +} + +pub struct Profile { + pub access_key: Option, + pub secret_key: Option, + pub x509_client_cert: Option, + pub x509_client_key: Option, + pub x509_client_cert_b64: Option, + pub x509_client_key_b64: Option, + pub tls_skip_verify: bool, + pub login: Option, + pub password: Option, + pub protocol: String, + pub region: String, + pub endpoints: Endpoint, + pub backoff_params: BackoffParams, + pub limiter_params: LimiterParams, +} + +impl Profile { + #[inline] + pub fn builder() -> ProfileBuilder { + ProfileBuilder::new() + } + + #[inline] + pub fn default() -> Result { + Ok(ProfileBuilder::from_standard_configuration(None, None)?.build()) + } +} + +/// builder for constructing a profile. +/// +/// this struct is used to configure the various parameters, +/// allowing for overrides via environment variables or explicit setting +#[derive(Deserialize, Default)] +#[serde(default)] +pub struct ProfileBuilder { + access_key: Option, + secret_key: Option, + x509_client_cert: Option, + x509_client_key: Option, + x509_client_cert_b64: Option, + x509_client_key_b64: Option, + tls_skip_verify: Option, + login: Option, + password: Option, + protocol: Option, + region: Option, + endpoints: EndpointBuilder, + backoff_params: Option, + limiter_params: Option, +} + +impl ProfileBuilder { + #[inline] + fn new() -> Self { + Self::default() + } + + pub fn from_env(self) -> Result { + macro_rules! get_from_env { + ($field:ident, $env:literal) => { + match std::env::var($env) { + Ok(v) => Some(v), + Err(std::env::VarError::NotPresent) => self.$field, + _ => { + return Err(ConfigurationFileError::InvalidEnvironmentVariable( + $env.to_string(), + )) + } + } + }; + } + + Ok(Self { + access_key: get_from_env!(access_key, "OSC_ACCESS_KEY"), + secret_key: get_from_env!(secret_key, "OSC_SECRET_KEY"), + x509_client_cert: get_from_env!(x509_client_cert, "OSC_X509_CLENT_CERT"), + x509_client_key: get_from_env!(x509_client_key, "OSC_X509_CLENT_KEY"), + x509_client_cert_b64: get_from_env!(x509_client_cert_b64, "OSC_X509_CLENT_CERT_B64"), + x509_client_key_b64: get_from_env!(x509_client_key_b64, "OSC_X509_CLENT_KEY_B64"), + tls_skip_verify: match std::env::var("OSC_TLS_SKIP_VERIFY") { + Ok(e) if e.to_lowercase() == "true" => Some(true), + Ok(_) => Some(false), + Err(std::env::VarError::NotPresent) => self.tls_skip_verify, + Err(_) => { + return Err(ConfigurationFileError::InvalidEnvironmentVariable( + "OSC_TLS_SKIP_VERIFY".to_string(), + )) + } + }, + login: get_from_env!(login, "OSC_LOGIN"), + password: get_from_env!(password, "OSC_PASSWORD"), + protocol: get_from_env!(protocol, "OSC_PROTOCOL"), + region: get_from_env!(region, "OSC_REGION"), + endpoints: self.endpoints.from_env()?, + backoff_params: None, + limiter_params: None, + }) + } + + pub fn access_key(mut self, access_key: impl ToString, secret_key: impl ToString) -> Self { + self.access_key = Some(access_key.to_string()); + self.secret_key = Some(secret_key.to_string()); + self + } + + #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] + pub fn x509_client(mut self, client_cert: impl ToString, client_key: impl ToString) -> Self { + self.x509_client_cert = Some(client_cert.to_string()); + self.x509_client_key = Some(client_key.to_string()); + self + } + + #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] + pub fn x509_client_b64( + mut self, + client_cert: impl ToString, + client_key: impl ToString, + ) -> Self { + self.x509_client_cert_b64 = Some(client_cert.to_string()); + self.x509_client_key_b64 = Some(client_key.to_string()); + self + } + + pub fn basic_auth(mut self, login: impl ToString, password: impl ToString) -> Self { + self.login = Some(login.to_string()); + self.password = Some(password.to_string()); + self + } + + pub fn protocol(mut self, protocol: impl ToString) -> Self { + self.protocol = Some(protocol.to_string()); + self + } + + pub fn region(mut self, region: impl ToString) -> Self { + self.region = Some(region.to_string()); + self + } + + pub fn from_file(path: impl AsRef, name: impl AsRef) -> Result { + let file = std::fs::File::open(path)?; + let reader = std::io::BufReader::new(file); + + let mut profile_file: std::collections::HashMap = + serde_json::from_reader(reader)?; + + match profile_file.remove(name.as_ref()) { + Some(u) => Ok(u), + None => Err(ConfigurationFileError::ProfileNotFound), + } + } + + pub fn from_standard_configuration( + path: impl Into>, + name: impl Into>, + ) -> Result { + let profile_path = { + let mut profile_path: Option = path.into(); + if profile_path.is_none() { + profile_path = match std::env::var("OSC_CONFIG_FILE") { + Ok(v) => Some(std::path::PathBuf::from(v)), + Err(std::env::VarError::NotPresent) => None, + Err(_) => { + return Err(ConfigurationFileError::InvalidEnvironmentVariable( + "OSC_CONFIG_FILE".to_string(), + )) + } + } + } + + if profile_path.is_none() { + if let Some(mut path) = home::home_dir() { + path.push(".osc/config.json"); + + if path.exists() { + profile_path = Some(path); + } + } + } + + profile_path + }; + + let profile_name = { + let mut profile_name: Option = name.into(); + if profile_name.is_none() { + profile_name = match std::env::var("OSC_PROFILE") { + Ok(v) => Some(v), + Err(std::env::VarError::NotPresent) => None, + Err(_) => { + return Err(ConfigurationFileError::InvalidEnvironmentVariable( + "OSC_PROFILE".to_string(), + )) + } + } + } + profile_name.unwrap_or_else(|| "default".to_string()) + }; + + if let Some(profile_path) = profile_path { + Self::from_file(&profile_path, &profile_name)?.from_env() + } else { + Self::default().from_env() + } + } + + pub fn build(self) -> Profile { + let protocol = self.protocol.unwrap_or_else(|| "https".to_string()); + let region = self.region.unwrap_or_else(|| "eu-west-2".to_string()); + let endpoints = self.endpoints.build(&protocol, ®ion); + + Profile { + access_key: self.access_key, + secret_key: self.secret_key, + x509_client_cert: self.x509_client_cert, + x509_client_key: self.x509_client_key, + x509_client_cert_b64: self.x509_client_cert_b64, + x509_client_key_b64: self.x509_client_key_b64, + tls_skip_verify: self.tls_skip_verify.unwrap_or_default(), + login: self.login, + password: self.password, + protocol, + region, + endpoints, + backoff_params: self.backoff_params.unwrap_or_default(), + limiter_params: self.limiter_params.unwrap_or_default(), + } + } +} + +impl TryFrom for Configuration { + type Error = ConfigurationFileError; + + fn try_from(value: Profile) -> std::result::Result { + let mut client_builder = + reqwest::blocking::Client::builder().min_tls_version(reqwest::tls::Version::TLS_1_2); + + if value.tls_skip_verify { + client_builder = client_builder.danger_accept_invalid_certs(true); + } + + #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] + { + if let Some((x509_client_key, x509_client_cert)) = + value.x509_client_key.zip(value.x509_client_cert) + { + let cert = std::fs::read(x509_client_cert)?; + let key = std::fs::read(x509_client_key)?; + let pkcs8 = reqwest::Identity::from_pkcs8_pem(&cert, &key) + .map_err(ConfigurationFileError::InvalidClientCertificate)?; + client_builder = client_builder.identity(pkcs8); + } else if let Some((x509_client_key_b64, x509_client_cert_b64)) = + value.x509_client_key_b64.zip(value.x509_client_cert_b64) + { + use base64::engine::{general_purpose::STANDARD, Engine as _}; + + let cert = STANDARD.decode(x509_client_cert_b64)?; + let key = STANDARD.decode(x509_client_key_b64)?; + let pkcs8 = reqwest::Identity::from_pkcs8_pem(&cert, &key) + .map_err(ConfigurationFileError::InvalidClientCertificate)?; + client_builder = client_builder.identity(pkcs8); + } + } + + let mut config = Configuration { + base_path: value.endpoints.api, + client: super::middleware::ClientWithBackoff::new( + client_builder.build().unwrap(), + value.backoff_params.clone(), + value.limiter_params.clone(), + ), + ..Default::default() + }; + + if let Some((access_key, secret_key)) = value.access_key.zip(value.secret_key) { + config.aws_v4_key = Some(super::configuration::AWSv4Key { + access_key, + secret_key: secret_key.into(), + region: value.region, + service: "oapi".to_string(), + }) + } else if let Some((login, password)) = value.login.zip(value.password) { + config.basic_auth = Some((login, Some(password))); + } + + Ok(config) + } +} + +#[derive(Debug)] +pub enum ConfigurationFileError { + ProfileNotFound, + Io(std::io::Error), + Json(serde_json::Error), + #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] + Base64(base64::DecodeError), + InvalidEnvironmentVariable(String), + #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] + InvalidClientCertificate(reqwest::Error), +} + +impl std::fmt::Display for ConfigurationFileError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + ConfigurationFileError::ProfileNotFound => write!(f, "profile not found"), + ConfigurationFileError::Io(e) => write!(f, "IO error: {}", e), + ConfigurationFileError::Json(e) => write!(f, "JSON error: {}", e), + #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] + ConfigurationFileError::Base64(e) => write!(f, "Base64 error: {}", e), + ConfigurationFileError::InvalidEnvironmentVariable(v) => { + write!(f, "invalid environment variable {}", v) + } + #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] + ConfigurationFileError::InvalidClientCertificate(e) => { + write!(f, "invalid client certificate: {}", e) + } + } + } +} + +impl From for ConfigurationFileError { + fn from(error: std::io::Error) -> Self { + ConfigurationFileError::Io(error) + } +} + +impl From for ConfigurationFileError { + fn from(error: serde_json::Error) -> Self { + match error.classify() { + serde_json::error::Category::Io => ConfigurationFileError::Io(error.into()), + _ => ConfigurationFileError::Json(error), + } + } +} + +#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] +impl From for ConfigurationFileError { + fn from(error: base64::DecodeError) -> Self { + ConfigurationFileError::Base64(error) + } +} + +impl std::error::Error for ConfigurationFileError {} + +pub type Result = std::result::Result; + +#[cfg(test)] +mod tests { + use super::*; + use std::env; + + #[test] + fn test_endpoint_builder_default() { + let builder = EndpointBuilder::new(); + let endpoint = builder.build("https", "eu-west-2"); + + assert_eq!(endpoint.api, "https://api.eu-west-2.outscale.com/api/v1"); + assert_eq!(endpoint.fcu, "https://fcu.eu-west-2.outscale.com"); + assert_eq!(endpoint.lbu, "https://lbu.eu-west-2.outscale.com"); + assert_eq!(endpoint.eim, "https://eim.eu-west-2.outscale.com"); + assert_eq!(endpoint.icu, "https://icu.eu-west-2.outscale.com"); + assert_eq!(endpoint.oos, "https://oos.eu-west-2.outscale.com"); + } + + #[test] + fn test_endpoint_builder_from_env() { + env::set_var("OSC_ENDPOINT_API", "https://api.custom.com"); + env::set_var("OSC_ENDPOINT_FCU", "https://fcu.custom.com"); + + let builder = EndpointBuilder::new(); + let updated_builder = builder.from_env().unwrap(); + let endpoint = updated_builder.build("https", "eu-west-2"); + + assert_eq!(endpoint.api, "https://api.custom.com"); + assert_eq!(endpoint.fcu, "https://fcu.custom.com"); + } + + #[test] + fn test_profile_builder_default() { + let builder = ProfileBuilder::new(); + let profile = builder.build(); + + assert_eq!(profile.protocol, "https"); + assert_eq!(profile.region, "eu-west-2"); + } + + #[test] + fn test_profile_builder_from_env() { + env::set_var("OSC_ACCESS_KEY", "test_key"); + env::set_var("OSC_SECRET_KEY", "test_secret"); + + let builder = ProfileBuilder::new(); + let updated_builder = builder.from_env().unwrap(); + + assert_eq!(updated_builder.access_key.unwrap(), "test_key"); + assert_eq!(updated_builder.secret_key.unwrap(), "test_secret"); + } + #[test] + fn test_full_profile_build() { + let profile = ProfileBuilder::new() + .access_key("my_access_key", "my_secret_key") + .protocol("http") + .region("us-west-1") + .build(); + + assert_eq!(profile.access_key.unwrap(), "my_access_key"); + assert_eq!(profile.secret_key.unwrap(), "my_secret_key"); + assert_eq!(profile.protocol, "http"); + assert_eq!(profile.region, "us-west-1"); + } +} diff --git a/templates/Cargo.mustache b/templates/Cargo.mustache index 89ce1a1f..3bdd9a1a 100644 --- a/templates/Cargo.mustache +++ b/templates/Cargo.mustache @@ -8,6 +8,7 @@ description = "Outscale API SDK" repository = "https://github.com/outscale/osc-sdk-rust/" [dependencies] +base64 = { version = "0.22", optional = true } home = "0.5.3" rand = "~0.9" serde = "^1.0" @@ -19,7 +20,6 @@ hyper = { version = "~0.14", features = ["full"] } hyper-tls = "~0.5" http = "~0.2" serde_yaml = "0.7" -base64 = "~0.7.0" futures = "^0.3" {{/hyper}} {{#reqwest}} @@ -50,5 +50,5 @@ tokio-core = "*" [features] default = ["reqwest/default-tls"] -native-tls = ["reqwest/native-tls"] -rustls-tls = ["reqwest/rustls-tls"] +native-tls = ["reqwest/native-tls", "dep:base64"] +rustls-tls = ["reqwest/rustls-tls", "dep:base64"] diff --git a/templates/configuration_file.rs b/templates/configuration_file.rs deleted file mode 100644 index f272d984..00000000 --- a/templates/configuration_file.rs +++ /dev/null @@ -1,164 +0,0 @@ -/* -Copyright (c) 2022 OUTSCALE SAS. All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, -this list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, -this list of conditions and the following disclaimer in the documentation -and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its contributors -may be used to endorse or promote products derived from this software without -specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE -USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ - -use crate::apis::configuration::{AWSv4Key, Configuration}; -use crate::apis::middleware::{BackoffParams, LimiterParams}; -use home::home_dir; -use serde::Deserialize; -use std::collections::HashMap; -use std::error::Error; -use std::fmt; -use std::fs::File; -use std::io::BufReader; -use std::path::PathBuf; - -#[derive(Deserialize, Debug)] -pub struct ConfigurationFile(pub HashMap); - -#[derive(Deserialize, Debug, Clone)] -pub struct Profile { - pub access_key: Option, - pub secret_key: Option, - pub x509_client_cert: Option, - pub x509_client_key: Option, - pub protocol: Option, - pub method: Option, - pub region: Option, - pub endpoints: Option, - #[serde(flatten)] - pub backoff_params: BackoffParams, - #[serde(skip)] - pub limiter_params: LimiterParams, -} - -#[derive(Deserialize, Debug, Clone)] -pub struct Endpoint { - pub api: Option, - pub fcu: Option, - pub lbu: Option, - pub eim: Option, - pub icu: Option, - pub oos: Option, -} - -#[derive(Debug, Clone)] -pub enum ConfigurationFileError { - CannotGetDefaultPath, - ProfileNotFound, -} - -impl fmt::Display for ConfigurationFileError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - ConfigurationFileError::CannotGetDefaultPath => { - write!(f, "cannot find default configuration file path") - } - ConfigurationFileError::ProfileNotFound => write!(f, "profile not found"), - } - } -} - -impl Error for ConfigurationFileError {} - -impl ConfigurationFile { - pub fn default_path() -> Result { - let mut path = match home_dir() { - Some(p) => p, - None => return Err(ConfigurationFileError::CannotGetDefaultPath), - }; - path.push(".osc"); - path.push("config.json"); - Ok(path) - } - - pub fn load_default() -> Result> { - let path = ConfigurationFile::default_path()?; - ConfigurationFile::load(&path) - } - - pub fn load(path: &PathBuf) -> Result> { - let file = File::open(path)?; - let reader = BufReader::new(file); - let configuration_file = serde_json::from_reader(reader)?; - Ok(configuration_file) - } - - pub fn configuration>( - &self, - profile_name: S, - ) -> Result> { - let profile_name = profile_name.into(); - let profile = match self.0.get(&profile_name) { - Some(profile) => profile.clone(), - None => return Err(Box::new(ConfigurationFileError::ProfileNotFound)), - }; - - let mut config = Configuration { - client: super::middleware::ClientWithBackoff::new( - reqwest::blocking::Client::new(), - profile.backoff_params.clone(), - profile.limiter_params.clone(), - ), - ..Default::default() - }; - - if let Some(ref region) = profile.region { - config.base_path = format!("https://api.{}.outscale.com/api/v1", region); - } - - if let Some(endpoints) = profile.endpoints { - if let Some(api_endpoint) = endpoints.api { - match profile.protocol { - Some(protocol) => { - config.base_path = format!("{}://{}", protocol, api_endpoint); - } - None => { - config.base_path = format!("https://{}", api_endpoint); - } - } - } - }; - - if let Some(access_key) = profile.access_key { - if let Some(secret_key) = profile.secret_key { - let region = match profile.region { - Some(r) => r.clone(), - None => "eu-west-2".to_string(), - }; - config.aws_v4_key = Some(AWSv4Key { - access_key, - secret_key: secret_key.into(), - region, - service: "oapi".to_string(), - }); - } - } - Ok(config) - } -} diff --git a/templates/profile.rs b/templates/profile.rs new file mode 100644 index 00000000..b5654086 --- /dev/null +++ b/templates/profile.rs @@ -0,0 +1,498 @@ +use crate::apis::{ + configuration::Configuration, + middleware::{BackoffParams, LimiterParams}, +}; + +pub struct Endpoint { + pub api: String, + pub fcu: String, + pub lbu: String, + pub eim: String, + pub icu: String, + pub oos: String, +} + +/// builder for constructing an endpoint configuration. +/// +/// this struct is used to configure the various api endpoints, +/// allowing for overrides via environment variables or explicit setting +#[derive(Deserialize, Default)] +pub struct EndpointBuilder { + api: Option, + fcu: Option, + lbu: Option, + eim: Option, + icu: Option, + oos: Option, +} + +impl EndpointBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn from_env(self) -> Result { + macro_rules! get_from_env { + ($field:ident, $env:literal) => { + match std::env::var($env) { + Ok(v) => Some(v), + Err(std::env::VarError::NotPresent) => self.$field, + _ => { + return Err(ConfigurationFileError::InvalidEnvironmentVariable( + $env.to_string(), + )) + } + } + }; + } + + Ok(Self { + api: get_from_env!(api, "OSC_ENDPOINT_API"), + fcu: get_from_env!(fcu, "OSC_ENDPOINT_FCU"), + lbu: get_from_env!(lbu, "OSC_ENDPOINT_LBU"), + eim: get_from_env!(eim, "OSC_ENDPOINT_EIM"), + icu: get_from_env!(icu, "OSC_ENDPOINT_ICU"), + oos: get_from_env!(oos, "OSC_ENDPOINT_OOS"), + }) + } + + pub fn build( + self, + protocol: impl ToString + std::fmt::Display, + region: impl ToString + std::fmt::Display, + ) -> Endpoint { + Endpoint { + api: self + .api + .unwrap_or_else(|| format!("{}://api.{}.outscale.com/api/v1", protocol, region)), + fcu: self + .fcu + .unwrap_or_else(|| format!("{}://fcu.{}.outscale.com", protocol, region)), + lbu: self + .lbu + .unwrap_or_else(|| format!("{}://lbu.{}.outscale.com", protocol, region)), + eim: self + .eim + .unwrap_or_else(|| format!("{}://eim.{}.outscale.com", protocol, region)), + icu: self + .icu + .unwrap_or_else(|| format!("{}://icu.{}.outscale.com", protocol, region)), + oos: self + .oos + .unwrap_or_else(|| format!("{}://oos.{}.outscale.com", protocol, region)), + } + } +} + +pub struct Profile { + pub access_key: Option, + pub secret_key: Option, + pub x509_client_cert: Option, + pub x509_client_key: Option, + pub x509_client_cert_b64: Option, + pub x509_client_key_b64: Option, + pub tls_skip_verify: bool, + pub login: Option, + pub password: Option, + pub protocol: String, + pub region: String, + pub endpoints: Endpoint, + pub backoff_params: BackoffParams, + pub limiter_params: LimiterParams, +} + +impl Profile { + #[inline] + pub fn builder() -> ProfileBuilder { + ProfileBuilder::new() + } + + #[inline] + pub fn default() -> Result { + Ok(ProfileBuilder::from_standard_configuration(None, None)?.build()) + } +} + +/// builder for constructing a profile. +/// +/// this struct is used to configure the various parameters, +/// allowing for overrides via environment variables or explicit setting +#[derive(Deserialize, Default)] +#[serde(default)] +pub struct ProfileBuilder { + access_key: Option, + secret_key: Option, + x509_client_cert: Option, + x509_client_key: Option, + x509_client_cert_b64: Option, + x509_client_key_b64: Option, + tls_skip_verify: Option, + login: Option, + password: Option, + protocol: Option, + region: Option, + endpoints: EndpointBuilder, + backoff_params: Option, + limiter_params: Option, +} + +impl ProfileBuilder { + #[inline] + fn new() -> Self { + Self::default() + } + + pub fn from_env(self) -> Result { + macro_rules! get_from_env { + ($field:ident, $env:literal) => { + match std::env::var($env) { + Ok(v) => Some(v), + Err(std::env::VarError::NotPresent) => self.$field, + _ => { + return Err(ConfigurationFileError::InvalidEnvironmentVariable( + $env.to_string(), + )) + } + } + }; + } + + Ok(Self { + access_key: get_from_env!(access_key, "OSC_ACCESS_KEY"), + secret_key: get_from_env!(secret_key, "OSC_SECRET_KEY"), + x509_client_cert: get_from_env!(x509_client_cert, "OSC_X509_CLENT_CERT"), + x509_client_key: get_from_env!(x509_client_key, "OSC_X509_CLENT_KEY"), + x509_client_cert_b64: get_from_env!(x509_client_cert_b64, "OSC_X509_CLENT_CERT_B64"), + x509_client_key_b64: get_from_env!(x509_client_key_b64, "OSC_X509_CLENT_KEY_B64"), + tls_skip_verify: match std::env::var("OSC_TLS_SKIP_VERIFY") { + Ok(e) if e.to_lowercase() == "true" => Some(true), + Ok(_) => Some(false), + Err(std::env::VarError::NotPresent) => self.tls_skip_verify, + Err(_) => { + return Err(ConfigurationFileError::InvalidEnvironmentVariable( + "OSC_TLS_SKIP_VERIFY".to_string(), + )) + } + }, + login: get_from_env!(login, "OSC_LOGIN"), + password: get_from_env!(password, "OSC_PASSWORD"), + protocol: get_from_env!(protocol, "OSC_PROTOCOL"), + region: get_from_env!(region, "OSC_REGION"), + endpoints: self.endpoints.from_env()?, + backoff_params: None, + limiter_params: None, + }) + } + + pub fn access_key(mut self, access_key: impl ToString, secret_key: impl ToString) -> Self { + self.access_key = Some(access_key.to_string()); + self.secret_key = Some(secret_key.to_string()); + self + } + + #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] + pub fn x509_client(mut self, client_cert: impl ToString, client_key: impl ToString) -> Self { + self.x509_client_cert = Some(client_cert.to_string()); + self.x509_client_key = Some(client_key.to_string()); + self + } + + #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] + pub fn x509_client_b64( + mut self, + client_cert: impl ToString, + client_key: impl ToString, + ) -> Self { + self.x509_client_cert_b64 = Some(client_cert.to_string()); + self.x509_client_key_b64 = Some(client_key.to_string()); + self + } + + pub fn basic_auth(mut self, login: impl ToString, password: impl ToString) -> Self { + self.login = Some(login.to_string()); + self.password = Some(password.to_string()); + self + } + + pub fn protocol(mut self, protocol: impl ToString) -> Self { + self.protocol = Some(protocol.to_string()); + self + } + + pub fn region(mut self, region: impl ToString) -> Self { + self.region = Some(region.to_string()); + self + } + + pub fn from_file(path: impl AsRef, name: impl AsRef) -> Result { + let file = std::fs::File::open(path)?; + let reader = std::io::BufReader::new(file); + + let mut profile_file: std::collections::HashMap = + serde_json::from_reader(reader)?; + + match profile_file.remove(name.as_ref()) { + Some(u) => Ok(u), + None => Err(ConfigurationFileError::ProfileNotFound), + } + } + + pub fn from_standard_configuration( + path: impl Into>, + name: impl Into>, + ) -> Result { + let profile_path = { + let mut profile_path: Option = path.into(); + if profile_path.is_none() { + profile_path = match std::env::var("OSC_CONFIG_FILE") { + Ok(v) => Some(std::path::PathBuf::from(v)), + Err(std::env::VarError::NotPresent) => None, + Err(_) => { + return Err(ConfigurationFileError::InvalidEnvironmentVariable( + "OSC_CONFIG_FILE".to_string(), + )) + } + } + } + + if profile_path.is_none() { + if let Some(mut path) = home::home_dir() { + path.push(".osc/config.json"); + + if path.exists() { + profile_path = Some(path); + } + } + } + + profile_path + }; + + let profile_name = { + let mut profile_name: Option = name.into(); + if profile_name.is_none() { + profile_name = match std::env::var("OSC_PROFILE") { + Ok(v) => Some(v), + Err(std::env::VarError::NotPresent) => None, + Err(_) => { + return Err(ConfigurationFileError::InvalidEnvironmentVariable( + "OSC_PROFILE".to_string(), + )) + } + } + } + profile_name.unwrap_or_else(|| "default".to_string()) + }; + + if let Some(profile_path) = profile_path { + Self::from_file(&profile_path, &profile_name)?.from_env() + } else { + Self::default().from_env() + } + } + + pub fn build(self) -> Profile { + let protocol = self.protocol.unwrap_or_else(|| "https".to_string()); + let region = self.region.unwrap_or_else(|| "eu-west-2".to_string()); + let endpoints = self.endpoints.build(&protocol, ®ion); + + Profile { + access_key: self.access_key, + secret_key: self.secret_key, + x509_client_cert: self.x509_client_cert, + x509_client_key: self.x509_client_key, + x509_client_cert_b64: self.x509_client_cert_b64, + x509_client_key_b64: self.x509_client_key_b64, + tls_skip_verify: self.tls_skip_verify.unwrap_or_default(), + login: self.login, + password: self.password, + protocol, + region, + endpoints, + backoff_params: self.backoff_params.unwrap_or_default(), + limiter_params: self.limiter_params.unwrap_or_default(), + } + } +} + +impl TryFrom for Configuration { + type Error = ConfigurationFileError; + + fn try_from(value: Profile) -> std::result::Result { + let mut client_builder = + reqwest::blocking::Client::builder().min_tls_version(reqwest::tls::Version::TLS_1_2); + + if value.tls_skip_verify { + client_builder = client_builder.danger_accept_invalid_certs(true); + } + + #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] + { + if let Some((x509_client_key, x509_client_cert)) = + value.x509_client_key.zip(value.x509_client_cert) + { + let cert = std::fs::read(x509_client_cert)?; + let key = std::fs::read(x509_client_key)?; + let pkcs8 = reqwest::Identity::from_pkcs8_pem(&cert, &key) + .map_err(ConfigurationFileError::InvalidClientCertificate)?; + client_builder = client_builder.identity(pkcs8); + } else if let Some((x509_client_key_b64, x509_client_cert_b64)) = + value.x509_client_key_b64.zip(value.x509_client_cert_b64) + { + use base64::engine::{general_purpose::STANDARD, Engine as _}; + + let cert = STANDARD.decode(x509_client_cert_b64)?; + let key = STANDARD.decode(x509_client_key_b64)?; + let pkcs8 = reqwest::Identity::from_pkcs8_pem(&cert, &key) + .map_err(ConfigurationFileError::InvalidClientCertificate)?; + client_builder = client_builder.identity(pkcs8); + } + } + + let mut config = Configuration { + base_path: value.endpoints.api, + client: super::middleware::ClientWithBackoff::new( + client_builder.build().unwrap(), + value.backoff_params.clone(), + value.limiter_params.clone(), + ), + ..Default::default() + }; + + if let Some((access_key, secret_key)) = value.access_key.zip(value.secret_key) { + config.aws_v4_key = Some(super::configuration::AWSv4Key { + access_key, + secret_key: secret_key.into(), + region: value.region, + service: "oapi".to_string(), + }) + } else if let Some((login, password)) = value.login.zip(value.password) { + config.basic_auth = Some((login, Some(password))); + } + + Ok(config) + } +} + +#[derive(Debug)] +pub enum ConfigurationFileError { + ProfileNotFound, + Io(std::io::Error), + Json(serde_json::Error), + #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] + Base64(base64::DecodeError), + InvalidEnvironmentVariable(String), + #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] + InvalidClientCertificate(reqwest::Error), +} + +impl std::fmt::Display for ConfigurationFileError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + ConfigurationFileError::ProfileNotFound => write!(f, "profile not found"), + ConfigurationFileError::Io(e) => write!(f, "IO error: {}", e), + ConfigurationFileError::Json(e) => write!(f, "JSON error: {}", e), + #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] + ConfigurationFileError::Base64(e) => write!(f, "Base64 error: {}", e), + ConfigurationFileError::InvalidEnvironmentVariable(v) => { + write!(f, "invalid environment variable {}", v) + } + #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] + ConfigurationFileError::InvalidClientCertificate(e) => { + write!(f, "invalid client certificate: {}", e) + } + } + } +} + +impl From for ConfigurationFileError { + fn from(error: std::io::Error) -> Self { + ConfigurationFileError::Io(error) + } +} + +impl From for ConfigurationFileError { + fn from(error: serde_json::Error) -> Self { + match error.classify() { + serde_json::error::Category::Io => ConfigurationFileError::Io(error.into()), + _ => ConfigurationFileError::Json(error), + } + } +} + +#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] +impl From for ConfigurationFileError { + fn from(error: base64::DecodeError) -> Self { + ConfigurationFileError::Base64(error) + } +} + +impl std::error::Error for ConfigurationFileError {} + +pub type Result = std::result::Result; + +#[cfg(test)] +mod tests { + use super::*; + use std::env; + + #[test] + fn test_endpoint_builder_default() { + let builder = EndpointBuilder::new(); + let endpoint = builder.build("https", "eu-west-2"); + + assert_eq!(endpoint.api, "https://api.eu-west-2.outscale.com/api/v1"); + assert_eq!(endpoint.fcu, "https://fcu.eu-west-2.outscale.com"); + assert_eq!(endpoint.lbu, "https://lbu.eu-west-2.outscale.com"); + assert_eq!(endpoint.eim, "https://eim.eu-west-2.outscale.com"); + assert_eq!(endpoint.icu, "https://icu.eu-west-2.outscale.com"); + assert_eq!(endpoint.oos, "https://oos.eu-west-2.outscale.com"); + } + + #[test] + fn test_endpoint_builder_from_env() { + env::set_var("OSC_ENDPOINT_API", "https://api.custom.com"); + env::set_var("OSC_ENDPOINT_FCU", "https://fcu.custom.com"); + + let builder = EndpointBuilder::new(); + let updated_builder = builder.from_env().unwrap(); + let endpoint = updated_builder.build("https", "eu-west-2"); + + assert_eq!(endpoint.api, "https://api.custom.com"); + assert_eq!(endpoint.fcu, "https://fcu.custom.com"); + } + + #[test] + fn test_profile_builder_default() { + let builder = ProfileBuilder::new(); + let profile = builder.build(); + + assert_eq!(profile.protocol, "https"); + assert_eq!(profile.region, "eu-west-2"); + } + + #[test] + fn test_profile_builder_from_env() { + env::set_var("OSC_ACCESS_KEY", "test_key"); + env::set_var("OSC_SECRET_KEY", "test_secret"); + + let builder = ProfileBuilder::new(); + let updated_builder = builder.from_env().unwrap(); + + assert_eq!(updated_builder.access_key.unwrap(), "test_key"); + assert_eq!(updated_builder.secret_key.unwrap(), "test_secret"); + } + #[test] + fn test_full_profile_build() { + let profile = ProfileBuilder::new() + .access_key("my_access_key", "my_secret_key") + .protocol("http") + .region("us-west-1") + .build(); + + assert_eq!(profile.access_key.unwrap(), "my_access_key"); + assert_eq!(profile.secret_key.unwrap(), "my_secret_key"); + assert_eq!(profile.protocol, "http"); + assert_eq!(profile.region, "us-west-1"); + } +} diff --git a/templates/reqwest/api_mod.mustache b/templates/reqwest/api_mod.mustache index 05dd04ae..873478a2 100644 --- a/templates/reqwest/api_mod.mustache +++ b/templates/reqwest/api_mod.mustache @@ -80,5 +80,5 @@ pub mod {{{classFilename}}}; {{/apiInfo}} pub mod configuration; -pub mod configuration_file; pub mod middleware; +pub mod profile;