From 2339db5a59df9b9082fa767a438c31ab0fa411e1 Mon Sep 17 00:00:00 2001 From: Zelda Hessler Date: Tue, 12 Mar 2024 11:31:35 -0500 Subject: [PATCH 01/23] add support for service env config --- .../src/default_provider/app_name.rs | 22 +- .../src/default_provider/retry_config.rs | 32 +- .../src/default_provider/use_dual_stack.rs | 10 +- .../src/default_provider/use_fips.rs | 10 +- .../aws-config/src/env_service_config.rs | 27 + aws/rust-runtime/aws-config/src/lib.rs | 13 +- .../src/{profile/mod.rs => profile.rs} | 17 +- .../aws-config/src/profile/credentials.rs | 7 +- .../src/profile/credentials/repr.rs | 19 +- .../aws-config/src/profile/parser.rs | 353 +------------ .../aws-config/src/profile/profile_file.rs | 248 +-------- .../aws-config/src/profile/region.rs | 4 +- .../aws-config/src/provider_config.rs | 5 +- .../aws-config/src/standard_property.rs | 468 ----------------- aws/rust-runtime/aws-runtime/Cargo.toml | 3 + .../aws-runtime/src/env_config.rs | 493 ++++++++++++++++++ aws/rust-runtime/aws-runtime/src/fs_util.rs | 66 +++ aws/rust-runtime/aws-runtime/src/lib.rs | 9 + aws/rust-runtime/aws-runtime/src/profile.rs | 35 ++ .../src/profile}/error.rs | 4 +- .../src/profile}/normalize.rs | 88 ++-- .../src/profile}/parse.rs | 16 +- .../aws-runtime/src/profile/profile_file.rs | 250 +++++++++ .../aws-runtime/src/profile/profile_set.rs | 343 ++++++++++++ .../src/profile}/section.rs | 296 ++++++----- .../src/profile}/source.rs | 30 +- aws/rust-runtime/aws-types/src/lib.rs | 2 + aws/rust-runtime/aws-types/src/sdk_config.rs | 37 +- .../aws-types/src/service_config.rs | 145 ++++++ .../smithy/rustsdk/AwsCodegenDecorator.kt | 1 + .../amazon/smithy/rustsdk/RegionDecorator.kt | 13 +- .../smithy/rustsdk/SdkConfigDecorator.kt | 14 +- .../rustsdk/ServiceEnvConfigDecorator.kt | 46 ++ .../dynamodb/tests/shared-config.rs | 41 +- 34 files changed, 1832 insertions(+), 1335 deletions(-) create mode 100644 aws/rust-runtime/aws-config/src/env_service_config.rs rename aws/rust-runtime/aws-config/src/{profile/mod.rs => profile.rs} (92%) delete mode 100644 aws/rust-runtime/aws-config/src/standard_property.rs create mode 100644 aws/rust-runtime/aws-runtime/src/env_config.rs create mode 100644 aws/rust-runtime/aws-runtime/src/fs_util.rs create mode 100644 aws/rust-runtime/aws-runtime/src/profile.rs rename aws/rust-runtime/{aws-config/src/profile/parser => aws-runtime/src/profile}/error.rs (94%) rename aws/rust-runtime/{aws-config/src/profile/parser => aws-runtime/src/profile}/normalize.rs (87%) rename aws/rust-runtime/{aws-config/src/profile/parser => aws-runtime/src/profile}/parse.rs (96%) create mode 100644 aws/rust-runtime/aws-runtime/src/profile/profile_file.rs create mode 100644 aws/rust-runtime/aws-runtime/src/profile/profile_set.rs rename aws/rust-runtime/{aws-config/src/profile/parser => aws-runtime/src/profile}/section.rs (51%) rename aws/rust-runtime/{aws-config/src/profile/parser => aws-runtime/src/profile}/source.rs (97%) create mode 100644 aws/rust-runtime/aws-types/src/service_config.rs create mode 100644 aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/ServiceEnvConfigDecorator.kt diff --git a/aws/rust-runtime/aws-config/src/default_provider/app_name.rs b/aws/rust-runtime/aws-config/src/default_provider/app_name.rs index 4507248a41..f9970bc81c 100644 --- a/aws/rust-runtime/aws-config/src/default_provider/app_name.rs +++ b/aws/rust-runtime/aws-config/src/default_provider/app_name.rs @@ -4,7 +4,7 @@ */ use crate::provider_config::ProviderConfig; -use crate::standard_property::{PropertyResolutionError, StandardProperty}; +use aws_runtime::env_config::{EnvConfigError, EnvConfigValue}; use aws_smithy_types::error::display::DisplayErrorContext; use aws_types::app_name::{AppName, InvalidAppName}; @@ -56,22 +56,24 @@ impl Builder { self } - async fn fallback_app_name( - &self, - ) -> Result, PropertyResolutionError> { - StandardProperty::new() + async fn fallback_app_name(&self) -> Result, EnvConfigError> { + let env = self.provider_config.env(); + let profiles = self.provider_config.profile().await; + + EnvConfigValue::new() .profile("sdk-ua-app-id") - .validate(&self.provider_config, |name| AppName::new(name.to_string())) - .await + .validate(&env, profiles, |name| AppName::new(name.to_string())) } /// Build an [`AppName`] from the default chain pub async fn app_name(self) -> Option { - let standard = StandardProperty::new() + let env = self.provider_config.env(); + let profiles = self.provider_config.profile().await; + + let standard = EnvConfigValue::new() .env("AWS_SDK_UA_APP_ID") .profile("sdk_ua_app_id") - .validate(&self.provider_config, |name| AppName::new(name.to_string())) - .await; + .validate(&env, profiles, |name| AppName::new(name.to_string())); let with_fallback = match standard { Ok(None) => self.fallback_app_name().await, other => other, diff --git a/aws/rust-runtime/aws-config/src/default_provider/retry_config.rs b/aws/rust-runtime/aws-config/src/default_provider/retry_config.rs index 955d4a8608..9eb4639c7f 100644 --- a/aws/rust-runtime/aws-config/src/default_provider/retry_config.rs +++ b/aws/rust-runtime/aws-config/src/default_provider/retry_config.rs @@ -5,7 +5,7 @@ use crate::provider_config::ProviderConfig; use crate::retry::error::{RetryConfigError, RetryConfigErrorKind}; -use crate::standard_property::{PropertyResolutionError, StandardProperty}; +use aws_runtime::env_config::{EnvConfigError, EnvConfigValue}; use aws_smithy_types::error::display::DisplayErrorContext; use aws_smithy_types::retry::{RetryConfig, RetryMode}; use std::str::FromStr; @@ -101,29 +101,31 @@ impl Builder { pub(crate) async fn try_retry_config( self, - ) -> Result> { - // Both of these can return errors due to invalid config settings and we want to surface those as early as possible + ) -> Result> { + let env = self.provider_config.env(); + let profiles = self.provider_config.profile().await; + // Both of these can return errors due to invalid config settings, and we want to surface those as early as possible // hence, we'll panic if any config values are invalid (missing values are OK though) - // We match this instead of unwrapping so we can print the error with the `Display` impl instead of the `Debug` impl that unwrap uses + // We match this instead of unwrapping, so we can print the error with the `Display` impl instead of the `Debug` impl that unwrap uses let mut retry_config = RetryConfig::standard(); - let max_attempts = StandardProperty::new() + let max_attempts = EnvConfigValue::new() .env(env::MAX_ATTEMPTS) .profile(profile_keys::MAX_ATTEMPTS) - .validate(&self.provider_config, validate_max_attempts); + .validate(&env, profiles, validate_max_attempts); - let retry_mode = StandardProperty::new() + let retry_mode = EnvConfigValue::new() .env(env::RETRY_MODE) .profile(profile_keys::RETRY_MODE) - .validate(&self.provider_config, |s| { + .validate(&env, profiles, |s| { RetryMode::from_str(s) .map_err(|err| RetryConfigErrorKind::InvalidRetryMode { source: err }.into()) }); - if let Some(max_attempts) = max_attempts.await? { + if let Some(max_attempts) = max_attempts? { retry_config = retry_config.with_max_attempts(max_attempts); } - if let Some(retry_mode) = retry_mode.await? { + if let Some(retry_mode) = retry_mode? { retry_config = retry_config.with_retry_mode(retry_mode); } @@ -146,12 +148,12 @@ mod test { use crate::retry::{ error::RetryConfigError, error::RetryConfigErrorKind, RetryConfig, RetryMode, }; - use crate::standard_property::PropertyResolutionError; + use aws_runtime::env_config::EnvConfigError; use aws_types::os_shim_internal::{Env, Fs}; async fn test_provider( vars: &[(&str, &str)], - ) -> Result> { + ) -> Result> { super::Builder::default() .configure(&ProviderConfig::no_configuration().with_env(Env::from_slice(vars))) .try_retry_config() @@ -296,7 +298,7 @@ max_attempts = potato test_provider(&[(env::MAX_ATTEMPTS, "not an integer")]) .await .unwrap_err() - .err, + .err(), RetryConfigError { kind: RetryConfigErrorKind::FailedToParseMaxAttempts { .. } } @@ -327,8 +329,8 @@ max_attempts = potato async fn disallow_zero_max_attempts() { let err = test_provider(&[(env::MAX_ATTEMPTS, "0")]) .await - .unwrap_err() - .err; + .unwrap_err(); + let err = err.err(); assert!(matches!( err, RetryConfigError { diff --git a/aws/rust-runtime/aws-config/src/default_provider/use_dual_stack.rs b/aws/rust-runtime/aws-config/src/default_provider/use_dual_stack.rs index 00cf17334d..4981da84cf 100644 --- a/aws/rust-runtime/aws-config/src/default_provider/use_dual_stack.rs +++ b/aws/rust-runtime/aws-config/src/default_provider/use_dual_stack.rs @@ -5,7 +5,7 @@ use crate::environment::parse_bool; use crate::provider_config::ProviderConfig; -use crate::standard_property::StandardProperty; +use aws_runtime::env_config::EnvConfigValue; use aws_smithy_types::error::display::DisplayErrorContext; mod env { @@ -17,11 +17,13 @@ mod profile_key { } pub(crate) async fn use_dual_stack_provider(provider_config: &ProviderConfig) -> Option { - StandardProperty::new() + let env = provider_config.env(); + let profiles = provider_config.profile().await; + + EnvConfigValue::new() .env(env::USE_DUAL_STACK) .profile(profile_key::USE_DUAL_STACK) - .validate(provider_config, parse_bool) - .await + .validate(&env, profiles, parse_bool) .map_err( |err| tracing::warn!(err = %DisplayErrorContext(&err), "invalid value for dual-stack setting"), ) diff --git a/aws/rust-runtime/aws-config/src/default_provider/use_fips.rs b/aws/rust-runtime/aws-config/src/default_provider/use_fips.rs index 0bb824d593..4124a6be84 100644 --- a/aws/rust-runtime/aws-config/src/default_provider/use_fips.rs +++ b/aws/rust-runtime/aws-config/src/default_provider/use_fips.rs @@ -5,7 +5,7 @@ use crate::environment::parse_bool; use crate::provider_config::ProviderConfig; -use crate::standard_property::StandardProperty; +use aws_runtime::env_config::EnvConfigValue; use aws_smithy_types::error::display::DisplayErrorContext; mod env { @@ -24,11 +24,13 @@ mod profile_key { /// /// If invalid values are found, the provider will return None and an error will be logged. pub async fn use_fips_provider(provider_config: &ProviderConfig) -> Option { - StandardProperty::new() + let env = provider_config.env(); + let profiles = provider_config.profile().await; + + EnvConfigValue::new() .env(env::USE_FIPS) .profile(profile_key::USE_FIPS) - .validate(provider_config, parse_bool) - .await + .validate(&env, profiles, parse_bool) .map_err( |err| tracing::warn!(err = %DisplayErrorContext(&err), "invalid value for FIPS setting"), ) diff --git a/aws/rust-runtime/aws-config/src/env_service_config.rs b/aws/rust-runtime/aws-config/src/env_service_config.rs new file mode 100644 index 0000000000..9a18039eeb --- /dev/null +++ b/aws/rust-runtime/aws-config/src/env_service_config.rs @@ -0,0 +1,27 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use aws_runtime::env_config::EnvConfigValue; +use aws_runtime::profile::profile_set::ProfileSet; +use aws_types::os_shim_internal::Env; +use aws_types::service_config::{LoadServiceConfig, ServiceConfigKey}; + +#[derive(Debug)] +pub(crate) struct EnvServiceConfig { + pub(crate) env: Env, + pub(crate) profiles: ProfileSet, +} + +impl LoadServiceConfig for EnvServiceConfig { + fn load_config(&self, key: ServiceConfigKey<'_>) -> Option { + let (value, _source) = EnvConfigValue::new() + .env(key.env()) + .profile(key.profile()) + .service_id(key.service_id()) + .load(&self.env, Some(&self.profiles))?; + + Some(value.to_string()) + } +} diff --git a/aws/rust-runtime/aws-config/src/lib.rs b/aws/rust-runtime/aws-config/src/lib.rs index f6d374d04f..ab59c8b17f 100644 --- a/aws/rust-runtime/aws-config/src/lib.rs +++ b/aws/rust-runtime/aws-config/src/lib.rs @@ -128,6 +128,7 @@ mod json_credentials; pub mod credential_process; pub mod default_provider; pub mod ecs; +mod env_service_config; pub mod environment; pub mod imds; pub mod meta; @@ -138,7 +139,6 @@ mod sensitive_command; #[cfg(feature = "sso")] pub mod sso; pub mod stalled_stream_protection; -pub(crate) mod standard_property; pub mod sts; pub mod timeout; pub mod web_identity_token; @@ -211,14 +211,15 @@ mod loader { use crate::default_provider::use_dual_stack::use_dual_stack_provider; use crate::default_provider::use_fips::use_fips_provider; use crate::default_provider::{app_name, credentials, region, retry_config, timeout_config}; + use crate::env_service_config::EnvServiceConfig; use crate::meta::region::ProvideRegion; - use crate::profile::profile_file::ProfileFiles; use crate::provider_config::ProviderConfig; use aws_credential_types::provider::{ token::{ProvideToken, SharedTokenProvider}, ProvideCredentials, SharedCredentialsProvider, }; use aws_credential_types::Credentials; + use aws_runtime::profile::profile_file::ProfileFiles; use aws_smithy_async::rt::sleep::{default_async_sleep, AsyncSleep, SharedAsyncSleep}; use aws_smithy_async::time::{SharedTimeSource, TimeSource}; use aws_smithy_runtime_api::client::behavior_version::BehaviorVersion; @@ -805,11 +806,17 @@ mod loader { } }; + let profiles = conf.profile().await; + let service_config = EnvServiceConfig { + env: conf.env(), + profiles: profiles.cloned().unwrap_or_default(), + }; let mut builder = SdkConfig::builder() .region(region) .retry_config(retry_config) .timeout_config(timeout_config) - .time_source(time_source); + .time_source(time_source) + .service_config(service_config); builder.set_behavior_version(self.behavior_version); builder.set_http_client(self.http_client); diff --git a/aws/rust-runtime/aws-config/src/profile/mod.rs b/aws/rust-runtime/aws-config/src/profile.rs similarity index 92% rename from aws/rust-runtime/aws-config/src/profile/mod.rs rename to aws/rust-runtime/aws-config/src/profile.rs index 51c790c717..840d80a30f 100644 --- a/aws/rust-runtime/aws-config/src/profile/mod.rs +++ b/aws/rust-runtime/aws-config/src/profile.rs @@ -10,15 +10,6 @@ mod parser; -// This can't be included in the other `pub use` statement until -// https://github.com/rust-lang/rust/pull/87487 is fixed by upgrading -// to Rust 1.60 -#[doc(inline)] -pub use parser::ProfileParseError; -pub(crate) use parser::PropertiesKey; -#[doc(inline)] -pub use parser::{load, Profile, ProfileFileLoadError, ProfileSet, Property}; - pub mod credentials; pub mod profile_file; pub mod region; @@ -29,9 +20,17 @@ pub mod token; #[doc(inline)] pub use token::ProfileFileTokenProvider; +#[doc(inline)] +pub use aws_runtime::profile::parse::ProfileParseError; +#[doc(inline)] +pub use aws_runtime::profile::profile_set::ProfileSet; +#[doc(inline)] +pub use aws_runtime::profile::section::{Profile, Property}; #[doc(inline)] pub use credentials::ProfileFileCredentialsProvider; #[doc(inline)] +pub use parser::{load, ProfileFileLoadError}; +#[doc(inline)] pub use region::ProfileFileRegionProvider; mod cell { diff --git a/aws/rust-runtime/aws-config/src/profile/credentials.rs b/aws/rust-runtime/aws-config/src/profile/credentials.rs index 47478a4c42..f54baa325f 100644 --- a/aws/rust-runtime/aws-config/src/profile/credentials.rs +++ b/aws/rust-runtime/aws-config/src/profile/credentials.rs @@ -22,14 +22,15 @@ //! - `exec` which contains a chain representation of providers to implement passing bootstrapped credentials //! through a series of providers. -use crate::profile::profile_file::ProfileFiles; -use crate::profile::Profile; -use crate::profile::{cell::ErrorTakingOnceCell, parser::ProfileFileLoadError}; +use crate::profile::cell::ErrorTakingOnceCell; use crate::provider_config::ProviderConfig; use aws_credential_types::{ provider::{self, error::CredentialsError, future, ProvideCredentials}, Credentials, }; +use aws_runtime::profile::error::ProfileFileLoadError; +use aws_runtime::profile::profile_file::ProfileFiles; +use aws_runtime::profile::section::Profile; use aws_smithy_types::error::display::DisplayErrorContext; use aws_types::SdkConfig; use std::borrow::Cow; diff --git a/aws/rust-runtime/aws-config/src/profile/credentials/repr.rs b/aws/rust-runtime/aws-config/src/profile/credentials/repr.rs index 9f5ff001bd..e4f02eda98 100644 --- a/aws/rust-runtime/aws-config/src/profile/credentials/repr.rs +++ b/aws/rust-runtime/aws-config/src/profile/credentials/repr.rs @@ -13,9 +13,10 @@ //! multiple actions into the same profile). use crate::profile::credentials::ProfileFileError; -use crate::profile::{Profile, ProfileSet}; use crate::sensitive_command::CommandWithSensitiveArgs; use aws_credential_types::Credentials; +use aws_runtime::profile::profile_set::ProfileSet; +use aws_runtime::profile::section::Profile; /// Chain of Profile Providers /// @@ -469,18 +470,17 @@ fn credential_process_from_profile( #[cfg(test)] mod tests { - use crate::profile::credentials::repr::{resolve_chain, BaseProvider, ProfileChain}; - use crate::profile::ProfileSet; + use crate::profile::credentials::repr::{BaseProvider, ProfileChain}; use crate::sensitive_command::CommandWithSensitiveArgs; use serde::Deserialize; use std::collections::HashMap; - use std::error::Error; - use std::fs; + #[cfg(feature = "test-utils")] #[test] - fn run_test_cases() -> Result<(), Box> { - let test_cases: Vec = - serde_json::from_str(&fs::read_to_string("./test-data/assume-role-tests.json")?)?; + fn run_test_cases() -> Result<(), Box> { + let test_cases: Vec = serde_json::from_str(&std::fs::read_to_string( + "./test-data/assume-role-tests.json", + )?)?; for test_case in test_cases { print!("checking: {}...", test_case.docs); check(test_case); @@ -489,7 +489,10 @@ mod tests { Ok(()) } + #[cfg(feature = "test-utils")] fn check(test_case: TestCase) { + use aws_runtime::profile::profile_set::ProfileSet; + crate::profile::credentials::repr::resolve_chain; let source = ProfileSet::new( test_case.input.profiles, test_case.input.selected_profile, diff --git a/aws/rust-runtime/aws-config/src/profile/parser.rs b/aws/rust-runtime/aws-config/src/profile/parser.rs index 8727b1eef4..23ab4c20c6 100644 --- a/aws/rust-runtime/aws-config/src/profile/parser.rs +++ b/aws/rust-runtime/aws-config/src/profile/parser.rs @@ -3,27 +3,18 @@ * SPDX-License-Identifier: Apache-2.0 */ -use self::parse::parse_profile_file; -use self::section::{Section, SsoSession}; -use self::source::Source; -use super::profile_file::ProfileFiles; -use crate::profile::parser::section::Properties; +//! Code for parsing AWS profile config + +use aws_runtime::profile::profile_file::ProfileFiles; +use aws_runtime::profile::source; use aws_types::os_shim_internal::{Env, Fs}; use std::borrow::Cow; -use std::collections::HashMap; - -pub use self::error::ProfileFileLoadError; -pub use self::parse::ProfileParseError; -pub use self::section::Profile; -pub use self::section::Property; -pub(crate) use self::section::PropertiesKey; - -mod error; -mod normalize; -mod parse; -mod section; -mod source; +pub use aws_runtime::profile::error::ProfileFileLoadError; +pub use aws_runtime::profile::parse::ProfileParseError; +pub use aws_runtime::profile::profile_set::ProfileSet; +pub use aws_runtime::profile::section::Profile; +pub use aws_runtime::profile::section::Property; /// Read & parse AWS config files /// @@ -73,329 +64,3 @@ pub async fn load( Ok(ProfileSet::parse(source)?) } - -/// A top-level configuration source containing multiple named profiles -#[derive(Debug, Eq, Clone, PartialEq)] -pub struct ProfileSet { - profiles: HashMap, - selected_profile: Cow<'static, str>, - sso_sessions: HashMap, - other_sections: Properties, -} - -impl ProfileSet { - /// Create a new Profile set directly from a HashMap - /// - /// This method creates a ProfileSet directly from a hashmap with no normalization for test purposes. - #[cfg(test)] - pub(crate) fn new( - profiles: HashMap>, - selected_profile: impl Into>, - sso_sessions: HashMap>, - ) -> Self { - let mut base = ProfileSet::empty(); - base.selected_profile = selected_profile.into(); - for (name, profile) in profiles { - base.profiles.insert( - name.clone(), - Profile::new( - name, - profile - .into_iter() - .map(|(k, v)| (k.clone(), Property::new(k, v))) - .collect(), - ), - ); - } - for (name, session) in sso_sessions { - base.sso_sessions.insert( - name.clone(), - SsoSession::new( - name, - session - .into_iter() - .map(|(k, v)| (k.clone(), Property::new(k, v))) - .collect(), - ), - ); - } - base - } - - /// Retrieves a key-value pair from the currently selected profile - pub fn get(&self, key: &str) -> Option<&str> { - self.profiles - .get(self.selected_profile.as_ref()) - .and_then(|profile| profile.get(key)) - } - - /// Retrieves a named profile from the profile set - pub fn get_profile(&self, profile_name: &str) -> Option<&Profile> { - self.profiles.get(profile_name) - } - - /// Returns the name of the currently selected profile - pub fn selected_profile(&self) -> &str { - self.selected_profile.as_ref() - } - - /// Returns true if no profiles are contained in this profile set - pub fn is_empty(&self) -> bool { - self.profiles.is_empty() - } - - /// Returns the names of the profiles in this config - pub fn profiles(&self) -> impl Iterator { - self.profiles.keys().map(String::as_ref) - } - - /// Returns the names of the SSO sessions in this config - pub fn sso_sessions(&self) -> impl Iterator { - self.sso_sessions.keys().map(String::as_ref) - } - - /// Retrieves a named SSO session from the config - pub(crate) fn sso_session(&self, name: &str) -> Option<&SsoSession> { - self.sso_sessions.get(name) - } - - /// Returns a struct allowing access to other sections in the profile config - #[allow(dead_code)] // Leaving this hidden for now. - pub(crate) fn other_sections(&self) -> &Properties { - &self.other_sections - } - - fn parse(source: Source) -> Result { - let mut base = ProfileSet::empty(); - base.selected_profile = source.profile; - - for file in source.files { - normalize::merge_in(&mut base, parse_profile_file(&file)?, file.kind); - } - Ok(base) - } - - fn empty() -> Self { - Self { - profiles: Default::default(), - selected_profile: "default".into(), - sso_sessions: Default::default(), - other_sections: Default::default(), - } - } -} - -#[cfg(test)] -mod test { - use super::section::Section; - use super::source::{File, Source}; - use crate::profile::profile_file::ProfileFileKind; - use crate::profile::ProfileSet; - use arbitrary::{Arbitrary, Unstructured}; - use serde::Deserialize; - use std::collections::HashMap; - use std::error::Error; - use std::fs; - use tracing_test::traced_test; - - /// Run all tests from `test-data/profile-parser-tests.json` - /// - /// These represent the bulk of the test cases and reach 100% coverage of the parser. - #[test] - #[traced_test] - fn run_tests() -> Result<(), Box> { - let tests = fs::read_to_string("test-data/profile-parser-tests.json")?; - let tests: ParserTests = serde_json::from_str(&tests)?; - for (i, test) in tests.tests.into_iter().enumerate() { - eprintln!("test: {}", i); - check(test); - } - Ok(()) - } - - #[test] - fn empty_source_empty_profile() { - let source = make_source(ParserInput { - config_file: Some("".to_string()), - credentials_file: Some("".to_string()), - }); - - let profile_set = ProfileSet::parse(source).expect("empty profiles are valid"); - assert!(profile_set.is_empty()); - } - - #[test] - fn profile_names_are_exposed() { - let source = make_source(ParserInput { - config_file: Some("[profile foo]\n[profile bar]".to_string()), - credentials_file: Some("".to_string()), - }); - - let profile_set = ProfileSet::parse(source).expect("profiles loaded"); - - let mut profile_names: Vec<_> = profile_set.profiles().collect(); - profile_names.sort(); - assert_eq!(profile_names, vec!["bar", "foo"]); - } - - /// Run all tests from the fuzzing corpus to validate coverage - #[test] - #[ignore] - fn run_fuzz_tests() -> Result<(), Box> { - let fuzz_corpus = fs::read_dir("fuzz/corpus/profile-parser")? - .map(|res| res.map(|entry| entry.path())) - .collect::, _>>()?; - for file in fuzz_corpus { - let raw = fs::read(file)?; - let mut unstructured = Unstructured::new(&raw); - let (conf, creds): (Option<&str>, Option<&str>) = - Arbitrary::arbitrary(&mut unstructured)?; - let profile_source = Source { - files: vec![ - File { - kind: ProfileFileKind::Config, - path: Some("~/.aws/config".to_string()), - contents: conf.unwrap_or_default().to_string(), - }, - File { - kind: ProfileFileKind::Credentials, - path: Some("~/.aws/credentials".to_string()), - contents: creds.unwrap_or_default().to_string(), - }, - ], - profile: "default".into(), - }; - // don't care if parse fails, just don't panic - let _ = ProfileSet::parse(profile_source); - } - - Ok(()) - } - - // for test comparison purposes, flatten a profile into a hashmap - #[derive(Debug)] - struct FlattenedProfileSet { - profiles: HashMap>, - sso_sessions: HashMap>, - } - fn flatten(config: ProfileSet) -> FlattenedProfileSet { - FlattenedProfileSet { - profiles: flatten_sections(config.profiles.values().map(|p| p as _)), - sso_sessions: flatten_sections(config.sso_sessions.values().map(|s| s as _)), - } - } - fn flatten_sections<'a>( - sections: impl Iterator, - ) -> HashMap> { - sections - .map(|section| { - ( - section.name().to_string(), - section - .properties() - .values() - .map(|prop| (prop.key().to_owned(), prop.value().to_owned())) - .collect(), - ) - }) - .collect() - } - - fn make_source(input: ParserInput) -> Source { - Source { - files: vec![ - File { - kind: ProfileFileKind::Config, - path: Some("~/.aws/config".to_string()), - contents: input.config_file.unwrap_or_default(), - }, - File { - kind: ProfileFileKind::Credentials, - path: Some("~/.aws/credentials".to_string()), - contents: input.credentials_file.unwrap_or_default(), - }, - ], - profile: "default".into(), - } - } - - // wrapper to generate nicer errors during test failure - fn check(test_case: ParserTest) { - let copy = test_case.clone(); - let parsed = ProfileSet::parse(make_source(test_case.input)); - let res = match (parsed.map(flatten), &test_case.output) { - ( - Ok(FlattenedProfileSet { - profiles: actual_profiles, - sso_sessions: actual_sso_sessions, - }), - ParserOutput::Config { - profiles, - sso_sessions, - }, - ) => { - if profiles != &actual_profiles { - Err(format!( - "mismatched profiles:\nExpected: {profiles:#?}\nActual: {actual_profiles:#?}", - )) - } else if sso_sessions != &actual_sso_sessions { - Err(format!( - "mismatched sso_sessions:\nExpected: {sso_sessions:#?}\nActual: {actual_sso_sessions:#?}", - )) - } else { - Ok(()) - } - } - (Err(msg), ParserOutput::ErrorContaining(substr)) => { - if format!("{}", msg).contains(substr) { - Ok(()) - } else { - Err(format!("Expected {} to contain {}", msg, substr)) - } - } - (Ok(output), ParserOutput::ErrorContaining(err)) => Err(format!( - "expected an error: {err} but parse succeeded:\n{output:#?}", - )), - (Err(err), ParserOutput::Config { .. }) => { - Err(format!("Expected to succeed but got: {}", err)) - } - }; - if let Err(e) = res { - eprintln!("Test case failed: {:#?}", copy); - eprintln!("failure: {}", e); - panic!("test failed") - } - } - - #[derive(Deserialize, Debug)] - #[serde(rename_all = "camelCase")] - struct ParserTests { - tests: Vec, - } - - #[derive(Deserialize, Debug, Clone)] - #[serde(rename_all = "camelCase")] - struct ParserTest { - _name: String, - input: ParserInput, - output: ParserOutput, - } - - #[derive(Deserialize, Debug, Clone)] - #[serde(rename_all = "camelCase")] - enum ParserOutput { - Config { - profiles: HashMap>, - #[serde(default)] - sso_sessions: HashMap>, - }, - ErrorContaining(String), - } - - #[derive(Deserialize, Debug, Clone)] - #[serde(rename_all = "camelCase")] - struct ParserInput { - config_file: Option, - credentials_file: Option, - } -} diff --git a/aws/rust-runtime/aws-config/src/profile/profile_file.rs b/aws/rust-runtime/aws-config/src/profile/profile_file.rs index dfe9eb33ca..5bad053522 100644 --- a/aws/rust-runtime/aws-config/src/profile/profile_file.rs +++ b/aws/rust-runtime/aws-config/src/profile/profile_file.rs @@ -3,248 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -//! Config structs to programmatically customize the profile files that get loaded - -use std::fmt; -use std::path::PathBuf; - -/// Provides the ability to programmatically override the profile files that get loaded by the SDK. -/// -/// The [`Default`] for `ProfileFiles` includes the default SDK config and credential files located in -/// `~/.aws/config` and `~/.aws/credentials` respectively. -/// -/// Any number of config and credential files may be added to the `ProfileFiles` file set, with the -/// only requirement being that there is at least one of them. Custom file locations that are added -/// will produce errors if they don't exist, while the default config/credentials files paths are -/// allowed to not exist even if they're included. -/// -/// # Example: Using a custom profile file path -/// -/// ```no_run -/// use aws_config::profile::{ProfileFileCredentialsProvider, ProfileFileRegionProvider}; -/// use aws_config::profile::profile_file::{ProfileFiles, ProfileFileKind}; -/// use std::sync::Arc; -/// -/// # async fn example() { -/// let profile_files = ProfileFiles::builder() -/// .with_file(ProfileFileKind::Credentials, "some/path/to/credentials-file") -/// .build(); -/// let sdk_config = aws_config::from_env() -/// .profile_files(profile_files) -/// .load() -/// .await; -/// # } -/// ``` -#[derive(Clone, Debug)] -pub struct ProfileFiles { - pub(crate) files: Vec, -} - -impl ProfileFiles { - /// Returns a builder to create `ProfileFiles` - pub fn builder() -> Builder { - Builder::new() - } -} - -impl Default for ProfileFiles { - fn default() -> Self { - Self { - files: vec![ - ProfileFile::Default(ProfileFileKind::Config), - ProfileFile::Default(ProfileFileKind::Credentials), - ], - } - } -} - -/// Profile file type (config or credentials) -#[derive(Copy, Clone, Debug)] -pub enum ProfileFileKind { - /// The SDK config file that typically resides in `~/.aws/config` - Config, - /// The SDK credentials file that typically resides in `~/.aws/credentials` - Credentials, -} - -impl ProfileFileKind { - pub(crate) fn default_path(&self) -> &'static str { - match &self { - ProfileFileKind::Credentials => "~/.aws/credentials", - ProfileFileKind::Config => "~/.aws/config", - } - } - - pub(crate) fn override_environment_variable(&self) -> &'static str { - match &self { - ProfileFileKind::Config => "AWS_CONFIG_FILE", - ProfileFileKind::Credentials => "AWS_SHARED_CREDENTIALS_FILE", - } - } -} - -/// A single profile file within a [`ProfileFiles`] file set. -#[derive(Clone)] -pub(crate) enum ProfileFile { - /// One of the default profile files (config or credentials in their default locations) - Default(ProfileFileKind), - /// A profile file at a custom location - FilePath { - kind: ProfileFileKind, - path: PathBuf, - }, - /// The direct contents of a profile file - FileContents { - kind: ProfileFileKind, - contents: String, - }, -} - -impl fmt::Debug for ProfileFile { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Default(kind) => f.debug_tuple("Default").field(kind).finish(), - Self::FilePath { kind, path } => f - .debug_struct("FilePath") - .field("kind", kind) - .field("path", path) - .finish(), - // Security: Redact the file contents since they may have credentials in them - Self::FileContents { kind, contents: _ } => f - .debug_struct("FileContents") - .field("kind", kind) - .field("contents", &"** redacted **") - .finish(), - } - } -} - -/// Builder for [`ProfileFiles`]. -#[derive(Clone, Default, Debug)] -pub struct Builder { - with_config: bool, - with_credentials: bool, - custom_sources: Vec, -} - -impl Builder { - /// Creates a new builder instance. - pub fn new() -> Self { - Default::default() - } - - /// Include the default SDK config file in the list of profile files to be loaded. - /// - /// The default SDK config typically resides in `~/.aws/config`. When this flag is enabled, - /// this config file will be included in the profile files that get loaded in the built - /// [`ProfileFiles`] file set. - /// - /// This flag defaults to `false` when using the builder to construct [`ProfileFiles`]. - pub fn include_default_config_file(mut self, include_default_config_file: bool) -> Self { - self.with_config = include_default_config_file; - self - } - - /// Include the default SDK credentials file in the list of profile files to be loaded. - /// - /// The default SDK config typically resides in `~/.aws/credentials`. When this flag is enabled, - /// this credentials file will be included in the profile files that get loaded in the built - /// [`ProfileFiles`] file set. - /// - /// This flag defaults to `false` when using the builder to construct [`ProfileFiles`]. - pub fn include_default_credentials_file( - mut self, - include_default_credentials_file: bool, - ) -> Self { - self.with_credentials = include_default_credentials_file; - self - } - - /// Include a custom `file` in the list of profile files to be loaded. - /// - /// The `kind` informs the parser how to treat the file. If it's intended to be like - /// the SDK credentials file typically in `~/.aws/config`, then use [`ProfileFileKind::Config`]. - /// Otherwise, use [`ProfileFileKind::Credentials`]. - pub fn with_file(mut self, kind: ProfileFileKind, file: impl Into) -> Self { - self.custom_sources.push(ProfileFile::FilePath { - kind, - path: file.into(), - }); - self - } - - /// Include custom file `contents` in the list of profile files to be loaded. - /// - /// The `kind` informs the parser how to treat the file. If it's intended to be like - /// the SDK credentials file typically in `~/.aws/config`, then use [`ProfileFileKind::Config`]. - /// Otherwise, use [`ProfileFileKind::Credentials`]. - pub fn with_contents(mut self, kind: ProfileFileKind, contents: impl Into) -> Self { - self.custom_sources.push(ProfileFile::FileContents { - kind, - contents: contents.into(), - }); - self - } - - /// Build the [`ProfileFiles`] file set. - pub fn build(self) -> ProfileFiles { - let mut files = self.custom_sources; - if self.with_credentials { - files.insert(0, ProfileFile::Default(ProfileFileKind::Credentials)); - } - if self.with_config { - files.insert(0, ProfileFile::Default(ProfileFileKind::Config)); - } - if files.is_empty() { - panic!("At least one profile file must be included in the `ProfileFiles` file set."); - } - ProfileFiles { files } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn redact_file_contents_in_profile_file_debug() { - let profile_file = ProfileFile::FileContents { - kind: ProfileFileKind::Config, - contents: "sensitive_contents".into(), - }; - let debug = format!("{:?}", profile_file); - assert!(!debug.contains("sensitive_contents")); - assert!(debug.contains("** redacted **")); - } - - #[test] - fn build_correctly_orders_default_config_credentials() { - let profile_files = ProfileFiles::builder() - .with_file(ProfileFileKind::Config, "foo") - .include_default_credentials_file(true) - .include_default_config_file(true) - .build(); - assert_eq!(3, profile_files.files.len()); - assert!(matches!( - profile_files.files[0], - ProfileFile::Default(ProfileFileKind::Config) - )); - assert!(matches!( - profile_files.files[1], - ProfileFile::Default(ProfileFileKind::Credentials) - )); - assert!(matches!( - profile_files.files[2], - ProfileFile::FilePath { - kind: ProfileFileKind::Config, - path: _ - } - )); - } - - #[test] - #[should_panic] - fn empty_builder_panics() { - ProfileFiles::builder().build(); - } -} +pub type ProfileFiles = aws_runtime::profile::profile_file::ProfileFiles; +pub type Builder = aws_runtime::profile::profile_file::Builder; +pub type ProfileFileKind = aws_runtime::profile::profile_file::ProfileFileKind; diff --git a/aws/rust-runtime/aws-config/src/profile/region.rs b/aws/rust-runtime/aws-config/src/profile/region.rs index a293733377..e69da8da65 100644 --- a/aws/rust-runtime/aws-config/src/profile/region.rs +++ b/aws/rust-runtime/aws-config/src/profile/region.rs @@ -6,9 +6,9 @@ //! Load a region from an AWS profile use crate::meta::region::{future, ProvideRegion}; -use crate::profile::profile_file::ProfileFiles; -use crate::profile::ProfileSet; use crate::provider_config::ProviderConfig; +use aws_runtime::profile::profile_file::ProfileFiles; +use aws_runtime::profile::profile_set::ProfileSet; use aws_types::region::Region; /// Load a region from a profile file diff --git a/aws/rust-runtime/aws-config/src/provider_config.rs b/aws/rust-runtime/aws-config/src/provider_config.rs index f7865a3d7a..ab9da98777 100644 --- a/aws/rust-runtime/aws-config/src/provider_config.rs +++ b/aws/rust-runtime/aws-config/src/provider_config.rs @@ -6,8 +6,9 @@ //! Configuration Options for Credential Providers use crate::profile; -use crate::profile::profile_file::ProfileFiles; -use crate::profile::{ProfileFileLoadError, ProfileSet}; +use aws_runtime::profile::error::ProfileFileLoadError; +use aws_runtime::profile::profile_file::ProfileFiles; +use aws_runtime::profile::profile_set::ProfileSet; use aws_smithy_async::rt::sleep::{default_async_sleep, AsyncSleep, SharedAsyncSleep}; use aws_smithy_async::time::{SharedTimeSource, TimeSource}; use aws_smithy_runtime_api::client::http::HttpClient; diff --git a/aws/rust-runtime/aws-config/src/standard_property.rs b/aws/rust-runtime/aws-config/src/standard_property.rs deleted file mode 100644 index ace1c23a67..0000000000 --- a/aws/rust-runtime/aws-config/src/standard_property.rs +++ /dev/null @@ -1,468 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -use crate::profile::{ProfileSet, PropertiesKey}; -use crate::provider_config::ProviderConfig; -use std::borrow::Cow; -use std::error::Error; -use std::fmt; - -#[derive(Debug)] -enum Location<'a> { - Environment, - Profile { name: Cow<'a, str> }, -} - -impl<'a> fmt::Display for Location<'a> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Location::Environment => write!(f, "environment variable"), - Location::Profile { name } => write!(f, "profile (`{name}`)"), - } - } -} - -#[derive(Debug)] -enum Scope<'a> { - Global, - Service { service_id: Cow<'a, str> }, -} - -impl<'a> fmt::Display for Scope<'a> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Scope::Global => write!(f, "global"), - Scope::Service { service_id } => write!(f, "service-specific (`{service_id}`)"), - } - } -} - -#[derive(Debug)] -pub(crate) struct PropertySource<'a> { - key: Cow<'a, str>, - location: Location<'a>, - source: Scope<'a>, -} - -impl<'a> PropertySource<'a> { - pub(crate) fn global_from_env(key: Cow<'a, str>) -> Self { - Self { - key, - location: Location::Environment, - source: Scope::Global, - } - } - - pub(crate) fn global_from_profile(key: Cow<'a, str>, profile_name: Cow<'a, str>) -> Self { - Self { - key, - location: Location::Profile { name: profile_name }, - source: Scope::Global, - } - } - - pub(crate) fn service_from_env(key: Cow<'a, str>, service_id: Cow<'a, str>) -> Self { - Self { - key, - location: Location::Environment, - source: Scope::Service { service_id }, - } - } - - pub(crate) fn service_from_profile( - key: Cow<'a, str>, - profile_name: Cow<'a, str>, - service_id: Cow<'a, str>, - ) -> Self { - Self { - key, - location: Location::Profile { name: profile_name }, - source: Scope::Service { service_id }, - } - } -} - -impl<'a> fmt::Display for PropertySource<'a> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{} {} key: `{}`", self.source, self.location, self.key) - } -} - -#[derive(Debug)] -pub(crate) struct PropertyResolutionError> { - property_source: String, - pub(crate) err: E, -} - -impl fmt::Display for PropertyResolutionError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}. source: {}", self.err, self.property_source) - } -} - -impl Error for PropertyResolutionError { - fn source(&self) -> Option<&(dyn Error + 'static)> { - self.err.source() - } -} - -/// Standard properties simplify code that reads properties from the environment and AWS Profile -/// -/// `StandardProperty` will first look in the environment, then the AWS profile. They track the -/// provenance of properties so that unified validation errors can be created. -/// -/// For a usage example, see [`crate::default_provider::retry_config`] -#[derive(Default)] -pub(crate) struct StandardProperty<'a> { - environment_variable: Option>, - profile_key: Option>, - service_id: Option>, -} - -impl<'a> StandardProperty<'a> { - pub(crate) fn new() -> Self { - Self::default() - } - - /// Set the environment variable to read - pub(crate) fn env(mut self, key: &'static str) -> Self { - self.environment_variable = Some(Cow::Borrowed(key)); - self - } - - /// Set the profile key to read - pub(crate) fn profile(mut self, key: &'static str) -> Self { - self.profile_key = Some(Cow::Borrowed(key)); - self - } - - #[allow(dead_code)] - /// Set the service id to check for service config - pub(crate) fn service_id(mut self, service_id: &'static str) -> Self { - self.service_id = Some(Cow::Borrowed(service_id)); - self - } - - /// Load the value from `provider_config`, validating with `validator` - pub(crate) async fn validate( - self, - provider_config: &ProviderConfig, - validator: impl Fn(&str) -> Result, - ) -> Result, PropertyResolutionError> { - let value = self.load(provider_config).await; - value - .map(|(v, ctx)| { - validator(v.as_ref()).map_err(|err| PropertyResolutionError { - property_source: format!("{}", ctx), - err, - }) - }) - .transpose() - } - - /// Load the value from `provider_config` - pub(crate) async fn load( - &self, - provider_config: &'a ProviderConfig, - ) -> Option<(Cow<'a, str>, PropertySource<'a>)> { - let env_value = self.environment_variable.as_ref().and_then(|env_var| { - // Check for a service-specific env var first - get_service_config_from_env(provider_config, self.service_id.clone(), env_var.clone()) - // Then check for a global env var - .or_else(|| { - provider_config.env().get(env_var).ok().map(|value| { - ( - Cow::Owned(value), - PropertySource::global_from_env(env_var.clone()), - ) - }) - }) - }); - - let profile = provider_config.profile().await?; - let profile_value = self.profile_key.as_ref().and_then(|profile_key| { - // Check for a service-specific profile key first - get_service_config_from_profile(profile, self.service_id.clone(), profile_key.clone()) - // Then check for a global profile key - .or_else(|| { - profile.get(profile_key.as_ref()).map(|value| { - ( - Cow::Borrowed(value), - PropertySource::global_from_profile( - profile_key.clone(), - Cow::Owned(profile.selected_profile().to_owned()), - ), - ) - }) - }) - }); - - env_value.or(profile_value) - } -} - -fn get_service_config_from_env<'a>( - provider_config: &'a ProviderConfig, - service_id: Option>, - env_var: Cow<'a, str>, -) -> Option<(Cow<'a, str>, PropertySource<'a>)> { - let service_id = service_id?; - let env_case_service_id = format_service_id_for_env(service_id.clone()); - let service_specific_env_key = format!("{env_var}_{env_case_service_id}"); - let env_var = provider_config.env().get(&service_specific_env_key).ok()?; - let env_var: Cow<'_, str> = Cow::Owned(env_var); - let source = PropertySource::service_from_env(env_var.clone(), service_id); - - Some((env_var, source)) -} - -fn get_service_config_from_profile<'a>( - profile: &ProfileSet, - service_id: Option>, - profile_key: Cow<'a, str>, -) -> Option<(Cow<'a, str>, PropertySource<'a>)> { - let service_id = service_id?.clone(); - let profile_case_service_id = format_service_id_for_profile(service_id.clone()); - - let services_section_name = profile.get("services")?; - let properties_key = PropertiesKey::builder() - .section_key("services") - .section_name(services_section_name) - .property_name(profile_case_service_id) - .sub_property_name(profile_key.clone()) - .build() - .ok()?; - let value = profile.other_sections().get(&properties_key)?; - let profile_name = Cow::Owned(profile.selected_profile().to_owned()); - - let source = PropertySource::service_from_profile(profile_key, profile_name, service_id); - - Some((Cow::Owned(value.clone()), source)) -} - -fn format_service_id_for_env(service_id: impl AsRef) -> String { - service_id.as_ref().to_uppercase().replace(' ', "_") -} - -fn format_service_id_for_profile(service_id: impl AsRef) -> String { - service_id.as_ref().to_lowercase().replace(' ', "-") -} - -#[cfg(test)] -mod test { - use super::StandardProperty; - use crate::provider_config::ProviderConfig; - use aws_types::os_shim_internal::{Env, Fs}; - use std::num::ParseIntError; - - fn validate_some_key(s: &str) -> Result { - s.parse() - } - - #[tokio::test] - async fn test_service_config_multiple_services() { - let env = Env::from_slice(&[ - ("AWS_CONFIG_FILE", "config"), - ("AWS_SOME_KEY", "1"), - ("AWS_SOME_KEY_SERVICE", "2"), - ("AWS_SOME_KEY_ANOTHER_SERVICE", "3"), - ]); - let fs = Fs::from_slice(&[( - "config", - r#"[default] -some_key = 4 -services = dev - -[services dev] -service = - some_key = 5 -another_service = - some_key = 6 -"#, - )]); - - let provider_config = ProviderConfig::no_configuration().with_env(env).with_fs(fs); - let global_from_env = StandardProperty::new() - .env("AWS_SOME_KEY") - .profile("some_key") - .validate(&provider_config, validate_some_key) - .await - .expect("config resolution succeeds"); - assert_eq!(Some(1), global_from_env); - - let service_from_env = StandardProperty::new() - .env("AWS_SOME_KEY") - .profile("some_key") - .service_id("service") - .validate(&provider_config, validate_some_key) - .await - .expect("config resolution succeeds"); - assert_eq!(Some(2), service_from_env); - - let other_service_from_env = StandardProperty::new() - .env("AWS_SOME_KEY") - .profile("some_key") - .service_id("another_service") - .validate(&provider_config, validate_some_key) - .await - .expect("config resolution succeeds"); - assert_eq!(Some(3), other_service_from_env); - - let global_from_profile = StandardProperty::new() - .profile("some_key") - .validate(&provider_config, validate_some_key) - .await - .expect("config resolution succeeds"); - assert_eq!(Some(4), global_from_profile); - - let service_from_profile = StandardProperty::new() - .profile("some_key") - .service_id("service") - .validate(&provider_config, validate_some_key) - .await - .expect("config resolution succeeds"); - assert_eq!(Some(5), service_from_profile); - - let service_from_profile = StandardProperty::new() - .profile("some_key") - .service_id("another_service") - .validate(&provider_config, validate_some_key) - .await - .expect("config resolution succeeds"); - assert_eq!(Some(6), service_from_profile); - } - - #[tokio::test] - async fn test_service_config_precedence() { - let env = Env::from_slice(&[ - ("AWS_CONFIG_FILE", "config"), - ("AWS_SOME_KEY", "1"), - ("AWS_SOME_KEY_S3", "2"), - ]); - let fs = Fs::from_slice(&[( - "config", - r#"[default] -some_key = 3 -services = dev - -[services dev] -s3 = - some_key = 4 -"#, - )]); - - let provider_config = ProviderConfig::no_configuration().with_env(env).with_fs(fs); - let global_from_env = StandardProperty::new() - .env("AWS_SOME_KEY") - .profile("some_key") - .validate(&provider_config, validate_some_key) - .await - .expect("config resolution succeeds"); - assert_eq!(Some(1), global_from_env); - - let service_from_env = StandardProperty::new() - .env("AWS_SOME_KEY") - .profile("some_key") - .service_id("s3") - .validate(&provider_config, validate_some_key) - .await - .expect("config resolution succeeds"); - assert_eq!(Some(2), service_from_env); - - let global_from_profile = StandardProperty::new() - .profile("some_key") - .validate(&provider_config, validate_some_key) - .await - .expect("config resolution succeeds"); - assert_eq!(Some(3), global_from_profile); - - let service_from_profile = StandardProperty::new() - .profile("some_key") - .service_id("s3") - .validate(&provider_config, validate_some_key) - .await - .expect("config resolution succeeds"); - assert_eq!(Some(4), service_from_profile); - } - - #[tokio::test] - async fn test_multiple_services() { - let env = Env::from_slice(&[ - ("AWS_CONFIG_FILE", "config"), - ("AWS_SOME_KEY", "1"), - ("AWS_SOME_KEY_S3", "2"), - ("AWS_SOME_KEY_EC2", "3"), - ]); - let fs = Fs::from_slice(&[( - "config", - r#"[default] -some_key = 4 -services = dev - -[services dev-wrong] -s3 = - some_key = 998 -ec2 = - some_key = 999 - -[services dev] -s3 = - some_key = 5 -ec2 = - some_key = 6 -"#, - )]); - - let provider_config = ProviderConfig::no_configuration().with_env(env).with_fs(fs); - let global_from_env = StandardProperty::new() - .env("AWS_SOME_KEY") - .profile("some_key") - .validate(&provider_config, validate_some_key) - .await - .expect("config resolution succeeds"); - assert_eq!(Some(1), global_from_env); - - let service_from_env = StandardProperty::new() - .env("AWS_SOME_KEY") - .profile("some_key") - .service_id("s3") - .validate(&provider_config, validate_some_key) - .await - .expect("config resolution succeeds"); - assert_eq!(Some(2), service_from_env); - - let service_from_env = StandardProperty::new() - .env("AWS_SOME_KEY") - .profile("some_key") - .service_id("ec2") - .validate(&provider_config, validate_some_key) - .await - .expect("config resolution succeeds"); - assert_eq!(Some(3), service_from_env); - - let global_from_profile = StandardProperty::new() - .profile("some_key") - .validate(&provider_config, validate_some_key) - .await - .expect("config resolution succeeds"); - assert_eq!(Some(4), global_from_profile); - - let service_from_profile = StandardProperty::new() - .profile("some_key") - .service_id("s3") - .validate(&provider_config, validate_some_key) - .await - .expect("config resolution succeeds"); - assert_eq!(Some(5), service_from_profile); - - let service_from_profile = StandardProperty::new() - .profile("some_key") - .service_id("ec2") - .validate(&provider_config, validate_some_key) - .await - .expect("config resolution succeeds"); - assert_eq!(Some(6), service_from_profile); - } -} diff --git a/aws/rust-runtime/aws-runtime/Cargo.toml b/aws/rust-runtime/aws-runtime/Cargo.toml index 66010331f6..ab6419e408 100644 --- a/aws/rust-runtime/aws-runtime/Cargo.toml +++ b/aws/rust-runtime/aws-runtime/Cargo.toml @@ -33,16 +33,19 @@ tracing = "0.1" uuid = { version = "1" } [dev-dependencies] +arbitrary = "1.3" aws-credential-types = { path = "../aws-credential-types", features = ["test-util"] } aws-smithy-async = { path = "../../../rust-runtime/aws-smithy-async", features = ["test-util"] } aws-smithy-protocol-test = { path = "../../../rust-runtime/aws-smithy-protocol-test" } aws-smithy-runtime-api = { path = "../../../rust-runtime/aws-smithy-runtime-api", features = ["test-util"] } aws-smithy-types = { path = "../../../rust-runtime/aws-smithy-types", features = ["test-util"] } bytes-utils = "0.1.2" +futures-util = { version = "0.3.29", default-features = false } proptest = "1.2" serde = { version = "1", features = ["derive"]} serde_json = "1" tokio = { version = "1.23.1", features = ["macros", "rt", "time"] } +tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } tracing-test = "0.2.4" [package.metadata.docs.rs] diff --git a/aws/rust-runtime/aws-runtime/src/env_config.rs b/aws/rust-runtime/aws-runtime/src/env_config.rs new file mode 100644 index 0000000000..9880f7bf13 --- /dev/null +++ b/aws/rust-runtime/aws-runtime/src/env_config.rs @@ -0,0 +1,493 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use std::borrow::Cow; +use std::error::Error; +use std::fmt; + +use aws_types::os_shim_internal::Env; + +use crate::profile::profile_set::ProfileSet; +use crate::profile::section::PropertiesKey; + +#[derive(Debug)] +enum Location<'a> { + Environment, + Profile { name: Cow<'a, str> }, +} + +impl<'a> fmt::Display for Location<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Location::Environment => write!(f, "environment variable"), + Location::Profile { name } => write!(f, "profile (`{name}`)"), + } + } +} + +#[derive(Debug)] +enum Scope<'a> { + Global, + Service { service_id: Cow<'a, str> }, +} + +impl<'a> fmt::Display for Scope<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Scope::Global => write!(f, "global"), + Scope::Service { service_id } => write!(f, "service-specific (`{service_id}`)"), + } + } +} + +/// +#[derive(Debug)] +pub struct EnvConfigSource<'a> { + key: Cow<'a, str>, + location: Location<'a>, + source: Scope<'a>, +} + +impl<'a> EnvConfigSource<'a> { + pub(crate) fn global_from_env(key: Cow<'a, str>) -> Self { + Self { + key, + location: Location::Environment, + source: Scope::Global, + } + } + + pub(crate) fn global_from_profile(key: Cow<'a, str>, profile_name: Cow<'a, str>) -> Self { + Self { + key, + location: Location::Profile { name: profile_name }, + source: Scope::Global, + } + } + + pub(crate) fn service_from_env(key: Cow<'a, str>, service_id: Cow<'a, str>) -> Self { + Self { + key, + location: Location::Environment, + source: Scope::Service { service_id }, + } + } + + pub(crate) fn service_from_profile( + key: Cow<'a, str>, + profile_name: Cow<'a, str>, + service_id: Cow<'a, str>, + ) -> Self { + Self { + key, + location: Location::Profile { name: profile_name }, + source: Scope::Service { service_id }, + } + } +} + +impl<'a> fmt::Display for EnvConfigSource<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} {} key: `{}`", self.source, self.location, self.key) + } +} + +/// An error occurred when resolving config from a user's environment. +#[derive(Debug)] +pub struct EnvConfigError> { + property_source: String, + err: E, +} + +impl EnvConfigError { + /// Return a reference to the inner error wrapped by this error. + pub fn err(&self) -> &E { + &self.err + } +} + +impl fmt::Display for EnvConfigError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}. source: {}", self.err, self.property_source) + } +} + +impl Error for EnvConfigError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + self.err.source() + } +} + +/// Environment config values are config values sourced from a user's environment variables or profile file. +/// +/// `EnvConfigValue` will first look in the environment, then the AWS profile. They track the +/// provenance of properties so that unified validation errors can be created. +/// +/// For a usage example, see [`crate::default_provider::retry_config`] +#[derive(Default, Debug)] +pub struct EnvConfigValue<'a> { + environment_variable: Option>, + profile_key: Option>, + service_id: Option>, +} + +impl<'a> EnvConfigValue<'a> { + /// Create a new `EnvConfigValue` + pub fn new() -> Self { + Self::default() + } + + /// Set the environment variable to read + pub fn env(mut self, key: &'a str) -> Self { + self.environment_variable = Some(Cow::Borrowed(key)); + self + } + + /// Set the profile key to read + pub fn profile(mut self, key: &'a str) -> Self { + self.profile_key = Some(Cow::Borrowed(key)); + self + } + + /// Set the service id to check for service config + pub fn service_id(mut self, service_id: &'a str) -> Self { + self.service_id = Some(Cow::Borrowed(service_id)); + self + } + + /// Load the value from `provider_config`, validating with `validator` + pub fn validate( + self, + env: &Env, + profiles: Option<&ProfileSet>, + validator: impl Fn(&str) -> Result, + ) -> Result, EnvConfigError> { + let value = self.load(env, profiles); + value + .map(|(v, ctx)| { + validator(v.as_ref()).map_err(|err| EnvConfigError { + property_source: format!("{}", ctx), + err, + }) + }) + .transpose() + } + + /// Load the value from the environment + pub fn load( + &self, + env: &'a Env, + profiles: Option<&'a ProfileSet>, + ) -> Option<(Cow<'a, str>, EnvConfigSource<'a>)> { + let env_value = self.environment_variable.as_ref().and_then(|env_var| { + // Check for a service-specific env var first + let service_config = + get_service_config_from_env(env, self.service_id.clone(), env_var.clone()); + // Then check for a global env var + let global_config = env.get(env_var).ok().map(|value| { + ( + Cow::Owned(value), + EnvConfigSource::global_from_env(env_var.clone()), + ) + }); + + let value = service_config.or(global_config); + tracing::trace!("ENV value = {value:?}"); + value + }); + + let profile_value = match (profiles, self.profile_key.as_ref()) { + (Some(profiles), Some(profile_key)) => { + // Check for a service-specific profile key first + let service_config = get_service_config_from_profile( + profiles, + self.service_id.clone(), + profile_key.clone(), + ); + let global_config = profiles.get(profile_key.as_ref()).map(|value| { + ( + Cow::Borrowed(value), + EnvConfigSource::global_from_profile( + profile_key.clone(), + Cow::Owned(profiles.selected_profile().to_owned()), + ), + ) + }); + + let value = service_config.or(global_config); + tracing::trace!("PROFILE value = {value:?}"); + value + } + _ => None, + }; + + env_value.or(profile_value) + } +} + +fn get_service_config_from_env<'a>( + env: &'a Env, + service_id: Option>, + env_var: Cow<'a, str>, +) -> Option<(Cow<'a, str>, EnvConfigSource<'a>)> { + let service_id = service_id?; + let env_case_service_id = format_service_id_for_env(service_id.clone()); + let service_specific_env_key = format!("{env_var}_{env_case_service_id}"); + let env_var = env.get(&service_specific_env_key).ok()?; + let env_var: Cow<'_, str> = Cow::Owned(env_var); + let source = EnvConfigSource::service_from_env(env_var.clone(), service_id); + + Some((env_var, source)) +} + +const SERVICES: &str = "services"; + +fn get_service_config_from_profile<'a>( + profile: &ProfileSet, + service_id: Option>, + profile_key: Cow<'a, str>, +) -> Option<(Cow<'a, str>, EnvConfigSource<'a>)> { + let service_id = service_id?.clone(); + let profile_case_service_id = format_service_id_for_profile(service_id.clone()); + let services_section_name = profile.get(SERVICES)?; + let properties_key = PropertiesKey::builder() + .section_key(SERVICES) + .section_name(services_section_name) + .property_name(profile_case_service_id) + .sub_property_name(profile_key.clone()) + .build() + .ok()?; + let value = profile.other_sections().get(&properties_key)?; + let profile_name = Cow::Owned(profile.selected_profile().to_owned()); + let source = EnvConfigSource::service_from_profile(profile_key, profile_name, service_id); + + Some((Cow::Owned(value.to_owned()), source)) +} + +fn format_service_id_for_env(service_id: impl AsRef) -> String { + service_id.as_ref().to_uppercase().replace(' ', "_") +} + +fn format_service_id_for_profile(service_id: impl AsRef) -> String { + service_id.as_ref().to_lowercase().replace(' ', "-") +} + +// TODO +// #[cfg(test)] +// mod test { +// use super::EnvConfigValue; +// use crate::provider_config::ProviderConfig; +// use aws_types::os_shim_internal::{Env, Fs}; +// use std::num::ParseIntError; +// +// fn validate_some_key(s: &str) -> Result { +// s.parse() +// } +// +// #[tokio::test] +// async fn test_service_config_multiple_services() { +// let env = Env::from_slice(&[ +// ("AWS_CONFIG_FILE", "config"), +// ("AWS_SOME_KEY", "1"), +// ("AWS_SOME_KEY_SERVICE", "2"), +// ("AWS_SOME_KEY_ANOTHER_SERVICE", "3"), +// ]); +// let fs = Fs::from_slice(&[( +// "config", +// r#"[default] +// some_key = 4 +// services = dev +// +// [services dev] +// service = +// some_key = 5 +// another_service = +// some_key = 6 +// "#, +// )]); +// +// let provider_config = ProviderConfig::no_configuration().with_env(env).with_fs(fs); +// let global_from_env = EnvConfigValue::new() +// .env("AWS_SOME_KEY") +// .profile("some_key") +// .validate(&provider_config, validate_some_key) +// .await +// .expect("config resolution succeeds"); +// assert_eq!(Some(1), global_from_env); +// +// let service_from_env = EnvConfigValue::new() +// .env("AWS_SOME_KEY") +// .profile("some_key") +// .service_id("service") +// .validate(&provider_config, validate_some_key) +// .await +// .expect("config resolution succeeds"); +// assert_eq!(Some(2), service_from_env); +// +// let other_service_from_env = EnvConfigValue::new() +// .env("AWS_SOME_KEY") +// .profile("some_key") +// .service_id("another_service") +// .validate(&provider_config, validate_some_key) +// .await +// .expect("config resolution succeeds"); +// assert_eq!(Some(3), other_service_from_env); +// +// let global_from_profile = EnvConfigValue::new() +// .profile("some_key") +// .validate(&provider_config, validate_some_key) +// .await +// .expect("config resolution succeeds"); +// assert_eq!(Some(4), global_from_profile); +// +// let service_from_profile = EnvConfigValue::new() +// .profile("some_key") +// .service_id("service") +// .validate(&provider_config, validate_some_key) +// .await +// .expect("config resolution succeeds"); +// assert_eq!(Some(5), service_from_profile); +// +// let service_from_profile = EnvConfigValue::new() +// .profile("some_key") +// .service_id("another_service") +// .validate(&provider_config, validate_some_key) +// .await +// .expect("config resolution succeeds"); +// assert_eq!(Some(6), service_from_profile); +// } +// +// #[tokio::test] +// async fn test_service_config_precedence() { +// let env = Env::from_slice(&[ +// ("AWS_CONFIG_FILE", "config"), +// ("AWS_SOME_KEY", "1"), +// ("AWS_SOME_KEY_S3", "2"), +// ]); +// let fs = Fs::from_slice(&[( +// "config", +// r#"[default] +// some_key = 3 +// services = dev +// +// [services dev] +// s3 = +// some_key = 4 +// "#, +// )]); +// +// let provider_config = ProviderConfig::no_configuration().with_env(env).with_fs(fs); +// let global_from_env = EnvConfigValue::new() +// .env("AWS_SOME_KEY") +// .profile("some_key") +// .validate(&provider_config, validate_some_key) +// .await +// .expect("config resolution succeeds"); +// assert_eq!(Some(1), global_from_env); +// +// let service_from_env = EnvConfigValue::new() +// .env("AWS_SOME_KEY") +// .profile("some_key") +// .service_id("s3") +// .validate(&provider_config, validate_some_key) +// .await +// .expect("config resolution succeeds"); +// assert_eq!(Some(2), service_from_env); +// +// let global_from_profile = EnvConfigValue::new() +// .profile("some_key") +// .validate(&provider_config, validate_some_key) +// .await +// .expect("config resolution succeeds"); +// assert_eq!(Some(3), global_from_profile); +// +// let service_from_profile = EnvConfigValue::new() +// .profile("some_key") +// .service_id("s3") +// .validate(&provider_config, validate_some_key) +// .await +// .expect("config resolution succeeds"); +// assert_eq!(Some(4), service_from_profile); +// } +// +// #[tokio::test] +// async fn test_multiple_services() { +// let env = Env::from_slice(&[ +// ("AWS_CONFIG_FILE", "config"), +// ("AWS_SOME_KEY", "1"), +// ("AWS_SOME_KEY_S3", "2"), +// ("AWS_SOME_KEY_EC2", "3"), +// ]); +// let fs = Fs::from_slice(&[( +// "config", +// r#"[default] +// some_key = 4 +// services = dev +// +// [services dev-wrong] +// s3 = +// some_key = 998 +// ec2 = +// some_key = 999 +// +// [services dev] +// s3 = +// some_key = 5 +// ec2 = +// some_key = 6 +// "#, +// )]); +// +// let provider_config = ProviderConfig::no_configuration().with_env(env).with_fs(fs); +// let global_from_env = EnvConfigValue::new() +// .env("AWS_SOME_KEY") +// .profile("some_key") +// .validate(&provider_config, validate_some_key) +// .await +// .expect("config resolution succeeds"); +// assert_eq!(Some(1), global_from_env); +// +// let service_from_env = EnvConfigValue::new() +// .env("AWS_SOME_KEY") +// .profile("some_key") +// .service_id("s3") +// .validate(&provider_config, validate_some_key) +// .await +// .expect("config resolution succeeds"); +// assert_eq!(Some(2), service_from_env); +// +// let service_from_env = EnvConfigValue::new() +// .env("AWS_SOME_KEY") +// .profile("some_key") +// .service_id("ec2") +// .validate(&provider_config, validate_some_key) +// .await +// .expect("config resolution succeeds"); +// assert_eq!(Some(3), service_from_env); +// +// let global_from_profile = EnvConfigValue::new() +// .profile("some_key") +// .validate(&provider_config, validate_some_key) +// .await +// .expect("config resolution succeeds"); +// assert_eq!(Some(4), global_from_profile); +// +// let service_from_profile = EnvConfigValue::new() +// .profile("some_key") +// .service_id("s3") +// .validate(&provider_config, validate_some_key) +// .await +// .expect("config resolution succeeds"); +// assert_eq!(Some(5), service_from_profile); +// +// let service_from_profile = EnvConfigValue::new() +// .profile("some_key") +// .service_id("ec2") +// .validate(&provider_config, validate_some_key) +// .await +// .expect("config resolution succeeds"); +// assert_eq!(Some(6), service_from_profile); +// } +// } diff --git a/aws/rust-runtime/aws-runtime/src/fs_util.rs b/aws/rust-runtime/aws-runtime/src/fs_util.rs new file mode 100644 index 0000000000..77dbcb733a --- /dev/null +++ b/aws/rust-runtime/aws-runtime/src/fs_util.rs @@ -0,0 +1,66 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use aws_types::os_shim_internal; + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +#[non_exhaustive] +pub(crate) enum Os { + /// A Windows-based operating system + Windows, + /// Any other operating system + NotWindows, +} + +impl Os { + /// Returns the current operating system + pub(crate) fn real() -> Self { + match std::env::consts::OS { + "windows" => Os::Windows, + _ => Os::NotWindows, + } + } +} + +/// Resolve a home directory given a set of environment variables +pub(crate) fn home_dir(env_var: &os_shim_internal::Env, os: Os) -> Option { + if let Ok(home) = env_var.get("HOME") { + tracing::debug!(src = "HOME", "loaded home directory"); + return Some(home); + } + + if os == Os::Windows { + if let Ok(home) = env_var.get("USERPROFILE") { + tracing::debug!(src = "USERPROFILE", "loaded home directory"); + return Some(home); + } + + let home_drive = env_var.get("HOMEDRIVE"); + let home_path = env_var.get("HOMEPATH"); + tracing::debug!(src = "HOMEDRIVE/HOMEPATH", "loaded home directory"); + if let (Ok(mut drive), Ok(path)) = (home_drive, home_path) { + drive.push_str(&path); + return Some(drive); + } + } + None +} + +#[cfg(test)] +mod test { + use super::{home_dir, Os}; + use aws_types::os_shim_internal::Env; + + #[test] + fn homedir_profile_only_windows() { + // windows specific variables should only be considered when the platform is windows + let env = Env::from_slice(&[("USERPROFILE", "C:\\Users\\name")]); + assert_eq!( + home_dir(&env, Os::Windows), + Some("C:\\Users\\name".to_string()) + ); + assert_eq!(home_dir(&env, Os::NotWindows), None); + } +} diff --git a/aws/rust-runtime/aws-runtime/src/lib.rs b/aws/rust-runtime/aws-runtime/src/lib.rs index 8487640adb..d6d1bd319a 100644 --- a/aws/rust-runtime/aws-runtime/src/lib.rs +++ b/aws/rust-runtime/aws-runtime/src/lib.rs @@ -40,3 +40,12 @@ pub mod request_info; /// Interceptor that determines the clock skew between the client and service. pub mod service_clock_skew; + +/// Supporting code for extracting config from an AWS config file. +pub mod profile; + +/// Filesystem utilities +pub mod fs_util; + +/// Supporting code for parsing AWS config values set in a user's environment +pub mod env_config; diff --git a/aws/rust-runtime/aws-runtime/src/profile.rs b/aws/rust-runtime/aws-runtime/src/profile.rs new file mode 100644 index 0000000000..10e2274550 --- /dev/null +++ b/aws/rust-runtime/aws-runtime/src/profile.rs @@ -0,0 +1,35 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use crate::env_config::{EnvConfigError, EnvConfigValue}; +use crate::profile::profile_set::ProfileSet; +use aws_types::os_shim_internal::Env; +use aws_types::service_config::ServiceConfigKey; +use std::error::Error; + +pub mod error; +mod normalize; +pub mod parse; +pub mod profile_file; +pub mod profile_set; +pub mod section; +pub mod source; + +/// Given a key, access to the environment, and a validator, return a config value if one was set. +pub async fn get_service_env_config<'a, T, E>( + key: ServiceConfigKey<'a>, + env: &'a Env, + profiles: Option<&'a ProfileSet>, + validator: impl Fn(&str) -> Result, +) -> Result, EnvConfigError> +where + E: Error + Send + Sync + 'static, +{ + EnvConfigValue::default() + .env(key.env()) + .profile(key.profile()) + .service_id(key.service_id()) + .validate(env, profiles, validator) +} diff --git a/aws/rust-runtime/aws-config/src/profile/parser/error.rs b/aws/rust-runtime/aws-runtime/src/profile/error.rs similarity index 94% rename from aws/rust-runtime/aws-config/src/profile/parser/error.rs rename to aws/rust-runtime/aws-runtime/src/profile/error.rs index 1351c242f8..34fab4aee7 100644 --- a/aws/rust-runtime/aws-config/src/profile/parser/error.rs +++ b/aws/rust-runtime/aws-runtime/src/profile/error.rs @@ -3,7 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -use crate::profile::ProfileParseError; +//! Errors related to AWS profile config files + +use crate::profile::parse::ProfileParseError; use std::error::Error; use std::fmt::{Display, Formatter}; use std::path::PathBuf; diff --git a/aws/rust-runtime/aws-config/src/profile/parser/normalize.rs b/aws/rust-runtime/aws-runtime/src/profile/normalize.rs similarity index 87% rename from aws/rust-runtime/aws-config/src/profile/parser/normalize.rs rename to aws/rust-runtime/aws-runtime/src/profile/normalize.rs index 2e93bc03b4..8657c53899 100644 --- a/aws/rust-runtime/aws-config/src/profile/parser/normalize.rs +++ b/aws/rust-runtime/aws-runtime/src/profile/normalize.rs @@ -3,29 +3,25 @@ * SPDX-License-Identifier: Apache-2.0 */ -use crate::profile::parser::{ - parse::{RawProfileSet, WHITESPACE}, - Section, SsoSession, -}; +use crate::profile::parse::{RawProfileSet, WHITESPACE}; use crate::profile::profile_file::ProfileFileKind; -use crate::profile::{Profile, ProfileSet, Property}; +use crate::profile::profile_set::ProfileSet; +use crate::profile::section::{Profile, PropertiesKey, Property, Section, SsoSession}; use std::borrow::Cow; use std::collections::HashMap; -use super::PropertiesKey; - const DEFAULT: &str = "default"; const PROFILE_PREFIX: &str = "profile"; const SSO_SESSION_PREFIX: &str = "sso-session"; /// Any section like `[ ]` or `[]` #[derive(Eq, PartialEq, Hash, Debug)] -struct SectionKey<'a> { +struct SectionPair<'a> { prefix: Option>, suffix: Cow<'a, str>, } -impl<'a> SectionKey<'a> { +impl<'a> SectionPair<'a> { fn is_unprefixed_default(&self) -> bool { self.prefix.is_none() && self.suffix == DEFAULT } @@ -34,16 +30,16 @@ impl<'a> SectionKey<'a> { self.prefix.as_deref() == Some(PROFILE_PREFIX) && self.suffix == DEFAULT } - fn parse(input: &str) -> SectionKey<'_> { + fn parse(input: &str) -> SectionPair<'_> { let input = input.trim_matches(WHITESPACE); match input.split_once(WHITESPACE) { // Something like `[profile name]` - Some((prefix, suffix)) => SectionKey { + Some((prefix, suffix)) => SectionPair { prefix: Some(prefix.trim().into()), suffix: suffix.trim().into(), }, // Either `[profile-name]` or `[default]` - None => SectionKey { + None => SectionPair { prefix: None, suffix: input.trim().into(), }, @@ -112,7 +108,7 @@ pub(super) fn merge_in( let validated_sections = raw_profile_set .into_iter() .map(|(section_key, properties)| { - (SectionKey::parse(section_key).valid_for(kind), properties) + (SectionPair::parse(section_key).valid_for(kind), properties) }); // remove invalid profiles & emit a warning @@ -235,10 +231,10 @@ fn parse_sub_properties(sub_properties_str: &str) -> impl Iterator = HashMap::new(); profile.insert("foo", HashMap::new()); - merge_in(&mut ProfileSet::empty(), profile, ProfileFileKind::Config); + merge_in(&mut ProfileSet::default(), profile, ProfileFileKind::Config); assert!(logs_contain("profile [foo] ignored")); } } diff --git a/aws/rust-runtime/aws-config/src/profile/parser/parse.rs b/aws/rust-runtime/aws-runtime/src/profile/parse.rs similarity index 96% rename from aws/rust-runtime/aws-config/src/profile/parser/parse.rs rename to aws/rust-runtime/aws-runtime/src/profile/parse.rs index f0f48f8f49..f481b7866e 100644 --- a/aws/rust-runtime/aws-config/src/profile/parser/parse.rs +++ b/aws/rust-runtime/aws-runtime/src/profile/parse.rs @@ -12,7 +12,7 @@ //! - profiles with invalid names //! - profile name normalization (`profile foo` => `foo`) -use crate::profile::parser::source::File; +use crate::profile::source::File; use std::borrow::Cow; use std::collections::HashMap; use std::error::Error; @@ -254,15 +254,7 @@ fn parse_property_line(line: &str) -> Result<(Cow<'_, str>, &str), PropertyError if k.is_empty() { return Err(PropertyError::NoName); } - Ok((to_ascii_lowercase(k), v)) -} - -pub(crate) fn to_ascii_lowercase(s: &str) -> Cow<'_, str> { - if s.bytes().any(|b| b.is_ascii_uppercase()) { - Cow::Owned(s.to_ascii_lowercase()) - } else { - Cow::Borrowed(s) - } + Ok((k.to_ascii_lowercase().into(), v)) } /// Prepare a line for parsing @@ -296,9 +288,9 @@ fn prepare_line(line: &str, comments_need_whitespace: bool) -> &str { #[cfg(test)] mod test { use super::{parse_profile_file, prepare_line, Location}; - use crate::profile::parser::parse::{parse_property_line, PropertyError}; - use crate::profile::parser::source::File; + use crate::profile::parse::{parse_property_line, PropertyError}; use crate::profile::profile_file::ProfileFileKind; + use crate::profile::source::File; use std::borrow::Cow; // most test cases covered by the JSON test suite diff --git a/aws/rust-runtime/aws-runtime/src/profile/profile_file.rs b/aws/rust-runtime/aws-runtime/src/profile/profile_file.rs new file mode 100644 index 0000000000..dfe9eb33ca --- /dev/null +++ b/aws/rust-runtime/aws-runtime/src/profile/profile_file.rs @@ -0,0 +1,250 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +//! Config structs to programmatically customize the profile files that get loaded + +use std::fmt; +use std::path::PathBuf; + +/// Provides the ability to programmatically override the profile files that get loaded by the SDK. +/// +/// The [`Default`] for `ProfileFiles` includes the default SDK config and credential files located in +/// `~/.aws/config` and `~/.aws/credentials` respectively. +/// +/// Any number of config and credential files may be added to the `ProfileFiles` file set, with the +/// only requirement being that there is at least one of them. Custom file locations that are added +/// will produce errors if they don't exist, while the default config/credentials files paths are +/// allowed to not exist even if they're included. +/// +/// # Example: Using a custom profile file path +/// +/// ```no_run +/// use aws_config::profile::{ProfileFileCredentialsProvider, ProfileFileRegionProvider}; +/// use aws_config::profile::profile_file::{ProfileFiles, ProfileFileKind}; +/// use std::sync::Arc; +/// +/// # async fn example() { +/// let profile_files = ProfileFiles::builder() +/// .with_file(ProfileFileKind::Credentials, "some/path/to/credentials-file") +/// .build(); +/// let sdk_config = aws_config::from_env() +/// .profile_files(profile_files) +/// .load() +/// .await; +/// # } +/// ``` +#[derive(Clone, Debug)] +pub struct ProfileFiles { + pub(crate) files: Vec, +} + +impl ProfileFiles { + /// Returns a builder to create `ProfileFiles` + pub fn builder() -> Builder { + Builder::new() + } +} + +impl Default for ProfileFiles { + fn default() -> Self { + Self { + files: vec![ + ProfileFile::Default(ProfileFileKind::Config), + ProfileFile::Default(ProfileFileKind::Credentials), + ], + } + } +} + +/// Profile file type (config or credentials) +#[derive(Copy, Clone, Debug)] +pub enum ProfileFileKind { + /// The SDK config file that typically resides in `~/.aws/config` + Config, + /// The SDK credentials file that typically resides in `~/.aws/credentials` + Credentials, +} + +impl ProfileFileKind { + pub(crate) fn default_path(&self) -> &'static str { + match &self { + ProfileFileKind::Credentials => "~/.aws/credentials", + ProfileFileKind::Config => "~/.aws/config", + } + } + + pub(crate) fn override_environment_variable(&self) -> &'static str { + match &self { + ProfileFileKind::Config => "AWS_CONFIG_FILE", + ProfileFileKind::Credentials => "AWS_SHARED_CREDENTIALS_FILE", + } + } +} + +/// A single profile file within a [`ProfileFiles`] file set. +#[derive(Clone)] +pub(crate) enum ProfileFile { + /// One of the default profile files (config or credentials in their default locations) + Default(ProfileFileKind), + /// A profile file at a custom location + FilePath { + kind: ProfileFileKind, + path: PathBuf, + }, + /// The direct contents of a profile file + FileContents { + kind: ProfileFileKind, + contents: String, + }, +} + +impl fmt::Debug for ProfileFile { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Default(kind) => f.debug_tuple("Default").field(kind).finish(), + Self::FilePath { kind, path } => f + .debug_struct("FilePath") + .field("kind", kind) + .field("path", path) + .finish(), + // Security: Redact the file contents since they may have credentials in them + Self::FileContents { kind, contents: _ } => f + .debug_struct("FileContents") + .field("kind", kind) + .field("contents", &"** redacted **") + .finish(), + } + } +} + +/// Builder for [`ProfileFiles`]. +#[derive(Clone, Default, Debug)] +pub struct Builder { + with_config: bool, + with_credentials: bool, + custom_sources: Vec, +} + +impl Builder { + /// Creates a new builder instance. + pub fn new() -> Self { + Default::default() + } + + /// Include the default SDK config file in the list of profile files to be loaded. + /// + /// The default SDK config typically resides in `~/.aws/config`. When this flag is enabled, + /// this config file will be included in the profile files that get loaded in the built + /// [`ProfileFiles`] file set. + /// + /// This flag defaults to `false` when using the builder to construct [`ProfileFiles`]. + pub fn include_default_config_file(mut self, include_default_config_file: bool) -> Self { + self.with_config = include_default_config_file; + self + } + + /// Include the default SDK credentials file in the list of profile files to be loaded. + /// + /// The default SDK config typically resides in `~/.aws/credentials`. When this flag is enabled, + /// this credentials file will be included in the profile files that get loaded in the built + /// [`ProfileFiles`] file set. + /// + /// This flag defaults to `false` when using the builder to construct [`ProfileFiles`]. + pub fn include_default_credentials_file( + mut self, + include_default_credentials_file: bool, + ) -> Self { + self.with_credentials = include_default_credentials_file; + self + } + + /// Include a custom `file` in the list of profile files to be loaded. + /// + /// The `kind` informs the parser how to treat the file. If it's intended to be like + /// the SDK credentials file typically in `~/.aws/config`, then use [`ProfileFileKind::Config`]. + /// Otherwise, use [`ProfileFileKind::Credentials`]. + pub fn with_file(mut self, kind: ProfileFileKind, file: impl Into) -> Self { + self.custom_sources.push(ProfileFile::FilePath { + kind, + path: file.into(), + }); + self + } + + /// Include custom file `contents` in the list of profile files to be loaded. + /// + /// The `kind` informs the parser how to treat the file. If it's intended to be like + /// the SDK credentials file typically in `~/.aws/config`, then use [`ProfileFileKind::Config`]. + /// Otherwise, use [`ProfileFileKind::Credentials`]. + pub fn with_contents(mut self, kind: ProfileFileKind, contents: impl Into) -> Self { + self.custom_sources.push(ProfileFile::FileContents { + kind, + contents: contents.into(), + }); + self + } + + /// Build the [`ProfileFiles`] file set. + pub fn build(self) -> ProfileFiles { + let mut files = self.custom_sources; + if self.with_credentials { + files.insert(0, ProfileFile::Default(ProfileFileKind::Credentials)); + } + if self.with_config { + files.insert(0, ProfileFile::Default(ProfileFileKind::Config)); + } + if files.is_empty() { + panic!("At least one profile file must be included in the `ProfileFiles` file set."); + } + ProfileFiles { files } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn redact_file_contents_in_profile_file_debug() { + let profile_file = ProfileFile::FileContents { + kind: ProfileFileKind::Config, + contents: "sensitive_contents".into(), + }; + let debug = format!("{:?}", profile_file); + assert!(!debug.contains("sensitive_contents")); + assert!(debug.contains("** redacted **")); + } + + #[test] + fn build_correctly_orders_default_config_credentials() { + let profile_files = ProfileFiles::builder() + .with_file(ProfileFileKind::Config, "foo") + .include_default_credentials_file(true) + .include_default_config_file(true) + .build(); + assert_eq!(3, profile_files.files.len()); + assert!(matches!( + profile_files.files[0], + ProfileFile::Default(ProfileFileKind::Config) + )); + assert!(matches!( + profile_files.files[1], + ProfileFile::Default(ProfileFileKind::Credentials) + )); + assert!(matches!( + profile_files.files[2], + ProfileFile::FilePath { + kind: ProfileFileKind::Config, + path: _ + } + )); + } + + #[test] + #[should_panic] + fn empty_builder_panics() { + ProfileFiles::builder().build(); + } +} diff --git a/aws/rust-runtime/aws-runtime/src/profile/profile_set.rs b/aws/rust-runtime/aws-runtime/src/profile/profile_set.rs new file mode 100644 index 0000000000..0235aed9ee --- /dev/null +++ b/aws/rust-runtime/aws-runtime/src/profile/profile_set.rs @@ -0,0 +1,343 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +//! Code for accessing and validating AWS config profiles + +use crate::profile::normalize; +use crate::profile::parse::{parse_profile_file, ProfileParseError}; +use crate::profile::section::{Profile, Properties, SsoSession}; +use crate::profile::source::Source; +use std::borrow::Cow; +use std::collections::HashMap; + +/// A top-level configuration source containing multiple named profiles +#[derive(Debug, Eq, Clone, PartialEq)] +pub struct ProfileSet { + pub(crate) profiles: HashMap, + pub(crate) selected_profile: Cow<'static, str>, + pub(crate) sso_sessions: HashMap, + pub(crate) other_sections: Properties, +} + +impl Default for ProfileSet { + fn default() -> Self { + Self { + profiles: Default::default(), + selected_profile: "default".into(), + sso_sessions: Default::default(), + other_sections: Default::default(), + } + } +} + +impl ProfileSet { + /// Create a new Profile set directly from a HashMap + /// + /// This method creates a ProfileSet directly from a hashmap with no normalization for test purposes. + #[cfg(test)] + pub fn new( + profiles: HashMap>, + selected_profile: impl Into>, + sso_sessions: HashMap>, + ) -> Self { + use crate::profile::section::Property; + + let mut base = ProfileSet::default(); + base.selected_profile = selected_profile.into(); + for (name, profile) in profiles { + base.profiles.insert( + name.clone(), + Profile::new( + name, + profile + .into_iter() + .map(|(k, v)| (k.clone(), Property::new(k, v))) + .collect(), + ), + ); + } + for (name, session) in sso_sessions { + base.sso_sessions.insert( + name.clone(), + SsoSession::new( + name, + session + .into_iter() + .map(|(k, v)| (k.clone(), Property::new(k, v))) + .collect(), + ), + ); + } + base + } + + /// Retrieves a key-value pair from the currently selected profile + pub fn get(&self, key: &str) -> Option<&str> { + self.profiles + .get(self.selected_profile.as_ref()) + .and_then(|profile| profile.get(key)) + } + + /// Retrieves a named profile from the profile set + pub fn get_profile(&self, profile_name: &str) -> Option<&Profile> { + self.profiles.get(profile_name) + } + + /// Returns the name of the currently selected profile + pub fn selected_profile(&self) -> &str { + self.selected_profile.as_ref() + } + + /// Returns true if no profiles are contained in this profile set + pub fn is_empty(&self) -> bool { + self.profiles.is_empty() + } + + /// Returns the names of the profiles in this config + pub fn profiles(&self) -> impl Iterator { + self.profiles.keys().map(String::as_ref) + } + + /// Returns the names of the SSO sessions in this config + pub fn sso_sessions(&self) -> impl Iterator { + self.sso_sessions.keys().map(String::as_ref) + } + + /// Retrieves a named SSO session from the config + pub fn sso_session(&self, name: &str) -> Option<&SsoSession> { + self.sso_sessions.get(name) + } + + /// Returns a struct allowing access to other sections in the profile config + pub fn other_sections(&self) -> &Properties { + &self.other_sections + } + + /// Given a [`Source`] of profile config, parse and merge them into a `ProfileSet`. + pub fn parse(source: Source) -> Result { + let mut base = ProfileSet::default(); + base.selected_profile = source.profile; + + for file in source.files { + normalize::merge_in(&mut base, parse_profile_file(&file)?, file.kind); + } + Ok(base) + } +} + +#[cfg(test)] +mod test { + use super::ProfileSet; + use crate::profile::profile_file::ProfileFileKind; + use crate::profile::section::Section; + use crate::profile::source::{File, Source}; + use arbitrary::{Arbitrary, Unstructured}; + use serde::Deserialize; + use std::collections::HashMap; + use std::error::Error; + use std::fs; + use tracing_test::traced_test; + + /// Run all tests from `test-data/profile-parser-tests.json` + /// + /// These represent the bulk of the test cases and reach 100% coverage of the parser. + #[test] + #[traced_test] + fn run_tests() -> Result<(), Box> { + let tests = fs::read_to_string("test-data/profile-parser-tests.json")?; + let tests: ParserTests = serde_json::from_str(&tests)?; + for (i, test) in tests.tests.into_iter().enumerate() { + eprintln!("test: {}", i); + check(test); + } + Ok(()) + } + + #[test] + fn empty_source_empty_profile() { + let source = make_source(ParserInput { + config_file: Some("".to_string()), + credentials_file: Some("".to_string()), + }); + + let profile_set = ProfileSet::parse(source).expect("empty profiles are valid"); + assert!(profile_set.is_empty()); + } + + #[test] + fn profile_names_are_exposed() { + let source = make_source(ParserInput { + config_file: Some("[profile foo]\n[profile bar]".to_string()), + credentials_file: Some("".to_string()), + }); + + let profile_set = ProfileSet::parse(source).expect("profiles loaded"); + + let mut profile_names: Vec<_> = profile_set.profiles().collect(); + profile_names.sort(); + assert_eq!(profile_names, vec!["bar", "foo"]); + } + + /// Run all tests from the fuzzing corpus to validate coverage + #[test] + #[ignore] + fn run_fuzz_tests() -> Result<(), Box> { + let fuzz_corpus = fs::read_dir("fuzz/corpus/profile-parser")? + .map(|res| res.map(|entry| entry.path())) + .collect::, _>>()?; + for file in fuzz_corpus { + let raw = fs::read(file)?; + let mut unstructured = Unstructured::new(&raw); + let (conf, creds): (Option<&str>, Option<&str>) = + Arbitrary::arbitrary(&mut unstructured)?; + let profile_source = Source { + files: vec![ + File { + kind: ProfileFileKind::Config, + path: Some("~/.aws/config".to_string()), + contents: conf.unwrap_or_default().to_string(), + }, + File { + kind: ProfileFileKind::Credentials, + path: Some("~/.aws/credentials".to_string()), + contents: creds.unwrap_or_default().to_string(), + }, + ], + profile: "default".into(), + }; + // don't care if parse fails, just don't panic + let _ = ProfileSet::parse(profile_source); + } + + Ok(()) + } + + // for test comparison purposes, flatten a profile into a hashmap + #[derive(Debug)] + struct FlattenedProfileSet { + profiles: HashMap>, + sso_sessions: HashMap>, + } + fn flatten(config: ProfileSet) -> FlattenedProfileSet { + FlattenedProfileSet { + profiles: flatten_sections(config.profiles.values().map(|p| p as _)), + sso_sessions: flatten_sections(config.sso_sessions.values().map(|s| s as _)), + } + } + fn flatten_sections<'a>( + sections: impl Iterator, + ) -> HashMap> { + sections + .map(|section| { + ( + section.name().to_string(), + section + .properties() + .values() + .map(|prop| (prop.key().to_owned(), prop.value().to_owned())) + .collect(), + ) + }) + .collect() + } + + fn make_source(input: ParserInput) -> Source { + Source { + files: vec![ + File { + kind: ProfileFileKind::Config, + path: Some("~/.aws/config".to_string()), + contents: input.config_file.unwrap_or_default(), + }, + File { + kind: ProfileFileKind::Credentials, + path: Some("~/.aws/credentials".to_string()), + contents: input.credentials_file.unwrap_or_default(), + }, + ], + profile: "default".into(), + } + } + + // wrapper to generate nicer errors during test failure + fn check(test_case: ParserTest) { + let copy = test_case.clone(); + let parsed = ProfileSet::parse(make_source(test_case.input)); + let res = match (parsed.map(flatten), &test_case.output) { + ( + Ok(FlattenedProfileSet { + profiles: actual_profiles, + sso_sessions: actual_sso_sessions, + }), + ParserOutput::Config { + profiles, + sso_sessions, + }, + ) => { + if profiles != &actual_profiles { + Err(format!( + "mismatched profiles:\nExpected: {profiles:#?}\nActual: {actual_profiles:#?}", + )) + } else if sso_sessions != &actual_sso_sessions { + Err(format!( + "mismatched sso_sessions:\nExpected: {sso_sessions:#?}\nActual: {actual_sso_sessions:#?}", + )) + } else { + Ok(()) + } + } + (Err(msg), ParserOutput::ErrorContaining(substr)) => { + if format!("{}", msg).contains(substr) { + Ok(()) + } else { + Err(format!("Expected {} to contain {}", msg, substr)) + } + } + (Ok(output), ParserOutput::ErrorContaining(err)) => Err(format!( + "expected an error: {err} but parse succeeded:\n{output:#?}", + )), + (Err(err), ParserOutput::Config { .. }) => { + Err(format!("Expected to succeed but got: {}", err)) + } + }; + if let Err(e) = res { + eprintln!("Test case failed: {:#?}", copy); + eprintln!("failure: {}", e); + panic!("test failed") + } + } + + #[derive(Deserialize, Debug)] + #[serde(rename_all = "camelCase")] + struct ParserTests { + tests: Vec, + } + + #[derive(Deserialize, Debug, Clone)] + #[serde(rename_all = "camelCase")] + struct ParserTest { + _name: String, + input: ParserInput, + output: ParserOutput, + } + + #[derive(Deserialize, Debug, Clone)] + #[serde(rename_all = "camelCase")] + enum ParserOutput { + Config { + profiles: HashMap>, + #[serde(default)] + sso_sessions: HashMap>, + }, + ErrorContaining(String), + } + + #[derive(Deserialize, Debug, Clone)] + #[serde(rename_all = "camelCase")] + struct ParserInput { + config_file: Option, + credentials_file: Option, + } +} diff --git a/aws/rust-runtime/aws-config/src/profile/parser/section.rs b/aws/rust-runtime/aws-runtime/src/profile/section.rs similarity index 51% rename from aws/rust-runtime/aws-config/src/profile/parser/section.rs rename to aws/rust-runtime/aws-runtime/src/profile/section.rs index 2850c77df5..2580663646 100644 --- a/aws/rust-runtime/aws-config/src/profile/parser/section.rs +++ b/aws/rust-runtime/aws-runtime/src/profile/section.rs @@ -3,7 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -use crate::profile::parser::parse::to_ascii_lowercase; +//! Sections within an AWS config profile. + use std::collections::HashMap; use std::fmt; @@ -37,12 +38,17 @@ type PropertyName = String; type SubPropertyName = String; type PropertyValue = String; -// [section-key section-name] -// property-name = property-value -// property-name = -// sub-property-name = property-value +/// A key for to a property value. +/// +/// ```txt +/// # An example AWS profile config section with properties and sub-properties +/// [section-key section-name] +/// property-name = property-value +/// property-name = +/// sub-property-name = property-value +/// ``` #[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub(crate) struct PropertiesKey { +pub struct PropertiesKey { section_key: SectionKey, section_name: SectionName, property_name: PropertyName, @@ -50,7 +56,8 @@ pub(crate) struct PropertiesKey { } impl PropertiesKey { - pub(crate) fn builder() -> PropertiesKeyBuilder { + /// Create a new [`PropertiseKeyBuilder`]. + pub fn builder() -> PropertiesKeyBuilder { Default::default() } } @@ -77,8 +84,9 @@ impl fmt::Display for PropertiesKey { } } -#[derive(Default)] -pub(crate) struct PropertiesKeyBuilder { +/// Builder for [`PropertiesKey`]s. +#[derive(Debug, Default)] +pub struct PropertiesKeyBuilder { section_key: Option, section_name: Option, property_name: Option, @@ -86,27 +94,33 @@ pub(crate) struct PropertiesKeyBuilder { } impl PropertiesKeyBuilder { - pub(crate) fn section_key(mut self, section_key: impl Into) -> Self { + /// Set the section key for this builder. + pub fn section_key(mut self, section_key: impl Into) -> Self { self.section_key = Some(section_key.into()); self } - pub(crate) fn section_name(mut self, section_name: impl Into) -> Self { + /// Set the section name for this builder. + pub fn section_name(mut self, section_name: impl Into) -> Self { self.section_name = Some(section_name.into()); self } - pub(crate) fn property_name(mut self, property_name: impl Into) -> Self { + /// Set the property name for this builder. + pub fn property_name(mut self, property_name: impl Into) -> Self { self.property_name = Some(property_name.into()); self } - pub(crate) fn sub_property_name(mut self, sub_property_name: impl Into) -> Self { + /// Set the sub-property name for this builder. + pub fn sub_property_name(mut self, sub_property_name: impl Into) -> Self { self.sub_property_name = Some(sub_property_name.into()); self } - pub(crate) fn build(self) -> Result { + /// Build this builder. If all required fields are set, + /// `Ok(PropertiesKey)` is returned. Otherwise, an error is returned. + pub fn build(self) -> Result { Ok(PropertiesKey { section_key: self .section_key @@ -122,19 +136,21 @@ impl PropertiesKeyBuilder { } } -#[allow(clippy::type_complexity)] +/// A map of [`PropertiesKey`]s to property values. #[derive(Clone, Debug, Default, PartialEq, Eq)] -pub(crate) struct Properties { +pub struct Properties { inner: HashMap, } #[allow(dead_code)] impl Properties { - pub(crate) fn new() -> Self { + /// Create a new empty [`Properties`]. + pub fn new() -> Self { Default::default() } - pub(crate) fn insert(&mut self, properties_key: PropertiesKey, value: PropertyValue) { + /// Insert a new key/value pair into this map. + pub fn insert(&mut self, properties_key: PropertiesKey, value: PropertyValue) { let _ = self .inner // If we don't clone then we don't get to log a useful warning for a value getting overwritten. @@ -146,7 +162,8 @@ impl Properties { .or_insert(value); } - pub(crate) fn get(&self, properties_key: &PropertiesKey) -> Option<&PropertyValue> { + /// Given a [`PropertiesKey`], return the corresponding value, if any. + pub fn get(&self, properties_key: &PropertiesKey) -> Option<&PropertyValue> { self.inner.get(properties_key) } } @@ -186,7 +203,7 @@ impl Section for SectionInner { fn get(&self, name: &str) -> Option<&str> { self.properties - .get(to_ascii_lowercase(name).as_ref()) + .get(name.to_ascii_lowercase().as_str()) .map(|prop| prop.value()) } @@ -195,8 +212,7 @@ impl Section for SectionInner { } fn insert(&mut self, name: String, value: Property) { - self.properties - .insert(to_ascii_lowercase(&name).into(), value); + self.properties.insert(name.to_ascii_lowercase(), value); } } @@ -250,7 +266,7 @@ impl Section for Profile { /// A `[sso-session name]` section in the config. #[derive(Debug, Clone, Eq, PartialEq)] -pub(crate) struct SsoSession(SectionInner); +pub struct SsoSession(SectionInner); impl SsoSession { /// Create a new SSO session section. @@ -262,7 +278,7 @@ impl SsoSession { } /// Returns a reference to the property named `name` - pub(crate) fn get(&self, name: &str) -> Option<&str> { + pub fn get(&self, name: &str) -> Option<&str> { self.0.get(name) } } @@ -289,119 +305,119 @@ impl Section for SsoSession { } } -#[cfg(test)] -mod test { - use super::PropertiesKey; - use crate::provider_config::ProviderConfig; - use aws_types::os_shim_internal::{Env, Fs}; - - #[tokio::test] - async fn test_other_properties_path_get() { - let _ = tracing_subscriber::fmt::try_init(); - const CFG: &str = r#"[default] -services = foo - -[services foo] -s3 = - endpoint_url = http://localhost:3000 - setting_a = foo - setting_b = bar - -ec2 = - endpoint_url = http://localhost:2000 - setting_a = foo - -[services bar] -ec2 = - endpoint_url = http://localhost:3000 - setting_b = bar -"#; - let env = Env::from_slice(&[("AWS_CONFIG_FILE", "config")]); - let fs = Fs::from_slice(&[("config", CFG)]); - - let provider_config = ProviderConfig::no_configuration().with_env(env).with_fs(fs); - - let p = provider_config.try_profile().await.unwrap(); - let other_sections = p.other_sections(); - - assert_eq!( - "http://localhost:3000", - other_sections - .get(&PropertiesKey { - section_key: "services".to_owned(), - section_name: "foo".to_owned(), - property_name: "s3".to_owned(), - sub_property_name: Some("endpoint_url".to_owned()) - }) - .expect("setting exists at path") - ); - assert_eq!( - "foo", - other_sections - .get(&PropertiesKey { - section_key: "services".to_owned(), - section_name: "foo".to_owned(), - property_name: "s3".to_owned(), - sub_property_name: Some("setting_a".to_owned()) - }) - .expect("setting exists at path") - ); - assert_eq!( - "bar", - other_sections - .get(&PropertiesKey { - section_key: "services".to_owned(), - section_name: "foo".to_owned(), - property_name: "s3".to_owned(), - sub_property_name: Some("setting_b".to_owned()) - }) - .expect("setting exists at path") - ); - - assert_eq!( - "http://localhost:2000", - other_sections - .get(&PropertiesKey { - section_key: "services".to_owned(), - section_name: "foo".to_owned(), - property_name: "ec2".to_owned(), - sub_property_name: Some("endpoint_url".to_owned()) - }) - .expect("setting exists at path") - ); - assert_eq!( - "foo", - other_sections - .get(&PropertiesKey { - section_key: "services".to_owned(), - section_name: "foo".to_owned(), - property_name: "ec2".to_owned(), - sub_property_name: Some("setting_a".to_owned()) - }) - .expect("setting exists at path") - ); - - assert_eq!( - "http://localhost:3000", - other_sections - .get(&PropertiesKey { - section_key: "services".to_owned(), - section_name: "bar".to_owned(), - property_name: "ec2".to_owned(), - sub_property_name: Some("endpoint_url".to_owned()) - }) - .expect("setting exists at path") - ); - assert_eq!( - "bar", - other_sections - .get(&PropertiesKey { - section_key: "services".to_owned(), - section_name: "bar".to_owned(), - property_name: "ec2".to_owned(), - sub_property_name: Some("setting_b".to_owned()) - }) - .expect("setting exists at path") - ); - } -} +// TODO +// #[cfg(test)] +// mod test { +// use super::PropertiesKey; +// use aws_types::os_shim_internal::{Env, Fs}; +// +// #[tokio::test] +// async fn test_other_properties_path_get() { +// let _ = tracing_subscriber::fmt::try_init(); +// const CFG: &str = r#"[default] +// services = foo +// +// [services foo] +// s3 = +// endpoint_url = http://localhost:3000 +// setting_a = foo +// setting_b = bar +// +// ec2 = +// endpoint_url = http://localhost:2000 +// setting_a = foo +// +// [services bar] +// ec2 = +// endpoint_url = http://localhost:3000 +// setting_b = bar +// "#; +// let env = Env::from_slice(&[("AWS_CONFIG_FILE", "config")]); +// let fs = Fs::from_slice(&[("config", CFG)]); +// +// let provider_config = ProviderConfig::no_configuration().with_env(env).with_fs(fs); +// +// let p = provider_config.try_profile().await.unwrap(); +// let other_sections = p.other_sections(); +// +// assert_eq!( +// "http://localhost:3000", +// other_sections +// .get(&PropertiesKey { +// section_key: "services".to_owned(), +// section_name: "foo".to_owned(), +// property_name: "s3".to_owned(), +// sub_property_name: Some("endpoint_url".to_owned()) +// }) +// .expect("setting exists at path") +// ); +// assert_eq!( +// "foo", +// other_sections +// .get(&PropertiesKey { +// section_key: "services".to_owned(), +// section_name: "foo".to_owned(), +// property_name: "s3".to_owned(), +// sub_property_name: Some("setting_a".to_owned()) +// }) +// .expect("setting exists at path") +// ); +// assert_eq!( +// "bar", +// other_sections +// .get(&PropertiesKey { +// section_key: "services".to_owned(), +// section_name: "foo".to_owned(), +// property_name: "s3".to_owned(), +// sub_property_name: Some("setting_b".to_owned()) +// }) +// .expect("setting exists at path") +// ); +// +// assert_eq!( +// "http://localhost:2000", +// other_sections +// .get(&PropertiesKey { +// section_key: "services".to_owned(), +// section_name: "foo".to_owned(), +// property_name: "ec2".to_owned(), +// sub_property_name: Some("endpoint_url".to_owned()) +// }) +// .expect("setting exists at path") +// ); +// assert_eq!( +// "foo", +// other_sections +// .get(&PropertiesKey { +// section_key: "services".to_owned(), +// section_name: "foo".to_owned(), +// property_name: "ec2".to_owned(), +// sub_property_name: Some("setting_a".to_owned()) +// }) +// .expect("setting exists at path") +// ); +// +// assert_eq!( +// "http://localhost:3000", +// other_sections +// .get(&PropertiesKey { +// section_key: "services".to_owned(), +// section_name: "bar".to_owned(), +// property_name: "ec2".to_owned(), +// sub_property_name: Some("endpoint_url".to_owned()) +// }) +// .expect("setting exists at path") +// ); +// assert_eq!( +// "bar", +// other_sections +// .get(&PropertiesKey { +// section_key: "services".to_owned(), +// section_name: "bar".to_owned(), +// property_name: "ec2".to_owned(), +// sub_property_name: Some("setting_b".to_owned()) +// }) +// .expect("setting exists at path") +// ); +// } +// } diff --git a/aws/rust-runtime/aws-config/src/profile/parser/source.rs b/aws/rust-runtime/aws-runtime/src/profile/source.rs similarity index 97% rename from aws/rust-runtime/aws-config/src/profile/parser/source.rs rename to aws/rust-runtime/aws-runtime/src/profile/source.rs index dc47c9b997..2640d7071c 100644 --- a/aws/rust-runtime/aws-config/src/profile/parser/source.rs +++ b/aws/rust-runtime/aws-runtime/src/profile/source.rs @@ -3,11 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -use crate::fs_util::{home_dir, Os}; +//! Code for handling in-memory sources of profile data use super::error::{CouldNotReadProfileFile, ProfileFileLoadError}; +use crate::fs_util::{home_dir, Os}; use crate::profile::profile_file::{ProfileFile, ProfileFileKind, ProfileFiles}; - use aws_smithy_types::error::display::DisplayErrorContext; use aws_types::os_shim_internal; use std::borrow::Cow; @@ -20,26 +20,28 @@ const HOME_EXPANSION_FAILURE_WARNING: &str = "home directory expansion was requested (via `~` character) for the profile \ config file path, but no home directory could be determined"; +#[derive(Debug)] /// In-memory source of profile data -pub(super) struct Source { +pub struct Source { /// Profile file sources - pub(super) files: Vec, + pub(crate) files: Vec, /// Profile to use /// /// Overridden via `$AWS_PROFILE`, defaults to `default` - pub(super) profile: Cow<'static, str>, + pub profile: Cow<'static, str>, } +#[derive(Debug)] /// In-memory configuration file -pub(super) struct File { - pub(super) kind: ProfileFileKind, - pub(super) path: Option, - pub(super) contents: String, +pub struct File { + pub(crate) kind: ProfileFileKind, + pub(crate) path: Option, + pub(crate) contents: String, } /// Load a [`Source`] from a given environment and filesystem. -pub(super) async fn load( +pub async fn load( proc_env: &os_shim_internal::Env, fs: &os_shim_internal::Fs, profile_files: &ProfileFiles, @@ -198,13 +200,13 @@ fn expand_home( #[cfg(test)] mod tests { - use crate::profile::parser::source::{ + use crate::profile::error::ProfileFileLoadError; + use crate::profile::profile_file::{ProfileFile, ProfileFileKind, ProfileFiles}; + use crate::profile::source::{ expand_home, load, load_config_file, HOME_EXPANSION_FAILURE_WARNING, }; - use crate::profile::parser::ProfileFileLoadError; - use crate::profile::profile_file::{ProfileFile, ProfileFileKind, ProfileFiles}; use aws_types::os_shim_internal::{Env, Fs}; - use futures_util::FutureExt; + use futures_util::future::FutureExt; use serde::Deserialize; use std::collections::HashMap; use std::error::Error; diff --git a/aws/rust-runtime/aws-types/src/lib.rs b/aws/rust-runtime/aws-types/src/lib.rs index 83b55b8654..926b44dd78 100644 --- a/aws/rust-runtime/aws-types/src/lib.rs +++ b/aws/rust-runtime/aws-types/src/lib.rs @@ -24,6 +24,8 @@ pub mod os_shim_internal; pub mod region; pub mod request_id; pub mod sdk_config; +pub mod service_config; + pub use sdk_config::SdkConfig; use aws_smithy_types::config_bag::{Storable, StoreReplace}; diff --git a/aws/rust-runtime/aws-types/src/sdk_config.rs b/aws/rust-runtime/aws-types/src/sdk_config.rs index 43022f9ca1..229475dc67 100644 --- a/aws/rust-runtime/aws-types/src/sdk_config.rs +++ b/aws/rust-runtime/aws-types/src/sdk_config.rs @@ -7,12 +7,14 @@ //! AWS Shared Config //! -//! This module contains an shared configuration representation that is agnostic from a specific service. +//! This module contains a shared configuration representation that is agnostic from a specific service. use crate::app_name::AppName; use crate::docs_for; use crate::region::Region; +use std::sync::Arc; +use crate::service_config::LoadServiceConfig; use aws_credential_types::provider::token::SharedTokenProvider; pub use aws_credential_types::provider::SharedCredentialsProvider; use aws_smithy_async::rt::sleep::AsyncSleep; @@ -68,6 +70,7 @@ pub struct SdkConfig { use_fips: Option, use_dual_stack: Option, behavior_version: Option, + service_config: Option>>, } /// Builder for AWS Shared Configuration @@ -92,6 +95,7 @@ pub struct Builder { use_fips: Option, use_dual_stack: Option, behavior_version: Option, + service_config: Option>, } impl Builder { @@ -606,6 +610,29 @@ impl Builder { self } + /// Sets the service config provider for the [`SdkConfig`]. + /// + /// This provider is used when creating a service-specific config from an + /// `SdkConfig` and provides access to config defined in the environment + /// which would otherwise be inaccessible. + pub fn service_config(mut self, service_config: impl LoadServiceConfig + 'static) -> Self { + self.set_service_config(Some(service_config)); + self + } + + /// Sets the service config provider for the [`SdkConfig`]. + /// + /// This provider is used when creating a service-specific config from an + /// `SdkConfig` and provides access to config defined in the environment + /// which would otherwise be inaccessible. + pub fn set_service_config( + &mut self, + service_config: Option, + ) -> &mut Self { + self.service_config = service_config.map(|it| Box::new(it) as _); + self + } + /// Build a [`SdkConfig`] from this builder. pub fn build(self) -> SdkConfig { SdkConfig { @@ -624,6 +651,7 @@ impl Builder { time_source: self.time_source, behavior_version: self.behavior_version, stalled_stream_protection_config: self.stalled_stream_protection_config, + service_config: self.service_config.map(Arc::new), } } } @@ -775,6 +803,11 @@ impl SdkConfig { self.behavior_version.clone() } + /// Return an immutable reference to the service config provider configured for this client. + pub fn service_config(&self) -> Option<&dyn LoadServiceConfig> { + self.service_config.as_deref().map(|it| it.as_ref()) + } + /// Config builder /// /// _Important:_ Using the `aws-config` crate to configure the SDK is preferred to invoking this @@ -807,6 +840,8 @@ impl SdkConfig { use_dual_stack: self.use_dual_stack, behavior_version: self.behavior_version, stalled_stream_protection_config: self.stalled_stream_protection_config, + // TODO + service_config: None, } } } diff --git a/aws/rust-runtime/aws-types/src/service_config.rs b/aws/rust-runtime/aws-types/src/service_config.rs new file mode 100644 index 0000000000..99c2991bed --- /dev/null +++ b/aws/rust-runtime/aws-types/src/service_config.rs @@ -0,0 +1,145 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +//! Code for extracting service config from the user's environment. + +use std::fmt; + +/// A struct used with the [`LoadServiceConfig`] trait to extract service config from the user's environment. +// [profile active-profile] +// services = dev +// +// [services dev] +// service-id = +// config-key = config-value +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct ServiceConfigKey<'a> { + service_id: &'a str, + profile: &'a str, + env: &'a str, +} + +impl<'a> ServiceConfigKey<'a> { + /// Create a new [`ServiceConfigKey`] builder struct. + pub fn builder() -> builder::Builder<'a> { + Default::default() + } + /// Get the service ID. + pub fn service_id(&self) -> &'a str { + self.service_id + } + /// Get the profile key. + pub fn profile(&self) -> &'a str { + self.profile + } + /// Get the environment key. + pub fn env(&self) -> &'a str { + self.env + } +} + +pub mod builder { + //! Builder for [`ServiceConfigKey`]. + + use super::ServiceConfigKey; + use std::fmt; + + /// Builder for [`ServiceConfigKey`]. + #[derive(Default, Debug)] + pub struct Builder<'a> { + service_id: Option<&'a str>, + profile: Option<&'a str>, + env: Option<&'a str>, + } + + impl<'a> Builder<'a> { + /// Set the service ID. + pub fn service_id(mut self, service_id: &'a str) -> Self { + self.service_id = Some(service_id); + self + } + + /// Set the profile key. + pub fn profile(mut self, profile: &'a str) -> Self { + self.profile = Some(profile); + self + } + + /// Set the environment key. + pub fn env(mut self, env: &'a str) -> Self { + self.env = Some(env); + self + } + + /// Build the [`ServiceConfigKey`]. + /// + /// Returns an error if any of the required fields are missing. + pub fn build(self) -> Result, Error> { + Ok(ServiceConfigKey { + service_id: self.service_id.ok_or_else(Error::missing_service_id)?, + profile: self.profile.ok_or_else(Error::missing_profile)?, + env: self.env.ok_or_else(Error::missing_env)?, + }) + } + } + + #[derive(Debug)] + enum ErrorKind { + MissingServiceId, + MissingProfile, + MissingEnv, + } + + impl fmt::Display for ErrorKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ErrorKind::MissingServiceId => write!(f, "missing required service-id"), + ErrorKind::MissingProfile => write!(f, "missing required active profile name"), + ErrorKind::MissingEnv => write!(f, "missing required environment variable name"), + } + } + } + + /// Error type for [`ServiceConfigKey::builder`] + #[derive(Debug)] + pub struct Error { + kind: ErrorKind, + } + + impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "couldn't build a ServiceEnvConfigKey: {}", self.kind) + } + } + + impl std::error::Error for Error {} + + impl Error { + /// Create a new "missing service ID" error + pub fn missing_service_id() -> Self { + Self { + kind: ErrorKind::MissingServiceId, + } + } + /// Create a new "missing profile key" error + pub fn missing_profile() -> Self { + Self { + kind: ErrorKind::MissingProfile, + } + } + /// Create a new "missing env key" error + pub fn missing_env() -> Self { + Self { + kind: ErrorKind::MissingEnv, + } + } + } +} + +/// Implementers of this trait can provide service config defined in a user's environment. +pub trait LoadServiceConfig: fmt::Debug + Send + Sync { + /// Given a [`ServiceConfigKey`], return the value associated with it. + fn load_config(&self, key: ServiceConfigKey<'_>) -> Option; +} diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCodegenDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCodegenDecorator.kt index 0fa045a76f..9facd1ede9 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCodegenDecorator.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCodegenDecorator.kt @@ -58,6 +58,7 @@ val DECORATORS: List = RetryInformationHeaderDecorator(), RemoveDefaultsDecorator(), TokenProvidersDecorator(), + ServiceEnvConfigDecorator(), ), // Service specific decorators ApiGatewayDecorator().onlyApplyTo("com.amazonaws.apigateway#BackplaneControlService"), diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/RegionDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/RegionDecorator.kt index 37b9a59b40..7f5ce13de8 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/RegionDecorator.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/RegionDecorator.kt @@ -80,6 +80,8 @@ import software.amazon.smithy.rust.codegen.core.util.thenSingletonListOf class RegionDecorator : ClientCodegenDecorator { override val name: String = "Region" override val order: Byte = 0 + private val envKey = "AWS_REGION".dq() + private val profileKey = "region".dq() // Services that have an endpoint ruleset that references the SDK::Region built in, or // that use SigV4, both need a configurable region. @@ -102,8 +104,15 @@ class RegionDecorator : ClientCodegenDecorator { adhocCustomization { section -> rust( """ - ${section.serviceConfigBuilder} = - ${section.serviceConfigBuilder}.region(${section.sdkConfig}.region().cloned()); + ${section.serviceConfigBuilder}.set_region( + ${section.sdkConfig} + .service_config() + .and_then(|conf| { + conf.load_config(service_config_key($envKey, $profileKey)) + .map(Region::new) + }) + .or_else(|| ${section.sdkConfig}.region().cloned()), + ); """, ) } diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SdkConfigDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SdkConfigDecorator.kt index f071835fc4..2b1872c569 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SdkConfigDecorator.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SdkConfigDecorator.kt @@ -20,6 +20,8 @@ import software.amazon.smithy.rust.codegen.core.smithy.customize.AdHocCustomizat import software.amazon.smithy.rust.codegen.core.smithy.customize.AdHocSection import software.amazon.smithy.rust.codegen.core.smithy.customize.adhocCustomization import software.amazon.smithy.rust.codegen.core.smithy.customize.writeCustomizations +import software.amazon.smithy.rust.codegen.core.util.dq +import software.amazon.smithy.rust.codegen.core.util.toSnakeCase sealed class SdkConfigSection(name: String) : AdHocSection(name) { /** @@ -54,8 +56,18 @@ object SdkConfigCustomization { map: Writable?, ) = adhocCustomization { section -> val mapBlock = map?.let { writable { rust(".map(#W)", it) } } ?: writable { } + val envKey = "AWS_${fieldName.toSnakeCase().uppercase()}".dq() + val profileKey = fieldName.toSnakeCase().dq() + rustTemplate( - "${section.serviceConfigBuilder}.set_$fieldName(${section.sdkConfig}.$fieldName()#{map});", + """ + ${section.serviceConfigBuilder}.set_$fieldName( + ${section.sdkConfig} + .service_config() + .and_then(|conf| conf.load_config(service_config_key($envKey, $profileKey)).map(|it| it.parse().unwrap())) + .or_else(|| ${section.sdkConfig}.$fieldName()#{map}) + ); + """, "map" to mapBlock, ) } diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/ServiceEnvConfigDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/ServiceEnvConfigDecorator.kt new file mode 100644 index 0000000000..87e9158b7b --- /dev/null +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/ServiceEnvConfigDecorator.kt @@ -0,0 +1,46 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rustsdk + +import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext +import software.amazon.smithy.rust.codegen.client.smithy.ClientRustModule +import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator +import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate +import software.amazon.smithy.rust.codegen.core.smithy.RustCrate +import software.amazon.smithy.rust.codegen.core.util.dq +import software.amazon.smithy.rust.codegen.core.util.sdkId +import software.amazon.smithy.rust.codegen.core.util.toSnakeCase + +class ServiceEnvConfigDecorator : ClientCodegenDecorator { + override val name: String = "ServiceEnvConfigDecorator" + override val order: Byte = 10 + + override fun extras( + codegenContext: ClientCodegenContext, + rustCrate: RustCrate, + ) { + val rc = codegenContext.runtimeConfig + val serviceId = codegenContext.serviceShape.sdkId().toSnakeCase().dq() + rustCrate.withModule(ClientRustModule.config) { + rustTemplate( + """ + fn service_config_key<'a>( + env: &'a str, + profile: &'a str, + ) -> aws_types::service_config::ServiceConfigKey<'a> { + #{ServiceConfigKey}::builder() + .service_id($serviceId) + .env(env) + .profile(profile) + .build() + .expect("all field sets explicitly, can't fail") + } + """, + "ServiceConfigKey" to AwsRuntimeType.awsTypes(rc).resolve("service_config::ServiceConfigKey"), + ) + } + } +} diff --git a/aws/sdk/integration-tests/dynamodb/tests/shared-config.rs b/aws/sdk/integration-tests/dynamodb/tests/shared-config.rs index 02786fe0af..e69e33a442 100644 --- a/aws/sdk/integration-tests/dynamodb/tests/shared-config.rs +++ b/aws/sdk/integration-tests/dynamodb/tests/shared-config.rs @@ -3,7 +3,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -use aws_sdk_dynamodb::config::{Credentials, Region, StalledStreamProtectionConfig}; +use aws_config::profile::profile_file::{ProfileFileKind, ProfileFiles}; +use aws_sdk_dynamodb::config::{ + BehaviorVersion, Credentials, Region, StalledStreamProtectionConfig, +}; use aws_smithy_runtime::client::http::test_util::capture_request; use http::Uri; @@ -27,3 +30,39 @@ async fn shared_config_testbed() { &Uri::from_static("http://localhost:8000") ); } + +#[tokio::test] +async fn service_config_from_profile() { + let _ = tracing_subscriber::fmt::try_init(); + + let config = r#" +[profile custom] +aws_access_key_id = test-access-key-id +aws_secret_access_key = test-secret-access-key +aws_session_token = test-session-token +region = us-east-1 +services = custom + +[services custom] +dynamodb = + region = us-west-1 +"# + .trim(); + + let shared_config = aws_config::ConfigLoader::default() + .behavior_version(BehaviorVersion::latest()) + .profile_name("custom") + .profile_files( + ProfileFiles::builder() + .with_contents(ProfileFileKind::Config, config) + .build(), + ) + .load() + .await; + let service_config = aws_sdk_dynamodb::Config::from(&shared_config); + + assert_eq!( + service_config.region().unwrap(), + &Region::from_static("us-west-1") + ); +} From 1c39d00e7da31717cdc7d7a98a29e4be7e87f19f Mon Sep 17 00:00:00 2001 From: Zelda Hessler Date: Tue, 19 Mar 2024 10:16:22 -0500 Subject: [PATCH 02/23] update tests --- .../src/default_provider/app_name.rs | 2 +- .../src/default_provider/endpoint_url.rs | 2 +- .../ignore_configured_endpoint_urls.rs | 2 +- .../src/default_provider/use_dual_stack.rs | 2 +- .../src/default_provider/use_fips.rs | 2 +- aws/rust-runtime/aws-config/src/lib.rs | 4 +- aws/rust-runtime/aws-config/src/profile.rs | 2 +- .../src/profile/credentials/repr.rs | 6 +- .../aws-config/src/profile/profile_file.rs | 19 + .../aws-config/src/profile/token.rs | 10 +- aws/rust-runtime/aws-runtime/Cargo.toml | 2 +- .../aws-runtime/src/env_config.rs | 464 +++++++++--------- .../aws-runtime/src/profile/profile_file.rs | 5 +- .../aws-runtime/src/profile/profile_set.rs | 2 + .../aws-runtime/src/profile/section.rs | 128 +---- aws/rust-runtime/aws-types/Cargo.toml | 2 +- aws/rust-runtime/aws-types/src/sdk_config.rs | 3 +- .../aws-types/src/service_config.rs | 1 + 18 files changed, 299 insertions(+), 359 deletions(-) diff --git a/aws/rust-runtime/aws-config/src/default_provider/app_name.rs b/aws/rust-runtime/aws-config/src/default_provider/app_name.rs index f9970bc81c..f0bf8485ea 100644 --- a/aws/rust-runtime/aws-config/src/default_provider/app_name.rs +++ b/aws/rust-runtime/aws-config/src/default_provider/app_name.rs @@ -89,9 +89,9 @@ impl Builder { #[cfg(test)] mod tests { use super::*; - use crate::profile::profile_file::{ProfileFileKind, ProfileFiles}; use crate::provider_config::ProviderConfig; use crate::test_case::{no_traffic_client, InstantSleep}; + use aws_runtime::profile::profile_file::{ProfileFileKind, ProfileFiles}; use aws_smithy_runtime_api::client::behavior_version::BehaviorVersion; use aws_types::os_shim_internal::{Env, Fs}; diff --git a/aws/rust-runtime/aws-config/src/default_provider/endpoint_url.rs b/aws/rust-runtime/aws-config/src/default_provider/endpoint_url.rs index 656212111d..04a9390547 100644 --- a/aws/rust-runtime/aws-config/src/default_provider/endpoint_url.rs +++ b/aws/rust-runtime/aws-config/src/default_provider/endpoint_url.rs @@ -41,8 +41,8 @@ pub async fn endpoint_url_provider(provider_config: &ProviderConfig) -> Option #[cfg(test)] mod test { use crate::default_provider::use_dual_stack::use_dual_stack_provider; - use crate::profile::profile_file::{ProfileFileKind, ProfileFiles}; use crate::provider_config::ProviderConfig; + use aws_runtime::profile::profile_file::{ProfileFileKind, ProfileFiles}; use aws_types::os_shim_internal::{Env, Fs}; use tracing_test::traced_test; diff --git a/aws/rust-runtime/aws-config/src/default_provider/use_fips.rs b/aws/rust-runtime/aws-config/src/default_provider/use_fips.rs index 4124a6be84..a546f18145 100644 --- a/aws/rust-runtime/aws-config/src/default_provider/use_fips.rs +++ b/aws/rust-runtime/aws-config/src/default_provider/use_fips.rs @@ -40,8 +40,8 @@ pub async fn use_fips_provider(provider_config: &ProviderConfig) -> Option #[cfg(test)] mod test { use crate::default_provider::use_fips::use_fips_provider; - use crate::profile::profile_file::{ProfileFileKind, ProfileFiles}; use crate::provider_config::ProviderConfig; + use aws_runtime::profile::profile_file::{ProfileFileKind, ProfileFiles}; use aws_types::os_shim_internal::{Env, Fs}; use tracing_test::traced_test; diff --git a/aws/rust-runtime/aws-config/src/lib.rs b/aws/rust-runtime/aws-config/src/lib.rs index 3a04bbca85..71f8c77721 100644 --- a/aws/rust-runtime/aws-config/src/lib.rs +++ b/aws/rust-runtime/aws-config/src/lib.rs @@ -873,11 +873,11 @@ mod loader { #[cfg(test)] mod test { - use crate::profile::profile_file::{ProfileFileKind, ProfileFiles}; use crate::test_case::{no_traffic_client, InstantSleep}; use crate::BehaviorVersion; use crate::{defaults, ConfigLoader}; use aws_credential_types::provider::ProvideCredentials; + use aws_runtime::profile::profile_file::{ProfileFileKind, ProfileFiles}; use aws_smithy_async::rt::sleep::TokioSleep; use aws_smithy_runtime::client::http::test_util::{infallible_client_fn, NeverClient}; use aws_types::app_name::AppName; @@ -886,8 +886,8 @@ mod loader { use std::sync::Arc; use tracing_test::traced_test; - #[tokio::test] #[traced_test] + #[tokio::test] async fn provider_config_used() { let env = Env::from_slice(&[ ("AWS_MAX_ATTEMPTS", "10"), diff --git a/aws/rust-runtime/aws-config/src/profile.rs b/aws/rust-runtime/aws-config/src/profile.rs index 840d80a30f..09792ae6f4 100644 --- a/aws/rust-runtime/aws-config/src/profile.rs +++ b/aws/rust-runtime/aws-config/src/profile.rs @@ -8,7 +8,7 @@ //! AWS profiles are typically stored in `~/.aws/config` and `~/.aws/credentials`. For more details //! see the [`load`] function. -mod parser; +pub mod parser; pub mod credentials; pub mod profile_file; diff --git a/aws/rust-runtime/aws-config/src/profile/credentials/repr.rs b/aws/rust-runtime/aws-config/src/profile/credentials/repr.rs index e4f02eda98..8c81aa79a2 100644 --- a/aws/rust-runtime/aws-config/src/profile/credentials/repr.rs +++ b/aws/rust-runtime/aws-config/src/profile/credentials/repr.rs @@ -470,10 +470,9 @@ fn credential_process_from_profile( #[cfg(test)] mod tests { - use crate::profile::credentials::repr::{BaseProvider, ProfileChain}; + use crate::profile::credentials::repr::BaseProvider; use crate::sensitive_command::CommandWithSensitiveArgs; use serde::Deserialize; - use std::collections::HashMap; #[cfg(feature = "test-utils")] #[test] @@ -517,6 +516,7 @@ mod tests { } } + #[cfg(feature = "test-utils")] #[derive(Deserialize)] struct TestCase { docs: String, @@ -524,6 +524,7 @@ mod tests { output: TestOutput, } + #[cfg(feature = "test-utils")] #[derive(Deserialize)] struct TestInput { profiles: HashMap>, @@ -532,6 +533,7 @@ mod tests { sso_sessions: HashMap>, } + #[cfg(feature = "test-utils")] fn to_test_output(profile_chain: ProfileChain<'_>) -> Vec { let mut output = vec![]; match profile_chain.base { diff --git a/aws/rust-runtime/aws-config/src/profile/profile_file.rs b/aws/rust-runtime/aws-config/src/profile/profile_file.rs index 5bad053522..3e745934ce 100644 --- a/aws/rust-runtime/aws-config/src/profile/profile_file.rs +++ b/aws/rust-runtime/aws-config/src/profile/profile_file.rs @@ -3,6 +3,25 @@ * SPDX-License-Identifier: Apache-2.0 */ +//! Re-exports for types since moved to the aws-runtime crate. + +/// Use aws_runtime::profile::profile_file::ProfileFiles instead. +#[deprecated( + since = "1.1.10", + note = "Use aws_runtime::profile::profile_file::ProfileFiles instead." +)] pub type ProfileFiles = aws_runtime::profile::profile_file::ProfileFiles; + +/// Use aws_runtime::profile::profile_file::Builder instead. +#[deprecated( + since = "1.1.10", + note = "Use aws_runtime::profile::profile_file::Builder." +)] pub type Builder = aws_runtime::profile::profile_file::Builder; + +/// Use aws_runtime::profile::profile_file::ProfileFileKind instead. +#[deprecated( + since = "1.1.10", + note = "Use aws_runtime::profile::profile_file::ProfileFileKind." +)] pub type ProfileFileKind = aws_runtime::profile::profile_file::ProfileFileKind; diff --git a/aws/rust-runtime/aws-config/src/profile/token.rs b/aws/rust-runtime/aws-config/src/profile/token.rs index 1865e65d7f..b53810a2f2 100644 --- a/aws/rust-runtime/aws-config/src/profile/token.rs +++ b/aws/rust-runtime/aws-config/src/profile/token.rs @@ -5,14 +5,14 @@ //! Profile File Based Token Providers -use crate::{ - profile::{cell::ErrorTakingOnceCell, profile_file::ProfileFiles, ProfileSet}, - provider_config::ProviderConfig, - sso::SsoTokenProvider, -}; +use crate::profile::cell::ErrorTakingOnceCell; +use crate::provider_config::ProviderConfig; +use crate::sso::SsoTokenProvider; use aws_credential_types::provider::{ error::TokenError, future, token::ProvideToken, token::Result as TokenResult, }; +use aws_runtime::profile::profile_file::ProfileFiles; +use aws_runtime::profile::profile_set::ProfileSet; use aws_types::{region::Region, SdkConfig}; async fn load_profile_set(provider_config: &ProviderConfig) -> Result<&ProfileSet, TokenError> { diff --git a/aws/rust-runtime/aws-runtime/Cargo.toml b/aws/rust-runtime/aws-runtime/Cargo.toml index ab6419e408..ee56bfdd18 100644 --- a/aws/rust-runtime/aws-runtime/Cargo.toml +++ b/aws/rust-runtime/aws-runtime/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aws-runtime" -version = "1.1.8" +version = "1.1.9" authors = ["AWS Rust SDK Team "] description = "Runtime support code for the AWS SDK. This crate isn't intended to be used directly." edition = "2021" diff --git a/aws/rust-runtime/aws-runtime/src/env_config.rs b/aws/rust-runtime/aws-runtime/src/env_config.rs index 9880f7bf13..53f7d84b9e 100644 --- a/aws/rust-runtime/aws-runtime/src/env_config.rs +++ b/aws/rust-runtime/aws-runtime/src/env_config.rs @@ -124,8 +124,6 @@ impl Error for EnvConfigError { /// /// `EnvConfigValue` will first look in the environment, then the AWS profile. They track the /// provenance of properties so that unified validation errors can be created. -/// -/// For a usage example, see [`crate::default_provider::retry_config`] #[derive(Default, Debug)] pub struct EnvConfigValue<'a> { environment_variable: Option>, @@ -274,220 +272,248 @@ fn format_service_id_for_profile(service_id: impl AsRef) -> String { service_id.as_ref().to_lowercase().replace(' ', "-") } -// TODO -// #[cfg(test)] -// mod test { -// use super::EnvConfigValue; -// use crate::provider_config::ProviderConfig; -// use aws_types::os_shim_internal::{Env, Fs}; -// use std::num::ParseIntError; -// -// fn validate_some_key(s: &str) -> Result { -// s.parse() -// } -// -// #[tokio::test] -// async fn test_service_config_multiple_services() { -// let env = Env::from_slice(&[ -// ("AWS_CONFIG_FILE", "config"), -// ("AWS_SOME_KEY", "1"), -// ("AWS_SOME_KEY_SERVICE", "2"), -// ("AWS_SOME_KEY_ANOTHER_SERVICE", "3"), -// ]); -// let fs = Fs::from_slice(&[( -// "config", -// r#"[default] -// some_key = 4 -// services = dev -// -// [services dev] -// service = -// some_key = 5 -// another_service = -// some_key = 6 -// "#, -// )]); -// -// let provider_config = ProviderConfig::no_configuration().with_env(env).with_fs(fs); -// let global_from_env = EnvConfigValue::new() -// .env("AWS_SOME_KEY") -// .profile("some_key") -// .validate(&provider_config, validate_some_key) -// .await -// .expect("config resolution succeeds"); -// assert_eq!(Some(1), global_from_env); -// -// let service_from_env = EnvConfigValue::new() -// .env("AWS_SOME_KEY") -// .profile("some_key") -// .service_id("service") -// .validate(&provider_config, validate_some_key) -// .await -// .expect("config resolution succeeds"); -// assert_eq!(Some(2), service_from_env); -// -// let other_service_from_env = EnvConfigValue::new() -// .env("AWS_SOME_KEY") -// .profile("some_key") -// .service_id("another_service") -// .validate(&provider_config, validate_some_key) -// .await -// .expect("config resolution succeeds"); -// assert_eq!(Some(3), other_service_from_env); -// -// let global_from_profile = EnvConfigValue::new() -// .profile("some_key") -// .validate(&provider_config, validate_some_key) -// .await -// .expect("config resolution succeeds"); -// assert_eq!(Some(4), global_from_profile); -// -// let service_from_profile = EnvConfigValue::new() -// .profile("some_key") -// .service_id("service") -// .validate(&provider_config, validate_some_key) -// .await -// .expect("config resolution succeeds"); -// assert_eq!(Some(5), service_from_profile); -// -// let service_from_profile = EnvConfigValue::new() -// .profile("some_key") -// .service_id("another_service") -// .validate(&provider_config, validate_some_key) -// .await -// .expect("config resolution succeeds"); -// assert_eq!(Some(6), service_from_profile); -// } -// -// #[tokio::test] -// async fn test_service_config_precedence() { -// let env = Env::from_slice(&[ -// ("AWS_CONFIG_FILE", "config"), -// ("AWS_SOME_KEY", "1"), -// ("AWS_SOME_KEY_S3", "2"), -// ]); -// let fs = Fs::from_slice(&[( -// "config", -// r#"[default] -// some_key = 3 -// services = dev -// -// [services dev] -// s3 = -// some_key = 4 -// "#, -// )]); -// -// let provider_config = ProviderConfig::no_configuration().with_env(env).with_fs(fs); -// let global_from_env = EnvConfigValue::new() -// .env("AWS_SOME_KEY") -// .profile("some_key") -// .validate(&provider_config, validate_some_key) -// .await -// .expect("config resolution succeeds"); -// assert_eq!(Some(1), global_from_env); -// -// let service_from_env = EnvConfigValue::new() -// .env("AWS_SOME_KEY") -// .profile("some_key") -// .service_id("s3") -// .validate(&provider_config, validate_some_key) -// .await -// .expect("config resolution succeeds"); -// assert_eq!(Some(2), service_from_env); -// -// let global_from_profile = EnvConfigValue::new() -// .profile("some_key") -// .validate(&provider_config, validate_some_key) -// .await -// .expect("config resolution succeeds"); -// assert_eq!(Some(3), global_from_profile); -// -// let service_from_profile = EnvConfigValue::new() -// .profile("some_key") -// .service_id("s3") -// .validate(&provider_config, validate_some_key) -// .await -// .expect("config resolution succeeds"); -// assert_eq!(Some(4), service_from_profile); -// } -// -// #[tokio::test] -// async fn test_multiple_services() { -// let env = Env::from_slice(&[ -// ("AWS_CONFIG_FILE", "config"), -// ("AWS_SOME_KEY", "1"), -// ("AWS_SOME_KEY_S3", "2"), -// ("AWS_SOME_KEY_EC2", "3"), -// ]); -// let fs = Fs::from_slice(&[( -// "config", -// r#"[default] -// some_key = 4 -// services = dev -// -// [services dev-wrong] -// s3 = -// some_key = 998 -// ec2 = -// some_key = 999 -// -// [services dev] -// s3 = -// some_key = 5 -// ec2 = -// some_key = 6 -// "#, -// )]); -// -// let provider_config = ProviderConfig::no_configuration().with_env(env).with_fs(fs); -// let global_from_env = EnvConfigValue::new() -// .env("AWS_SOME_KEY") -// .profile("some_key") -// .validate(&provider_config, validate_some_key) -// .await -// .expect("config resolution succeeds"); -// assert_eq!(Some(1), global_from_env); -// -// let service_from_env = EnvConfigValue::new() -// .env("AWS_SOME_KEY") -// .profile("some_key") -// .service_id("s3") -// .validate(&provider_config, validate_some_key) -// .await -// .expect("config resolution succeeds"); -// assert_eq!(Some(2), service_from_env); -// -// let service_from_env = EnvConfigValue::new() -// .env("AWS_SOME_KEY") -// .profile("some_key") -// .service_id("ec2") -// .validate(&provider_config, validate_some_key) -// .await -// .expect("config resolution succeeds"); -// assert_eq!(Some(3), service_from_env); -// -// let global_from_profile = EnvConfigValue::new() -// .profile("some_key") -// .validate(&provider_config, validate_some_key) -// .await -// .expect("config resolution succeeds"); -// assert_eq!(Some(4), global_from_profile); -// -// let service_from_profile = EnvConfigValue::new() -// .profile("some_key") -// .service_id("s3") -// .validate(&provider_config, validate_some_key) -// .await -// .expect("config resolution succeeds"); -// assert_eq!(Some(5), service_from_profile); -// -// let service_from_profile = EnvConfigValue::new() -// .profile("some_key") -// .service_id("ec2") -// .validate(&provider_config, validate_some_key) -// .await -// .expect("config resolution succeeds"); -// assert_eq!(Some(6), service_from_profile); -// } -// } +#[cfg(test)] +mod test { + use std::borrow::Cow; + use std::collections::HashMap; + use std::num::ParseIntError; + + use aws_types::os_shim_internal::Env; + + use crate::profile::profile_set::ProfileSet; + use crate::profile::section::{Properties, PropertiesKey}; + + use super::EnvConfigValue; + + fn validate_some_key(s: &str) -> Result { + s.parse() + } + + fn new_prop_key( + section_key: impl Into, + section_name: impl Into, + property_name: impl Into, + sub_property_name: Option>, + ) -> PropertiesKey { + let mut builder = PropertiesKey::builder() + .section_key(section_key) + .section_name(section_name) + .property_name(property_name); + + if let Some(sub_property_name) = sub_property_name { + builder = builder.sub_property_name(sub_property_name); + } + + builder.build().unwrap() + } + + #[tokio::test] + async fn test_service_config_multiple_services() { + let env = Env::from_slice(&[ + ("AWS_CONFIG_FILE", "config"), + ("AWS_SOME_KEY", "1"), + ("AWS_SOME_KEY_SERVICE", "2"), + ("AWS_SOME_KEY_ANOTHER_SERVICE", "3"), + ]); + let profiles = ProfileSet::new( + HashMap::from([( + "default".to_owned(), + HashMap::from([ + ("some_key".to_owned(), "4".to_owned()), + ("services".to_owned(), "dev".to_owned()), + ]), + )]), + Cow::Borrowed("default"), + HashMap::new(), + Properties::new_from_slice(&[ + ( + new_prop_key("services", "dev", "service", Some("some_key")), + "5".to_string(), + ), + ( + new_prop_key("services", "dev", "another_service", Some("some_key")), + "6".to_string(), + ), + ]), + ); + let profiles = Some(&profiles); + let global_from_env = EnvConfigValue::new() + .env("AWS_SOME_KEY") + .profile("some_key") + .validate(&env, profiles, validate_some_key) + .expect("config resolution succeeds"); + assert_eq!(Some(1), global_from_env); + + let service_from_env = EnvConfigValue::new() + .env("AWS_SOME_KEY") + .profile("some_key") + .service_id("service") + .validate(&env, profiles, validate_some_key) + .expect("config resolution succeeds"); + assert_eq!(Some(2), service_from_env); + + let other_service_from_env = EnvConfigValue::new() + .env("AWS_SOME_KEY") + .profile("some_key") + .service_id("another_service") + .validate(&env, profiles, validate_some_key) + .expect("config resolution succeeds"); + assert_eq!(Some(3), other_service_from_env); + + let global_from_profile = EnvConfigValue::new() + .profile("some_key") + .validate(&env, profiles, validate_some_key) + .expect("config resolution succeeds"); + assert_eq!(Some(4), global_from_profile); + + let service_from_profile = EnvConfigValue::new() + .profile("some_key") + .service_id("service") + .validate(&env, profiles, validate_some_key) + .expect("config resolution succeeds"); + assert_eq!(Some(5), service_from_profile); + + let service_from_profile = EnvConfigValue::new() + .profile("some_key") + .service_id("another_service") + .validate(&env, profiles, validate_some_key) + .expect("config resolution succeeds"); + assert_eq!(Some(6), service_from_profile); + } + + #[tokio::test] + async fn test_service_config_precedence() { + let env = Env::from_slice(&[ + ("AWS_CONFIG_FILE", "config"), + ("AWS_SOME_KEY", "1"), + ("AWS_SOME_KEY_S3", "2"), + ]); + + let profiles = ProfileSet::new( + HashMap::from([( + "default".to_owned(), + HashMap::from([ + ("some_key".to_owned(), "3".to_owned()), + ("services".to_owned(), "dev".to_owned()), + ]), + )]), + Cow::Borrowed("default"), + HashMap::new(), + Properties::new_from_slice(&[( + new_prop_key("services", "dev", "s3", Some("some_key")), + "4".to_string(), + )]), + ); + let profiles = Some(&profiles); + let global_from_env = EnvConfigValue::new() + .env("AWS_SOME_KEY") + .profile("some_key") + .validate(&env, profiles, validate_some_key) + .expect("config resolution succeeds"); + assert_eq!(Some(1), global_from_env); + + let service_from_env = EnvConfigValue::new() + .env("AWS_SOME_KEY") + .profile("some_key") + .service_id("s3") + .validate(&env, profiles, validate_some_key) + .expect("config resolution succeeds"); + assert_eq!(Some(2), service_from_env); + + let global_from_profile = EnvConfigValue::new() + .profile("some_key") + .validate(&env, profiles, validate_some_key) + .expect("config resolution succeeds"); + assert_eq!(Some(3), global_from_profile); + + let service_from_profile = EnvConfigValue::new() + .profile("some_key") + .service_id("s3") + .validate(&env, profiles, validate_some_key) + .expect("config resolution succeeds"); + assert_eq!(Some(4), service_from_profile); + } + + #[tokio::test] + async fn test_multiple_services() { + let env = Env::from_slice(&[ + ("AWS_CONFIG_FILE", "config"), + ("AWS_SOME_KEY", "1"), + ("AWS_SOME_KEY_S3", "2"), + ("AWS_SOME_KEY_EC2", "3"), + ]); + + let profiles = ProfileSet::new( + HashMap::from([( + "default".to_owned(), + HashMap::from([ + ("some_key".to_owned(), "4".to_owned()), + ("services".to_owned(), "dev".to_owned()), + ]), + )]), + Cow::Borrowed("default"), + HashMap::new(), + Properties::new_from_slice(&[ + ( + new_prop_key("services", "dev-wrong", "s3", Some("some_key")), + "998".to_string(), + ), + ( + new_prop_key("services", "dev-wrong", "ec2", Some("some_key")), + "999".to_string(), + ), + ( + new_prop_key("services", "dev", "s3", Some("some_key")), + "5".to_string(), + ), + ( + new_prop_key("services", "dev", "ec2", Some("some_key")), + "6".to_string(), + ), + ]), + ); + let profiles = Some(&profiles); + let global_from_env = EnvConfigValue::new() + .env("AWS_SOME_KEY") + .profile("some_key") + .validate(&env, profiles, validate_some_key) + .expect("config resolution succeeds"); + assert_eq!(Some(1), global_from_env); + + let service_from_env = EnvConfigValue::new() + .env("AWS_SOME_KEY") + .profile("some_key") + .service_id("s3") + .validate(&env, profiles, validate_some_key) + .expect("config resolution succeeds"); + assert_eq!(Some(2), service_from_env); + + let service_from_env = EnvConfigValue::new() + .env("AWS_SOME_KEY") + .profile("some_key") + .service_id("ec2") + .validate(&env, profiles, validate_some_key) + .expect("config resolution succeeds"); + assert_eq!(Some(3), service_from_env); + + let global_from_profile = EnvConfigValue::new() + .profile("some_key") + .validate(&env, profiles, validate_some_key) + .expect("config resolution succeeds"); + assert_eq!(Some(4), global_from_profile); + + let service_from_profile = EnvConfigValue::new() + .profile("some_key") + .service_id("s3") + .validate(&env, profiles, validate_some_key) + .expect("config resolution succeeds"); + assert_eq!(Some(5), service_from_profile); + + let service_from_profile = EnvConfigValue::new() + .profile("some_key") + .service_id("ec2") + .validate(&env, profiles, validate_some_key) + .expect("config resolution succeeds"); + assert_eq!(Some(6), service_from_profile); + } +} diff --git a/aws/rust-runtime/aws-runtime/src/profile/profile_file.rs b/aws/rust-runtime/aws-runtime/src/profile/profile_file.rs index dfe9eb33ca..56eff14fa5 100644 --- a/aws/rust-runtime/aws-runtime/src/profile/profile_file.rs +++ b/aws/rust-runtime/aws-runtime/src/profile/profile_file.rs @@ -20,9 +20,8 @@ use std::path::PathBuf; /// /// # Example: Using a custom profile file path /// -/// ```no_run -/// use aws_config::profile::{ProfileFileCredentialsProvider, ProfileFileRegionProvider}; -/// use aws_config::profile::profile_file::{ProfileFiles, ProfileFileKind}; +/// ```no_run,ignore +/// use aws_runtime::profile::profile_file::{ProfileFiles, ProfileFileKind}; /// use std::sync::Arc; /// /// # async fn example() { diff --git a/aws/rust-runtime/aws-runtime/src/profile/profile_set.rs b/aws/rust-runtime/aws-runtime/src/profile/profile_set.rs index 0235aed9ee..e3b2459ae9 100644 --- a/aws/rust-runtime/aws-runtime/src/profile/profile_set.rs +++ b/aws/rust-runtime/aws-runtime/src/profile/profile_set.rs @@ -41,6 +41,7 @@ impl ProfileSet { profiles: HashMap>, selected_profile: impl Into>, sso_sessions: HashMap>, + other_sections: Properties, ) -> Self { use crate::profile::section::Property; @@ -70,6 +71,7 @@ impl ProfileSet { ), ); } + base.other_sections = other_sections; base } diff --git a/aws/rust-runtime/aws-runtime/src/profile/section.rs b/aws/rust-runtime/aws-runtime/src/profile/section.rs index 2580663646..38f6b122b1 100644 --- a/aws/rust-runtime/aws-runtime/src/profile/section.rs +++ b/aws/rust-runtime/aws-runtime/src/profile/section.rs @@ -56,7 +56,7 @@ pub struct PropertiesKey { } impl PropertiesKey { - /// Create a new [`PropertiseKeyBuilder`]. + /// Create a new [`PropertiesKeyBuilder`]. pub fn builder() -> PropertiesKeyBuilder { Default::default() } @@ -149,6 +149,15 @@ impl Properties { Default::default() } + #[cfg(test)] + pub fn new_from_slice(slice: &[(PropertiesKey, PropertyValue)]) -> Self { + let mut properties = Self::new(); + for (key, value) in slice { + properties.insert(key.clone(), value.clone()); + } + properties + } + /// Insert a new key/value pair into this map. pub fn insert(&mut self, properties_key: PropertiesKey, value: PropertyValue) { let _ = self @@ -304,120 +313,3 @@ impl Section for SsoSession { self.0.insert(name, value) } } - -// TODO -// #[cfg(test)] -// mod test { -// use super::PropertiesKey; -// use aws_types::os_shim_internal::{Env, Fs}; -// -// #[tokio::test] -// async fn test_other_properties_path_get() { -// let _ = tracing_subscriber::fmt::try_init(); -// const CFG: &str = r#"[default] -// services = foo -// -// [services foo] -// s3 = -// endpoint_url = http://localhost:3000 -// setting_a = foo -// setting_b = bar -// -// ec2 = -// endpoint_url = http://localhost:2000 -// setting_a = foo -// -// [services bar] -// ec2 = -// endpoint_url = http://localhost:3000 -// setting_b = bar -// "#; -// let env = Env::from_slice(&[("AWS_CONFIG_FILE", "config")]); -// let fs = Fs::from_slice(&[("config", CFG)]); -// -// let provider_config = ProviderConfig::no_configuration().with_env(env).with_fs(fs); -// -// let p = provider_config.try_profile().await.unwrap(); -// let other_sections = p.other_sections(); -// -// assert_eq!( -// "http://localhost:3000", -// other_sections -// .get(&PropertiesKey { -// section_key: "services".to_owned(), -// section_name: "foo".to_owned(), -// property_name: "s3".to_owned(), -// sub_property_name: Some("endpoint_url".to_owned()) -// }) -// .expect("setting exists at path") -// ); -// assert_eq!( -// "foo", -// other_sections -// .get(&PropertiesKey { -// section_key: "services".to_owned(), -// section_name: "foo".to_owned(), -// property_name: "s3".to_owned(), -// sub_property_name: Some("setting_a".to_owned()) -// }) -// .expect("setting exists at path") -// ); -// assert_eq!( -// "bar", -// other_sections -// .get(&PropertiesKey { -// section_key: "services".to_owned(), -// section_name: "foo".to_owned(), -// property_name: "s3".to_owned(), -// sub_property_name: Some("setting_b".to_owned()) -// }) -// .expect("setting exists at path") -// ); -// -// assert_eq!( -// "http://localhost:2000", -// other_sections -// .get(&PropertiesKey { -// section_key: "services".to_owned(), -// section_name: "foo".to_owned(), -// property_name: "ec2".to_owned(), -// sub_property_name: Some("endpoint_url".to_owned()) -// }) -// .expect("setting exists at path") -// ); -// assert_eq!( -// "foo", -// other_sections -// .get(&PropertiesKey { -// section_key: "services".to_owned(), -// section_name: "foo".to_owned(), -// property_name: "ec2".to_owned(), -// sub_property_name: Some("setting_a".to_owned()) -// }) -// .expect("setting exists at path") -// ); -// -// assert_eq!( -// "http://localhost:3000", -// other_sections -// .get(&PropertiesKey { -// section_key: "services".to_owned(), -// section_name: "bar".to_owned(), -// property_name: "ec2".to_owned(), -// sub_property_name: Some("endpoint_url".to_owned()) -// }) -// .expect("setting exists at path") -// ); -// assert_eq!( -// "bar", -// other_sections -// .get(&PropertiesKey { -// section_key: "services".to_owned(), -// section_name: "bar".to_owned(), -// property_name: "ec2".to_owned(), -// sub_property_name: Some("setting_b".to_owned()) -// }) -// .expect("setting exists at path") -// ); -// } -// } diff --git a/aws/rust-runtime/aws-types/Cargo.toml b/aws/rust-runtime/aws-types/Cargo.toml index fe4d2cbe26..714425a5ac 100644 --- a/aws/rust-runtime/aws-types/Cargo.toml +++ b/aws/rust-runtime/aws-types/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aws-types" -version = "1.1.8" +version = "1.1.9" authors = ["AWS Rust SDK Team ", "Russell Cohen "] description = "Cross-service types for the AWS SDK." edition = "2021" diff --git a/aws/rust-runtime/aws-types/src/sdk_config.rs b/aws/rust-runtime/aws-types/src/sdk_config.rs index 229475dc67..196d3f1228 100644 --- a/aws/rust-runtime/aws-types/src/sdk_config.rs +++ b/aws/rust-runtime/aws-types/src/sdk_config.rs @@ -840,8 +840,7 @@ impl SdkConfig { use_dual_stack: self.use_dual_stack, behavior_version: self.behavior_version, stalled_stream_protection_config: self.stalled_stream_protection_config, - // TODO - service_config: None, + service_config: self.service_config.and_then(Arc::into_inner), } } } diff --git a/aws/rust-runtime/aws-types/src/service_config.rs b/aws/rust-runtime/aws-types/src/service_config.rs index 99c2991bed..99faa7d50c 100644 --- a/aws/rust-runtime/aws-types/src/service_config.rs +++ b/aws/rust-runtime/aws-types/src/service_config.rs @@ -85,6 +85,7 @@ pub mod builder { } } + #[allow(clippy::enum_variant_names)] #[derive(Debug)] enum ErrorKind { MissingServiceId, From 6aee5d96b555afe43394c24dcdd4a9ce88b042d2 Mon Sep 17 00:00:00 2001 From: Zelda Hessler Date: Tue, 19 Mar 2024 10:23:42 -0500 Subject: [PATCH 03/23] move test JSONs --- aws/rust-runtime/aws-runtime/src/env_config.rs | 8 ++++---- .../test-data/file-location-tests.json | 0 .../test-data/profile-parser-tests.json | 0 3 files changed, 4 insertions(+), 4 deletions(-) rename aws/rust-runtime/{aws-config => aws-runtime}/test-data/file-location-tests.json (100%) rename aws/rust-runtime/{aws-config => aws-runtime}/test-data/profile-parser-tests.json (100%) diff --git a/aws/rust-runtime/aws-runtime/src/env_config.rs b/aws/rust-runtime/aws-runtime/src/env_config.rs index 53f7d84b9e..38c06f15b1 100644 --- a/aws/rust-runtime/aws-runtime/src/env_config.rs +++ b/aws/rust-runtime/aws-runtime/src/env_config.rs @@ -456,19 +456,19 @@ mod test { Properties::new_from_slice(&[ ( new_prop_key("services", "dev-wrong", "s3", Some("some_key")), - "998".to_string(), + "998".into(), ), ( new_prop_key("services", "dev-wrong", "ec2", Some("some_key")), - "999".to_string(), + "999".into(), ), ( new_prop_key("services", "dev", "s3", Some("some_key")), - "5".to_string(), + "5".into(), ), ( new_prop_key("services", "dev", "ec2", Some("some_key")), - "6".to_string(), + "6".into(), ), ]), ); diff --git a/aws/rust-runtime/aws-config/test-data/file-location-tests.json b/aws/rust-runtime/aws-runtime/test-data/file-location-tests.json similarity index 100% rename from aws/rust-runtime/aws-config/test-data/file-location-tests.json rename to aws/rust-runtime/aws-runtime/test-data/file-location-tests.json diff --git a/aws/rust-runtime/aws-config/test-data/profile-parser-tests.json b/aws/rust-runtime/aws-runtime/test-data/profile-parser-tests.json similarity index 100% rename from aws/rust-runtime/aws-config/test-data/profile-parser-tests.json rename to aws/rust-runtime/aws-runtime/test-data/profile-parser-tests.json From d847bc8ecb8068964c3ed691c2bb5cc2fd9dd539 Mon Sep 17 00:00:00 2001 From: Zelda Hessler Date: Tue, 19 Mar 2024 10:29:14 -0500 Subject: [PATCH 04/23] fix clippy lints --- .../aws-runtime/src/profile/profile_set.rs | 12 ++++++++---- .../smithy/rustsdk/ServiceEnvConfigDecorator.kt | 1 + 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/aws/rust-runtime/aws-runtime/src/profile/profile_set.rs b/aws/rust-runtime/aws-runtime/src/profile/profile_set.rs index e3b2459ae9..d603c8e018 100644 --- a/aws/rust-runtime/aws-runtime/src/profile/profile_set.rs +++ b/aws/rust-runtime/aws-runtime/src/profile/profile_set.rs @@ -45,8 +45,10 @@ impl ProfileSet { ) -> Self { use crate::profile::section::Property; - let mut base = ProfileSet::default(); - base.selected_profile = selected_profile.into(); + let mut base = ProfileSet { + selected_profile: selected_profile.into(), + ..Default::default() + }; for (name, profile) in profiles { base.profiles.insert( name.clone(), @@ -119,8 +121,10 @@ impl ProfileSet { /// Given a [`Source`] of profile config, parse and merge them into a `ProfileSet`. pub fn parse(source: Source) -> Result { - let mut base = ProfileSet::default(); - base.selected_profile = source.profile; + let mut base = ProfileSet { + selected_profile: source.profile, + ..Default::default() + }; for file in source.files { normalize::merge_in(&mut base, parse_profile_file(&file)?, file.kind); diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/ServiceEnvConfigDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/ServiceEnvConfigDecorator.kt index 87e9158b7b..f70566878b 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/ServiceEnvConfigDecorator.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/ServiceEnvConfigDecorator.kt @@ -27,6 +27,7 @@ class ServiceEnvConfigDecorator : ClientCodegenDecorator { rustCrate.withModule(ClientRustModule.config) { rustTemplate( """ + #[allow(dead_code)] fn service_config_key<'a>( env: &'a str, profile: &'a str, From 4504106832ee837f7767e72d77fdb13d2b4f8571 Mon Sep 17 00:00:00 2001 From: Zelda Hessler Date: Tue, 19 Mar 2024 10:40:28 -0500 Subject: [PATCH 05/23] fix codegen attribute --- .../amazon/smithy/rustsdk/ServiceEnvConfigDecorator.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/ServiceEnvConfigDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/ServiceEnvConfigDecorator.kt index f70566878b..c8646b5e64 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/ServiceEnvConfigDecorator.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/ServiceEnvConfigDecorator.kt @@ -8,6 +8,7 @@ package software.amazon.smithy.rustsdk import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext import software.amazon.smithy.rust.codegen.client.smithy.ClientRustModule import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator +import software.amazon.smithy.rust.codegen.core.rustlang.Attribute import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate import software.amazon.smithy.rust.codegen.core.smithy.RustCrate import software.amazon.smithy.rust.codegen.core.util.dq @@ -25,9 +26,9 @@ class ServiceEnvConfigDecorator : ClientCodegenDecorator { val rc = codegenContext.runtimeConfig val serviceId = codegenContext.serviceShape.sdkId().toSnakeCase().dq() rustCrate.withModule(ClientRustModule.config) { + Attribute.AllowDeadCode.render(this) rustTemplate( """ - #[allow(dead_code)] fn service_config_key<'a>( env: &'a str, profile: &'a str, From 5746e524531392a4b1f16e37d47a871989183524 Mon Sep 17 00:00:00 2001 From: Zelda Hessler Date: Tue, 19 Mar 2024 11:10:28 -0500 Subject: [PATCH 06/23] fix integration test import --- aws/sdk/integration-tests/dynamodb/Cargo.toml | 1 + aws/sdk/integration-tests/dynamodb/tests/shared-config.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/aws/sdk/integration-tests/dynamodb/Cargo.toml b/aws/sdk/integration-tests/dynamodb/Cargo.toml index fabca6704c..135d5f8a43 100644 --- a/aws/sdk/integration-tests/dynamodb/Cargo.toml +++ b/aws/sdk/integration-tests/dynamodb/Cargo.toml @@ -13,6 +13,7 @@ publish = false [dependencies] approx = "0.5.1" aws-config = { path = "../../build/aws-sdk/sdk/aws-config" } +aws-runtime = { path = "../../build/aws-sdk/sdk/aws-runtime" } aws-credential-types = { path = "../../build/aws-sdk/sdk/aws-credential-types", features = ["test-util"] } aws-sdk-dynamodb = { path = "../../build/aws-sdk/sdk/dynamodb", features = ["behavior-version-latest"] } aws-smithy-async = { path = "../../build/aws-sdk/sdk/aws-smithy-async", features = ["test-util"] } diff --git a/aws/sdk/integration-tests/dynamodb/tests/shared-config.rs b/aws/sdk/integration-tests/dynamodb/tests/shared-config.rs index e69e33a442..db29c9d275 100644 --- a/aws/sdk/integration-tests/dynamodb/tests/shared-config.rs +++ b/aws/sdk/integration-tests/dynamodb/tests/shared-config.rs @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -use aws_config::profile::profile_file::{ProfileFileKind, ProfileFiles}; +use aws_runtime::profile::profile_file::{ProfileFileKind, ProfileFiles}; use aws_sdk_dynamodb::config::{ BehaviorVersion, Credentials, Region, StalledStreamProtectionConfig, }; From f1ba0c0bd77bb6e84f6a6496646fd7a6bae6cc5b Mon Sep 17 00:00:00 2001 From: Zelda Hessler Date: Tue, 19 Mar 2024 11:58:24 -0500 Subject: [PATCH 07/23] fix usage of fs_util --- aws/rust-runtime/aws-config/src/fs_util.rs | 62 -------------------- aws/rust-runtime/aws-config/src/lib.rs | 6 +- aws/rust-runtime/aws-config/src/sso/cache.rs | 2 +- aws/rust-runtime/aws-runtime/src/fs_util.rs | 15 ++--- 4 files changed, 11 insertions(+), 74 deletions(-) delete mode 100644 aws/rust-runtime/aws-config/src/fs_util.rs diff --git a/aws/rust-runtime/aws-config/src/fs_util.rs b/aws/rust-runtime/aws-config/src/fs_util.rs deleted file mode 100644 index 89af426d89..0000000000 --- a/aws/rust-runtime/aws-config/src/fs_util.rs +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -use aws_types::os_shim_internal; - -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub(crate) enum Os { - Windows, - NotWindows, -} - -impl Os { - pub(crate) fn real() -> Self { - match std::env::consts::OS { - "windows" => Os::Windows, - _ => Os::NotWindows, - } - } -} - -/// Resolve a home directory given a set of environment variables -pub(crate) fn home_dir(env_var: &os_shim_internal::Env, os: Os) -> Option { - if let Ok(home) = env_var.get("HOME") { - tracing::debug!(src = "HOME", "loaded home directory"); - return Some(home); - } - - if os == Os::Windows { - if let Ok(home) = env_var.get("USERPROFILE") { - tracing::debug!(src = "USERPROFILE", "loaded home directory"); - return Some(home); - } - - let home_drive = env_var.get("HOMEDRIVE"); - let home_path = env_var.get("HOMEPATH"); - tracing::debug!(src = "HOMEDRIVE/HOMEPATH", "loaded home directory"); - if let (Ok(mut drive), Ok(path)) = (home_drive, home_path) { - drive.push_str(&path); - return Some(drive); - } - } - None -} - -#[cfg(test)] -mod test { - use super::*; - use aws_types::os_shim_internal::Env; - - #[test] - fn homedir_profile_only_windows() { - // windows specific variables should only be considered when the platform is windows - let env = Env::from_slice(&[("USERPROFILE", "C:\\Users\\name")]); - assert_eq!( - home_dir(&env, Os::Windows), - Some("C:\\Users\\name".to_string()) - ); - assert_eq!(home_dir(&env, Os::NotWindows), None); - } -} diff --git a/aws/rust-runtime/aws-config/src/lib.rs b/aws/rust-runtime/aws-config/src/lib.rs index 71f8c77721..e624c3d929 100644 --- a/aws/rust-runtime/aws-config/src/lib.rs +++ b/aws/rust-runtime/aws-config/src/lib.rs @@ -118,12 +118,10 @@ pub mod identity { #[allow(dead_code)] const PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); -#[cfg(test)] -mod test_case; - -mod fs_util; mod http_credential_provider; mod json_credentials; +#[cfg(test)] +mod test_case; pub mod credential_process; pub mod default_provider; diff --git a/aws/rust-runtime/aws-config/src/sso/cache.rs b/aws/rust-runtime/aws-config/src/sso/cache.rs index 5d78798f87..03903776e6 100644 --- a/aws/rust-runtime/aws-config/src/sso/cache.rs +++ b/aws/rust-runtime/aws-config/src/sso/cache.rs @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -use crate::fs_util::{home_dir, Os}; +use aws_runtime::fs_util::{home_dir, Os}; use aws_smithy_json::deserialize::token::skip_value; use aws_smithy_json::deserialize::Token; use aws_smithy_json::deserialize::{json_token_iter, EscapeError}; diff --git a/aws/rust-runtime/aws-runtime/src/fs_util.rs b/aws/rust-runtime/aws-runtime/src/fs_util.rs index 77dbcb733a..7fe4f4ccd9 100644 --- a/aws/rust-runtime/aws-runtime/src/fs_util.rs +++ b/aws/rust-runtime/aws-runtime/src/fs_util.rs @@ -5,27 +5,28 @@ use aws_types::os_shim_internal; +/// An operating system, like Windows or Linux #[derive(Debug, Copy, Clone, Eq, PartialEq)] #[non_exhaustive] -pub(crate) enum Os { +pub enum Os { /// A Windows-based operating system Windows, - /// Any other operating system - NotWindows, + /// Any Unix-based operating system + Unix, } impl Os { /// Returns the current operating system - pub(crate) fn real() -> Self { + pub fn real() -> Self { match std::env::consts::OS { "windows" => Os::Windows, - _ => Os::NotWindows, + _ => Os::Unix, } } } /// Resolve a home directory given a set of environment variables -pub(crate) fn home_dir(env_var: &os_shim_internal::Env, os: Os) -> Option { +pub fn home_dir(env_var: &os_shim_internal::Env, os: Os) -> Option { if let Ok(home) = env_var.get("HOME") { tracing::debug!(src = "HOME", "loaded home directory"); return Some(home); @@ -61,6 +62,6 @@ mod test { home_dir(&env, Os::Windows), Some("C:\\Users\\name".to_string()) ); - assert_eq!(home_dir(&env, Os::NotWindows), None); + assert_eq!(home_dir(&env, Os::Unix), None); } } From d98aa86346aefa8c7e535bdbba2e901908c51ba4 Mon Sep 17 00:00:00 2001 From: Zelda Hessler Date: Tue, 19 Mar 2024 16:11:43 -0500 Subject: [PATCH 08/23] add support for s3 express env config --- .../customize/s3/S3ExpressDecorator.kt | 87 ++++++++++++++++++- 1 file changed, 86 insertions(+), 1 deletion(-) diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/customize/s3/S3ExpressDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/customize/s3/S3ExpressDecorator.kt index 4f8d545aa9..ba83d23a9e 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/customize/s3/S3ExpressDecorator.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/customize/s3/S3ExpressDecorator.kt @@ -28,11 +28,14 @@ import software.amazon.smithy.rust.codegen.core.rustlang.writable import software.amazon.smithy.rust.codegen.core.smithy.RuntimeConfig import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType.Companion.preludeScope +import software.amazon.smithy.rust.codegen.core.smithy.customize.AdHocCustomization +import software.amazon.smithy.rust.codegen.core.smithy.customize.adhocCustomization import software.amazon.smithy.rust.codegen.core.util.dq import software.amazon.smithy.rust.codegen.core.util.getTrait import software.amazon.smithy.rustsdk.AwsCargoDependency import software.amazon.smithy.rustsdk.AwsRuntimeType import software.amazon.smithy.rustsdk.InlineAwsDependency +import software.amazon.smithy.rustsdk.SdkConfigSection class S3ExpressDecorator : ClientCodegenDecorator { override val name: String = "S3ExpressDecorator" @@ -66,7 +69,54 @@ class S3ExpressDecorator : ClientCodegenDecorator { override fun configCustomizations( codegenContext: ClientCodegenContext, baseCustomizations: List, - ): List = baseCustomizations + listOf(S3ExpressIdentityProviderConfig(codegenContext)) + ): List { +// val runtimeConfig = codegenContext.runtimeConfig +// val boolSymbol = RuntimeType.Bool.toSymbol() + + return baseCustomizations + + listOf( + S3ExpressIdentityProviderConfig(codegenContext), +// standardConfigParam( +// ConfigParam.Builder() +// .name("disable_express_session_auth") +// .type(boolSymbol) +// .newtype( +// configParamNewtype( +// "DisableExpressSessionAuth", +// boolSymbol, +// runtimeConfig, +// ), +// ) +// .setterDocs(writable { docs("A") }) +// .getterDocs(writable { docs("B") }) +// .build(), +// ), +// standardConfigParam( +// ConfigParam.Builder() +// .name("disable_multi_region_access_points") +// .type(boolSymbol) +// .newtype( +// configParamNewtype( +// "DisableMultiRegionAccessPoints", +// boolSymbol, +// runtimeConfig, +// ), +// ) +// .setterDocs(writable { docs("C") }) +// .getterDocs(writable { docs("D") }) +// .build(), +// ), +// standardConfigParam( +// ConfigParam.Builder() +// .name("use_arn_region") +// .type(boolSymbol) +// .newtype(configParamNewtype("UseArnRegion", boolSymbol, runtimeConfig)) +// .setterDocs(writable { docs("E") }) +// .getterDocs(writable { docs("F") }) +// .build(), +// ), + ) + } override fun operationCustomizations( codegenContext: ClientCodegenContext, @@ -77,6 +127,41 @@ class S3ExpressDecorator : ClientCodegenDecorator { S3ExpressRequestChecksumCustomization( codegenContext, operation, ) + + override fun extraSections(codegenContext: ClientCodegenContext): List { + return listOf( + adhocCustomization { section -> + rust( + """ + ${section.serviceConfigBuilder}.set_disable_s3_express_session_auth( + ${section.sdkConfig} + .service_config() + .and_then(|conf| { + let str_config = conf.load_config(service_config_key("AWS_S3_DISABLE_EXPRESS_SESSION_AUTH", "s3_disable_express_session_auth")); + str_config.and_then(|it| it.parse::().ok()) + }), + ); + ${section.serviceConfigBuilder}.set_disable_multi_region_access_points( + ${section.sdkConfig} + .service_config() + .and_then(|conf| { + let str_config = conf.load_config(service_config_key("AWS_S3_DISABLE_MULTIREGION_ACCESS_POINTS", "s3_disable_multi_region_access_points")); + str_config.and_then(|it| it.parse::().ok()) + }), + ); + ${section.serviceConfigBuilder}.set_use_arn_region( + ${section.sdkConfig} + .service_config() + .and_then(|conf| { + let str_config = conf.load_config(service_config_key("AWS_S3_USE_ARN_REGION", "s3_use_arn_region")); + str_config.and_then(|it| it.parse::().ok()) + }), + ); + """, + ) + }, + ) + } } private class S3ExpressServiceRuntimePluginCustomization(codegenContext: ClientCodegenContext) : From 0e4e3bea30f4fb4acfbd7e5dd6b8c87e3eb9287a Mon Sep 17 00:00:00 2001 From: Zelda Hessler Date: Wed, 20 Mar 2024 11:38:10 -0500 Subject: [PATCH 09/23] remove comment --- .../customize/s3/S3ExpressDecorator.kt | 49 +------------------ 1 file changed, 1 insertion(+), 48 deletions(-) diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/customize/s3/S3ExpressDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/customize/s3/S3ExpressDecorator.kt index ba83d23a9e..68c234d30e 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/customize/s3/S3ExpressDecorator.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/customize/s3/S3ExpressDecorator.kt @@ -69,54 +69,7 @@ class S3ExpressDecorator : ClientCodegenDecorator { override fun configCustomizations( codegenContext: ClientCodegenContext, baseCustomizations: List, - ): List { -// val runtimeConfig = codegenContext.runtimeConfig -// val boolSymbol = RuntimeType.Bool.toSymbol() - - return baseCustomizations + - listOf( - S3ExpressIdentityProviderConfig(codegenContext), -// standardConfigParam( -// ConfigParam.Builder() -// .name("disable_express_session_auth") -// .type(boolSymbol) -// .newtype( -// configParamNewtype( -// "DisableExpressSessionAuth", -// boolSymbol, -// runtimeConfig, -// ), -// ) -// .setterDocs(writable { docs("A") }) -// .getterDocs(writable { docs("B") }) -// .build(), -// ), -// standardConfigParam( -// ConfigParam.Builder() -// .name("disable_multi_region_access_points") -// .type(boolSymbol) -// .newtype( -// configParamNewtype( -// "DisableMultiRegionAccessPoints", -// boolSymbol, -// runtimeConfig, -// ), -// ) -// .setterDocs(writable { docs("C") }) -// .getterDocs(writable { docs("D") }) -// .build(), -// ), -// standardConfigParam( -// ConfigParam.Builder() -// .name("use_arn_region") -// .type(boolSymbol) -// .newtype(configParamNewtype("UseArnRegion", boolSymbol, runtimeConfig)) -// .setterDocs(writable { docs("E") }) -// .getterDocs(writable { docs("F") }) -// .build(), -// ), - ) - } + ): List = baseCustomizations + listOf(S3ExpressIdentityProviderConfig(codegenContext)) override fun operationCustomizations( codegenContext: ClientCodegenContext, From 8fdcb1168d49854def7d1dc546ea9f7984e26b19 Mon Sep 17 00:00:00 2001 From: Zelda Hessler Date: Mon, 25 Mar 2024 07:36:30 -0700 Subject: [PATCH 10/23] Update aws/rust-runtime/aws-types/src/sdk_config.rs Co-authored-by: John DiSanti --- aws/rust-runtime/aws-types/src/sdk_config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws/rust-runtime/aws-types/src/sdk_config.rs b/aws/rust-runtime/aws-types/src/sdk_config.rs index 196d3f1228..19f1a89a40 100644 --- a/aws/rust-runtime/aws-types/src/sdk_config.rs +++ b/aws/rust-runtime/aws-types/src/sdk_config.rs @@ -70,7 +70,7 @@ pub struct SdkConfig { use_fips: Option, use_dual_stack: Option, behavior_version: Option, - service_config: Option>>, + service_config: Option>, } /// Builder for AWS Shared Configuration From 5bc1fa19e576d9b0bbd13b67fe84b3b0b359ae1d Mon Sep 17 00:00:00 2001 From: Zelda Hessler Date: Mon, 25 Mar 2024 07:36:39 -0700 Subject: [PATCH 11/23] Update aws/rust-runtime/aws-runtime/src/profile/section.rs Co-authored-by: John DiSanti --- aws/rust-runtime/aws-runtime/src/profile/section.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws/rust-runtime/aws-runtime/src/profile/section.rs b/aws/rust-runtime/aws-runtime/src/profile/section.rs index 38f6b122b1..705a6b59ba 100644 --- a/aws/rust-runtime/aws-runtime/src/profile/section.rs +++ b/aws/rust-runtime/aws-runtime/src/profile/section.rs @@ -150,7 +150,7 @@ impl Properties { } #[cfg(test)] - pub fn new_from_slice(slice: &[(PropertiesKey, PropertyValue)]) -> Self { + pub(crate) fn new_from_slice(slice: &[(PropertiesKey, PropertyValue)]) -> Self { let mut properties = Self::new(); for (key, value) in slice { properties.insert(key.clone(), value.clone()); From 28102c3d9c49c33779c617e3014666c6d400e28e Mon Sep 17 00:00:00 2001 From: Zelda Hessler Date: Mon, 25 Mar 2024 14:43:43 -0500 Subject: [PATCH 12/23] big rename of env config types --- .../src/default_provider/app_name.rs | 10 +- .../src/default_provider/endpoint_url.rs | 10 +- .../ignore_configured_endpoint_urls.rs | 10 +- .../src/default_provider/use_dual_stack.rs | 17 +- .../src/default_provider/use_fips.rs | 10 +- .../aws-config/src/env_service_config.rs | 6 +- aws/rust-runtime/aws-config/src/lib.rs | 45 ++++- aws/rust-runtime/aws-config/src/profile.rs | 10 +- .../aws-config/src/profile/credentials.rs | 9 +- .../src/profile/credentials/repr.rs | 3 +- .../aws-config/src/profile/parser.rs | 13 +- .../aws-config/src/profile/profile_file.rs | 21 +- .../aws-config/src/profile/region.rs | 7 +- .../aws-config/src/profile/token.rs | 7 +- .../aws-config/src/provider_config.rs | 12 +- .../aws-runtime/src/env_config.rs | 52 +++-- .../src/{profile => env_config}/error.rs | 28 +-- .../profile_file.rs => env_config/file.rs} | 116 +++++------ .../src/{profile => env_config}/normalize.rs | 37 ++-- .../src/{profile => env_config}/parse.rs | 40 ++-- .../section.rs => env_config/property.rs} | 137 ------------- .../profile_set.rs => env_config/section.rs} | 189 +++++++++++++++--- .../src/{profile => env_config}/source.rs | 60 +++--- aws/rust-runtime/aws-runtime/src/lib.rs | 6 +- aws/rust-runtime/aws-runtime/src/profile.rs | 35 ---- aws/rust-runtime/aws-types/src/sdk_config.rs | 10 +- .../dynamodb/tests/shared-config.rs | 6 +- 27 files changed, 485 insertions(+), 421 deletions(-) rename aws/rust-runtime/aws-runtime/src/{profile => env_config}/error.rs (59%) rename aws/rust-runtime/aws-runtime/src/{profile/profile_file.rs => env_config/file.rs} (65%) rename aws/rust-runtime/aws-runtime/src/{profile => env_config}/normalize.rs (92%) rename aws/rust-runtime/aws-runtime/src/{profile => env_config}/parse.rs (92%) rename aws/rust-runtime/aws-runtime/src/{profile/section.rs => env_config/property.rs} (60%) rename aws/rust-runtime/aws-runtime/src/{profile/profile_set.rs => env_config/section.rs} (69%) rename aws/rust-runtime/aws-runtime/src/{profile => env_config}/source.rs (90%) delete mode 100644 aws/rust-runtime/aws-runtime/src/profile.rs diff --git a/aws/rust-runtime/aws-config/src/default_provider/app_name.rs b/aws/rust-runtime/aws-config/src/default_provider/app_name.rs index f0bf8485ea..7a411e1a73 100644 --- a/aws/rust-runtime/aws-config/src/default_provider/app_name.rs +++ b/aws/rust-runtime/aws-config/src/default_provider/app_name.rs @@ -89,9 +89,10 @@ impl Builder { #[cfg(test)] mod tests { use super::*; + #[allow(deprecated)] + use crate::profile::profile_file::{ProfileFileKind, ProfileFiles}; use crate::provider_config::ProviderConfig; use crate::test_case::{no_traffic_client, InstantSleep}; - use aws_runtime::profile::profile_file::{ProfileFileKind, ProfileFiles}; use aws_smithy_runtime_api::client::behavior_version::BehaviorVersion; use aws_types::os_shim_internal::{Env, Fs}; @@ -125,8 +126,13 @@ mod tests { .http_client(no_traffic_client()) .profile_name("custom") .profile_files( + #[allow(deprecated)] ProfileFiles::builder() - .with_file(ProfileFileKind::Config, "test_config") + .with_file( + #[allow(deprecated)] + ProfileFileKind::Config, + "test_config", + ) .build(), ) .load() diff --git a/aws/rust-runtime/aws-config/src/default_provider/endpoint_url.rs b/aws/rust-runtime/aws-config/src/default_provider/endpoint_url.rs index 04a9390547..a34ee29479 100644 --- a/aws/rust-runtime/aws-config/src/default_provider/endpoint_url.rs +++ b/aws/rust-runtime/aws-config/src/default_provider/endpoint_url.rs @@ -41,8 +41,9 @@ pub async fn endpoint_url_provider(provider_config: &ProviderConfig) -> Option #[cfg(test)] mod test { use crate::default_provider::use_dual_stack::use_dual_stack_provider; + #[allow(deprecated)] + use crate::profile::profile_file::{ProfileFileKind, ProfileFiles}; use crate::provider_config::ProviderConfig; - use aws_runtime::profile::profile_file::{ProfileFileKind, ProfileFiles}; use aws_types::os_shim_internal::{Env, Fs}; use tracing_test::traced_test; @@ -57,8 +58,13 @@ mod test { .with_env(Env::from_slice(&[("AWS_USE_DUALSTACK_ENDPOINT", "TRUE")])) .with_profile_config( Some( + #[allow(deprecated)] ProfileFiles::builder() - .with_file(ProfileFileKind::Config, "conf") + .with_file( + #[allow(deprecated)] + ProfileFileKind::Config, + "conf", + ) .build(), ), None, @@ -76,8 +82,13 @@ mod test { let conf = ProviderConfig::empty() .with_profile_config( Some( + #[allow(deprecated)] ProfileFiles::builder() - .with_file(ProfileFileKind::Config, "conf") + .with_file( + #[allow(deprecated)] + ProfileFileKind::Config, + "conf", + ) .build(), ), None, diff --git a/aws/rust-runtime/aws-config/src/default_provider/use_fips.rs b/aws/rust-runtime/aws-config/src/default_provider/use_fips.rs index a546f18145..92932f4610 100644 --- a/aws/rust-runtime/aws-config/src/default_provider/use_fips.rs +++ b/aws/rust-runtime/aws-config/src/default_provider/use_fips.rs @@ -40,8 +40,9 @@ pub async fn use_fips_provider(provider_config: &ProviderConfig) -> Option #[cfg(test)] mod test { use crate::default_provider::use_fips::use_fips_provider; + #[allow(deprecated)] + use crate::profile::profile_file::{ProfileFileKind, ProfileFiles}; use crate::provider_config::ProviderConfig; - use aws_runtime::profile::profile_file::{ProfileFileKind, ProfileFiles}; use aws_types::os_shim_internal::{Env, Fs}; use tracing_test::traced_test; @@ -64,8 +65,13 @@ mod test { .with_env(Env::from_slice(&[("AWS_USE_FIPS_ENDPOINT", "TRUE")])) .with_profile_config( Some( + #[allow(deprecated)] ProfileFiles::builder() - .with_file(ProfileFileKind::Config, "conf") + .with_file( + #[allow(deprecated)] + ProfileFileKind::Config, + "conf", + ) .build(), ), None, diff --git a/aws/rust-runtime/aws-config/src/env_service_config.rs b/aws/rust-runtime/aws-config/src/env_service_config.rs index 9a18039eeb..00c67a1275 100644 --- a/aws/rust-runtime/aws-config/src/env_service_config.rs +++ b/aws/rust-runtime/aws-config/src/env_service_config.rs @@ -3,15 +3,15 @@ * SPDX-License-Identifier: Apache-2.0 */ +use aws_runtime::env_config::section::EnvConfigSections; use aws_runtime::env_config::EnvConfigValue; -use aws_runtime::profile::profile_set::ProfileSet; use aws_types::os_shim_internal::Env; use aws_types::service_config::{LoadServiceConfig, ServiceConfigKey}; #[derive(Debug)] pub(crate) struct EnvServiceConfig { pub(crate) env: Env, - pub(crate) profiles: ProfileSet, + pub(crate) env_config_sections: EnvConfigSections, } impl LoadServiceConfig for EnvServiceConfig { @@ -20,7 +20,7 @@ impl LoadServiceConfig for EnvServiceConfig { .env(key.env()) .profile(key.profile()) .service_id(key.service_id()) - .load(&self.env, Some(&self.profiles))?; + .load(&self.env, Some(&self.env_config_sections))?; Some(value.to_string()) } diff --git a/aws/rust-runtime/aws-config/src/lib.rs b/aws/rust-runtime/aws-config/src/lib.rs index e624c3d929..41617eaa66 100644 --- a/aws/rust-runtime/aws-config/src/lib.rs +++ b/aws/rust-runtime/aws-config/src/lib.rs @@ -212,7 +212,6 @@ mod loader { ProvideCredentials, SharedCredentialsProvider, }; use aws_credential_types::Credentials; - use aws_runtime::profile::profile_file::ProfileFiles; use aws_smithy_async::rt::sleep::{default_async_sleep, AsyncSleep, SharedAsyncSleep}; use aws_smithy_async::time::{SharedTimeSource, TimeSource}; use aws_smithy_runtime_api::client::behavior_version::BehaviorVersion; @@ -233,6 +232,8 @@ mod loader { retry_config, timeout_config, use_dual_stack, use_fips, }; use crate::meta::region::ProvideRegion; + #[allow(deprecated)] + use crate::profile::profile_file::ProfileFiles; use crate::provider_config::ProviderConfig; #[derive(Default, Debug)] @@ -266,6 +267,7 @@ mod loader { provider_config: Option, http_client: Option, profile_name_override: Option, + #[allow(deprecated)] profile_files_override: Option, use_fips: Option, use_dual_stack: Option, @@ -570,6 +572,7 @@ mod loader { /// .load() /// .await; /// # } + #[allow(deprecated)] pub fn profile_files(mut self, profile_files: ProfileFiles) -> Self { self.profile_files_override = Some(profile_files); self @@ -809,7 +812,7 @@ mod loader { let profiles = conf.profile().await; let service_config = EnvServiceConfig { env: conf.env(), - profiles: profiles.cloned().unwrap_or_default(), + env_config_sections: profiles.cloned().unwrap_or_default(), }; let mut builder = SdkConfig::builder() .region(region) @@ -871,11 +874,12 @@ mod loader { #[cfg(test)] mod test { + #[allow(deprecated)] + use crate::profile::profile_file::{ProfileFileKind, ProfileFiles}; use crate::test_case::{no_traffic_client, InstantSleep}; use crate::BehaviorVersion; use crate::{defaults, ConfigLoader}; use aws_credential_types::provider::ProvideCredentials; - use aws_runtime::profile::profile_file::{ProfileFileKind, ProfileFiles}; use aws_smithy_async::rt::sleep::TokioSleep; use aws_smithy_runtime::client::http::test_util::{infallible_client_fn, NeverClient}; use aws_types::app_name::AppName; @@ -902,8 +906,13 @@ mod loader { .http_client(NeverClient::new()) .profile_name("custom") .profile_files( + #[allow(deprecated)] ProfileFiles::builder() - .with_file(ProfileFileKind::Config, "test_config") + .with_file( + #[allow(deprecated)] + ProfileFileKind::Config, + "test_config", + ) .build(), ) .load() @@ -1021,8 +1030,13 @@ mod loader { .env(env) .profile_name("custom") .profile_files( + #[allow(deprecated)] ProfileFiles::builder() - .with_file(ProfileFileKind::Config, "test_config") + .with_file( + #[allow(deprecated)] + ProfileFileKind::Config, + "test_config", + ) .build(), ) .load() @@ -1034,8 +1048,13 @@ mod loader { .fs(fs) .profile_name("custom") .profile_files( + #[allow(deprecated)] ProfileFiles::builder() - .with_file(ProfileFileKind::Config, "test_config") + .with_file( + #[allow(deprecated)] + ProfileFileKind::Config, + "test_config", + ) .build(), ) .load() @@ -1057,8 +1076,13 @@ mod loader { .env(env.clone()) .profile_name("custom") .profile_files( + #[allow(deprecated)] ProfileFiles::builder() - .with_file(ProfileFileKind::Config, "test_config") + .with_file( + #[allow(deprecated)] + ProfileFileKind::Config, + "test_config", + ) .build(), ) .load() @@ -1085,8 +1109,13 @@ mod loader { .endpoint_url("http://localhost") .profile_name("custom") .profile_files( + #[allow(deprecated)] ProfileFiles::builder() - .with_file(ProfileFileKind::Config, "test_config") + .with_file( + #[allow(deprecated)] + ProfileFileKind::Config, + "test_config", + ) .build(), ) .load() diff --git a/aws/rust-runtime/aws-config/src/profile.rs b/aws/rust-runtime/aws-config/src/profile.rs index 09792ae6f4..5b77b99f9b 100644 --- a/aws/rust-runtime/aws-config/src/profile.rs +++ b/aws/rust-runtime/aws-config/src/profile.rs @@ -21,15 +21,17 @@ pub mod token; pub use token::ProfileFileTokenProvider; #[doc(inline)] -pub use aws_runtime::profile::parse::ProfileParseError; +pub use aws_runtime::env_config::error::EnvConfigFileLoadError as ProfileFileLoadError; #[doc(inline)] -pub use aws_runtime::profile::profile_set::ProfileSet; +pub use aws_runtime::env_config::parse::EnvConfigParseError as ProfileParseError; #[doc(inline)] -pub use aws_runtime::profile::section::{Profile, Property}; +pub use aws_runtime::env_config::property::Property; +#[doc(inline)] +pub use aws_runtime::env_config::section::{EnvConfigSections as ProfileSet, Profile}; #[doc(inline)] pub use credentials::ProfileFileCredentialsProvider; #[doc(inline)] -pub use parser::{load, ProfileFileLoadError}; +pub use parser::load; #[doc(inline)] pub use region::ProfileFileRegionProvider; diff --git a/aws/rust-runtime/aws-config/src/profile/credentials.rs b/aws/rust-runtime/aws-config/src/profile/credentials.rs index f54baa325f..d7dc12e329 100644 --- a/aws/rust-runtime/aws-config/src/profile/credentials.rs +++ b/aws/rust-runtime/aws-config/src/profile/credentials.rs @@ -23,14 +23,15 @@ //! through a series of providers. use crate::profile::cell::ErrorTakingOnceCell; +#[allow(deprecated)] +use crate::profile::profile_file::ProfileFiles; +use crate::profile::Profile; +use crate::profile::ProfileFileLoadError; use crate::provider_config::ProviderConfig; use aws_credential_types::{ provider::{self, error::CredentialsError, future, ProvideCredentials}, Credentials, }; -use aws_runtime::profile::error::ProfileFileLoadError; -use aws_runtime::profile::profile_file::ProfileFiles; -use aws_runtime::profile::section::Profile; use aws_smithy_types::error::display::DisplayErrorContext; use aws_types::SdkConfig; use std::borrow::Cow; @@ -377,6 +378,7 @@ impl Display for ProfileFileError { pub struct Builder { provider_config: Option, profile_override: Option, + #[allow(deprecated)] profile_files: Option, custom_providers: HashMap, Arc>, } @@ -444,6 +446,7 @@ impl Builder { } /// Set the profile file that should be used by the [`ProfileFileCredentialsProvider`] + #[allow(deprecated)] pub fn profile_files(mut self, profile_files: ProfileFiles) -> Self { self.profile_files = Some(profile_files); self diff --git a/aws/rust-runtime/aws-config/src/profile/credentials/repr.rs b/aws/rust-runtime/aws-config/src/profile/credentials/repr.rs index 8c81aa79a2..2d14ab76f6 100644 --- a/aws/rust-runtime/aws-config/src/profile/credentials/repr.rs +++ b/aws/rust-runtime/aws-config/src/profile/credentials/repr.rs @@ -13,10 +13,9 @@ //! multiple actions into the same profile). use crate::profile::credentials::ProfileFileError; +use crate::profile::{Profile, ProfileSet}; use crate::sensitive_command::CommandWithSensitiveArgs; use aws_credential_types::Credentials; -use aws_runtime::profile::profile_set::ProfileSet; -use aws_runtime::profile::section::Profile; /// Chain of Profile Providers /// diff --git a/aws/rust-runtime/aws-config/src/profile/parser.rs b/aws/rust-runtime/aws-config/src/profile/parser.rs index 23ab4c20c6..a2b4d4371a 100644 --- a/aws/rust-runtime/aws-config/src/profile/parser.rs +++ b/aws/rust-runtime/aws-config/src/profile/parser.rs @@ -5,16 +5,15 @@ //! Code for parsing AWS profile config -use aws_runtime::profile::profile_file::ProfileFiles; -use aws_runtime::profile::source; +use aws_runtime::env_config::file::EnvConfigFiles as ProfileFiles; +use aws_runtime::env_config::source; use aws_types::os_shim_internal::{Env, Fs}; use std::borrow::Cow; -pub use aws_runtime::profile::error::ProfileFileLoadError; -pub use aws_runtime::profile::parse::ProfileParseError; -pub use aws_runtime::profile::profile_set::ProfileSet; -pub use aws_runtime::profile::section::Profile; -pub use aws_runtime::profile::section::Property; +pub use aws_runtime::env_config::error::EnvConfigFileLoadError as ProfileFileLoadError; +pub use aws_runtime::env_config::parse::EnvConfigParseError as ProfileParseError; +pub use aws_runtime::env_config::property::Property; +pub use aws_runtime::env_config::section::{EnvConfigSections as ProfileSet, Profile}; /// Read & parse AWS config files /// diff --git a/aws/rust-runtime/aws-config/src/profile/profile_file.rs b/aws/rust-runtime/aws-config/src/profile/profile_file.rs index 3e745934ce..14af175cb2 100644 --- a/aws/rust-runtime/aws-config/src/profile/profile_file.rs +++ b/aws/rust-runtime/aws-config/src/profile/profile_file.rs @@ -5,23 +5,20 @@ //! Re-exports for types since moved to the aws-runtime crate. -/// Use aws_runtime::profile::profile_file::ProfileFiles instead. +/// Use aws_runtime::env_config::file::EnvConfigFiles instead. #[deprecated( since = "1.1.10", - note = "Use aws_runtime::profile::profile_file::ProfileFiles instead." + note = "Use aws_runtime::env_config::file::EnvConfigFiles instead." )] -pub type ProfileFiles = aws_runtime::profile::profile_file::ProfileFiles; +pub type ProfileFiles = aws_runtime::env_config::file::EnvConfigFiles; -/// Use aws_runtime::profile::profile_file::Builder instead. -#[deprecated( - since = "1.1.10", - note = "Use aws_runtime::profile::profile_file::Builder." -)] -pub type Builder = aws_runtime::profile::profile_file::Builder; +/// Use aws_runtime::env_config::file::Builder instead. +#[deprecated(since = "1.1.10", note = "Use aws_runtime::env_config::file::Builder.")] +pub type Builder = aws_runtime::env_config::file::Builder; -/// Use aws_runtime::profile::profile_file::ProfileFileKind instead. +/// Use aws_runtime::env_config::file::EnvConfigFileKind instead. #[deprecated( since = "1.1.10", - note = "Use aws_runtime::profile::profile_file::ProfileFileKind." + note = "Use aws_runtime::env_config::file::EnvConfigFileKind." )] -pub type ProfileFileKind = aws_runtime::profile::profile_file::ProfileFileKind; +pub type ProfileFileKind = aws_runtime::env_config::file::EnvConfigFileKind; diff --git a/aws/rust-runtime/aws-config/src/profile/region.rs b/aws/rust-runtime/aws-config/src/profile/region.rs index e69da8da65..34a3fffaca 100644 --- a/aws/rust-runtime/aws-config/src/profile/region.rs +++ b/aws/rust-runtime/aws-config/src/profile/region.rs @@ -6,9 +6,10 @@ //! Load a region from an AWS profile use crate::meta::region::{future, ProvideRegion}; +#[allow(deprecated)] +use crate::profile::profile_file::ProfileFiles; +use crate::profile::ProfileSet; use crate::provider_config::ProviderConfig; -use aws_runtime::profile::profile_file::ProfileFiles; -use aws_runtime::profile::profile_set::ProfileSet; use aws_types::region::Region; /// Load a region from a profile file @@ -45,6 +46,7 @@ pub struct ProfileFileRegionProvider { pub struct Builder { config: Option, profile_override: Option, + #[allow(deprecated)] profile_files: Option, } @@ -62,6 +64,7 @@ impl Builder { } /// Set the profile file that should be used by the [`ProfileFileRegionProvider`] + #[allow(deprecated)] pub fn profile_files(mut self, profile_files: ProfileFiles) -> Self { self.profile_files = Some(profile_files); self diff --git a/aws/rust-runtime/aws-config/src/profile/token.rs b/aws/rust-runtime/aws-config/src/profile/token.rs index b53810a2f2..2e33307988 100644 --- a/aws/rust-runtime/aws-config/src/profile/token.rs +++ b/aws/rust-runtime/aws-config/src/profile/token.rs @@ -6,13 +6,14 @@ //! Profile File Based Token Providers use crate::profile::cell::ErrorTakingOnceCell; +#[allow(deprecated)] +use crate::profile::profile_file::ProfileFiles; +use crate::profile::ProfileSet; use crate::provider_config::ProviderConfig; use crate::sso::SsoTokenProvider; use aws_credential_types::provider::{ error::TokenError, future, token::ProvideToken, token::Result as TokenResult, }; -use aws_runtime::profile::profile_file::ProfileFiles; -use aws_runtime::profile::profile_set::ProfileSet; use aws_types::{region::Region, SdkConfig}; async fn load_profile_set(provider_config: &ProviderConfig) -> Result<&ProfileSet, TokenError> { @@ -123,6 +124,7 @@ impl ProvideToken for ProfileFileTokenProvider { pub struct Builder { provider_config: Option, profile_override: Option, + #[allow(deprecated)] profile_files: Option, } @@ -140,6 +142,7 @@ impl Builder { } /// Set the profile file that should be used by the [`ProfileFileTokenProvider`] + #[allow(deprecated)] pub fn profile_files(mut self, profile_files: ProfileFiles) -> Self { self.profile_files = Some(profile_files); self diff --git a/aws/rust-runtime/aws-config/src/provider_config.rs b/aws/rust-runtime/aws-config/src/provider_config.rs index ab9da98777..569af53fcb 100644 --- a/aws/rust-runtime/aws-config/src/provider_config.rs +++ b/aws/rust-runtime/aws-config/src/provider_config.rs @@ -6,9 +6,9 @@ //! Configuration Options for Credential Providers use crate::profile; -use aws_runtime::profile::error::ProfileFileLoadError; -use aws_runtime::profile::profile_file::ProfileFiles; -use aws_runtime::profile::profile_set::ProfileSet; +#[allow(deprecated)] +use crate::profile::profile_file::ProfileFiles; +use crate::profile::{ProfileFileLoadError, ProfileSet}; use aws_smithy_async::rt::sleep::{default_async_sleep, AsyncSleep, SharedAsyncSleep}; use aws_smithy_async::time::{SharedTimeSource, TimeSource}; use aws_smithy_runtime_api::client::http::HttpClient; @@ -45,6 +45,7 @@ pub struct ProviderConfig { /// An AWS profile created from `ProfileFiles` and a `profile_name` parsed_profile: Arc>>, /// A list of [std::path::Path]s to profile files + #[allow(deprecated)] profile_files: ProfileFiles, /// An override to use when constructing a `ProfileSet` profile_name_override: Option>, @@ -78,6 +79,7 @@ impl Default for ProviderConfig { use_fips: None, use_dual_stack: None, parsed_profile: Default::default(), + #[allow(deprecated)] profile_files: ProfileFiles::default(), profile_name_override: None, } @@ -98,6 +100,7 @@ impl ProviderConfig { let env = Env::from_slice(&[]); Self { parsed_profile: Default::default(), + #[allow(deprecated)] profile_files: ProfileFiles::default(), env, fs, @@ -150,6 +153,7 @@ impl ProviderConfig { use_fips: None, use_dual_stack: None, parsed_profile: Default::default(), + #[allow(deprecated)] profile_files: ProfileFiles::default(), profile_name_override: None, } @@ -162,6 +166,7 @@ impl ProviderConfig { ) -> Self { Self { parsed_profile: Default::default(), + #[allow(deprecated)] profile_files: ProfileFiles::default(), env: Env::default(), fs: Fs::default(), @@ -294,6 +299,7 @@ impl ProviderConfig { } /// Override the profile file paths (`~/.aws/config` by default) and name (`default` by default) + #[allow(deprecated)] pub(crate) fn with_profile_config( self, profile_files: Option, diff --git a/aws/rust-runtime/aws-runtime/src/env_config.rs b/aws/rust-runtime/aws-runtime/src/env_config.rs index 38c06f15b1..52c2ff46cc 100644 --- a/aws/rust-runtime/aws-runtime/src/env_config.rs +++ b/aws/rust-runtime/aws-runtime/src/env_config.rs @@ -3,14 +3,38 @@ * SPDX-License-Identifier: Apache-2.0 */ +use crate::env_config::property::PropertiesKey; +use crate::env_config::section::EnvConfigSections; +use aws_types::os_shim_internal::Env; +use aws_types::service_config::ServiceConfigKey; use std::borrow::Cow; use std::error::Error; use std::fmt; -use aws_types::os_shim_internal::Env; - -use crate::profile::profile_set::ProfileSet; -use crate::profile::section::PropertiesKey; +pub mod error; +pub mod file; +mod normalize; +pub mod parse; +pub mod property; +pub mod section; +pub mod source; + +/// Given a key, access to the environment, and a validator, return a config value if one was set. +pub async fn get_service_env_config<'a, T, E>( + key: ServiceConfigKey<'a>, + env: &'a Env, + shared_config_sections: Option<&'a EnvConfigSections>, + validator: impl Fn(&str) -> Result, +) -> Result, EnvConfigError> +where + E: Error + Send + Sync + 'static, +{ + EnvConfigValue::default() + .env(key.env()) + .profile(key.profile()) + .service_id(key.service_id()) + .validate(env, shared_config_sections, validator) +} #[derive(Debug)] enum Location<'a> { @@ -159,7 +183,7 @@ impl<'a> EnvConfigValue<'a> { pub fn validate( self, env: &Env, - profiles: Option<&ProfileSet>, + profiles: Option<&EnvConfigSections>, validator: impl Fn(&str) -> Result, ) -> Result, EnvConfigError> { let value = self.load(env, profiles); @@ -177,7 +201,7 @@ impl<'a> EnvConfigValue<'a> { pub fn load( &self, env: &'a Env, - profiles: Option<&'a ProfileSet>, + profiles: Option<&'a EnvConfigSections>, ) -> Option<(Cow<'a, str>, EnvConfigSource<'a>)> { let env_value = self.environment_variable.as_ref().and_then(|env_var| { // Check for a service-specific env var first @@ -243,7 +267,7 @@ fn get_service_config_from_env<'a>( const SERVICES: &str = "services"; fn get_service_config_from_profile<'a>( - profile: &ProfileSet, + profile: &EnvConfigSections, service_id: Option>, profile_key: Cow<'a, str>, ) -> Option<(Cow<'a, str>, EnvConfigSource<'a>)> { @@ -274,15 +298,13 @@ fn format_service_id_for_profile(service_id: impl AsRef) -> String { #[cfg(test)] mod test { + use crate::env_config::property::{Properties, PropertiesKey}; + use crate::env_config::section::EnvConfigSections; + use aws_types::os_shim_internal::Env; use std::borrow::Cow; use std::collections::HashMap; use std::num::ParseIntError; - use aws_types::os_shim_internal::Env; - - use crate::profile::profile_set::ProfileSet; - use crate::profile::section::{Properties, PropertiesKey}; - use super::EnvConfigValue; fn validate_some_key(s: &str) -> Result { @@ -315,7 +337,7 @@ mod test { ("AWS_SOME_KEY_SERVICE", "2"), ("AWS_SOME_KEY_ANOTHER_SERVICE", "3"), ]); - let profiles = ProfileSet::new( + let profiles = EnvConfigSections::new( HashMap::from([( "default".to_owned(), HashMap::from([ @@ -389,7 +411,7 @@ mod test { ("AWS_SOME_KEY_S3", "2"), ]); - let profiles = ProfileSet::new( + let profiles = EnvConfigSections::new( HashMap::from([( "default".to_owned(), HashMap::from([ @@ -443,7 +465,7 @@ mod test { ("AWS_SOME_KEY_EC2", "3"), ]); - let profiles = ProfileSet::new( + let profiles = EnvConfigSections::new( HashMap::from([( "default".to_owned(), HashMap::from([ diff --git a/aws/rust-runtime/aws-runtime/src/profile/error.rs b/aws/rust-runtime/aws-runtime/src/env_config/error.rs similarity index 59% rename from aws/rust-runtime/aws-runtime/src/profile/error.rs rename to aws/rust-runtime/aws-runtime/src/env_config/error.rs index 34fab4aee7..d187132ca8 100644 --- a/aws/rust-runtime/aws-runtime/src/profile/error.rs +++ b/aws/rust-runtime/aws-runtime/src/env_config/error.rs @@ -5,7 +5,7 @@ //! Errors related to AWS profile config files -use crate::profile::parse::ProfileParseError; +use crate::env_config::parse::EnvConfigParseError; use std::error::Error; use std::fmt::{Display, Formatter}; use std::path::PathBuf; @@ -13,47 +13,47 @@ use std::sync::Arc; /// Failed to read or parse the profile file(s) #[derive(Debug, Clone)] -pub enum ProfileFileLoadError { +pub enum EnvConfigFileLoadError { /// The profile could not be parsed #[non_exhaustive] - ParseError(ProfileParseError), + ParseError(EnvConfigParseError), /// Attempt to read the AWS config file (`~/.aws/config` by default) failed with a filesystem error. #[non_exhaustive] - CouldNotReadFile(CouldNotReadProfileFile), + CouldNotReadFile(CouldNotReadConfigFile), } -impl Display for ProfileFileLoadError { +impl Display for EnvConfigFileLoadError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - ProfileFileLoadError::ParseError(_err) => { + EnvConfigFileLoadError::ParseError(_err) => { write!(f, "could not parse profile file") } - ProfileFileLoadError::CouldNotReadFile(err) => { + EnvConfigFileLoadError::CouldNotReadFile(err) => { write!(f, "could not read file `{}`", err.path.display()) } } } } -impl Error for ProfileFileLoadError { +impl Error for EnvConfigFileLoadError { fn source(&self) -> Option<&(dyn Error + 'static)> { match self { - ProfileFileLoadError::ParseError(err) => Some(err), - ProfileFileLoadError::CouldNotReadFile(details) => Some(&details.cause), + EnvConfigFileLoadError::ParseError(err) => Some(err), + EnvConfigFileLoadError::CouldNotReadFile(details) => Some(&details.cause), } } } -impl From for ProfileFileLoadError { - fn from(err: ProfileParseError) -> Self { - ProfileFileLoadError::ParseError(err) +impl From for EnvConfigFileLoadError { + fn from(err: EnvConfigParseError) -> Self { + EnvConfigFileLoadError::ParseError(err) } } /// An error encountered while reading the AWS config file #[derive(Debug, Clone)] -pub struct CouldNotReadProfileFile { +pub struct CouldNotReadConfigFile { pub(crate) path: PathBuf, pub(crate) cause: Arc, } diff --git a/aws/rust-runtime/aws-runtime/src/profile/profile_file.rs b/aws/rust-runtime/aws-runtime/src/env_config/file.rs similarity index 65% rename from aws/rust-runtime/aws-runtime/src/profile/profile_file.rs rename to aws/rust-runtime/aws-runtime/src/env_config/file.rs index 56eff14fa5..abce21075b 100644 --- a/aws/rust-runtime/aws-runtime/src/profile/profile_file.rs +++ b/aws/rust-runtime/aws-runtime/src/env_config/file.rs @@ -10,10 +10,10 @@ use std::path::PathBuf; /// Provides the ability to programmatically override the profile files that get loaded by the SDK. /// -/// The [`Default`] for `ProfileFiles` includes the default SDK config and credential files located in +/// The [`Default`] for `EnvConfigFiles` includes the default SDK config and credential files located in /// `~/.aws/config` and `~/.aws/credentials` respectively. /// -/// Any number of config and credential files may be added to the `ProfileFiles` file set, with the +/// Any number of config and credential files may be added to the `EnvConfigFiles` file set, with the /// only requirement being that there is at least one of them. Custom file locations that are added /// will produce errors if they don't exist, while the default config/credentials files paths are /// allowed to not exist even if they're included. @@ -21,12 +21,12 @@ use std::path::PathBuf; /// # Example: Using a custom profile file path /// /// ```no_run,ignore -/// use aws_runtime::profile::profile_file::{ProfileFiles, ProfileFileKind}; +/// use aws_runtime::env_config::file::{EnvConfigFiles, SharedConfigFileKind}; /// use std::sync::Arc; /// /// # async fn example() { -/// let profile_files = ProfileFiles::builder() -/// .with_file(ProfileFileKind::Credentials, "some/path/to/credentials-file") +/// let profile_files = EnvConfigFiles::builder() +/// .with_file(SharedConfigFileKind::Credentials, "some/path/to/credentials-file") /// .build(); /// let sdk_config = aws_config::from_env() /// .profile_files(profile_files) @@ -35,23 +35,23 @@ use std::path::PathBuf; /// # } /// ``` #[derive(Clone, Debug)] -pub struct ProfileFiles { - pub(crate) files: Vec, +pub struct EnvConfigFiles { + pub(crate) files: Vec, } -impl ProfileFiles { - /// Returns a builder to create `ProfileFiles` +impl EnvConfigFiles { + /// Returns a builder to create `EnvConfigFiles` pub fn builder() -> Builder { Builder::new() } } -impl Default for ProfileFiles { +impl Default for EnvConfigFiles { fn default() -> Self { Self { files: vec![ - ProfileFile::Default(ProfileFileKind::Config), - ProfileFile::Default(ProfileFileKind::Credentials), + EnvConfigFile::Default(EnvConfigFileKind::Config), + EnvConfigFile::Default(EnvConfigFileKind::Credentials), ], } } @@ -59,47 +59,47 @@ impl Default for ProfileFiles { /// Profile file type (config or credentials) #[derive(Copy, Clone, Debug)] -pub enum ProfileFileKind { +pub enum EnvConfigFileKind { /// The SDK config file that typically resides in `~/.aws/config` Config, /// The SDK credentials file that typically resides in `~/.aws/credentials` Credentials, } -impl ProfileFileKind { +impl EnvConfigFileKind { pub(crate) fn default_path(&self) -> &'static str { match &self { - ProfileFileKind::Credentials => "~/.aws/credentials", - ProfileFileKind::Config => "~/.aws/config", + EnvConfigFileKind::Credentials => "~/.aws/credentials", + EnvConfigFileKind::Config => "~/.aws/config", } } pub(crate) fn override_environment_variable(&self) -> &'static str { match &self { - ProfileFileKind::Config => "AWS_CONFIG_FILE", - ProfileFileKind::Credentials => "AWS_SHARED_CREDENTIALS_FILE", + EnvConfigFileKind::Config => "AWS_CONFIG_FILE", + EnvConfigFileKind::Credentials => "AWS_SHARED_CREDENTIALS_FILE", } } } -/// A single profile file within a [`ProfileFiles`] file set. +/// A single config file within a [`EnvConfigFiles`] file set. #[derive(Clone)] -pub(crate) enum ProfileFile { +pub(crate) enum EnvConfigFile { /// One of the default profile files (config or credentials in their default locations) - Default(ProfileFileKind), + Default(EnvConfigFileKind), /// A profile file at a custom location FilePath { - kind: ProfileFileKind, + kind: EnvConfigFileKind, path: PathBuf, }, /// The direct contents of a profile file FileContents { - kind: ProfileFileKind, + kind: EnvConfigFileKind, contents: String, }, } -impl fmt::Debug for ProfileFile { +impl fmt::Debug for EnvConfigFile { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Default(kind) => f.debug_tuple("Default").field(kind).finish(), @@ -118,12 +118,12 @@ impl fmt::Debug for ProfileFile { } } -/// Builder for [`ProfileFiles`]. +/// Builder for [`EnvConfigFiles`]. #[derive(Clone, Default, Debug)] pub struct Builder { with_config: bool, with_credentials: bool, - custom_sources: Vec, + custom_sources: Vec, } impl Builder { @@ -136,9 +136,9 @@ impl Builder { /// /// The default SDK config typically resides in `~/.aws/config`. When this flag is enabled, /// this config file will be included in the profile files that get loaded in the built - /// [`ProfileFiles`] file set. + /// [`EnvConfigFiles`] file set. /// - /// This flag defaults to `false` when using the builder to construct [`ProfileFiles`]. + /// This flag defaults to `false` when using the builder to construct [`EnvConfigFiles`]. pub fn include_default_config_file(mut self, include_default_config_file: bool) -> Self { self.with_config = include_default_config_file; self @@ -148,9 +148,9 @@ impl Builder { /// /// The default SDK config typically resides in `~/.aws/credentials`. When this flag is enabled, /// this credentials file will be included in the profile files that get loaded in the built - /// [`ProfileFiles`] file set. + /// [`EnvConfigFiles`] file set. /// - /// This flag defaults to `false` when using the builder to construct [`ProfileFiles`]. + /// This flag defaults to `false` when using the builder to construct [`EnvConfigFiles`]. pub fn include_default_credentials_file( mut self, include_default_credentials_file: bool, @@ -162,10 +162,10 @@ impl Builder { /// Include a custom `file` in the list of profile files to be loaded. /// /// The `kind` informs the parser how to treat the file. If it's intended to be like - /// the SDK credentials file typically in `~/.aws/config`, then use [`ProfileFileKind::Config`]. - /// Otherwise, use [`ProfileFileKind::Credentials`]. - pub fn with_file(mut self, kind: ProfileFileKind, file: impl Into) -> Self { - self.custom_sources.push(ProfileFile::FilePath { + /// the SDK credentials file typically in `~/.aws/config`, then use [`EnvConfigFileKind::Config`]. + /// Otherwise, use [`EnvConfigFileKind::Credentials`]. + pub fn with_file(mut self, kind: EnvConfigFileKind, file: impl Into) -> Self { + self.custom_sources.push(EnvConfigFile::FilePath { kind, path: file.into(), }); @@ -175,29 +175,29 @@ impl Builder { /// Include custom file `contents` in the list of profile files to be loaded. /// /// The `kind` informs the parser how to treat the file. If it's intended to be like - /// the SDK credentials file typically in `~/.aws/config`, then use [`ProfileFileKind::Config`]. - /// Otherwise, use [`ProfileFileKind::Credentials`]. - pub fn with_contents(mut self, kind: ProfileFileKind, contents: impl Into) -> Self { - self.custom_sources.push(ProfileFile::FileContents { + /// the SDK credentials file typically in `~/.aws/config`, then use [`EnvConfigFileKind::Config`]. + /// Otherwise, use [`EnvConfigFileKind::Credentials`]. + pub fn with_contents(mut self, kind: EnvConfigFileKind, contents: impl Into) -> Self { + self.custom_sources.push(EnvConfigFile::FileContents { kind, contents: contents.into(), }); self } - /// Build the [`ProfileFiles`] file set. - pub fn build(self) -> ProfileFiles { + /// Build the [`EnvConfigFiles`] file set. + pub fn build(self) -> EnvConfigFiles { let mut files = self.custom_sources; if self.with_credentials { - files.insert(0, ProfileFile::Default(ProfileFileKind::Credentials)); + files.insert(0, EnvConfigFile::Default(EnvConfigFileKind::Credentials)); } if self.with_config { - files.insert(0, ProfileFile::Default(ProfileFileKind::Config)); + files.insert(0, EnvConfigFile::Default(EnvConfigFileKind::Config)); } if files.is_empty() { - panic!("At least one profile file must be included in the `ProfileFiles` file set."); + panic!("At least one profile file must be included in the `EnvConfigFiles` file set."); } - ProfileFiles { files } + EnvConfigFiles { files } } } @@ -207,35 +207,35 @@ mod tests { #[test] fn redact_file_contents_in_profile_file_debug() { - let profile_file = ProfileFile::FileContents { - kind: ProfileFileKind::Config, + let shared_config_file = EnvConfigFile::FileContents { + kind: EnvConfigFileKind::Config, contents: "sensitive_contents".into(), }; - let debug = format!("{:?}", profile_file); + let debug = format!("{shared_config_file:?}"); assert!(!debug.contains("sensitive_contents")); assert!(debug.contains("** redacted **")); } #[test] fn build_correctly_orders_default_config_credentials() { - let profile_files = ProfileFiles::builder() - .with_file(ProfileFileKind::Config, "foo") + let shared_config_files = EnvConfigFiles::builder() + .with_file(EnvConfigFileKind::Config, "foo") .include_default_credentials_file(true) .include_default_config_file(true) .build(); - assert_eq!(3, profile_files.files.len()); + assert_eq!(3, shared_config_files.files.len()); assert!(matches!( - profile_files.files[0], - ProfileFile::Default(ProfileFileKind::Config) + shared_config_files.files[0], + EnvConfigFile::Default(EnvConfigFileKind::Config) )); assert!(matches!( - profile_files.files[1], - ProfileFile::Default(ProfileFileKind::Credentials) + shared_config_files.files[1], + EnvConfigFile::Default(EnvConfigFileKind::Credentials) )); assert!(matches!( - profile_files.files[2], - ProfileFile::FilePath { - kind: ProfileFileKind::Config, + shared_config_files.files[2], + EnvConfigFile::FilePath { + kind: EnvConfigFileKind::Config, path: _ } )); @@ -244,6 +244,6 @@ mod tests { #[test] #[should_panic] fn empty_builder_panics() { - ProfileFiles::builder().build(); + EnvConfigFiles::builder().build(); } } diff --git a/aws/rust-runtime/aws-runtime/src/profile/normalize.rs b/aws/rust-runtime/aws-runtime/src/env_config/normalize.rs similarity index 92% rename from aws/rust-runtime/aws-runtime/src/profile/normalize.rs rename to aws/rust-runtime/aws-runtime/src/env_config/normalize.rs index 8657c53899..5e19edbb6e 100644 --- a/aws/rust-runtime/aws-runtime/src/profile/normalize.rs +++ b/aws/rust-runtime/aws-runtime/src/env_config/normalize.rs @@ -3,10 +3,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -use crate::profile::parse::{RawProfileSet, WHITESPACE}; -use crate::profile::profile_file::ProfileFileKind; -use crate::profile::profile_set::ProfileSet; -use crate::profile::section::{Profile, PropertiesKey, Property, Section, SsoSession}; +use crate::env_config::file::EnvConfigFileKind; +use crate::env_config::parse::{RawProfileSet, WHITESPACE}; +use crate::env_config::property::{PropertiesKey, Property}; +use crate::env_config::section::{EnvConfigSections, Profile, Section, SsoSession}; use std::borrow::Cow; use std::collections::HashMap; @@ -52,9 +52,9 @@ impl<'a> SectionPair<'a> { /// 2. For Config files, the profile must either be `default` or it must have a profile prefix /// 3. For credentials files, the profile name MUST NOT have a profile prefix /// 4. Only config files can have sections other than `profile` sections - fn valid_for(self, kind: ProfileFileKind) -> Result { + fn valid_for(self, kind: EnvConfigFileKind) -> Result { match kind { - ProfileFileKind::Config => match (&self.prefix, &self.suffix) { + EnvConfigFileKind::Config => match (&self.prefix, &self.suffix) { (Some(prefix), suffix) => { if validate_identifier(suffix).is_ok() { Ok(self) @@ -70,7 +70,7 @@ impl<'a> SectionPair<'a> { } } }, - ProfileFileKind::Credentials => match (&self.prefix, &self.suffix) { + EnvConfigFileKind::Credentials => match (&self.prefix, &self.suffix) { (Some(prefix), suffix) => { if prefix == PROFILE_PREFIX { Err(format!("profile `{suffix}` ignored because credential profiles must NOT begin with `profile`")) @@ -100,9 +100,9 @@ impl<'a> SectionPair<'a> { /// - A profile named `profile default` takes priority over a profile named `default`. /// - Profiles with identical names are merged pub(super) fn merge_in( - base: &mut ProfileSet, + base: &mut EnvConfigSections, raw_profile_set: RawProfileSet<'_>, - kind: ProfileFileKind, + kind: EnvConfigFileKind, ) { // parse / validate sections let validated_sections = raw_profile_set @@ -230,11 +230,10 @@ fn parse_sub_properties(sub_properties_str: &str) -> impl Iterator = HashMap::new(); profile.insert("foo", HashMap::new()); - merge_in(&mut ProfileSet::default(), profile, ProfileFileKind::Config); + merge_in( + &mut EnvConfigSections::default(), + profile, + EnvConfigFileKind::Config, + ); assert!(logs_contain("profile [foo] ignored")); } } diff --git a/aws/rust-runtime/aws-runtime/src/profile/parse.rs b/aws/rust-runtime/aws-runtime/src/env_config/parse.rs similarity index 92% rename from aws/rust-runtime/aws-runtime/src/profile/parse.rs rename to aws/rust-runtime/aws-runtime/src/env_config/parse.rs index f481b7866e..54527917d8 100644 --- a/aws/rust-runtime/aws-runtime/src/profile/parse.rs +++ b/aws/rust-runtime/aws-runtime/src/env_config/parse.rs @@ -12,7 +12,7 @@ //! - profiles with invalid names //! - profile name normalization (`profile foo` => `foo`) -use crate::profile::source::File; +use crate::env_config::source::File; use std::borrow::Cow; use std::collections::HashMap; use std::error::Error; @@ -25,7 +25,7 @@ pub(super) type RawProfileSet<'a> = HashMap<&'a str, HashMap, Cow<' /// /// Profile parsing is actually quite strict about what is and is not whitespace, so use this instead /// of `.is_whitespace()` / `.trim()` -pub(super) const WHITESPACE: &[char] = &[' ', '\t']; +pub(crate) const WHITESPACE: &[char] = &[' ', '\t']; const COMMENT: &[char] = &['#', ';']; /// Location for use during error reporting @@ -37,7 +37,7 @@ struct Location { /// An error encountered while parsing a profile #[derive(Debug, Clone)] -pub struct ProfileParseError { +pub struct EnvConfigParseError { /// Location where this error occurred location: Location, @@ -45,7 +45,7 @@ pub struct ProfileParseError { message: String, } -impl Display for ProfileParseError { +impl Display for EnvConfigParseError { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!( f, @@ -55,14 +55,14 @@ impl Display for ProfileParseError { } } -impl Error for ProfileParseError {} +impl Error for EnvConfigParseError {} /// Validate that a line represents a valid subproperty /// /// - Sub-properties looks like regular properties (`k=v`) that are nested within an existing property. /// - Sub-properties must be validated for compatibility with other SDKs, but they are not actually /// parsed into structured data. -fn validate_subproperty(value: &str, location: Location) -> Result<(), ProfileParseError> { +fn validate_subproperty(value: &str, location: Location) -> Result<(), EnvConfigParseError> { if value.trim_matches(WHITESPACE).is_empty() { Ok(()) } else { @@ -104,7 +104,7 @@ enum State<'a> { } /// Parse `file` into a `RawProfileSet` -pub(super) fn parse_profile_file(file: &File) -> Result, ProfileParseError> { +pub(super) fn parse_profile_file(file: &File) -> Result, EnvConfigParseError> { let mut parser = Parser { data: HashMap::new(), state: State::Starting, @@ -119,7 +119,7 @@ pub(super) fn parse_profile_file(file: &File) -> Result, Profi impl<'a> Parser<'a> { /// Parse `file` containing profile data into `self.data`. - fn parse_profile(&mut self, file: &'a str) -> Result<(), ProfileParseError> { + fn parse_profile(&mut self, file: &'a str) -> Result<(), EnvConfigParseError> { for (line_number, line) in file.lines().enumerate() { self.location.line_number = line_number + 1; // store a 1-indexed line number if is_empty_line(line) || is_comment_line(line) { @@ -139,7 +139,7 @@ impl<'a> Parser<'a> { /// Parse a property line like `a = b` /// /// A property line is only valid when we're within a profile definition, `[profile foo]` - fn read_property_line(&mut self, line: &'a str) -> Result<(), ProfileParseError> { + fn read_property_line(&mut self, line: &'a str) -> Result<(), EnvConfigParseError> { let location = &self.location; let (current_profile, name) = match &self.state { State::Starting => return Err(self.make_error("Expected a profile definition")), @@ -160,8 +160,8 @@ impl<'a> Parser<'a> { } /// Create a location-tagged error message - fn make_error(&self, message: &str) -> ProfileParseError { - ProfileParseError { + fn make_error(&self, message: &str) -> EnvConfigParseError { + EnvConfigParseError { location: self.location.clone(), message: message.into(), } @@ -170,7 +170,7 @@ impl<'a> Parser<'a> { /// Parse the lines of a property after the first line. /// /// This is triggered by lines that start with whitespace. - fn read_property_continuation(&mut self, line: &'a str) -> Result<(), ProfileParseError> { + fn read_property_continuation(&mut self, line: &'a str) -> Result<(), EnvConfigParseError> { let current_property = match &self.state { State::Starting => return Err(self.make_error("Expected a profile definition")), State::ReadingProfile { @@ -200,7 +200,7 @@ impl<'a> Parser<'a> { Ok(()) } - fn read_profile_line(&mut self, line: &'a str) -> Result<(), ProfileParseError> { + fn read_profile_line(&mut self, line: &'a str) -> Result<(), EnvConfigParseError> { let line = prepare_line(line, false); let profile_name = line .strip_prefix('[') @@ -227,17 +227,17 @@ enum PropertyError { } impl PropertyError { - fn into_error(self, ctx: &str, location: Location) -> ProfileParseError { + fn into_error(self, ctx: &str, location: Location) -> EnvConfigParseError { let mut ctx = ctx.to_string(); match self { PropertyError::NoName => { ctx.get_mut(0..1).unwrap().make_ascii_uppercase(); - ProfileParseError { + EnvConfigParseError { location, message: format!("{} did not have a name", ctx), } } - PropertyError::NoEquals => ProfileParseError { + PropertyError::NoEquals => EnvConfigParseError { location, message: format!("Expected an '=' sign defining a {}", ctx), }, @@ -288,9 +288,9 @@ fn prepare_line(line: &str, comments_need_whitespace: bool) -> &str { #[cfg(test)] mod test { use super::{parse_profile_file, prepare_line, Location}; - use crate::profile::parse::{parse_property_line, PropertyError}; - use crate::profile::profile_file::ProfileFileKind; - use crate::profile::source::File; + use crate::env_config::file::EnvConfigFileKind; + use crate::env_config::parse::{parse_property_line, PropertyError}; + use crate::env_config::source::File; use std::borrow::Cow; // most test cases covered by the JSON test suite @@ -338,7 +338,7 @@ mod test { #[test] fn error_line_numbers() { let file = File { - kind: ProfileFileKind::Config, + kind: EnvConfigFileKind::Config, path: Some("~/.aws/config".into()), contents: "[default\nk=v".into(), }; diff --git a/aws/rust-runtime/aws-runtime/src/profile/section.rs b/aws/rust-runtime/aws-runtime/src/env_config/property.rs similarity index 60% rename from aws/rust-runtime/aws-runtime/src/profile/section.rs rename to aws/rust-runtime/aws-runtime/src/env_config/property.rs index 705a6b59ba..f83bfbcdf8 100644 --- a/aws/rust-runtime/aws-runtime/src/profile/section.rs +++ b/aws/rust-runtime/aws-runtime/src/env_config/property.rs @@ -176,140 +176,3 @@ impl Properties { self.inner.get(properties_key) } } - -/// Represents a top-level section (e.g., `[profile name]`) in a config file. -pub(crate) trait Section { - /// The name of this section - fn name(&self) -> &str; - - /// Returns all the properties in this section - fn properties(&self) -> &HashMap; - - /// Returns a reference to the property named `name` - fn get(&self, name: &str) -> Option<&str>; - - /// True if there are no properties in this section. - fn is_empty(&self) -> bool; - - /// Insert a property into a section - fn insert(&mut self, name: String, value: Property); -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub(super) struct SectionInner { - pub(super) name: String, - pub(super) properties: HashMap, -} - -impl Section for SectionInner { - fn name(&self) -> &str { - &self.name - } - - fn properties(&self) -> &HashMap { - &self.properties - } - - fn get(&self, name: &str) -> Option<&str> { - self.properties - .get(name.to_ascii_lowercase().as_str()) - .map(|prop| prop.value()) - } - - fn is_empty(&self) -> bool { - self.properties.is_empty() - } - - fn insert(&mut self, name: String, value: Property) { - self.properties.insert(name.to_ascii_lowercase(), value); - } -} - -/// An individual configuration profile -/// -/// An AWS config may be composed of a multiple named profiles within a [`ProfileSet`](crate::profile::ProfileSet). -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct Profile(SectionInner); - -impl Profile { - /// Create a new profile - pub fn new(name: impl Into, properties: HashMap) -> Self { - Self(SectionInner { - name: name.into(), - properties, - }) - } - - /// The name of this profile - pub fn name(&self) -> &str { - self.0.name() - } - - /// Returns a reference to the property named `name` - pub fn get(&self, name: &str) -> Option<&str> { - self.0.get(name) - } -} - -impl Section for Profile { - fn name(&self) -> &str { - self.0.name() - } - - fn properties(&self) -> &HashMap { - self.0.properties() - } - - fn get(&self, name: &str) -> Option<&str> { - self.0.get(name) - } - - fn is_empty(&self) -> bool { - self.0.is_empty() - } - - fn insert(&mut self, name: String, value: Property) { - self.0.insert(name, value) - } -} - -/// A `[sso-session name]` section in the config. -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct SsoSession(SectionInner); - -impl SsoSession { - /// Create a new SSO session section. - pub(super) fn new(name: impl Into, properties: HashMap) -> Self { - Self(SectionInner { - name: name.into(), - properties, - }) - } - - /// Returns a reference to the property named `name` - pub fn get(&self, name: &str) -> Option<&str> { - self.0.get(name) - } -} - -impl Section for SsoSession { - fn name(&self) -> &str { - self.0.name() - } - - fn properties(&self) -> &HashMap { - self.0.properties() - } - - fn get(&self, name: &str) -> Option<&str> { - self.0.get(name) - } - - fn is_empty(&self) -> bool { - self.0.is_empty() - } - - fn insert(&mut self, name: String, value: Property) { - self.0.insert(name, value) - } -} diff --git a/aws/rust-runtime/aws-runtime/src/profile/profile_set.rs b/aws/rust-runtime/aws-runtime/src/env_config/section.rs similarity index 69% rename from aws/rust-runtime/aws-runtime/src/profile/profile_set.rs rename to aws/rust-runtime/aws-runtime/src/env_config/section.rs index d603c8e018..8a14b95e64 100644 --- a/aws/rust-runtime/aws-runtime/src/profile/profile_set.rs +++ b/aws/rust-runtime/aws-runtime/src/env_config/section.rs @@ -3,25 +3,162 @@ * SPDX-License-Identifier: Apache-2.0 */ -//! Code for accessing and validating AWS config profiles +//! Sections within an AWS config profile. -use crate::profile::normalize; -use crate::profile::parse::{parse_profile_file, ProfileParseError}; -use crate::profile::section::{Profile, Properties, SsoSession}; -use crate::profile::source::Source; +use crate::env_config::normalize; +use crate::env_config::parse::{parse_profile_file, EnvConfigParseError}; +use crate::env_config::property::{Properties, Property}; +use crate::env_config::source::Source; use std::borrow::Cow; use std::collections::HashMap; +/// Represents a top-level section (e.g., `[profile name]`) in a config file. +pub(crate) trait Section { + /// The name of this section + fn name(&self) -> &str; + + /// Returns all the properties in this section + fn properties(&self) -> &HashMap; + + /// Returns a reference to the property named `name` + fn get(&self, name: &str) -> Option<&str>; + + /// True if there are no properties in this section. + fn is_empty(&self) -> bool; + + /// Insert a property into a section + fn insert(&mut self, name: String, value: Property); +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub(super) struct SectionInner { + pub(super) name: String, + pub(super) properties: HashMap, +} + +impl Section for SectionInner { + fn name(&self) -> &str { + &self.name + } + + fn properties(&self) -> &HashMap { + &self.properties + } + + fn get(&self, name: &str) -> Option<&str> { + self.properties + .get(name.to_ascii_lowercase().as_str()) + .map(|prop| prop.value()) + } + + fn is_empty(&self) -> bool { + self.properties.is_empty() + } + + fn insert(&mut self, name: String, value: Property) { + self.properties.insert(name.to_ascii_lowercase(), value); + } +} + +/// An individual configuration profile +/// +/// An AWS config may be composed of a multiple named profiles within a [`EnvConfigSections`]. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Profile(SectionInner); + +impl Profile { + /// Create a new profile + pub fn new(name: impl Into, properties: HashMap) -> Self { + Self(SectionInner { + name: name.into(), + properties, + }) + } + + /// The name of this profile + pub fn name(&self) -> &str { + self.0.name() + } + + /// Returns a reference to the property named `name` + pub fn get(&self, name: &str) -> Option<&str> { + self.0.get(name) + } +} + +impl Section for Profile { + fn name(&self) -> &str { + self.0.name() + } + + fn properties(&self) -> &HashMap { + self.0.properties() + } + + fn get(&self, name: &str) -> Option<&str> { + self.0.get(name) + } + + fn is_empty(&self) -> bool { + self.0.is_empty() + } + + fn insert(&mut self, name: String, value: Property) { + self.0.insert(name, value) + } +} + +/// A `[sso-session name]` section in the config. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct SsoSession(SectionInner); + +impl SsoSession { + /// Create a new SSO session section. + pub(super) fn new(name: impl Into, properties: HashMap) -> Self { + Self(SectionInner { + name: name.into(), + properties, + }) + } + + /// Returns a reference to the property named `name` + pub fn get(&self, name: &str) -> Option<&str> { + self.0.get(name) + } +} + +impl Section for SsoSession { + fn name(&self) -> &str { + self.0.name() + } + + fn properties(&self) -> &HashMap { + self.0.properties() + } + + fn get(&self, name: &str) -> Option<&str> { + self.0.get(name) + } + + fn is_empty(&self) -> bool { + self.0.is_empty() + } + + fn insert(&mut self, name: String, value: Property) { + self.0.insert(name, value) + } +} + /// A top-level configuration source containing multiple named profiles #[derive(Debug, Eq, Clone, PartialEq)] -pub struct ProfileSet { +pub struct EnvConfigSections { pub(crate) profiles: HashMap, pub(crate) selected_profile: Cow<'static, str>, pub(crate) sso_sessions: HashMap, pub(crate) other_sections: Properties, } -impl Default for ProfileSet { +impl Default for EnvConfigSections { fn default() -> Self { Self { profiles: Default::default(), @@ -32,7 +169,7 @@ impl Default for ProfileSet { } } -impl ProfileSet { +impl EnvConfigSections { /// Create a new Profile set directly from a HashMap /// /// This method creates a ProfileSet directly from a hashmap with no normalization for test purposes. @@ -43,9 +180,7 @@ impl ProfileSet { sso_sessions: HashMap>, other_sections: Properties, ) -> Self { - use crate::profile::section::Property; - - let mut base = ProfileSet { + let mut base = EnvConfigSections { selected_profile: selected_profile.into(), ..Default::default() }; @@ -119,9 +254,9 @@ impl ProfileSet { &self.other_sections } - /// Given a [`Source`] of profile config, parse and merge them into a `ProfileSet`. - pub fn parse(source: Source) -> Result { - let mut base = ProfileSet { + /// Given a [`Source`] of profile config, parse and merge them into a `EnvConfigSections`. + pub fn parse(source: Source) -> Result { + let mut base = EnvConfigSections { selected_profile: source.profile, ..Default::default() }; @@ -135,10 +270,10 @@ impl ProfileSet { #[cfg(test)] mod test { - use super::ProfileSet; - use crate::profile::profile_file::ProfileFileKind; - use crate::profile::section::Section; - use crate::profile::source::{File, Source}; + use super::EnvConfigSections; + use crate::env_config::file::EnvConfigFileKind; + use crate::env_config::section::Section; + use crate::env_config::source::{File, Source}; use arbitrary::{Arbitrary, Unstructured}; use serde::Deserialize; use std::collections::HashMap; @@ -168,7 +303,7 @@ mod test { credentials_file: Some("".to_string()), }); - let profile_set = ProfileSet::parse(source).expect("empty profiles are valid"); + let profile_set = EnvConfigSections::parse(source).expect("empty profiles are valid"); assert!(profile_set.is_empty()); } @@ -179,7 +314,7 @@ mod test { credentials_file: Some("".to_string()), }); - let profile_set = ProfileSet::parse(source).expect("profiles loaded"); + let profile_set = EnvConfigSections::parse(source).expect("profiles loaded"); let mut profile_names: Vec<_> = profile_set.profiles().collect(); profile_names.sort(); @@ -201,12 +336,12 @@ mod test { let profile_source = Source { files: vec![ File { - kind: ProfileFileKind::Config, + kind: EnvConfigFileKind::Config, path: Some("~/.aws/config".to_string()), contents: conf.unwrap_or_default().to_string(), }, File { - kind: ProfileFileKind::Credentials, + kind: EnvConfigFileKind::Credentials, path: Some("~/.aws/credentials".to_string()), contents: creds.unwrap_or_default().to_string(), }, @@ -214,7 +349,7 @@ mod test { profile: "default".into(), }; // don't care if parse fails, just don't panic - let _ = ProfileSet::parse(profile_source); + let _ = EnvConfigSections::parse(profile_source); } Ok(()) @@ -226,7 +361,7 @@ mod test { profiles: HashMap>, sso_sessions: HashMap>, } - fn flatten(config: ProfileSet) -> FlattenedProfileSet { + fn flatten(config: EnvConfigSections) -> FlattenedProfileSet { FlattenedProfileSet { profiles: flatten_sections(config.profiles.values().map(|p| p as _)), sso_sessions: flatten_sections(config.sso_sessions.values().map(|s| s as _)), @@ -253,12 +388,12 @@ mod test { Source { files: vec![ File { - kind: ProfileFileKind::Config, + kind: EnvConfigFileKind::Config, path: Some("~/.aws/config".to_string()), contents: input.config_file.unwrap_or_default(), }, File { - kind: ProfileFileKind::Credentials, + kind: EnvConfigFileKind::Credentials, path: Some("~/.aws/credentials".to_string()), contents: input.credentials_file.unwrap_or_default(), }, @@ -270,7 +405,7 @@ mod test { // wrapper to generate nicer errors during test failure fn check(test_case: ParserTest) { let copy = test_case.clone(); - let parsed = ProfileSet::parse(make_source(test_case.input)); + let parsed = EnvConfigSections::parse(make_source(test_case.input)); let res = match (parsed.map(flatten), &test_case.output) { ( Ok(FlattenedProfileSet { diff --git a/aws/rust-runtime/aws-runtime/src/profile/source.rs b/aws/rust-runtime/aws-runtime/src/env_config/source.rs similarity index 90% rename from aws/rust-runtime/aws-runtime/src/profile/source.rs rename to aws/rust-runtime/aws-runtime/src/env_config/source.rs index 2640d7071c..c18d573c8d 100644 --- a/aws/rust-runtime/aws-runtime/src/profile/source.rs +++ b/aws/rust-runtime/aws-runtime/src/env_config/source.rs @@ -5,9 +5,9 @@ //! Code for handling in-memory sources of profile data -use super::error::{CouldNotReadProfileFile, ProfileFileLoadError}; +use super::error::{CouldNotReadConfigFile, EnvConfigFileLoadError}; +use crate::env_config::file::{EnvConfigFile, EnvConfigFileKind, EnvConfigFiles}; use crate::fs_util::{home_dir, Os}; -use crate::profile::profile_file::{ProfileFile, ProfileFileKind, ProfileFiles}; use aws_smithy_types::error::display::DisplayErrorContext; use aws_types::os_shim_internal; use std::borrow::Cow; @@ -15,7 +15,6 @@ use std::io::ErrorKind; use std::path::{Component, Path, PathBuf}; use std::sync::Arc; use tracing::{warn, Instrument}; - const HOME_EXPANSION_FAILURE_WARNING: &str = "home directory expansion was requested (via `~` character) for the profile \ config file path, but no home directory could be determined"; @@ -35,7 +34,7 @@ pub struct Source { #[derive(Debug)] /// In-memory configuration file pub struct File { - pub(crate) kind: ProfileFileKind, + pub(crate) kind: EnvConfigFileKind, pub(crate) path: Option, pub(crate) contents: String, } @@ -44,8 +43,8 @@ pub struct File { pub async fn load( proc_env: &os_shim_internal::Env, fs: &os_shim_internal::Fs, - profile_files: &ProfileFiles, -) -> Result { + profile_files: &EnvConfigFiles, +) -> Result { let home = home_dir(proc_env, Os::real()); let mut files = Vec::new(); @@ -87,13 +86,13 @@ fn file_contents_to_string(path: &Path, contents: Vec) -> String { /// * `fs`: Filesystem abstraction /// * `environment`: Process environment abstraction async fn load_config_file( - source: &ProfileFile, + source: &EnvConfigFile, home_directory: &Option, fs: &os_shim_internal::Fs, environment: &os_shim_internal::Env, -) -> Result { +) -> Result { let (path, kind, contents) = match source { - ProfileFile::Default(kind) => { + EnvConfigFile::Default(kind) => { let (path_is_default, path) = environment .get(kind.override_environment_variable()) .map(|p| (false, Cow::Owned(p))) @@ -128,12 +127,12 @@ async fn load_config_file( let contents = file_contents_to_string(&expanded, data); (Some(Cow::Owned(expanded)), kind, contents) } - ProfileFile::FilePath { kind, path } => { + EnvConfigFile::FilePath { kind, path } => { let data = match fs.read_to_end(&path).await { Ok(data) => data, Err(e) => { - return Err(ProfileFileLoadError::CouldNotReadFile( - CouldNotReadProfileFile { + return Err(EnvConfigFileLoadError::CouldNotReadFile( + CouldNotReadConfigFile { path: path.clone(), cause: Arc::new(e), }, @@ -146,7 +145,7 @@ async fn load_config_file( file_contents_to_string(path, data), ) } - ProfileFile::FileContents { kind, contents } => (None, kind, contents.clone()), + EnvConfigFile::FileContents { kind, contents } => (None, kind, contents.clone()), }; tracing::debug!(path = ?path, size = ?contents.len(), "config file loaded"); Ok(File { @@ -200,9 +199,9 @@ fn expand_home( #[cfg(test)] mod tests { - use crate::profile::error::ProfileFileLoadError; - use crate::profile::profile_file::{ProfileFile, ProfileFileKind, ProfileFiles}; - use crate::profile::source::{ + use crate::env_config::error::EnvConfigFileLoadError; + use crate::env_config::file::{EnvConfigFile, EnvConfigFileKind, EnvConfigFiles}; + use crate::env_config::source::{ expand_home, load, load_config_file, HOME_EXPANSION_FAILURE_WARNING, }; use aws_types::os_shim_internal::{Env, Fs}; @@ -278,7 +277,7 @@ mod tests { let fs = Fs::from_slice(&[]); let _src = load_config_file( - &ProfileFile::Default(ProfileFileKind::Config), + &EnvConfigFile::Default(EnvConfigFileKind::Config), &None, &fs, &env, @@ -294,7 +293,7 @@ mod tests { let fs = Fs::from_slice(&[]); let _src = load_config_file( - &ProfileFile::Default(ProfileFileKind::Config), + &EnvConfigFile::Default(EnvConfigFileKind::Config), &None, &fs, &env, @@ -383,8 +382,8 @@ mod tests { "; let env = Env::from_slice(&[]); let fs = Fs::from_slice(&[]); - let profile_files = ProfileFiles::builder() - .with_contents(ProfileFileKind::Credentials, contents) + let profile_files = EnvConfigFiles::builder() + .with_contents(EnvConfigFileKind::Credentials, contents) .build(); let source = load(&env, &fs, &profile_files).await.unwrap(); assert_eq!(1, source.files.len()); @@ -406,8 +405,11 @@ mod tests { let fs = Fs::from_map(fs); let env = Env::from_slice(&[]); - let profile_files = ProfileFiles::builder() - .with_file(ProfileFileKind::Credentials, "/custom/path/to/credentials") + let profile_files = EnvConfigFiles::builder() + .with_file( + EnvConfigFileKind::Credentials, + "/custom/path/to/credentials", + ) .build(); let source = load(&env, &fs, &profile_files).await.unwrap(); assert_eq!(1, source.files.len()); @@ -438,8 +440,8 @@ mod tests { let fs = Fs::from_map(fs); let env = Env::from_slice(&[("HOME", "/user/name")]); - let profile_files = ProfileFiles::builder() - .with_contents(ProfileFileKind::Config, custom_contents) + let profile_files = EnvConfigFiles::builder() + .with_contents(EnvConfigFileKind::Config, custom_contents) .include_default_credentials_file(true) .include_default_config_file(true) .build(); @@ -460,8 +462,8 @@ mod tests { let fs = Fs::from_slice(&[]); let env = Env::from_slice(&[("HOME", "/user/name")]); - let profile_files = ProfileFiles::builder() - .with_contents(ProfileFileKind::Config, custom_contents) + let profile_files = EnvConfigFiles::builder() + .with_contents(EnvConfigFileKind::Config, custom_contents) .include_default_credentials_file(true) .include_default_config_file(true) .build(); @@ -477,12 +479,12 @@ mod tests { async fn misconfigured_programmatic_custom_profile_path_must_error() { let fs = Fs::from_slice(&[]); let env = Env::from_slice(&[]); - let profile_files = ProfileFiles::builder() - .with_file(ProfileFileKind::Config, "definitely-doesnt-exist") + let profile_files = EnvConfigFiles::builder() + .with_file(EnvConfigFileKind::Config, "definitely-doesnt-exist") .build(); assert!(matches!( load(&env, &fs, &profile_files).await, - Err(ProfileFileLoadError::CouldNotReadFile(_)) + Err(EnvConfigFileLoadError::CouldNotReadFile(_)) )); } } diff --git a/aws/rust-runtime/aws-runtime/src/lib.rs b/aws/rust-runtime/aws-runtime/src/lib.rs index d6d1bd319a..3030866953 100644 --- a/aws/rust-runtime/aws-runtime/src/lib.rs +++ b/aws/rust-runtime/aws-runtime/src/lib.rs @@ -41,11 +41,9 @@ pub mod request_info; /// Interceptor that determines the clock skew between the client and service. pub mod service_clock_skew; -/// Supporting code for extracting config from an AWS config file. -pub mod profile; - /// Filesystem utilities pub mod fs_util; -/// Supporting code for parsing AWS config values set in a user's environment +/// Supporting code for parsing AWS config values set in a user's environment or +/// in a shared config file. pub mod env_config; diff --git a/aws/rust-runtime/aws-runtime/src/profile.rs b/aws/rust-runtime/aws-runtime/src/profile.rs deleted file mode 100644 index 10e2274550..0000000000 --- a/aws/rust-runtime/aws-runtime/src/profile.rs +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -use crate::env_config::{EnvConfigError, EnvConfigValue}; -use crate::profile::profile_set::ProfileSet; -use aws_types::os_shim_internal::Env; -use aws_types::service_config::ServiceConfigKey; -use std::error::Error; - -pub mod error; -mod normalize; -pub mod parse; -pub mod profile_file; -pub mod profile_set; -pub mod section; -pub mod source; - -/// Given a key, access to the environment, and a validator, return a config value if one was set. -pub async fn get_service_env_config<'a, T, E>( - key: ServiceConfigKey<'a>, - env: &'a Env, - profiles: Option<&'a ProfileSet>, - validator: impl Fn(&str) -> Result, -) -> Result, EnvConfigError> -where - E: Error + Send + Sync + 'static, -{ - EnvConfigValue::default() - .env(key.env()) - .profile(key.profile()) - .service_id(key.service_id()) - .validate(env, profiles, validator) -} diff --git a/aws/rust-runtime/aws-types/src/sdk_config.rs b/aws/rust-runtime/aws-types/src/sdk_config.rs index 19f1a89a40..a8f0071f68 100644 --- a/aws/rust-runtime/aws-types/src/sdk_config.rs +++ b/aws/rust-runtime/aws-types/src/sdk_config.rs @@ -95,7 +95,7 @@ pub struct Builder { use_fips: Option, use_dual_stack: Option, behavior_version: Option, - service_config: Option>, + service_config: Option>, } impl Builder { @@ -629,7 +629,7 @@ impl Builder { &mut self, service_config: Option, ) -> &mut Self { - self.service_config = service_config.map(|it| Box::new(it) as _); + self.service_config = service_config.map(|it| Arc::new(it) as Arc); self } @@ -651,7 +651,7 @@ impl Builder { time_source: self.time_source, behavior_version: self.behavior_version, stalled_stream_protection_config: self.stalled_stream_protection_config, - service_config: self.service_config.map(Arc::new), + service_config: self.service_config, } } } @@ -805,7 +805,7 @@ impl SdkConfig { /// Return an immutable reference to the service config provider configured for this client. pub fn service_config(&self) -> Option<&dyn LoadServiceConfig> { - self.service_config.as_deref().map(|it| it.as_ref()) + self.service_config.as_deref() } /// Config builder @@ -840,7 +840,7 @@ impl SdkConfig { use_dual_stack: self.use_dual_stack, behavior_version: self.behavior_version, stalled_stream_protection_config: self.stalled_stream_protection_config, - service_config: self.service_config.and_then(Arc::into_inner), + service_config: self.service_config, } } } diff --git a/aws/sdk/integration-tests/dynamodb/tests/shared-config.rs b/aws/sdk/integration-tests/dynamodb/tests/shared-config.rs index db29c9d275..a0a26bc0e2 100644 --- a/aws/sdk/integration-tests/dynamodb/tests/shared-config.rs +++ b/aws/sdk/integration-tests/dynamodb/tests/shared-config.rs @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -use aws_runtime::profile::profile_file::{ProfileFileKind, ProfileFiles}; +use aws_runtime::env_config::file::{EnvConfigFileKind, EnvConfigFiles}; use aws_sdk_dynamodb::config::{ BehaviorVersion, Credentials, Region, StalledStreamProtectionConfig, }; @@ -53,8 +53,8 @@ dynamodb = .behavior_version(BehaviorVersion::latest()) .profile_name("custom") .profile_files( - ProfileFiles::builder() - .with_contents(ProfileFileKind::Config, config) + EnvConfigFiles::builder() + .with_contents(EnvConfigFileKind::Config, config) .build(), ) .load() From fca10de1070d7079785669876ded123441358545 Mon Sep 17 00:00:00 2001 From: Zelda Hessler Date: Mon, 25 Mar 2024 14:50:26 -0500 Subject: [PATCH 13/23] add missing doc comment --- aws/rust-runtime/aws-runtime/src/env_config.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/aws/rust-runtime/aws-runtime/src/env_config.rs b/aws/rust-runtime/aws-runtime/src/env_config.rs index 52c2ff46cc..95ff25472f 100644 --- a/aws/rust-runtime/aws-runtime/src/env_config.rs +++ b/aws/rust-runtime/aws-runtime/src/env_config.rs @@ -66,7 +66,14 @@ impl<'a> fmt::Display for Scope<'a> { } } +/// The source that env config was derived from. /// +/// Includes: +/// +/// - Whether some config came from a config file or an env var. +/// - The key used to identify the config value. +/// +/// Only used when displaying config-extraction errors. #[derive(Debug)] pub struct EnvConfigSource<'a> { key: Cow<'a, str>, From 25fea3c6487c6ac52d1e4d624a7ff4f2e66d24c7 Mon Sep 17 00:00:00 2001 From: Zelda Hessler Date: Mon, 25 Mar 2024 14:53:46 -0500 Subject: [PATCH 14/23] remove unnecessary async --- aws/rust-runtime/aws-runtime/src/env_config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws/rust-runtime/aws-runtime/src/env_config.rs b/aws/rust-runtime/aws-runtime/src/env_config.rs index 95ff25472f..bd522fe841 100644 --- a/aws/rust-runtime/aws-runtime/src/env_config.rs +++ b/aws/rust-runtime/aws-runtime/src/env_config.rs @@ -20,7 +20,7 @@ pub mod section; pub mod source; /// Given a key, access to the environment, and a validator, return a config value if one was set. -pub async fn get_service_env_config<'a, T, E>( +pub fn get_service_env_config<'a, T, E>( key: ServiceConfigKey<'a>, env: &'a Env, shared_config_sections: Option<&'a EnvConfigSections>, From fc1c32c285cbb053dd3ec7b10bbe61fe5b44cbf3 Mon Sep 17 00:00:00 2001 From: Zelda Hessler Date: Tue, 26 Mar 2024 11:07:39 -0500 Subject: [PATCH 15/23] appease the versioner --- rust-runtime/aws-smithy-async/Cargo.toml | 2 +- rust-runtime/aws-smithy-mocks-experimental/Cargo.toml | 2 +- rust-runtime/aws-smithy-wasm/Cargo.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/rust-runtime/aws-smithy-async/Cargo.toml b/rust-runtime/aws-smithy-async/Cargo.toml index 1f8ef4815c..d992d86ad8 100644 --- a/rust-runtime/aws-smithy-async/Cargo.toml +++ b/rust-runtime/aws-smithy-async/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aws-smithy-async" -version = "1.2.0" +version = "1.2.1" authors = ["AWS Rust SDK Team ", "John DiSanti "] description = "Async runtime agnostic abstractions for smithy-rs." edition = "2021" diff --git a/rust-runtime/aws-smithy-mocks-experimental/Cargo.toml b/rust-runtime/aws-smithy-mocks-experimental/Cargo.toml index 4db2dda05a..f7828bc2d5 100644 --- a/rust-runtime/aws-smithy-mocks-experimental/Cargo.toml +++ b/rust-runtime/aws-smithy-mocks-experimental/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aws-smithy-mocks-experimental" -version = "0.2.0" +version = "0.2.1" authors = ["AWS Rust SDK Team "] description = "Experimental testing utilities for smithy-rs generated clients" edition = "2021" diff --git a/rust-runtime/aws-smithy-wasm/Cargo.toml b/rust-runtime/aws-smithy-wasm/Cargo.toml index bf4602baec..5dc19b50b2 100644 --- a/rust-runtime/aws-smithy-wasm/Cargo.toml +++ b/rust-runtime/aws-smithy-wasm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aws-smithy-wasm" -version = "0.1.1" +version = "0.1.2" authors = [ "AWS Rust SDK Team ", "Eduardo Rodrigues <16357187+eduardomourar@users.noreply.github.com>", From cd423f30f03e89727cb4056c1ed55f733c2f9a37 Mon Sep 17 00:00:00 2001 From: Zelda Hessler Date: Tue, 26 Mar 2024 11:52:02 -0500 Subject: [PATCH 16/23] fix logging test broken by crate move --- aws/rust-runtime/aws-config/src/lib.rs | 31 ++++++++++++-------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/aws/rust-runtime/aws-config/src/lib.rs b/aws/rust-runtime/aws-config/src/lib.rs index 41617eaa66..b97178be47 100644 --- a/aws/rust-runtime/aws-config/src/lib.rs +++ b/aws/rust-runtime/aws-config/src/lib.rs @@ -882,15 +882,15 @@ mod loader { use aws_credential_types::provider::ProvideCredentials; use aws_smithy_async::rt::sleep::TokioSleep; use aws_smithy_runtime::client::http::test_util::{infallible_client_fn, NeverClient}; + use aws_smithy_runtime::test_util::capture_test_logs::capture_test_logs; use aws_types::app_name::AppName; use aws_types::os_shim_internal::{Env, Fs}; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; - use tracing_test::traced_test; - #[traced_test] #[tokio::test] async fn provider_config_used() { + let (_guard, logs_rx) = capture_test_logs(); let env = Env::from_slice(&[ ("AWS_MAX_ATTEMPTS", "10"), ("AWS_REGION", "us-west-4"), @@ -930,21 +930,18 @@ mod loader { .access_key_id(), ); assert_eq!(Some(&AppName::new("correct").unwrap()), loader.app_name()); - logs_assert(|lines| { - let num_config_loader_logs = lines - .iter() - .filter(|l| l.contains("provider_config_used")) - .filter(|l| l.contains("config file loaded")) - .count(); - match num_config_loader_logs { - 0 => Err("no config file logs found!".to_string()), - 1 => Ok(()), - more => Err(format!( - "the config file was parsed more than once! (parsed {})", - more - )), - } - }); + + let num_config_loader_logs = logs_rx.contents() + .lines() + // The logger uses fancy formatting, so we have to account for that. + .filter(|l| l.contains("config file loaded \u{1b}[3mpath\u{1b}[0m\u{1b}[2m=\u{1b}[0mSome(\"test_config\") \u{1b}[3msize\u{1b}[0m\u{1b}[2m=\u{1b}")) + .count(); + + match num_config_loader_logs { + 0 => panic!("no config file logs found!"), + 1 => (), + more => panic!("the config file was parsed more than once! (parsed {more})",), + }; } fn base_conf() -> ConfigLoader { From 69ea83a62f7ff470fe70814a191b2135a37777f6 Mon Sep 17 00:00:00 2001 From: Zelda Hessler Date: Tue, 26 Mar 2024 12:28:28 -0500 Subject: [PATCH 17/23] update external-types.toml --- aws/rust-runtime/aws-config/external-types.toml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/aws/rust-runtime/aws-config/external-types.toml b/aws/rust-runtime/aws-config/external-types.toml index 13cdd87597..9c8d3c1b24 100644 --- a/aws/rust-runtime/aws-config/external-types.toml +++ b/aws/rust-runtime/aws-config/external-types.toml @@ -8,13 +8,21 @@ allowed_external_types = [ "aws_credential_types::provider::credentials::Result", "aws_credential_types::provider::credentials::SharedCredentialsProvider", "aws_credential_types::provider::token::ProvideToken", + "aws_runtime::env_config::error::EnvConfigFileLoadError", + "aws_runtime::env_config::file::Builder", + "aws_runtime::env_config::file::EnvConfigFileKind", + "aws_runtime::env_config::file::EnvConfigFiles", + "aws_runtime::env_config::parse::EnvConfigParseError", + "aws_runtime::env_config::property::Property", + "aws_runtime::env_config::section::EnvConfigSections", + "aws_runtime::env_config::section::Profile", "aws_smithy_async::rt::sleep::AsyncSleep", "aws_smithy_async::rt::sleep::SharedAsyncSleep", "aws_smithy_async::time::SharedTimeSource", "aws_smithy_async::time::TimeSource", - "aws_smithy_runtime_api::box_error::BoxError", "aws_smithy_runtime::client::identity::cache::IdentityCache", "aws_smithy_runtime::client::identity::cache::lazy::LazyCacheBuilder", + "aws_smithy_runtime_api::box_error::BoxError", "aws_smithy_runtime_api::client::behavior_version::BehaviorVersion", "aws_smithy_runtime_api::client::dns::ResolveDns", "aws_smithy_runtime_api::client::dns::SharedDnsResolver", From 02d7ae9a4f4b7f81b435b9c066b713d1c12d0b1c Mon Sep 17 00:00:00 2001 From: Zelda Hessler Date: Thu, 28 Mar 2024 09:33:10 -0500 Subject: [PATCH 18/23] PR comments update --- aws/rust-runtime/aws-config/Cargo.toml | 9 ++---- .../aws-config/src/profile/profile_file.rs | 6 ++-- .../aws-runtime/src/env_config.rs | 12 +++---- .../rustsdk/customize/s3/S3Decorator.kt | 31 +++++++++++++++++++ .../customize/s3/S3ExpressDecorator.kt | 16 ---------- 5 files changed, 42 insertions(+), 32 deletions(-) diff --git a/aws/rust-runtime/aws-config/Cargo.toml b/aws/rust-runtime/aws-config/Cargo.toml index 006a2f5731..8ba7eb09e4 100644 --- a/aws/rust-runtime/aws-config/Cargo.toml +++ b/aws/rust-runtime/aws-config/Cargo.toml @@ -35,6 +35,8 @@ aws-smithy-runtime = { path = "../../sdk/build/aws-sdk/sdk/aws-smithy-runtime", aws-smithy-runtime-api = { path = "../../sdk/build/aws-sdk/sdk/aws-smithy-runtime-api", features = ["client"] } aws-smithy-types = { path = "../../sdk/build/aws-sdk/sdk/aws-smithy-types" } aws-types = { path = "../../sdk/build/aws-sdk/sdk/aws-types" } +bytes = "1.1.0" +http = "0.2.4" hyper = { version = "0.14.26", default-features = false } time = { version = "0.3.4", features = ["parsing"] } tokio = { version = "1.13.1", features = ["sync"] } @@ -44,9 +46,6 @@ url = "2.3.1" # implementation detail of IMDS credentials provider fastrand = "2.0.0" -bytes = "1.1.0" -http = "0.2.4" - # implementation detail of SSO credential caching aws-sdk-sso = { path = "../../sdk/build/aws-sdk/sdk/sso", default-features = false, optional = true } ring = { version = "0.17.5", optional = true } @@ -62,12 +61,8 @@ aws-smithy-runtime-api = { path = "../../sdk/build/aws-sdk/sdk/aws-smithy-runtim futures-util = { version = "0.3.29", default-features = false } tracing-test = "0.2.4" tracing-subscriber = { version = "0.3.16", features = ["fmt", "json"] } - tokio = { version = "1.23.1", features = ["full", "test-util"] } -# used for fuzzing profile parsing -arbitrary = "1.3" - # used for test case deserialization serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/aws/rust-runtime/aws-config/src/profile/profile_file.rs b/aws/rust-runtime/aws-config/src/profile/profile_file.rs index 14af175cb2..c99bd4f16c 100644 --- a/aws/rust-runtime/aws-config/src/profile/profile_file.rs +++ b/aws/rust-runtime/aws-config/src/profile/profile_file.rs @@ -7,18 +7,18 @@ /// Use aws_runtime::env_config::file::EnvConfigFiles instead. #[deprecated( - since = "1.1.10", + since = "1.1.11", note = "Use aws_runtime::env_config::file::EnvConfigFiles instead." )] pub type ProfileFiles = aws_runtime::env_config::file::EnvConfigFiles; /// Use aws_runtime::env_config::file::Builder instead. -#[deprecated(since = "1.1.10", note = "Use aws_runtime::env_config::file::Builder.")] +#[deprecated(since = "1.1.11", note = "Use aws_runtime::env_config::file::Builder.")] pub type Builder = aws_runtime::env_config::file::Builder; /// Use aws_runtime::env_config::file::EnvConfigFileKind instead. #[deprecated( - since = "1.1.10", + since = "1.1.11", note = "Use aws_runtime::env_config::file::EnvConfigFileKind." )] pub type ProfileFileKind = aws_runtime::env_config::file::EnvConfigFileKind; diff --git a/aws/rust-runtime/aws-runtime/src/env_config.rs b/aws/rust-runtime/aws-runtime/src/env_config.rs index bd522fe841..55c8e8ed1d 100644 --- a/aws/rust-runtime/aws-runtime/src/env_config.rs +++ b/aws/rust-runtime/aws-runtime/src/env_config.rs @@ -78,7 +78,7 @@ impl<'a> fmt::Display for Scope<'a> { pub struct EnvConfigSource<'a> { key: Cow<'a, str>, location: Location<'a>, - source: Scope<'a>, + scope: Scope<'a>, } impl<'a> EnvConfigSource<'a> { @@ -86,7 +86,7 @@ impl<'a> EnvConfigSource<'a> { Self { key, location: Location::Environment, - source: Scope::Global, + scope: Scope::Global, } } @@ -94,7 +94,7 @@ impl<'a> EnvConfigSource<'a> { Self { key, location: Location::Profile { name: profile_name }, - source: Scope::Global, + scope: Scope::Global, } } @@ -102,7 +102,7 @@ impl<'a> EnvConfigSource<'a> { Self { key, location: Location::Environment, - source: Scope::Service { service_id }, + scope: Scope::Service { service_id }, } } @@ -114,14 +114,14 @@ impl<'a> EnvConfigSource<'a> { Self { key, location: Location::Profile { name: profile_name }, - source: Scope::Service { service_id }, + scope: Scope::Service { service_id }, } } } impl<'a> fmt::Display for EnvConfigSource<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{} {} key: `{}`", self.source, self.location, self.key) + write!(f, "{} {} key: `{}`", self.scope, self.location, self.key) } } diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/customize/s3/S3Decorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/customize/s3/S3Decorator.kt index 8af54cfe6e..498a155a28 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/customize/s3/S3Decorator.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/customize/s3/S3Decorator.kt @@ -28,17 +28,21 @@ import software.amazon.smithy.rust.codegen.client.smithy.generators.OperationGen import software.amazon.smithy.rust.codegen.client.smithy.generators.OperationSection import software.amazon.smithy.rust.codegen.client.smithy.protocols.ClientRestXmlFactory import software.amazon.smithy.rust.codegen.core.rustlang.Writable +import software.amazon.smithy.rust.codegen.core.rustlang.rust import software.amazon.smithy.rust.codegen.core.rustlang.rustBlockTemplate import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate import software.amazon.smithy.rust.codegen.core.rustlang.writable import software.amazon.smithy.rust.codegen.core.smithy.CodegenContext import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType +import software.amazon.smithy.rust.codegen.core.smithy.customize.AdHocCustomization +import software.amazon.smithy.rust.codegen.core.smithy.customize.adhocCustomization import software.amazon.smithy.rust.codegen.core.smithy.protocols.ProtocolFunctions import software.amazon.smithy.rust.codegen.core.smithy.protocols.ProtocolMap import software.amazon.smithy.rust.codegen.core.smithy.protocols.RestXml import software.amazon.smithy.rust.codegen.core.smithy.traits.AllowInvalidXmlRoot import software.amazon.smithy.rust.codegen.core.util.hasTrait import software.amazon.smithy.rust.codegen.core.util.letIf +import software.amazon.smithy.rustsdk.SdkConfigSection import software.amazon.smithy.rustsdk.getBuiltIn import software.amazon.smithy.rustsdk.toWritable import java.util.logging.Logger @@ -154,6 +158,33 @@ class S3Decorator : ClientCodegenDecorator { } } + override fun extraSections(codegenContext: ClientCodegenContext): List { + return listOf( + adhocCustomization { section -> + rust( + """ + ${section.serviceConfigBuilder}.set_disable_multi_region_access_points( + ${section.sdkConfig} + .service_config() + .and_then(|conf| { + let str_config = conf.load_config(service_config_key("AWS_S3_DISABLE_MULTIREGION_ACCESS_POINTS", "s3_disable_multi_region_access_points")); + str_config.and_then(|it| it.parse::().ok()) + }), + ); + ${section.serviceConfigBuilder}.set_use_arn_region( + ${section.sdkConfig} + .service_config() + .and_then(|conf| { + let str_config = conf.load_config(service_config_key("AWS_S3_USE_ARN_REGION", "s3_use_arn_region")); + str_config.and_then(|it| it.parse::().ok()) + }), + ); + """, + ) + }, + ) + } + private fun isInInvalidXmlRootAllowList(shape: Shape): Boolean { return shape.isStructureShape && invalidXmlRootAllowList.contains(shape.id) } diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/customize/s3/S3ExpressDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/customize/s3/S3ExpressDecorator.kt index 435ae9aeb6..51ddef12da 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/customize/s3/S3ExpressDecorator.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/customize/s3/S3ExpressDecorator.kt @@ -97,22 +97,6 @@ class S3ExpressDecorator : ClientCodegenDecorator { str_config.and_then(|it| it.parse::().ok()) }), ); - ${section.serviceConfigBuilder}.set_disable_multi_region_access_points( - ${section.sdkConfig} - .service_config() - .and_then(|conf| { - let str_config = conf.load_config(service_config_key("AWS_S3_DISABLE_MULTIREGION_ACCESS_POINTS", "s3_disable_multi_region_access_points")); - str_config.and_then(|it| it.parse::().ok()) - }), - ); - ${section.serviceConfigBuilder}.set_use_arn_region( - ${section.sdkConfig} - .service_config() - .and_then(|conf| { - let str_config = conf.load_config(service_config_key("AWS_S3_USE_ARN_REGION", "s3_use_arn_region")); - str_config.and_then(|it| it.parse::().ok()) - }), - ); """, ) }, From d7744fa4b5dd9735c552b7fe555f99d03d686892 Mon Sep 17 00:00:00 2001 From: Zelda Hessler Date: Thu, 28 Mar 2024 12:37:52 -0500 Subject: [PATCH 19/23] undo change to parse_property_line (old one was better) --- .../aws-runtime/src/env_config/parse.rs | 14 +++- .../s3/tests/s3_env_config.rs | 77 +++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 aws/sdk/integration-tests/s3/tests/s3_env_config.rs diff --git a/aws/rust-runtime/aws-runtime/src/env_config/parse.rs b/aws/rust-runtime/aws-runtime/src/env_config/parse.rs index 54527917d8..05c68d3ab6 100644 --- a/aws/rust-runtime/aws-runtime/src/env_config/parse.rs +++ b/aws/rust-runtime/aws-runtime/src/env_config/parse.rs @@ -254,7 +254,19 @@ fn parse_property_line(line: &str) -> Result<(Cow<'_, str>, &str), PropertyError if k.is_empty() { return Err(PropertyError::NoName); } - Ok((k.to_ascii_lowercase().into(), v)) + // We don't want to blindly use `alloc::str::to_ascii_lowercase` because it + // always allocates. Instead, we check for uppercase ascii letters. Then, + // we only allocate in the case that there ARE letters that need to be + // lower-cased. + Ok((to_ascii_lowercase(k), v)) +} + +pub(crate) fn to_ascii_lowercase(s: &str) -> Cow<'_, str> { + if s.bytes().any(|b| b.is_ascii_uppercase()) { + Cow::Owned(s.to_ascii_lowercase()) + } else { + Cow::Borrowed(s) + } } /// Prepare a line for parsing diff --git a/aws/sdk/integration-tests/s3/tests/s3_env_config.rs b/aws/sdk/integration-tests/s3/tests/s3_env_config.rs new file mode 100644 index 0000000000..5d1dd7203d --- /dev/null +++ b/aws/sdk/integration-tests/s3/tests/s3_env_config.rs @@ -0,0 +1,77 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#[allow(deprecated)] +use aws_config::profile::profile_file::{ProfileFileKind, ProfileFiles}; +use aws_sdk_s3::config::BehaviorVersion; +use aws_sdk_s3::error::DisplayErrorContext; +use aws_sdk_s3::primitives::SdkBody; +use aws_sdk_s3::Client; +use aws_smithy_runtime::client::http::test_util::{ReplayEvent, StaticReplayClient}; +use aws_smithy_runtime::test_util::capture_test_logs::capture_test_logs; +use http::Uri; + +const NOT_LIKE_OTHER_SERVICE_SPECIFIC_CONFIGURATION: &str = r#"[default] +services = foo +s3_use_arn_region = false +region = us-east-1 +services = foo + +[services foo] +s3 = + s3_use_arn_region = true + region = us-west-2 +"#; + +#[tokio::test] +async fn s3_env_config_works() { + let (_guard, _logs_rx) = capture_test_logs(); + let http_client= StaticReplayClient::new(vec![ReplayEvent::new( + http::Request::builder() + .uri(Uri::from_static("https://kms.cn-north-1.amazonaws.com.cn/")) + .body(SdkBody::from(r#"{"NumberOfBytes":64}"#)).unwrap(), + http::Response::builder() + .status(http::StatusCode::from_u16(200).unwrap()) + .body(SdkBody::from(r#"{"Plaintext":"6CG0fbzzhg5G2VcFCPmJMJ8Njv3voYCgrGlp3+BZe7eDweCXgiyDH9BnkKvLmS7gQhnYDUlyES3fZVGwv5+CxA=="}"#)).unwrap()) + ]); + + let sdk_config = aws_config::defaults(BehaviorVersion::latest()) + .test_credentials() + .http_client(http_client) + .profile_files( + #[allow(deprecated)] + ProfileFiles::builder() + .with_contents( + #[allow(deprecated)] + ProfileFileKind::Config, + NOT_LIKE_OTHER_SERVICE_SPECIFIC_CONFIGURATION, + ) + .build(), + ) + .load() + .await; + + let client = Client::new(&sdk_config); + + let err = client + .get_object() + .bucket("arn:aws:s3-object-lambda:us-east-1:123412341234:accesspoint/myolap") + .key("s3.txt") + .send() + .await + .expect_err("should fail—cross region invalid arn"); + + let err = DisplayErrorContext(err); + panic!("{err:?}"); + + // assert!( + // format!("{:?}", err).contains( + // "Invalid configuration: region from ARN `us-east-1` \ + // does not match client region `us-west-2` and UseArnRegion is `false`" + // ), + // "{}", + // err + // ); +} From ebfab854cdb85feffb1c10daebac63368861f69c Mon Sep 17 00:00:00 2001 From: Zelda Hessler Date: Thu, 28 Mar 2024 13:03:23 -0500 Subject: [PATCH 20/23] remove unnecessary s3 env config test --- .../s3/tests/s3_env_config.rs | 77 ------------------- 1 file changed, 77 deletions(-) delete mode 100644 aws/sdk/integration-tests/s3/tests/s3_env_config.rs diff --git a/aws/sdk/integration-tests/s3/tests/s3_env_config.rs b/aws/sdk/integration-tests/s3/tests/s3_env_config.rs deleted file mode 100644 index 5d1dd7203d..0000000000 --- a/aws/sdk/integration-tests/s3/tests/s3_env_config.rs +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -#[allow(deprecated)] -use aws_config::profile::profile_file::{ProfileFileKind, ProfileFiles}; -use aws_sdk_s3::config::BehaviorVersion; -use aws_sdk_s3::error::DisplayErrorContext; -use aws_sdk_s3::primitives::SdkBody; -use aws_sdk_s3::Client; -use aws_smithy_runtime::client::http::test_util::{ReplayEvent, StaticReplayClient}; -use aws_smithy_runtime::test_util::capture_test_logs::capture_test_logs; -use http::Uri; - -const NOT_LIKE_OTHER_SERVICE_SPECIFIC_CONFIGURATION: &str = r#"[default] -services = foo -s3_use_arn_region = false -region = us-east-1 -services = foo - -[services foo] -s3 = - s3_use_arn_region = true - region = us-west-2 -"#; - -#[tokio::test] -async fn s3_env_config_works() { - let (_guard, _logs_rx) = capture_test_logs(); - let http_client= StaticReplayClient::new(vec![ReplayEvent::new( - http::Request::builder() - .uri(Uri::from_static("https://kms.cn-north-1.amazonaws.com.cn/")) - .body(SdkBody::from(r#"{"NumberOfBytes":64}"#)).unwrap(), - http::Response::builder() - .status(http::StatusCode::from_u16(200).unwrap()) - .body(SdkBody::from(r#"{"Plaintext":"6CG0fbzzhg5G2VcFCPmJMJ8Njv3voYCgrGlp3+BZe7eDweCXgiyDH9BnkKvLmS7gQhnYDUlyES3fZVGwv5+CxA=="}"#)).unwrap()) - ]); - - let sdk_config = aws_config::defaults(BehaviorVersion::latest()) - .test_credentials() - .http_client(http_client) - .profile_files( - #[allow(deprecated)] - ProfileFiles::builder() - .with_contents( - #[allow(deprecated)] - ProfileFileKind::Config, - NOT_LIKE_OTHER_SERVICE_SPECIFIC_CONFIGURATION, - ) - .build(), - ) - .load() - .await; - - let client = Client::new(&sdk_config); - - let err = client - .get_object() - .bucket("arn:aws:s3-object-lambda:us-east-1:123412341234:accesspoint/myolap") - .key("s3.txt") - .send() - .await - .expect_err("should fail—cross region invalid arn"); - - let err = DisplayErrorContext(err); - panic!("{err:?}"); - - // assert!( - // format!("{:?}", err).contains( - // "Invalid configuration: region from ARN `us-east-1` \ - // does not match client region `us-west-2` and UseArnRegion is `false`" - // ), - // "{}", - // err - // ); -} From 590c000f85baef0e3fb12a411023f33aa7d42b14 Mon Sep 17 00:00:00 2001 From: Zelda Hessler Date: Thu, 28 Mar 2024 15:31:02 -0500 Subject: [PATCH 21/23] add changelog entry --- CHANGELOG.next.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.next.toml b/CHANGELOG.next.toml index 39d5e0ea92..5cbb2efeb4 100644 --- a/CHANGELOG.next.toml +++ b/CHANGELOG.next.toml @@ -64,3 +64,9 @@ message = "Stalled stream protection on downloads will now only trigger if the u references = ["smithy-rs#3485"] meta = { "breaking" = false, "tada" = false, "bug" = true } author = "jdisanti" + +[[aws-sdk-rust]] +message = "Users may now set service-specific configuration in the environment. For more information, see [this discussion topic](https://github.com/smithy-lang/smithy-rs/discussions/3537)." +references = ["smithy-rs#3493"] +meta = { "breaking" = false, "tada" = true, "bug" = false } +author = "Velfi" From 631a3e8ad58c05b5ff99c913c870496258fbf732 Mon Sep 17 00:00:00 2001 From: Zelda Hessler Date: Fri, 29 Mar 2024 09:37:53 -0500 Subject: [PATCH 22/23] remove duplicated changelog entry --- CHANGELOG.next.toml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/CHANGELOG.next.toml b/CHANGELOG.next.toml index 5cbb2efeb4..7fae9074e1 100644 --- a/CHANGELOG.next.toml +++ b/CHANGELOG.next.toml @@ -59,12 +59,6 @@ references = ["smithy-rs#3485"] meta = { "breaking" = false, "tada" = false, "bug" = true } authors = ["jdisanti"] -[[aws-sdk-rust]] -message = "Stalled stream protection on downloads will now only trigger if the upstream source is too slow. Previously, stalled stream protection could be erroneously triggered if the user was slowly consuming the stream slower than the minimum speed limit." -references = ["smithy-rs#3485"] -meta = { "breaking" = false, "tada" = false, "bug" = true } -author = "jdisanti" - [[aws-sdk-rust]] message = "Users may now set service-specific configuration in the environment. For more information, see [this discussion topic](https://github.com/smithy-lang/smithy-rs/discussions/3537)." references = ["smithy-rs#3493"] From a8e3ac3cf077de1f04ec9dbc6e4593676d3b0689 Mon Sep 17 00:00:00 2001 From: Zelda Hessler Date: Fri, 29 Mar 2024 07:38:13 -0700 Subject: [PATCH 23/23] Update aws/rust-runtime/aws-runtime/src/env_config/file.rs Co-authored-by: ysaito1001 --- aws/rust-runtime/aws-runtime/src/env_config/file.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws/rust-runtime/aws-runtime/src/env_config/file.rs b/aws/rust-runtime/aws-runtime/src/env_config/file.rs index abce21075b..7057e5661b 100644 --- a/aws/rust-runtime/aws-runtime/src/env_config/file.rs +++ b/aws/rust-runtime/aws-runtime/src/env_config/file.rs @@ -146,7 +146,7 @@ impl Builder { /// Include the default SDK credentials file in the list of profile files to be loaded. /// - /// The default SDK config typically resides in `~/.aws/credentials`. When this flag is enabled, + /// The default SDK credentials typically reside in `~/.aws/credentials`. When this flag is enabled, /// this credentials file will be included in the profile files that get loaded in the built /// [`EnvConfigFiles`] file set. ///