diff --git a/CHANGELOG.next.toml b/CHANGELOG.next.toml index cfeb53879e..0faa2266e8 100644 --- a/CHANGELOG.next.toml +++ b/CHANGELOG.next.toml @@ -450,3 +450,21 @@ message = """ references = ["smithy-rs#3076"] meta = { "breaking" = true, "tada" = false, "bug" = false, "target" = "all" } author = "ysaito1001" + +[[aws-sdk-rust]] +message = "**This change has [detailed upgrade guidance](https://github.com/awslabs/aws-sdk-rust/discussions/923).**

The AWS credentials cache has been replaced with a more generic identity cache." +references = ["smithy-rs#3077"] +meta = { "breaking" = true, "tada" = false, "bug" = false } +author = "jdisanti" + +[[smithy-rs]] +message = "**Behavior Break!** Identities for auth are now cached by default. See the `Config` builder's `identity_cache()` method docs for an example of how to disable this caching." +references = ["smithy-rs#3077"] +meta = { "breaking" = true, "tada" = false, "bug" = false, "target" = "client" } +author = "jdisanti" + +[[smithy-rs]] +message = "Clients now have a default async sleep implementation so that one does not need to be specified if you're using Tokio." +references = ["smithy-rs#3071"] +meta = { "breaking" = false, "tada" = true, "bug" = false, "target" = "client" } +author = "jdisanti" diff --git a/aws/rust-runtime/aws-config/external-types.toml b/aws/rust-runtime/aws-config/external-types.toml index c6328580b0..e1593172a6 100644 --- a/aws/rust-runtime/aws-config/external-types.toml +++ b/aws/rust-runtime/aws-config/external-types.toml @@ -12,15 +12,18 @@ allowed_external_types = [ "aws_smithy_async::rt::sleep::SharedAsyncSleep", "aws_smithy_async::time::SharedTimeSource", "aws_smithy_async::time::TimeSource", - "aws_smithy_types::body::SdkBody", "aws_smithy_http::endpoint", "aws_smithy_http::endpoint::error::InvalidEndpointError", "aws_smithy_http::result::SdkError", + "aws_smithy_runtime::client::identity::cache::IdentityCache", + "aws_smithy_runtime::client::identity::cache::lazy::LazyCacheBuilder", "aws_smithy_runtime_api::client::dns::ResolveDns", "aws_smithy_runtime_api::client::dns::SharedDnsResolver", "aws_smithy_runtime_api::client::http::HttpClient", "aws_smithy_runtime_api::client::http::SharedHttpClient", + "aws_smithy_runtime_api::client::identity::ResolveCachedIdentity", "aws_smithy_runtime_api::client::identity::ResolveIdentity", + "aws_smithy_types::body::SdkBody", "aws_smithy_types::retry", "aws_smithy_types::retry::*", "aws_smithy_types::timeout", diff --git a/aws/rust-runtime/aws-config/src/imds/client.rs b/aws/rust-runtime/aws-config/src/imds/client.rs index e956193e38..fd2315a316 100644 --- a/aws/rust-runtime/aws-config/src/imds/client.rs +++ b/aws/rust-runtime/aws-config/src/imds/client.rs @@ -433,7 +433,6 @@ impl Builder { .runtime_plugin(common_plugin.clone()) .runtime_plugin(TokenRuntimePlugin::new( common_plugin, - config.time_source(), self.token_ttl.unwrap_or(DEFAULT_TOKEN_TTL), )) .with_connection_poisoning() @@ -748,6 +747,7 @@ pub(crate) mod test { /// Tokens are refreshed up to 120 seconds early to avoid using an expired token. #[tokio::test] async fn token_refresh_buffer() { + let _logs = capture_test_logs(); let (_, http_client) = mock_imds_client(vec![ ReplayEvent::new( token_request("http://[fd00:ec2::254]", 600), @@ -785,11 +785,14 @@ pub(crate) mod test { .token_ttl(Duration::from_secs(600)) .build(); + tracing::info!("resp1 -----------------------------------------------------------"); let resp1 = client.get("/latest/metadata").await.expect("success"); // now the cached credential has expired time_source.advance(Duration::from_secs(400)); + tracing::info!("resp2 -----------------------------------------------------------"); let resp2 = client.get("/latest/metadata").await.expect("success"); time_source.advance(Duration::from_secs(150)); + tracing::info!("resp3 -----------------------------------------------------------"); let resp3 = client.get("/latest/metadata").await.expect("success"); http_client.assert_requests_match(&[]); assert_eq!("test-imds-output1", resp1.as_ref()); diff --git a/aws/rust-runtime/aws-config/src/imds/client/token.rs b/aws/rust-runtime/aws-config/src/imds/client/token.rs index 57c0aeddd1..d91fa3b89f 100644 --- a/aws/rust-runtime/aws-config/src/imds/client/token.rs +++ b/aws/rust-runtime/aws-config/src/imds/client/token.rs @@ -14,10 +14,11 @@ //! - Retry token loading when it fails //! - Attach the token to the request in the `x-aws-ec2-metadata-token` header +use crate::identity::IdentityCache; use crate::imds::client::error::{ImdsError, TokenError, TokenErrorKind}; -use aws_credential_types::cache::ExpiringCache; use aws_smithy_async::time::SharedTimeSource; use aws_smithy_runtime::client::orchestrator::operation::Operation; +use aws_smithy_runtime::expiring_cache::ExpiringCache; use aws_smithy_runtime_api::box_error::BoxError; use aws_smithy_runtime_api::client::auth::static_resolver::StaticAuthSchemeOptionResolver; use aws_smithy_runtime_api::client::auth::{ @@ -50,6 +51,12 @@ const X_AWS_EC2_METADATA_TOKEN_TTL_SECONDS: &str = "x-aws-ec2-metadata-token-ttl const X_AWS_EC2_METADATA_TOKEN: &str = "x-aws-ec2-metadata-token"; const IMDS_TOKEN_AUTH_SCHEME: AuthSchemeId = AuthSchemeId::new(X_AWS_EC2_METADATA_TOKEN); +#[derive(Debug)] +struct TtlToken { + value: HeaderValue, + ttl: Duration, +} + /// IMDS Token #[derive(Clone)] struct Token { @@ -76,20 +83,18 @@ pub(super) struct TokenRuntimePlugin { } impl TokenRuntimePlugin { - pub(super) fn new( - common_plugin: SharedRuntimePlugin, - time_source: SharedTimeSource, - token_ttl: Duration, - ) -> Self { + pub(super) fn new(common_plugin: SharedRuntimePlugin, token_ttl: Duration) -> Self { Self { components: RuntimeComponentsBuilder::new("TokenRuntimePlugin") .with_auth_scheme(TokenAuthScheme::new()) .with_auth_scheme_option_resolver(Some(StaticAuthSchemeOptionResolver::new(vec![ IMDS_TOKEN_AUTH_SCHEME, ]))) + // The TokenResolver has a cache of its own, so don't use identity caching + .with_identity_cache(Some(IdentityCache::no_cache())) .with_identity_resolver( IMDS_TOKEN_AUTH_SCHEME, - TokenResolver::new(common_plugin, time_source, token_ttl), + TokenResolver::new(common_plugin, token_ttl), ), } } @@ -107,8 +112,7 @@ impl RuntimePlugin for TokenRuntimePlugin { #[derive(Debug)] struct TokenResolverInner { cache: ExpiringCache, - refresh: Operation<(), Token, TokenError>, - time_source: SharedTimeSource, + refresh: Operation<(), TtlToken, TokenError>, } #[derive(Clone, Debug)] @@ -117,11 +121,7 @@ struct TokenResolver { } impl TokenResolver { - fn new( - common_plugin: SharedRuntimePlugin, - time_source: SharedTimeSource, - token_ttl: Duration, - ) -> Self { + fn new(common_plugin: SharedRuntimePlugin, token_ttl: Duration) -> Self { Self { inner: Arc::new(TokenResolverInner { cache: ExpiringCache::new(TOKEN_REFRESH_BUFFER), @@ -141,26 +141,26 @@ impl TokenResolver { .try_into() .unwrap()) }) - .deserializer({ - let time_source = time_source.clone(); - move |response| { - let now = time_source.now(); - parse_token_response(response, now) - .map_err(OrchestratorError::operation) - } + .deserializer(move |response| { + parse_token_response(response).map_err(OrchestratorError::operation) }) .build(), - time_source, }), } } - async fn get_token(&self) -> Result<(Token, SystemTime), ImdsError> { - self.inner - .refresh - .invoke(()) - .await + async fn get_token( + &self, + time_source: SharedTimeSource, + ) -> Result<(Token, SystemTime), ImdsError> { + let result = self.inner.refresh.invoke(()).await; + let now = time_source.now(); + result .map(|token| { + let token = Token { + value: token.value, + expiry: now + token.ttl, + }; let expiry = token.expiry; (token, expiry) }) @@ -168,7 +168,7 @@ impl TokenResolver { } } -fn parse_token_response(response: &HttpResponse, now: SystemTime) -> Result { +fn parse_token_response(response: &HttpResponse) -> Result { match response.status().as_u16() { 400 => return Err(TokenErrorKind::InvalidParameters.into()), 403 => return Err(TokenErrorKind::Forbidden.into()), @@ -187,30 +187,38 @@ fn parse_token_response(response: &HttpResponse, now: SystemTime) -> Result( &'a self, - _components: &'a RuntimeComponents, + components: &'a RuntimeComponents, _config_bag: &'a ConfigBag, ) -> IdentityFuture<'a> { + let time_source = components + .time_source() + .expect("time source required for IMDS token caching"); IdentityFuture::new(async { - let preloaded_token = self - .inner - .cache - .yield_or_clear_if_expired(self.inner.time_source.now()) - .await; + let now = time_source.now(); + let preloaded_token = self.inner.cache.yield_or_clear_if_expired(now).await; let token = match preloaded_token { - Some(token) => Ok(token), + Some(token) => { + tracing::trace!( + buffer_time=?TOKEN_REFRESH_BUFFER, + expiration=?token.expiry, + now=?now, + "loaded IMDS token from cache"); + Ok(token) + } None => { + tracing::debug!("IMDS token cache miss"); self.inner .cache - .get_or_load(|| async { self.get_token().await }) + .get_or_load(|| async { self.get_token(time_source).await }) .await } }?; diff --git a/aws/rust-runtime/aws-config/src/lib.rs b/aws/rust-runtime/aws-config/src/lib.rs index a12dea2a3b..c0cec0cfcd 100644 --- a/aws/rust-runtime/aws-config/src/lib.rs +++ b/aws/rust-runtime/aws-config/src/lib.rs @@ -102,6 +102,12 @@ pub use aws_types::{ /// Load default sources for all configuration with override support pub use loader::ConfigLoader; +/// Types for configuring identity caching. +pub mod identity { + pub use aws_smithy_runtime::client::identity::IdentityCache; + pub use aws_smithy_runtime::client::identity::LazyCacheBuilder; +} + #[allow(dead_code)] const PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -155,11 +161,11 @@ mod loader { use crate::meta::region::ProvideRegion; use crate::profile::profile_file::ProfileFiles; use crate::provider_config::ProviderConfig; - use aws_credential_types::cache::CredentialsCache; use aws_credential_types::provider::{ProvideCredentials, SharedCredentialsProvider}; use aws_smithy_async::rt::sleep::{default_async_sleep, AsyncSleep, SharedAsyncSleep}; use aws_smithy_async::time::{SharedTimeSource, TimeSource}; use aws_smithy_runtime_api::client::http::HttpClient; + use aws_smithy_runtime_api::client::identity::{ResolveCachedIdentity, SharedIdentityCache}; use aws_smithy_runtime_api::shared::IntoShared; use aws_smithy_types::retry::RetryConfig; use aws_smithy_types::timeout::TimeoutConfig; @@ -189,7 +195,7 @@ mod loader { #[derive(Default, Debug)] pub struct ConfigLoader { app_name: Option, - credentials_cache: Option, + identity_cache: Option, credentials_provider: CredentialsProviderOption, endpoint_url: Option, region: Option>, @@ -333,22 +339,45 @@ mod loader { self } - /// Override the credentials cache used to build [`SdkConfig`](aws_types::SdkConfig). + /// The credentials cache has been replaced. Use the identity_cache() method instead. See its rustdoc for an example. + #[deprecated( + note = "The credentials cache has been replaced. Use the identity_cache() method instead for equivalent functionality. See its rustdoc for an example." + )] + pub fn credentials_cache(self) -> Self { + self + } + + /// Override the identity cache used to build [`SdkConfig`](aws_types::SdkConfig). + /// + /// The identity cache caches AWS credentials and SSO tokens. By default, a lazy cache is used + /// that will load credentials upon first request, cache them, and then reload them during + /// another request when they are close to expiring. /// /// # Examples /// - /// Override the credentials cache but load the default value for region: + /// Change a setting on the default lazy caching implementation: /// ```no_run - /// # use aws_credential_types::cache::CredentialsCache; + /// use aws_config::identity::IdentityCache; + /// use std::time::Duration; + /// /// # async fn create_config() { /// let config = aws_config::from_env() - /// .credentials_cache(CredentialsCache::lazy()) + /// .identity_cache( + /// IdentityCache::lazy() + /// // Change the load timeout to 10 seconds. + /// // Note: there are other timeouts that could trigger if the load timeout is too long. + /// .load_timeout(Duration::from_secs(10)) + /// .build() + /// ) /// .load() /// .await; /// # } /// ``` - pub fn credentials_cache(mut self, credentials_cache: CredentialsCache) -> Self { - self.credentials_cache = Some(credentials_cache); + pub fn identity_cache( + mut self, + identity_cache: impl ResolveCachedIdentity + 'static, + ) -> Self { + self.identity_cache = Some(identity_cache.into_shared()); self } @@ -656,17 +685,6 @@ mod loader { CredentialsProviderOption::ExplicitlyUnset => None, }; - let credentials_cache = if credentials_provider.is_some() { - Some(self.credentials_cache.unwrap_or_else(|| { - let mut builder = - CredentialsCache::lazy_builder().time_source(conf.time_source()); - builder.set_sleep_impl(conf.sleep_impl()); - builder.into_credentials_cache() - })) - } else { - None - }; - let mut builder = SdkConfig::builder() .region(region) .retry_config(retry_config) @@ -675,7 +693,7 @@ mod loader { builder.set_http_client(self.http_client); builder.set_app_name(app_name); - builder.set_credentials_cache(credentials_cache); + builder.set_identity_cache(self.identity_cache); builder.set_credentials_provider(credentials_provider); builder.set_sleep_impl(sleep_impl); builder.set_endpoint_url(self.endpoint_url); @@ -705,13 +723,11 @@ mod loader { use crate::{from_env, ConfigLoader}; use aws_credential_types::provider::ProvideCredentials; use aws_smithy_async::rt::sleep::TokioSleep; - use aws_smithy_async::time::{StaticTimeSource, TimeSource}; use aws_smithy_runtime::client::http::test_util::{infallible_client_fn, NeverClient}; 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 std::time::{SystemTime, UNIX_EPOCH}; use tracing_test::traced_test; #[tokio::test] @@ -800,7 +816,7 @@ mod loader { #[tokio::test] async fn disable_default_credentials() { let config = from_env().no_credentials().load().await; - assert!(config.credentials_cache().is_none()); + assert!(config.identity_cache().is_none()); assert!(config.credentials_provider().is_none()); } @@ -827,35 +843,5 @@ mod loader { let num_requests = num_requests.load(Ordering::Relaxed); assert!(num_requests > 0, "{}", num_requests); } - - #[tokio::test] - async fn time_source_is_passed() { - #[derive(Debug)] - struct PanicTs; - impl TimeSource for PanicTs { - fn now(&self) -> SystemTime { - panic!("timesource-was-used") - } - } - let config = from_env() - .sleep_impl(InstantSleep) - .time_source(StaticTimeSource::new(UNIX_EPOCH)) - .http_client(no_traffic_client()) - .load() - .await; - // assert that the innards contain the customized fields - for inner in ["InstantSleep", "StaticTimeSource"] { - assert!( - format!("{:#?}", config.credentials_cache()).contains(inner), - "{:#?}", - config.credentials_cache() - ); - assert!( - format!("{:#?}", config.credentials_provider()).contains(inner), - "{:#?}", - config.credentials_cache() - ); - } - } } } diff --git a/aws/rust-runtime/aws-config/src/sso/credentials.rs b/aws/rust-runtime/aws-config/src/sso/credentials.rs index 15e57546c5..fbf7ff4553 100644 --- a/aws/rust-runtime/aws-config/src/sso/credentials.rs +++ b/aws/rust-runtime/aws-config/src/sso/credentials.rs @@ -11,9 +11,9 @@ //! This provider is included automatically when profiles are loaded. use super::cache::load_cached_token; +use crate::identity::IdentityCache; use crate::provider_config::ProviderConfig; use crate::sso::SsoTokenProvider; -use aws_credential_types::cache::CredentialsCache; use aws_credential_types::provider::{self, error::CredentialsError, future, ProvideCredentials}; use aws_credential_types::Credentials; use aws_sdk_sso::types::RoleCredentials; @@ -253,7 +253,7 @@ async fn load_sso_credentials( let config = sdk_config .to_builder() .region(sso_provider_config.region.clone()) - .credentials_cache(CredentialsCache::no_caching()) + .identity_cache(IdentityCache::no_cache()) .build(); // TODO(enableNewSmithyRuntimeCleanup): Use `customize().config_override()` to set the region instead of creating a new client once middleware is removed let client = SsoClient::new(&config); diff --git a/aws/rust-runtime/aws-config/src/sso/token.rs b/aws/rust-runtime/aws-config/src/sso/token.rs index 11870a241f..b4546cd769 100644 --- a/aws/rust-runtime/aws-config/src/sso/token.rs +++ b/aws/rust-runtime/aws-config/src/sso/token.rs @@ -10,14 +10,15 @@ //! //! This provider is included automatically when profiles are loaded. +use crate::identity::IdentityCache; use crate::sso::cache::{ load_cached_token, save_cached_token, CachedSsoToken, CachedSsoTokenError, }; -use aws_credential_types::cache::{CredentialsCache, ExpiringCache}; use aws_sdk_ssooidc::error::DisplayErrorContext; use aws_sdk_ssooidc::operation::create_token::CreateTokenOutput; use aws_sdk_ssooidc::Client as SsoOidcClient; use aws_smithy_async::time::SharedTimeSource; +use aws_smithy_runtime::expiring_cache::ExpiringCache; use aws_smithy_runtime_api::client::identity::http::Token; use aws_smithy_runtime_api::client::identity::{Identity, IdentityFuture, ResolveIdentity}; use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents; @@ -74,7 +75,7 @@ impl SsoTokenProvider { .sdk_config .to_builder() .region(Some(inner.region.clone())) - .credentials_cache(CredentialsCache::no_caching()) + .identity_cache(IdentityCache::no_cache()) .build(); let client = SsoOidcClient::new(&config); let resp = client diff --git a/aws/rust-runtime/aws-config/src/sts/assume_role.rs b/aws/rust-runtime/aws-config/src/sts/assume_role.rs index 112358ec90..293951b37b 100644 --- a/aws/rust-runtime/aws-config/src/sts/assume_role.rs +++ b/aws/rust-runtime/aws-config/src/sts/assume_role.rs @@ -5,7 +5,6 @@ //! Assume credentials for a role through the AWS Security Token Service (STS). -use aws_credential_types::cache::CredentialsCache; use aws_credential_types::provider::{ self, error::CredentialsError, future, ProvideCredentials, SharedCredentialsProvider, }; @@ -14,6 +13,7 @@ use aws_sdk_sts::operation::assume_role::AssumeRoleError; use aws_sdk_sts::types::PolicyDescriptorType; use aws_sdk_sts::Client as StsClient; use aws_smithy_http::result::SdkError; +use aws_smithy_runtime::client::identity::IdentityCache; use aws_smithy_types::error::display::DisplayErrorContext; use aws_types::region::Region; use aws_types::SdkConfig; @@ -100,10 +100,7 @@ pub struct AssumeRoleProviderBuilder { session_length: Option, policy: Option, policy_arns: Option>, - region_override: Option, - - credentials_cache: Option, sdk_config: Option, } @@ -118,7 +115,6 @@ impl AssumeRoleProviderBuilder { pub fn new(role: impl Into) -> Self { Self { role_arn: role.into(), - credentials_cache: None, external_id: None, session_name: None, session_length: None, @@ -196,18 +192,6 @@ impl AssumeRoleProviderBuilder { self } - #[deprecated( - note = "This should not be necessary as the default, no caching, is usually what you want." - )] - /// Set the [`CredentialsCache`] for credentials retrieved from STS. - /// - /// By default, an [`AssumeRoleProvider`] internally uses `NoCredentialsCache` because the - /// provider itself will be wrapped by `LazyCredentialsCache` when a service client is created. - pub fn credentials_cache(mut self, cache: CredentialsCache) -> Self { - self.credentials_cache = Some(cache); - self - } - /// Sets the configuration used for this provider /// /// This enables overriding the connection used to communicate with STS in addition to other internal @@ -239,13 +223,10 @@ impl AssumeRoleProviderBuilder { Some(conf) => conf, None => crate::load_from_env().await, }; - // ignore a credentials cache set from SdkConfig + // ignore a identity cache set from SdkConfig conf = conf .into_builder() - .credentials_cache( - self.credentials_cache - .unwrap_or(CredentialsCache::no_caching()), - ) + .identity_cache(IdentityCache::no_cache()) .build(); // set a region override if one exists diff --git a/aws/rust-runtime/aws-config/src/test_case.rs b/aws/rust-runtime/aws-config/src/test_case.rs index d567bae89e..9a8af1ad6a 100644 --- a/aws/rust-runtime/aws-config/src/test_case.rs +++ b/aws/rust-runtime/aws-config/src/test_case.rs @@ -11,23 +11,18 @@ use aws_smithy_async::rt::sleep::{AsyncSleep, Sleep, TokioSleep}; use aws_smithy_runtime::client::http::test_util::dvr::{ NetworkTraffic, RecordingClient, ReplayingClient, }; +use aws_smithy_runtime::test_util::capture_test_logs::capture_test_logs; use aws_smithy_runtime_api::shared::IntoShared; use aws_smithy_types::error::display::DisplayErrorContext; use aws_types::os_shim_internal::{Env, Fs}; use aws_types::sdk_config::SharedHttpClient; use serde::Deserialize; use std::collections::HashMap; -use std::env; use std::error::Error; use std::fmt::Debug; use std::future::Future; -use std::io::Write; use std::path::{Path, PathBuf}; -use std::sync::{Arc, Mutex}; use std::time::{Duration, UNIX_EPOCH}; -use tracing::dispatcher::DefaultGuard; -use tracing::Level; -use tracing_subscriber::fmt::TestWriter; /// Test case credentials /// @@ -137,81 +132,6 @@ pub(crate) struct Metadata { name: String, } -// TODO(enableNewSmithyRuntimeCleanup): Replace Tee, capture_test_logs, and Rx with -// the implementations added to aws_smithy_runtime::test_util::capture_test_logs -struct Tee { - buf: Arc>>, - quiet: bool, - inner: W, -} - -/// Capture logs from this test. -/// -/// The logs will be captured until the `DefaultGuard` is dropped. -/// -/// *Why use this instead of traced_test?* -/// This captures _all_ logs, not just logs produced by the current crate. -fn capture_test_logs() -> (DefaultGuard, Rx) { - // it may be helpful to upstream this at some point - let (mut writer, rx) = Tee::stdout(); - if env::var("VERBOSE_TEST_LOGS").is_ok() { - writer.loud(); - } else { - eprintln!("To see full logs from this test set VERBOSE_TEST_LOGS=true"); - } - let subscriber = tracing_subscriber::fmt() - .with_max_level(Level::TRACE) - .with_writer(Mutex::new(writer)) - .finish(); - let guard = tracing::subscriber::set_default(subscriber); - (guard, rx) -} - -struct Rx(Arc>>); -impl Rx { - pub(crate) fn contents(&self) -> String { - String::from_utf8(self.0.lock().unwrap().clone()).unwrap() - } -} - -impl Tee { - fn stdout() -> (Self, Rx) { - let buf: Arc>> = Default::default(); - ( - Tee { - buf: buf.clone(), - quiet: true, - inner: TestWriter::new(), - }, - Rx(buf), - ) - } -} - -impl Tee { - fn loud(&mut self) { - self.quiet = false; - } -} - -impl Write for Tee -where - W: Write, -{ - fn write(&mut self, buf: &[u8]) -> std::io::Result { - self.buf.lock().unwrap().extend_from_slice(buf); - if !self.quiet { - self.inner.write(buf) - } else { - Ok(buf.len()) - } - } - - fn flush(&mut self) -> std::io::Result<()> { - self.inner.flush() - } -} - impl TestEnvironment { pub(crate) async fn from_dir(dir: impl AsRef) -> Result> { let dir = dir.as_ref(); diff --git a/aws/rust-runtime/aws-credential-types/Cargo.toml b/aws/rust-runtime/aws-credential-types/Cargo.toml index ac5886fa73..ec4ebea555 100644 --- a/aws/rust-runtime/aws-credential-types/Cargo.toml +++ b/aws/rust-runtime/aws-credential-types/Cargo.toml @@ -15,20 +15,11 @@ test-util = [] aws-smithy-async = { path = "../../../rust-runtime/aws-smithy-async" } aws-smithy-types = { path = "../../../rust-runtime/aws-smithy-types" } aws-smithy-runtime-api = { path = "../../../rust-runtime/aws-smithy-runtime-api", features = ["client"] } -fastrand = "2.0.0" -tokio = { version = "1.23.1", features = ["sync"] } -tracing = "0.1" zeroize = "1" [dev-dependencies] -aws-smithy-async = { path = "../../../rust-runtime/aws-smithy-async", features = ["rt-tokio", "test-util"] } - -# used to test compatibility -async-trait = "0.1.51" -env_logger = "0.10.0" - +async-trait = "0.1.51" # used to test compatibility tokio = { version = "1.23.1", features = ["full", "test-util", "rt"] } -tracing-test = "0.2.4" [package.metadata.docs.rs] all-features = true diff --git a/aws/rust-runtime/aws-credential-types/external-types.toml b/aws/rust-runtime/aws-credential-types/external-types.toml index 88a5088190..e1d82c7db4 100644 --- a/aws/rust-runtime/aws-credential-types/external-types.toml +++ b/aws/rust-runtime/aws-credential-types/external-types.toml @@ -1,6 +1,7 @@ allowed_external_types = [ "aws_smithy_async::rt::sleep::AsyncSleep", "aws_smithy_async::rt::sleep::SharedAsyncSleep", + "aws_smithy_runtime_api::client::identity::ResolveIdentity", "aws_smithy_types::config_bag::storable::Storable", "aws_smithy_types::config_bag::storable::StoreReplace", "aws_smithy_types::config_bag::storable::Storer", diff --git a/aws/rust-runtime/aws-credential-types/src/cache.rs b/aws/rust-runtime/aws-credential-types/src/cache.rs deleted file mode 100644 index a1351d0f9c..0000000000 --- a/aws/rust-runtime/aws-credential-types/src/cache.rs +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -//! Types and traits for enabling caching - -mod expiring_cache; -mod lazy_caching; -mod no_caching; - -pub use expiring_cache::ExpiringCache; -pub use lazy_caching::Builder as LazyBuilder; -use no_caching::NoCredentialsCache; - -use crate::provider::{future, SharedCredentialsProvider}; -use aws_smithy_types::config_bag::{Storable, StoreReplace}; -use std::sync::Arc; - -/// Asynchronous Cached Credentials Provider -pub trait ProvideCachedCredentials: Send + Sync + std::fmt::Debug { - /// Returns a future that provides cached credentials. - fn provide_cached_credentials<'a>(&'a self) -> future::ProvideCredentials<'a> - where - Self: 'a; -} - -/// Credentials cache wrapper that may be shared -/// -/// Newtype wrapper around `ProvideCachedCredentials` that implements `Clone` using an internal -/// `Arc`. -#[derive(Clone, Debug)] -pub struct SharedCredentialsCache(Arc); - -impl SharedCredentialsCache { - /// Create a new `SharedCredentialsCache` from `ProvideCachedCredentials` - /// - /// The given `cache` will be wrapped in an internal `Arc`. If your - /// cache is already in an `Arc`, use `SharedCredentialsCache::from(cache)` instead. - pub fn new(provider: impl ProvideCachedCredentials + 'static) -> Self { - Self(Arc::new(provider)) - } -} - -impl AsRef for SharedCredentialsCache { - fn as_ref(&self) -> &(dyn ProvideCachedCredentials + 'static) { - self.0.as_ref() - } -} - -impl From> for SharedCredentialsCache { - fn from(cache: Arc) -> Self { - SharedCredentialsCache(cache) - } -} - -impl ProvideCachedCredentials for SharedCredentialsCache { - fn provide_cached_credentials<'a>(&'a self) -> future::ProvideCredentials<'a> - where - Self: 'a, - { - self.0.provide_cached_credentials() - } -} - -impl Storable for SharedCredentialsCache { - type Storer = StoreReplace; -} - -#[derive(Clone, Debug)] -pub(crate) enum Inner { - Lazy(lazy_caching::Builder), - NoCaching, -} - -/// `CredentialsCache` allows for configuring and creating a credentials cache. -/// -/// # Examples -/// -/// ```no_run -/// use aws_credential_types::Credentials; -/// use aws_credential_types::cache::CredentialsCache; -/// use aws_credential_types::credential_fn::provide_credentials_fn; -/// use aws_credential_types::provider::SharedCredentialsProvider; -/// -/// let credentials_cache = CredentialsCache::lazy_builder() -/// .into_credentials_cache() -/// .create_cache(SharedCredentialsProvider::new(provide_credentials_fn(|| async { -/// // An async process to retrieve credentials would go here: -/// Ok(Credentials::new( -/// "example", -/// "example", -/// None, -/// None, -/// "my_provider_name" -/// )) -/// }))); -/// ``` -#[derive(Clone, Debug)] -pub struct CredentialsCache { - pub(crate) inner: Inner, -} - -impl CredentialsCache { - /// Creates a [`CredentialsCache`] from the default [`LazyBuilder`]. - pub fn lazy() -> Self { - Self::lazy_builder().into_credentials_cache() - } - - /// Returns the default [`LazyBuilder`]. - pub fn lazy_builder() -> LazyBuilder { - lazy_caching::Builder::new() - } - - /// Creates a [`CredentialsCache`] that offers no caching ability. - pub fn no_caching() -> Self { - Self { - inner: Inner::NoCaching, - } - } - - /// Creates a [`SharedCredentialsCache`] wrapping a concrete caching implementation. - pub fn create_cache(self, provider: SharedCredentialsProvider) -> SharedCredentialsCache { - match self.inner { - Inner::Lazy(builder) => SharedCredentialsCache::new(builder.build(provider)), - Inner::NoCaching => SharedCredentialsCache::new(NoCredentialsCache::new(provider)), - } - } -} - -impl Storable for CredentialsCache { - type Storer = StoreReplace; -} diff --git a/aws/rust-runtime/aws-credential-types/src/cache/expiring_cache.rs b/aws/rust-runtime/aws-credential-types/src/cache/expiring_cache.rs deleted file mode 100644 index 67556aad9b..0000000000 --- a/aws/rust-runtime/aws-credential-types/src/cache/expiring_cache.rs +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -use std::future::Future; -use std::marker::PhantomData; -use std::sync::Arc; -use std::time::{Duration, SystemTime}; -use tokio::sync::{OnceCell, RwLock}; - -/// Expiry-aware cache -/// -/// [`ExpiringCache`] implements two important features: -/// 1. Respect expiry of contents -/// 2. Deduplicate load requests to prevent thundering herds when no value is present. -#[derive(Debug)] -pub struct ExpiringCache { - /// Amount of time before the actual expiration time - /// when the value is considered expired. - buffer_time: Duration, - value: Arc>>, - _phantom: PhantomData, -} - -impl Clone for ExpiringCache { - fn clone(&self) -> Self { - Self { - buffer_time: self.buffer_time, - value: self.value.clone(), - _phantom: Default::default(), - } - } -} - -impl ExpiringCache -where - T: Clone, -{ - /// Creates `ExpiringCache` with the given `buffer_time`. - pub fn new(buffer_time: Duration) -> Self { - ExpiringCache { - buffer_time, - value: Arc::new(RwLock::new(OnceCell::new())), - _phantom: Default::default(), - } - } - - #[cfg(test)] - async fn get(&self) -> Option - where - T: Clone, - { - self.value - .read() - .await - .get() - .cloned() - .map(|(creds, _expiry)| creds) - } - - /// Attempts to refresh the cached value with the given future. - /// If multiple threads attempt to refresh at the same time, one of them will win, - /// and the others will await that thread's result rather than multiple refreshes occurring. - /// The function given to acquire a value future, `f`, will not be called - /// if another thread is chosen to load the value. - pub async fn get_or_load(&self, f: F) -> Result - where - F: FnOnce() -> Fut, - Fut: Future>, - { - let lock = self.value.read().await; - let future = lock.get_or_try_init(f); - future.await.map(|(value, _expiry)| value.clone()) - } - - /// If the value is expired, clears the cache. Otherwise, yields the current value. - pub async fn yield_or_clear_if_expired(&self, now: SystemTime) -> Option { - // Short-circuit if the value is not expired - if let Some((value, expiry)) = self.value.read().await.get() { - if !expired(*expiry, self.buffer_time, now) { - return Some(value.clone()); - } - } - - // Acquire a write lock to clear the cache, but then once the lock is acquired, - // check again that the value is not already cleared. If it has been cleared, - // then another thread is refreshing the cache by the time the write lock was acquired. - let mut lock = self.value.write().await; - if let Some((_value, expiration)) = lock.get() { - // Also check that we're clearing the expired value and not a value - // that has been refreshed by another thread. - if expired(*expiration, self.buffer_time, now) { - *lock = OnceCell::new(); - } - } - None - } -} - -fn expired(expiration: SystemTime, buffer_time: Duration, now: SystemTime) -> bool { - now >= (expiration - buffer_time) -} - -#[cfg(test)] -mod tests { - use super::{expired, ExpiringCache}; - use crate::{provider::error::CredentialsError, Credentials}; - use std::time::{Duration, SystemTime}; - use tracing_test::traced_test; - - fn credentials(expired_secs: u64) -> Result<(Credentials, SystemTime), CredentialsError> { - let expiry = epoch_secs(expired_secs); - let creds = Credentials::new("test", "test", None, Some(expiry), "test"); - Ok((creds, expiry)) - } - - fn epoch_secs(secs: u64) -> SystemTime { - SystemTime::UNIX_EPOCH + Duration::from_secs(secs) - } - - #[test] - fn expired_check() { - let ts = epoch_secs(100); - assert!(expired(ts, Duration::from_secs(10), epoch_secs(1000))); - assert!(expired(ts, Duration::from_secs(10), epoch_secs(90))); - assert!(!expired(ts, Duration::from_secs(10), epoch_secs(10))); - } - - #[traced_test] - #[tokio::test] - async fn cache_clears_if_expired_only() { - let cache = ExpiringCache::new(Duration::from_secs(10)); - assert!(cache - .yield_or_clear_if_expired(epoch_secs(100)) - .await - .is_none()); - - cache - .get_or_load(|| async { credentials(100) }) - .await - .unwrap(); - assert_eq!(Some(epoch_secs(100)), cache.get().await.unwrap().expiry()); - - // It should not clear the credentials if they're not expired - assert_eq!( - Some(epoch_secs(100)), - cache - .yield_or_clear_if_expired(epoch_secs(10)) - .await - .unwrap() - .expiry() - ); - - // It should clear the credentials if they're expired - assert!(cache - .yield_or_clear_if_expired(epoch_secs(500)) - .await - .is_none()); - assert!(cache.get().await.is_none()); - } -} diff --git a/aws/rust-runtime/aws-credential-types/src/cache/lazy_caching.rs b/aws/rust-runtime/aws-credential-types/src/cache/lazy_caching.rs deleted file mode 100644 index d919278db8..0000000000 --- a/aws/rust-runtime/aws-credential-types/src/cache/lazy_caching.rs +++ /dev/null @@ -1,586 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -//! Lazy, credentials cache implementation - -use std::time::Duration; - -use aws_smithy_async::future::timeout::Timeout; -use aws_smithy_async::rt::sleep::{AsyncSleep, SharedAsyncSleep}; -use aws_smithy_async::time::SharedTimeSource; -use tracing::{debug, info, info_span, Instrument}; - -use crate::cache::{ExpiringCache, ProvideCachedCredentials}; -use crate::provider::SharedCredentialsProvider; -use crate::provider::{error::CredentialsError, future, ProvideCredentials}; - -const DEFAULT_LOAD_TIMEOUT: Duration = Duration::from_secs(5); -const DEFAULT_CREDENTIAL_EXPIRATION: Duration = Duration::from_secs(15 * 60); -const DEFAULT_BUFFER_TIME: Duration = Duration::from_secs(10); -const DEFAULT_BUFFER_TIME_JITTER_FRACTION: fn() -> f64 = fastrand::f64; - -#[derive(Debug)] -pub(crate) struct LazyCredentialsCache { - time: SharedTimeSource, - sleeper: SharedAsyncSleep, - cache: ExpiringCache, - provider: SharedCredentialsProvider, - load_timeout: Duration, - buffer_time: Duration, - buffer_time_jitter_fraction: fn() -> f64, - default_credential_expiration: Duration, -} - -impl LazyCredentialsCache { - fn new( - time: SharedTimeSource, - sleeper: SharedAsyncSleep, - provider: SharedCredentialsProvider, - load_timeout: Duration, - buffer_time: Duration, - buffer_time_jitter_fraction: fn() -> f64, - default_credential_expiration: Duration, - ) -> Self { - Self { - time, - sleeper, - cache: ExpiringCache::new(buffer_time), - provider, - load_timeout, - buffer_time, - buffer_time_jitter_fraction, - default_credential_expiration, - } - } -} - -impl ProvideCachedCredentials for LazyCredentialsCache { - fn provide_cached_credentials<'a>(&'a self) -> future::ProvideCredentials<'_> - where - Self: 'a, - { - let now = self.time.now(); - let provider = self.provider.clone(); - let timeout_future = self.sleeper.sleep(self.load_timeout); - let load_timeout = self.load_timeout; - let cache = self.cache.clone(); - let default_credential_expiration = self.default_credential_expiration; - - future::ProvideCredentials::new(async move { - // Attempt to get cached credentials, or clear the cache if they're expired - if let Some(credentials) = cache.yield_or_clear_if_expired(now).await { - debug!("loaded credentials from cache"); - Ok(credentials) - } else { - // If we didn't get credentials from the cache, then we need to try and load. - // There may be other threads also loading simultaneously, but this is OK - // since the futures are not eagerly executed, and the cache will only run one - // of them. - let future = Timeout::new(provider.provide_credentials(), timeout_future); - let start_time = self.time.now(); - let result = cache - .get_or_load(|| { - let span = info_span!("lazy_load_credentials"); - let provider = provider.clone(); - async move { - let credentials = match future.await { - Ok(creds) => creds?, - Err(_err) => match provider.fallback_on_interrupt() { - Some(creds) => creds, - None => { - return Err(CredentialsError::provider_timed_out( - load_timeout, - )) - } - }, - }; - // If the credentials don't have an expiration time, then create a default one - let expiry = credentials - .expiry() - .unwrap_or(now + default_credential_expiration); - - let jitter = self - .buffer_time - .mul_f64((self.buffer_time_jitter_fraction)()); - - // Logging for cache miss should be emitted here as opposed to after the call to - // `cache.get_or_load` above. In the case of multiple threads concurrently executing - // `cache.get_or_load`, logging inside `cache.get_or_load` ensures that it is emitted - // only once for the first thread that succeeds in populating a cache value. - info!( - "credentials cache miss occurred; added new AWS credentials (took {:?})", - self.time.now().duration_since(start_time) - ); - - Ok((credentials, expiry + jitter)) - } - // Only instrument the the actual load future so that no span - // is opened if the cache decides not to execute it. - .instrument(span) - }) - .await; - debug!("loaded credentials"); - result - } - }) - } -} - -use crate::Credentials; -pub use builder::Builder; - -mod builder { - use std::time::Duration; - - use crate::cache::{CredentialsCache, Inner}; - use crate::provider::SharedCredentialsProvider; - use aws_smithy_async::rt::sleep::{default_async_sleep, AsyncSleep, SharedAsyncSleep}; - use aws_smithy_async::time::{SharedTimeSource, TimeSource}; - - use super::{ - LazyCredentialsCache, DEFAULT_BUFFER_TIME, DEFAULT_BUFFER_TIME_JITTER_FRACTION, - DEFAULT_CREDENTIAL_EXPIRATION, DEFAULT_LOAD_TIMEOUT, - }; - use aws_smithy_runtime_api::shared::IntoShared; - - /// Builder for constructing a `LazyCredentialsCache`. - /// - /// `LazyCredentialsCache` implements [`ProvideCachedCredentials`](crate::cache::ProvideCachedCredentials) by caching - /// credentials that it loads by calling a user-provided [`ProvideCredentials`](crate::provider::ProvideCredentials) implementation. - /// - /// For example, you can provide a [`ProvideCredentials`](crate::provider::ProvideCredentials) implementation that calls - /// AWS STS's AssumeRole operation to get temporary credentials, and `LazyCredentialsCache` - /// will cache those credentials until they expire. - /// - /// Callers outside of this crate cannot call `build` directly. They can instead call - /// `into_credentials_cache` to obtain a [`CredentialsCache`]. Its `create_cache` then calls - /// `build` to create a `LazyCredentialsCache`. - #[derive(Clone, Debug, Default)] - pub struct Builder { - sleep_impl: Option, - time_source: Option, - load_timeout: Option, - buffer_time: Option, - buffer_time_jitter_fraction: Option f64>, - default_credential_expiration: Option, - } - - impl Builder { - /// Creates a new builder - pub fn new() -> Self { - Default::default() - } - - /// Implementation of [`AsyncSleep`](aws_smithy_async::rt::sleep::AsyncSleep) to use for timeouts. - /// - /// This enables use of the `LazyCredentialsCache` with other async runtimes. - /// If using Tokio as the async runtime, this should be set to an instance of - /// [`TokioSleep`](aws_smithy_async::rt::sleep::TokioSleep). - pub fn sleep_impl(mut self, sleep_impl: impl AsyncSleep + 'static) -> Self { - self.set_sleep_impl(Some(sleep_impl.into_shared())); - self - } - - /// Implementation of [`AsyncSleep`](aws_smithy_async::rt::sleep::AsyncSleep) to use for timeouts. - /// - /// This enables use of the `LazyCredentialsCache` with other async runtimes. - /// If using Tokio as the async runtime, this should be set to an instance of - /// [`TokioSleep`](aws_smithy_async::rt::sleep::TokioSleep). - pub fn set_sleep_impl(&mut self, sleep_impl: Option) -> &mut Self { - self.sleep_impl = sleep_impl; - self - } - - #[doc(hidden)] // because they only exist for tests - pub fn time_source(mut self, time_source: impl TimeSource + 'static) -> Self { - self.set_time_source(Some(time_source.into_shared())); - self - } - - #[doc(hidden)] // because they only exist for tests - pub fn set_time_source(&mut self, time_source: Option) -> &mut Self { - self.time_source = time_source; - self - } - - /// Timeout for the given [`ProvideCredentials`](crate::provider::ProvideCredentials) implementation. - /// - /// Defaults to 5 seconds. - pub fn load_timeout(mut self, timeout: Duration) -> Self { - self.set_load_timeout(Some(timeout)); - self - } - - /// Timeout for the given [`ProvideCredentials`](crate::provider::ProvideCredentials) implementation. - /// - /// Defaults to 5 seconds. - pub fn set_load_timeout(&mut self, timeout: Option) -> &mut Self { - self.load_timeout = timeout; - self - } - - /// Amount of time before the actual credential expiration time - /// where credentials are considered expired. - /// - /// For example, if credentials are expiring in 15 minutes, and the buffer time is 10 seconds, - /// then any requests made after 14 minutes and 50 seconds will load new credentials. - /// - /// Defaults to 10 seconds. - pub fn buffer_time(mut self, buffer_time: Duration) -> Self { - self.set_buffer_time(Some(buffer_time)); - self - } - - /// Amount of time before the actual credential expiration time - /// where credentials are considered expired. - /// - /// For example, if credentials are expiring in 15 minutes, and the buffer time is 10 seconds, - /// then any requests made after 14 minutes and 50 seconds will load new credentials. - /// - /// Defaults to 10 seconds. - pub fn set_buffer_time(&mut self, buffer_time: Option) -> &mut Self { - self.buffer_time = buffer_time; - self - } - - /// A random percentage by which buffer time is jittered for randomization. - /// - /// For example, if credentials are expiring in 15 minutes, the buffer time is 10 seconds, - /// and buffer time jitter fraction is 0.2, then buffer time is adjusted to 8 seconds. - /// Therefore, any requests made after 14 minutes and 52 seconds will load new credentials. - /// - /// Defaults to a randomly generated value between 0.0 and 1.0. This setter is for testing only. - #[cfg(feature = "test-util")] - pub fn buffer_time_jitter_fraction( - mut self, - buffer_time_jitter_fraction: fn() -> f64, - ) -> Self { - self.set_buffer_time_jitter_fraction(Some(buffer_time_jitter_fraction)); - self - } - - /// A random percentage by which buffer time is jittered for randomization. - /// - /// For example, if credentials are expiring in 15 minutes, the buffer time is 10 seconds, - /// and buffer time jitter fraction is 0.2, then buffer time is adjusted to 8 seconds. - /// Therefore, any requests made after 14 minutes and 52 seconds will load new credentials. - /// - /// Defaults to a randomly generated value between 0.0 and 1.0. This setter is for testing only. - #[cfg(feature = "test-util")] - pub fn set_buffer_time_jitter_fraction( - &mut self, - buffer_time_jitter_fraction: Option f64>, - ) -> &mut Self { - self.buffer_time_jitter_fraction = buffer_time_jitter_fraction; - self - } - - /// Default expiration time to set on credentials if they don't have an expiration time. - /// - /// This is only used if the given [`ProvideCredentials`](crate::provider::ProvideCredentials) returns - /// [`Credentials`](crate::Credentials) that don't have their `expiry` set. - /// This must be at least 15 minutes. - /// - /// Defaults to 15 minutes. - pub fn default_credential_expiration(mut self, duration: Duration) -> Self { - self.set_default_credential_expiration(Some(duration)); - self - } - - /// Default expiration time to set on credentials if they don't have an expiration time. - /// - /// This is only used if the given [`ProvideCredentials`](crate::provider::ProvideCredentials) returns - /// [`Credentials`](crate::Credentials) that don't have their `expiry` set. - /// This must be at least 15 minutes. - /// - /// Defaults to 15 minutes. - pub fn set_default_credential_expiration( - &mut self, - duration: Option, - ) -> &mut Self { - self.default_credential_expiration = duration; - self - } - - /// Converts [`Builder`] into [`CredentialsCache`]. - pub fn into_credentials_cache(self) -> CredentialsCache { - CredentialsCache { - inner: Inner::Lazy(self), - } - } - - /// Creates the [`LazyCredentialsCache`] with the passed-in `provider`. - /// - /// # Panics - /// This will panic if no `sleep` implementation is given and if no default crate features - /// are used. By default, the [`TokioSleep`](aws_smithy_async::rt::sleep::TokioSleep) - /// implementation will be set automatically. - pub(crate) fn build(self, provider: SharedCredentialsProvider) -> LazyCredentialsCache { - let default_credential_expiration = self - .default_credential_expiration - .unwrap_or(DEFAULT_CREDENTIAL_EXPIRATION); - assert!( - default_credential_expiration >= DEFAULT_CREDENTIAL_EXPIRATION, - "default_credential_expiration must be at least 15 minutes" - ); - LazyCredentialsCache::new( - self.time_source.unwrap_or_default(), - self.sleep_impl.unwrap_or_else(|| { - default_async_sleep().expect("no default sleep implementation available") - }), - provider, - self.load_timeout.unwrap_or(DEFAULT_LOAD_TIMEOUT), - self.buffer_time.unwrap_or(DEFAULT_BUFFER_TIME), - self.buffer_time_jitter_fraction - .unwrap_or(DEFAULT_BUFFER_TIME_JITTER_FRACTION), - default_credential_expiration, - ) - } - } -} - -#[cfg(test)] -mod tests { - use std::sync::{Arc, Mutex}; - use std::time::{Duration, SystemTime, UNIX_EPOCH}; - - use aws_smithy_async::rt::sleep::{SharedAsyncSleep, TokioSleep}; - use aws_smithy_async::test_util::{instant_time_and_sleep, ManualTimeSource}; - use aws_smithy_async::time::{SharedTimeSource, TimeSource}; - use tracing::info; - use tracing_test::traced_test; - - use crate::provider::SharedCredentialsProvider; - use crate::{ - cache::ProvideCachedCredentials, credential_fn::provide_credentials_fn, - provider::error::CredentialsError, Credentials, - }; - - use super::{ - LazyCredentialsCache, DEFAULT_BUFFER_TIME, DEFAULT_CREDENTIAL_EXPIRATION, - DEFAULT_LOAD_TIMEOUT, - }; - - const BUFFER_TIME_NO_JITTER: fn() -> f64 = || 0_f64; - - fn test_provider( - time: impl TimeSource + 'static, - buffer_time_jitter_fraction: fn() -> f64, - load_list: Vec, - ) -> LazyCredentialsCache { - let load_list = Arc::new(Mutex::new(load_list)); - LazyCredentialsCache::new( - SharedTimeSource::new(time), - SharedAsyncSleep::new(TokioSleep::new()), - SharedCredentialsProvider::new(provide_credentials_fn(move || { - let list = load_list.clone(); - async move { - let mut list = list.lock().unwrap(); - if list.len() > 0 { - let next = list.remove(0); - info!("refreshing the credentials to {:?}", next); - next - } else { - drop(list); - panic!("no more credentials") - } - } - })), - DEFAULT_LOAD_TIMEOUT, - DEFAULT_BUFFER_TIME, - buffer_time_jitter_fraction, - DEFAULT_CREDENTIAL_EXPIRATION, - ) - } - - fn epoch_secs(secs: u64) -> SystemTime { - SystemTime::UNIX_EPOCH + Duration::from_secs(secs) - } - - fn credentials(expired_secs: u64) -> Credentials { - Credentials::new("test", "test", None, Some(epoch_secs(expired_secs)), "test") - } - - async fn expect_creds(expired_secs: u64, provider: &LazyCredentialsCache) { - let creds = provider - .provide_cached_credentials() - .await - .expect("expected credentials"); - assert_eq!(Some(epoch_secs(expired_secs)), creds.expiry()); - } - - #[traced_test] - #[tokio::test] - async fn initial_populate_credentials() { - let time = ManualTimeSource::new(UNIX_EPOCH); - let provider = SharedCredentialsProvider::new(provide_credentials_fn(|| async { - info!("refreshing the credentials"); - Ok(credentials(1000)) - })); - let credentials_cache = LazyCredentialsCache::new( - SharedTimeSource::new(time), - SharedAsyncSleep::new(TokioSleep::new()), - provider, - DEFAULT_LOAD_TIMEOUT, - DEFAULT_BUFFER_TIME, - BUFFER_TIME_NO_JITTER, - DEFAULT_CREDENTIAL_EXPIRATION, - ); - assert_eq!( - epoch_secs(1000), - credentials_cache - .provide_cached_credentials() - .await - .unwrap() - .expiry() - .unwrap() - ); - } - - #[traced_test] - #[tokio::test] - async fn reload_expired_credentials() { - let time = ManualTimeSource::new(epoch_secs(100)); - let credentials_cache = test_provider( - time.clone(), - BUFFER_TIME_NO_JITTER, - vec![ - Ok(credentials(1000)), - Ok(credentials(2000)), - Ok(credentials(3000)), - ], - ); - - expect_creds(1000, &credentials_cache).await; - expect_creds(1000, &credentials_cache).await; - time.set_time(epoch_secs(1500)); - expect_creds(2000, &credentials_cache).await; - expect_creds(2000, &credentials_cache).await; - time.set_time(epoch_secs(2500)); - expect_creds(3000, &credentials_cache).await; - expect_creds(3000, &credentials_cache).await; - } - - #[traced_test] - #[tokio::test] - async fn load_failed_error() { - let time = ManualTimeSource::new(epoch_secs(100)); - let credentials_cache = test_provider( - time.clone(), - BUFFER_TIME_NO_JITTER, - vec![ - Ok(credentials(1000)), - Err(CredentialsError::not_loaded("failed")), - ], - ); - - expect_creds(1000, &credentials_cache).await; - time.set_time(epoch_secs(1500)); - assert!(credentials_cache - .provide_cached_credentials() - .await - .is_err()); - } - - #[traced_test] - #[test] - fn load_contention() { - let rt = tokio::runtime::Builder::new_multi_thread() - .enable_time() - .worker_threads(16) - .build() - .unwrap(); - - let time = ManualTimeSource::new(epoch_secs(0)); - let credentials_cache = Arc::new(test_provider( - time.clone(), - BUFFER_TIME_NO_JITTER, - vec![ - Ok(credentials(500)), - Ok(credentials(1500)), - Ok(credentials(2500)), - Ok(credentials(3500)), - Ok(credentials(4500)), - ], - )); - - // credentials are available up until 4500 seconds after the unix epoch - // 4*50 = 200 tasks are launched => we can advance time 4500/20 => 225 seconds per advance - for _ in 0..4 { - let mut tasks = Vec::new(); - for _ in 0..50 { - let credentials_cache = credentials_cache.clone(); - let time = time.clone(); - tasks.push(rt.spawn(async move { - let now = time.advance(Duration::from_secs(22)); - - let creds = credentials_cache - .provide_cached_credentials() - .await - .unwrap(); - assert!( - creds.expiry().unwrap() >= now, - "{:?} >= {:?}", - creds.expiry(), - now - ); - })); - } - for task in tasks { - rt.block_on(task).unwrap(); - } - } - } - - #[tokio::test] - #[traced_test] - async fn load_timeout() { - let (time, sleep) = instant_time_and_sleep(epoch_secs(100)); - let credentials_cache = LazyCredentialsCache::new( - SharedTimeSource::new(time.clone()), - SharedAsyncSleep::new(sleep), - SharedCredentialsProvider::new(provide_credentials_fn(|| async { - aws_smithy_async::future::never::Never::new().await; - Ok(credentials(1000)) - })), - Duration::from_secs(5), - DEFAULT_BUFFER_TIME, - BUFFER_TIME_NO_JITTER, - DEFAULT_CREDENTIAL_EXPIRATION, - ); - - assert!(matches!( - credentials_cache.provide_cached_credentials().await, - Err(CredentialsError::ProviderTimedOut { .. }) - )); - assert_eq!(time.now(), epoch_secs(105)); - } - - #[tokio::test] - async fn buffer_time_jitter() { - let time = ManualTimeSource::new(epoch_secs(100)); - let buffer_time_jitter_fraction = || 0.5_f64; - let credentials_cache = test_provider( - time.clone(), - buffer_time_jitter_fraction, - vec![Ok(credentials(1000)), Ok(credentials(2000))], - ); - - expect_creds(1000, &credentials_cache).await; - let buffer_time_with_jitter = - (DEFAULT_BUFFER_TIME.as_secs_f64() * buffer_time_jitter_fraction()) as u64; - assert_eq!(buffer_time_with_jitter, 5); - // Advance time to the point where the first credentials are about to expire (but haven't). - let almost_expired_secs = 1000 - buffer_time_with_jitter - 1; - time.set_time(epoch_secs(almost_expired_secs)); - // We should still use the first credentials. - expect_creds(1000, &credentials_cache).await; - // Now let the first credentials expire. - let expired_secs = almost_expired_secs + 1; - time.set_time(epoch_secs(expired_secs)); - // Now that the first credentials have been expired, the second credentials will be retrieved. - expect_creds(2000, &credentials_cache).await; - } -} diff --git a/aws/rust-runtime/aws-credential-types/src/cache/no_caching.rs b/aws/rust-runtime/aws-credential-types/src/cache/no_caching.rs deleted file mode 100644 index 5827f5cea1..0000000000 --- a/aws/rust-runtime/aws-credential-types/src/cache/no_caching.rs +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -//! Credentials cache that offers no caching ability - -use crate::cache::ProvideCachedCredentials; -use crate::provider::SharedCredentialsProvider; -use crate::provider::{future, ProvideCredentials}; -use tracing::debug; - -#[derive(Debug)] -pub(crate) struct NoCredentialsCache { - provider: SharedCredentialsProvider, -} - -impl NoCredentialsCache { - pub(crate) fn new(provider: SharedCredentialsProvider) -> Self { - Self { provider } - } -} - -impl ProvideCachedCredentials for NoCredentialsCache { - fn provide_cached_credentials<'a>(&'a self) -> future::ProvideCredentials<'_> - where - Self: 'a, - { - debug!("Delegating `provide_cached_credentials` to `provide_credentials` on the provider"); - self.provider.provide_credentials() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::credential_fn::provide_credentials_fn; - use crate::Credentials; - use std::sync::{Arc, Mutex}; - use std::time::{Duration, SystemTime}; - - fn test_provider(load_list: Vec) -> NoCredentialsCache { - let load_list = Arc::new(Mutex::new(load_list)); - NoCredentialsCache::new(SharedCredentialsProvider::new(provide_credentials_fn( - move || { - let list = load_list.clone(); - async move { - let next = list.lock().unwrap().remove(0); - next - } - }, - ))) - } - - fn epoch_secs(secs: u64) -> SystemTime { - SystemTime::UNIX_EPOCH + Duration::from_secs(secs) - } - - fn credentials(expired_secs: u64) -> Credentials { - Credentials::new("test", "test", None, Some(epoch_secs(expired_secs)), "test") - } - - async fn expect_creds(expired_secs: u64, provider: &NoCredentialsCache) { - let creds = provider - .provide_cached_credentials() - .await - .expect("expected credentials"); - assert_eq!(Some(epoch_secs(expired_secs)), creds.expiry()); - } - - #[tokio::test] - async fn no_caching() { - let credentials_cache = test_provider(vec![ - Ok(credentials(1000)), - Ok(credentials(2000)), - Ok(credentials(3000)), - ]); - - expect_creds(1000, &credentials_cache).await; - expect_creds(2000, &credentials_cache).await; - expect_creds(3000, &credentials_cache).await; - } -} diff --git a/aws/rust-runtime/aws-credential-types/src/lib.rs b/aws/rust-runtime/aws-credential-types/src/lib.rs index de662f6a50..58a3148eb8 100644 --- a/aws/rust-runtime/aws-credential-types/src/lib.rs +++ b/aws/rust-runtime/aws-credential-types/src/lib.rs @@ -17,7 +17,6 @@ unreachable_pub )] -pub mod cache; pub mod credential_fn; mod credentials_impl; pub mod provider; diff --git a/aws/rust-runtime/aws-credential-types/src/provider.rs b/aws/rust-runtime/aws-credential-types/src/provider.rs index 9be88b590f..35c6f91448 100644 --- a/aws/rust-runtime/aws-credential-types/src/provider.rs +++ b/aws/rust-runtime/aws-credential-types/src/provider.rs @@ -72,7 +72,9 @@ construct credentials from hardcoded values. //! ``` use crate::Credentials; -use aws_smithy_types::config_bag::{Storable, StoreReplace}; +use aws_smithy_runtime_api::client::identity::{Identity, IdentityFuture, ResolveIdentity}; +use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents; +use aws_smithy_types::config_bag::{ConfigBag, Storable, StoreReplace}; use std::sync::Arc; /// Credentials provider errors @@ -355,3 +357,17 @@ impl ProvideCredentials for SharedCredentialsProvider { impl Storable for SharedCredentialsProvider { type Storer = StoreReplace; } + +impl ResolveIdentity for SharedCredentialsProvider { + fn resolve_identity<'a>( + &'a self, + _runtime_components: &'a RuntimeComponents, + _config_bag: &'a ConfigBag, + ) -> IdentityFuture<'a> { + IdentityFuture::new(async move { Ok(self.provide_credentials().await?.into()) }) + } + + fn fallback_on_interrupt(&self) -> Option { + ProvideCredentials::fallback_on_interrupt(self).map(|creds| creds.into()) + } +} diff --git a/aws/rust-runtime/aws-inlineable/src/lib.rs b/aws/rust-runtime/aws-inlineable/src/lib.rs index 05679f1f29..71858b2324 100644 --- a/aws/rust-runtime/aws-inlineable/src/lib.rs +++ b/aws/rust-runtime/aws-inlineable/src/lib.rs @@ -22,9 +22,6 @@ /// Interceptors for API Gateway pub mod apigateway_interceptors; -/// Stub credentials provider for use when no credentials provider is used. -pub mod no_credentials; - /// Support types required for adding presigning to an operation in a generated service. pub mod presigning; diff --git a/aws/rust-runtime/aws-inlineable/src/no_credentials.rs b/aws/rust-runtime/aws-inlineable/src/no_credentials.rs deleted file mode 100644 index d58386b31b..0000000000 --- a/aws/rust-runtime/aws-inlineable/src/no_credentials.rs +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -use aws_credential_types::provider::{error::CredentialsError, future, ProvideCredentials}; - -/// Stub credentials provider for use when no credentials provider is used. -#[non_exhaustive] -#[derive(Debug)] -pub struct NoCredentials; - -impl ProvideCredentials for NoCredentials { - fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials<'a> - where - Self: 'a, - { - future::ProvideCredentials::ready(Err(CredentialsError::not_loaded( - "No credentials provider was enabled for the service. \ - hint: use aws-config to provide a credentials chain.", - ))) - } -} diff --git a/aws/rust-runtime/aws-runtime/src/identity.rs b/aws/rust-runtime/aws-runtime/src/identity.rs deleted file mode 100644 index 3f2753cc16..0000000000 --- a/aws/rust-runtime/aws-runtime/src/identity.rs +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -/// Credentials-based identity support. -pub mod credentials { - use aws_credential_types::cache::SharedCredentialsCache; - use aws_smithy_runtime_api::client::identity::{Identity, IdentityFuture, ResolveIdentity}; - use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents; - use aws_smithy_types::config_bag::ConfigBag; - - /// Smithy identity resolver for AWS credentials. - #[derive(Debug)] - pub struct CredentialsIdentityResolver { - credentials_cache: SharedCredentialsCache, - } - - impl CredentialsIdentityResolver { - /// Creates a new `CredentialsIdentityResolver`. - pub fn new(credentials_cache: SharedCredentialsCache) -> Self { - Self { credentials_cache } - } - } - - impl ResolveIdentity for CredentialsIdentityResolver { - fn resolve_identity<'a>( - &'a self, - _runtime_components: &'a RuntimeComponents, - _config_bag: &'a ConfigBag, - ) -> IdentityFuture<'a> { - let cache = self.credentials_cache.clone(); - IdentityFuture::new(async move { - let credentials = cache.as_ref().provide_cached_credentials().await?; - let expiration = credentials.expiry(); - Ok(Identity::new(credentials, expiration)) - }) - } - } -} diff --git a/aws/rust-runtime/aws-runtime/src/lib.rs b/aws/rust-runtime/aws-runtime/src/lib.rs index 6ad3cec356..085ec4fc97 100644 --- a/aws/rust-runtime/aws-runtime/src/lib.rs +++ b/aws/rust-runtime/aws-runtime/src/lib.rs @@ -16,9 +16,6 @@ /// Supporting code for authentication in the AWS SDK. pub mod auth; -/// Supporting code for identity in the AWS SDK. -pub mod identity; - /// Supporting code for recursion detection in the AWS SDK. pub mod recursion_detection; diff --git a/aws/rust-runtime/aws-types/external-types.toml b/aws/rust-runtime/aws-types/external-types.toml index ca85884065..e5e613b5ae 100644 --- a/aws/rust-runtime/aws-types/external-types.toml +++ b/aws/rust-runtime/aws-types/external-types.toml @@ -7,6 +7,8 @@ allowed_external_types = [ "aws_smithy_async::time::TimeSource", "aws_smithy_runtime_api::client::http::HttpClient", "aws_smithy_runtime_api::client::http::SharedHttpClient", + "aws_smithy_runtime_api::client::identity::ResolveCachedIdentity", + "aws_smithy_runtime_api::client::identity::SharedIdentityCache", "aws_smithy_types::config_bag::storable::Storable", "aws_smithy_types::config_bag::storable::StoreReplace", "aws_smithy_types::config_bag::storable::Storer", diff --git a/aws/rust-runtime/aws-types/src/sdk_config.rs b/aws/rust-runtime/aws-types/src/sdk_config.rs index e97a58cc3d..4538a4bd31 100644 --- a/aws/rust-runtime/aws-types/src/sdk_config.rs +++ b/aws/rust-runtime/aws-types/src/sdk_config.rs @@ -13,13 +13,13 @@ use crate::app_name::AppName; use crate::docs_for; use crate::region::Region; -pub use aws_credential_types::cache::CredentialsCache; pub use aws_credential_types::provider::SharedCredentialsProvider; use aws_smithy_async::rt::sleep::AsyncSleep; pub use aws_smithy_async::rt::sleep::SharedAsyncSleep; pub use aws_smithy_async::time::{SharedTimeSource, TimeSource}; use aws_smithy_runtime_api::client::http::HttpClient; pub use aws_smithy_runtime_api::client::http::SharedHttpClient; +use aws_smithy_runtime_api::client::identity::{ResolveCachedIdentity, SharedIdentityCache}; use aws_smithy_runtime_api::shared::IntoShared; pub use aws_smithy_types::retry::RetryConfig; pub use aws_smithy_types::timeout::TimeoutConfig; @@ -51,7 +51,7 @@ these services, this setting has no effect" #[derive(Debug, Clone)] pub struct SdkConfig { app_name: Option, - credentials_cache: Option, + identity_cache: Option, credentials_provider: Option, region: Option, endpoint_url: Option, @@ -72,7 +72,7 @@ pub struct SdkConfig { #[derive(Debug, Default)] pub struct Builder { app_name: Option, - credentials_cache: Option, + identity_cache: Option, credentials_provider: Option, region: Option, endpoint_url: Option, @@ -296,40 +296,64 @@ impl Builder { self } - /// Set the [`CredentialsCache`] for the builder + /// Set the identity cache for caching credentials and SSO tokens. + /// + /// The default identity cache will wait until the first request that requires authentication + /// to load an identity. Once the identity is loaded, it is cached until shortly before it + /// expires. /// /// # Examples + /// Disabling identity caching: /// ```rust - /// use aws_credential_types::cache::CredentialsCache; - /// use aws_types::SdkConfig; + /// # use aws_types::SdkConfig; + /// use aws_smithy_runtime::client::identity::IdentityCache; /// let config = SdkConfig::builder() - /// .credentials_cache(CredentialsCache::lazy()) + /// .identity_cache(IdentityCache::no_cache()) /// .build(); /// ``` - pub fn credentials_cache(mut self, cache: CredentialsCache) -> Self { - self.set_credentials_cache(Some(cache)); + /// Changing settings on the default cache implementation: + /// ```rust + /// # use aws_types::SdkConfig; + /// use aws_smithy_runtime::client::identity::IdentityCache; + /// use std::time::Duration; + /// + /// let config = SdkConfig::builder() + /// .identity_cache( + /// IdentityCache::lazy() + /// .load_timeout(Duration::from_secs(10)) + /// .build() + /// ) + /// .build(); + /// ``` + pub fn identity_cache(mut self, cache: impl ResolveCachedIdentity + 'static) -> Self { + self.set_identity_cache(Some(cache.into_shared())); self } - /// Set the [`CredentialsCache`] for the builder + /// Set the identity cache for caching credentials and SSO tokens. + /// + /// The default identity cache will wait until the first request that requires authentication + /// to load an identity. Once the identity is loaded, it is cached until shortly before it + /// expires. /// /// # Examples /// ```rust - /// use aws_credential_types::cache::CredentialsCache; - /// use aws_types::SdkConfig; - /// fn override_credentials_cache() -> bool { + /// # use aws_types::SdkConfig; + /// use aws_smithy_runtime::client::identity::IdentityCache; + /// + /// fn override_identity_cache() -> bool { /// // ... /// # true /// } /// /// let mut builder = SdkConfig::builder(); - /// if override_credentials_cache() { - /// builder.set_credentials_cache(Some(CredentialsCache::lazy())); + /// if override_identity_cache() { + /// builder.set_identity_cache(Some(IdentityCache::lazy().build())); /// } /// let config = builder.build(); /// ``` - pub fn set_credentials_cache(&mut self, cache: Option) -> &mut Self { - self.credentials_cache = cache; + pub fn set_identity_cache(&mut self, cache: Option) -> &mut Self { + self.identity_cache = cache; self } @@ -516,7 +540,7 @@ impl Builder { pub fn build(self) -> SdkConfig { SdkConfig { app_name: self.app_name, - credentials_cache: self.credentials_cache, + identity_cache: self.identity_cache, credentials_provider: self.credentials_provider, region: self.region, endpoint_url: self.endpoint_url, @@ -558,9 +582,9 @@ impl SdkConfig { self.sleep_impl.clone() } - /// Configured credentials cache - pub fn credentials_cache(&self) -> Option<&CredentialsCache> { - self.credentials_cache.as_ref() + /// Configured identity cache + pub fn identity_cache(&self) -> Option { + self.identity_cache.clone() } /// Configured credentials provider @@ -611,7 +635,7 @@ impl SdkConfig { pub fn into_builder(self) -> Builder { Builder { app_name: self.app_name, - credentials_cache: self.credentials_cache, + identity_cache: self.identity_cache, credentials_provider: self.credentials_provider, region: self.region, endpoint_url: self.endpoint_url, 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 8a63872df1..59ef326a2f 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 @@ -29,7 +29,6 @@ import software.amazon.smithy.rustsdk.endpoints.RequireEndpointRules val DECORATORS: List = listOf( // General AWS Decorators listOf( - CredentialsCacheDecorator(), CredentialsProviderDecorator(), RegionDecorator(), RequireEndpointRules(), diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/CredentialCaches.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/CredentialCaches.kt deleted file mode 100644 index 51f88d2401..0000000000 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/CredentialCaches.kt +++ /dev/null @@ -1,158 +0,0 @@ -/* - * 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.customize.ClientCodegenDecorator -import software.amazon.smithy.rust.codegen.client.smithy.generators.config.ConfigCustomization -import software.amazon.smithy.rust.codegen.client.smithy.generators.config.ServiceConfig -import software.amazon.smithy.rust.codegen.core.rustlang.rust -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.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 - -class CredentialsCacheDecorator : ClientCodegenDecorator { - override val name: String = "CredentialsCache" - override val order: Byte = 0 - - override fun configCustomizations( - codegenContext: ClientCodegenContext, - baseCustomizations: List, - ): List { - return baseCustomizations + CredentialCacheConfig(codegenContext) - } - - override fun extraSections(codegenContext: ClientCodegenContext): List = - listOf( - adhocCustomization { section -> - rust("${section.serviceConfigBuilder}.set_credentials_cache(${section.sdkConfig}.credentials_cache().cloned());") - }, - ) -} - -/** - * Add a `.credentials_cache` field and builder to the `Config` for a given service - */ -class CredentialCacheConfig(codegenContext: ClientCodegenContext) : ConfigCustomization() { - private val runtimeConfig = codegenContext.runtimeConfig - private val codegenScope = arrayOf( - *preludeScope, - "CredentialsCache" to AwsRuntimeType.awsCredentialTypes(runtimeConfig).resolve("cache::CredentialsCache"), - "CredentialsIdentityResolver" to AwsRuntimeType.awsRuntime(runtimeConfig) - .resolve("identity::credentials::CredentialsIdentityResolver"), - "DefaultProvider" to defaultProvider(), - "SIGV4_SCHEME_ID" to AwsRuntimeType.awsRuntime(runtimeConfig).resolve("auth::sigv4::SCHEME_ID"), - "SharedAsyncSleep" to RuntimeType.smithyAsync(runtimeConfig).resolve("rt::sleep::SharedAsyncSleep"), - "SharedCredentialsCache" to AwsRuntimeType.awsCredentialTypes(runtimeConfig) - .resolve("cache::SharedCredentialsCache"), - "SharedCredentialsProvider" to AwsRuntimeType.awsCredentialTypes(runtimeConfig) - .resolve("provider::SharedCredentialsProvider"), - "SharedIdentityResolver" to RuntimeType.smithyRuntimeApi(runtimeConfig) - .resolve("client::identity::SharedIdentityResolver"), - ) - - override fun section(section: ServiceConfig) = writable { - when (section) { - ServiceConfig.ConfigImpl -> { - rustTemplate( - """ - /// Returns the credentials cache. - pub fn credentials_cache(&self) -> #{Option}<#{SharedCredentialsCache}> { - self.config.load::<#{SharedCredentialsCache}>().cloned() - } - """, - *codegenScope, - ) - } - - ServiceConfig.BuilderImpl -> { - rustTemplate( - """ - /// Sets the credentials cache for this service - pub fn credentials_cache(mut self, credentials_cache: #{CredentialsCache}) -> Self { - self.set_credentials_cache(#{Some}(credentials_cache)); - self - } - - """, - *codegenScope, - ) - - rustTemplate( - """ - /// Sets the credentials cache for this service - pub fn set_credentials_cache(&mut self, credentials_cache: #{Option}<#{CredentialsCache}>) -> &mut Self { - self.config.store_or_unset(credentials_cache); - self - } - """, - *codegenScope, - ) - } - - ServiceConfig.BuilderBuild -> { - rustTemplate( - """ - if let Some(credentials_provider) = layer.load::<#{SharedCredentialsProvider}>().cloned() { - let cache_config = layer.load::<#{CredentialsCache}>().cloned() - .unwrap_or_else({ - let sleep = self.runtime_components.sleep_impl(); - || match sleep { - Some(sleep) => { - #{CredentialsCache}::lazy_builder() - .sleep_impl(sleep) - .into_credentials_cache() - } - None => #{CredentialsCache}::lazy(), - } - }); - let shared_credentials_cache = cache_config.create_cache(credentials_provider); - layer.store_put(shared_credentials_cache); - } - """, - *codegenScope, - ) - } - - is ServiceConfig.OperationConfigOverride -> { - rustTemplate( - """ - match ( - resolver.config_mut().load::<#{CredentialsCache}>().cloned(), - resolver.config_mut().load::<#{SharedCredentialsProvider}>().cloned(), - ) { - (#{None}, #{None}) => {} - (#{None}, _) => { - panic!("also specify `.credentials_cache` when overriding credentials provider for the operation"); - } - (_, #{None}) => { - panic!("also specify `.credentials_provider` when overriding credentials cache for the operation"); - } - ( - #{Some}(credentials_cache), - #{Some}(credentials_provider), - ) => { - let credentials_cache = credentials_cache.create_cache(credentials_provider); - resolver.runtime_components_mut().push_identity_resolver( - #{SIGV4_SCHEME_ID}, - #{SharedIdentityResolver}::new( - #{CredentialsIdentityResolver}::new(credentials_cache), - ), - ); - } - } - """, - *codegenScope, - ) - } - - else -> emptySection - } - } -} diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/CredentialProviders.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/CredentialProviders.kt index e62773aa9a..46d8e42734 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/CredentialProviders.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/CredentialProviders.kt @@ -79,6 +79,18 @@ class CredentialProviderConfig(codegenContext: ClientCodegenContext) : ConfigCus override fun section(section: ServiceConfig) = writable { when (section) { + ServiceConfig.ConfigImpl -> { + rustTemplate( + """ + /// Returns the credentials provider for this service + pub fn credentials_provider(&self) -> Option<#{SharedCredentialsProvider}> { + self.config.load::<#{SharedCredentialsProvider}>().cloned() + } + """, + *codegenScope, + ) + } + ServiceConfig.BuilderImpl -> { rustTemplate( """ @@ -121,10 +133,8 @@ class CredentialsIdentityResolverRegistration( override fun section(section: ServiceRuntimePluginSection): Writable = writable { when (section) { is ServiceRuntimePluginSection.RegisterRuntimeComponents -> { - rustBlockTemplate("if let Some(credentials_cache) = ${section.serviceConfigName}.credentials_cache()") { + rustBlockTemplate("if let Some(creds_provider) = ${section.serviceConfigName}.credentials_provider()") { val codegenScope = arrayOf( - "CredentialsIdentityResolver" to AwsRuntimeType.awsRuntime(runtimeConfig) - .resolve("identity::credentials::CredentialsIdentityResolver"), "SharedIdentityResolver" to RuntimeType.smithyRuntimeApi(runtimeConfig) .resolve("client::identity::SharedIdentityResolver"), "SIGV4A_SCHEME_ID" to AwsRuntimeType.awsRuntime(runtimeConfig) @@ -133,24 +143,15 @@ class CredentialsIdentityResolverRegistration( .resolve("auth::sigv4::SCHEME_ID"), ) - rustTemplate( - """ - let shared_identity_resolver = #{SharedIdentityResolver}::new( - #{CredentialsIdentityResolver}::new(credentials_cache) - ); - """, - *codegenScope, - ) - if (codegenContext.serviceShape.supportedAuthSchemes().contains("sigv4a")) { featureGateBlock("sigv4a") { section.registerIdentityResolver(this) { - rustTemplate("#{SIGV4A_SCHEME_ID}, shared_identity_resolver.clone()", *codegenScope) + rustTemplate("#{SIGV4A_SCHEME_ID}, creds_provider.clone()", *codegenScope) } } } section.registerIdentityResolver(this) { - rustTemplate("#{SIGV4_SCHEME_ID}, shared_identity_resolver,", *codegenScope) + rustTemplate("#{SIGV4_SCHEME_ID}, creds_provider,", *codegenScope) } } } @@ -159,6 +160,3 @@ class CredentialsIdentityResolverRegistration( } } } - -fun defaultProvider() = - RuntimeType.forInlineDependency(InlineAwsDependency.forRustFile("no_credentials")).resolve("NoCredentials") 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 7167910c08..37a31fb615 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 @@ -78,6 +78,10 @@ class GenericSmithySdkConfigSettings : ClientCodegenDecorator { ${section.serviceConfigBuilder}.set_http_client(${section.sdkConfig}.http_client()); ${section.serviceConfigBuilder}.set_time_source(${section.sdkConfig}.time_source()); + + if let Some(cache) = ${section.sdkConfig}.identity_cache() { + ${section.serviceConfigBuilder}.set_identity_cache(cache); + } """, ) }, diff --git a/aws/sdk-codegen/src/test/kotlin/software/amazon/smithy/rustsdk/CredentialCacheConfigTest.kt b/aws/sdk-codegen/src/test/kotlin/software/amazon/smithy/rustsdk/CredentialCacheConfigTest.kt deleted file mode 100644 index 6eaefa90e3..0000000000 --- a/aws/sdk-codegen/src/test/kotlin/software/amazon/smithy/rustsdk/CredentialCacheConfigTest.kt +++ /dev/null @@ -1,191 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package software.amazon.smithy.rustsdk - -import org.junit.jupiter.api.Test -import software.amazon.smithy.rust.codegen.core.rustlang.Attribute -import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency -import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate -import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType -import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel -import software.amazon.smithy.rust.codegen.core.testutil.testModule -import software.amazon.smithy.rust.codegen.core.testutil.tokioTest -import software.amazon.smithy.rust.codegen.core.testutil.unitTest - -internal class CredentialCacheConfigTest { - private val model = """ - namespace com.example - use aws.protocols#awsJson1_0 - use aws.api#service - use aws.auth#sigv4 - use smithy.rules#endpointRuleSet - - @service(sdkId: "Some Value") - @awsJson1_0 - @sigv4(name: "dontcare") - @auth([sigv4]) - @endpointRuleSet({ - "version": "1.0", - "rules": [{ - "type": "endpoint", - "conditions": [{"fn": "isSet", "argv": [{"ref": "Region"}]}], - "endpoint": { "url": "https://example.com" } - }], - "parameters": { - "Region": { "required": false, "type": "String", "builtIn": "AWS::Region" }, - } - }) - service HelloService { - operations: [SayHello], - version: "1" - } - - operation SayHello { input: TestInput } - structure TestInput { - foo: String, - } - """.asSmithyModel() - - @Test - fun `config override for credentials`() { - awsSdkIntegrationTest(model) { clientCodegenContext, rustCrate -> - val runtimeConfig = clientCodegenContext.runtimeConfig - val codegenScope = arrayOf( - *RuntimeType.preludeScope, - "Credentials" to AwsRuntimeType.awsCredentialTypesTestUtil(runtimeConfig) - .resolve("Credentials"), - "CredentialsCache" to AwsRuntimeType.awsCredentialTypes(runtimeConfig) - .resolve("cache::CredentialsCache"), - "Region" to AwsRuntimeType.awsTypes(runtimeConfig).resolve("region::Region"), - "ReplayEvent" to CargoDependency.smithyRuntime(runtimeConfig) - .toDevDependency().withFeature("test-util").toType() - .resolve("client::http::test_util::ReplayEvent"), - "RuntimePlugin" to RuntimeType.smithyRuntimeApi(runtimeConfig) - .resolve("client::runtime_plugin::RuntimePlugin"), - "SdkBody" to RuntimeType.sdkBody(runtimeConfig), - "SharedCredentialsCache" to AwsRuntimeType.awsCredentialTypes(runtimeConfig) - .resolve("cache::SharedCredentialsCache"), - "StaticReplayClient" to CargoDependency.smithyRuntime(runtimeConfig) - .toDevDependency().withFeature("test-util").toType() - .resolve("client::http::test_util::StaticReplayClient"), - ) - rustCrate.testModule { - unitTest( - "test_overriding_only_credentials_provider_should_panic", - additionalAttributes = listOf(Attribute.shouldPanic("also specify `.credentials_cache` when overriding credentials provider for the operation")), - ) { - rustTemplate( - """ - use #{RuntimePlugin}; - - let client_config = crate::config::Config::builder().build(); - let config_override = - crate::config::Config::builder().credentials_provider(#{Credentials}::for_tests()); - let sut = crate::config::ConfigOverrideRuntimePlugin::new( - config_override, - client_config.config, - &client_config.runtime_components, - ); - - // this should cause `panic!` - let _ = sut.config().unwrap(); - """, - *codegenScope, - ) - } - - unitTest( - "test_overriding_only_credentials_cache_should_panic", - additionalAttributes = listOf(Attribute.shouldPanic("also specify `.credentials_provider` when overriding credentials cache for the operation")), - ) { - rustTemplate( - """ - use #{RuntimePlugin}; - - let client_config = crate::config::Config::builder().build(); - let config_override = crate::config::Config::builder() - .credentials_cache(#{CredentialsCache}::no_caching()); - let sut = crate::config::ConfigOverrideRuntimePlugin::new( - config_override, - client_config.config, - &client_config.runtime_components, - ); - - // this should cause `panic!` - let _ = sut.config().unwrap(); - """, - *codegenScope, - ) - } - - unitTest("test_not_overriding_cache_and_provider_leads_to_no_shared_credentials_cache_in_layer") { - rustTemplate( - """ - use #{RuntimePlugin}; - - let client_config = crate::config::Config::builder().build(); - let config_override = crate::config::Config::builder(); - let sut = crate::config::ConfigOverrideRuntimePlugin::new( - config_override, - client_config.config, - &client_config.runtime_components, - ); - let sut_layer = sut.config().unwrap(); - assert!(sut_layer - .load::<#{SharedCredentialsCache}>() - .is_none()); - """, - *codegenScope, - ) - } - - tokioTest("test_specifying_credentials_provider_only_at_operation_level_should_work") { - // per https://github.com/awslabs/aws-sdk-rust/issues/901 - rustTemplate( - """ - let http_client = #{StaticReplayClient}::new( - vec![#{ReplayEvent}::new( - http::Request::builder() - .body(#{SdkBody}::from("request body")) - .unwrap(), - http::Response::builder() - .status(200) - .body(#{SdkBody}::from("response")) - .unwrap(), - )], - ); - let client_config = crate::config::Config::builder() - .http_client(http_client) - .build(); - let client = crate::client::Client::from_conf(client_config); - - let credentials = #{Credentials}::new( - "test", - "test", - #{None}, - #{None}, - "test", - ); - let operation_config_override = crate::config::Config::builder() - .credentials_cache(#{CredentialsCache}::no_caching()) - .credentials_provider(credentials.clone()) - .region(#{Region}::new("us-west-2")); - - let _ = client - .say_hello() - .customize() - .config_override(operation_config_override) - .send() - .await - .expect("success"); - """, - *codegenScope, - ) - } - } - } - } -} diff --git a/aws/sdk/integration-tests/no-default-features/Cargo.toml b/aws/sdk/integration-tests/no-default-features/Cargo.toml index 8408ac6694..ec8f24bd89 100644 --- a/aws/sdk/integration-tests/no-default-features/Cargo.toml +++ b/aws/sdk/integration-tests/no-default-features/Cargo.toml @@ -17,6 +17,7 @@ publish = false aws-config = { path = "../../build/aws-sdk/sdk/aws-config", default-features = false } aws-sdk-s3 = { path = "../../build/aws-sdk/sdk/s3", default-features = false } aws-smithy-async = { path = "../../build/aws-sdk/sdk/aws-smithy-async" } +aws-smithy-runtime= { path = "../../build/aws-sdk/sdk/aws-smithy-runtime", features = ["test-util"] } aws-credential-types = { path = "../../build/aws-sdk/sdk/aws-credential-types", features = ["test-util"] } futures = "0.3.25" tokio = { version = "1.23.1", features = ["full", "test-util"] } diff --git a/aws/sdk/integration-tests/no-default-features/tests/client-construction.rs b/aws/sdk/integration-tests/no-default-features/tests/client-construction.rs index e7d11dbc31..e1f8049055 100644 --- a/aws/sdk/integration-tests/no-default-features/tests/client-construction.rs +++ b/aws/sdk/integration-tests/no-default-features/tests/client-construction.rs @@ -3,9 +3,15 @@ * SPDX-License-Identifier: Apache-2.0 */ -use aws_sdk_s3::config::{Config, Credentials, SharedAsyncSleep, Sleep}; +use aws_sdk_s3::config::IdentityCache; +use aws_sdk_s3::config::{ + retry::RetryConfig, timeout::TimeoutConfig, Config, Credentials, Region, SharedAsyncSleep, + Sleep, +}; use aws_sdk_s3::error::DisplayErrorContext; use aws_smithy_async::rt::sleep::AsyncSleep; +use aws_smithy_runtime::client::http::test_util::capture_request; +use aws_smithy_runtime::test_util::capture_test_logs::capture_test_logs; use std::time::Duration; // This will fail due to lack of a connector when constructing the SDK Config @@ -51,3 +57,71 @@ async fn test_clients_from_service_config() { "expected '{msg}' to contain 'No HTTP client was available to send this request. Enable the `rustls` crate feature or set a HTTP client to fix this.'" ); } + +#[tokio::test] +#[should_panic( + expected = "Invalid client configuration: An async sleep implementation is required for retry to work." +)] +async fn test_missing_async_sleep_time_source_retries() { + let _logs = capture_test_logs(); + let (http_client, _) = capture_request(None); + + // Configure retry and timeouts without providing a sleep impl + let config = Config::builder() + .http_client(http_client) + .region(Region::new("us-east-1")) + .credentials_provider(Credentials::for_tests()) + .retry_config(RetryConfig::standard()) + .timeout_config(TimeoutConfig::disabled()) + .build(); + + // should panic with a validation error + let _client = aws_sdk_s3::Client::from_conf(config); +} + +#[tokio::test] +#[should_panic( + expected = "Invalid client configuration: An async sleep implementation is required for timeouts to work." +)] +async fn test_missing_async_sleep_time_source_timeouts() { + let _logs = capture_test_logs(); + let (http_client, _) = capture_request(None); + + // Configure retry and timeouts without providing a sleep impl + let config = Config::builder() + .http_client(http_client) + .region(Region::new("us-east-1")) + .credentials_provider(Credentials::for_tests()) + .retry_config(RetryConfig::disabled()) + .timeout_config( + TimeoutConfig::builder() + .operation_timeout(Duration::from_secs(5)) + .build(), + ) + .build(); + + // should panic with a validation error + let _client = aws_sdk_s3::Client::from_conf(config); +} + +#[tokio::test] +#[should_panic( + expected = "Invalid client configuration: Lazy identity caching requires an async sleep implementation to be configured." +)] +async fn test_time_source_for_identity_cache() { + let _logs = capture_test_logs(); + let (http_client, _) = capture_request(None); + + // Configure an identity cache without providing a sleep impl or time source + let config = Config::builder() + .http_client(http_client) + .region(Region::new("us-east-1")) + .identity_cache(IdentityCache::lazy().build()) + .credentials_provider(Credentials::for_tests()) + .retry_config(RetryConfig::disabled()) + .timeout_config(TimeoutConfig::disabled()) + .build(); + + // should panic with a validation error + let _client = aws_sdk_s3::Client::from_conf(config); +} diff --git a/aws/sdk/integration-tests/s3/tests/client_construction.rs b/aws/sdk/integration-tests/s3/tests/client_construction.rs new file mode 100644 index 0000000000..e7dbc11ce6 --- /dev/null +++ b/aws/sdk/integration-tests/s3/tests/client_construction.rs @@ -0,0 +1,42 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +mod with_sdk_config { + use aws_config::SdkConfig; + use aws_sdk_s3 as s3; + + #[tokio::test] + async fn using_config_loader() { + // When using `aws_config::load_from_env`, things should just work + let config = aws_config::load_from_env().await; + assert!(config.timeout_config().unwrap().has_timeouts()); + assert!(config.retry_config().unwrap().has_retry()); + let _s3 = s3::Client::new(&config); + } + + #[test] + fn manual_config_construction_all_defaults() { + // When manually constructing `SdkConfig` with everything unset, + // it should work since there will be no timeouts or retries enabled, + // and thus, no sleep impl is required. + let config = SdkConfig::builder().build(); + assert!(config.timeout_config().is_none()); + assert!(config.retry_config().is_none()); + let _s3 = s3::Client::new(&config); + } +} + +mod with_service_config { + use aws_sdk_s3 as s3; + + #[test] + fn manual_config_construction_all_defaults() { + // When manually constructing `Config` with everything unset, + // it should work since there will be no timeouts or retries enabled, + // and thus, no sleep impl is required. + let config = s3::Config::builder().build(); + let _s3 = s3::Client::from_conf(config); + } +} diff --git a/aws/sdk/integration-tests/s3/tests/sleep_impl_use_cases.rs b/aws/sdk/integration-tests/s3/tests/sleep_impl_use_cases.rs deleted file mode 100644 index dfa82e9660..0000000000 --- a/aws/sdk/integration-tests/s3/tests/sleep_impl_use_cases.rs +++ /dev/null @@ -1,199 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -mod with_sdk_config { - use aws_config::retry::RetryConfig; - use aws_config::timeout::TimeoutConfig; - use aws_config::SdkConfig; - use aws_sdk_s3 as s3; - use aws_smithy_async::rt::sleep::SharedAsyncSleep; - use std::time::Duration; - - #[tokio::test] - async fn using_config_loader() { - // When using `aws_config::load_from_env`, things should just work - let config = aws_config::load_from_env().await; - assert!(config.timeout_config().unwrap().has_timeouts()); - assert!(config.retry_config().unwrap().has_retry()); - let _s3 = s3::Client::new(&config); - } - - #[test] - fn manual_config_construction_all_defaults() { - // When manually constructing `SdkConfig` with everything unset, - // it should work since there will be no timeouts or retries enabled, - // and thus, no sleep impl is required. - let config = SdkConfig::builder().build(); - assert!(config.timeout_config().is_none()); - assert!(config.retry_config().is_none()); - let _s3 = s3::Client::new(&config); - } - - #[test] - fn no_sleep_no_timeouts_no_retries() { - // When explicitly setting timeouts and retries to their disabled - // states, it should work since no sleep impl is required. - let config = SdkConfig::builder() - .timeout_config(TimeoutConfig::disabled()) - .retry_config(RetryConfig::disabled()) - .build(); - assert!(!config.timeout_config().unwrap().has_timeouts()); - assert!(!config.retry_config().unwrap().has_retry()); - let _s3 = s3::Client::new(&config); - } - - #[test] - #[should_panic] - fn no_sleep_no_timeouts_yes_retries() { - // When retries are enabled and a sleep impl isn't given, it should panic - let config = SdkConfig::builder() - .timeout_config(TimeoutConfig::disabled()) - .retry_config(RetryConfig::standard()) - .build(); - assert!(!config.timeout_config().unwrap().has_timeouts()); - assert!(config.retry_config().unwrap().has_retry()); - let _s3 = s3::Client::new(&config); - } - - #[test] - #[should_panic] - fn no_sleep_yes_timeouts_no_retries() { - // When timeouts are enabled and a sleep impl isn't given, it should panic - let config = SdkConfig::builder() - .timeout_config( - TimeoutConfig::builder() - .operation_timeout(Duration::from_millis(100)) - .build(), - ) - .retry_config(RetryConfig::disabled()) - .build(); - assert!(config.timeout_config().unwrap().has_timeouts()); - assert!(!config.retry_config().unwrap().has_retry()); - let _s3 = s3::Client::new(&config); - } - - #[test] - #[should_panic] - fn no_sleep_yes_timeouts_yes_retries() { - // When timeouts and retries are enabled but a sleep impl isn't given, it should panic - let config = SdkConfig::builder() - .timeout_config( - TimeoutConfig::builder() - .operation_timeout(Duration::from_millis(100)) - .build(), - ) - .retry_config(RetryConfig::standard().with_max_attempts(2)) - .build(); - assert!(config.timeout_config().unwrap().has_timeouts()); - assert!(config.retry_config().unwrap().has_retry()); - let _s3 = s3::Client::new(&config); - } - - #[test] - fn yes_sleep_yes_timeouts_yes_retries() { - // When a sleep impl is given, enabling timeouts/retries should work - let config = SdkConfig::builder() - .timeout_config( - TimeoutConfig::builder() - .operation_timeout(Duration::from_millis(100)) - .build(), - ) - .retry_config(RetryConfig::standard().with_max_attempts(2)) - .sleep_impl(SharedAsyncSleep::new( - aws_smithy_async::rt::sleep::TokioSleep::new(), - )) - .build(); - assert!(config.timeout_config().unwrap().has_timeouts()); - assert!(config.retry_config().unwrap().has_retry()); - let _s3 = s3::Client::new(&config); - } -} - -mod with_service_config { - use aws_config::retry::RetryConfig; - use aws_config::timeout::TimeoutConfig; - use aws_config::SdkConfig; - use aws_sdk_s3 as s3; - use aws_smithy_async::rt::sleep::SharedAsyncSleep; - use std::time::Duration; - - #[test] - fn manual_config_construction_all_defaults() { - // When manually constructing `Config` with everything unset, - // it should work since there will be no timeouts or retries enabled, - // and thus, no sleep impl is required. - let config = s3::Config::builder().build(); - let _s3 = s3::Client::from_conf(config); - } - - #[test] - fn no_sleep_no_timeouts_no_retries() { - // When explicitly setting timeouts and retries to their disabled - // states, it should work since no sleep impl is required. - let config = s3::Config::builder() - .timeout_config(TimeoutConfig::disabled()) - .retry_config(RetryConfig::disabled()) - .build(); - let _s3 = s3::Client::from_conf(config); - } - - #[test] - #[should_panic] - fn no_sleep_no_timeouts_yes_retries() { - // When retries are enabled and a sleep impl isn't given, it should panic - let config = s3::Config::builder() - .timeout_config(TimeoutConfig::disabled()) - .retry_config(RetryConfig::standard()) - .build(); - let _s3 = s3::Client::from_conf(config); - } - - #[test] - #[should_panic] - fn no_sleep_yes_timeouts_no_retries() { - // When timeouts are enabled and a sleep impl isn't given, it should panic - let config = s3::Config::builder() - .timeout_config( - TimeoutConfig::builder() - .operation_timeout(Duration::from_millis(100)) - .build(), - ) - .retry_config(RetryConfig::disabled()) - .build(); - let _s3 = s3::Client::from_conf(config); - } - - #[test] - #[should_panic] - fn no_sleep_yes_timeouts_yes_retries() { - // When retries and timeouts are enabled and a sleep impl isn't given, it should panic - let config = s3::Config::builder() - .timeout_config( - TimeoutConfig::builder() - .operation_timeout(Duration::from_millis(100)) - .build(), - ) - .retry_config(RetryConfig::standard().with_max_attempts(2)) - .build(); - let _s3 = s3::Client::from_conf(config); - } - - #[test] - fn yes_sleep_yes_timeouts_yes_retries() { - // When a sleep impl is given, enabling timeouts/retries should work - let config = SdkConfig::builder() - .timeout_config( - TimeoutConfig::builder() - .operation_timeout(Duration::from_millis(100)) - .build(), - ) - .retry_config(RetryConfig::standard().with_max_attempts(2)) - .sleep_impl(SharedAsyncSleep::new( - aws_smithy_async::rt::sleep::TokioSleep::new(), - )) - .build(); - let _s3 = s3::Client::new(&config); - } -} diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/IdentityCacheDecorator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/IdentityCacheDecorator.kt new file mode 100644 index 0000000000..d3015e7693 --- /dev/null +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/IdentityCacheDecorator.kt @@ -0,0 +1,105 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rust.codegen.client.smithy.customizations + +import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext +import software.amazon.smithy.rust.codegen.client.smithy.generators.config.ConfigCustomization +import software.amazon.smithy.rust.codegen.client.smithy.generators.config.ServiceConfig +import software.amazon.smithy.rust.codegen.core.rustlang.Writable +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.RuntimeType +import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType.Companion.preludeScope + +class IdentityCacheConfigCustomization(codegenContext: ClientCodegenContext) : ConfigCustomization() { + private val moduleUseName = codegenContext.moduleUseName() + + private val codegenScope = codegenContext.runtimeConfig.let { rc -> + val api = RuntimeType.smithyRuntimeApi(rc) + arrayOf( + *preludeScope, + "ResolveCachedIdentity" to api.resolve("client::identity::ResolveCachedIdentity"), + "SharedIdentityCache" to api.resolve("client::identity::SharedIdentityCache"), + ) + } + + override fun section(section: ServiceConfig): Writable = writable { + when (section) { + is ServiceConfig.BuilderImpl -> { + val docs = """ + /// Set the identity cache for auth. + /// + /// The identity cache defaults to a lazy caching implementation that will resolve + /// an identity when it is requested, and place it in the cache thereafter. Subsequent + /// requests will take the value from the cache while it is still valid. Once it expires, + /// the next request will result in refreshing the identity. + /// + /// This configuration allows you to disable or change the default caching mechanism. + /// To use a custom caching mechanism, implement the [`ResolveCachedIdentity`](#{ResolveCachedIdentity}) + /// trait and pass that implementation into this function. + /// + /// ## Examples + /// + /// Disabling identity caching: + /// ```no_run + /// use $moduleUseName::config::IdentityCache; + /// + /// let config = $moduleUseName::Config::builder() + /// .identity_cache(IdentityCache::no_cache()) + /// // ... + /// .build(); + /// let client = $moduleUseName::Client::from_conf(config); + /// ``` + /// + /// Customizing lazy caching: + /// ```no_run + /// use $moduleUseName::config::IdentityCache; + /// use std::time::Duration; + /// + /// let config = $moduleUseName::Config::builder() + /// .identity_cache( + /// IdentityCache::lazy() + /// // change the load timeout to 10 seconds + /// .load_timeout(Duration::from_secs(10)) + /// .build() + /// ) + /// // ... + /// .build(); + /// let client = $moduleUseName::Client::from_conf(config); + /// ``` + """ + rustTemplate( + """ + $docs + pub fn identity_cache(mut self, identity_cache: impl #{ResolveCachedIdentity} + 'static) -> Self { + self.set_identity_cache(identity_cache); + self + } + + $docs + pub fn set_identity_cache(&mut self, identity_cache: impl #{ResolveCachedIdentity} + 'static) -> &mut Self { + self.runtime_components.set_identity_cache(#{Some}(identity_cache)); + self + } + """, + *codegenScope, + ) + } + is ServiceConfig.ConfigImpl -> { + rustTemplate( + """ + /// Returns the configured identity cache for auth. + pub fn identity_cache(&self) -> #{Option}<#{SharedIdentityCache}> { + self.runtime_components.identity_cache() + } + """, + *codegenScope, + ) + } + else -> { } + } + } +} diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customize/RequiredCustomizations.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customize/RequiredCustomizations.kt index d47ac45fd9..a1956f442e 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customize/RequiredCustomizations.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customize/RequiredCustomizations.kt @@ -10,6 +10,7 @@ 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.customizations.ConnectionPoisoningRuntimePluginCustomization import software.amazon.smithy.rust.codegen.client.smithy.customizations.HttpChecksumRequiredGenerator +import software.amazon.smithy.rust.codegen.client.smithy.customizations.IdentityCacheConfigCustomization import software.amazon.smithy.rust.codegen.client.smithy.customizations.InterceptorConfigCustomization import software.amazon.smithy.rust.codegen.client.smithy.customizations.MetadataCustomization import software.amazon.smithy.rust.codegen.client.smithy.customizations.ResiliencyConfigCustomization @@ -56,6 +57,7 @@ class RequiredCustomizations : ClientCodegenDecorator { baseCustomizations: List, ): List = baseCustomizations + ResiliencyConfigCustomization(codegenContext) + + IdentityCacheConfigCustomization(codegenContext) + InterceptorConfigCustomization(codegenContext) + TimeSourceCustomization(codegenContext) + RetryClassifierConfigCustomization(codegenContext) diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ClientRuntimeTypesReExportGenerator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ClientRuntimeTypesReExportGenerator.kt index 88e6bd2ffd..690bf41a0b 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ClientRuntimeTypesReExportGenerator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ClientRuntimeTypesReExportGenerator.kt @@ -27,11 +27,13 @@ class ClientRuntimeTypesReExportGenerator( pub use #{Intercept}; pub use #{RuntimeComponents}; pub use #{SharedInterceptor}; + pub use #{IdentityCache}; """, "ConfigBag" to RuntimeType.configBag(rc), "Intercept" to RuntimeType.intercept(rc), "RuntimeComponents" to RuntimeType.runtimeComponents(rc), "SharedInterceptor" to RuntimeType.sharedInterceptor(rc), + "IdentityCache" to RuntimeType.smithyRuntime(rc).resolve("client::identity::IdentityCache"), ) if (codegenContext.enableUserConfigurableRuntimePlugins) { diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/FluentClientGenerator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/FluentClientGenerator.kt index dfd11ed295..9d2bc151e7 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/FluentClientGenerator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/FluentClientGenerator.kt @@ -18,6 +18,7 @@ import software.amazon.smithy.rust.codegen.client.smithy.generators.PaginatorGen import software.amazon.smithy.rust.codegen.client.smithy.generators.isPaginated import software.amazon.smithy.rust.codegen.core.rustlang.Attribute import software.amazon.smithy.rust.codegen.core.rustlang.Attribute.Companion.derive +import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency import software.amazon.smithy.rust.codegen.core.rustlang.EscapeFor import software.amazon.smithy.rust.codegen.core.rustlang.RustModule import software.amazon.smithy.rust.codegen.core.rustlang.RustReservedWords @@ -94,23 +95,6 @@ class FluentClientGenerator( private fun renderFluentClient(crate: RustCrate) { crate.withModule(ClientRustModule.client) { - val clientScope = arrayOf( - *preludeScope, - "Arc" to RuntimeType.Arc, - "client_docs" to writable - { - customizations.forEach { - it.section( - FluentClientSection.FluentClientDocs( - serviceShape, - ), - )(this) - } - }, - "RetryConfig" to RuntimeType.smithyTypes(runtimeConfig).resolve("retry::RetryConfig"), - "RuntimePlugins" to RuntimeType.runtimePlugins(runtimeConfig), - "TimeoutConfig" to RuntimeType.smithyTypes(runtimeConfig).resolve("timeout::TimeoutConfig"), - ) rustTemplate( """ ##[derive(Debug)] @@ -131,27 +115,22 @@ class FluentClientGenerator( /// /// ## Panics /// - /// This method will panic if the `conf` has retry or timeouts enabled without a `sleep_impl`. - /// If you experience this panic, it can be fixed by setting the `sleep_impl`, or by disabling - /// retries and timeouts. + /// This method will panic in the following cases: + /// + /// - Retries or timeouts are enabled without a `sleep_impl` configured. + /// - Identity caching is enabled without a `sleep_impl` and `time_source` configured. + /// + /// The panic message for each of these will have instructions on how to resolve them. pub fn from_conf(conf: crate::Config) -> Self { - let has_retry_config = conf.retry_config().map(#{RetryConfig}::has_retry).unwrap_or_default(); - let has_timeout_config = conf.timeout_config().map(#{TimeoutConfig}::has_timeouts).unwrap_or_default(); - let sleep_impl = conf.sleep_impl(); - if (has_retry_config || has_timeout_config) && sleep_impl.is_none() { - panic!( - "An async sleep implementation is required for retries or timeouts to work. \ - Set the `sleep_impl` on the Config passed into this function to fix this panic." - ); + let handle = Handle { + conf: conf.clone(), + runtime_plugins: #{base_client_runtime_plugins}(conf), + }; + if let Err(err) = Self::validate_config(&handle) { + panic!("Invalid client configuration: {err}"); } - Self { - handle: #{Arc}::new( - Handle { - conf: conf.clone(), - runtime_plugins: #{base_client_runtime_plugins}(conf), - } - ) + handle: #{Arc}::new(handle) } } @@ -159,10 +138,32 @@ class FluentClientGenerator( pub fn config(&self) -> &crate::Config { &self.handle.conf } + + fn validate_config(handle: &Handle) -> Result<(), #{BoxError}> { + let mut cfg = #{ConfigBag}::base(); + handle.runtime_plugins + .apply_client_configuration(&mut cfg)? + .validate_base_client_config(&cfg)?; + Ok(()) + } } """, - *clientScope, + *preludeScope, + "Arc" to RuntimeType.Arc, "base_client_runtime_plugins" to baseClientRuntimePluginsFn(codegenContext), + "BoxError" to RuntimeType.boxError(runtimeConfig), + "client_docs" to writable { + customizations.forEach { + it.section( + FluentClientSection.FluentClientDocs( + serviceShape, + ), + )(this) + } + }, + "ConfigBag" to RuntimeType.configBag(runtimeConfig), + "RuntimePlugins" to RuntimeType.runtimePlugins(runtimeConfig), + "tracing" to CargoDependency.Tracing.toType(), ) } @@ -459,17 +460,12 @@ private fun baseClientRuntimePluginsFn(codegenContext: ClientCodegenContext): Ru let mut configured_plugins = #{Vec}::new(); ::std::mem::swap(&mut config.runtime_plugins, &mut configured_plugins); - let defaults = [ - #{default_http_client_plugin}(), - #{default_retry_config_plugin}(${codegenContext.serviceShape.sdkId().dq()}), - #{default_sleep_impl_plugin}(), - #{default_time_source_plugin}(), - #{default_timeout_config_plugin}(), - ].into_iter().flatten(); - let mut plugins = #{RuntimePlugins}::new() // defaults - .with_client_plugins(defaults) + .with_client_plugins(#{default_plugins}( + #{DefaultPluginParams}::new() + .with_retry_partition_name(${codegenContext.serviceShape.sdkId().dq()}) + )) // user config .with_client_plugin( #{StaticRuntimePlugin}::new() @@ -486,11 +482,8 @@ private fun baseClientRuntimePluginsFn(codegenContext: ClientCodegenContext): Ru } """, *preludeScope, - "default_http_client_plugin" to rt.resolve("client::defaults::default_http_client_plugin"), - "default_retry_config_plugin" to rt.resolve("client::defaults::default_retry_config_plugin"), - "default_sleep_impl_plugin" to rt.resolve("client::defaults::default_sleep_impl_plugin"), - "default_timeout_config_plugin" to rt.resolve("client::defaults::default_timeout_config_plugin"), - "default_time_source_plugin" to rt.resolve("client::defaults::default_time_source_plugin"), + "DefaultPluginParams" to rt.resolve("client::defaults::DefaultPluginParams"), + "default_plugins" to rt.resolve("client::defaults::default_plugins"), "NoAuthRuntimePlugin" to rt.resolve("client::auth::no_auth::NoAuthRuntimePlugin"), "RuntimePlugins" to RuntimeType.runtimePlugins(rc), "StaticRuntimePlugin" to api.resolve("client::runtime_plugin::StaticRuntimePlugin"), diff --git a/rust-runtime/aws-smithy-runtime-api/src/client/auth.rs b/rust-runtime/aws-smithy-runtime-api/src/client/auth.rs index e6bb9c3440..8cc11806f9 100644 --- a/rust-runtime/aws-smithy-runtime-api/src/client/auth.rs +++ b/rust-runtime/aws-smithy-runtime-api/src/client/auth.rs @@ -8,6 +8,7 @@ use crate::box_error::BoxError; use crate::client::identity::{Identity, SharedIdentityResolver}; use crate::client::orchestrator::HttpRequest; +use crate::client::runtime_components::sealed::ValidateConfig; use crate::client::runtime_components::{GetIdentityResolver, RuntimeComponents}; use crate::impl_shared_conversions; use aws_smithy_types::config_bag::{ConfigBag, Storable, StoreReplace}; @@ -187,6 +188,8 @@ impl AuthScheme for SharedAuthScheme { } } +impl ValidateConfig for SharedAuthScheme {} + impl_shared_conversions!(convert SharedAuthScheme from AuthScheme using SharedAuthScheme::new); #[deprecated(note = "Renamed to Sign.")] diff --git a/rust-runtime/aws-smithy-runtime-api/src/client/endpoint.rs b/rust-runtime/aws-smithy-runtime-api/src/client/endpoint.rs index 8c138075fa..a0818cf4ec 100644 --- a/rust-runtime/aws-smithy-runtime-api/src/client/endpoint.rs +++ b/rust-runtime/aws-smithy-runtime-api/src/client/endpoint.rs @@ -6,6 +6,7 @@ //! APIs needed to configure endpoint resolution for clients. use crate::box_error::BoxError; +use crate::client::runtime_components::sealed::ValidateConfig; use crate::impl_shared_conversions; use aws_smithy_types::config_bag::{Storable, StoreReplace}; use aws_smithy_types::endpoint::Endpoint; @@ -70,4 +71,6 @@ impl ResolveEndpoint for SharedEndpointResolver { } } +impl ValidateConfig for SharedEndpointResolver {} + impl_shared_conversions!(convert SharedEndpointResolver from ResolveEndpoint using SharedEndpointResolver::new); diff --git a/rust-runtime/aws-smithy-runtime-api/src/client/http.rs b/rust-runtime/aws-smithy-runtime-api/src/client/http.rs index f271540f11..321e70198d 100644 --- a/rust-runtime/aws-smithy-runtime-api/src/client/http.rs +++ b/rust-runtime/aws-smithy-runtime-api/src/client/http.rs @@ -54,6 +54,7 @@ pub mod request; pub mod response; use crate::client::orchestrator::{HttpRequest, HttpResponse}; +use crate::client::runtime_components::sealed::ValidateConfig; use crate::client::runtime_components::RuntimeComponents; use crate::impl_shared_conversions; use aws_smithy_http::result::ConnectorError; @@ -172,6 +173,8 @@ impl HttpClient for SharedHttpClient { } } +impl ValidateConfig for SharedHttpClient {} + impl_shared_conversions!(convert SharedHttpClient from HttpClient using SharedHttpClient::new); /// Builder for [`HttpConnectorSettings`]. diff --git a/rust-runtime/aws-smithy-runtime-api/src/client/identity.rs b/rust-runtime/aws-smithy-runtime-api/src/client/identity.rs index 8ec6fb8851..fae5e86055 100644 --- a/rust-runtime/aws-smithy-runtime-api/src/client/identity.rs +++ b/rust-runtime/aws-smithy-runtime-api/src/client/identity.rs @@ -5,7 +5,8 @@ use crate::box_error::BoxError; use crate::client::auth::AuthSchemeId; -use crate::client::runtime_components::RuntimeComponents; +use crate::client::runtime_components::sealed::ValidateConfig; +use crate::client::runtime_components::{RuntimeComponents, RuntimeComponentsBuilder}; use crate::impl_shared_conversions; use aws_smithy_types::config_bag::ConfigBag; use std::any::Any; @@ -62,6 +63,37 @@ pub trait ResolveCachedIdentity: fmt::Debug + Send + Sync { runtime_components: &'a RuntimeComponents, config_bag: &'a ConfigBag, ) -> IdentityFuture<'a>; + + /// Validate the base client configuration for this implementation. + /// + /// This gets called upon client construction. The full config may not be available at + /// this time (hence why it has [`RuntimeComponentsBuilder`] as an argument rather + /// than [`RuntimeComponents`]). Any error returned here will become a panic + /// in the client constructor. + fn validate_base_client_config( + &self, + runtime_components: &RuntimeComponentsBuilder, + cfg: &ConfigBag, + ) -> Result<(), BoxError> { + let _ = (runtime_components, cfg); + Ok(()) + } + + /// Validate the final client configuration for this implementation. + /// + /// This gets called immediately after the [`Intercept::read_before_execution`] trait hook + /// when the final configuration has been resolved. Any error returned here will + /// cause the operation to return that error. + /// + /// [`Intercept::read_before_execution`]: crate::client::interceptors::Intercept::read_before_execution + fn validate_final_config( + &self, + runtime_components: &RuntimeComponents, + cfg: &ConfigBag, + ) -> Result<(), BoxError> { + let _ = (runtime_components, cfg); + Ok(()) + } } /// Shared identity cache. @@ -87,6 +119,24 @@ impl ResolveCachedIdentity for SharedIdentityCache { } } +impl ValidateConfig for SharedIdentityCache { + fn validate_base_client_config( + &self, + runtime_components: &RuntimeComponentsBuilder, + cfg: &ConfigBag, + ) -> Result<(), BoxError> { + self.0.validate_base_client_config(runtime_components, cfg) + } + + fn validate_final_config( + &self, + runtime_components: &RuntimeComponents, + cfg: &ConfigBag, + ) -> Result<(), BoxError> { + self.0.validate_final_config(runtime_components, cfg) + } +} + impl_shared_conversions!(convert SharedIdentityCache from ResolveCachedIdentity using SharedIdentityCache::new); #[deprecated(note = "Renamed to ResolveIdentity.")] @@ -191,6 +241,8 @@ impl ConfiguredIdentityResolver { } } +impl ValidateConfig for ConfiguredIdentityResolver {} + /// An identity that can be used for authentication. /// /// The [`Identity`] is a container for any arbitrary identity data that may be used diff --git a/rust-runtime/aws-smithy-runtime-api/src/client/interceptors.rs b/rust-runtime/aws-smithy-runtime-api/src/client/interceptors.rs index 2dc05f7bcf..370742c249 100644 --- a/rust-runtime/aws-smithy-runtime-api/src/client/interceptors.rs +++ b/rust-runtime/aws-smithy-runtime-api/src/client/interceptors.rs @@ -15,6 +15,7 @@ use crate::client::interceptors::context::{ BeforeTransmitInterceptorContextRef, FinalizerInterceptorContextMut, FinalizerInterceptorContextRef, }; +use crate::client::runtime_components::sealed::ValidateConfig; use crate::client::runtime_components::RuntimeComponents; use aws_smithy_types::config_bag::{ConfigBag, Storable, StoreReplace}; use std::fmt; @@ -621,6 +622,8 @@ impl SharedInterceptor { } } +impl ValidateConfig for SharedInterceptor {} + impl Intercept for SharedInterceptor { fn name(&self) -> &'static str { self.interceptor.name() diff --git a/rust-runtime/aws-smithy-runtime-api/src/client/retries.rs b/rust-runtime/aws-smithy-runtime-api/src/client/retries.rs index e3616e42b3..036371ddc1 100644 --- a/rust-runtime/aws-smithy-runtime-api/src/client/retries.rs +++ b/rust-runtime/aws-smithy-runtime-api/src/client/retries.rs @@ -12,6 +12,7 @@ pub mod classifiers; use crate::box_error::BoxError; use crate::client::interceptors::context::InterceptorContext; +use crate::client::runtime_components::sealed::ValidateConfig; use crate::client::runtime_components::RuntimeComponents; use aws_smithy_types::config_bag::{ConfigBag, Storable, StoreReplace}; use std::fmt; @@ -107,6 +108,8 @@ impl RetryStrategy for SharedRetryStrategy { } } +impl ValidateConfig for SharedRetryStrategy {} + /// A type to track the number of requests sent by the orchestrator for a given operation. /// /// `RequestAttempts` is added to the `ConfigBag` by the orchestrator, diff --git a/rust-runtime/aws-smithy-runtime-api/src/client/retries/classifiers.rs b/rust-runtime/aws-smithy-runtime-api/src/client/retries/classifiers.rs index 549b855956..fc8544c7cb 100644 --- a/rust-runtime/aws-smithy-runtime-api/src/client/retries/classifiers.rs +++ b/rust-runtime/aws-smithy-runtime-api/src/client/retries/classifiers.rs @@ -6,6 +6,7 @@ //! Classifier for determining if a retry is necessary and related code. use crate::client::interceptors::context::InterceptorContext; +use crate::client::runtime_components::sealed::ValidateConfig; use crate::impl_shared_conversions; use aws_smithy_types::retry::ErrorKind; use std::fmt; @@ -237,6 +238,8 @@ impl ClassifyRetry for SharedRetryClassifier { } } +impl ValidateConfig for SharedRetryClassifier {} + #[cfg(test)] mod tests { use super::RetryClassifierPriority; diff --git a/rust-runtime/aws-smithy-runtime-api/src/client/runtime_components.rs b/rust-runtime/aws-smithy-runtime-api/src/client/runtime_components.rs index 6871f59f22..2a994d7e5a 100644 --- a/rust-runtime/aws-smithy-runtime-api/src/client/runtime_components.rs +++ b/rust-runtime/aws-smithy-runtime-api/src/client/runtime_components.rs @@ -11,6 +11,7 @@ //! [`ConfigBag`](aws_smithy_types::config_bag::ConfigBag) instead of in //! [`RuntimeComponents`](RuntimeComponents). +use crate::box_error::BoxError; use crate::client::auth::{ AuthScheme, AuthSchemeId, ResolveAuthSchemeOptions, SharedAuthScheme, SharedAuthSchemeOptionResolver, @@ -18,19 +19,162 @@ use crate::client::auth::{ use crate::client::endpoint::{ResolveEndpoint, SharedEndpointResolver}; use crate::client::http::{HttpClient, SharedHttpClient}; use crate::client::identity::{ - ConfiguredIdentityResolver, ResolveIdentity, SharedIdentityResolver, + ConfiguredIdentityResolver, ResolveCachedIdentity, ResolveIdentity, SharedIdentityCache, + SharedIdentityResolver, }; use crate::client::interceptors::{Intercept, SharedInterceptor}; use crate::client::retries::classifiers::{ClassifyRetry, SharedRetryClassifier}; use crate::client::retries::{RetryStrategy, SharedRetryStrategy}; +use crate::impl_shared_conversions; use crate::shared::IntoShared; use aws_smithy_async::rt::sleep::{AsyncSleep, SharedAsyncSleep}; use aws_smithy_async::time::{SharedTimeSource, TimeSource}; +use aws_smithy_types::config_bag::ConfigBag; use std::fmt; +use std::sync::Arc; pub(crate) static EMPTY_RUNTIME_COMPONENTS_BUILDER: RuntimeComponentsBuilder = RuntimeComponentsBuilder::new("empty"); +pub(crate) mod sealed { + use super::*; + + /// Validates client configuration. + /// + /// This trait can be used to validate that certain required components or config values + /// are available, and provide an error with helpful instructions if they are not. + pub trait ValidateConfig: fmt::Debug + Send + Sync { + /// Validate the base client configuration. + /// + /// This gets called upon client construction. The full config may not be available at + /// this time (hence why it has [`RuntimeComponentsBuilder`] as an argument rather + /// than [`RuntimeComponents`]). Any error returned here will become a panic + /// in the client constructor. + fn validate_base_client_config( + &self, + runtime_components: &RuntimeComponentsBuilder, + cfg: &ConfigBag, + ) -> Result<(), BoxError> { + let _ = (runtime_components, cfg); + Ok(()) + } + + /// Validate the final client configuration. + /// + /// This gets called immediately after the [`Intercept::read_before_execution`] trait hook + /// when the final configuration has been resolved. Any error returned here will + /// cause the operation to return that error. + fn validate_final_config( + &self, + runtime_components: &RuntimeComponents, + cfg: &ConfigBag, + ) -> Result<(), BoxError> { + let _ = (runtime_components, cfg); + Ok(()) + } + } +} +use sealed::ValidateConfig; + +#[derive(Clone)] +enum ValidatorInner { + BaseConfigStaticFn(fn(&RuntimeComponentsBuilder, &ConfigBag) -> Result<(), BoxError>), + Shared(Arc), +} + +impl fmt::Debug for ValidatorInner { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::BaseConfigStaticFn(_) => f.debug_tuple("StaticFn").finish(), + Self::Shared(_) => f.debug_tuple("Shared").finish(), + } + } +} + +/// A client config validator. +#[derive(Clone, Debug)] +pub struct SharedConfigValidator { + inner: ValidatorInner, +} + +impl SharedConfigValidator { + /// Creates a new shared config validator. + pub(crate) fn new(validator: impl ValidateConfig + 'static) -> Self { + Self { + inner: ValidatorInner::Shared(Arc::new(validator) as _), + } + } + + /// Creates a base client validator from a function. + /// + /// A base client validator gets called upon client construction. The full + /// config may not be available at this time (hence why it has + /// [`RuntimeComponentsBuilder`] as an argument rather than [`RuntimeComponents`]). + /// Any error returned from the validator function will become a panic in the + /// client constructor. + /// + /// # Examples + /// + /// Creating a validator function: + /// ```no_run + /// use aws_smithy_runtime_api::box_error::BoxError; + /// use aws_smithy_runtime_api::client::runtime_components::{ + /// RuntimeComponentsBuilder, + /// SharedConfigValidator + /// }; + /// use aws_smithy_types::config_bag::ConfigBag; + /// + /// fn my_validation( + /// components: &RuntimeComponentsBuilder, + /// config: &ConfigBag + /// ) -> Result<(), BoxError> { + /// if components.sleep_impl().is_none() { + /// return Err("I need a sleep_impl!".into()); + /// } + /// Ok(()) + /// } + /// + /// let validator = SharedConfigValidator::base_client_config_fn(my_validation); + /// ``` + pub fn base_client_config_fn( + validator: fn(&RuntimeComponentsBuilder, &ConfigBag) -> Result<(), BoxError>, + ) -> Self { + Self { + inner: ValidatorInner::BaseConfigStaticFn(validator), + } + } +} + +impl ValidateConfig for SharedConfigValidator { + fn validate_base_client_config( + &self, + runtime_components: &RuntimeComponentsBuilder, + cfg: &ConfigBag, + ) -> Result<(), BoxError> { + match &self.inner { + ValidatorInner::BaseConfigStaticFn(validator) => (validator)(runtime_components, cfg), + ValidatorInner::Shared(validator) => { + validator.validate_base_client_config(runtime_components, cfg) + } + } + } + + fn validate_final_config( + &self, + runtime_components: &RuntimeComponents, + cfg: &ConfigBag, + ) -> Result<(), BoxError> { + match &self.inner { + ValidatorInner::Shared(validator) => { + validator.validate_final_config(runtime_components, cfg) + } + _ => Ok(()), + } + } +} + +impl_shared_conversions!(convert SharedConfigValidator from ValidateConfig using SharedConfigValidator::new); + /// Internal to `declare_runtime_components!`. /// /// Merges a field from one builder into another. @@ -196,6 +340,9 @@ declare_runtime_components! { #[atLeastOneRequired] auth_schemes: Vec, + #[required] + identity_cache: Option, + #[atLeastOneRequired] identity_resolvers: Vec, @@ -209,6 +356,8 @@ declare_runtime_components! { time_source: Option, sleep_impl: Option, + + config_validators: Vec, } } @@ -241,6 +390,11 @@ impl RuntimeComponents { .map(|s| s.value.clone()) } + /// Returns the identity cache. + pub fn identity_cache(&self) -> SharedIdentityCache { + self.identity_cache.value.clone() + } + /// Returns an iterator over the interceptors. pub fn interceptors(&self) -> impl Iterator + '_ { self.interceptors.iter().map(|s| s.value.clone()) @@ -265,6 +419,45 @@ impl RuntimeComponents { pub fn time_source(&self) -> Option { self.time_source.as_ref().map(|s| s.value.clone()) } + + /// Returns the config validators. + pub fn config_validators(&self) -> impl Iterator + '_ { + self.config_validators.iter().map(|s| s.value.clone()) + } + + /// Validate the final client configuration. + /// + /// This is intended to be called internally by the client. + pub fn validate_final_config(&self, cfg: &ConfigBag) -> Result<(), BoxError> { + macro_rules! validate { + (Required: $field:expr) => { + ValidateConfig::validate_final_config(&$field.value, self, cfg)?; + }; + (Option: $field:expr) => { + if let Some(field) = $field.as_ref() { + ValidateConfig::validate_final_config(&field.value, self, cfg)?; + } + }; + (Vec: $field:expr) => { + for entry in &$field { + ValidateConfig::validate_final_config(&entry.value, self, cfg)?; + } + }; + } + + tracing::trace!(runtime_components=?self, cfg=?cfg, "validating final config"); + for validator in self.config_validators() { + validator.validate_final_config(self, cfg)?; + } + validate!(Option: self.http_client); + validate!(Required: self.endpoint_resolver); + validate!(Vec: self.auth_schemes); + validate!(Required: self.identity_cache); + validate!(Vec: self.identity_resolvers); + validate!(Vec: self.interceptors); + validate!(Required: self.retry_strategy); + Ok(()) + } } impl RuntimeComponentsBuilder { @@ -353,6 +546,30 @@ impl RuntimeComponentsBuilder { self } + /// Returns the identity cache. + pub fn identity_cache(&self) -> Option { + self.identity_cache.as_ref().map(|s| s.value.clone()) + } + + /// Sets the identity cache. + pub fn set_identity_cache( + &mut self, + identity_cache: Option, + ) -> &mut Self { + self.identity_cache = + identity_cache.map(|c| Tracked::new(self.builder_name, c.into_shared())); + self + } + + /// Sets the identity cache. + pub fn with_identity_cache( + mut self, + identity_cache: Option, + ) -> Self { + self.set_identity_cache(identity_cache); + self + } + /// Adds an identity resolver. pub fn push_identity_resolver( &mut self, @@ -429,7 +646,7 @@ impl RuntimeComponentsBuilder { self.retry_classifiers.iter().map(|s| s.value.clone()) } - /// Adds all the given retry_classifiers. + /// Adds all the given retry classifiers. pub fn extend_retry_classifiers( &mut self, retry_classifiers: impl Iterator, @@ -526,6 +743,68 @@ impl RuntimeComponentsBuilder { self.time_source = time_source.map(|s| Tracked::new(self.builder_name, s.into_shared())); self } + + /// Returns the config validators. + pub fn config_validators(&self) -> impl Iterator + '_ { + self.config_validators.iter().map(|s| s.value.clone()) + } + + /// Adds all the given config validators. + pub fn extend_config_validators( + &mut self, + config_validators: impl Iterator, + ) -> &mut Self { + self.config_validators + .extend(config_validators.map(|s| Tracked::new(self.builder_name, s))); + self + } + + /// Adds a config validator. + pub fn push_config_validator( + &mut self, + config_validator: impl ValidateConfig + 'static, + ) -> &mut Self { + self.config_validators.push(Tracked::new( + self.builder_name, + config_validator.into_shared(), + )); + self + } + + /// Adds a config validator. + pub fn with_config_validator( + mut self, + config_validator: impl ValidateConfig + 'static, + ) -> Self { + self.push_config_validator(config_validator); + self + } + + /// Validate the base client configuration. + /// + /// This is intended to be called internally by the client. + pub fn validate_base_client_config(&self, cfg: &ConfigBag) -> Result<(), BoxError> { + macro_rules! validate { + ($field:expr) => { + for entry in &$field { + ValidateConfig::validate_base_client_config(&entry.value, self, cfg)?; + } + }; + } + + tracing::trace!(runtime_components=?self, cfg=?cfg, "validating base client config"); + for validator in self.config_validators() { + validator.validate_base_client_config(self, cfg)?; + } + validate!(self.http_client); + validate!(self.endpoint_resolver); + validate!(self.auth_schemes); + validate!(self.identity_cache); + validate!(self.identity_resolvers); + validate!(self.interceptors); + validate!(self.retry_strategy); + Ok(()) + } } #[derive(Clone, Debug)] @@ -550,7 +829,6 @@ impl RuntimeComponentsBuilder { pub fn for_tests() -> Self { use crate::client::endpoint::{EndpointFuture, EndpointResolverParams}; use crate::client::identity::IdentityFuture; - use aws_smithy_types::config_bag::ConfigBag; #[derive(Debug)] struct FakeAuthSchemeOptionResolver; @@ -654,11 +932,27 @@ impl RuntimeComponentsBuilder { } } + #[derive(Debug)] + struct FakeIdentityCache; + impl ResolveCachedIdentity for FakeIdentityCache { + fn resolve_cached_identity<'a>( + &'a self, + resolver: SharedIdentityResolver, + components: &'a RuntimeComponents, + config_bag: &'a ConfigBag, + ) -> IdentityFuture<'a> { + IdentityFuture::new(async move { + resolver.resolve_identity(components, config_bag).await + }) + } + } + Self::new("aws_smithy_runtime_api::client::runtime_components::RuntimeComponentBuilder::for_tests") .with_auth_scheme(FakeAuthScheme) .with_auth_scheme_option_resolver(Some(FakeAuthSchemeOptionResolver)) .with_endpoint_resolver(Some(FakeEndpointResolver)) .with_http_client(Some(FakeClient)) + .with_identity_cache(Some(FakeIdentityCache)) .with_identity_resolver(AuthSchemeId::new("fake"), FakeIdentityResolver) .with_retry_strategy(Some(FakeRetryStrategy)) .with_sleep_impl(Some(SharedAsyncSleep::new(FakeSleep))) @@ -699,6 +993,16 @@ impl GetIdentityResolver for RuntimeComponents { #[cfg(all(test, feature = "test-util"))] mod tests { use super::{BuildError, RuntimeComponentsBuilder, Tracked}; + use crate::client::runtime_components::ValidateConfig; + + #[derive(Clone, Debug, Eq, PartialEq)] + struct TestComponent(String); + impl ValidateConfig for TestComponent {} + impl From<&'static str> for TestComponent { + fn from(value: &'static str) -> Self { + TestComponent(value.into()) + } + } #[test] #[allow(unreachable_pub)] @@ -707,35 +1011,38 @@ mod tests { declare_runtime_components! { fields for TestRc and TestRcBuilder { #[required] - some_required_string: Option, + some_required_component: Option, - some_optional_string: Option, + some_optional_component: Option, #[atLeastOneRequired] - some_required_vec: Vec, + some_required_vec: Vec, - some_optional_vec: Vec, + some_optional_vec: Vec, } } let builder1 = TestRcBuilder { builder_name: "builder1", - some_required_string: Some(Tracked::new("builder1", "override_me".into())), - some_optional_string: Some(Tracked::new("builder1", "override_me optional".into())), + some_required_component: Some(Tracked::new("builder1", "override_me".into())), + some_optional_component: Some(Tracked::new("builder1", "override_me optional".into())), some_required_vec: vec![Tracked::new("builder1", "first".into())], some_optional_vec: vec![Tracked::new("builder1", "first optional".into())], }; let builder2 = TestRcBuilder { builder_name: "builder2", - some_required_string: Some(Tracked::new("builder2", "override_me_too".into())), - some_optional_string: Some(Tracked::new("builder2", "override_me_too optional".into())), + some_required_component: Some(Tracked::new("builder2", "override_me_too".into())), + some_optional_component: Some(Tracked::new( + "builder2", + "override_me_too optional".into(), + )), some_required_vec: vec![Tracked::new("builder2", "second".into())], some_optional_vec: vec![Tracked::new("builder2", "second optional".into())], }; let builder3 = TestRcBuilder { builder_name: "builder3", - some_required_string: Some(Tracked::new("builder3", "correct".into())), - some_optional_string: Some(Tracked::new("builder3", "correct optional".into())), + some_required_component: Some(Tracked::new("builder3", "correct".into())), + some_optional_component: Some(Tracked::new("builder3", "correct optional".into())), some_required_vec: vec![Tracked::new("builder3", "third".into())], some_optional_vec: vec![Tracked::new("builder3", "third optional".into())], }; @@ -746,26 +1053,29 @@ mod tests { .build() .expect("success"); assert_eq!( - Tracked::new("builder3", "correct".to_string()), - rc.some_required_string + Tracked::new("builder3", TestComponent::from("correct")), + rc.some_required_component ); assert_eq!( - Some(Tracked::new("builder3", "correct optional".to_string())), - rc.some_optional_string + Some(Tracked::new( + "builder3", + TestComponent::from("correct optional") + )), + rc.some_optional_component ); assert_eq!( vec![ - Tracked::new("builder1", "first".to_string()), - Tracked::new("builder2", "second".into()), - Tracked::new("builder3", "third".into()) + Tracked::new("builder1", TestComponent::from("first")), + Tracked::new("builder2", TestComponent::from("second")), + Tracked::new("builder3", TestComponent::from("third")) ], rc.some_required_vec ); assert_eq!( vec![ - Tracked::new("builder1", "first optional".to_string()), - Tracked::new("builder2", "second optional".into()), - Tracked::new("builder3", "third optional".into()) + Tracked::new("builder1", TestComponent::from("first optional")), + Tracked::new("builder2", TestComponent::from("second optional")), + Tracked::new("builder3", TestComponent::from("third optional")) ], rc.some_optional_vec ); @@ -774,19 +1084,19 @@ mod tests { #[test] #[allow(unreachable_pub)] #[allow(dead_code)] - #[should_panic(expected = "the `_some_string` runtime component is required")] + #[should_panic(expected = "the `_some_component` runtime component is required")] fn require_field_singular() { declare_runtime_components! { fields for TestRc and TestRcBuilder { #[required] - _some_string: Option, + _some_component: Option, } } let rc = TestRcBuilder::new("test").build().unwrap(); // Ensure the correct types were used - let _: Tracked = rc._some_string; + let _: Tracked = rc._some_component; } #[test] @@ -797,14 +1107,14 @@ mod tests { declare_runtime_components! { fields for TestRc and TestRcBuilder { #[atLeastOneRequired] - _some_vec: Vec, + _some_vec: Vec, } } let rc = TestRcBuilder::new("test").build().unwrap(); // Ensure the correct types were used - let _: Vec> = rc._some_vec; + let _: Vec> = rc._some_vec; } #[test] @@ -813,16 +1123,16 @@ mod tests { fn optional_fields_dont_panic() { declare_runtime_components! { fields for TestRc and TestRcBuilder { - _some_optional_string: Option, - _some_optional_vec: Vec, + _some_optional_component: Option, + _some_optional_vec: Vec, } } let rc = TestRcBuilder::new("test").build().unwrap(); // Ensure the correct types were used - let _: Option> = rc._some_optional_string; - let _: Vec> = rc._some_optional_vec; + let _: Option> = rc._some_optional_component; + let _: Vec> = rc._some_optional_vec; } #[test] diff --git a/rust-runtime/aws-smithy-runtime/src/client/auth/http.rs b/rust-runtime/aws-smithy-runtime/src/client/auth/http.rs index 3a6281373b..9aa9589ef0 100644 --- a/rust-runtime/aws-smithy-runtime/src/client/auth/http.rs +++ b/rust-runtime/aws-smithy-runtime/src/client/auth/http.rs @@ -95,8 +95,8 @@ impl Sign for ApiKeySigner { request .headers_mut() .try_append( - self.name.clone(), - format!("{} {}", self.scheme, api_key.token(),), + self.name.to_ascii_lowercase(), + format!("{} {}", self.scheme, api_key.token()), ) .map_err(|_| { "API key contains characters that can't be included in a HTTP header" diff --git a/rust-runtime/aws-smithy-runtime/src/client/defaults.rs b/rust-runtime/aws-smithy-runtime/src/client/defaults.rs index 6bba117144..cc6de23d6a 100644 --- a/rust-runtime/aws-smithy-runtime/src/client/defaults.rs +++ b/rust-runtime/aws-smithy-runtime/src/client/defaults.rs @@ -9,17 +9,21 @@ //! for _your_ client, since many things can change these defaults on the way to //! code generating and constructing a full client. +use crate::client::identity::IdentityCache; use crate::client::retries::strategy::StandardRetryStrategy; use crate::client::retries::RetryPartition; use aws_smithy_async::rt::sleep::default_async_sleep; use aws_smithy_async::time::SystemTimeSource; +use aws_smithy_runtime_api::box_error::BoxError; use aws_smithy_runtime_api::client::http::SharedHttpClient; -use aws_smithy_runtime_api::client::runtime_components::RuntimeComponentsBuilder; +use aws_smithy_runtime_api::client::runtime_components::{ + RuntimeComponentsBuilder, SharedConfigValidator, +}; use aws_smithy_runtime_api::client::runtime_plugin::{ Order, SharedRuntimePlugin, StaticRuntimePlugin, }; use aws_smithy_runtime_api::shared::IntoShared; -use aws_smithy_types::config_bag::{FrozenLayer, Layer}; +use aws_smithy_types::config_bag::{ConfigBag, FrozenLayer, Layer}; use aws_smithy_types::retry::RetryConfig; use aws_smithy_types::timeout::TimeoutConfig; use std::borrow::Cow; @@ -82,7 +86,11 @@ pub fn default_retry_config_plugin( ) -> Option { Some( default_plugin("default_retry_config_plugin", |components| { - components.with_retry_strategy(Some(StandardRetryStrategy::new())) + components + .with_retry_strategy(Some(StandardRetryStrategy::new())) + .with_config_validator(SharedConfigValidator::base_client_config_fn( + validate_retry_config, + )) }) .with_config(layer("default_retry_config", |layer| { layer.store_put(RetryConfig::disabled()); @@ -92,13 +100,108 @@ pub fn default_retry_config_plugin( ) } +fn validate_retry_config( + components: &RuntimeComponentsBuilder, + cfg: &ConfigBag, +) -> Result<(), BoxError> { + if let Some(retry_config) = cfg.load::() { + if retry_config.has_retry() && components.sleep_impl().is_none() { + Err("An async sleep implementation is required for retry to work. Please provide a `sleep_impl` on \ + the config, or disable timeouts.".into()) + } else { + Ok(()) + } + } else { + Err( + "The default retry config was removed, and no other config was put in its place." + .into(), + ) + } +} + /// Runtime plugin that sets the default timeout config (no timeouts). pub fn default_timeout_config_plugin() -> Option { Some( - default_plugin("default_timeout_config_plugin", |c| c) - .with_config(layer("default_timeout_config", |layer| { - layer.store_put(TimeoutConfig::disabled()); - })) - .into_shared(), + default_plugin("default_timeout_config_plugin", |components| { + components.with_config_validator(SharedConfigValidator::base_client_config_fn( + validate_timeout_config, + )) + }) + .with_config(layer("default_timeout_config", |layer| { + layer.store_put(TimeoutConfig::disabled()); + })) + .into_shared(), ) } + +fn validate_timeout_config( + components: &RuntimeComponentsBuilder, + cfg: &ConfigBag, +) -> Result<(), BoxError> { + if let Some(timeout_config) = cfg.load::() { + if timeout_config.has_timeouts() && components.sleep_impl().is_none() { + Err("An async sleep implementation is required for timeouts to work. Please provide a `sleep_impl` on \ + the config, or disable timeouts.".into()) + } else { + Ok(()) + } + } else { + Err( + "The default timeout config was removed, and no other config was put in its place." + .into(), + ) + } +} + +/// Runtime plugin that registers the default identity cache implementation. +pub fn default_identity_cache_plugin() -> Option { + Some( + default_plugin("default_identity_cache_plugin", |components| { + components.with_identity_cache(Some(IdentityCache::lazy().build())) + }) + .into_shared(), + ) +} + +/// Arguments for the [`default_plugins`] method. +/// +/// This is a struct to enable adding new parameters in the future without breaking the API. +#[non_exhaustive] +#[derive(Debug, Default)] +pub struct DefaultPluginParams { + retry_partition_name: Option>, +} + +impl DefaultPluginParams { + /// Creates a new [`DefaultPluginParams`]. + pub fn new() -> Self { + Default::default() + } + + /// Sets the retry partition name. + pub fn with_retry_partition_name(mut self, name: impl Into>) -> Self { + self.retry_partition_name = Some(name.into()); + self + } +} + +/// All default plugins. +pub fn default_plugins( + params: DefaultPluginParams, +) -> impl IntoIterator { + [ + default_http_client_plugin(), + default_identity_cache_plugin(), + default_retry_config_plugin( + params + .retry_partition_name + .expect("retry_partition_name is required"), + ), + default_sleep_impl_plugin(), + default_time_source_plugin(), + default_timeout_config_plugin(), + ] + .into_iter() + .flatten() + .collect::>() +} diff --git a/rust-runtime/aws-smithy-runtime/src/client/http/test_util/replay.rs b/rust-runtime/aws-smithy-runtime/src/client/http/test_util/replay.rs index b34e0aa0ff..e911237390 100644 --- a/rust-runtime/aws-smithy-runtime/src/client/http/test_util/replay.rs +++ b/rust-runtime/aws-smithy-runtime/src/client/http/test_util/replay.rs @@ -63,9 +63,9 @@ impl ValidateRequest { fn assert_matches(&self, index: usize, ignore_headers: &[&str]) { let (actual, expected) = (&self.actual, &self.expected); assert_eq!( - actual.uri(), expected.uri(), - "Request #{index} - URI doesn't match expected value" + actual.uri(), + "request[{index}] - URI doesn't match expected value" ); for (name, value) in expected.headers() { if !ignore_headers.contains(&name) { @@ -74,8 +74,8 @@ impl ValidateRequest { .get(name) .unwrap_or_else(|| panic!("Request #{index} - Header {name:?} is missing")); assert_eq!( - actual_header, value, - "Request #{index} - Header {name:?} doesn't match expected value", + value, actual_header, + "request[{index}] - Header {name:?} doesn't match expected value", ); } } @@ -94,9 +94,9 @@ impl ValidateRequest { match (actual_str, expected_str) { (Ok(actual), Ok(expected)) => assert_ok(validate_body(actual, expected, media_type)), _ => assert_eq!( - actual.body().bytes(), expected.body().bytes(), - "Request #{index} - Body contents didn't match expected value" + actual.body().bytes(), + "request[{index}] - Body contents didn't match expected value" ), }; } diff --git a/rust-runtime/aws-smithy-runtime/src/client/identity/cache/lazy.rs b/rust-runtime/aws-smithy-runtime/src/client/identity/cache/lazy.rs index 36a5968b4c..1ed06cba18 100644 --- a/rust-runtime/aws-smithy-runtime/src/client/identity/cache/lazy.rs +++ b/rust-runtime/aws-smithy-runtime/src/client/identity/cache/lazy.rs @@ -241,19 +241,65 @@ impl LazyCache { } } +macro_rules! required_err { + ($thing:literal, $how:literal) => { + BoxError::from(concat!( + "Lazy identity caching requires ", + $thing, + " to be configured. ", + $how, + " If this isn't possible, then disable identity caching by calling ", + "the `identity_cache` method on config with `IdentityCache::no_cache()`", + )) + }; +} +macro_rules! validate_components { + ($components:ident) => { + let _ = $components.time_source().ok_or_else(|| { + required_err!( + "a time source", + "Set a time source using the `time_source` method on config." + ) + })?; + let _ = $components.sleep_impl().ok_or_else(|| { + required_err!( + "an async sleep implementation", + "Set a sleep impl using the `sleep_impl` method on config." + ) + })?; + }; +} + impl ResolveCachedIdentity for LazyCache { + fn validate_base_client_config( + &self, + runtime_components: &aws_smithy_runtime_api::client::runtime_components::RuntimeComponentsBuilder, + _cfg: &ConfigBag, + ) -> Result<(), BoxError> { + validate_components!(runtime_components); + Ok(()) + } + + fn validate_final_config( + &self, + runtime_components: &RuntimeComponents, + _cfg: &ConfigBag, + ) -> Result<(), BoxError> { + validate_components!(runtime_components); + Ok(()) + } + fn resolve_cached_identity<'a>( &'a self, resolver: SharedIdentityResolver, runtime_components: &'a RuntimeComponents, config_bag: &'a ConfigBag, ) -> IdentityFuture<'a> { - let time_source = runtime_components - .time_source() - .expect("Identity caching requires a time source to be configured. If this isn't possible, then disable identity caching."); - let sleep_impl = runtime_components - .sleep_impl() - .expect("Identity caching requires a sleep impl to be configured. If this isn't possible, then disable identity caching."); + let (time_source, sleep_impl) = ( + runtime_components.time_source().expect("validated"), + runtime_components.sleep_impl().expect("validated"), + ); + let now = time_source.now(); let timeout_future = sleep_impl.sleep(self.load_timeout); let load_timeout = self.load_timeout; @@ -263,7 +309,12 @@ impl ResolveCachedIdentity for LazyCache { IdentityFuture::new(async move { // Attempt to get cached identity, or clear the cache if they're expired if let Some(identity) = cache.yield_or_clear_if_expired(now).await { - tracing::debug!("loaded identity from cache"); + tracing::debug!( + buffer_time=?self.buffer_time, + cached_expiration=?identity.expiration(), + now=?now, + "loaded identity from cache" + ); Ok(identity) } else { // If we didn't get identity from the cache, then we need to try and load. diff --git a/rust-runtime/aws-smithy-runtime/src/client/interceptors.rs b/rust-runtime/aws-smithy-runtime/src/client/interceptors.rs index 8b2522bf95..54faa6ae0d 100644 --- a/rust-runtime/aws-smithy-runtime/src/client/interceptors.rs +++ b/rust-runtime/aws-smithy-runtime/src/client/interceptors.rs @@ -65,6 +65,11 @@ macro_rules! interceptor_impl_fn { runtime_components: &RuntimeComponents, cfg: &mut ConfigBag, ) -> Result<(), InterceptorError> { + tracing::trace!(concat!( + "running `", + stringify!($interceptor), + "` interceptors" + )); let mut result: Result<(), (&str, BoxError)> = Ok(()); let ctx = ctx.into(); for interceptor in self.into_iter() { diff --git a/rust-runtime/aws-smithy-runtime/src/client/orchestrator.rs b/rust-runtime/aws-smithy-runtime/src/client/orchestrator.rs index ab328e5356..61d3948605 100644 --- a/rust-runtime/aws-smithy-runtime/src/client/orchestrator.rs +++ b/rust-runtime/aws-smithy-runtime/src/client/orchestrator.rs @@ -182,10 +182,13 @@ fn apply_configuration( continue_on_err!([ctx] => Interceptors::new(operation_rc_builder.interceptors()).read_before_execution(true, ctx, cfg)); // The order below is important. Client interceptors must run before operation interceptors. - Ok(RuntimeComponents::builder("merged orchestrator components") + let components = RuntimeComponents::builder("merged orchestrator components") .merge_from(&client_rc_builder) .merge_from(&operation_rc_builder) - .build()?) + .build()?; + + components.validate_final_config(cfg)?; + Ok(components) } #[instrument(skip_all, level = "debug")] @@ -516,7 +519,7 @@ mod tests { impl TestOperationRuntimePlugin { fn new() -> Self { Self { - builder: RuntimeComponentsBuilder::new("TestOperationRuntimePlugin") + builder: RuntimeComponentsBuilder::for_tests() .with_retry_strategy(Some(SharedRetryStrategy::new(NeverRetryStrategy::new()))) .with_endpoint_resolver(Some(SharedEndpointResolver::new( StaticUriEndpointResolver::http_localhost(8080), diff --git a/rust-runtime/aws-smithy-runtime/src/client/orchestrator/auth.rs b/rust-runtime/aws-smithy-runtime/src/client/orchestrator/auth.rs index d5c685ead2..8c6f7000e4 100644 --- a/rust-runtime/aws-smithy-runtime/src/client/orchestrator/auth.rs +++ b/rust-runtime/aws-smithy-runtime/src/client/orchestrator/auth.rs @@ -9,7 +9,7 @@ use aws_smithy_runtime_api::client::auth::{ AuthScheme, AuthSchemeEndpointConfig, AuthSchemeId, AuthSchemeOptionResolverParams, ResolveAuthSchemeOptions, }; -use aws_smithy_runtime_api::client::identity::ResolveIdentity; +use aws_smithy_runtime_api::client::identity::ResolveCachedIdentity; use aws_smithy_runtime_api::client::interceptors::context::InterceptorContext; use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents; use aws_smithy_types::config_bag::ConfigBag; @@ -65,20 +65,22 @@ pub(super) async fn orchestrate_auth( if let Some(auth_scheme) = runtime_components.auth_scheme(scheme_id) { // Use the resolved auth scheme to resolve an identity if let Some(identity_resolver) = auth_scheme.identity_resolver(runtime_components) { + let identity_cache = runtime_components.identity_cache(); let signer = auth_scheme.signer(); trace!( auth_scheme = ?auth_scheme, + identity_cache = ?identity_cache, identity_resolver = ?identity_resolver, signer = ?signer, - "resolved auth scheme, identity resolver, and signing implementation" + "resolved auth scheme, identity cache, identity resolver, and signing implementation" ); match extract_endpoint_auth_scheme_config(endpoint, scheme_id) { Ok(auth_scheme_endpoint_config) => { trace!(auth_scheme_endpoint_config = ?auth_scheme_endpoint_config, "extracted auth scheme endpoint config"); - let identity = identity_resolver - .resolve_identity(runtime_components, cfg) + let identity = identity_cache + .resolve_cached_identity(identity_resolver, runtime_components, cfg) .await?; trace!(identity = ?identity, "resolved identity"); @@ -412,4 +414,71 @@ mod tests { .expect("gimme the string, dammit!") ); } + + #[cfg(feature = "http-auth")] + #[tokio::test] + async fn use_identity_cache() { + use crate::client::auth::http::{ApiKeyAuthScheme, ApiKeyLocation}; + use aws_smithy_runtime_api::client::auth::http::HTTP_API_KEY_AUTH_SCHEME_ID; + use aws_smithy_runtime_api::client::identity::http::Token; + use aws_smithy_types::body::SdkBody; + + let mut ctx = InterceptorContext::new(Input::doesnt_matter()); + ctx.enter_serialization_phase(); + ctx.set_request( + http::Request::builder() + .body(SdkBody::empty()) + .unwrap() + .try_into() + .unwrap(), + ); + let _ = ctx.take_input(); + ctx.enter_before_transmit_phase(); + + #[derive(Debug)] + struct Cache; + impl ResolveCachedIdentity for Cache { + fn resolve_cached_identity<'a>( + &'a self, + _resolver: SharedIdentityResolver, + _: &'a RuntimeComponents, + _config_bag: &'a ConfigBag, + ) -> IdentityFuture<'a> { + IdentityFuture::ready(Ok(Identity::new(Token::new("cached (pass)", None), None))) + } + } + + let runtime_components = RuntimeComponentsBuilder::for_tests() + .with_auth_scheme(SharedAuthScheme::new(ApiKeyAuthScheme::new( + "result:", + ApiKeyLocation::Header, + "Authorization", + ))) + .with_auth_scheme_option_resolver(Some(SharedAuthSchemeOptionResolver::new( + StaticAuthSchemeOptionResolver::new(vec![HTTP_API_KEY_AUTH_SCHEME_ID]), + ))) + .with_identity_cache(Some(Cache)) + .with_identity_resolver( + HTTP_API_KEY_AUTH_SCHEME_ID, + SharedIdentityResolver::new(Token::new("uncached (fail)", None)), + ) + .build() + .unwrap(); + let mut layer = Layer::new("test"); + layer.store_put(Endpoint::builder().url("dontcare").build()); + layer.store_put(AuthSchemeOptionResolverParams::new("doesntmatter")); + let config_bag = ConfigBag::of_layers(vec![layer]); + + orchestrate_auth(&mut ctx, &runtime_components, &config_bag) + .await + .expect("success"); + assert_eq!( + "result: cached (pass)", + ctx.request() + .expect("request is set") + .headers() + .get("Authorization") + .unwrap() + ); + } } diff --git a/rust-runtime/aws-smithy-runtime/src/client/orchestrator/operation.rs b/rust-runtime/aws-smithy-runtime/src/client/orchestrator/operation.rs index 569b5b0b13..752de03110 100644 --- a/rust-runtime/aws-smithy-runtime/src/client/orchestrator/operation.rs +++ b/rust-runtime/aws-smithy-runtime/src/client/orchestrator/operation.rs @@ -4,12 +4,10 @@ */ use crate::client::auth::no_auth::{NoAuthScheme, NO_AUTH_SCHEME_ID}; -use crate::client::defaults::{ - default_http_client_plugin, default_retry_config_plugin, default_sleep_impl_plugin, - default_time_source_plugin, default_timeout_config_plugin, -}; +use crate::client::defaults::{default_plugins, DefaultPluginParams}; use crate::client::http::connection_poisoning::ConnectionPoisoningInterceptor; use crate::client::identity::no_auth::NoAuthIdentityResolver; +use crate::client::identity::IdentityCache; use crate::client::orchestrator::endpoints::StaticUriEndpointResolver; use crate::client::retries::strategy::{NeverRetryStrategy, StandardRetryStrategy}; use aws_smithy_async::rt::sleep::AsyncSleep; @@ -243,6 +241,8 @@ impl OperationBuilder { ))); self.runtime_components .push_auth_scheme(SharedAuthScheme::new(NoAuthScheme::default())); + self.runtime_components + .set_identity_cache(Some(IdentityCache::no_cache())); self.runtime_components.push_identity_resolver( NO_AUTH_SCHEME_ID, SharedIdentityResolver::new(NoAuthIdentityResolver::new()), @@ -325,18 +325,10 @@ impl OperationBuilder { let service_name = self.service_name.expect("service_name required"); let operation_name = self.operation_name.expect("operation_name required"); - let defaults = [ - default_http_client_plugin(), - default_retry_config_plugin(service_name.clone()), - default_sleep_impl_plugin(), - default_time_source_plugin(), - default_timeout_config_plugin(), - ] - .into_iter() - .flatten(); - let mut runtime_plugins = RuntimePlugins::new() - .with_client_plugins(defaults) + .with_client_plugins(default_plugins( + DefaultPluginParams::new().with_retry_partition_name(service_name.clone()), + )) .with_client_plugin( StaticRuntimePlugin::new() .with_config(self.config.freeze())