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#[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#[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, ®ion);
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}