Share HTTP connectors between providers and clients (#2876)

## Motivation and Context
Clients using separate connectors is mostly confusing and troublesome
for customers.

## Description
Change the behavior of `ConfigLoader::http_connector` to set both the
client & credential provider HTTP connector.

**Note**: It is still possible to separate control clients for the
credential provider. Because `HttpConnector` is used, the timeout
settings can still be late-bound.

## Testing
Unit tests.

## Checklist
<!--- If a checkbox below is not applicable, then please DELETE it
rather than leaving it unchecked -->
- [x] I have updated `CHANGELOG.next.toml` if I made changes to the
smithy-rs codegen or runtime crates
- [x] I have updated `CHANGELOG.next.toml` if I made changes to the AWS
SDK, generated SDK code, or SDK runtime crates

----

_By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice._
This commit is contained in:
Russell Cohen 2023-07-27 12:15:43 -04:00 committed by GitHub
parent 1fbafc8366
commit 79be310546
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 165 additions and 74 deletions

View File

@ -665,6 +665,12 @@ references = ["smithy-rs#2865"]
meta = { "breaking" = true, "tada" = false, "bug" = false, "target" = "server" }
author = "david-perez"
[[aws-sdk-rust]]
message = "**Behavior change**: Credential providers now share the HTTP connector used by the SDK. If you want to keep a separate connector for clients, use `<service>::ConfigBuilder::http_connector` when constructing the client."
references = ["aws-sdk-rust#579", "aws-sdk-rust#338"]
meta = { "breaking" = true, "tada" = false, "bug" = false }
author = "rcoh"
[[smithy-rs]]
message = """
[RestJson1](https://awslabs.github.io/smithy/2.0/aws/protocols/aws-restjson1-protocol.html#operation-error-serialization) server SDKs now serialize only the [shape name](https://smithy.io/2.0/spec/model.html#shape-id) in operation error responses. Previously (from versions 0.52.0 to 0.55.4), the full shape ID was rendered.

View File

@ -118,12 +118,9 @@ mod tests {
async fn profile_name_override() {
let fs = Fs::from_slice(&[("test_config", "[profile custom]\nsdk_ua_app_id = correct")]);
let conf = crate::from_env()
.configure(
ProviderConfig::empty()
.with_fs(fs)
.with_sleep(InstantSleep)
.with_http_connector(no_traffic_connector()),
)
.sleep_impl(InstantSleep)
.fs(fs)
.http_connector(no_traffic_connector())
.profile_name("custom")
.profile_files(
ProfileFiles::builder()

View File

@ -161,6 +161,7 @@ mod loader {
use aws_smithy_types::timeout::TimeoutConfig;
use aws_types::app_name::AppName;
use aws_types::docs_for;
use aws_types::os_shim_internal::{Env, Fs};
use aws_types::SdkConfig;
use crate::connector::default_connector;
@ -205,6 +206,8 @@ mod loader {
use_fips: Option<bool>,
use_dual_stack: Option<bool>,
time_source: Option<SharedTimeSource>,
env: Option<Env>,
fs: Option<Fs>,
}
impl ConfigLoader {
@ -281,35 +284,43 @@ mod loader {
self
}
/// Override the [`HttpConnector`] for this [`ConfigLoader`]. The connector will be used when
/// sending operations. This **does not set** the HTTP connector used by config providers.
/// To change that connector, use [ConfigLoader::configure].
/// Override the [`HttpConnector`] for this [`ConfigLoader`]. The connector will be used for
/// both AWS services and credential providers. When [`HttpConnector::ConnectorFn`] is used,
/// the connector will be lazily instantiated as needed based on the provided settings.
///
/// **Note**: In order to take advantage of late-configured timeout settings, you MUST use
/// [`HttpConnector::ConnectorFn`]
/// when configuring this connector.
///
/// If you wish to use a separate connector when creating clients, use the client-specific config.
/// ## Examples
/// ```no_run
/// # #[cfg(feature = "client-hyper")]
/// # use aws_smithy_async::rt::sleep::SharedAsyncSleep;
/// use aws_smithy_client::http_connector::HttpConnector;
/// #[cfg(feature = "client-hyper")]
/// # async fn create_config() {
/// use std::time::Duration;
/// use aws_smithy_client::{Client, hyper_ext};
/// use aws_smithy_client::erase::DynConnector;
/// use aws_smithy_client::http_connector::ConnectorSettings;
///
/// let https_connector = hyper_rustls::HttpsConnectorBuilder::new()
/// .with_webpki_roots()
/// .https_only()
/// .enable_http1()
/// .enable_http2()
/// .build();
/// let smithy_connector = hyper_ext::Adapter::builder()
/// // Optionally set things like timeouts as well
/// .connector_settings(
/// ConnectorSettings::builder()
/// .connect_timeout(Duration::from_secs(5))
/// .build()
/// )
/// .build(https_connector);
/// let connector_fn = |settings: &ConnectorSettings, sleep: Option<SharedAsyncSleep>| {
/// let https_connector = hyper_rustls::HttpsConnectorBuilder::new()
/// .with_webpki_roots()
/// // NOTE: setting `https_only()` will not allow this connector to work with IMDS.
/// .https_only()
/// .enable_http1()
/// .enable_http2()
/// .build();
/// let mut smithy_connector = hyper_ext::Adapter::builder()
/// // Optionally set things like timeouts as well
/// .connector_settings(settings.clone());
/// smithy_connector.set_sleep_impl(sleep);
/// Some(DynConnector::new(smithy_connector.build(https_connector)))
/// };
/// let connector = HttpConnector::ConnectorFn(std::sync::Arc::new(connector_fn));
/// let sdk_config = aws_config::from_env()
/// .http_connector(smithy_connector)
/// .http_connector(connector)
/// .load()
/// .await;
/// # }
@ -532,6 +543,9 @@ mod loader {
/// let shared_config = aws_config::from_env().configure(provider_config).load().await;
/// # }
/// ```
#[deprecated(
note = "Use setters on this builder instead. configure is very hard to use correctly."
)]
pub fn configure(mut self, provider_config: ProviderConfig) -> Self {
self.provider_config = Some(provider_config);
self
@ -547,9 +561,35 @@ mod loader {
/// This means that if you provide a region provider that does not return a region, no region will
/// be set in the resulting [`SdkConfig`](aws_types::SdkConfig)
pub async fn load(self) -> SdkConfig {
let http_connector = self
.http_connector
.unwrap_or_else(|| HttpConnector::ConnectorFn(Arc::new(default_connector)));
let time_source = self.time_source.unwrap_or_default();
let sleep_impl = if self.sleep.is_some() {
self.sleep
} else {
if default_async_sleep().is_none() {
tracing::warn!(
"An implementation of AsyncSleep was requested by calling default_async_sleep \
but no default was set.
This happened when ConfigLoader::load was called during Config construction. \
You can fix this by setting a sleep_impl on the ConfigLoader before calling \
load or by enabling the rt-tokio feature"
);
}
default_async_sleep()
};
let conf = self
.provider_config
.unwrap_or_default()
.unwrap_or_else(|| {
ProviderConfig::init(time_source.clone(), sleep_impl.clone())
.with_fs(self.fs.unwrap_or_default())
.with_env(self.env.unwrap_or_default())
.with_http_connector(http_connector.clone())
})
.with_profile_config(self.profile_files_override, self.profile_name_override);
let region = if let Some(provider) = self.region {
provider.region().await
@ -579,21 +619,6 @@ mod loader {
.await
};
let sleep_impl = if self.sleep.is_some() {
self.sleep
} else {
if default_async_sleep().is_none() {
tracing::warn!(
"An implementation of AsyncSleep was requested by calling default_async_sleep \
but no default was set.
This happened when ConfigLoader::load was called during Config construction. \
You can fix this by setting a sleep_impl on the ConfigLoader before calling \
load or by enabling the rt-tokio feature"
);
}
default_async_sleep()
};
let timeout_config = if let Some(timeout_config) = self.timeout_config {
timeout_config
} else {
@ -603,10 +628,6 @@ mod loader {
.await
};
let http_connector = self
.http_connector
.unwrap_or_else(|| HttpConnector::ConnectorFn(Arc::new(default_connector)));
let credentials_provider = match self.credentials_provider {
CredentialsProviderOption::Set(provider) => Some(provider),
CredentialsProviderOption::NotSet => {
@ -641,13 +662,11 @@ mod loader {
use_dual_stack_provider(&conf).await
};
let ts = self.time_source.unwrap_or_default();
let mut builder = SdkConfig::builder()
.region(region)
.retry_config(retry_config)
.timeout_config(timeout_config)
.time_source(ts)
.time_source(time_source)
.http_connector(http_connector);
builder.set_app_name(app_name);
@ -661,18 +680,35 @@ mod loader {
}
}
#[cfg(test)]
impl ConfigLoader {
pub(crate) fn env(mut self, env: Env) -> Self {
self.env = Some(env);
self
}
pub(crate) fn fs(mut self, fs: Fs) -> Self {
self.fs = Some(fs);
self
}
}
#[cfg(test)]
mod test {
use aws_credential_types::provider::ProvideCredentials;
use aws_smithy_async::rt::sleep::TokioSleep;
use aws_smithy_async::time::{StaticTimeSource, TimeSource};
use aws_smithy_client::erase::DynConnector;
use aws_smithy_client::never::NeverConnector;
use aws_smithy_client::test_connection::infallible_connection_fn;
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;
use crate::profile::profile_file::{ProfileFileKind, ProfileFiles};
use crate::provider_config::ProviderConfig;
use crate::test_case::{no_traffic_connector, InstantSleep};
use crate::{from_env, ConfigLoader};
@ -688,13 +724,10 @@ mod loader {
let fs =
Fs::from_slice(&[("test_config", "[profile custom]\nsdk-ua-app-id = correct")]);
let loader = from_env()
.configure(
ProviderConfig::empty()
.with_sleep(TokioSleep::new())
.with_env(env)
.with_fs(fs)
.with_http_connector(DynConnector::new(NeverConnector::new())),
)
.sleep_impl(TokioSleep::new())
.env(env)
.fs(fs)
.http_connector(DynConnector::new(NeverConnector::new()))
.profile_name("custom")
.profile_files(
ProfileFiles::builder()
@ -734,11 +767,9 @@ mod loader {
}
fn base_conf() -> ConfigLoader {
from_env().configure(
ProviderConfig::empty()
.with_sleep(InstantSleep)
.with_http_connector(no_traffic_connector()),
)
from_env()
.sleep_impl(InstantSleep)
.http_connector(no_traffic_connector())
}
#[tokio::test]
@ -770,5 +801,54 @@ mod loader {
assert!(config.credentials_cache().is_none());
assert!(config.credentials_provider().is_none());
}
#[tokio::test]
async fn connector_is_shared() {
let num_requests = Arc::new(AtomicUsize::new(0));
let movable = num_requests.clone();
let conn = infallible_connection_fn(move |_req| {
movable.fetch_add(1, Ordering::Relaxed);
http::Response::new("ok!")
});
let config = from_env().http_connector(conn.clone()).load().await;
config
.credentials_provider()
.unwrap()
.provide_credentials()
.await
.expect_err("no traffic is allowed");
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_connector(no_traffic_connector())
.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()
);
}
}
}
}

View File

@ -150,6 +150,21 @@ impl ProviderConfig {
}
}
/// Initializer for ConfigBag to avoid possibly setting incorrect defaults.
pub(crate) fn init(time_source: SharedTimeSource, sleep: Option<SharedAsyncSleep>) -> Self {
Self {
parsed_profile: Default::default(),
profile_files: ProfileFiles::default(),
env: Env::default(),
fs: Fs::default(),
time_source,
connector: HttpConnector::Prebuilt(None),
sleep,
region: None,
profile_name_override: None,
}
}
/// Create a default provider config with the region region automatically loaded from the default chain.
///
/// # Examples
@ -270,10 +285,7 @@ impl ProviderConfig {
self.with_region(provider_chain.region().await)
}
// these setters are doc(hidden) because they only exist for tests
#[doc(hidden)]
pub fn with_fs(self, fs: Fs) -> Self {
pub(crate) fn with_fs(self, fs: Fs) -> Self {
ProviderConfig {
parsed_profile: Default::default(),
fs,
@ -281,8 +293,7 @@ impl ProviderConfig {
}
}
#[cfg(test)]
pub fn with_env(self, env: Env) -> Self {
pub(crate) fn with_env(self, env: Env) -> Self {
ProviderConfig {
parsed_profile: Default::default(),
env,
@ -303,14 +314,11 @@ impl ProviderConfig {
/// Override the HTTPS connector for this configuration
///
/// **Warning**: Use of this method will prevent you from taking advantage of the HTTP connect timeouts.
/// Consider [`ProviderConfig::with_tcp_connector`].
///
/// # Stability
/// This method is expected to change to support HTTP configuration.
pub fn with_http_connector(self, connector: DynConnector) -> Self {
/// **Note**: In order to take advantage of late-configured timeout settings, use [`HttpConnector::ConnectorFn`]
/// when configuring this connector.
pub fn with_http_connector(self, connector: impl Into<HttpConnector>) -> Self {
ProviderConfig {
connector: HttpConnector::Prebuilt(Some(connector)),
connector: connector.into(),
..self
}
}