aws_config/profile/
region.rs1use 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#[doc = include_str!("location_of_profile_files.md")]
21#[derive(Debug, Default)]
40pub struct ProfileFileRegionProvider {
41 provider_config: ProviderConfig,
42}
43
44#[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 pub fn configure(mut self, config: &ProviderConfig) -> Self {
56 self.config = Some(config.clone());
57 self
58 }
59
60 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 #[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 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 pub fn new() -> Self {
90 Self {
91 provider_config: ProviderConfig::default(),
92 }
93 }
94
95 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 if visited_profiles.contains(&selected_profile) {
120 return None;
121 } else {
122 visited_profiles.push(selected_profile);
123 }
124
125 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 match (selected_profile_region, source_profile) {
133 (Some(region), _) => {
135 return Some(region);
136 }
137 (None, Some(source_profile)) if source_profile == selected_profile => {
139 return None;
140 }
141 (None, None) => {
143 return None;
144 }
145 (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}