openstack_sdk/
config.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License at
4//
5//     http://www.apache.org/licenses/LICENSE-2.0
6//
7// Unless required by applicable law or agreed to in writing, software
8// distributed under the License is distributed on an "AS IS" BASIS,
9// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10// See the License for the specific language governing permissions and
11// limitations under the License.
12//
13// SPDX-License-Identifier: Apache-2.0
14
15//! Module to handle OpenStack config
16//!
17//! ```rust
18//! let cfg = openstack_sdk::config::ConfigFile::new().unwrap();
19//! let profile = cfg
20//!     .get_cloud_config("devstack")
21//!     .expect("Cloud devstack not found");
22//! ```
23//!
24//! It is possible to create a config by passing paths to a [builder](ConfigFileBuilder).
25//!
26//! ```no_run
27//! let cfg = openstack_sdk::config::ConfigFile::builder()
28//!     .add_source("c1.yaml")
29//!     .expect("Failed to load 'c1.yaml'")
30//!     .add_source("s2.yaml")
31//!     .expect("Failed to load 's2.yaml'")
32//!     .build();
33//! ```
34//!
35//! It is also possible to create a config with [`ConfigFile::new_with_user_specified_configs`].
36//! This is similar to what the python OpenStackSDK does.
37//!
38//! ```no_run
39//! let cfg = openstack_sdk::config::ConfigFile::new_with_user_specified_configs(
40//!     Some("c1.yaml"),
41//!     Some("s2.yaml"),
42//! ).expect("Failed to load the configuration files");
43//! ```
44
45use secrecy::{ExposeSecret, SecretString};
46use std::fmt;
47use std::path::{Path, PathBuf};
48use tracing::{error, trace, warn};
49
50use serde::Deserialize;
51use std::collections::hash_map::DefaultHasher;
52use std::collections::HashMap;
53use std::collections::HashSet;
54use std::env;
55use std::hash::{Hash, Hasher};
56
57use thiserror::Error;
58
59/// Errors which may occur when dealing with OpenStack connection
60/// configuration data.
61#[derive(Debug, Error)]
62#[non_exhaustive]
63pub enum ConfigError {
64    #[error("Cloud {0} not found")]
65    CloudNotFound(String),
66
67    #[error("Profile {} not found", profile_name)]
68    MissingProfile { profile_name: String },
69
70    #[error("unknown error")]
71    Unknown,
72
73    #[error("failed to deserialize config: {}", source)]
74    Parse {
75        /// The source of the error.
76        #[from]
77        source: config::ConfigError,
78    },
79}
80
81impl ConfigError {
82    pub fn parse(source: config::ConfigError) -> Self {
83        ConfigError::Parse { source }
84    }
85}
86
87/// Errors which may occur when adding sources to the [`ConfigFileBuilder`].
88#[derive(Error)]
89#[non_exhaustive]
90pub enum ConfigFileBuilderError {
91    #[error("Failed to parse file {path:?}: {source}")]
92    FileParse {
93        source: Box<config::ConfigError>,
94        builder: ConfigFileBuilder,
95        path: PathBuf,
96    },
97    #[error("Failed to deserialize config {path:?}: {source}")]
98    ConfigDeserialize {
99        source: Box<config::ConfigError>,
100        builder: ConfigFileBuilder,
101        path: PathBuf,
102    },
103}
104
105impl fmt::Debug for ConfigFileBuilderError {
106    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107        match self {
108            ConfigFileBuilderError::FileParse {
109                ref source,
110                ref path,
111                ..
112            } => f
113                .debug_struct("FileParse")
114                .field("source", source)
115                .field("path", path)
116                .finish_non_exhaustive(),
117            ConfigFileBuilderError::ConfigDeserialize {
118                ref source,
119                ref path,
120                ..
121            } => f
122                .debug_struct("ConfigDeserialize")
123                .field("source", source)
124                .field("path", path)
125                .finish_non_exhaustive(),
126        }
127    }
128}
129
130/// A builder to create a [`ConfigFile`] by specifying which files to load.
131pub struct ConfigFileBuilder {
132    sources: Vec<config::Config>,
133}
134
135impl ConfigFileBuilder {
136    /// Add a source to the builder. This will directly parse the config and check if it is valid.
137    /// Values of sources added first will be overridden by later added sources, if the keys match.
138    /// In other words, the sources will be merged, with the later taking precedence over the
139    /// earlier ones.
140    pub fn add_source(mut self, source: impl AsRef<Path>) -> Result<Self, ConfigFileBuilderError> {
141        let config = match config::Config::builder()
142            .add_source(config::File::from(source.as_ref()))
143            .build()
144        {
145            Ok(config) => config,
146            Err(error) => {
147                return Err(ConfigFileBuilderError::FileParse {
148                    source: Box::new(error),
149                    builder: self,
150                    path: source.as_ref().to_owned(),
151                });
152            }
153        };
154
155        if let Err(error) = config.clone().try_deserialize::<ConfigFile>() {
156            return Err(ConfigFileBuilderError::ConfigDeserialize {
157                source: Box::new(error),
158                builder: self,
159                path: source.as_ref().to_owned(),
160            });
161        }
162
163        self.sources.push(config);
164        Ok(self)
165    }
166
167    /// This will build a [`ConfigFile`] with the previously specified sources. Since
168    /// the sources have already been checked on errors, this will not fail.
169    pub fn build(self) -> ConfigFile {
170        let mut config = config::Config::builder();
171
172        for source in self.sources {
173            config = config.add_source(source);
174        }
175
176        config.build().unwrap().try_deserialize().unwrap()
177    }
178}
179
180/// CacheConfig structure
181#[derive(Deserialize, Debug, Clone)]
182pub struct CacheConfig {
183    pub auth: Option<bool>,
184}
185
186/// ConfigFile structure
187#[derive(Deserialize, Debug, Clone)]
188pub struct ConfigFile {
189    /// Cache configuration
190    pub cache: Option<CacheConfig>,
191    /// clouds configuration
192    pub clouds: Option<HashMap<String, CloudConfig>>,
193    /// vendor clouds information (profiles)
194    #[serde(rename = "public-clouds")]
195    pub public_clouds: Option<HashMap<String, CloudConfig>>,
196}
197
198/// Authentication data
199///
200/// Sensitive fields are wrapped into the
201/// [SensitiveString](https://docs.rs/secrecy/0.10.3/secrecy/type.SecretString.html) to prevent
202/// accidental exposure in logs.
203#[derive(Clone, Default, Deserialize)]
204pub struct Auth {
205    /// Authentication URL
206    pub auth_url: Option<String>,
207    /// Authentication endpoint type (public/internal/admin)
208    pub endpoint: Option<String>,
209    /// Auth Token
210    pub token: Option<SecretString>,
211
212    /// Auth User.Name
213    pub username: Option<String>,
214    /// Auth User.ID
215    pub user_id: Option<String>,
216    /// Auth User.Domain.Name
217    pub user_domain_name: Option<String>,
218    /// Auth User.Domain.ID
219    pub user_domain_id: Option<String>,
220    /// Auth User password
221    pub password: Option<SecretString>,
222
223    /// Auth (totp) MFA passcode
224    pub passcode: Option<SecretString>,
225
226    /// `Domain` scope Domain.ID
227    pub domain_id: Option<String>,
228    /// `Domain` scope Domain.Name
229    pub domain_name: Option<String>,
230    /// `Project` scope Project.ID
231    pub project_id: Option<String>,
232    /// `Project` scope Project.Name
233    pub project_name: Option<String>,
234    /// `Project` scope Project.Domain.ID
235    pub project_domain_id: Option<String>,
236    /// `Project` scope Project.Domain.Name
237    pub project_domain_name: Option<String>,
238
239    /// `Federation` protocol
240    pub protocol: Option<String>,
241    /// `Federation` identity provider
242    pub identity_provider: Option<String>,
243
244    /// `Application Credential` ID
245    pub application_credential_id: Option<String>,
246    /// `Application Credential` Name
247    pub application_credential_name: Option<String>,
248    /// `Application Credential` Secret
249    pub application_credential_secret: Option<SecretString>,
250
251    /// `System scope`
252    pub system_scope: Option<String>,
253}
254
255impl fmt::Debug for Auth {
256    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
257        f.debug_struct("Auth")
258            .field("auth_url", &self.auth_url)
259            .field("domain_id", &self.domain_id)
260            .field("domain_name", &self.domain_name)
261            .field("project_id", &self.project_id)
262            .field("project_name", &self.project_name)
263            .field("project_domain_id", &self.project_domain_id)
264            .field("project_domain_name", &self.project_domain_name)
265            .field("username", &self.username)
266            .field("user_domain_id", &self.user_domain_id)
267            .field("user_domain_name", &self.user_domain_name)
268            .field("protocol", &self.protocol)
269            .field("identity_provider", &self.identity_provider)
270            .field("application_credential_id", &self.application_credential_id)
271            .field(
272                "application_credential_name",
273                &self.application_credential_name,
274            )
275            .field("system_scope", &self.system_scope)
276            .finish()
277    }
278}
279
280/// CloudConfig structure
281#[derive(Deserialize, Default, Clone)]
282pub struct CloudConfig {
283    /// Authorization data
284    pub auth: Option<Auth>,
285    /// Authorization type. While it can be enum it would make hard to extend SDK with custom implementations
286    pub auth_type: Option<String>,
287    /// Authorization methods (in the case when auth_type = `multifactor`.
288    pub auth_methods: Option<Vec<String>>,
289
290    /// Vendor Profile (by name from clouds-public.yaml or TBD: URL)
291    pub profile: Option<String>,
292    /// Interface name to be used for endpoints selection
293    pub interface: Option<String>,
294    /// Region name
295    pub region_name: Option<String>,
296
297    /// Custom CA Certificate
298    pub cacert: Option<String>,
299    /// Verify SSL Certificates
300    pub verify: Option<bool>,
301
302    /// All other options
303    #[serde(flatten)]
304    pub options: HashMap<String, config::Value>,
305}
306
307impl fmt::Debug for CloudConfig {
308    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
309        f.debug_struct("CloudConfig")
310            .field("auth", &self.auth)
311            .finish()
312    }
313}
314
315/// Get a user authentication hash
316pub fn get_config_identity_hash(config: &CloudConfig) -> u64 {
317    // Calculate hash of the auth information
318    let mut s = DefaultHasher::new();
319    if let Some(auth) = &config.auth {
320        if let Some(data) = &auth.auth_url {
321            data.hash(&mut s);
322        }
323        if let Some(data) = &auth.username {
324            data.hash(&mut s);
325        }
326        if let Some(data) = &auth.user_id {
327            data.hash(&mut s);
328        }
329        if let Some(data) = &auth.user_domain_id {
330            data.hash(&mut s);
331        }
332        if let Some(data) = &auth.user_domain_name {
333            data.hash(&mut s);
334        }
335        if let Some(data) = &auth.identity_provider {
336            data.hash(&mut s);
337        }
338        if let Some(data) = &auth.protocol {
339            data.hash(&mut s);
340        }
341        if let Some(data) = &auth.application_credential_name {
342            data.hash(&mut s);
343        }
344        if let Some(data) = &auth.application_credential_id {
345            data.hash(&mut s);
346        }
347        if let Some(data) = &auth.system_scope {
348            data.hash(&mut s);
349        }
350    }
351    if let Some(data) = &config.profile {
352        data.hash(&mut s);
353    }
354    s.finish()
355}
356
357/// CloudConfig struct implementation
358impl CloudConfig {
359    /// Update unset CloudConfig with values from the `update` var
360    pub fn update(&mut self, update: &CloudConfig) {
361        if let Some(update_auth) = &update.auth {
362            let auth = self.auth.get_or_insert(Auth::default());
363            if auth.auth_url.is_none() && update_auth.auth_url.is_some() {
364                auth.auth_url.clone_from(&update_auth.auth_url);
365            }
366            if auth.domain_id.is_none() && update_auth.domain_id.is_some() {
367                auth.domain_id.clone_from(&update_auth.domain_id);
368            }
369            if auth.domain_name.is_none() && update_auth.domain_name.is_some() {
370                auth.domain_name.clone_from(&update_auth.domain_name);
371            }
372            if auth.endpoint.is_none() && update_auth.endpoint.is_some() {
373                auth.endpoint.clone_from(&update_auth.endpoint);
374            }
375            if auth.password.is_none() && update_auth.password.is_some() {
376                auth.password.clone_from(&update_auth.password);
377            }
378            if auth.project_id.is_none() && update_auth.project_id.is_some() {
379                auth.project_id.clone_from(&update_auth.project_id);
380            }
381            if auth.project_name.is_none() && update_auth.project_name.is_some() {
382                auth.project_name.clone_from(&update_auth.project_name);
383            }
384            if auth.project_domain_id.is_none() && update_auth.project_domain_id.is_some() {
385                auth.project_domain_id
386                    .clone_from(&update_auth.project_domain_id);
387            }
388            if auth.project_domain_name.is_none() && update_auth.project_domain_name.is_some() {
389                auth.project_domain_name
390                    .clone_from(&update_auth.project_domain_name);
391            }
392            if auth.token.is_none() && update_auth.token.is_some() {
393                auth.token.clone_from(&update_auth.token);
394            }
395            if auth.username.is_none() && update_auth.username.is_some() {
396                auth.username.clone_from(&update_auth.username);
397            }
398            if auth.user_domain_name.is_none() && update_auth.user_domain_name.is_some() {
399                auth.user_domain_name
400                    .clone_from(&update_auth.user_domain_name);
401            }
402            if auth.user_domain_id.is_none() && update_auth.user_domain_id.is_some() {
403                auth.user_domain_id.clone_from(&update_auth.user_domain_id);
404            }
405            if auth.protocol.is_none() && update_auth.protocol.is_some() {
406                auth.protocol.clone_from(&update_auth.protocol);
407            }
408            if auth.identity_provider.is_none() && update_auth.identity_provider.is_some() {
409                auth.identity_provider
410                    .clone_from(&update_auth.identity_provider);
411            }
412            if auth.application_credential_id.is_none()
413                && update_auth.application_credential_id.is_some()
414            {
415                auth.application_credential_id
416                    .clone_from(&update_auth.application_credential_id);
417            }
418            if auth.application_credential_name.is_none()
419                && update_auth.application_credential_name.is_some()
420            {
421                auth.application_credential_name
422                    .clone_from(&update_auth.application_credential_name);
423            }
424            if auth.application_credential_secret.is_none()
425                && update_auth.application_credential_secret.is_some()
426            {
427                auth.application_credential_secret
428                    .clone_from(&update_auth.application_credential_secret);
429            }
430            if auth.system_scope.is_none() && update_auth.system_scope.is_some() {
431                auth.system_scope.clone_from(&update_auth.system_scope);
432            }
433        }
434        if self.auth_type.is_none() && update.auth_type.is_some() {
435            self.auth_type.clone_from(&update.auth_type);
436        }
437        if self.profile.is_none() && update.profile.is_some() {
438            self.profile.clone_from(&update.profile);
439        }
440        if self.interface.is_none() && update.interface.is_some() {
441            self.interface.clone_from(&update.interface);
442        }
443        if self.region_name.is_none() && update.region_name.is_some() {
444            self.region_name.clone_from(&update.region_name);
445        }
446        if self.cacert.is_none() && update.cacert.is_some() {
447            self.cacert.clone_from(&update.cacert);
448        }
449        if self.verify.is_none() && update.verify.is_some() {
450            self.verify.clone_from(&update.verify);
451        }
452        let current_keys: HashSet<String> = self.options.keys().cloned().collect();
453        self.options.extend(
454            update
455                .options
456                .clone()
457                .into_iter()
458                .filter(|x| !current_keys.contains(&x.0)),
459        );
460    }
461
462    /// Get sensitive config values
463    ///
464    /// This method allows getting list of sensitive values from the config which need to be
465    /// censored from logs. It is purposely only available inside the crate for now to prevent
466    /// users from accidentally logging those. It can be made public though.
467    pub(crate) fn get_sensitive_values(&self) -> Vec<&str> {
468        let mut res = Vec::new();
469        if let Some(auth) = &self.auth {
470            if let Some(val) = &auth.password {
471                res.push(val.expose_secret());
472            }
473            if let Some(val) = &auth.application_credential_secret {
474                res.push(val.expose_secret());
475            }
476            if let Some(val) = &auth.token {
477                res.push(val.expose_secret());
478            }
479            if let Some(val) = &auth.passcode {
480                res.push(val.expose_secret());
481            }
482        }
483        res
484    }
485}
486
487const CONFIG_SUFFIXES: &[&str] = &[".yaml", ".yml", ".json"];
488
489/// Get Paths in which to search for the configuration file
490fn get_config_file_search_paths<S: AsRef<str>>(filename: S) -> Vec<PathBuf> {
491    let paths: Vec<PathBuf> = vec![
492        env::current_dir().expect("Cannot determine current workdir"),
493        dirs::config_dir()
494            .expect("Cannot determine users XDG_CONFIG_HOME")
495            .join("openstack"),
496        dirs::home_dir()
497            .expect("Cannot determine users XDG_HOME")
498            .join(".config/openstack"),
499        PathBuf::from("/etc/openstack"),
500    ];
501
502    paths
503        .iter()
504        .flat_map(|x| {
505            CONFIG_SUFFIXES
506                .iter()
507                .map(|y| x.join(format!("{}{}", filename.as_ref(), y)))
508        })
509        .collect()
510}
511
512/// Searches for a `clouds-public.{yaml,yml,json}` config file.
513///
514/// The following locations will be tried in order:
515///
516/// - `./clouds-public.{yaml,yml,json}` (current working directory)
517/// - `$XDG_CONFIG_HOME/openstack/clouds-public.{yaml,yml,json}`
518/// - `$XDG_HOME/.config/openstack/clouds-public.{yaml,yml,json}`
519/// - `/etc/openstack/clouds-public.{yaml,yml,json}`
520pub fn find_vendor_file() -> Option<PathBuf> {
521    get_config_file_search_paths("clouds-public")
522        .into_iter()
523        .find(|path| path.is_file())
524}
525
526/// Searches for a `clouds.{yaml,yml,json}` config file.
527///
528/// The following locations will be tried in order:
529///
530/// - `./clouds.{yaml,yml,json}` (current working directory)
531/// - `$XDG_CONFIG_HOME/openstack/clouds.{yaml,yml,json}`
532/// - `$XDG_HOME/.config/openstack/clouds.{yaml,yml,json}`
533/// - `/etc/openstack/clouds.{yaml,yml,json}`
534pub fn find_clouds_file() -> Option<PathBuf> {
535    get_config_file_search_paths("clouds")
536        .into_iter()
537        .find(|path| path.is_file())
538}
539
540/// Searches for a `secure.{yaml,yml,json}` config file.
541///
542/// The following locations will be tried in order:
543///
544/// - `./secure.{yaml,yml,json}` (current working directory)
545/// - `$XDG_CONFIG_HOME/openstack/secure.{yaml,yml,json}`
546/// - `$XDG_HOME/.config/openstack/secure.{yaml,yml,json}`
547/// - `/etc/openstack/secure.{yaml,yml,json}`
548pub fn find_secure_file() -> Option<PathBuf> {
549    get_config_file_search_paths("secure")
550        .into_iter()
551        .find(|path| path.is_file())
552}
553
554impl ConfigFile {
555    /// A builder to create a `ConfigFile` by specifying which files to load.
556    pub fn builder() -> ConfigFileBuilder {
557        ConfigFileBuilder {
558            sources: Vec::new(),
559        }
560    }
561
562    /// Create a `ConfigFile` which also loads the default sources in the following order:
563    ///
564    /// - `clouds-public.{yaml,yml,json}` (see [`find_vendor_file`] for search paths)
565    /// - `clouds.{yaml,yml,json}` (see [`find_clouds_file`] for search paths)
566    /// - the provided clouds file
567    /// - `secure.{yaml,yml,json}` (see [`find_secure_file`] for search paths)
568    /// - the provided secure file
569    ///
570    /// If a source is not a valid config it will be ignored, but if one of the sources
571    /// has syntax errors (YAML/JSON) or one of the user specified configs does not
572    /// exist, a [`ConfigError`] will be returned.
573    pub fn new_with_user_specified_configs(
574        clouds: Option<impl AsRef<Path>>,
575        secure: Option<impl AsRef<Path>>,
576    ) -> Result<Self, ConfigError> {
577        let mut builder = Self::builder();
578
579        for path in find_vendor_file()
580            .into_iter()
581            .chain(find_clouds_file())
582            .chain(clouds.map(|path| path.as_ref().to_owned()))
583            .chain(find_secure_file())
584            .chain(secure.map(|path| path.as_ref().to_owned()))
585        {
586            builder = match builder.add_source(&path) {
587                Ok(builder) => {
588                    trace!("Using config file {path:?}");
589                    builder
590                }
591                Err(ConfigFileBuilderError::FileParse { source, .. }) => {
592                    return Err(ConfigError::parse(*source));
593                }
594                Err(ConfigFileBuilderError::ConfigDeserialize {
595                    source,
596                    builder,
597                    path,
598                }) => {
599                    error!(
600                        "The file {path:?} could not be deserialized and will be ignored: {source}"
601                    );
602                    builder
603                }
604            };
605        }
606
607        Ok(builder.build())
608    }
609
610    /// A convenience function which calls [`new_with_user_specified_configs`](ConfigFile::new_with_user_specified_configs) without an
611    /// additional clouds or secret file.
612    pub fn new() -> Result<Self, ConfigError> {
613        Self::new_with_user_specified_configs(None::<PathBuf>, None::<PathBuf>)
614    }
615
616    /// Get cloud connection configuration by name.
617    ///
618    /// This method does not raise exception when the cloud is
619    /// not found.
620    pub fn get_cloud_config<S: AsRef<str>>(
621        &self,
622        cloud_name: S,
623    ) -> Result<Option<CloudConfig>, ConfigError> {
624        if let Some(clouds) = &self.clouds {
625            if let Some(cfg) = clouds.get(cloud_name.as_ref()) {
626                let mut config = cfg.clone();
627                if let Some(ref profile_name) = config.profile {
628                    let mut profile_definition: Option<&CloudConfig> = None;
629                    // TODO: profile may be URL to .well_known
630                    // Merge profile
631                    match &self.public_clouds {
632                        Some(profiles) => {
633                            profile_definition = profiles.get(profile_name);
634                        }
635                        None => {
636                            warn!("Cannot find profiles definition");
637                        }
638                    }
639                    if let Some(profile) = profile_definition {
640                        config.update(profile);
641                    }
642                }
643
644                return Ok(Some(config));
645            }
646        }
647        Ok(None)
648    }
649
650    /// Return true if auth caching is enabled
651    pub fn is_auth_cache_enabled(&self) -> bool {
652        self.cache.as_ref().and_then(|c| c.auth).unwrap_or(true)
653    }
654
655    /// Return list of available cloud connections
656    pub fn get_available_clouds(&self) -> Vec<String> {
657        if let Some(clouds) = &self.clouds {
658            return clouds.keys().cloned().collect();
659        }
660        Vec::new()
661    }
662}
663
664#[cfg(test)]
665mod tests {
666    use crate::config;
667    use secrecy::ExposeSecret;
668    use std::env;
669    use std::io::Write;
670    use std::path::PathBuf;
671    use tempfile::Builder;
672
673    use super::ConfigFile;
674
675    #[test]
676    fn test_get_search_paths() {
677        let fname = "clouds";
678        let cwd = env::current_dir().unwrap();
679        let conf_dir = dirs::config_dir().unwrap().join("openstack");
680        let unix_conf_home = dirs::home_dir().unwrap().join(".config/openstack");
681        let site_conf = PathBuf::from("/etc/openstack");
682        assert_eq!(
683            vec![
684                PathBuf::from(format!("{}/{}.yaml", cwd.display(), fname)),
685                PathBuf::from(format!("{}/{}.yml", cwd.display(), fname)),
686                PathBuf::from(format!("{}/{}.json", cwd.display(), fname)),
687                PathBuf::from(format!("{}/{}.yaml", conf_dir.display(), fname)),
688                PathBuf::from(format!("{}/{}.yml", conf_dir.display(), fname)),
689                PathBuf::from(format!("{}/{}.json", conf_dir.display(), fname)),
690                PathBuf::from(format!("{}/{}.yaml", unix_conf_home.display(), fname)),
691                PathBuf::from(format!("{}/{}.yml", unix_conf_home.display(), fname)),
692                PathBuf::from(format!("{}/{}.json", unix_conf_home.display(), fname)),
693                PathBuf::from(format!("{}/{}.yaml", site_conf.display(), fname)),
694                PathBuf::from(format!("{}/{}.yml", site_conf.display(), fname)),
695                PathBuf::from(format!("{}/{}.json", site_conf.display(), fname)),
696            ],
697            config::get_config_file_search_paths(fname)
698        );
699    }
700
701    #[test]
702    fn test_default_auth_cache_enabled() {
703        let cfg = ConfigFile::new().unwrap();
704        assert!(cfg.is_auth_cache_enabled());
705    }
706
707    #[test]
708    fn test_get_available_clouds() {
709        let cfg = ConfigFile::new().unwrap();
710        let _ = cfg.get_available_clouds();
711    }
712
713    #[test]
714    fn test_from_custom_files() {
715        let mut cloud_file = Builder::new().suffix(".yaml").tempfile().unwrap();
716        let mut secure_file = Builder::new().suffix(".yaml").tempfile().unwrap();
717
718        const CLOUD_DATA: &str = r#"
719            clouds:
720              fake_cloud:
721                auth:
722                  auth_url: http://fake.com
723                  username: override_me
724        "#;
725        const SECURE_DATA: &str = r#"
726            clouds:
727              fake_cloud:
728                auth:
729                  username: foo
730                  password: bar
731        "#;
732
733        write!(cloud_file, "{}", CLOUD_DATA).unwrap();
734        write!(secure_file, "{}", SECURE_DATA).unwrap();
735
736        let cfg = ConfigFile::builder()
737            .add_source(cloud_file.path())
738            .unwrap()
739            .add_source(secure_file.path())
740            .unwrap()
741            .build();
742
743        let profile = cfg
744            .get_cloud_config("fake_cloud")
745            .unwrap()
746            .expect("Profile exists");
747        let auth = profile.auth.expect("Auth defined");
748
749        assert_eq!(auth.auth_url, Some(String::from("http://fake.com")));
750        assert_eq!(auth.username, Some(String::from("foo")));
751        assert_eq!(auth.password.unwrap().expose_secret(), String::from("bar"));
752    }
753
754    #[test]
755    fn test_sensitive_values() {
756        let mut cloud_file = Builder::new().suffix(".yaml").tempfile().unwrap();
757
758        const CLOUD_DATA: &str = r#"
759            clouds:
760              fake_cloud:
761                auth:
762                  auth_url: http://fake.com
763                  username: override_me
764                  password: pwd
765                  application_credential_secret: app_cred_secret
766                  token: tkn
767                  passcode: pcd
768        "#;
769
770        write!(cloud_file, "{}", CLOUD_DATA).unwrap();
771
772        let cfg = ConfigFile::builder()
773            .add_source(cloud_file.path())
774            .unwrap()
775            .build();
776
777        let profile = cfg
778            .get_cloud_config("fake_cloud")
779            .unwrap()
780            .expect("Profile exists");
781        assert_eq!(
782            vec!["pwd", "app_cred_secret", "tkn", "pcd"],
783            profile.get_sensitive_values()
784        );
785    }
786}