aws_config/profile/
region.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! Load a region from an AWS profile
7
8use crate::meta::region::{future, ProvideRegion};
9#[allow(deprecated)]
10use crate::profile::profile_file::ProfileFiles;
11use crate::profile::ProfileSet;
12use crate::provider_config::ProviderConfig;
13use aws_types::region::Region;
14
15/// Load a region from a profile file
16///
17/// This provider will attempt to load AWS shared configuration, then read the `region` property
18/// from the active profile.
19///
20#[doc = include_str!("location_of_profile_files.md")]
21///
22/// # Examples
23///
24/// **Loads "us-west-2" as the region**
25/// ```ini
26/// [default]
27/// region = us-west-2
28/// ```
29///
30/// **Loads `us-east-1` as the region _if and only if_ the `AWS_PROFILE` environment variable is set
31/// to `other`.**
32///
33/// ```ini
34/// [profile other]
35/// region = us-east-1
36/// ```
37///
38/// This provider is part of the [default region provider chain](crate::default_provider::region).
39#[derive(Debug, Default)]
40pub struct ProfileFileRegionProvider {
41    provider_config: ProviderConfig,
42}
43
44/// Builder for [ProfileFileRegionProvider]
45#[derive(Debug, Default)]
46pub struct Builder {
47    config: Option<ProviderConfig>,
48    profile_override: Option<String>,
49    #[allow(deprecated)]
50    profile_files: Option<ProfileFiles>,
51}
52
53impl Builder {
54    /// Override the configuration for this provider
55    pub fn configure(mut self, config: &ProviderConfig) -> Self {
56        self.config = Some(config.clone());
57        self
58    }
59
60    /// Override the profile name used by the [`ProfileFileRegionProvider`]
61    pub fn profile_name(mut self, profile_name: impl Into<String>) -> Self {
62        self.profile_override = Some(profile_name.into());
63        self
64    }
65
66    /// Set the profile file that should be used by the [`ProfileFileRegionProvider`]
67    #[allow(deprecated)]
68    pub fn profile_files(mut self, profile_files: ProfileFiles) -> Self {
69        self.profile_files = Some(profile_files);
70        self
71    }
72
73    /// Build a [ProfileFileRegionProvider] from this builder
74    pub fn build(self) -> ProfileFileRegionProvider {
75        let conf = self
76            .config
77            .unwrap_or_default()
78            .with_profile_config(self.profile_files, self.profile_override);
79        ProfileFileRegionProvider {
80            provider_config: conf,
81        }
82    }
83}
84
85impl ProfileFileRegionProvider {
86    /// Create a new [ProfileFileRegionProvider]
87    ///
88    /// To override the selected profile, set the `AWS_PROFILE` environment variable or use the [`Builder`].
89    pub fn new() -> Self {
90        Self {
91            provider_config: ProviderConfig::default(),
92        }
93    }
94
95    /// [`Builder`] to construct a [`ProfileFileRegionProvider`]
96    pub fn builder() -> Builder {
97        Builder::default()
98    }
99
100    async fn region(&self) -> Option<Region> {
101        let profile_set = self.provider_config.profile().await?;
102
103        resolve_profile_chain_for_region(profile_set)
104    }
105}
106
107fn resolve_profile_chain_for_region(profile_set: &'_ ProfileSet) -> Option<Region> {
108    if profile_set.is_empty() {
109        return None;
110    }
111
112    let mut selected_profile = profile_set.selected_profile();
113    let mut visited_profiles = vec![];
114
115    loop {
116        let profile = profile_set.get_profile(selected_profile)?;
117        // Check to see if we're in a loop and return if that's true.
118        // Else, add the profile we're currently checking to our list of visited profiles.
119        if visited_profiles.contains(&selected_profile) {
120            return None;
121        } else {
122            visited_profiles.push(selected_profile);
123        }
124
125        // Attempt to get region and source_profile for current profile
126        let selected_profile_region = profile
127            .get("region")
128            .map(|region| Region::new(region.to_owned()));
129        let source_profile = profile.get("source_profile");
130
131        // Check to see what we got
132        match (selected_profile_region, source_profile) {
133            // Profile had a region specified, return it :D
134            (Some(region), _) => {
135                return Some(region);
136            }
137            // No region specified, source_profile is self-referential so we return to avoid infinite loop
138            (None, Some(source_profile)) if source_profile == selected_profile => {
139                return None;
140            }
141            // No region specified, no source_profile specified so we return empty-handed
142            (None, None) => {
143                return None;
144            }
145            // No region specified, check source profile for a region in next loop iteration
146            (None, Some(source_profile)) => {
147                selected_profile = source_profile;
148            }
149        }
150    }
151}
152
153impl ProvideRegion for ProfileFileRegionProvider {
154    fn region(&self) -> future::ProvideRegion<'_> {
155        future::ProvideRegion::new(self.region())
156    }
157}
158
159#[cfg(test)]
160mod test {
161    use crate::profile::ProfileFileRegionProvider;
162    use crate::provider_config::ProviderConfig;
163    use crate::test_case::no_traffic_client;
164    use aws_types::os_shim_internal::{Env, Fs};
165    use aws_types::region::Region;
166    use futures_util::FutureExt;
167    use tracing_test::traced_test;
168
169    fn provider_config(dir_name: &str) -> ProviderConfig {
170        let fs = Fs::from_test_dir(format!("test-data/profile-provider/{}/fs", dir_name), "/");
171        let env = Env::from_slice(&[("HOME", "/home")]);
172        ProviderConfig::empty()
173            .with_fs(fs)
174            .with_env(env)
175            .with_http_client(no_traffic_client())
176    }
177
178    #[traced_test]
179    #[test]
180    fn load_region() {
181        let provider = ProfileFileRegionProvider::builder()
182            .configure(&provider_config("region_override"))
183            .build();
184        assert_eq!(
185            provider.region().now_or_never().unwrap(),
186            Some(Region::from_static("us-east-1"))
187        );
188    }
189
190    #[test]
191    fn load_region_env_profile_override() {
192        let conf = provider_config("region_override").with_env(Env::from_slice(&[
193            ("HOME", "/home"),
194            ("AWS_PROFILE", "base"),
195        ]));
196        let provider = ProfileFileRegionProvider::builder()
197            .configure(&conf)
198            .build();
199        assert_eq!(
200            provider.region().now_or_never().unwrap(),
201            Some(Region::from_static("us-east-1"))
202        );
203    }
204
205    #[test]
206    fn load_region_nonexistent_profile() {
207        let conf = provider_config("region_override").with_env(Env::from_slice(&[
208            ("HOME", "/home"),
209            ("AWS_PROFILE", "doesnotexist"),
210        ]));
211        let provider = ProfileFileRegionProvider::builder()
212            .configure(&conf)
213            .build();
214        assert_eq!(provider.region().now_or_never().unwrap(), None);
215    }
216
217    #[test]
218    fn load_region_explicit_override() {
219        let conf = provider_config("region_override");
220        let provider = ProfileFileRegionProvider::builder()
221            .configure(&conf)
222            .profile_name("base")
223            .build();
224        assert_eq!(
225            provider.region().now_or_never().unwrap(),
226            Some(Region::from_static("us-east-1"))
227        );
228    }
229
230    #[tokio::test]
231    async fn load_region_from_source_profile() {
232        let config = r#"
233[profile credentials]
234aws_access_key_id = test-access-key-id
235aws_secret_access_key = test-secret-access-key
236aws_session_token = test-session-token
237region = us-east-1
238
239[profile needs-source]
240source_profile = credentials
241role_arn = arn:aws:iam::123456789012:role/test
242"#
243        .trim();
244
245        let fs = Fs::from_slice(&[("test_config", config)]);
246        let env = Env::from_slice(&[("AWS_CONFIG_FILE", "test_config")]);
247        let provider_config = ProviderConfig::empty()
248            .with_fs(fs)
249            .with_env(env)
250            .with_http_client(no_traffic_client());
251
252        assert_eq!(
253            Some(Region::new("us-east-1")),
254            ProfileFileRegionProvider::builder()
255                .profile_name("needs-source")
256                .configure(&provider_config)
257                .build()
258                .region()
259                .await
260        );
261    }
262}