From 5f7113f506f301296311ef637e40d226e0a6e548 Mon Sep 17 00:00:00 2001 From: Zelda Hessler Date: Mon, 1 Apr 2024 09:50:31 -0500 Subject: [PATCH] Sourcing service config from the environment. (#3493) ## Motivation and Context https://github.com/smithy-lang/smithy-rs/issues/2863 https://github.com/awslabs/aws-sdk-rust/issues/1060 ## Description This PR adds a new feature: the ability to source service-specific config from the environment. This is **only** supported when creating a service config from an `SdkConfig`. I've posted [a guide](https://github.com/smithy-lang/smithy-rs/discussions/3537) to our discussions board. [This also adds support for setting an endpoint URL in environment config.](https://github.com/smithy-lang/smithy-rs/issues/2863) ## Testing I have written several tests ensuring config is extracted with the correct precedence. ## Checklist - [x] I have updated `CHANGELOG.next.toml` if I made changes to the AWS SDK, generated SDK code, or SDK runtime crates ---- _By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice._ --------- Co-authored-by: John DiSanti Co-authored-by: ysaito1001 --- CHANGELOG.next.toml | 42 ++ aws/rust-runtime/aws-config/Cargo.toml | 9 +- .../aws-config/external-types.toml | 10 +- .../src/default_provider/app_name.rs | 30 +- .../src/default_provider/endpoint_url.rs | 18 +- .../ignore_configured_endpoint_urls.rs | 18 +- .../src/default_provider/retry_config.rs | 32 +- .../src/default_provider/use_dual_stack.rs | 25 +- .../src/default_provider/use_fips.rs | 18 +- .../aws-config/src/env_service_config.rs | 27 + aws/rust-runtime/aws-config/src/lib.rs | 87 ++- .../src/{profile/mod.rs => profile.rs} | 21 +- .../aws-config/src/profile/credentials.rs | 6 +- .../src/profile/credentials/repr.rs | 20 +- .../aws-config/src/profile/parser.rs | 352 +---------- .../aws-config/src/profile/parser/section.rs | 407 ------------- .../aws-config/src/profile/profile_file.rs | 264 +-------- .../aws-config/src/profile/region.rs | 3 + .../aws-config/src/profile/token.rs | 13 +- .../aws-config/src/provider_config.rs | 7 + aws/rust-runtime/aws-config/src/sso/cache.rs | 2 +- .../aws-config/src/standard_property.rs | 468 --------------- aws/rust-runtime/aws-runtime/Cargo.toml | 5 +- .../aws-runtime/src/env_config.rs | 548 ++++++++++++++++++ .../src/env_config}/error.rs | 30 +- .../aws-runtime/src/env_config/file.rs | 249 ++++++++ .../src/env_config}/normalize.rs | 109 ++-- .../src/env_config}/parse.rs | 44 +- .../aws-runtime/src/env_config/property.rs | 178 ++++++ .../aws-runtime/src/env_config/section.rs | 484 ++++++++++++++++ .../src/env_config}/source.rs | 82 +-- .../src/fs_util.rs | 19 +- aws/rust-runtime/aws-runtime/src/lib.rs | 7 + .../test-data/file-location-tests.json | 0 .../test-data/profile-parser-tests.json | 0 aws/rust-runtime/aws-types/Cargo.toml | 2 +- aws/rust-runtime/aws-types/src/lib.rs | 2 + aws/rust-runtime/aws-types/src/sdk_config.rs | 36 +- .../aws-types/src/service_config.rs | 146 +++++ .../smithy/rustsdk/AwsCodegenDecorator.kt | 1 + .../amazon/smithy/rustsdk/RegionDecorator.kt | 13 +- .../smithy/rustsdk/SdkConfigDecorator.kt | 14 +- .../rustsdk/ServiceEnvConfigDecorator.kt | 48 ++ .../rustsdk/customize/s3/S3Decorator.kt | 31 + .../customize/s3/S3ExpressDecorator.kt | 22 + aws/sdk/integration-tests/dynamodb/Cargo.toml | 1 + .../dynamodb/tests/shared-config.rs | 41 +- rust-runtime/aws-smithy-async/Cargo.toml | 2 +- .../aws-smithy-mocks-experimental/Cargo.toml | 2 +- rust-runtime/aws-smithy-wasm/Cargo.toml | 2 +- 50 files changed, 2280 insertions(+), 1717 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} (90%) delete mode 100644 aws/rust-runtime/aws-config/src/profile/parser/section.rs delete mode 100644 aws/rust-runtime/aws-config/src/standard_property.rs create mode 100644 aws/rust-runtime/aws-runtime/src/env_config.rs rename aws/rust-runtime/{aws-config/src/profile/parser => aws-runtime/src/env_config}/error.rs (57%) create mode 100644 aws/rust-runtime/aws-runtime/src/env_config/file.rs rename aws/rust-runtime/{aws-config/src/profile/parser => aws-runtime/src/env_config}/normalize.rs (83%) rename aws/rust-runtime/{aws-config/src/profile/parser => aws-runtime/src/env_config}/parse.rs (90%) create mode 100644 aws/rust-runtime/aws-runtime/src/env_config/property.rs create mode 100644 aws/rust-runtime/aws-runtime/src/env_config/section.rs rename aws/rust-runtime/{aws-config/src/profile/parser => aws-runtime/src/env_config}/source.rs (88%) rename aws/rust-runtime/{aws-config => aws-runtime}/src/fs_util.rs (77%) 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%) 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/CHANGELOG.next.toml b/CHANGELOG.next.toml index 12ef93cd27..7fae9074e1 100644 --- a/CHANGELOG.next.toml +++ b/CHANGELOG.next.toml @@ -22,3 +22,45 @@ message = "Make `BehaviorVersion` be future-proof by disallowing it to be constr references = ["aws-sdk-rust#1111", "smithy-rs#3513"] meta = { "breaking" = true, "tada" = false, "bug" = true, "target" = "client" } author = "Ten0" + +[[smithy-rs]] +message = """ +Stalled stream protection now supports request upload streams. It is currently off by default, but will be enabled by default in a future release. To enable it now, you can do the following: + +```rust +let config = my_service::Config::builder() + .stalled_stream_protection(StalledStreamProtectionConfig::enabled().build()) + // ... + .build(); +``` +""" +references = ["smithy-rs#3485"] +meta = { "breaking" = false, "tada" = true, "bug" = false } +authors = ["jdisanti"] + +[[aws-sdk-rust]] +message = """ +Stalled stream protection now supports request upload streams. It is currently off by default, but will be enabled by default in a future release. To enable it now, you can do the following: + +```rust +let config = aws_config::defaults(BehaviorVersion::latest()) + .stalled_stream_protection(StalledStreamProtectionConfig::enabled().build()) + .load() + .await; +``` +""" +references = ["smithy-rs#3485"] +meta = { "breaking" = false, "tada" = true, "bug" = false } +author = "jdisanti" + +[[smithy-rs]] +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 } +authors = ["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" 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/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", 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..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 @@ -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, @@ -87,6 +89,7 @@ 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}; @@ -123,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 eaae3f2a68..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 @@ -5,7 +5,7 @@ use crate::environment::parse_url; 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 endpoint_url_provider(provider_config: &ProviderConfig) -> Option { - StandardProperty::new() + let env = provider_config.env(); + let profiles = provider_config.profile().await; + + EnvConfigValue::new() .env(env::ENDPOINT_URL) .profile(profile_key::ENDPOINT_URL) - .validate(provider_config, parse_url) - .await + .validate(&env, profiles, parse_url) .map_err( |err| tracing::warn!(err = %DisplayErrorContext(&err), "invalid value for endpoint URL setting"), ) @@ -39,6 +41,7 @@ pub async fn endpoint_url_provider(provider_config: &ProviderConfig) -> Option Option { - StandardProperty::new() + let env = provider_config.env(); + let profiles = provider_config.profile().await; + + EnvConfigValue::new() .env(env::IGNORE_CONFIGURED_ENDPOINT_URLS) .profile(profile_key::IGNORE_CONFIGURED_ENDPOINT_URLS) - .validate(provider_config, parse_bool) - .await + .validate(&env, profiles, parse_bool) .map_err( |err| tracing::warn!(err = %DisplayErrorContext(&err), "invalid value for 'ignore configured endpoint URLs' setting"), ) @@ -41,6 +43,7 @@ pub async fn ignore_configured_endpoint_urls_provider( mod test { use super::env; use super::ignore_configured_endpoint_urls_provider; + #[allow(deprecated)] use crate::profile::profile_file::{ProfileFileKind, ProfileFiles}; use crate::provider_config::ProviderConfig; use aws_types::os_shim_internal::{Env, Fs}; @@ -70,8 +73,13 @@ mod test { )])) .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/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..5004938c08 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"), ) @@ -31,6 +33,7 @@ pub(crate) async fn use_dual_stack_provider(provider_config: &ProviderConfig) -> #[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_types::os_shim_internal::{Env, Fs}; @@ -55,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, @@ -74,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 0bb824d593..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 @@ -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"), ) @@ -38,6 +40,7 @@ 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_types::os_shim_internal::{Env, Fs}; @@ -62,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 new file mode 100644 index 0000000000..00c67a1275 --- /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::section::EnvConfigSections; +use aws_runtime::env_config::EnvConfigValue; +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) env_config_sections: EnvConfigSections, +} + +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.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 d5be002d6d..b97178be47 100644 --- a/aws/rust-runtime/aws-config/src/lib.rs +++ b/aws/rust-runtime/aws-config/src/lib.rs @@ -118,16 +118,15 @@ 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; pub mod ecs; +mod env_service_config; pub mod environment; pub mod imds; pub mod meta; @@ -138,7 +137,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; @@ -208,6 +206,7 @@ pub async fn load_defaults(version: BehaviorVersion) -> SdkConfig { } mod loader { + use crate::env_service_config::EnvServiceConfig; use aws_credential_types::provider::{ token::{ProvideToken, SharedTokenProvider}, ProvideCredentials, SharedCredentialsProvider, @@ -233,6 +232,7 @@ 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; @@ -267,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, @@ -571,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 @@ -807,11 +809,17 @@ mod loader { } }; + let profiles = conf.profile().await; + let service_config = EnvServiceConfig { + env: conf.env(), + env_config_sections: 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); // If an endpoint URL is set programmatically, then our work is done. let endpoint_url = if self.endpoint_url.is_some() { @@ -866,6 +874,7 @@ 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; @@ -873,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; #[tokio::test] - #[traced_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"), @@ -897,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() @@ -916,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 { @@ -1016,8 +1027,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() @@ -1029,8 +1045,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() @@ -1052,8 +1073,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() @@ -1080,8 +1106,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/mod.rs b/aws/rust-runtime/aws-config/src/profile.rs similarity index 90% rename from aws/rust-runtime/aws-config/src/profile/mod.rs rename to aws/rust-runtime/aws-config/src/profile.rs index 51c790c717..5b77b99f9b 100644 --- a/aws/rust-runtime/aws-config/src/profile/mod.rs +++ b/aws/rust-runtime/aws-config/src/profile.rs @@ -8,16 +8,7 @@ //! AWS profiles are typically stored in `~/.aws/config` and `~/.aws/credentials`. For more details //! see the [`load`] function. -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 parser; pub mod credentials; pub mod profile_file; @@ -29,9 +20,19 @@ pub mod token; #[doc(inline)] pub use token::ProfileFileTokenProvider; +#[doc(inline)] +pub use aws_runtime::env_config::error::EnvConfigFileLoadError as ProfileFileLoadError; +#[doc(inline)] +pub use aws_runtime::env_config::parse::EnvConfigParseError as ProfileParseError; +#[doc(inline)] +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; +#[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 9d8b4255b0..f4247531bd 100644 --- a/aws/rust-runtime/aws-config/src/profile/credentials.rs +++ b/aws/rust-runtime/aws-config/src/profile/credentials.rs @@ -22,9 +22,11 @@ //! - `exec` which contains a chain representation of providers to implement passing bootstrapped credentials //! 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::{cell::ErrorTakingOnceCell, parser::ProfileFileLoadError}; +use crate::profile::ProfileFileLoadError; use crate::provider_config::ProviderConfig; use aws_credential_types::{ provider::{self, error::CredentialsError, future, ProvideCredentials}, @@ -376,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>, } @@ -443,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 9f5ff001bd..2d14ab76f6 100644 --- a/aws/rust-runtime/aws-config/src/profile/credentials/repr.rs +++ b/aws/rust-runtime/aws-config/src/profile/credentials/repr.rs @@ -469,18 +469,16 @@ 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; 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 +487,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, @@ -514,6 +515,7 @@ mod tests { } } + #[cfg(feature = "test-utils")] #[derive(Deserialize)] struct TestCase { docs: String, @@ -521,6 +523,7 @@ mod tests { output: TestOutput, } + #[cfg(feature = "test-utils")] #[derive(Deserialize)] struct TestInput { profiles: HashMap>, @@ -529,6 +532,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/parser.rs b/aws/rust-runtime/aws-config/src/profile/parser.rs index 8727b1eef4..a2b4d4371a 100644 --- a/aws/rust-runtime/aws-config/src/profile/parser.rs +++ b/aws/rust-runtime/aws-config/src/profile/parser.rs @@ -3,27 +3,17 @@ * 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::env_config::file::EnvConfigFiles as ProfileFiles; +use aws_runtime::env_config::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::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 /// @@ -73,329 +63,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/parser/section.rs b/aws/rust-runtime/aws-config/src/profile/parser/section.rs deleted file mode 100644 index 2850c77df5..0000000000 --- a/aws/rust-runtime/aws-config/src/profile/parser/section.rs +++ /dev/null @@ -1,407 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -use crate::profile::parser::parse::to_ascii_lowercase; -use std::collections::HashMap; -use std::fmt; - -/// Key-Value property pair -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct Property { - key: String, - value: String, -} - -impl Property { - /// Value of this property - pub fn value(&self) -> &str { - &self.value - } - - /// Name of this property - pub fn key(&self) -> &str { - &self.key - } - - /// Creates a new property - pub fn new(key: String, value: String) -> Self { - Property { key, value } - } -} - -type SectionKey = String; -type SectionName = String; -type PropertyName = String; -type SubPropertyName = String; -type PropertyValue = String; - -// [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 { - section_key: SectionKey, - section_name: SectionName, - property_name: PropertyName, - sub_property_name: Option, -} - -impl PropertiesKey { - pub(crate) fn builder() -> PropertiesKeyBuilder { - Default::default() - } -} - -impl fmt::Display for PropertiesKey { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let PropertiesKey { - section_key, - section_name, - property_name, - sub_property_name, - } = self; - match sub_property_name { - Some(sub_property_name) => { - write!( - f, - "[{section_key} {section_name}].{property_name}.{sub_property_name}" - ) - } - None => { - write!(f, "[{section_key} {section_name}].{property_name}") - } - } - } -} - -#[derive(Default)] -pub(crate) struct PropertiesKeyBuilder { - section_key: Option, - section_name: Option, - property_name: Option, - sub_property_name: Option, -} - -impl PropertiesKeyBuilder { - pub(crate) 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 { - self.section_name = Some(section_name.into()); - self - } - - pub(crate) 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 { - self.sub_property_name = Some(sub_property_name.into()); - self - } - - pub(crate) fn build(self) -> Result { - Ok(PropertiesKey { - section_key: self - .section_key - .ok_or("A section_key is required".to_owned())?, - section_name: self - .section_name - .ok_or("A section_name is required".to_owned())?, - property_name: self - .property_name - .ok_or("A property_name is required".to_owned())?, - sub_property_name: self.sub_property_name, - }) - } -} - -#[allow(clippy::type_complexity)] -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub(crate) struct Properties { - inner: HashMap, -} - -#[allow(dead_code)] -impl Properties { - pub(crate) fn new() -> Self { - Default::default() - } - - pub(crate) 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. - .entry(properties_key.clone()) - .and_modify(|v| { - tracing::trace!("overwriting {properties_key}: was {v}, now {value}"); - *v = value.clone(); - }) - .or_insert(value); - } - - pub(crate) fn get(&self, properties_key: &PropertiesKey) -> Option<&PropertyValue> { - 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(to_ascii_lowercase(name).as_ref()) - .map(|prop| prop.value()) - } - - fn is_empty(&self) -> bool { - self.properties.is_empty() - } - - fn insert(&mut self, name: String, value: Property) { - self.properties - .insert(to_ascii_lowercase(&name).into(), 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(crate) 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(crate) 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) - } -} - -#[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") - ); - } -} 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..c99bd4f16c 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,22 @@ * 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(); - } -} +//! Re-exports for types since moved to the aws-runtime crate. + +/// Use aws_runtime::env_config::file::EnvConfigFiles instead. +#[deprecated( + 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.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.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-config/src/profile/region.rs b/aws/rust-runtime/aws-config/src/profile/region.rs index a293733377..34a3fffaca 100644 --- a/aws/rust-runtime/aws-config/src/profile/region.rs +++ b/aws/rust-runtime/aws-config/src/profile/region.rs @@ -6,6 +6,7 @@ //! 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; @@ -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 1865e65d7f..2e33307988 100644 --- a/aws/rust-runtime/aws-config/src/profile/token.rs +++ b/aws/rust-runtime/aws-config/src/profile/token.rs @@ -5,11 +5,12 @@ //! Profile File Based Token Providers -use crate::{ - profile::{cell::ErrorTakingOnceCell, profile_file::ProfileFiles, ProfileSet}, - provider_config::ProviderConfig, - sso::SsoTokenProvider, -}; +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, }; @@ -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 f7865a3d7a..569af53fcb 100644 --- a/aws/rust-runtime/aws-config/src/provider_config.rs +++ b/aws/rust-runtime/aws-config/src/provider_config.rs @@ -6,6 +6,7 @@ //! Configuration Options for Credential Providers use crate::profile; +#[allow(deprecated)] use crate::profile::profile_file::ProfileFiles; use crate::profile::{ProfileFileLoadError, ProfileSet}; use aws_smithy_async::rt::sleep::{default_async_sleep, AsyncSleep, SharedAsyncSleep}; @@ -44,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>, @@ -77,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, } @@ -97,6 +100,7 @@ impl ProviderConfig { let env = Env::from_slice(&[]); Self { parsed_profile: Default::default(), + #[allow(deprecated)] profile_files: ProfileFiles::default(), env, fs, @@ -149,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, } @@ -161,6 +166,7 @@ impl ProviderConfig { ) -> Self { Self { parsed_profile: Default::default(), + #[allow(deprecated)] profile_files: ProfileFiles::default(), env: Env::default(), fs: Fs::default(), @@ -293,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-config/src/sso/cache.rs b/aws/rust-runtime/aws-config/src/sso/cache.rs index 3955d1b3aa..2aa34024b4 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-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..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" @@ -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..55c8e8ed1d --- /dev/null +++ b/aws/rust-runtime/aws-runtime/src/env_config.rs @@ -0,0 +1,548 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * 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; + +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 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> { + 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}`)"), + } + } +} + +/// 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>, + location: Location<'a>, + scope: Scope<'a>, +} + +impl<'a> EnvConfigSource<'a> { + pub(crate) fn global_from_env(key: Cow<'a, str>) -> Self { + Self { + key, + location: Location::Environment, + scope: 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 }, + scope: Scope::Global, + } + } + + pub(crate) fn service_from_env(key: Cow<'a, str>, service_id: Cow<'a, str>) -> Self { + Self { + key, + location: Location::Environment, + scope: 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 }, + 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.scope, 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. +#[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<&EnvConfigSections>, + 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 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 + 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: &EnvConfigSections, + 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(' ', "-") +} + +#[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 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 = EnvConfigSections::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 = EnvConfigSections::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 = EnvConfigSections::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".into(), + ), + ( + new_prop_key("services", "dev-wrong", "ec2", Some("some_key")), + "999".into(), + ), + ( + new_prop_key("services", "dev", "s3", Some("some_key")), + "5".into(), + ), + ( + new_prop_key("services", "dev", "ec2", Some("some_key")), + "6".into(), + ), + ]), + ); + 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-config/src/profile/parser/error.rs b/aws/rust-runtime/aws-runtime/src/env_config/error.rs similarity index 57% rename from aws/rust-runtime/aws-config/src/profile/parser/error.rs rename to aws/rust-runtime/aws-runtime/src/env_config/error.rs index 1351c242f8..d187132ca8 100644 --- a/aws/rust-runtime/aws-config/src/profile/parser/error.rs +++ b/aws/rust-runtime/aws-runtime/src/env_config/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::env_config::parse::EnvConfigParseError; use std::error::Error; use std::fmt::{Display, Formatter}; use std::path::PathBuf; @@ -11,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/env_config/file.rs b/aws/rust-runtime/aws-runtime/src/env_config/file.rs new file mode 100644 index 0000000000..7057e5661b --- /dev/null +++ b/aws/rust-runtime/aws-runtime/src/env_config/file.rs @@ -0,0 +1,249 @@ +/* + * 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 `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 `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. +/// +/// # Example: Using a custom profile file path +/// +/// ```no_run,ignore +/// use aws_runtime::env_config::file::{EnvConfigFiles, SharedConfigFileKind}; +/// use std::sync::Arc; +/// +/// # async fn example() { +/// 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) +/// .load() +/// .await; +/// # } +/// ``` +#[derive(Clone, Debug)] +pub struct EnvConfigFiles { + pub(crate) files: Vec, +} + +impl EnvConfigFiles { + /// Returns a builder to create `EnvConfigFiles` + pub fn builder() -> Builder { + Builder::new() + } +} + +impl Default for EnvConfigFiles { + fn default() -> Self { + Self { + files: vec![ + EnvConfigFile::Default(EnvConfigFileKind::Config), + EnvConfigFile::Default(EnvConfigFileKind::Credentials), + ], + } + } +} + +/// Profile file type (config or credentials) +#[derive(Copy, Clone, Debug)] +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 EnvConfigFileKind { + pub(crate) fn default_path(&self) -> &'static str { + match &self { + EnvConfigFileKind::Credentials => "~/.aws/credentials", + EnvConfigFileKind::Config => "~/.aws/config", + } + } + + pub(crate) fn override_environment_variable(&self) -> &'static str { + match &self { + EnvConfigFileKind::Config => "AWS_CONFIG_FILE", + EnvConfigFileKind::Credentials => "AWS_SHARED_CREDENTIALS_FILE", + } + } +} + +/// A single config file within a [`EnvConfigFiles`] file set. +#[derive(Clone)] +pub(crate) enum EnvConfigFile { + /// One of the default profile files (config or credentials in their default locations) + Default(EnvConfigFileKind), + /// A profile file at a custom location + FilePath { + kind: EnvConfigFileKind, + path: PathBuf, + }, + /// The direct contents of a profile file + FileContents { + kind: EnvConfigFileKind, + contents: String, + }, +} + +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(), + 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 [`EnvConfigFiles`]. +#[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 + /// [`EnvConfigFiles`] file set. + /// + /// 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 + } + + /// Include the default SDK credentials file in the list of profile files to be loaded. + /// + /// 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. + /// + /// 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, + ) -> 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 [`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(), + }); + 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 [`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 [`EnvConfigFiles`] file set. + pub fn build(self) -> EnvConfigFiles { + let mut files = self.custom_sources; + if self.with_credentials { + files.insert(0, EnvConfigFile::Default(EnvConfigFileKind::Credentials)); + } + if self.with_config { + files.insert(0, EnvConfigFile::Default(EnvConfigFileKind::Config)); + } + if files.is_empty() { + panic!("At least one profile file must be included in the `EnvConfigFiles` file set."); + } + EnvConfigFiles { files } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn redact_file_contents_in_profile_file_debug() { + let shared_config_file = EnvConfigFile::FileContents { + kind: EnvConfigFileKind::Config, + contents: "sensitive_contents".into(), + }; + let debug = format!("{shared_config_file:?}"); + assert!(!debug.contains("sensitive_contents")); + assert!(debug.contains("** redacted **")); + } + + #[test] + fn build_correctly_orders_default_config_credentials() { + 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, shared_config_files.files.len()); + assert!(matches!( + shared_config_files.files[0], + EnvConfigFile::Default(EnvConfigFileKind::Config) + )); + assert!(matches!( + shared_config_files.files[1], + EnvConfigFile::Default(EnvConfigFileKind::Credentials) + )); + assert!(matches!( + shared_config_files.files[2], + EnvConfigFile::FilePath { + kind: EnvConfigFileKind::Config, + path: _ + } + )); + } + + #[test] + #[should_panic] + fn empty_builder_panics() { + EnvConfigFiles::builder().build(); + } +} diff --git a/aws/rust-runtime/aws-config/src/profile/parser/normalize.rs b/aws/rust-runtime/aws-runtime/src/env_config/normalize.rs similarity index 83% rename from aws/rust-runtime/aws-config/src/profile/parser/normalize.rs rename to aws/rust-runtime/aws-runtime/src/env_config/normalize.rs index 2e93bc03b4..5e19edbb6e 100644 --- a/aws/rust-runtime/aws-config/src/profile/parser/normalize.rs +++ b/aws/rust-runtime/aws-runtime/src/env_config/normalize.rs @@ -3,29 +3,25 @@ * SPDX-License-Identifier: Apache-2.0 */ -use crate::profile::parser::{ - parse::{RawProfileSet, WHITESPACE}, - Section, SsoSession, -}; -use crate::profile::profile_file::ProfileFileKind; -use crate::profile::{Profile, ProfileSet, Property}; +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; -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(), }, @@ -56,9 +52,9 @@ impl<'a> SectionKey<'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) @@ -74,7 +70,7 @@ impl<'a> SectionKey<'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`")) @@ -104,15 +100,15 @@ impl<'a> SectionKey<'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 .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 @@ -234,11 +230,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 EnvConfigSections::default(), + profile, + EnvConfigFileKind::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/env_config/parse.rs similarity index 90% rename from aws/rust-runtime/aws-config/src/profile/parser/parse.rs rename to aws/rust-runtime/aws-runtime/src/env_config/parse.rs index f0f48f8f49..05c68d3ab6 100644 --- a/aws/rust-runtime/aws-config/src/profile/parser/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::parser::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), }, @@ -254,6 +254,10 @@ fn parse_property_line(line: &str) -> Result<(Cow<'_, str>, &str), PropertyError if k.is_empty() { return Err(PropertyError::NoName); } + // 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)) } @@ -296,9 +300,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::profile_file::ProfileFileKind; + 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 @@ -346,7 +350,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/env_config/property.rs b/aws/rust-runtime/aws-runtime/src/env_config/property.rs new file mode 100644 index 0000000000..f83bfbcdf8 --- /dev/null +++ b/aws/rust-runtime/aws-runtime/src/env_config/property.rs @@ -0,0 +1,178 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +//! Sections within an AWS config profile. + +use std::collections::HashMap; +use std::fmt; + +/// Key-Value property pair +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Property { + key: String, + value: String, +} + +impl Property { + /// Value of this property + pub fn value(&self) -> &str { + &self.value + } + + /// Name of this property + pub fn key(&self) -> &str { + &self.key + } + + /// Creates a new property + pub fn new(key: String, value: String) -> Self { + Property { key, value } + } +} + +type SectionKey = String; +type SectionName = String; +type PropertyName = String; +type SubPropertyName = String; +type PropertyValue = String; + +/// 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 struct PropertiesKey { + section_key: SectionKey, + section_name: SectionName, + property_name: PropertyName, + sub_property_name: Option, +} + +impl PropertiesKey { + /// Create a new [`PropertiesKeyBuilder`]. + pub fn builder() -> PropertiesKeyBuilder { + Default::default() + } +} + +impl fmt::Display for PropertiesKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let PropertiesKey { + section_key, + section_name, + property_name, + sub_property_name, + } = self; + match sub_property_name { + Some(sub_property_name) => { + write!( + f, + "[{section_key} {section_name}].{property_name}.{sub_property_name}" + ) + } + None => { + write!(f, "[{section_key} {section_name}].{property_name}") + } + } + } +} + +/// Builder for [`PropertiesKey`]s. +#[derive(Debug, Default)] +pub struct PropertiesKeyBuilder { + section_key: Option, + section_name: Option, + property_name: Option, + sub_property_name: Option, +} + +impl PropertiesKeyBuilder { + /// 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 + } + + /// 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 + } + + /// 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 + } + + /// 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 + } + + /// 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 + .ok_or("A section_key is required".to_owned())?, + section_name: self + .section_name + .ok_or("A section_name is required".to_owned())?, + property_name: self + .property_name + .ok_or("A property_name is required".to_owned())?, + sub_property_name: self.sub_property_name, + }) + } +} + +/// A map of [`PropertiesKey`]s to property values. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Properties { + inner: HashMap, +} + +#[allow(dead_code)] +impl Properties { + /// Create a new empty [`Properties`]. + pub fn new() -> Self { + Default::default() + } + + #[cfg(test)] + 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()); + } + properties + } + + /// 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. + .entry(properties_key.clone()) + .and_modify(|v| { + tracing::trace!("overwriting {properties_key}: was {v}, now {value}"); + *v = value.clone(); + }) + .or_insert(value); + } + + /// Given a [`PropertiesKey`], return the corresponding value, if any. + pub fn get(&self, properties_key: &PropertiesKey) -> Option<&PropertyValue> { + self.inner.get(properties_key) + } +} diff --git a/aws/rust-runtime/aws-runtime/src/env_config/section.rs b/aws/rust-runtime/aws-runtime/src/env_config/section.rs new file mode 100644 index 0000000000..8a14b95e64 --- /dev/null +++ b/aws/rust-runtime/aws-runtime/src/env_config/section.rs @@ -0,0 +1,484 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +//! Sections within an AWS config profile. + +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 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 EnvConfigSections { + fn default() -> Self { + Self { + profiles: Default::default(), + selected_profile: "default".into(), + sso_sessions: Default::default(), + other_sections: Default::default(), + } + } +} + +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. + #[cfg(test)] + pub fn new( + profiles: HashMap>, + selected_profile: impl Into>, + sso_sessions: HashMap>, + other_sections: Properties, + ) -> Self { + let mut base = EnvConfigSections { + selected_profile: selected_profile.into(), + ..Default::default() + }; + 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.other_sections = other_sections; + 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 `EnvConfigSections`. + pub fn parse(source: Source) -> Result { + let mut base = EnvConfigSections { + selected_profile: source.profile, + ..Default::default() + }; + + for file in source.files { + normalize::merge_in(&mut base, parse_profile_file(&file)?, file.kind); + } + Ok(base) + } +} + +#[cfg(test)] +mod test { + 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; + 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 = EnvConfigSections::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 = EnvConfigSections::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: EnvConfigFileKind::Config, + path: Some("~/.aws/config".to_string()), + contents: conf.unwrap_or_default().to_string(), + }, + File { + kind: EnvConfigFileKind::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 _ = EnvConfigSections::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: 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 _)), + } + } + 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: EnvConfigFileKind::Config, + path: Some("~/.aws/config".to_string()), + contents: input.config_file.unwrap_or_default(), + }, + File { + kind: EnvConfigFileKind::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 = EnvConfigSections::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/source.rs b/aws/rust-runtime/aws-runtime/src/env_config/source.rs similarity index 88% rename from aws/rust-runtime/aws-config/src/profile/parser/source.rs rename to aws/rust-runtime/aws-runtime/src/env_config/source.rs index 26dc4e1298..8661b93826 100644 --- a/aws/rust-runtime/aws-config/src/profile/parser/source.rs +++ b/aws/rust-runtime/aws-runtime/src/env_config/source.rs @@ -3,11 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -use crate::fs_util::{home_dir, Os}; - -use super::error::{CouldNotReadProfileFile, ProfileFileLoadError}; -use crate::profile::profile_file::{ProfileFile, ProfileFileKind, ProfileFiles}; +//! Code for handling in-memory sources of profile data +use super::error::{CouldNotReadConfigFile, EnvConfigFileLoadError}; +use crate::env_config::file::{EnvConfigFile, EnvConfigFileKind, EnvConfigFiles}; +use crate::fs_util::{home_dir, Os}; use aws_smithy_types::error::display::DisplayErrorContext; use aws_types::os_shim_internal; use std::borrow::Cow; @@ -15,35 +15,36 @@ 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"; +#[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: EnvConfigFileKind, + 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, -) -> Result { + profile_files: &EnvConfigFiles, +) -> Result { let home = home_dir(proc_env, Os::real()); let mut files = Vec::new(); @@ -85,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))) @@ -126,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), }, @@ -144,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 { @@ -198,13 +199,13 @@ fn expand_home( #[cfg(test)] mod tests { - use crate::profile::parser::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 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; @@ -276,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, @@ -292,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, @@ -381,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()); @@ -404,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 +442,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 +464,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 +481,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-config/src/fs_util.rs b/aws/rust-runtime/aws-runtime/src/fs_util.rs similarity index 77% rename from aws/rust-runtime/aws-config/src/fs_util.rs rename to aws/rust-runtime/aws-runtime/src/fs_util.rs index 89af426d89..7fe4f4ccd9 100644 --- a/aws/rust-runtime/aws-config/src/fs_util.rs +++ b/aws/rust-runtime/aws-runtime/src/fs_util.rs @@ -5,23 +5,28 @@ use aws_types::os_shim_internal; +/// An operating system, like Windows or Linux #[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub(crate) enum Os { +#[non_exhaustive] +pub enum Os { + /// A Windows-based operating system Windows, - NotWindows, + /// Any Unix-based operating system + Unix, } impl Os { - pub(crate) fn real() -> Self { + /// Returns the current operating system + 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); @@ -46,7 +51,7 @@ pub(crate) fn home_dir(env_var: &os_shim_internal::Env, os: Os) -> Option", "Russell Cohen "] description = "Cross-service types for the AWS SDK." edition = "2021" 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..a8f0071f68 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| Arc::new(it) as Arc); + 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, } } } @@ -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() + } + /// Config builder /// /// _Important:_ Using the `aws-config` crate to configure the SDK is preferred to invoking this @@ -807,6 +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, } } } 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..99faa7d50c --- /dev/null +++ b/aws/rust-runtime/aws-types/src/service_config.rs @@ -0,0 +1,146 @@ +/* + * 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)?, + }) + } + } + + #[allow(clippy::enum_variant_names)] + #[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..c8646b5e64 --- /dev/null +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/ServiceEnvConfigDecorator.kt @@ -0,0 +1,48 @@ +/* + * 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.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 +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) { + Attribute.AllowDeadCode.render(this) + 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-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 9866173bff..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 @@ -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 import software.amazon.smithy.rustsdk.SigV4AuthDecorator class S3ExpressDecorator : ClientCodegenDecorator { @@ -80,6 +83,25 @@ 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()) + }), + ); + """, + ) + }, + ) + } } private class S3ExpressServiceRuntimePluginCustomization(codegenContext: ClientCodegenContext) : 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 02786fe0af..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,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -use aws_sdk_dynamodb::config::{Credentials, Region, StalledStreamProtectionConfig}; +use aws_runtime::env_config::file::{EnvConfigFileKind, EnvConfigFiles}; +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( + EnvConfigFiles::builder() + .with_contents(EnvConfigFileKind::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") + ); +} 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>",