Skip to main content

outscale_api/apis/
profile.rs

1use crate::apis::{
2    configuration::Configuration,
3    middleware::{BackoffParams, LimiterParams},
4};
5
6pub struct Endpoint {
7    pub api: String,
8    pub fcu: String,
9    pub lbu: String,
10    pub eim: String,
11    pub icu: String,
12    pub oos: String,
13}
14
15/// builder for constructing an endpoint configuration.
16///
17/// this struct is used to configure the various api endpoints,
18/// allowing for overrides via environment variables or explicit setting
19#[derive(Deserialize, Default)]
20pub struct EndpointBuilder {
21    api: Option<String>,
22    fcu: Option<String>,
23    lbu: Option<String>,
24    eim: Option<String>,
25    icu: Option<String>,
26    oos: Option<String>,
27}
28
29impl EndpointBuilder {
30    pub fn new() -> Self {
31        Self::default()
32    }
33
34    pub fn from_env(self) -> Result<Self> {
35        macro_rules! get_from_env {
36            ($field:ident, $env:literal) => {
37                match std::env::var($env) {
38                    Ok(v) => Some(v),
39                    Err(std::env::VarError::NotPresent) => self.$field,
40                    _ => {
41                        return Err(ConfigurationFileError::InvalidEnvironmentVariable(
42                            $env.to_string(),
43                        ))
44                    }
45                }
46            };
47        }
48
49        Ok(Self {
50            api: get_from_env!(api, "OSC_ENDPOINT_API"),
51            fcu: get_from_env!(fcu, "OSC_ENDPOINT_FCU"),
52            lbu: get_from_env!(lbu, "OSC_ENDPOINT_LBU"),
53            eim: get_from_env!(eim, "OSC_ENDPOINT_EIM"),
54            icu: get_from_env!(icu, "OSC_ENDPOINT_ICU"),
55            oos: get_from_env!(oos, "OSC_ENDPOINT_OOS"),
56        })
57    }
58
59    pub fn build(
60        self,
61        protocol: impl ToString + std::fmt::Display,
62        region: impl ToString + std::fmt::Display,
63    ) -> Endpoint {
64        Endpoint {
65            api: self
66                .api
67                .unwrap_or_else(|| format!("{}://api.{}.outscale.com/api/v1", protocol, region)),
68            fcu: self
69                .fcu
70                .unwrap_or_else(|| format!("{}://fcu.{}.outscale.com", protocol, region)),
71            lbu: self
72                .lbu
73                .unwrap_or_else(|| format!("{}://lbu.{}.outscale.com", protocol, region)),
74            eim: self
75                .eim
76                .unwrap_or_else(|| format!("{}://eim.{}.outscale.com", protocol, region)),
77            icu: self
78                .icu
79                .unwrap_or_else(|| format!("{}://icu.{}.outscale.com", protocol, region)),
80            oos: self
81                .oos
82                .unwrap_or_else(|| format!("{}://oos.{}.outscale.com", protocol, region)),
83        }
84    }
85}
86
87pub struct Profile {
88    pub access_key: Option<String>,
89    pub secret_key: Option<String>,
90    pub x509_client_cert: Option<String>,
91    pub x509_client_key: Option<String>,
92    pub x509_client_cert_b64: Option<String>,
93    pub x509_client_key_b64: Option<String>,
94    pub tls_skip_verify: bool,
95    pub login: Option<String>,
96    pub password: Option<String>,
97    pub protocol: String,
98    pub region: String,
99    pub endpoints: Endpoint,
100    pub backoff_params: BackoffParams,
101    pub limiter_params: LimiterParams,
102}
103
104impl Profile {
105    #[inline]
106    pub fn builder() -> ProfileBuilder {
107        ProfileBuilder::new()
108    }
109
110    #[inline]
111    pub fn default() -> Result<Profile> {
112        Ok(ProfileBuilder::from_standard_configuration(None, None)?.build())
113    }
114}
115
116/// builder for constructing a profile.
117///
118/// this struct is used to configure the various parameters,
119/// allowing for overrides via environment variables or explicit setting
120#[derive(Deserialize, Default)]
121#[serde(default)]
122pub struct ProfileBuilder {
123    access_key: Option<String>,
124    secret_key: Option<String>,
125    x509_client_cert: Option<String>,
126    x509_client_key: Option<String>,
127    x509_client_cert_b64: Option<String>,
128    x509_client_key_b64: Option<String>,
129    tls_skip_verify: Option<bool>,
130    login: Option<String>,
131    password: Option<String>,
132    protocol: Option<String>,
133    region: Option<String>,
134    endpoints: EndpointBuilder,
135    backoff_params: Option<BackoffParams>,
136    limiter_params: Option<LimiterParams>,
137}
138
139impl ProfileBuilder {
140    #[inline]
141    fn new() -> Self {
142        Self::default()
143    }
144
145    pub fn from_env(self) -> Result<Self> {
146        macro_rules! get_from_env {
147            ($field:ident, $env:literal) => {
148                match std::env::var($env) {
149                    Ok(v) => Some(v),
150                    Err(std::env::VarError::NotPresent) => self.$field,
151                    _ => {
152                        return Err(ConfigurationFileError::InvalidEnvironmentVariable(
153                            $env.to_string(),
154                        ))
155                    }
156                }
157            };
158        }
159
160        Ok(Self {
161            access_key: get_from_env!(access_key, "OSC_ACCESS_KEY"),
162            secret_key: get_from_env!(secret_key, "OSC_SECRET_KEY"),
163            x509_client_cert: get_from_env!(x509_client_cert, "OSC_X509_CLENT_CERT"),
164            x509_client_key: get_from_env!(x509_client_key, "OSC_X509_CLENT_KEY"),
165            x509_client_cert_b64: get_from_env!(x509_client_cert_b64, "OSC_X509_CLENT_CERT_B64"),
166            x509_client_key_b64: get_from_env!(x509_client_key_b64, "OSC_X509_CLENT_KEY_B64"),
167            tls_skip_verify: match std::env::var("OSC_TLS_SKIP_VERIFY") {
168                Ok(e) if e.to_lowercase() == "true" => Some(true),
169                Ok(_) => Some(false),
170                Err(std::env::VarError::NotPresent) => self.tls_skip_verify,
171                Err(_) => {
172                    return Err(ConfigurationFileError::InvalidEnvironmentVariable(
173                        "OSC_TLS_SKIP_VERIFY".to_string(),
174                    ))
175                }
176            },
177            login: get_from_env!(login, "OSC_LOGIN"),
178            password: get_from_env!(password, "OSC_PASSWORD"),
179            protocol: get_from_env!(protocol, "OSC_PROTOCOL"),
180            region: get_from_env!(region, "OSC_REGION"),
181            endpoints: self.endpoints.from_env()?,
182            backoff_params: None,
183            limiter_params: None,
184        })
185    }
186
187    pub fn access_key(mut self, access_key: impl ToString, secret_key: impl ToString) -> Self {
188        self.access_key = Some(access_key.to_string());
189        self.secret_key = Some(secret_key.to_string());
190        self
191    }
192
193    #[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
194    pub fn x509_client(mut self, client_cert: impl ToString, client_key: impl ToString) -> Self {
195        self.x509_client_cert = Some(client_cert.to_string());
196        self.x509_client_key = Some(client_key.to_string());
197        self
198    }
199
200    #[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
201    pub fn x509_client_b64(
202        mut self,
203        client_cert: impl ToString,
204        client_key: impl ToString,
205    ) -> Self {
206        self.x509_client_cert_b64 = Some(client_cert.to_string());
207        self.x509_client_key_b64 = Some(client_key.to_string());
208        self
209    }
210
211    pub fn basic_auth(mut self, login: impl ToString, password: impl ToString) -> Self {
212        self.login = Some(login.to_string());
213        self.password = Some(password.to_string());
214        self
215    }
216
217    pub fn protocol(mut self, protocol: impl ToString) -> Self {
218        self.protocol = Some(protocol.to_string());
219        self
220    }
221
222    pub fn region(mut self, region: impl ToString) -> Self {
223        self.region = Some(region.to_string());
224        self
225    }
226
227    pub fn from_file(path: impl AsRef<std::path::Path>, name: impl AsRef<str>) -> Result<Self> {
228        let file = std::fs::File::open(path)?;
229        let reader = std::io::BufReader::new(file);
230
231        let mut profile_file: std::collections::HashMap<String, ProfileBuilder> =
232            serde_json::from_reader(reader)?;
233
234        match profile_file.remove(name.as_ref()) {
235            Some(u) => Ok(u),
236            None => Err(ConfigurationFileError::ProfileNotFound),
237        }
238    }
239
240    pub fn from_standard_configuration(
241        path: impl Into<Option<std::path::PathBuf>>,
242        name: impl Into<Option<String>>,
243    ) -> Result<Self> {
244        let profile_path = {
245            let mut profile_path: Option<std::path::PathBuf> = path.into();
246            if profile_path.is_none() {
247                profile_path = match std::env::var("OSC_CONFIG_FILE") {
248                    Ok(v) => Some(std::path::PathBuf::from(v)),
249                    Err(std::env::VarError::NotPresent) => None,
250                    Err(_) => {
251                        return Err(ConfigurationFileError::InvalidEnvironmentVariable(
252                            "OSC_CONFIG_FILE".to_string(),
253                        ))
254                    }
255                }
256            }
257
258            if profile_path.is_none() {
259                if let Some(mut path) = home::home_dir() {
260                    path.push(".osc/config.json");
261
262                    if path.exists() {
263                        profile_path = Some(path);
264                    }
265                }
266            }
267
268            profile_path
269        };
270
271        let profile_name = {
272            let mut profile_name: Option<String> = name.into();
273            if profile_name.is_none() {
274                profile_name = match std::env::var("OSC_PROFILE") {
275                    Ok(v) => Some(v),
276                    Err(std::env::VarError::NotPresent) => None,
277                    Err(_) => {
278                        return Err(ConfigurationFileError::InvalidEnvironmentVariable(
279                            "OSC_PROFILE".to_string(),
280                        ))
281                    }
282                }
283            }
284            profile_name.unwrap_or_else(|| "default".to_string())
285        };
286
287        if let Some(profile_path) = profile_path {
288            Self::from_file(&profile_path, &profile_name)?.from_env()
289        } else {
290            Self::default().from_env()
291        }
292    }
293
294    pub fn build(self) -> Profile {
295        let protocol = self.protocol.unwrap_or_else(|| "https".to_string());
296        let region = self.region.unwrap_or_else(|| "eu-west-2".to_string());
297        let endpoints = self.endpoints.build(&protocol, &region);
298
299        Profile {
300            access_key: self.access_key,
301            secret_key: self.secret_key,
302            x509_client_cert: self.x509_client_cert,
303            x509_client_key: self.x509_client_key,
304            x509_client_cert_b64: self.x509_client_cert_b64,
305            x509_client_key_b64: self.x509_client_key_b64,
306            tls_skip_verify: self.tls_skip_verify.unwrap_or_default(),
307            login: self.login,
308            password: self.password,
309            protocol,
310            region,
311            endpoints,
312            backoff_params: self.backoff_params.unwrap_or_default(),
313            limiter_params: self.limiter_params.unwrap_or_default(),
314        }
315    }
316}
317
318impl TryFrom<Profile> for Configuration {
319    type Error = ConfigurationFileError;
320
321    fn try_from(value: Profile) -> std::result::Result<Self, Self::Error> {
322        let mut client_builder =
323            reqwest::blocking::Client::builder().min_tls_version(reqwest::tls::Version::TLS_1_2);
324
325        if value.tls_skip_verify {
326            client_builder = client_builder.danger_accept_invalid_certs(true);
327        }
328
329        {
330            #[cfg(feature = "rustls-tls")]
331            fn mk_identity(mut key: Vec<u8>, mut cert: Vec<u8>) -> Result<reqwest::tls::Identity> {
332                key.append(&mut cert);
333                reqwest::Identity::from_pem(&key)
334                    .map_err(ConfigurationFileError::InvalidClientCertificate)
335            }
336
337            #[cfg(feature = "native-tls")]
338            fn mk_identity(key: Vec<u8>, cert: Vec<u8>) -> Result<reqwest::tls::Identity> {
339                key.append(cert);
340                reqwest::Identity::from_pkcs8_pem(&key, &cert)
341                    .map_err(ConfigurationFileError::InvalidClientCertificate)
342            }
343
344            #[cfg(not(any(feature = "rustls-tls", feature = "native-tls")))]
345            fn mk_identity(_: Vec<u8>, cert: Vec<u8>) -> Result<reqwest::tls::Identity> {
346                Err(ConfigurationFileError::NonSupportedFeature(
347                    "mTLS required rustls-tls or native-tls feature flag".to_string(),
348                ))
349            }
350
351            if let Some((x509_client_key, x509_client_cert)) =
352                value.x509_client_key.zip(value.x509_client_cert)
353            {
354                let cert = std::fs::read(x509_client_cert)?;
355                let key = std::fs::read(x509_client_key)?;
356                let pkcs8 = mk_identity(key, cert)?;
357                client_builder = client_builder.identity(pkcs8);
358            } else if let Some((x509_client_key_b64, x509_client_cert_b64)) =
359                value.x509_client_key_b64.zip(value.x509_client_cert_b64)
360            {
361                use base64::engine::{general_purpose::STANDARD, Engine as _};
362
363                let cert = STANDARD.decode(x509_client_cert_b64)?;
364                let key = STANDARD.decode(x509_client_key_b64)?;
365                let pkcs8 = mk_identity(key, cert)?;
366                client_builder = client_builder.identity(pkcs8);
367            }
368        }
369
370        let mut config = Configuration {
371            base_path: value.endpoints.api,
372            client: super::middleware::ClientWithBackoff::new(
373                client_builder.build().unwrap(),
374                value.backoff_params.clone(),
375                value.limiter_params.clone(),
376            ),
377            ..Default::default()
378        };
379
380        if let Some((access_key, secret_key)) = value.access_key.zip(value.secret_key) {
381            config.aws_v4_key = Some(super::configuration::AWSv4Key {
382                access_key,
383                secret_key: secret_key.into(),
384                region: value.region,
385                service: "oapi".to_string(),
386            })
387        } else if let Some((login, password)) = value.login.zip(value.password) {
388            config.basic_auth = Some((login, Some(password)));
389        }
390
391        Ok(config)
392    }
393}
394
395#[derive(Debug)]
396pub enum ConfigurationFileError {
397    ProfileNotFound,
398    Io(std::io::Error),
399    Json(serde_json::Error),
400    Base64(base64::DecodeError),
401    InvalidEnvironmentVariable(String),
402    InvalidClientCertificate(reqwest::Error),
403    NonSupportedFeature(String),
404}
405
406impl std::fmt::Display for ConfigurationFileError {
407    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
408        match self {
409            ConfigurationFileError::ProfileNotFound => write!(f, "profile not found"),
410            ConfigurationFileError::Io(e) => write!(f, "IO error: {}", e),
411            ConfigurationFileError::Json(e) => write!(f, "JSON error: {}", e),
412            ConfigurationFileError::Base64(e) => write!(f, "Base64 error: {}", e),
413            ConfigurationFileError::InvalidEnvironmentVariable(v) => {
414                write!(f, "invalid environment variable {}", v)
415            }
416            ConfigurationFileError::InvalidClientCertificate(e) => {
417                write!(f, "invalid client certificate: {}", e)
418            }
419            ConfigurationFileError::NonSupportedFeature(e) => {
420                write!(f, "non supported feature: {}", e)
421            }
422        }
423    }
424}
425
426impl From<std::io::Error> for ConfigurationFileError {
427    fn from(error: std::io::Error) -> Self {
428        ConfigurationFileError::Io(error)
429    }
430}
431
432impl From<serde_json::Error> for ConfigurationFileError {
433    fn from(error: serde_json::Error) -> Self {
434        match error.classify() {
435            serde_json::error::Category::Io => ConfigurationFileError::Io(error.into()),
436            _ => ConfigurationFileError::Json(error),
437        }
438    }
439}
440
441impl From<base64::DecodeError> for ConfigurationFileError {
442    fn from(error: base64::DecodeError) -> Self {
443        ConfigurationFileError::Base64(error)
444    }
445}
446
447impl std::error::Error for ConfigurationFileError {}
448
449pub type Result<T> = std::result::Result<T, ConfigurationFileError>;
450
451#[cfg(test)]
452mod tests {
453    use super::*;
454    use std::env;
455
456    #[test]
457    fn test_endpoint_builder_default() {
458        let builder = EndpointBuilder::new();
459        let endpoint = builder.build("https", "eu-west-2");
460
461        assert_eq!(endpoint.api, "https://api.eu-west-2.outscale.com/api/v1");
462        assert_eq!(endpoint.fcu, "https://fcu.eu-west-2.outscale.com");
463        assert_eq!(endpoint.lbu, "https://lbu.eu-west-2.outscale.com");
464        assert_eq!(endpoint.eim, "https://eim.eu-west-2.outscale.com");
465        assert_eq!(endpoint.icu, "https://icu.eu-west-2.outscale.com");
466        assert_eq!(endpoint.oos, "https://oos.eu-west-2.outscale.com");
467    }
468
469    #[test]
470    fn test_endpoint_builder_from_env() {
471        env::set_var("OSC_ENDPOINT_API", "https://api.custom.com");
472        env::set_var("OSC_ENDPOINT_FCU", "https://fcu.custom.com");
473
474        let builder = EndpointBuilder::new();
475        let updated_builder = builder.from_env().unwrap();
476        let endpoint = updated_builder.build("https", "eu-west-2");
477
478        assert_eq!(endpoint.api, "https://api.custom.com");
479        assert_eq!(endpoint.fcu, "https://fcu.custom.com");
480    }
481
482    #[test]
483    fn test_profile_builder_default() {
484        let builder = ProfileBuilder::new();
485        let profile = builder.build();
486
487        assert_eq!(profile.protocol, "https");
488        assert_eq!(profile.region, "eu-west-2");
489    }
490
491    #[test]
492    fn test_profile_builder_from_env() {
493        env::set_var("OSC_ACCESS_KEY", "test_key");
494        env::set_var("OSC_SECRET_KEY", "test_secret");
495
496        let builder = ProfileBuilder::new();
497        let updated_builder = builder.from_env().unwrap();
498
499        assert_eq!(updated_builder.access_key.unwrap(), "test_key");
500        assert_eq!(updated_builder.secret_key.unwrap(), "test_secret");
501    }
502    #[test]
503    fn test_full_profile_build() {
504        let profile = ProfileBuilder::new()
505            .access_key("my_access_key", "my_secret_key")
506            .protocol("http")
507            .region("us-west-1")
508            .build();
509
510        assert_eq!(profile.access_key.unwrap(), "my_access_key");
511        assert_eq!(profile.secret_key.unwrap(), "my_secret_key");
512        assert_eq!(profile.protocol, "http");
513        assert_eq!(profile.region, "us-west-1");
514    }
515}