aws_runtime/
env_config.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6use crate::env_config::property::PropertiesKey;
7use crate::env_config::section::EnvConfigSections;
8use aws_types::origin::Origin;
9use aws_types::os_shim_internal::Env;
10use aws_types::service_config::ServiceConfigKey;
11use std::borrow::Cow;
12use std::error::Error;
13use std::fmt;
14
15pub mod error;
16pub mod file;
17mod normalize;
18pub mod parse;
19pub mod property;
20pub mod section;
21pub mod source;
22
23/// Given a key, access to the environment, and a validator, return a config value if one was set.
24pub fn get_service_env_config<'a, T, E>(
25    key: ServiceConfigKey<'a>,
26    env: &'a Env,
27    shared_config_sections: Option<&'a EnvConfigSections>,
28    validator: impl Fn(&str) -> Result<T, E>,
29) -> Result<Option<T>, EnvConfigError<E>>
30where
31    E: Error + Send + Sync + 'static,
32{
33    EnvConfigValue::default()
34        .env(key.env())
35        .profile(key.profile())
36        .service_id(key.service_id())
37        .validate(env, shared_config_sections, validator)
38}
39
40#[derive(Debug)]
41enum Location<'a> {
42    Environment,
43    Profile { name: Cow<'a, str> },
44}
45
46impl<'a> fmt::Display for Location<'a> {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        match self {
49            Location::Environment => write!(f, "environment variable"),
50            Location::Profile { name } => write!(f, "profile (`{name}`)"),
51        }
52    }
53}
54
55#[derive(Debug)]
56enum Scope<'a> {
57    Global,
58    Service { service_id: Cow<'a, str> },
59}
60
61impl<'a> fmt::Display for Scope<'a> {
62    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63        match self {
64            Scope::Global => write!(f, "global"),
65            Scope::Service { service_id } => write!(f, "service-specific (`{service_id}`)"),
66        }
67    }
68}
69
70/// The source that env config was derived from.
71///
72/// Includes:
73///
74/// - Whether some config came from a config file or an env var.
75/// - The key used to identify the config value.
76///
77/// Only used when displaying config-extraction errors.
78#[derive(Debug)]
79pub struct EnvConfigSource<'a> {
80    key: Cow<'a, str>,
81    location: Location<'a>,
82    scope: Scope<'a>,
83}
84
85#[allow(clippy::from_over_into)]
86impl Into<Origin> for &EnvConfigSource<'_> {
87    fn into(self) -> Origin {
88        match (&self.scope, &self.location) {
89            (Scope::Global, Location::Environment) => Origin::shared_environment_variable(),
90            (Scope::Global, Location::Profile { .. }) => Origin::shared_profile_file(),
91            (Scope::Service { .. }, Location::Environment) => {
92                Origin::service_environment_variable()
93            }
94            (Scope::Service { .. }, Location::Profile { .. }) => Origin::service_profile_file(),
95        }
96    }
97}
98
99impl<'a> EnvConfigSource<'a> {
100    pub(crate) fn global_from_env(key: Cow<'a, str>) -> Self {
101        Self {
102            key,
103            location: Location::Environment,
104            scope: Scope::Global,
105        }
106    }
107
108    pub(crate) fn global_from_profile(key: Cow<'a, str>, profile_name: Cow<'a, str>) -> Self {
109        Self {
110            key,
111            location: Location::Profile { name: profile_name },
112            scope: Scope::Global,
113        }
114    }
115
116    pub(crate) fn service_from_env(key: Cow<'a, str>, service_id: Cow<'a, str>) -> Self {
117        Self {
118            key,
119            location: Location::Environment,
120            scope: Scope::Service { service_id },
121        }
122    }
123
124    pub(crate) fn service_from_profile(
125        key: Cow<'a, str>,
126        profile_name: Cow<'a, str>,
127        service_id: Cow<'a, str>,
128    ) -> Self {
129        Self {
130            key,
131            location: Location::Profile { name: profile_name },
132            scope: Scope::Service { service_id },
133        }
134    }
135}
136
137impl<'a> fmt::Display for EnvConfigSource<'a> {
138    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139        write!(f, "{} {} key: `{}`", self.scope, self.location, self.key)
140    }
141}
142
143/// An error occurred when resolving config from a user's environment.
144#[derive(Debug)]
145pub struct EnvConfigError<E = Box<dyn Error>> {
146    property_source: String,
147    err: E,
148}
149
150impl<E> EnvConfigError<E> {
151    /// Return a reference to the inner error wrapped by this error.
152    pub fn err(&self) -> &E {
153        &self.err
154    }
155}
156
157impl<E: fmt::Display> fmt::Display for EnvConfigError<E> {
158    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159        write!(f, "{}. source: {}", self.err, self.property_source)
160    }
161}
162
163impl<E: Error> Error for EnvConfigError<E> {
164    fn source(&self) -> Option<&(dyn Error + 'static)> {
165        self.err.source()
166    }
167}
168
169/// Environment config values are config values sourced from a user's environment variables or profile file.
170///
171/// `EnvConfigValue` will first look in the environment, then the AWS profile. They track the
172/// provenance of properties so that unified validation errors can be created.
173#[derive(Default, Debug)]
174pub struct EnvConfigValue<'a> {
175    environment_variable: Option<Cow<'a, str>>,
176    profile_key: Option<Cow<'a, str>>,
177    service_id: Option<Cow<'a, str>>,
178}
179
180impl<'a> EnvConfigValue<'a> {
181    /// Create a new `EnvConfigValue`
182    pub fn new() -> Self {
183        Self::default()
184    }
185
186    /// Set the environment variable to read
187    pub fn env(mut self, key: &'a str) -> Self {
188        self.environment_variable = Some(Cow::Borrowed(key));
189        self
190    }
191
192    /// Set the profile key to read
193    pub fn profile(mut self, key: &'a str) -> Self {
194        self.profile_key = Some(Cow::Borrowed(key));
195        self
196    }
197
198    /// Set the service id to check for service config
199    pub fn service_id(mut self, service_id: &'a str) -> Self {
200        self.service_id = Some(Cow::Borrowed(service_id));
201        self
202    }
203
204    /// Load the value from the env or profile files, validating with `validator`
205    pub fn validate<T, E: Error + Send + Sync + 'static>(
206        self,
207        env: &Env,
208        profiles: Option<&EnvConfigSections>,
209        validator: impl Fn(&str) -> Result<T, E>,
210    ) -> Result<Option<T>, EnvConfigError<E>> {
211        let value = self.load(env, profiles);
212        value
213            .map(|(v, ctx)| {
214                validator(v.as_ref()).map_err(|err| EnvConfigError {
215                    property_source: format!("{}", ctx),
216                    err,
217                })
218            })
219            .transpose()
220    }
221
222    /// Load the value from the env or profile files, validating with `validator`
223    ///
224    /// This version of the function will also return the origin of the config.
225    pub fn validate_and_return_origin<T, E: Error + Send + Sync + 'static>(
226        self,
227        env: &Env,
228        profiles: Option<&EnvConfigSections>,
229        validator: impl Fn(&str) -> Result<T, E>,
230    ) -> Result<(Option<T>, Origin), EnvConfigError<E>> {
231        let value = self.load(env, profiles);
232        match value {
233            Some((v, ctx)) => {
234                let origin: Origin = (&ctx).into();
235                validator(v.as_ref())
236                    .map_err(|err| EnvConfigError {
237                        property_source: format!("{}", ctx),
238                        err,
239                    })
240                    .map(|value| (Some(value), origin))
241            }
242            None => Ok((None, Origin::unknown())),
243        }
244    }
245
246    /// Load the value from the environment
247    pub fn load(
248        &self,
249        env: &'a Env,
250        profiles: Option<&'a EnvConfigSections>,
251    ) -> Option<(Cow<'a, str>, EnvConfigSource<'a>)> {
252        let env_value = self.environment_variable.as_ref().and_then(|env_var| {
253            // Check for a service-specific env var first
254            let service_config =
255                get_service_config_from_env(env, self.service_id.clone(), env_var.clone());
256            // Then check for a global env var
257            let global_config = env.get(env_var).ok().map(|value| {
258                (
259                    Cow::Owned(value),
260                    EnvConfigSource::global_from_env(env_var.clone()),
261                )
262            });
263
264            if let Some(v) = service_config {
265                tracing::trace!("(service env) {env_var} = {v:?}");
266                Some(v)
267            } else if let Some(v) = global_config {
268                tracing::trace!("(global env) {env_var} = {v:?}");
269                Some(v)
270            } else {
271                tracing::trace!("(env) no value set for {env_var}");
272                None
273            }
274        });
275
276        let profile_value = match (profiles, self.profile_key.as_ref()) {
277            (Some(profiles), Some(profile_key)) => {
278                // Check for a service-specific profile key first
279                let service_config = get_service_config_from_profile(
280                    profiles,
281                    self.service_id.clone(),
282                    profile_key.clone(),
283                );
284                let global_config = profiles.get(profile_key.as_ref()).map(|value| {
285                    (
286                        Cow::Borrowed(value),
287                        EnvConfigSource::global_from_profile(
288                            profile_key.clone(),
289                            Cow::Owned(profiles.selected_profile().to_owned()),
290                        ),
291                    )
292                });
293
294                if let Some(v) = service_config {
295                    tracing::trace!("(service profile) {profile_key} = {v:?}");
296                    Some(v)
297                } else if let Some(v) = global_config {
298                    tracing::trace!("(global profile) {profile_key} = {v:?}");
299                    Some(v)
300                } else {
301                    tracing::trace!("(service profile) no value set for {profile_key}");
302                    None
303                }
304            }
305            _ => None,
306        };
307
308        env_value.or(profile_value)
309    }
310}
311
312fn get_service_config_from_env<'a>(
313    env: &'a Env,
314    service_id: Option<Cow<'a, str>>,
315    env_var: Cow<'a, str>,
316) -> Option<(Cow<'a, str>, EnvConfigSource<'a>)> {
317    let service_id = service_id?;
318    let env_case_service_id = format_service_id_for_env(service_id.clone());
319    let service_specific_env_key = format!("{env_var}_{env_case_service_id}");
320    let env_var = env.get(&service_specific_env_key).ok()?;
321    let env_var: Cow<'_, str> = Cow::Owned(env_var);
322    let source = EnvConfigSource::service_from_env(env_var.clone(), service_id);
323
324    Some((env_var, source))
325}
326
327const SERVICES: &str = "services";
328
329fn get_service_config_from_profile<'a>(
330    profile: &EnvConfigSections,
331    service_id: Option<Cow<'a, str>>,
332    profile_key: Cow<'a, str>,
333) -> Option<(Cow<'a, str>, EnvConfigSource<'a>)> {
334    let service_id = service_id?.clone();
335    let profile_case_service_id = format_service_id_for_profile(service_id.clone());
336    let services_section_name = profile.get(SERVICES)?;
337    let properties_key = PropertiesKey::builder()
338        .section_key(SERVICES)
339        .section_name(services_section_name)
340        .property_name(profile_case_service_id)
341        .sub_property_name(profile_key.clone())
342        .build()
343        .ok()?;
344    let value = profile.other_sections().get(&properties_key)?;
345    let profile_name = Cow::Owned(profile.selected_profile().to_owned());
346    let source = EnvConfigSource::service_from_profile(profile_key, profile_name, service_id);
347
348    Some((Cow::Owned(value.to_owned()), source))
349}
350
351fn format_service_id_for_env(service_id: impl AsRef<str>) -> String {
352    service_id.as_ref().to_uppercase().replace(' ', "_")
353}
354
355fn format_service_id_for_profile(service_id: impl AsRef<str>) -> String {
356    service_id.as_ref().to_lowercase().replace(' ', "-")
357}
358
359#[cfg(test)]
360mod test {
361    use crate::env_config::property::{Properties, PropertiesKey};
362    use crate::env_config::section::EnvConfigSections;
363    use aws_types::os_shim_internal::Env;
364    use std::borrow::Cow;
365    use std::collections::HashMap;
366    use std::num::ParseIntError;
367
368    use super::EnvConfigValue;
369
370    fn validate_some_key(s: &str) -> Result<i32, ParseIntError> {
371        s.parse()
372    }
373
374    fn new_prop_key(
375        section_key: impl Into<String>,
376        section_name: impl Into<String>,
377        property_name: impl Into<String>,
378        sub_property_name: Option<impl Into<String>>,
379    ) -> PropertiesKey {
380        let mut builder = PropertiesKey::builder()
381            .section_key(section_key)
382            .section_name(section_name)
383            .property_name(property_name);
384
385        if let Some(sub_property_name) = sub_property_name {
386            builder = builder.sub_property_name(sub_property_name);
387        }
388
389        builder.build().unwrap()
390    }
391
392    #[tokio::test]
393    async fn test_service_config_multiple_services() {
394        let env = Env::from_slice(&[
395            ("AWS_CONFIG_FILE", "config"),
396            ("AWS_SOME_KEY", "1"),
397            ("AWS_SOME_KEY_SERVICE", "2"),
398            ("AWS_SOME_KEY_ANOTHER_SERVICE", "3"),
399        ]);
400        let profiles = EnvConfigSections::new(
401            HashMap::from([(
402                "default".to_owned(),
403                HashMap::from([
404                    ("some_key".to_owned(), "4".to_owned()),
405                    ("services".to_owned(), "dev".to_owned()),
406                ]),
407            )]),
408            Cow::Borrowed("default"),
409            HashMap::new(),
410            Properties::new_from_slice(&[
411                (
412                    new_prop_key("services", "dev", "service", Some("some_key")),
413                    "5".to_string(),
414                ),
415                (
416                    new_prop_key("services", "dev", "another_service", Some("some_key")),
417                    "6".to_string(),
418                ),
419            ]),
420        );
421        let profiles = Some(&profiles);
422        let global_from_env = EnvConfigValue::new()
423            .env("AWS_SOME_KEY")
424            .profile("some_key")
425            .validate(&env, profiles, validate_some_key)
426            .expect("config resolution succeeds");
427        assert_eq!(Some(1), global_from_env);
428
429        let service_from_env = EnvConfigValue::new()
430            .env("AWS_SOME_KEY")
431            .profile("some_key")
432            .service_id("service")
433            .validate(&env, profiles, validate_some_key)
434            .expect("config resolution succeeds");
435        assert_eq!(Some(2), service_from_env);
436
437        let other_service_from_env = EnvConfigValue::new()
438            .env("AWS_SOME_KEY")
439            .profile("some_key")
440            .service_id("another_service")
441            .validate(&env, profiles, validate_some_key)
442            .expect("config resolution succeeds");
443        assert_eq!(Some(3), other_service_from_env);
444
445        let global_from_profile = EnvConfigValue::new()
446            .profile("some_key")
447            .validate(&env, profiles, validate_some_key)
448            .expect("config resolution succeeds");
449        assert_eq!(Some(4), global_from_profile);
450
451        let service_from_profile = EnvConfigValue::new()
452            .profile("some_key")
453            .service_id("service")
454            .validate(&env, profiles, validate_some_key)
455            .expect("config resolution succeeds");
456        assert_eq!(Some(5), service_from_profile);
457
458        let service_from_profile = EnvConfigValue::new()
459            .profile("some_key")
460            .service_id("another_service")
461            .validate(&env, profiles, validate_some_key)
462            .expect("config resolution succeeds");
463        assert_eq!(Some(6), service_from_profile);
464    }
465
466    #[tokio::test]
467    async fn test_service_config_precedence() {
468        let env = Env::from_slice(&[
469            ("AWS_CONFIG_FILE", "config"),
470            ("AWS_SOME_KEY", "1"),
471            ("AWS_SOME_KEY_S3", "2"),
472        ]);
473
474        let profiles = EnvConfigSections::new(
475            HashMap::from([(
476                "default".to_owned(),
477                HashMap::from([
478                    ("some_key".to_owned(), "3".to_owned()),
479                    ("services".to_owned(), "dev".to_owned()),
480                ]),
481            )]),
482            Cow::Borrowed("default"),
483            HashMap::new(),
484            Properties::new_from_slice(&[(
485                new_prop_key("services", "dev", "s3", Some("some_key")),
486                "4".to_string(),
487            )]),
488        );
489        let profiles = Some(&profiles);
490        let global_from_env = EnvConfigValue::new()
491            .env("AWS_SOME_KEY")
492            .profile("some_key")
493            .validate(&env, profiles, validate_some_key)
494            .expect("config resolution succeeds");
495        assert_eq!(Some(1), global_from_env);
496
497        let service_from_env = EnvConfigValue::new()
498            .env("AWS_SOME_KEY")
499            .profile("some_key")
500            .service_id("s3")
501            .validate(&env, profiles, validate_some_key)
502            .expect("config resolution succeeds");
503        assert_eq!(Some(2), service_from_env);
504
505        let global_from_profile = EnvConfigValue::new()
506            .profile("some_key")
507            .validate(&env, profiles, validate_some_key)
508            .expect("config resolution succeeds");
509        assert_eq!(Some(3), global_from_profile);
510
511        let service_from_profile = EnvConfigValue::new()
512            .profile("some_key")
513            .service_id("s3")
514            .validate(&env, profiles, validate_some_key)
515            .expect("config resolution succeeds");
516        assert_eq!(Some(4), service_from_profile);
517    }
518
519    #[tokio::test]
520    async fn test_multiple_services() {
521        let env = Env::from_slice(&[
522            ("AWS_CONFIG_FILE", "config"),
523            ("AWS_SOME_KEY", "1"),
524            ("AWS_SOME_KEY_S3", "2"),
525            ("AWS_SOME_KEY_EC2", "3"),
526        ]);
527
528        let profiles = EnvConfigSections::new(
529            HashMap::from([(
530                "default".to_owned(),
531                HashMap::from([
532                    ("some_key".to_owned(), "4".to_owned()),
533                    ("services".to_owned(), "dev".to_owned()),
534                ]),
535            )]),
536            Cow::Borrowed("default"),
537            HashMap::new(),
538            Properties::new_from_slice(&[
539                (
540                    new_prop_key("services", "dev-wrong", "s3", Some("some_key")),
541                    "998".into(),
542                ),
543                (
544                    new_prop_key("services", "dev-wrong", "ec2", Some("some_key")),
545                    "999".into(),
546                ),
547                (
548                    new_prop_key("services", "dev", "s3", Some("some_key")),
549                    "5".into(),
550                ),
551                (
552                    new_prop_key("services", "dev", "ec2", Some("some_key")),
553                    "6".into(),
554                ),
555            ]),
556        );
557        let profiles = Some(&profiles);
558        let global_from_env = EnvConfigValue::new()
559            .env("AWS_SOME_KEY")
560            .profile("some_key")
561            .validate(&env, profiles, validate_some_key)
562            .expect("config resolution succeeds");
563        assert_eq!(Some(1), global_from_env);
564
565        let service_from_env = EnvConfigValue::new()
566            .env("AWS_SOME_KEY")
567            .profile("some_key")
568            .service_id("s3")
569            .validate(&env, profiles, validate_some_key)
570            .expect("config resolution succeeds");
571        assert_eq!(Some(2), service_from_env);
572
573        let service_from_env = EnvConfigValue::new()
574            .env("AWS_SOME_KEY")
575            .profile("some_key")
576            .service_id("ec2")
577            .validate(&env, profiles, validate_some_key)
578            .expect("config resolution succeeds");
579        assert_eq!(Some(3), service_from_env);
580
581        let global_from_profile = EnvConfigValue::new()
582            .profile("some_key")
583            .validate(&env, profiles, validate_some_key)
584            .expect("config resolution succeeds");
585        assert_eq!(Some(4), global_from_profile);
586
587        let service_from_profile = EnvConfigValue::new()
588            .profile("some_key")
589            .service_id("s3")
590            .validate(&env, profiles, validate_some_key)
591            .expect("config resolution succeeds");
592        assert_eq!(Some(5), service_from_profile);
593
594        let service_from_profile = EnvConfigValue::new()
595            .profile("some_key")
596            .service_id("ec2")
597            .validate(&env, profiles, validate_some_key)
598            .expect("config resolution succeeds");
599        assert_eq!(Some(6), service_from_profile);
600    }
601}